rustqual 1.2.4

Comprehensive Rust code quality analyzer — seven dimensions: IOSP, Complexity, DRY, SRP, Coupling, Test Quality, Architecture
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
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
//! Canonical call-target collection with receiver-type tracking.
//!
//! Turns a `syn::Block` into a `HashSet<String>` of canonical call
//! targets. Handles:
//! - `crate::` / `self::` / `super::` prefixed calls (resolved via
//!   `forbidden_rule::resolve_to_crate_absolute`).
//! - `Self::method(...)` in impl blocks (via `self_type` context).
//! - Alias-resolved unqualified calls (via `gather_alias_map`).
//! - Macro descent (`assert!(foo(x))` records `foo`).
//! - Receiver-type-tracked method calls: `let s = RlmSession::open();
//!   s.search(x);` → `crate::…::RlmSession::search` (not `<method>:search`).
//!
//! Binding-extraction helpers live in [`super::bindings`]; this file
//! owns the visitor, the scope stack, and the target canonicalisation.
//!
//! See `D-3` and `D-4` in the v1.1.0 plan for the resolution order and
//! the binding scan patterns.

use super::bindings::{
    canonical_from_type, extract_let_binding, normalize_alias_expansion, CanonScope,
};
use super::local_symbols::{scope_for_local, FileScope};
use super::type_infer::resolve::{resolve_type, ResolveContext};
use super::type_infer::self_subst::substitute_bare_self;
use super::type_infer::{
    extract_bindings, extract_for_bindings, infer_type, BindingLookup, CanonicalType, InferContext,
    WorkspaceTypeIndex,
};
use crate::adapters::analyzers::architecture::forbidden_rule::{
    file_to_module_segments, resolve_to_crate_absolute_in,
};
use crate::adapters::shared::use_tree::AliasTarget;
use std::collections::{HashMap, HashSet};
use syn::visit::Visit;

/// Canonical marker for method calls whose receiver-type we can't resolve.
/// Any `<method>:<name>` string is layer-unknown by construction and
/// never counts as a delegation target.
const METHOD_UNKNOWN_PREFIX: &str = "<method>:";
/// Canonical marker for unqualified / unresolved call paths. All `<bare>:…`
/// strings are layer-unknown (external, stdlib, or not aliased).
const BARE_UNKNOWN_PREFIX: &str = "<bare>:";

/// Input for the canonical-call collector. Per-file lookup tables live
/// in `file`; the rest is per-fn.
pub struct FnContext<'a> {
    pub file: &'a FileScope<'a>,
    /// Mod-path of the fn declaration inside `file.path`. Empty for
    /// top-level fns.
    pub mod_stack: &'a [String],
    /// Body of the function we analyse.
    pub body: &'a syn::Block,
    /// Named signature parameters with their declared types.
    pub signature_params: Vec<(String, &'a syn::Type)>,
    /// Canonical generic-param map: `name → ParamInfo` (canonicalised
    /// bounds + turbofish substitution position). Callers MUST build
    /// this via `signature_params::item_canonical_generics` or
    /// `method_canonical_generics`; constructing it ad-hoc bypasses
    /// the canonicaliser AND the position-tagging and leaves
    /// `Q::method()` dispatch / turbofish substitution broken.
    pub generic_params: HashMap<String, super::signature_params::ParamInfo>,
    /// Type-path of the enclosing `impl` block, if any.
    pub self_type: Option<Vec<String>>,
    /// Workspace type-index for shallow inference fallback. `None` for
    /// unit-test fixtures.
    pub workspace_index: Option<&'a WorkspaceTypeIndex>,
    /// All workspace `FileScope`s. Lets alias expansion switch into
    /// the alias's declaring scope. `None` for unit-test fixtures.
    pub workspace_files: Option<&'a HashMap<String, FileScope<'a>>>,
}

// qual:api
/// Collect the canonical call-target set from a fn body. Entry point for
/// Check A / Check B call-graph construction.
pub fn collect_canonical_calls(ctx: &FnContext<'_>) -> HashSet<String> {
    let mut collector = CanonicalCallCollector::new(ctx);
    collector.seed_signature_bindings();
    collector.visit_block(ctx.body);
    collector.calls
}

// qual:allow(srp) — LCOM4 here counts visitor methods (touch `calls`)
// separately from scope helpers (touch `bindings`). They're the two
// halves of a single walk; splitting them further fragments the
// visit-order invariants the walker depends on.
struct CanonicalCallCollector<'a> {
    file: &'a FileScope<'a>,
    /// Mod-path inside `file.path` of the fn under analysis. Read-only
    /// for the duration of the body walk.
    mod_stack: &'a [String],
    /// Full canonical path of the enclosing impl's self-type (with
    /// `crate` prefix), if any — used to resolve `Self::method`.
    self_type_canonical: Option<Vec<String>>,
    signature_params: Vec<(String, &'a syn::Type)>,
    /// Generic type-param name → canonicalised trait-bound paths.
    /// Empty bounds keep the param-name reservation so an unbound
    /// `Q::method(...)` doesn't fall through to `crate_root_modules`.
    generic_params: HashMap<String, super::signature_params::ParamInfo>,
    /// Scope stack of variable-name → canonical-type-path bindings.
    /// Always non-empty while a collection is in flight.
    bindings: Vec<HashMap<String, Vec<String>>>,
    /// Parallel scope stack for non-Path bindings (`Result<…>`,
    /// `dyn Trait`, etc.). Pushed/popped in lockstep with `bindings`.
    non_path_bindings: Vec<HashMap<String, CanonicalType>>,
    calls: HashSet<String>,
    /// Workspace type-index for shallow inference fallback. `None`
    /// for unit-test fixtures.
    workspace_index: Option<&'a WorkspaceTypeIndex>,
    /// Workspace `FileScope` map for alias decl-site resolution.
    workspace_files: Option<&'a HashMap<String, FileScope<'a>>>,
}

