hunyi 0.6.1

渾儀 (Hunyi) — Tianheng's semantic (AST/syn) observation dimension, the complement of the static import boundary. Declare in Rust how a module's public surface must behave: what its API must not expose (types — including named public re-exports and, opt-in, a trait impl's impl-site positions — and no dyn / impl Trait or async fn seam), where a trait may be implemented, that it declares no bare pub, and which markers a type must not acquire — observed via syn, reacted in CI. The heavy syn dependency is quarantined here, never in the core.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
use super::super::*;
use super::helpers::*;
// --- dyn-trait-boundary ---------------------------------------------------

/// Like [`findings`] but for the dyn-trait capability: write `files`, return the rendered
/// `dyn` shapes exposed by `module`. Shape-only, so it takes no forbidden set.
pub(super) fn dyn_findings(
    name: &str,
    files: &[(&str, &str)],
    module: &str,
) -> Result<Vec<String>, String> {
    shape_findings("dyn", name, files, module, dyn_module_findings)
}

pub(super) fn dyn_mod(name: &str, body: &str) -> Result<Vec<String>, String> {
    dyn_findings(
        name,
        &[("lib.rs", "pub mod m;\n"), ("m.rs", body)],
        "crate::m",
    )
}

/// Like [`dyn_findings`] but for the operand-scoped rule: write `files`, return the rendered
/// `dyn` shapes whose principal trait resolves into `forbidden`.
pub(super) fn dyn_operand_findings(
    name: &str,
    files: &[(&str, &str)],
    module: &str,
    forbidden: &[&str],
    deps: &[&str],
) -> Result<Vec<String>, String> {
    operand_findings(
        "dyn",
        name,
        files,
        module,
        forbidden,
        deps,
        dyn_operand_module_findings,
    )
}

pub(super) fn dyn_operand_mod(
    name: &str,
    body: &str,
    forbidden: &[&str],
) -> Result<Vec<String>, String> {
    dyn_operand_findings(
        name,
        &[("lib.rs", "pub mod m;\n"), ("m.rs", body)],
        "crate::m",
        forbidden,
        &[],
    )
}

#[test]
pub(super) fn a_dyn_in_a_supertrait_or_assoc_type_bound_is_observed() {
    // A `dyn` inside a supertrait's generic argument, or inside a
    // public associated type's `: Bound`, is a real exposed trait-object in the trait's public
    // contract. The sibling signature-coupling collector already walks these bound positions
    // (paths_in_bounds); the dyn collector now matches it — previously it skipped supertraits and
    // associated-type bounds entirely, silently dropping the dyn (a false negative → exit 0).
    assert!(
        dyn_mod(
            "supertrait-dyn",
            "pub trait Facade: AsRef<Box<dyn crate::ports::Port>> {}\n",
        )
        .unwrap()
        .contains(&"dyn crate::ports::Port exposed by trait crate::m::Facade".to_string()),
        "a dyn in a supertrait generic argument must be observed",
    );
    assert!(
        dyn_mod(
            "assoc-bound-dyn",
            "pub trait F { type Bar: AsRef<Box<dyn crate::ports::Port>>; }\n",
        )
        .unwrap()
        .contains(&"dyn crate::ports::Port exposed by type trait crate::m::F::Bar".to_string()),
        "a dyn in an associated-type bound must be observed",
    );
}

#[test]
pub(super) fn a_dyn_in_an_inherent_impl_generic_bound_is_observed() {
    // A `dyn` in an inherent impl's own generic-param bound is exposed on the inherent
    // API; the dyn collector's inherent-impl arm now walks the impl generics (parity with the path
    // collector's own generics walk and with the struct/enum/trait arms).
    let out = dyn_mod(
        "dyn-impl-generics",
        "pub struct Foo<T>(T);\nimpl<T: AsRef<Box<dyn crate::ports::Port>>> Foo<T> { pub fn m(&self) {} }\n",
    )
    .unwrap();
    // The seam now names the bounded parameter (`generics: T`), not a bare `(generics)`: two impl
    // blocks bounding different parameters to the same forbidden type are two distinct violations,
    // and rendering them identically made a report unreadable even where identity was correct.
    assert!(
        out.iter()
            .any(|f| f.contains("dyn crate::ports::Port") && f.contains("(generics: T)")),
        "a dyn in an inherent-impl generic bound must be observed and name its bound: {out:?}"
    );
}

/// A non-public `type` alias hiding a `dyn` in a public position is not observed — the stated bound, with
/// its control beside it so the empty result is discriminating rather than vacuous.
///
/// Kept for the CONTRACT rather than for a change: `semantic-dyn-trait-boundary` declares this bound (the
/// resolver does not expand `type` aliases) and nothing pinned it. Measured before it was written, so the
/// assertion records behaviour rather than restating the spec: the aliased form yields no finding and the
/// direct form yields one.
#[test]
pub(super) fn a_private_alias_hiding_a_dyn_is_a_stated_bound() {
    let hidden = dyn_mod(
        "alias-hidden-dyn",
        "type Handler = Box<dyn crate::ports::Port>;\npub fn make() -> Handler { todo!() }\n",
    )
    .unwrap();
    assert!(
        hidden.is_empty(),
        "a dyn behind a non-public type alias is a stated bound: {hidden:?}"
    );

    let direct = dyn_mod(
        "alias-hidden-dyn-control",
        "pub fn make() -> Box<dyn crate::ports::Port> { todo!() }\n",
    )
    .unwrap();
    assert_eq!(
        direct,
        ["dyn crate::ports::Port exposed by fn crate::m::make"],
        "the control must react, or the empty result above says nothing"
    );
}

#[test]
pub(super) fn dyn_operand_flags_a_named_trait_and_passes_others() {
    // A dyn of the listed trait is flagged; a dyn of an unlisted trait passes.
    assert_eq!(
        dyn_operand_mod(
            "named",
            "pub fn c() -> Box<dyn crate::ports::Port> { todo!() }\n",
            &["crate::ports::Port"],
        )
        .unwrap(),
        ["dyn crate::ports::Port exposed by fn crate::m::c"],
    );
    assert!(
        dyn_operand_mod(
            "other",
            "pub fn e() -> Box<dyn std::error::Error> { todo!() }\n",
            &["crate::ports::Port"],
        )
        .unwrap()
        .is_empty(),
        "a dyn of an unlisted trait passes",
    );
}

#[test]
pub(super) fn dyn_operand_honors_a_module_prefix() {
    // A module-prefix operand forbids any dyn of a trait under it (exact-or-`::` prefix).
    assert_eq!(
        dyn_operand_mod(
            "prefix",
            "pub fn c() -> Box<dyn crate::ports::Port> { todo!() }\n",
            &["crate::ports"],
        )
        .unwrap(),
        ["dyn crate::ports::Port exposed by fn crate::m::c"],
    );
}

#[test]
pub(super) fn dyn_operand_matches_a_reexported_trait_by_its_defining_path() {
    // The trait is defined at crate::ports::Port and re-exported as crate::Port; the module
    // exposes `dyn crate::Port`. Forbidding either path matches — both canonicalize through
    // the re-export closure to the defining path.
    let files = &[
        (
            "lib.rs",
            "pub mod ports;\npub use crate::ports::Port;\npub mod m;\n",
        ),
        ("ports.rs", "pub trait Port {}\n"),
        ("m.rs", "pub fn c() -> Box<dyn crate::Port> { todo!() }\n"),
    ];
    // Forbid by the DEFINING path — the exposed facade `crate::Port` canonicalizes to it.
    assert_eq!(
        dyn_operand_findings(
            "reexport-defining",
            files,
            "crate::m",
            &["crate::ports::Port"],
            &[],
        )
        .unwrap(),
        ["dyn crate::Port exposed by fn crate::m::c"],
        "a dyn written through a re-export facade matches the forbidden defining path",
    );
}

#[test]
pub(super) fn a_cfg_sibling_child_module_does_not_shadow_a_different_branchs_own_extern_principal()
{
    // extern_resolution must not compute externs_type/renames_bare over the flattened union
    // of mutually-exclusive #[cfg] branches. The "u" branch (platform.rs) declares a LOCAL
    // `mod traits { .. }`; the mutually-exclusive "w" branch (win_platform.rs) has no local
    // `mod traits` and its `dyn traits::Marker` genuinely names the extern crate `traits`.
    // The "u" branch's local `mod traits` must not suppress the "w" branch's extern match.
    let files = &[
        (
            "lib.rs",
            "#[cfg(feature = \"u\")] pub mod platform;\n\
             #[cfg(feature = \"w\")] #[path = \"win_platform.rs\"] pub mod platform;\n",
        ),
        (
            "platform.rs",
            "pub mod traits { pub trait Marker {} }\npub fn open() -> u8 { 0 }\n",
        ),
        (
            "win_platform.rs",
            "pub fn f() -> Box<dyn traits::Marker> { todo!() }\n",
        ),
    ];
    assert_eq!(
        dyn_operand_findings(
            "cfg-sibling-childmod-shadow",
            files,
            "crate::platform",
            &["traits::Marker"],
            &["traits"],
        )
        .unwrap(),
        ["dyn traits::Marker exposed by fn crate::platform::f"],
        "the w branch's own genuine extern dyn-principal must react, regardless of the u \
         branch's own local mod traits",
    );
}

#[test]
pub(super) fn a_cfg_split_module_with_two_inline_siblings_child_module_does_not_shadow_the_others_own_extern_principal()
 {
    // The operand-scoped (`shape_scan.rs`/`crate_scope.rs`) analogue of
    // `a_cfg_split_module_with_two_inline_siblings_child_module_does_not_shadow_the_others_extern_reexport`
    // above: `operand_module_findings` groups its per-branch `FileExternScope` (and `uses_by_branch`)
    // by branch index too, not just by file — two INLINE `#[cfg]` siblings share the identical
    // enclosing lib.rs, so a file-keyed group would let the "u" arm's local `mod traits` suppress
    // the "w" arm's genuine extern `dyn traits::Marker`, the identical conflation the file-form
    // version above exercises, but with both arms declared inline in one shared file.
    let files = &[(
        "lib.rs",
        "#[cfg(feature = \"u\")] pub mod platform {\n\
         pub mod traits { pub trait Marker {} }\n\
         pub fn open() -> u8 { 0 }\n}\n\
         #[cfg(feature = \"w\")] pub mod platform {\n\
         pub fn f() -> Box<dyn traits::Marker> { todo!() }\n}\n",
    )];
    assert_eq!(
        dyn_operand_findings(
            "cfg-split-inline-inline-childmod-shadow",
            files,
            "crate::platform",
            &["traits::Marker"],
            &["traits"],
        )
        .unwrap(),
        ["dyn traits::Marker exposed by fn crate::platform::f"],
        "the w arm's own genuine extern dyn-principal must react, regardless of the u arm's own \
         local mod traits, even though both arms are inline and share lib.rs",
    );
}

#[test]
pub(super) fn dyn_operand_ignores_auto_trait_markers() {
    // `dyn Port + Send`: the sole non-auto trait is Port. Forbidding Port flags it; forbidding
    // only the Send marker flags nothing (Send is an auto trait, never an operand, and a bare Send
    // does not resolve).
    assert_eq!(
        dyn_operand_mod(
            "marker-port",
            "pub fn c() -> Box<dyn crate::ports::Port + Send> { todo!() }\n",
            &["crate::ports::Port"],
        )
        .unwrap(),
        ["dyn crate::ports::Port + Send exposed by fn crate::m::c"],
    );
    assert!(
        dyn_operand_mod(
            "marker-send",
            "pub fn c() -> Box<dyn crate::ports::Port + Send> { todo!() }\n",
            &["Send"],
        )
        .unwrap()
        .is_empty(),
        "the trailing Send marker is not the operand",
    );
}

#[test]
pub(super) fn dyn_operand_matches_when_an_auto_trait_is_written_before_the_principal() {
    // `dyn Send + crate::ports::Port` — the auto trait is written FIRST. Rust allows this (only
    // lifetimes are order-constrained), so the principal is not "the first trait bound"; skipping
    // auto traits, Port is the operand and forbidding it must flag the exposure. Taking the first
    // trait bound (Send) would silently pass a forbidden operand — a false negative.
    assert_eq!(
        dyn_operand_mod(
            "auto-first",
            "pub fn c() -> Box<dyn Send + crate::ports::Port> { todo!() }\n",
            &["crate::ports::Port"],
        )
        .unwrap(),
        ["dyn Send + crate::ports::Port exposed by fn crate::m::c"],
    );
    // Two auto traits before the principal is still resolved.
    assert_eq!(
        dyn_operand_mod(
            "auto-first-2",
            "pub fn c() -> Box<dyn Send + Sync + crate::ports::Port> { todo!() }\n",
            &["crate::ports::Port"],
        )
        .unwrap(),
        ["dyn Send + Sync + crate::ports::Port exposed by fn crate::m::c"],
    );
}

#[test]
pub(super) fn dyn_operand_matches_a_dyn_nested_deep() {
    // Nested inside Vec<Box<dyn …>> — still matched by its principal trait.
    assert_eq!(
        dyn_operand_mod(
            "nested",
            "pub fn c() -> Vec<Box<dyn crate::ports::Port>> { todo!() }\n",
            &["crate::ports::Port"],
        )
        .unwrap(),
        ["dyn crate::ports::Port exposed by fn crate::m::c"],
    );
}

#[test]
pub(super) fn dyn_operand_empty_set_degenerates_to_any() {
    // An empty forbidden set reacts to any dyn — identical to shape-only, never a no-op.
    let body = "pub fn c() -> Box<dyn crate::ports::Port> { todo!() }\n";
    assert_eq!(
        dyn_operand_mod("empty", body, &[]).unwrap(),
        dyn_mod("empty-shape", body).unwrap(),
        "must_not_expose_dyn_of([]) matches exactly what shape-only must_not_expose_dyn does",
    );
    assert_eq!(
        dyn_operand_mod("empty2", body, &[]).unwrap(),
        ["dyn crate::ports::Port exposed by fn crate::m::c"],
    );
}

#[test]
pub(super) fn dyn_operand_boundary_carries_its_operands_and_severity() {
    let b = DynTraitBoundary::in_crate("core")
        .module("crate::core")
        .must_not_expose_dyn_of(["crate::ports::Port"])
        .warn()
        .because("the core seam must not leak a dyn Port");
    assert_eq!(b.forbidden_operands(), ["crate::ports::Port"]);
    assert_eq!(b.severity(), Severity::Warn);
    // Shape-only still constructs an empty operand set (regression guard).
    let shape = DynTraitBoundary::in_crate("core")
        .module("crate::core")
        .must_not_expose_dyn()
        .because("no dyn at all");
    assert!(shape.forbidden_operands().is_empty());
}

/// A macro-generated `dyn` is not observed — the universal 渾儀 macro-expansion bound, with its control.
///
/// Kept for the CONTRACT rather than for a change: `semantic-dyn-trait-boundary` declares this and nothing
/// pinned it. Measured, and the control is what makes the empty result mean something — the same signature
/// written directly reacts, so the emptiness is about the macro body rather than about a fixture that
/// reacts to nothing.
#[test]
pub(super) fn a_macro_generated_dyn_is_a_documented_coverage_bound() {
    let hidden = dyn_mod(
        "macro-dyn",
        "macro_rules! mk { () => { pub fn c() -> Box<dyn crate::ports::Port> { todo!() } }; }\nmk!();\n",
    )
    .unwrap();
    assert!(
        hidden.is_empty(),
        "a dyn produced by a macro expansion is a documented bound: {hidden:?}"
    );

    let direct = dyn_mod(
        "macro-dyn-control",
        "pub fn c() -> Box<dyn crate::ports::Port> { todo!() }\n",
    )
    .unwrap();
    assert_eq!(
        direct,
        ["dyn crate::ports::Port exposed by fn crate::m::c"],
        "the control must react, or the empty result above says nothing"
    );
}

#[test]
pub(super) fn an_unrenderable_sub_node_is_a_stated_rendering_bound() {
    let out1 = dyn_mod(
        "macro-subnode-dyn-1",
        "pub fn f() -> Box<dyn crate::ports::Port<m!(1)>> { todo!() }\n",
    )
    .unwrap();
    let out2 = dyn_mod(
        "macro-subnode-dyn-2",
        "pub fn f() -> Box<dyn crate::ports::Port<m!(2)>> { todo!() }\n",
    )
    .unwrap();
    assert_eq!(out1, out2);
}