Skip to main content

devscout_rs/resolve/
index.rs

1use crate::graph::{AlsoIn, Def, FragExtensionMethod, FragFact, FragRef, Fragment, OrderedMap};
2use std::collections::{HashMap, HashSet};
3
4// ---------------------------------------------------------------------------
5// Def index.
6// ---------------------------------------------------------------------------
7
8/// `Vec<Def>` in first-insertion order (partial-class duplicates land in
9/// `also_in`, never as a second Vec entry) plus two lookup indexes over it:
10/// `qualified_name_to_def` (id -> index, every def incl. enum members) and
11/// `simple_name_to_defs` (bare name -> indexes, EVERY def EXCEPT enum members
12/// -- the asymmetry described in the module header).
13pub struct DefIndex {
14    /// The defs value.
15    pub defs: Vec<Def>,
16    /// Parallel to `defs` (same indexes), NOT fields on `Def`: a def's
17    /// property/field lists are RESOLUTION inputs only. No graph.json reader
18    /// consumes them (graph-query walks EDGES), so serializing them would
19    /// multiply the def table's size on disk and in every `scout refs`/`scout
20    /// impact` read for nothing. Keeping them off `Def` keeps the on-disk def
21    /// rows to exactly the serialized fields.
22    pub member_lists: Vec<MemberLists>,
23    /// The qualified name to def value.
24    pub qualified_name_to_def: HashMap<String, usize>,
25    /// The qualified name and arity to def value.
26    pub qualified_name_and_arity_to_def: HashMap<(String, usize), usize>,
27    /// The simple name to defs value.
28    pub simple_name_to_defs: HashMap<String, Vec<usize>>,
29    /// The extension tier's own lookup: "<method_name> <this_type>" (one ASCII
30    /// space) -> every candidate declaring that PAIR, as (def index, entry).
31    ///
32    /// The key was the (name, this_type, arity) TRIPLE until the range
33    /// amendment. Arity moved out of the key and into a per-entry range filter
34    /// because an exact arity is not the language's rule:
35    /// `Send(this T t, X a, Y b = null)` accepts two OR three arguments, and
36    /// keying on 3 alone made a call with two arguments miss it -- which in turn
37    /// made a DIFFERENT class's exact-arity-2 `Send` look like the only
38    /// candidate and win an edge it had no right to -- the false-uniqueness
39    /// shape.
40    ///
41    /// Build order is def order -- files in fragment-map order, defs in source
42    /// order, extension entries in declaration order -- so a bucket holding two
43    /// candidates holds them in the same order on every run. The tier refuses on
44    /// a 2-candidate bucket anyway; the order is pinned because it is what a
45    /// later scored tier would read.
46    pub extension_index: HashMap<String, Vec<ExtCandidate>>,
47    /// Member name -> every def that vouches for a member of that name, the
48    /// pool the scored tier's UNIQUENESS FALLBACK draws from when a ref's
49    /// qualifier resolves to nothing at all (`x.Tally()` where the extractor
50    /// could vouch for no type for `x`). Built AFTER the main def loop, not
51    /// during it, so a partial class contributes its accumulated member set once
52    /// instead of appearing in a bucket per declaring file.
53    ///
54    /// Insertion order is def order (fragment order, then source order), and
55    /// within one def the names are collected methods -> properties -> fields ->
56    /// extension-method names, first insertion winning. This order decides which
57    /// candidates a `<= SCORED_UNIQUENESS_CAP` pool holds.
58    pub member_name_to_defs: HashMap<String, Vec<usize>>,
59}
60
61/// One bucket slot: which def declares the entry, and the entry itself (the
62/// range + generic-argument facts the tier filters on).
63pub struct ExtCandidate {
64    /// The def idx value.
65    pub def_idx: usize,
66    /// The entry value.
67    pub entry: FragExtensionMethod,
68}
69
70/// The non-method halves of a def's member surface. `methods` stays on `Def`
71/// (it IS serialized).
72#[derive(Default)]
73pub struct MemberLists {
74    /// The properties value.
75    pub properties: Vec<String>,
76    /// The fields value.
77    pub fields: Vec<String>,
78    /// Kept here for the same reason as `properties`/`fields`: a resolution
79    /// input, never a serialized def field. Holds the (name, this_type,
80    /// arity_min, arity_max) QUADRUPLES this def has already contributed, so a
81    /// partial class re-declaring one of its own entries in a second file can
82    /// never push the def into its own extension bucket twice and make itself
83    /// look like two candidates.
84    pub extension_methods: Vec<(String, String, usize, i64)>,
85    /// DIRECT base-type names, unioned across a partial class's declarations,
86    /// resolved LAZILY by the veto walk.
87    pub bases: Vec<String>,
88    /// The declaring type's own type-parameter names (empty for a non-generic
89    /// declaration). A resolution input, like `bases`: the ctor-DI resolver's
90    /// "is this def itself an open-generic implementation" signal. First
91    /// declaration wins, like `namespace`/`kind` -- not unioned across a partial
92    /// class the way `bases` is.
93    pub type_params: Vec<String>,
94    /// Per base name that carried a type-argument list, that list's generic-arg
95    /// descriptors relative to `type_params`. First declaration wins, same as
96    /// `type_params`.
97    pub base_generic_args: Vec<(String, Vec<String>)>,
98    /// Method name -> declared return type NAME, the fact that turns a
99    /// `var x = Q.M(...)` local into an ordinary receiver. A partial class's
100    /// later parts contribute the names the first part did not answer; a name it
101    /// already answered keeps its answer.
102    pub method_returns: OrderedMap<String>,
103    /// Property name -> declared type fact, the second half of a property hop.
104    /// Merged across a partial class exactly like `method_returns`.
105    pub property_types: OrderedMap<FragFact>,
106    /// Field name -> declared type fact, the field half of a bare-identifier
107    /// receiver's type when the file that reads it carries no local/
108    /// parameter/field fact of its own for the name (a sibling partial-class
109    /// file's field, reached only through this merged table). Merged across
110    /// a partial class exactly like `property_types`.
111    pub field_types: OrderedMap<FragFact>,
112    /// The file whose declaration supplied each `property_types` /
113    /// `field_types` entry -- one key per key of the table beside it, filled
114    /// in at merge time. Purely an in-memory bookkeeping table (nothing here
115    /// is serialized), and the only record of WHERE a merged fact came from:
116    /// a partial class's def carries the FIRST declaring file, which is not
117    /// necessarily the file that declared any given member. The type name a
118    /// fact holds is a bare identifier that only means anything under the
119    /// `using` directives, aliases and namespace of the file that WROTE it,
120    /// so `bare_receiver_field_or_property_type`'s caller resolves it in
121    /// that file's context rather than the reading file's.
122    pub property_type_files: OrderedMap<String>,
123    /// The `field_types` half of `property_type_files`.
124    pub field_type_files: OrderedMap<String>,
125    /// Method name -> the generic-arg descriptors `method_returns` itself
126    /// strips off (see `FragDef.method_return_args`), read ONLY by the
127    /// awaited-call unwrap: an entry here exists exactly when the same name
128    /// has a `method_returns` entry AND that return type carried a
129    /// top-level type-argument list. Merged across a partial class exactly
130    /// like `method_returns`.
131    pub method_return_args: OrderedMap<Vec<String>>,
132    /// Declared method names `methods` (on `Def`) does not carry -- see
133    /// `FragDef.non_public_methods`. Merged across a partial class exactly
134    /// like `properties`/`fields` (union, first-insertion order). Read ONLY
135    /// by `declares_member_any_visibility`, itself read ONLY for
136    /// hierarchy-internal receivers (`base.` and the `this.` shape's own
137    /// base walk); every other caller of a "does this def declare the
138    /// member" question keeps reading `Def.methods` alone.
139    pub non_public_methods: Vec<String>,
140    /// Method name -> the (min, max) argument-count ranges the overloads
141    /// sharing that name accept -- see `FragDef.method_arities`. Merged
142    /// across a partial class as a UNION of ranges per NAME, the way
143    /// `Def.methods` unions the names themselves: every declaring part
144    /// contributes the overloads it declares, duplicates dropped, because
145    /// the parts of one partial class share a single overload set and a
146    /// call some part's overload accepts is a call the type accepts. Read
147    /// by `declares_member`/`declares_member_any_visibility` for a ref that
148    /// carries an `argCount`.
149    pub method_arities: OrderedMap<Vec<(usize, i64)>>,
150    /// Method name -> the overloads' own parameter-descriptor lists, each
151    /// paired with the file whose fragment declared it -- see
152    /// `FragDef.method_params`. Merged across a partial class as a UNION per
153    /// NAME, the way `method_arities` is: every declaring part contributes
154    /// the overloads it declares, an overload whose `params` list already
155    /// exists for that name skipped (first file wins for duplicates). In
156    /// memory only; nothing here is serialized.
157    pub method_params: HashMap<String, Vec<MethodOverloadParams>>,
158    /// Declared method names carrying the literal `override` modifier -- see
159    /// `FragDef.override_methods`. Merged across a partial class exactly like
160    /// `non_public_methods`: union, first-insertion order. Read ONLY by the
161    /// resolver's member-level `overrides` pass, itself run only for a type
162    /// already known to be a registered DI implementation.
163    pub override_methods: Vec<String>,
164}
165
166/// One method overload's parameter-descriptor list plus its declaring file.
167///
168/// The (params, file) pair `MemberLists::method_params` keeps per method
169/// name. In-memory resolution input only; nothing here is serialized.
170pub struct MethodOverloadParams {
171    /// The parameter type descriptors, in order (see extract.rs's
172    /// `type_descriptor`/`DefRecord::method_params`).
173    pub params: Vec<String>,
174    /// The fragment-relative file path that declared this overload.
175    pub file: String,
176}
177
178#[allow(
179    clippy::too_many_lines,
180    clippy::cognitive_complexity,
181    reason = "one ordered pass building every lookup table the resolver ladder reads, so the tables stay mutually consistent"
182)]
183pub(super) fn build_def_index(fragments_by_file: &[(String, Fragment)]) -> DefIndex {
184    let mut defs: Vec<Def> = Vec::new();
185    let mut member_lists: Vec<MemberLists> = Vec::new();
186    let mut qualified_name_to_def: HashMap<String, usize> = HashMap::new();
187    let mut qualified_name_and_arity_to_def: HashMap<(String, usize), usize> = HashMap::new();
188    let mut simple_name_to_defs: HashMap<String, Vec<usize>> = HashMap::new();
189    let mut extension_index: HashMap<String, Vec<ExtCandidate>> = HashMap::new();
190
191    // One entry per key of `map`, all naming `file` -- the shape
192    // `MemberLists::property_type_files`/`field_type_files` hold for a def's
193    // FIRST declaration, where every fact came from the same file by
194    // construction.
195    fn keys_mapped_to_file<V>(map: &OrderedMap<V>, file: &str) -> OrderedMap<String> {
196        let mut out = OrderedMap::new();
197        for (name, _) in map.iter() {
198            out.insert(name.clone(), file.to_string());
199        }
200        out
201    }
202
203    // Dedupe on the def's OWN quadruple list FIRST, then push into the bucket --
204    // the guard is what keeps one def out of the same bucket twice.
205    fn add_extension_method(
206        member_lists: &mut [MemberLists],
207        extension_index: &mut HashMap<String, Vec<ExtCandidate>>,
208        idx: usize,
209        entry: &FragExtensionMethod,
210    ) {
211        if member_lists[idx]
212            .extension_methods
213            .iter()
214            .any(|(n, t, lo, hi)| {
215                *n == entry.name
216                    && *t == entry.this_type
217                    && *lo == entry.arity_min
218                    && *hi == entry.arity_max
219            })
220        {
221            return;
222        }
223        member_lists[idx].extension_methods.push((
224            entry.name.clone(),
225            entry.this_type.clone(),
226            entry.arity_min,
227            entry.arity_max,
228        ));
229        extension_index
230            .entry(format!("{} {}", entry.name, entry.this_type))
231            .or_default()
232            .push(ExtCandidate {
233                def_idx: idx,
234                entry: entry.clone(),
235            });
236    }
237
238    for (file, frag) in fragments_by_file {
239        for d in &frag.defs {
240            let def_key = (d.id.clone(), d.type_params.len());
241            match qualified_name_and_arity_to_def.get(&def_key) {
242                None => {
243                    let idx = defs.len();
244                    defs.push(Def {
245                        id: d.id.clone(),
246                        name: d.name.clone(),
247                        namespace: d.namespace.clone(),
248                        kind: d.kind.clone(),
249                        file: file.clone(),
250                        line: d.line,
251                        methods: d.methods.clone(),
252                        test_methods: d.test_methods.clone(),
253                        also_in: Vec::new(),
254                        end_line: d.end_line,
255                    });
256                    member_lists.push(MemberLists {
257                        properties: d.properties.clone(),
258                        fields: d.fields.clone(),
259                        extension_methods: Vec::new(),
260                        bases: d.bases.clone(),
261                        type_params: d.type_params.clone(),
262                        base_generic_args: d
263                            .base_generic_args
264                            .iter()
265                            .map(|(k, v)| (k.clone(), v.clone()))
266                            .collect(),
267                        method_returns: d.method_returns.clone(),
268                        property_types: d.property_types.clone(),
269                        field_types: d.field_types.clone(),
270                        property_type_files: keys_mapped_to_file(&d.property_types, file),
271                        field_type_files: keys_mapped_to_file(&d.field_types, file),
272                        method_return_args: d.method_return_args.clone(),
273                        non_public_methods: d.non_public_methods.clone(),
274                        method_arities: d.method_arities.clone(),
275                        method_params: {
276                            let mut m: HashMap<String, Vec<MethodOverloadParams>> = HashMap::new();
277                            for (name, overloads) in d.method_params.iter() {
278                                m.insert(
279                                    name.clone(),
280                                    overloads
281                                        .iter()
282                                        .map(|params| MethodOverloadParams {
283                                            params: params.clone(),
284                                            file: file.clone(),
285                                        })
286                                        .collect(),
287                                );
288                            }
289                            m
290                        },
291                        override_methods: d.override_methods.clone(),
292                    });
293                    for e in &d.extension_methods {
294                        add_extension_method(&mut member_lists, &mut extension_index, idx, e);
295                    }
296                    qualified_name_to_def.entry(d.id.clone()).or_insert(idx);
297                    qualified_name_and_arity_to_def.insert(def_key, idx);
298                    // Enum members are reachable by exact id (below, via
299                    // qualified_name_to_def) but deliberately excluded from
300                    // simple_name_to_defs -- see module header.
301                    if d.kind != "enum-member" {
302                        simple_name_to_defs
303                            .entry(d.name.clone())
304                            .or_default()
305                            .push(idx);
306                    }
307                }
308                Some(&idx) => {
309                    // A partial class contributes its own members to the same
310                    // record; first declaring file wins the order, later ones
311                    // append what is new.
312                    defs[idx].also_in.push(AlsoIn {
313                        file: file.clone(),
314                        line: d.line,
315                    });
316                    for m in &d.methods {
317                        if !defs[idx].methods.contains(m) {
318                            defs[idx].methods.push(m.clone());
319                        }
320                    }
321                    // Test-coverage stage -- unioned exactly like `methods`
322                    // above it, so a partial test class split across two files
323                    // is one def declaring the union of both parts' tests.
324                    for t in &d.test_methods {
325                        if !defs[idx].test_methods.contains(t) {
326                            defs[idx].test_methods.push(t.clone());
327                        }
328                    }
329                    for p in &d.properties {
330                        if !member_lists[idx].properties.contains(p) {
331                            member_lists[idx].properties.push(p.clone());
332                        }
333                    }
334                    for f in &d.fields {
335                        if !member_lists[idx].fields.contains(f) {
336                            member_lists[idx].fields.push(f.clone());
337                        }
338                    }
339                    for m in &d.non_public_methods {
340                        if !member_lists[idx].non_public_methods.contains(m) {
341                            member_lists[idx].non_public_methods.push(m.clone());
342                        }
343                    }
344                    for m in &d.override_methods {
345                        if !member_lists[idx].override_methods.contains(m) {
346                            member_lists[idx].override_methods.push(m.clone());
347                        }
348                    }
349                    for e in &d.extension_methods {
350                        add_extension_method(&mut member_lists, &mut extension_index, idx, e);
351                    }
352                    for b in &d.bases {
353                        if !member_lists[idx].bases.contains(b) {
354                            member_lists[idx].bases.push(b.clone());
355                        }
356                    }
357                    // A partial class's later parts contribute the member types
358                    // the first part did not declare; a name the first part
359                    // already answered keeps its answer, the same
360                    // first-declaration-wins rule the extractor applies within
361                    // one declaration.
362                    for (name, returns) in d.method_returns.iter() {
363                        if member_lists[idx].method_returns.get(name).is_none() {
364                            member_lists[idx]
365                                .method_returns
366                                .insert(name.clone(), returns.clone());
367                        }
368                    }
369                    // The declaring FILE is recorded alongside each fact
370                    // it accepts -- see `MemberLists::property_type_files`:
371                    // a type name is only meaningful under the usings of
372                    // the file that wrote it, and for a partial class that
373                    // is not always the def's own first-declaring file.
374                    for (name, fact) in d.property_types.iter() {
375                        if member_lists[idx].property_types.get(name).is_none() {
376                            member_lists[idx]
377                                .property_types
378                                .insert(name.clone(), fact.clone());
379                            member_lists[idx]
380                                .property_type_files
381                                .insert(name.clone(), file.clone());
382                        }
383                    }
384                    for (name, fact) in d.field_types.iter() {
385                        if member_lists[idx].field_types.get(name).is_none() {
386                            member_lists[idx]
387                                .field_types
388                                .insert(name.clone(), fact.clone());
389                            member_lists[idx]
390                                .field_type_files
391                                .insert(name.clone(), file.clone());
392                        }
393                    }
394                    for (name, args) in d.method_return_args.iter() {
395                        if member_lists[idx].method_return_args.get(name).is_none() {
396                            member_lists[idx]
397                                .method_return_args
398                                .insert(name.clone(), args.clone());
399                        }
400                    }
401                    // A UNION per NAME, unlike `method_returns` above: a
402                    // partial class's parts declare OVERLOADS of one name,
403                    // not competing answers for it, so `void Run()` in one
404                    // file and `void Run(int)` in another must both be
405                    // admitted -- first-declaration-wins here would have
406                    // hidden the sibling file's overload and made
407                    // `method_arity_admits` refuse a call the type really
408                    // accepts. Ranges already recorded are not repeated, so
409                    // a part re-declaring an overload the merged set holds
410                    // leaves the set unchanged.
411                    for (name, ranges) in d.method_arities.iter() {
412                        let mut merged = member_lists[idx]
413                            .method_arities
414                            .get(name)
415                            .cloned()
416                            .unwrap_or_default();
417                        for range in ranges {
418                            if !merged.contains(range) {
419                                merged.push(*range);
420                            }
421                        }
422                        member_lists[idx]
423                            .method_arities
424                            .insert(name.clone(), merged);
425                    }
426                    // A UNION per NAME, same as `method_arities` just above:
427                    // every declaring part's overloads are admitted, an
428                    // overload whose `params` list a merged entry already
429                    // holds skipped -- first file wins for that duplicate.
430                    for (name, overloads) in d.method_params.iter() {
431                        let merged = member_lists[idx]
432                            .method_params
433                            .entry(name.clone())
434                            .or_default();
435                        for params in overloads {
436                            if !merged.iter().any(|o| &o.params == params) {
437                                merged.push(MethodOverloadParams {
438                                    params: params.clone(),
439                                    file: file.clone(),
440                                });
441                            }
442                        }
443                    }
444                }
445            }
446        }
447    }
448
449    // See `DefIndex::member_name_to_defs`. A second pass over the ALREADY-MERGED
450    // def records, so a partial class spread over three files lands in each of
451    // its member-name buckets exactly once.
452    let mut member_name_to_defs: HashMap<String, Vec<usize>> = HashMap::new();
453    for idx in 0..defs.len() {
454        let mut names: Vec<&str> = Vec::new();
455        let mut seen: HashSet<&str> = HashSet::new();
456        for m in &defs[idx].methods {
457            if seen.insert(m.as_str()) {
458                names.push(m.as_str());
459            }
460        }
461        for p in &member_lists[idx].properties {
462            if seen.insert(p.as_str()) {
463                names.push(p.as_str());
464            }
465        }
466        for f in &member_lists[idx].fields {
467            if seen.insert(f.as_str()) {
468                names.push(f.as_str());
469            }
470        }
471        for (name, ..) in &member_lists[idx].extension_methods {
472            if seen.insert(name.as_str()) {
473                names.push(name.as_str());
474            }
475        }
476        for n in names {
477            member_name_to_defs
478                .entry(n.to_string())
479                .or_default()
480                .push(idx);
481        }
482    }
483
484    DefIndex {
485        defs,
486        member_lists,
487        qualified_name_to_def,
488        qualified_name_and_arity_to_def,
489        simple_name_to_defs,
490        extension_index,
491        member_name_to_defs,
492    }
493}
494
495// A synthetic bare type reference, for re-running the ladder on a name the
496// RESOLVER derived rather than one it read off a ref: a base type, a receiver's
497// recorded type, a property's declared type, a callee's owner. Every field
498// except `name`/`outer_types` is what a bare, non-generic, non-member reference
499// carries, so a probe can never re-enter a member tier by accident.
500pub(super) fn name_probe(name: String, namespace: &str, outer_types: Vec<String>) -> FragRef {
501    FragRef {
502        kind: "uses-type".to_string(),
503        name,
504        qualified: None,
505        member: None,
506        line: 0,
507        namespace: Some(namespace.to_string()),
508        type_arg_count: None,
509        generic: false,
510        receiver_type: None,
511        arg_count: None,
512        receiver_args: None,
513        outer_types,
514        args: None,
515        receiver_property_owner: None,
516        receiver_call_owner: None,
517        receiver_call_member: None,
518        receiver_base: false,
519        receiver_awaited: false,
520        receiver_local: false,
521        receiver_lambda: None,
522        receiver_nullable: false,
523        lambda_arg_arity: None,
524    }
525}