impl<'a> CanonicalCallCollector<'a> {
    fn new(ctx: &'a FnContext<'a>) -> Self {
        let self_type_canonical = ctx.self_type.as_ref().map(|segs| {
            // Qualified impl path (`impl crate::foo::Bar { ... }`) — use
            // as-is so Self::method canonicalises to `crate::foo::Bar::method`.
            if segs.first().map(|s| s.as_str()) == Some("crate") {
                return segs.clone();
            }
            let mut full = vec!["crate".to_string()];
            full.extend(file_to_module_segments(ctx.file.path));
            full.extend(ctx.mod_stack.iter().cloned());
            full.extend_from_slice(segs);
            full
        });
        Self {
            file: ctx.file,
            mod_stack: ctx.mod_stack,
            self_type_canonical,
            signature_params: ctx.signature_params.clone(),
            generic_params: ctx.generic_params.clone(),
            bindings: vec![HashMap::new()],
            non_path_bindings: vec![HashMap::new()],
            calls: HashSet::new(),
            workspace_index: ctx.workspace_index,
            workspace_files: ctx.workspace_files,
        }
    }

    fn seed_signature_bindings(&mut self) {
        // `self` is `FnArg::Receiver` and never appears in
        // `signature_params`. Seed it explicitly so `self.helper()` and
        // `self.field.method()` route through `method_returns` /
        // `struct_fields` instead of collapsing to `<method>:…`.
        if let Some(self_canonical) = self.self_type_canonical.clone() {
            self.bindings[0].insert("self".to_string(), self_canonical);
        }
        let params = self.signature_params.clone();
        for (name, ty) in &params {
            // When workspace_index is available, use the full resolver:
            // it handles Stage-3 type-alias expansion, Stage-2 dyn Trait,
            // stdlib wrappers, and plain Path in one pass.
            if self.workspace_index.is_some() {
                self.seed_param_via_resolver(name, ty);
                continue;
            }
            // Legacy fast-path for unit-test fixtures without an index.
            if let Some(canonical) = canonical_from_type(
                ty,
                self.file.alias_map,
                self.file.local_symbols,
                self.file.crate_root_modules,
                self.file.path,
            ) {
                self.bindings[0].insert(name.clone(), canonical);
            }
        }
    }

    /// Install a signature-param binding using the full `resolve_type`
    /// pipeline. Path → legacy scope, wrappers / trait bounds →
    /// `non_path_bindings`, `Opaque` dropped. Always seeds frame 0
    /// because signature params live for the whole body walk.
    fn seed_param_via_resolver(&mut self, name: &str, ty: &syn::Type) {
        match self.resolve_param_type(ty) {
            CanonicalType::Path(segs) => {
                self.bindings[0].insert(name.to_string(), segs);
            }
            CanonicalType::Opaque => {}
            other => {
                self.non_path_bindings[0].insert(name.to_string(), other);
            }
        }
    }

    /// Resolve a parameter / closure-arg type through the full
    /// scope-aware pipeline (alias expansion, transparent wrappers,
    /// trait-bound extraction, inline-mod resolution). Pre-substitutes
    /// bare `Self` with `self_type_canonical` so impl-body declarations
    /// like `fn merge(&self, other: Self)` and typed closure params
    /// resolve to the enclosing impl type. Used by both signature
    /// seeding and closure-param seeding.
    fn resolve_param_type(&self, ty: &syn::Type) -> CanonicalType {
        let rctx = ResolveContext {
            file: self.file,
            mod_stack: self.mod_stack,
            type_aliases: self.workspace_index.map(|w| &w.type_aliases),
            transparent_wrappers: self.workspace_index.map(|w| &w.transparent_wrappers),
            workspace_files: self.workspace_files,
            alias_param_subs: None,
            generic_params: Some(&self.generic_params),
        };
        match self.self_type_canonical.as_deref() {
            Some(impl_segs) => resolve_type(&substitute_bare_self(ty, impl_segs), &rctx),
            None => resolve_type(ty, &rctx),
        }
    }

    fn enter_scope(&mut self) {
        self.bindings.push(HashMap::new());
        self.non_path_bindings.push(HashMap::new());
    }

    fn exit_scope(&mut self) {
        self.bindings.pop();
        self.non_path_bindings.pop();
    }

    /// Return the innermost binding scope. The stack is seeded non-empty
    /// in `new()` and only mutated via paired `enter_scope` / `exit_scope`
    /// calls, so `last_mut()` is always `Some`; fall back to index access
    /// to avoid panic-helper methods in production code.
    fn current_scope_mut(&mut self) -> &mut HashMap<String, Vec<String>> {
        if self.bindings.is_empty() {
            self.bindings.push(HashMap::new());
        }
        let last = self.bindings.len() - 1;
        &mut self.bindings[last]
    }

