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}