    /// Parallel accessor for the non-path scope stack. Same invariants
    /// and fallback semantics as `current_scope_mut`.
    fn current_non_path_scope_mut(&mut self) -> &mut HashMap<String, CanonicalType> {
        if self.non_path_bindings.is_empty() {
            self.non_path_bindings.push(HashMap::new());
        }
        let last = self.non_path_bindings.len() - 1;
        &mut self.non_path_bindings[last]
    }

    /// Install a binding in the path-scope and evict any stale entry
    /// for the same name in the non-path scope (a `let` that shadows a
    /// previous wrapper-typed binding with a plain Path binding).
    /// Operation.
    fn install_path_binding(&mut self, name: String, segs: Vec<String>) {
        self.current_non_path_scope_mut().remove(&name);
        self.current_scope_mut().insert(name, segs);
    }

    /// Install a wrapper / trait-bound binding in the non-path scope
    /// and evict any stale Path binding for the same name (shadowing
    /// the other way). Operation.
    fn install_non_path_binding(&mut self, name: String, ty: CanonicalType) {
        self.current_scope_mut().remove(&name);
        self.current_non_path_scope_mut().insert(name, ty);
    }

    /// Install closure parameter bindings. For `|x: T|` the type goes
    /// through the same scope-aware pipeline as signature params. For
    /// untyped or destructured patterns, every bound ident gets an
    /// `Opaque` tombstone — without this, an outer same-name binding
    /// could leak into the closure body and synthesize a stale edge.
    fn install_closure_param(&mut self, pat: &syn::Pat) {
        if let syn::Pat::Type(pt) = pat {
            if let Some(name) = extract_pat_ident_name(pt.pat.as_ref()) {
                match self.resolve_param_type(&pt.ty) {
                    CanonicalType::Path(segs) => self.install_path_binding(name, segs),
                    other => self.install_non_path_binding(name, other),
                }
                return;
            }
        }
        let mut idents = Vec::new();
        collect_pattern_idents(pat, &mut idents);
        for name in idents {
            self.install_non_path_binding(name, CanonicalType::Opaque);
        }
    }

    /// Resolve `Q::method(...)` where `Q` is a generic type param with
    /// trait bound(s) to the trait-method anchor canonicals. Returns
    /// `None` when the first segment isn't a known generic param, or
    /// when the path is an explicit absolute path (`::Q::method(...)`
    /// is the caller's disambiguation away from in-scope generics —
    /// gated centrally via `matched_generic_param`), or when the
    /// param has bounds we couldn't canonicalise. Multiple bounds ⇒
    /// multiple anchors (over-approximation; matches what
    /// `populate_anchor_index` mints). Empty result for a generic
    /// without resolvable bounds — the caller short-circuits the call
    /// rather than falling into local-symbol / crate-root lookup,
    /// which would mis-route `Q` as a type. Operation.
    fn canonicalise_generic_param_path(
        &self,
        segments: &[String],
        leading_colon_set: bool,
    ) -> Option<Vec<String>> {
        if segments.len() < 2 {
            return None;
        }
        let info = super::signature_params::matched_generic_param(
            segments,
            leading_colon_set,
            &self.generic_params,
        )?;
        let method_tail = &segments[1..];
        let canonicals: Vec<String> = info
            .bounds
            .iter()
            .map(|bound| {
                let mut full = bound.clone();
                full.extend_from_slice(method_tail);
                full.join("::")
            })
            .collect();
        Some(canonicals)
    }

    /// Turn a path-segment list into the canonical String used for all
    /// call-target comparisons in the call-parity check.
    /// `leading_colon_set` reflects `syn::Path.leading_colon` —
    /// `::Foo::bar()` is Rust 2018+ extern-root syntax that explicitly
    /// disambiguates AWAY from workspace symbols, so it short-circuits
    /// straight to `<bare>:` without consulting the alias / local /
    /// crate-root resolvers. Mirrors the same gate
    /// `canonicalise_workspace_path` enforces for `Option`-returning
    /// type canonicalisation. Integration: each branch delegates to a
    /// dedicated helper.
    fn canonicalise_path(&self, segments: &[String], leading_colon_set: bool) -> String {
        if segments.is_empty() {
            return String::new();
        }
        // Extern-root path (`::Foo::bar`) — workspace canonicalisation
        // does not apply. Without this gate, a same-named workspace
        // symbol would produce a false `crate::...::Foo::bar` edge.
        if leading_colon_set {
            return bare(&segments.join("::"));
        }
        if segments[0] == "Self" {
            return self.canonicalise_self_path(segments);
        }
        if matches!(segments[0].as_str(), "crate" | "self" | "super") {
            return self.canonicalise_keyword_path(segments);
        }
        if let Some(canonical) = self.canonicalise_alias_path(segments) {
            return canonical;
        }
        if let Some(canonical) = self.canonicalise_local_symbol_path(segments) {
            return canonical;
        }
        // Rust 2018+ absolute call: `app::foo()` without `use` is the
        // crate-root `app` module, equivalent to `crate::app::foo()`.
        // If `app` is a known workspace root module, prepend `crate::`
        // so the canonical matches graph nodes.
        if self.file.crate_root_modules.contains(&segments[0]) {
            let mut full = vec!["crate".to_string()];
            full.extend_from_slice(segments);
            return full.join("::");
        }
        // Unknown path (external crate, stdlib, or not imported) → bare.
        bare(&segments.join("::"))
    }

    /// `Self::method` — substitute the enclosing impl's canonical
    /// self-type for `Self`. Falls back to `<bare>:` when we're not
    /// inside an impl. Operation.
    fn canonicalise_self_path(&self, segments: &[String]) -> String {
        if let Some(self_canonical) = &self.self_type_canonical {
            let mut full = self_canonical.clone();
            full.extend_from_slice(&segments[1..]);
            return full.join("::");
        }
        bare(&segments.join("::"))
    }

    fn canonicalise_keyword_path(&self, segments: &[String]) -> String {
        if let Some(resolved) =
            resolve_to_crate_absolute_in(self.file.path, self.mod_stack, segments)
        {
            let mut full = vec!["crate".to_string()];
            full.extend(resolved);
            return full.join("::");
        }
        bare(&segments.join("::"))
    }

    /// First segment hits a `use` alias visible at the current
    /// `mod_stack`. The alias path is then re-normalised (it may
    /// itself reference `self::`/`super::` or a Rust-2018 crate-root
    /// module). Returns `None` when no alias matches.
    fn canonicalise_alias_path(&self, segments: &[String]) -> Option<String> {
        let alias = self.lookup_alias_at_scope(&segments[0])?;
        let mut full = alias.segments.to_vec();
        full.extend_from_slice(&segments[1..]);
        let scope = CanonScope {
            file: self.file,
            mod_stack: self.mod_stack,
        };
        let normalized = normalize_alias_expansion(full, alias.absolute_root, &scope)?;
        Some(normalized.join("::"))
    }

    /// Look up `name` in the alias map for exactly the current
    /// `mod_stack`. Falls back to the flat top-level `alias_map` for
    /// legacy callers that don't populate `aliases_per_scope`.
    fn lookup_alias_at_scope(&self, name: &str) -> Option<&AliasTarget> {
        if let Some(map) = self.file.aliases_per_scope.get(self.mod_stack) {
            return map.get(name);
        }
        self.file.alias_map.get(name)
    }

    /// Same-file fallback: first segment is declared in this file at
    /// exactly the current `mod_stack`. Returns `None` when the name
    /// isn't in `local_symbols` or its declaration is in a different
    /// scope, letting the caller fall through to crate-root resolution.
    fn canonicalise_local_symbol_path(&self, segments: &[String]) -> Option<String> {
        if !self.file.local_symbols.contains(&segments[0]) {
            return None;
        }
        let mod_path = scope_for_local(self.file.local_decl_scopes, &segments[0], self.mod_stack)?;
        let mut full = vec!["crate".to_string()];
        full.extend(file_to_module_segments(self.file.path));
        full.extend(mod_path.iter().cloned());
        full.extend_from_slice(segments);
        Some(full.join("::"))
    }

    fn record_call(&mut self, target: String) {
        self.calls.insert(target);
    }

    /// Resolve a method call's receiver to the canonical call-graph
    /// targets. Fast-path returns a single element; trait-dispatch
    /// inference returns the synthetic anchor `<Trait>::<method>` so
    /// `dyn Trait` calls collapse to one boundary regardless of how
    /// many impls exist. Empty vec means unresolved — caller records
    /// `<method>:name`. Integration: fast-path first, inference
    /// fallback second.
    fn resolve_method_targets(&self, receiver: &syn::Expr, method_name: &str) -> Vec<String> {
        if let Some(c) = self.try_fast_path_receiver(receiver, method_name) {
            return vec![c];
        }
        self.try_inferred_targets(receiver, method_name)
    }

    /// Fast-path: receiver is a bare ident with a concrete binding in
    /// the legacy path scope. Walks both scope stacks from innermost to
    /// outermost so a non-path shadow (`let r: Result<_,_> = …`
    /// shadowing an outer `let r: Session = …`) aborts the fast-path
    /// and hands off to inference, instead of producing a stale concrete
    /// edge. Operation.
    fn try_fast_path_receiver(&self, receiver: &syn::Expr, method_name: &str) -> Option<String> {
        let syn::Expr::Path(p) = receiver else {
            return None;
        };
        if p.path.segments.len() != 1 {
            return None;
        }
        let ident = p.path.segments[0].ident.to_string();
        for (path_scope, non_path_scope) in self
            .bindings
            .iter()
            .rev()
            .zip(self.non_path_bindings.iter().rev())
        {
            if non_path_scope.contains_key(&ident) {
                return None;
            }
            if let Some(binding) = path_scope.get(&ident) {
                let mut full = binding.clone();
                full.push(method_name.to_string());
                return Some(full.join("::"));
            }
        }
        None
    }

    /// Inference fallback: run shallow type inference over the receiver
    /// expression, then project the result into one or more canonical
    /// call-graph targets. Returns `Vec::new()` when the workspace index
    /// isn't present, inference fails, or the inferred type isn't
    /// resolvable to a concrete edge. Operation.
    fn try_inferred_targets(&self, receiver: &syn::Expr, method_name: &str) -> Vec<String> {
        let Some(workspace) = self.workspace_index else {
            return Vec::new();
        };
        let Some(inferred) = self.infer_receiver_type(receiver) else {
            return Vec::new();
        };
        canonical_edges_for_method(&inferred, method_name, workspace)
    }

    /// Run `infer_type` over `receiver` with the current collector
    /// state. Returns the raw `CanonicalType` so `try_inferred_targets`
    /// can project it to 0/1/N edges. Operation: adapter build +
    /// delegate.
    fn infer_receiver_type(&self, expr: &syn::Expr) -> Option<CanonicalType> {
        let adapter = CollectorBindings {
            scope: &self.bindings,
            non_path_scope: &self.non_path_bindings,
        };
        let ctx = InferContext {
            file: self.file,
            mod_stack: self.mod_stack,
            workspace: self.workspace_index?,
            bindings: &adapter,
            self_type: self.self_type_canonical.clone(),
            workspace_files: self.workspace_files,
            generic_params: Some(&self.generic_params),
        };
        infer_type(expr, &ctx)
    }

    /// `let x: T = …` — route the annotation through the full resolver
    /// when a workspace index is available so alias expansion + wrapper
    /// peeling + trait-bound extraction all apply. Returns `true` when
    /// a binding was installed. Returns `false` only when there's no
    /// workspace index, the pattern isn't a typed ident, or the
    /// annotation is the explicit `_` inference placeholder — the caller
    /// then falls through to initializer-based inference (which is what
    /// rustc does). An annotation that names an unresolvable type still
    /// installs an `Opaque` tombstone so outer path bindings with the
    /// same name don't leak back in. Operation.
    fn try_install_annotated_binding(&mut self, local: &syn::Local) -> bool {
        let Some(wi) = self.workspace_index else {
            return false;
        };
        let syn::Pat::Type(pt) = &local.pat else {
            return false;
        };
        let syn::Pat::Ident(pi) = pt.pat.as_ref() else {
            return false;
        };
        if matches!(pt.ty.as_ref(), syn::Type::Infer(_)) {
            return false;
        }
        let rctx = ResolveContext {
            file: self.file,
            mod_stack: self.mod_stack,
            type_aliases: Some(&wi.type_aliases),
            transparent_wrappers: Some(&wi.transparent_wrappers),
            workspace_files: self.workspace_files,
            alias_param_subs: None,
            generic_params: Some(&self.generic_params),
        };
        let name = pi.ident.to_string();
        let resolved = match self.self_type_canonical.as_deref() {
            Some(impl_segs) => {
                resolve_type(&substitute_bare_self(pt.ty.as_ref(), impl_segs), &rctx)
            }
            None => resolve_type(pt.ty.as_ref(), &rctx),
        };
        match resolved {
            CanonicalType::Path(segs) => self.install_path_binding(name, segs),
            other => self.install_non_path_binding(name, other),
        }
        true
    }

    /// Install a `let x = expr` binding via shallow inference on the
    /// initializer. `Path` results go into the legacy scope, non-Path
    /// results (wrappers, trait bounds) into `non_path_bindings`, and
    /// unresolvable initializers (`let s = external()` where we can't
    /// name the return type) into `non_path_bindings` as an `Opaque`
    /// tombstone so an outer `s: Session` doesn't leak back in when
    /// `s.method()` is resolved. Only simple `Pat::Ident` patterns are
    /// handled here; destructuring flows through `install_destructure_bindings`.
    /// Operation.
    fn install_inferred_let_binding(&mut self, local: &syn::Local) {
        let Some(name) = extract_pat_ident_name(&local.pat) else {
            return;
        };
        let inferred = local
            .init
            .as_ref()
            .and_then(|init| self.infer_receiver_type(&init.expr))
            .unwrap_or(CanonicalType::Opaque);
        match inferred {
            CanonicalType::Path(segs) => self.install_path_binding(name, segs),
            other => self.install_non_path_binding(name, other),
        }
    }

    fn collect_macro_body(&mut self, mac: &syn::Macro) {
        for expr in parse_macro_tokens(mac.tokens.clone()) {
            self.visit_expr(&expr);
        }
    }

    /// Extract pattern bindings from `pat` against a matched-type from
    /// `matched_expr`, installing them into the current scope. Path
    /// bindings go into the legacy scope, wrapper/trait-bound bindings
    /// into `non_path_bindings`. If the matched expression is itself
    /// unresolvable, every syntactic binding in the pattern gets an
    /// `Opaque` tombstone so outer same-name bindings can't leak back
    /// in at a later `.method()` call. Used by `let`-destructuring,
    /// `if let`, `while let`, `match` arms. Integration.
    fn install_destructure_bindings(&mut self, pat: &syn::Pat, matched_expr: &syn::Expr) {
        let matched = self
            .infer_receiver_type(matched_expr)
            .unwrap_or(CanonicalType::Opaque);
        let pairs = self.extract_pattern_pairs(pat, &matched, PatKind::Value);
        self.install_binding_pairs_with_tombstones(pat, pairs);
    }

    /// Extract for-loop element-type bindings from `pat` against
    /// `iter_expr` (the thing being iterated over). Unresolvable
    /// iterators tombstone their pattern idents, same as
    /// `install_destructure_bindings`. Integration.
    fn install_for_bindings(&mut self, pat: &syn::Pat, iter_expr: &syn::Expr) {
        let iter_type = self
            .infer_receiver_type(iter_expr)
            .unwrap_or(CanonicalType::Opaque);
        let pairs = self.extract_pattern_pairs(pat, &iter_type, PatKind::Iterator);
        self.install_binding_pairs_with_tombstones(pat, pairs);
    }

    /// Wrapper around `patterns::extract_bindings` / `extract_for_bindings`
    /// that builds a fresh `InferContext`. Operation.
    fn extract_pattern_pairs(
        &self,
        pat: &syn::Pat,
        matched: &CanonicalType,
        kind: PatKind,
    ) -> Vec<(String, CanonicalType)> {
        let Some(workspace) = self.workspace_index else {
            return Vec::new();
        };
        let adapter = CollectorBindings {
            scope: &self.bindings,
            non_path_scope: &self.non_path_bindings,
        };
        let ictx = InferContext {
            file: self.file,
            mod_stack: self.mod_stack,
            workspace,
            bindings: &adapter,
            self_type: self.self_type_canonical.clone(),
            workspace_files: self.workspace_files,
            generic_params: Some(&self.generic_params),
        };
        match kind {
            PatKind::Value => extract_bindings(pat, matched, &ictx),
            PatKind::Iterator => extract_for_bindings(pat, matched, &ictx),
        }
    }

    /// Dispatch each `(name, type)` pair into the right scope map, then
    /// walk `pat` and install `Opaque` tombstones for every syntactic
    /// ident the resolver didn't reach. This keeps an unresolvable
    /// `let (_, s) = external()` or `for s in opaque_iter` from letting
    /// an outer `s: Session` leak back in at `s.method()` time.
    /// Operation.
    fn install_binding_pairs_with_tombstones(
        &mut self,
        pat: &syn::Pat,
        pairs: Vec<(String, CanonicalType)>,
    ) {
        let mut resolved: HashSet<String> = HashSet::new();
        for (name, ty) in pairs {
            resolved.insert(name.clone());
            match ty {
                CanonicalType::Path(segs) => self.install_path_binding(name, segs),
                other => self.install_non_path_binding(name, other),
            }
        }
        let mut idents = Vec::new();
        collect_pattern_idents(pat, &mut idents);
        for name in idents {
            if !resolved.contains(&name) {
                self.install_non_path_binding(name, CanonicalType::Opaque);
            }
        }
    }
}

/// Whether `extract_pattern_pairs` should use value-pattern
/// (`let` / `if let` / `match`) or for-loop element-type extraction.
enum PatKind {
    Value,
    Iterator,
}

/// Best-effort extraction of expressions from a macro token stream.
/// Most macros accept comma-separated exprs (`assert!(a, b)`,
/// `format!("{}", x)`), but block-like bodies (`tokio::select! { ... }`)
/// and separator-`;` variants (`vec![x; n]`) don't. We try three
/// strategies in order:
/// 1. Comma-separated `syn::Expr` list (covers ~90% of macro calls).
/// 2. Brace-wrapped parse as a `syn::Block` — extracts every statement
///    expression, covering block-bodied and `;`-separated forms.
/// 3. Single `syn::Expr` — for macros whose argument is one expression.
///
/// Still silent-skips on total parse failure (extern-DSL macros, custom
/// grammar) — a documented limitation of syntax-level call-graph
/// construction.
fn parse_macro_tokens(tokens: proc_macro2::TokenStream) -> Vec<syn::Expr> {
    use syn::parse::Parser;
    use syn::punctuated::Punctuated;
    use syn::Token;
    let parser = Punctuated::<syn::Expr, Token![,]>::parse_terminated;
    if let Ok(exprs) = parser.parse2(tokens.clone()) {
        return exprs.into_iter().collect();
    }
    let braced = quote::quote! { { #tokens } };
    if let Ok(block) = syn::parse2::<syn::Block>(braced) {
        return block
            .stmts
            .into_iter()
            .filter_map(|stmt| match stmt {
                syn::Stmt::Expr(e, _) => Some(e),
                syn::Stmt::Local(l) => l.init.map(|init| *init.expr),
                _ => None,
            })
            .collect();
    }
    if let Ok(expr) = syn::parse2::<syn::Expr>(tokens) {
        return vec![expr];
    }
    Vec::new()
}

/// Project an inferred receiver type to the canonical call-graph
/// edge(s) for a method call. `Path` yields one concrete edge.
/// `TraitBound` (Stage 2) yields one synthetic anchor edge
/// `<Trait>::<method>` provided the method is declared on the trait —
/// the touchpoint walker decides target-boundary status via
/// `is_anchor_target_capability` (target-declared callable body OR
/// overriding impl in target), so call-parity stays sound for
/// Ports&Adapters architectures without fanning out N per-impl edges
/// (which would otherwise turn one boundary call into N
/// false-positive Check C touchpoints). Wrapper variants
/// (`Result`/`Option`/…) yield no direct edge — the combinator table
/// already unwrapped them in the method-return lookup.
/// Operation: variant dispatch.
fn canonical_edges_for_method(
    ty: &CanonicalType,
    method: &str,
    workspace: &WorkspaceTypeIndex,
) -> Vec<String> {
    // Both TraitBound (impl/dyn Trait) and GenericParamBound
    // (`fn f<Q: T>() -> Q`) dispatch identically through the trait
    // anchor — only their turbofish-overridability differs. Use
    // `as_trait_bounds()` so both variants route together.
    if let Some(bounds) = ty.as_trait_bounds() {
        return bounds
            .iter()
            .flat_map(|trait_segs| trait_dispatch_edges(trait_segs, method, workspace))
            .collect();
    }
    match ty {
        CanonicalType::Path(segs) => {
            let mut full = segs.clone();
            full.push(method.to_string());
            vec![full.join("::")]
        }
        _ => Vec::new(),
    }
}

/// Emit a single synthetic trait-method anchor `<Trait>::<method>` for
/// `dyn Trait.method()` dispatch. The anchor represents the logical
/// capability; concrete impls are NOT fanned out as separate edges
/// here — fanout would build N-element touchpoint sets that fire
/// Check C false-positives for a single boundary call. The anchor is
/// intentionally treated as a leaf in the call graph (no
/// anchor → impl edges are added) — calls inside default bodies and
/// overriding impl bodies are out of scope; documented as a known
/// limitation in `book/adapter-parity.md`. Filters on
/// `trait_has_method` so `dyn Trait.unrelated_method()` still falls
/// through to `<method>:name`. Operation: index lookup.
fn trait_dispatch_edges(
    trait_segs: &[String],
    method: &str,
    workspace: &WorkspaceTypeIndex,
) -> Vec<String> {
    let trait_canonical = trait_segs.join("::");
    if !workspace.trait_has_method(&trait_canonical, method) {
        return Vec::new();
    }
    vec![format!("{trait_canonical}::{method}")]
}

/// Adapter that exposes the collector's `Vec<HashMap<String, Vec<String>>>`
/// scope stack as a `BindingLookup` for the inference engine. Bindings
/// in the old scope are always concrete type paths, so we wrap each as
/// `CanonicalType::Path(segs)`. Stdlib-wrapper bindings (`Option<T>`,
/// `Result<T,_>`) are never stored in the old scope — they're either
/// unwrapped via `?` before `let` binds them, or simply not populated
/// by the legacy `extract_let_binding`.
struct CollectorBindings<'a> {
    scope: &'a [HashMap<String, Vec<String>>],
    non_path_scope: &'a [HashMap<String, CanonicalType>],
}

impl BindingLookup for CollectorBindings<'_> {
    fn lookup(&self, ident: &str) -> Option<CanonicalType> {
        // Walk both stacks in lockstep from innermost to outermost so
        // shadowing works across kinds (a wrapper-typed `let` hides an
        // outer path-typed `let` with the same name and vice versa).
        // Install helpers evict the sibling entry at the same level, so
        // at most one map hits per frame.
        for (path_frame, non_path_frame) in self
            .scope
            .iter()
            .rev()
            .zip(self.non_path_scope.iter().rev())
        {
            if let Some(ty) = non_path_frame.get(ident) {
                return Some(ty.clone());
            }
            if let Some(segs) = path_frame.get(ident) {
                return Some(CanonicalType::Path(segs.clone()));
            }
        }
        None
    }
}

/// Peel `Pat::Type` wrappers to reach a `Pat::Ident` and return its
/// identifier. Returns `None` for destructuring / tuple / struct
/// patterns — those flow through `patterns::extract_bindings`.
/// Operation: recursive pattern peel.
// qual:recursive
fn extract_pat_ident_name(pat: &syn::Pat) -> Option<String> {
    match pat {
        syn::Pat::Ident(pi) => Some(pi.ident.to_string()),
        syn::Pat::Type(pt) => extract_pat_ident_name(&pt.pat),
        _ => None,
    }
}

/// Collect every binding ident introduced by `pat` (ignoring subpatterns
/// that don't bind names — `_`, literals, ref subslices without idents).
/// Used to install `Opaque` tombstones for syntactic bindings whose
/// matched type couldn't be inferred. Integration: dispatch over pat
/// variants, each arm delegates to a recursive helper.
// qual:recursive
fn collect_pattern_idents(pat: &syn::Pat, out: &mut Vec<String>) {
    match pat {
        syn::Pat::Ident(pi) => push_pat_ident(pi, out),
        syn::Pat::Type(pt) => collect_pattern_idents(&pt.pat, out),
        syn::Pat::Reference(r) => collect_pattern_idents(&r.pat, out),
        syn::Pat::Paren(p) => collect_pattern_idents(&p.pat, out),
        syn::Pat::Tuple(t) => walk_each(t.elems.iter(), out),
        syn::Pat::TupleStruct(ts) => walk_each(ts.elems.iter(), out),
        syn::Pat::Struct(s) => walk_each(s.fields.iter().map(|f| f.pat.as_ref()), out),
        syn::Pat::Slice(s) => walk_each(s.elems.iter(), out),
        syn::Pat::Or(o) => walk_each(o.cases.iter().take(1), out),
        _ => {}
    }
}

/// Recurse into every pattern in `iter`. Operation: closure-free fn
/// keeps lifetime inference simple when called from the main walker.
fn walk_each<'p, I: Iterator<Item = &'p syn::Pat>>(iter: I, out: &mut Vec<String>) {
    for p in iter {
        collect_pattern_idents(p, out);
    }
}

/// Push a `Pat::Ident`'s name and recurse into its optional subpattern
/// (`x @ Some(inner)`). Operation: closure-hidden recursion.
fn push_pat_ident(pi: &syn::PatIdent, out: &mut Vec<String>) {
    out.push(pi.ident.to_string());
    if let Some((_, sub)) = &pi.subpat {
        collect_pattern_idents(sub, out);
    }
}

/// Prefix an unresolved single-ident or segment path with the layer-unknown
/// `<bare>:` marker. Centralised so the BP-010 format-repetition detector
/// sees exactly one format string, and so the marker can evolve together.
fn bare(path: &str) -> String {
    format!("{BARE_UNKNOWN_PREFIX}{path}")
}

/// Prefix a method identifier with the layer-unknown `<method>:` marker.
fn method_unknown(method: &str) -> String {
    format!("{METHOD_UNKNOWN_PREFIX}{method}")
}

// The Visit impl uses an independent `'ast` lifetime so the same
// collector can walk both the main fn body (long-lived) and macro
// bodies we parse on-the-fly (locally-owned, short-lived). The struct's
// `'a` carries state references (alias_map etc.); it never constrains
// the AST lifetime.
impl<'a, 'ast> Visit<'ast> for CanonicalCallCollector<'a> {
    fn visit_block(&mut self, block: &'ast syn::Block) {
        self.enter_scope();
        syn::visit::visit_block(self, block);
        self.exit_scope();
    }

    fn visit_local(&mut self, local: &'ast syn::Local) {
        // Walk the initializer first so calls in the RHS are recorded
        // before the binding is installed. Rust shadowing semantics
        // reference the outer binding in the RHS.
        if let Some(init) = &local.init {
            self.visit_expr(&init.expr);
            if let Some((_, else_expr)) = &init.diverge {
                self.visit_expr(else_expr);
            }
        }
        if self.try_install_annotated_binding(local) {
            return;
        }
        // The legacy `extract_let_binding` shortcut isn't mod-scope aware
        // — it would install `let s = inner::Session::new()` as
        // `crate::file::Session` while the index keys it under
        // `crate::file::inner::Session`. Skip it when a workspace index
        // is available so inference (which is scope-aware) takes over.
        if self.workspace_index.is_none() {
            if let Some((name, ty_canonical)) = extract_let_binding(
                local,
                self.file.alias_map,
                self.file.local_symbols,
                self.file.crate_root_modules,
                self.file.path,
            ) {
                self.install_path_binding(name, ty_canonical);
                return;
            }
        }
        if extract_pat_ident_name(&local.pat).is_some() {
            self.install_inferred_let_binding(local);
            return;
        }
        // Destructuring: `let Some(x) = opt`, `let Ctx { field } = …`,
        // `let (a, b) = …`, `let Pat = expr else { return; }`. Install
        // all pattern-extracted bindings into the current scope.
        if let Some(init) = local.init.as_ref() {
            self.install_destructure_bindings(&local.pat, &init.expr);
        }
    }

    fn visit_expr_if(&mut self, expr_if: &'ast syn::ExprIf) {
        self.enter_scope();
        // `if let PAT = SCRUTINEE { THEN }` — extract bindings visible
        // in the then-block only. Non-let conditions are visited via
        // the default walker and don't introduce bindings.
        if let syn::Expr::Let(let_expr) = expr_if.cond.as_ref() {
            self.visit_expr(&let_expr.expr);
            self.install_destructure_bindings(&let_expr.pat, &let_expr.expr);
        } else {
            self.visit_expr(&expr_if.cond);
        }
        self.visit_block(&expr_if.then_branch);
        self.exit_scope();
        if let Some((_, else_branch)) = &expr_if.else_branch {
            self.visit_expr(else_branch);
        }
    }

    fn visit_expr_while(&mut self, expr_while: &'ast syn::ExprWhile) {
        self.enter_scope();
        if let syn::Expr::Let(let_expr) = expr_while.cond.as_ref() {
            self.visit_expr(&let_expr.expr);
            self.install_destructure_bindings(&let_expr.pat, &let_expr.expr);
        } else {
            self.visit_expr(&expr_while.cond);
        }
        self.visit_block(&expr_while.body);
        self.exit_scope();
    }

    fn visit_expr_match(&mut self, expr_match: &'ast syn::ExprMatch) {
        self.visit_expr(&expr_match.expr);
        for arm in &expr_match.arms {
            self.enter_scope();
            self.install_destructure_bindings(&arm.pat, &expr_match.expr);
            if let Some((_, guard)) = &arm.guard {
                self.visit_expr(guard);
            }
            self.visit_expr(&arm.body);
            self.exit_scope();
        }
    }

    fn visit_expr_for_loop(&mut self, for_loop: &'ast syn::ExprForLoop) {
        self.visit_expr(&for_loop.expr);
        self.enter_scope();
        self.install_for_bindings(&for_loop.pat, &for_loop.expr);
        self.visit_block(&for_loop.body);
        self.exit_scope();
    }

    fn visit_expr_call(&mut self, call: &'ast syn::ExprCall) {
        // Walk func + args first so nested calls / macros are recorded.
        self.visit_expr(&call.func);
        for arg in &call.args {
            self.visit_expr(arg);
        }
        if let syn::Expr::Path(p) = call.func.as_ref() {
            let segments: Vec<String> = p
                .path
                .segments
                .iter()
                .map(|s| s.ident.to_string())
                .collect();
            let leading_colon = p.path.leading_colon.is_some();
            if let Some(targets) = self.canonicalise_generic_param_path(&segments, leading_colon) {
                for t in targets {
                    self.record_call(t);
                }
            } else {
                let canonical = self.canonicalise_path(&segments, leading_colon);
                self.record_call(canonical);
            }
        }
    }

    fn visit_expr_method_call(&mut self, call: &'ast syn::ExprMethodCall) {
        // Walk receiver + args so nested resolution / method chains record.
        self.visit_expr(&call.receiver);
        for arg in &call.args {
            self.visit_expr(arg);
        }
        let method_name = call.method.to_string();
        let targets = self.resolve_method_targets(&call.receiver, &method_name);
        if targets.is_empty() {
            self.record_call(method_unknown(&method_name));
        } else {
            for t in targets {
                self.record_call(t);
            }
        }
    }

    fn visit_macro(&mut self, mac: &'ast syn::Macro) {
        self.collect_macro_body(mac);
    }

    fn visit_expr_closure(&mut self, c: &'ast syn::ExprClosure) {
        self.enter_scope();
        for input in &c.inputs {
            self.install_closure_param(input);
        }
        self.visit_expr(&c.body);
        self.exit_scope();
    }
}