1use std::cell::RefCell;
2use std::collections::{HashMap, HashSet};
3use std::path::Path;
4use std::time::{Duration, Instant};
5
6use rusqlite::types::ToSql;
7use rusqlite::{Connection, OptionalExtension, Row, Statement, params, params_from_iter};
8
9use crate::models::{
10 LabeledSite, ReferenceCounts, ReferenceReport, ReferenceSite, ReferenceTarget, SiteLabel,
11 Symbol, TextOnlyLines, TextScanSummary,
12};
13use crate::queries::{
14 QueryError, built_receiver_sql, class_chain_sql, constructed_class, direct_identifier_rows,
15 documentation_language_list, enclosing_scope_sql, get_symbol_by_name,
16 grouped_member_access_rows, has_column, implicit_receiver_language_list, import_sites,
17 import_sites_of_absent, inherited_writes_among, module_import_binding_sql, path_proximity,
18 pending_target_predicate, populate_lambda_enclosing_names, receiver_type_sql,
19 symbol_not_found_parts,
20};
21use crate::text_scan::scan_word;
22
23pub(crate) mod arity;
24mod js_bindings;
25mod js_modules;
26mod receiver_types;
27mod sections;
28pub(crate) mod variant_names;
29
30pub(crate) use sections::{destructurings, enum_variant_names, uses_of_variants};
31
32const RESOLVER_ROW_CAP: usize = 5_000;
33const OTHER_DEFINITION_CAP: usize = 12;
34const ACCOUNTING_TIME_LIMIT: Duration = Duration::from_secs(3);
36const JUDGE_CHUNK: usize = 2_000;
38const NOT_CHECKED: &str = "not checked within the time limit";
39const TEXT_SCAN_BUDGET: Duration = Duration::from_secs(3);
40
41const CLASS_LIKE_KINDS: &[&str] = &[
42 "class",
43 "struct",
44 "interface",
45 "trait",
46 "enum",
47 "record",
48 "object",
49 "protocol",
50 "union",
51];
52const SELF_RECEIVERS: &[&str] = &["self", "this", "cls", "Self", "super"];
53const EXTERNAL_RUST_ROOTS: &[&str] = &["std", "core", "alloc"];
54const IMPORT_BASIS: &str = "import";
56const NAME_RESOLVED_BASES: &[&str] = &[
58 IMPORT_BASIS,
59 "same file",
60 "enclosing scope",
61 "qualified path",
62];
63
64pub fn select_reference_target(
67 conn: &Connection,
68 name: &str,
69 path_filter: Option<&str>,
70) -> Result<Symbol, QueryError> {
71 match get_symbol_by_name(conn, name, path_filter)? {
72 Some(target) => Ok(target),
73 None => {
74 let (workspace, hint) = symbol_not_found_parts(conn, name, path_filter);
75 Err(QueryError::SymbolNotFound {
76 name: name.to_string(),
77 workspace,
78 hint,
79 })
80 }
81 }
82}
83
84pub fn reference_report(
87 conn: &Connection,
88 root: &Path,
89 target: &Symbol,
90 _include_external: bool,
91) -> Result<ReferenceReport, QueryError> {
92 report_within(conn, root, target, ACCOUNTING_TIME_LIMIT)
93}
94
95pub fn reference_report_for_edit(
101 conn: &Connection,
102 root: &Path,
103 target: &Symbol,
104) -> Result<ReferenceReport, QueryError> {
105 report_with(
106 conn,
107 root,
108 target,
109 ACCOUNTING_TIME_LIMIT,
110 ReportMode {
111 text_scan: false,
112 arity_exclusion: false,
113 absent_target: crate::queries::get_symbol_by_id(conn, &target.symbol_id)?.is_none(),
114 },
115 )
116}
117
118#[derive(Clone, Copy)]
120struct ReportMode {
121 text_scan: bool,
122 arity_exclusion: bool,
123 absent_target: bool,
125}
126
127fn report_within(
128 conn: &Connection,
129 root: &Path,
130 target: &Symbol,
131 time_limit: Duration,
132) -> Result<ReferenceReport, QueryError> {
133 let mode = ReportMode {
134 text_scan: true,
135 arity_exclusion: true,
136 absent_target: false,
137 };
138 report_with(conn, root, target, time_limit, mode)
139}
140
141fn report_with(
142 conn: &Connection,
143 root: &Path,
144 target: &Symbol,
145 time_limit: Duration,
146 mode: ReportMode,
147) -> Result<ReferenceReport, QueryError> {
148 let class = constructor_class(conn, target)?;
149 let group = overload_group(conn, target)?;
150 let mut accounting = Accounting::new(conn, root, time_limit);
151 accounting.absent = mode.absent_target.then(|| target.clone());
152 let target_arity = match arity::group_arity(conn, root, &group, false)? {
153 Some(own) if mode.arity_exclusion => {
154 arity::widened_by_ancestors(&mut accounting, target, own)?
155 }
156 _ => None,
157 };
158 accounting.restart_clock(time_limit);
159 let mut specs = vec![Spec {
160 name: target.name.clone(),
161 arity: target_arity,
162 local_to: enclosing_function(conn, &group)?,
163 group,
164 scope: if class.is_some() {
165 RowScope::Calls
166 } else {
167 RowScope::All
168 },
169 builds_class: false,
170 }];
171 if let Some(class) = &class {
172 specs.push(Spec {
173 name: class.name.clone(),
174 group: vec![class.clone()],
175 scope: RowScope::Calls,
176 builds_class: true,
177 local_to: None,
178 arity: if mode.arity_exclusion {
179 arity::group_arity(conn, root, &specs[0].group, true)?
180 } else {
181 None
182 },
183 });
184 }
185
186 let mut site_rows = Vec::new();
187 let mut cannot_see = Vec::new();
188 for spec in &specs {
189 accounting.account_spec(spec, &mut site_rows, &mut cannot_see)?;
190 }
191 let definitions_with_name = accounting.definition_count(&target.name)?;
192
193 let mut sites = merge_site_rows(site_rows);
194 let unchecked = sites
195 .iter()
196 .filter(|site| site.basis.ends_with(NOT_CHECKED))
197 .count();
198 if unchecked > 0 {
199 cannot_see.push(format!(
200 "{unchecked} of {} sites were not checked within the {} s time limit",
201 sites.len(),
202 time_limit.as_secs()
203 ));
204 }
205 let mut report_sites: Vec<ReferenceSite> = sites.iter().map(|row| row.site.clone()).collect();
206 populate_lambda_enclosing_names(conn, &mut report_sites)?;
207 for (row, site) in sites.iter_mut().zip(report_sites) {
208 row.site.enclosing_symbol_name = site.enclosing_symbol_name;
209 }
210 sites.sort_by(|a, b| {
211 a.label
212 .rank()
213 .cmp(&b.label.rank())
214 .then_with(|| a.site.path.cmp(&b.site.path))
215 .then_with(|| a.site.start_line.cmp(&b.site.start_line))
216 .then_with(|| a.site.start_column.cmp(&b.site.start_column))
217 });
218
219 let text = match &class {
220 _ if !mode.text_scan => TextOutcome::not_run(),
221 Some(class) => TextOutcome::skipped(class),
222 None => accounting.text_outcome(root, &target.name, &sites)?,
223 };
224
225 let mut counts = ReferenceCounts {
226 sites: sites.len(),
227 ..text.counts
228 };
229 for site in &sites {
230 match site.label {
231 SiteLabel::Proven => counts.proven += 1,
232 SiteLabel::Likely => counts.likely += 1,
233 SiteLabel::Unresolved => counts.unresolved += 1,
234 SiteLabel::Excluded => counts.excluded += 1,
235 }
236 }
237
238 let reach = sections::reach_sections(&mut accounting, target, &sites)?;
239 cannot_see.splice(0..0, cannot_see_for_language(&target.language));
240 cannot_see.extend(reach.notes);
241 Ok(ReferenceReport {
242 target: report_target(conn, target, definitions_with_name)?,
243 counts,
244 sites,
245 text_only: text.text_only,
246 text_scan: text.summary,
247 cannot_see,
248 imports: match mode.absent_target {
249 true if target.parent_symbol_id.is_some() => Vec::new(),
250 true => import_sites_of_absent(conn, &target.name, &target.path)?,
251 false => import_sites(conn, &target.name, Some(&target.path))?,
252 },
253 overrides: reach.overrides,
254 overridden: reach.overridden,
255 constructions: reach.constructions,
256 variant_uses: reach.variant_uses,
257 registrations: reach.registrations,
258 tests: reach.tests,
259 })
260}
261
262fn cannot_see_for_language(language: &str) -> Vec<String> {
263 let mut items = Vec::new();
264 match language {
265 "python" => items.push("calls through getattr() or names built at runtime".to_string()),
266 "javascript" | "typescript" | "tsx" | "jsx" | "vue" => items
267 .push("calls through computed property names or values built at runtime".to_string()),
268 "rust" => items.push("calls inside macro bodies".to_string()),
269 "java" | "csharp" | "kotlin" => {
270 items.push("calls through reflection or dependency injection".to_string());
271 }
272 "go" => items.push(
273 "calls through a Go interface that the index does not record reach methods this list does not show"
274 .to_string(),
275 ),
276 _ => {}
277 }
278 items
279}
280
281fn report_target(
282 conn: &Connection,
283 target: &Symbol,
284 definitions_with_name: usize,
285) -> Result<ReferenceTarget, QueryError> {
286 let mut qualified = target.clone();
287 crate::queries::qualify_members(conn, std::iter::once(&mut qualified))?;
288 Ok(ReferenceTarget {
289 symbol_id: target.symbol_id.clone(),
290 name: target.name.clone(),
291 display_name: qualified.name,
292 kind: crate::formatters::display_kind(target).to_string(),
293 path: target.path.clone(),
294 line: target.start_line,
295 definitions_with_name,
296 })
297}
298
299fn is_constructor(target: &Symbol) -> bool {
300 target.kind == "constructor" || (target.language == "python" && target.name == "__init__")
301}
302
303fn constructor_class(conn: &Connection, target: &Symbol) -> Result<Option<Symbol>, QueryError> {
304 if !is_constructor(target) {
305 return Ok(None);
306 }
307 let class_id = match constructed_class(conn, &target.symbol_id)? {
308 Some((id, _)) => id,
309 None => match &target.parent_symbol_id {
310 Some(parent) => parent.clone(),
311 None => return Ok(None),
312 },
313 };
314 crate::queries::get_symbol_by_id(conn, &class_id)
315}
316
317pub(crate) fn inherited_group_arity(
321 conn: &Connection,
322 root: &Path,
323 target: &Symbol,
324) -> Result<Option<arity::Arity>, QueryError> {
325 let group = overload_group(conn, target)?;
326 let Some(own) = arity::group_arity(conn, root, &group, false)? else {
327 return Ok(None);
328 };
329 if !matches!(target.language.as_str(), "java" | "csharp" | "kotlin") {
330 return Ok(Some(own));
331 }
332 let mut accounting = Accounting::new(conn, root, ACCOUNTING_TIME_LIMIT);
333 Ok(arity::widened_by_ancestors(&mut accounting, target, own.clone())?.or(Some(own)))
334}
335
336pub(crate) fn overload_group(
338 conn: &Connection,
339 target: &Symbol,
340) -> Result<Vec<Symbol>, QueryError> {
341 let mut stmt = conn.prepare(
342 "SELECT symbol_id FROM symbols
343 WHERE name = ?1 AND path = ?2 AND kind = ?3 AND parent_symbol_id IS ?4
344 ORDER BY start_line, symbol_id",
345 )?;
346 let ids = stmt
347 .query_map(
348 params![
349 target.name,
350 target.path,
351 target.kind,
352 target.parent_symbol_id
353 ],
354 |row| row.get::<_, String>(0),
355 )?
356 .collect::<Result<Vec<_>, _>>()?;
357 let mut group = vec![target.clone()];
358 for id in ids.into_iter().filter(|id| *id != target.symbol_id) {
359 if let Some(symbol) = crate::queries::get_symbol_by_id(conn, &id)? {
360 group.push(symbol);
361 }
362 }
363 Ok(group)
364}
365
366const DEFINITION_BY_ID_SQL: &str =
367 "SELECT s.symbol_id, s.name, s.kind, s.language, s.path, s.start_line,
368 s.body_start_line, s.parent_symbol_id, par.name, par.kind
369 FROM symbols s LEFT JOIN symbols par ON par.symbol_id = s.parent_symbol_id
370 WHERE s.symbol_id = ?1";
371
372const LINKS_ENCLOSING_CLASS_SQL: &str = "EXISTS (
375 WITH RECURSIVE enclosing(symbol_id, depth) AS (
376 SELECT r.from_symbol_id, 0
377 UNION ALL
378 SELECT s.parent_symbol_id, enclosing.depth + 1
379 FROM symbols s JOIN enclosing ON s.symbol_id = enclosing.symbol_id
380 WHERE s.parent_symbol_id IS NOT NULL AND enclosing.depth < 32
381 )
382 SELECT 1 FROM enclosing WHERE symbol_id = t.parent_symbol_id
383 )";
384
385const SHADOW_IN_CHAIN_SQL: &str = "WITH RECURSIVE chain(symbol_id, depth) AS (
386 SELECT ?1, 0
387 UNION ALL
388 SELECT s.parent_symbol_id, chain.depth + 1
389 FROM symbols s JOIN chain ON s.symbol_id = chain.symbol_id
390 WHERE s.parent_symbol_id IS NOT NULL AND chain.depth < 32
391 )
392 SELECT d.symbol_id, COALESCE(d.signature, ''), d.kind
393 FROM chain
394 JOIN symbols scope ON scope.symbol_id = chain.symbol_id
395 JOIN symbols d ON d.parent_symbol_id = chain.symbol_id
396 WHERE d.name = ?2 AND d.path = ?3
397 AND (d.kind IN ('variable', 'parameter', 'function', 'constant', 'class')
398 OR ?4 AND d.kind IN ('type', 'interface', 'enum'))
399 AND (chain.depth = 0 OR scope.kind IN ('function', 'method', 'constructor'))
400 ORDER BY chain.depth, d.start_line > ?5, abs(?5 - d.start_line)";
401
402const SHADOW_TOP_LEVEL_SQL: &str = "SELECT symbol_id, COALESCE(signature, ''), kind FROM symbols
403 WHERE name = ?1 AND path = ?2 AND parent_symbol_id IS NULL
404 AND kind NOT IN ('import', 'export', 'module', 'namespace')
405 ORDER BY start_line > ?3, abs(?3 - start_line)";
406
407const JAVA_STATIC_IMPORT_SQL: &str = "SELECT signature FROM symbols
408 WHERE kind = 'import' AND path = ?1 AND name = ?2 AND signature LIKE 'import static %'";
409
410const IMPORT_SOURCES_SQL: &str = "SELECT COALESCE(json_extract(metadata_json, '$.source'), '')
411 FROM symbols
412 WHERE kind = 'import' AND path = ?1 AND json_valid(metadata_json)
413 AND (name = ?2 OR json_extract(metadata_json, '$.alias') = ?2
414 OR json_extract(metadata_json, '$.local_name') = ?2)";
415
416const PENDING_IMPORT_MODULES_SQL: &str = "SELECT target_namespace_json FROM pending_relationships
417 WHERE kind = 'imports' AND path = ?1 AND target_terminal_name = ?2";
418
419const FILE_EXISTS_SQL: &str = "SELECT 1 FROM files WHERE path = ?1 OR path = ?2 LIMIT 1";
420
421const SOURCE_FILES_SQL: &str = "SELECT 1 FROM files
422 WHERE '/' || replace(replace(path, '\\', '/'), '-', '_') LIKE ?1 ESCAPE '\\'
423 OR '/' || replace(replace(path, '\\', '/'), '-', '_') LIKE ?2 ESCAPE '\\'
424 LIMIT 1";
425
426const RECEIVER_TYPE_SQL: &str = "WITH RECURSIVE chain(symbol_id, depth) AS (
427 SELECT ?1, 0
428 UNION ALL
429 SELECT s.parent_symbol_id, chain.depth + 1
430 FROM symbols s JOIN chain ON s.symbol_id = chain.symbol_id
431 WHERE s.parent_symbol_id IS NOT NULL AND chain.depth < 32
432 ), typed(depth, resolved_type) AS (
433 SELECT chain.depth, tf.resolved_type
434 FROM chain
435 JOIN symbols r ON r.parent_symbol_id = chain.symbol_id
436 JOIN type_facts tf ON tf.symbol_id = r.symbol_id
437 WHERE r.name = ?2 AND r.path = ?3
438 AND r.kind IN ('variable', 'parameter', 'field', 'property')
439 )
440 SELECT DISTINCT resolved_type FROM typed
441 WHERE depth = (SELECT MIN(depth) FROM typed)";
442
443const ANCESTORS_BY_RELATIONSHIP_SQL: &str = "SELECT DISTINCT base.name
444 FROM symbols child
445 JOIN relationships r ON r.from_symbol_id = child.symbol_id
446 AND r.kind IN ('extends', 'implements')
447 JOIN symbols base ON base.symbol_id = r.to_symbol_id
448 WHERE child.name = ?1";
449
450const ANCESTORS_BY_PENDING_SQL: &str = "SELECT DISTINCT pe.target_terminal_name
451 FROM symbols child
452 JOIN pending_relationships pe ON pe.from_symbol_id = child.symbol_id
453 AND pe.kind IN ('extends', 'implements')
454 WHERE child.name = ?1";
455
456const OWNER_IN_PACKAGE_SQL: &str = "SELECT COUNT(*) FROM symbols
457 WHERE name = ?1
458 AND kind IN ('class', 'struct', 'interface', 'trait', 'enum', 'record', 'object', 'protocol', 'union')
459 AND replace(path, '\\', '/') LIKE ?2 ESCAPE '\\'";
460
461const TYPE_HEAD_KINDS_SQL: &str =
462 "SELECT DISTINCT kind FROM symbols WHERE name = ?1 AND kind != 'parameter'";
463
464const MEMBER_ACCESS_PATHS_SQL: &str =
465 "SELECT DISTINCT path FROM identifiers WHERE name = ?1 AND kind = 'member_access'";
466
467const VARIABLE_REF_SITES_SQL: &str = "SELECT path, start_line, start_column FROM identifiers
468 WHERE name = ?1 AND kind = 'variable_ref' AND start_column IS NOT NULL";
469
470const TYPE_DECLARATIONS_SQL: &str = "SELECT COALESCE(signature, '') FROM symbols
471 WHERE name = ?1 AND kind = 'type' AND language = ?2";
472
473const TOP_LEVEL_VARIABLE_SQL: &str = "SELECT 1 FROM symbols
474 WHERE path = ?1 AND name = ?2 AND parent_symbol_id IS NULL
475 AND kind IN ('variable', 'constant') LIMIT 1";
476
477const FN_TYPES: [&str; 5] = ["fn(", "fn (", "Fn(", "FnMut(", "FnOnce("];
478
479#[derive(Clone, Copy, PartialEq, Eq)]
481enum RowScope {
482 All,
483 Calls,
484 Inheritance,
486}
487
488struct Spec {
489 name: String,
490 group: Vec<Symbol>,
491 scope: RowScope,
492 builds_class: bool,
493 arity: Option<arity::Arity>,
495 local_to: Option<Symbol>,
497}
498
499#[derive(Clone, Copy, PartialEq, Eq, Hash)]
500enum Source {
501 Pending,
502 Relationship,
503 Identifier,
504}
505
506impl Source {
507 fn tag(self) -> &'static str {
508 match self {
509 Source::Pending => "P",
510 Source::Relationship => "R",
511 Source::Identifier => "I",
512 }
513 }
514}
515
516struct NameRow {
517 key: String,
518 source: Source,
519 caller_id: Option<String>,
520 caller_name: String,
521 caller_end_line: Option<usize>,
523 path: String,
524 language: String,
525 kind: String,
526 line: usize,
527 column: Option<usize>,
528 end_line: Option<usize>,
529 receiver: Option<String>,
530 namespace: Vec<String>,
531 to_symbol_id: Option<String>,
532 site_line: usize,
533 site_column: Option<usize>,
534 consumed: bool,
535 paired: bool,
536 hidden_receiver: bool,
539 chain: Option<Vec<String>>,
542 arg_count: Option<u32>,
545 links_enclosing_class: bool,
547}
548
549impl NameRow {
550 fn has_qualifier(&self) -> bool {
551 self.hidden_receiver
552 || self.receiver.as_deref().is_some_and(|r| !r.is_empty())
553 || !self.namespace.is_empty()
554 }
555
556 fn qualifier(&self) -> Option<&str> {
557 self.namespace
558 .first()
559 .map(String::as_str)
560 .or(self.receiver.as_deref())
561 .filter(|q| is_plain_identifier(q))
562 }
563
564 fn is_super_call(&self) -> bool {
565 self.receiver.as_deref().is_some_and(is_super_receiver)
566 }
567
568 fn is_call(&self) -> bool {
569 matches!(self.kind.as_str(), "calls" | "call" | "instantiates")
570 }
571
572 fn is_not_named_by(&self, kind: &str) -> bool {
575 let value_kind = matches!(
576 kind,
577 "field" | "property" | "variable" | "constant" | "parameter"
578 );
579 let java_call = self.language == "java" && matches!(self.kind.as_str(), "calls" | "call");
580 let type_use_of_value =
581 self.is_bare_type_use() && (value_kind || matches!(kind, "function" | "method"));
582 (java_call && value_kind) || type_use_of_value
583 }
584
585 fn is_bare_use(&self) -> bool {
586 !self.has_qualifier()
587 && matches!(
588 self.kind.as_str(),
589 "calls" | "call" | "variable_ref" | "references"
590 )
591 }
592
593 fn is_bare_type_use(&self) -> bool {
596 self.kind == "type_usage"
597 && language_family(&self.language) == "js"
598 && self.chain.as_ref().is_some_and(Vec::is_empty)
599 }
600
601 fn display_kind(&self) -> String {
602 match self.kind.as_str() {
603 "call" => "calls".to_string(),
604 other => other.to_string(),
605 }
606 }
607
608 fn can_reach(&self, definition: &Definition) -> bool {
611 let call_of_a_field = self.is_call()
612 && !definition.callable
613 && languages_compatible(&self.language, &definition.language);
614 let member_of_a_scoped_function =
615 definition.free && language_family(&self.language) == "js" && self.has_qualifier();
616 !call_of_a_field && !member_of_a_scoped_function && !self.reads_a_module(&definition.kind)
617 }
618
619 fn reads_a_module(&self, kind: &str) -> bool {
621 self.language == "rust"
622 && self.kind == "variable_ref"
623 && matches!(kind, "namespace" | "module")
624 }
625}
626
627fn is_super_receiver(receiver: &str) -> bool {
630 matches!(receiver, "super" | "base") || receiver.starts_with("super<")
631}
632
633const LOCAL_FUNCTION_LANGUAGES: &[&str] = &[
636 "python", "rust", "kotlin", "csharp", "swift", "scala", "dart",
637];
638
639fn enclosing_function(conn: &Connection, group: &[Symbol]) -> Result<Option<Symbol>, QueryError> {
642 let Some(parent) = group
643 .first()
644 .and_then(|first| first.parent_symbol_id.as_deref())
645 else {
646 return Ok(None);
647 };
648 let local = group.iter().all(|member| {
649 member.kind == "function"
650 && LOCAL_FUNCTION_LANGUAGES.contains(&member.language.as_str())
651 && member.parent_symbol_id.as_deref() == Some(parent)
652 });
653 if !local {
654 return Ok(None);
655 }
656 Ok(crate::queries::get_symbol_by_id(conn, parent)?
657 .filter(|outer| matches!(outer.kind.as_str(), "function" | "method" | "constructor")))
658}
659
660fn is_property_method(symbol: &Symbol) -> bool {
663 symbol.kind == "method"
664 && symbol.parent_symbol_id.is_none()
665 && language_family(&symbol.language) == "js"
666}
667
668fn extended_type(signature: &str) -> Option<String> {
670 let rest = signature
671 .match_indices('(')
672 .find_map(|(open, _)| arity::after_csharp_this(&signature[open + 1..]))?;
673 let written = rest.split_whitespace().next()?;
674 let unqualified = written.split('<').next()?.rsplit('.').next()?;
675 Some(unqualified.trim_end_matches('?').to_string()).filter(|t| is_plain_identifier(t))
676}
677
678fn is_test_label(signature: &str) -> bool {
681 signature.split_once('(').is_some_and(|(callee, rest)| {
682 !callee.is_empty()
683 && callee.split('.').all(is_plain_identifier)
684 && rest.trim_start().starts_with(['"', '\'', '`'])
685 })
686}
687
688fn is_plain_identifier(text: &str) -> bool {
689 let mut chars = text.chars();
690 chars
691 .next()
692 .is_some_and(|c| c.is_alphabetic() || c == '_' || c == '$')
693 && chars.all(is_word_char)
694}
695
696fn is_word_char(c: char) -> bool {
697 c.is_alphanumeric() || c == '_' || c == '$'
698}
699
700fn is_record(signature: &str) -> bool {
703 let declaration = outside_quotes_and_parentheses(signature);
704 declaration.match_indices("record").any(|(at, word)| {
705 let after = &declaration[at + word.len()..];
706 !declaration[..at].ends_with(is_word_char)
707 && after.starts_with(char::is_whitespace)
708 && after.trim_start().starts_with(is_word_char)
709 })
710}
711
712fn outside_quotes_and_parentheses(text: &str) -> String {
715 let mut outside = String::with_capacity(text.len());
716 let mut depth = 0usize;
717 let mut chars = text.chars();
718 while let Some(c) = chars.next() {
719 match c {
720 '"' | '\'' => {
721 while let Some(inner) = chars.next() {
722 match inner {
723 '\\' => {
724 chars.next();
725 }
726 _ if inner == c => break,
727 _ => {}
728 }
729 }
730 if depth == 0 {
731 outside.push(' ');
732 }
733 }
734 '(' => depth += 1,
735 ')' => {
736 depth = depth.saturating_sub(1);
737 if depth == 0 {
738 outside.push(' ');
739 }
740 }
741 _ if depth == 0 => outside.push(c),
742 _ => {}
743 }
744 }
745 outside
746}
747
748fn is_function_type(signature: &str, name: &str, language: &str) -> bool {
751 let Some(declared) = signature
752 .trim_start()
753 .strip_prefix("type")
754 .and_then(|rest| rest.trim_start().strip_prefix(name))
755 else {
756 return false;
757 };
758 let declared = declared.trim_start();
759 let declared = match declared.chars().next() {
760 Some(open @ ('[' | '<')) => {
761 let close = if open == '[' { ']' } else { '>' };
762 let mut depth = 0;
763 let end = declared.char_indices().find(|&(_, c)| {
764 depth += i32::from(c == open) - i32::from(c == close);
765 depth == 0
766 });
767 end.map_or("", |(end, _)| &declared[end + 1..])
768 }
769 _ => declared,
770 };
771 let declared = declared.trim_start().trim_start_matches('=').trim_start();
772 if language == "go" {
773 declared.starts_with("func")
774 } else {
775 FN_TYPES.iter().any(|fn_type| declared.contains(fn_type))
776 }
777}
778
779fn split_line<'a>(lines: &[&'a str], line: usize, column: usize) -> Option<(&'a str, &'a str)> {
781 lines.get(line.checked_sub(1)?)?.split_at_checked(column)
782}
783
784fn after_blanks_and_comments(mut text: &str) -> &str {
786 loop {
787 text = text.trim_start();
788 if text.starts_with("//") {
789 return "";
790 }
791 match text.strip_prefix("/*") {
792 Some(comment) => text = comment.split_once("*/").map_or("", |(_, rest)| rest),
793 None => return text,
794 }
795 }
796}
797
798fn before_blanks_and_comments(mut text: &str) -> &str {
800 loop {
801 text = text.trim_end();
802 match text
803 .strip_suffix("*/")
804 .and_then(|comment| comment.rsplit_once("/*"))
805 {
806 Some((rest, _)) => text = rest,
807 None => return text,
808 }
809 }
810}
811
812fn is_object_value(lines: &[&str], line: usize, column: usize, name: &str) -> bool {
817 let Some((before, rest)) = split_line(lines, line, column) else {
818 return false;
819 };
820 let after = rest.get(name.len()..).unwrap_or("");
821 before_blanks_and_comments(before)
822 .chars()
823 .next_back()
824 .is_none_or(|c| matches!(c, '{' | ',' | ':'))
825 && after_blanks_and_comments(after)
826 .chars()
827 .next()
828 .is_none_or(|c| matches!(c, ',' | '}'))
829}
830
831#[derive(Clone)]
832struct Definition {
833 symbol_id: String,
834 name: String,
835 path: String,
836 start_line: usize,
837 decorated_start_line: Option<usize>,
839 body_start_line: Option<usize>,
840 owner_name: Option<String>,
841 local: bool,
842 kind: String,
843 language: String,
844 signature: String,
845 class_name: Option<String>,
847 top_level: bool,
848 callable: bool,
851 free: bool,
854}
855
856impl Definition {
857 fn display(&self) -> String {
858 match &self.owner_name {
859 Some(owner) => format!("{owner}.{}", self.name),
860 None => self.name.clone(),
861 }
862 }
863
864 fn resolves_to(&self) -> String {
865 format!(
866 "resolves to `{}` ({}:{})",
867 self.display(),
868 self.path,
869 self.start_line
870 )
871 }
872}
873
874#[derive(Clone)]
875struct Verdict {
876 label: SiteLabel,
877 basis: String,
878 claimed: bool,
879}
880
881impl Verdict {
882 fn new(label: SiteLabel, basis: impl Into<String>, claimed: bool) -> Self {
883 Verdict {
884 label,
885 basis: basis.into(),
886 claimed,
887 }
888 }
889}
890
891struct SiteRow {
892 site: ReferenceSite,
893 label: SiteLabel,
894 basis: String,
895 claimed: bool,
896 end_line: Option<usize>,
897 arg_count: Option<u32>,
898}
899
900struct Claim {
901 basis: Option<String>,
902}
903
904struct Judged {
905 claimed: bool,
906 basis: Option<String>,
907}
908
909struct TextOutcome {
910 counts: ReferenceCounts,
911 text_only: Vec<TextOnlyLines>,
912 summary: TextScanSummary,
913}
914
915impl TextOutcome {
916 fn not_run() -> Self {
917 TextOutcome {
918 counts: ReferenceCounts::default(),
919 text_only: Vec::new(),
920 summary: TextScanSummary {
921 files_scanned: 0,
922 files_total: 0,
923 complete: true,
924 elapsed_ms: 0,
925 skipped: None,
926 },
927 }
928 }
929
930 fn skipped(class: &Symbol) -> Self {
931 TextOutcome {
932 counts: ReferenceCounts::default(),
933 text_only: Vec::new(),
934 summary: TextScanSummary {
935 files_scanned: 0,
936 files_total: 0,
937 complete: true,
938 elapsed_ms: 0,
939 skipped: Some(class.name.clone()),
940 },
941 }
942 }
943}
944
945type ImportMatch = (String, String, String);
947
948struct Accounting<'a> {
949 conn: &'a Connection,
950 root: &'a Path,
951 judge_deadline: Instant,
953 deadline: Instant,
954 judge_stmt: RefCell<Option<Statement<'a>>>,
955 absent: Option<Symbol>,
958 absent_judge_stmt: RefCell<Option<Statement<'a>>>,
959 definitions_stmt: RefCell<Option<Statement<'a>>>,
960 import_match_stmt: Option<Statement<'a>>,
961 statements: RefCell<HashMap<&'static str, Statement<'a>>>,
962 external_sources: HashMap<String, bool>,
963 import_matches: HashMap<(String, String, String), Vec<ImportMatch>>,
965 js: js_modules::JsCaches,
966 receivers: receiver_types::ReceiverCaches,
967}
968
969impl<'a> Accounting<'a> {
970 fn new(conn: &'a Connection, root: &'a Path, time_limit: Duration) -> Self {
971 Accounting {
972 conn,
973 root,
974 judge_deadline: Instant::now() + time_limit / 2,
975 deadline: Instant::now() + time_limit,
976 judge_stmt: RefCell::new(None),
977 absent: None,
978 absent_judge_stmt: RefCell::new(None),
979 definitions_stmt: RefCell::new(None),
980 import_match_stmt: None,
981 statements: RefCell::new(HashMap::new()),
982 external_sources: HashMap::new(),
983 import_matches: HashMap::new(),
984 js: js_modules::JsCaches::default(),
985 receivers: receiver_types::ReceiverCaches::default(),
986 }
987 }
988
989 fn out_of_time(&self) -> bool {
990 Instant::now() >= self.deadline
991 }
992
993 fn restart_clock(&mut self, time_limit: Duration) {
996 self.judge_deadline = Instant::now() + time_limit / 2;
997 self.deadline = Instant::now() + time_limit;
998 }
999
1000 fn with_statement<T>(
1003 &self,
1004 sql: &'static str,
1005 run: impl FnOnce(&mut Statement<'a>) -> rusqlite::Result<T>,
1006 ) -> Result<T, QueryError> {
1007 let mut statements = self.statements.borrow_mut();
1008 if !statements.contains_key(sql) {
1009 statements.insert(sql, self.conn.prepare(sql)?);
1010 }
1011 let stmt = statements
1012 .get_mut(sql)
1013 .expect("statement was just prepared");
1014 Ok(run(stmt)?)
1015 }
1016
1017 fn rows_at_path<T>(
1020 &self,
1021 sql: &'static str,
1022 path: &str,
1023 others: &[&dyn ToSql],
1024 read: fn(&Row) -> rusqlite::Result<T>,
1025 ) -> Result<Vec<T>, QueryError> {
1026 let windows = path.replace('/', "\\");
1027 let mut spellings = vec![path];
1028 if windows != path {
1029 spellings.push(&windows);
1030 }
1031 let mut rows = Vec::new();
1032 for spelling in spellings {
1033 let bound = std::iter::once(&spelling as &dyn ToSql).chain(others.iter().copied());
1034 rows.extend(self.with_statement(sql, |stmt| {
1035 stmt.query_map(params_from_iter(bound), read)?
1036 .collect::<Result<Vec<_>, _>>()
1037 })?);
1038 }
1039 Ok(rows)
1040 }
1041
1042 fn definitions_sql(&self) -> String {
1043 let decorated_start = if has_column(self.conn, "structural_facts", "end_byte")
1044 && has_column(self.conn, "symbols", "end_byte")
1045 {
1046 "(SELECT MIN(f.start_line) FROM structural_facts f
1047 WHERE f.path = s.path AND f.pattern_id LIKE '%.decorated_definition.%'
1048 AND f.end_byte = s.end_byte AND f.start_byte < s.start_byte)"
1049 } else {
1050 "NULL"
1051 };
1052 format!(
1053 "SELECT s.symbol_id, s.name, s.kind, s.language, s.path, s.start_line,
1054 s.body_start_line, s.parent_symbol_id, par.name, par.kind, {decorated_start},
1055 COALESCE(s.signature, ''), s.visibility,
1056 CASE WHEN json_valid(s.metadata_json) THEN
1057 json_extract(s.metadata_json, '$.className') END,
1058 CASE WHEN json_valid(s.metadata_json) THEN
1059 json_extract(s.metadata_json, '$.isCommonJSExport') END,
1060 COALESCE(par.signature, '')
1061 FROM symbols s LEFT JOIN symbols par ON par.symbol_id = s.parent_symbol_id
1062 WHERE s.name = ?1 AND s.kind NOT IN ('import', 'export')
1063 AND s.language NOT IN ({})",
1064 documentation_language_list()
1065 )
1066 }
1067
1068 fn definitions(&self, name: &str) -> Result<Vec<Definition>, QueryError> {
1069 let mut slot = self.definitions_stmt.borrow_mut();
1070 if slot.is_none() {
1071 *slot = Some(self.conn.prepare(&self.definitions_sql())?);
1072 }
1073 let stmt = slot
1074 .as_mut()
1075 .expect("definitions statement was just prepared");
1076 let rows = stmt.query_map(params![name], |row| {
1077 let signature: String = row.get(11)?;
1078 if is_test_label(&signature) {
1079 return Ok(None);
1080 }
1081 let kind: String = row.get(2)?;
1082 let language: String = row.get(3)?;
1083 let parent_id: Option<String> = row.get(7)?;
1084 let parent_kind: Option<String> = row.get(9)?;
1085 let parent_name: Option<String> = row.get(8)?;
1086 let class_name: Option<String> = row.get(13)?;
1087 let in_function = parent_kind
1088 .as_deref()
1089 .is_some_and(|k| matches!(k, "function" | "method" | "constructor"));
1090 let local = kind == "parameter" || kind == "variable" && in_function;
1091 let exported = row.get::<_, Option<String>>(12)?.as_deref() == Some("public")
1092 || row.get::<_, Option<i64>>(14)? == Some(1);
1093 let free = language_family(&language) == "js"
1094 && matches!(kind.as_str(), "function" | "variable" | "constant")
1095 && class_name.is_none()
1096 && !exported
1097 && !local
1098 && (parent_id.is_none() || in_function);
1099 let record_component = language == "java"
1100 && kind == "property"
1101 && is_record(&row.get::<_, String>(15)?)
1102 && !signature.split_whitespace().any(|word| word == "static");
1103 let function_typed = language == "rust"
1104 && matches!(kind.as_str(), "variable" | "constant")
1105 && FN_TYPES.iter().any(|fn_type| signature.contains(fn_type));
1106 let callable = record_component
1107 || function_typed
1108 || !(matches!(language.as_str(), "go" | "java" | "rust")
1109 && matches!(
1110 kind.as_str(),
1111 "field" | "property" | "variable" | "constant"
1112 ));
1113 Ok(Some(Definition {
1114 symbol_id: row.get(0)?,
1115 name: row.get(1)?,
1116 path: row.get::<_, String>(4)?.replace('\\', "/"),
1117 start_line: row.get::<_, i64>(5)? as usize,
1118 decorated_start_line: row.get::<_, Option<i64>>(10)?.map(|v| v as usize),
1119 body_start_line: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
1120 owner_name: parent_name.filter(|_| {
1121 parent_kind
1122 .as_deref()
1123 .is_some_and(|k| CLASS_LIKE_KINDS.contains(&k))
1124 }),
1125 local,
1126 top_level: parent_id.is_none(),
1127 kind,
1128 language,
1129 signature,
1130 class_name,
1131 callable,
1132 free,
1133 }))
1134 })?;
1135 let mut definitions: Vec<Definition> = rows
1136 .filter_map(Result::transpose)
1137 .collect::<Result<_, _>>()?;
1138 drop(slot);
1139 let writes =
1140 inherited_writes_among(self.conn, definitions.iter().map(|d| d.symbol_id.as_str()))?;
1141 definitions.retain(|d| !writes.contains(&d.symbol_id));
1142 let free_paths: HashSet<&str> = definitions
1143 .iter()
1144 .filter(|d| d.free)
1145 .map(|d| d.path.as_str())
1146 .collect();
1147 let member_paths = if free_paths.is_empty() {
1148 HashSet::new()
1149 } else {
1150 self.paths_using_as_member(name, &free_paths)?
1151 };
1152 for definition in &mut definitions {
1153 definition.free &= !member_paths.contains(&definition.path);
1154 if !definition.callable {
1155 definition.callable = match definition.language.as_str() {
1156 "go" => self.holds_func(definition)?,
1157 "rust" => self.holds_fn_alias(definition)?,
1158 _ => false,
1159 };
1160 }
1161 }
1162 Ok(definitions)
1163 }
1164
1165 fn paths_using_as_member(
1168 &self,
1169 name: &str,
1170 paths: &HashSet<&str>,
1171 ) -> Result<HashSet<String>, QueryError> {
1172 if !crate::queries::has_table(self.conn, "identifiers") {
1173 return Ok(HashSet::new());
1174 }
1175 let normalized = |path: String| path.replace('\\', "/");
1176 let mut found: HashSet<String> = self.with_statement(MEMBER_ACCESS_PATHS_SQL, |stmt| {
1177 stmt.query_map(params![name], |r| r.get::<_, String>(0))?
1178 .map(|path| path.map(normalized))
1179 .collect()
1180 })?;
1181 let mut sites: HashMap<String, Vec<(usize, usize)>> = HashMap::new();
1182 let rows = self.with_statement(VARIABLE_REF_SITES_SQL, |stmt| {
1183 stmt.query_map(params![name], |r| {
1184 Ok((
1185 normalized(r.get::<_, String>(0)?),
1186 r.get::<_, i64>(1)? as usize,
1187 r.get::<_, i64>(2)? as usize,
1188 ))
1189 })?
1190 .collect::<Result<Vec<_>, _>>()
1191 })?;
1192 for (path, line, column) in rows {
1193 if paths.contains(path.as_str()) && !found.contains(&path) {
1194 sites.entry(path).or_default().push((line, column));
1195 }
1196 }
1197 for (path, sites) in sites {
1198 let Ok(text) = std::fs::read_to_string(self.root.join(&path)) else {
1199 continue;
1200 };
1201 let lines: Vec<&str> = text.lines().collect();
1202 if sites
1203 .iter()
1204 .any(|&(line, column)| is_object_value(&lines, line, column, name))
1205 {
1206 found.insert(path);
1207 }
1208 }
1209 Ok(found)
1210 }
1211
1212 fn holds_func(&self, definition: &Definition) -> Result<bool, QueryError> {
1215 if definition.kind == "constant" {
1216 return Ok(false);
1217 }
1218 let signature = definition.signature.trim_start();
1219 let signature = ["var ", "const "]
1220 .iter()
1221 .find_map(|keyword| signature.strip_prefix(keyword))
1222 .unwrap_or(signature);
1223 let declared = signature
1224 .trim_start()
1225 .strip_prefix(definition.name.as_str())
1226 .filter(|rest| !rest.starts_with(|c: char| c.is_alphanumeric() || c == '_'))
1227 .map_or("", str::trim_start);
1228 if declared.is_empty() || declared.starts_with('=') {
1229 return Ok(true);
1230 }
1231 let type_name: String = declared
1232 .chars()
1233 .take_while(|c| c.is_alphanumeric() || *c == '_')
1234 .collect();
1235 if type_name.is_empty() {
1236 return Ok(false);
1237 }
1238 if type_name == "func" || declared[type_name.len()..].starts_with('.') {
1239 return Ok(true);
1240 }
1241 self.declares_function_type(&type_name, "go")
1242 }
1243
1244 fn holds_fn_alias(&self, definition: &Definition) -> Result<bool, QueryError> {
1247 let declared = definition
1248 .signature
1249 .split_once(definition.name.as_str())
1250 .and_then(|(_, rest)| rest.trim_start().strip_prefix(':'))
1251 .and_then(|declared| declared.split(['=', ';']).next())
1252 .and_then(|declared| declared.rsplit("::").next())
1253 .map(str::trim);
1254 match declared {
1255 Some(type_name) if is_plain_identifier(type_name) => {
1256 self.declares_function_type(type_name, "rust")
1257 }
1258 _ => Ok(false),
1259 }
1260 }
1261
1262 fn declares_function_type(&self, type_name: &str, language: &str) -> Result<bool, QueryError> {
1264 let signatures = self.with_statement(TYPE_DECLARATIONS_SQL, |stmt| {
1265 stmt.query_map(params![type_name, language], |r| r.get::<_, String>(0))?
1266 .collect::<Result<Vec<_>, _>>()
1267 })?;
1268 Ok(signatures
1269 .iter()
1270 .any(|signature| is_function_type(signature, type_name, language)))
1271 }
1272
1273 fn definition_count(&self, name: &str) -> Result<usize, QueryError> {
1274 Ok(self.definitions(name)?.iter().filter(|d| !d.local).count())
1275 }
1276
1277 fn account_spec(
1278 &mut self,
1279 spec: &Spec,
1280 out: &mut Vec<SiteRow>,
1281 cannot_see: &mut Vec<String>,
1282 ) -> Result<(), QueryError> {
1283 let definitions = self.definitions(&spec.name)?;
1284 let group_ids: HashSet<&str> = spec.group.iter().map(|s| s.symbol_id.as_str()).collect();
1285 let target = &spec.group[0];
1286
1287 let mut rows = self.load_rows(&spec.name, &definitions, spec.scope)?;
1288 pair_identifiers(&mut rows);
1289 rows.retain(|row| !row.consumed);
1290 mark_hidden_receivers(self.root, &mut rows);
1291 js_modules::complete_js_chains(self.root, &mut rows);
1292
1293 let mut order: Vec<usize> = (0..rows.len()).collect();
1294 order.sort_by(|&a, &b| {
1295 let (a, b) = (&rows[a], &rows[b]);
1296 shared_leading_segments(&b.path, &target.path)
1297 .cmp(&shared_leading_segments(&a.path, &target.path))
1298 .then_with(|| a.path.cmp(&b.path))
1299 .then_with(|| a.line.cmp(&b.line))
1300 });
1301 let (claims, judged) = self.claims(spec, &rows, &order, cannot_see)?;
1302 let mut unchecked: Vec<usize> = order[judged..].to_vec();
1303 let mut verdicts: HashMap<String, Verdict> = HashMap::new();
1304 let mut undecided: Vec<usize> = Vec::new();
1305 for (position, &index) in order[..judged].iter().enumerate() {
1306 if self.out_of_time() {
1307 unchecked.extend_from_slice(&order[position..judged]);
1308 break;
1309 }
1310 match self.decide_cheap(&rows[index], spec, target, &group_ids, &claims)? {
1311 Some(verdict) => {
1312 verdicts.insert(rows[index].key.clone(), verdict);
1313 }
1314 None => undecided.push(index),
1315 }
1316 }
1317
1318 let mut others: Vec<&Definition> = definitions
1319 .iter()
1320 .filter(|d| !d.local && !group_ids.contains(d.symbol_id.as_str()))
1321 .filter(|d| !(spec.scope == RowScope::Inheritance && d.kind == "constructor"))
1322 .collect();
1323 others.sort_by_key(|d| {
1324 (
1325 std::cmp::Reverse(shared_leading_segments(&d.path, &target.path)),
1326 d.path.clone(),
1327 d.start_line,
1328 )
1329 });
1330 let mut checked = 0;
1331 for other in &others {
1332 if undecided.is_empty() || checked >= OTHER_DEFINITION_CAP || self.out_of_time() {
1333 break;
1334 }
1335 let mut kept = Vec::with_capacity(undecided.len());
1336 let mut complete = true;
1337 for chunk in undecided.chunks(JUDGE_CHUNK) {
1338 if self.out_of_time() {
1339 complete = false;
1340 kept.extend_from_slice(chunk);
1341 continue;
1342 }
1343 let chunk_rows: Vec<&NameRow> = chunk.iter().map(|&index| &rows[index]).collect();
1344 let judged = self.judge(&spec.name, &other.symbol_id, &chunk_rows)?;
1345 for &index in chunk {
1346 let row = &rows[index];
1347 if row.hidden_receiver
1348 || row.language == "java" && !row.can_reach(other)
1349 || row.reads_a_module(&other.kind)
1350 {
1351 kept.push(index);
1352 continue;
1353 }
1354 let Some(Judged {
1355 claimed: true,
1356 basis: Some(basis),
1357 }) = judged.get(&row.key)
1358 else {
1359 kept.push(index);
1360 continue;
1361 };
1362 if basis == IMPORT_BASIS
1363 && row.language == "python"
1364 && !self.python_import_reaches(row, &spec.name, &other.path)?
1365 {
1366 kept.push(index);
1367 continue;
1368 }
1369 verdicts.insert(
1370 row.key.clone(),
1371 Verdict::new(SiteLabel::Excluded, other.resolves_to(), false),
1372 );
1373 }
1374 }
1375 undecided = kept;
1376 if !complete {
1377 break;
1378 }
1379 checked += 1;
1380 }
1381 let unchecked_others = others.len() - checked;
1382 if !undecided.is_empty() && unchecked_others > 0 {
1383 cannot_see.push(if unchecked_others == 1 {
1384 "1 other definition with this name was not checked".to_string()
1385 } else {
1386 format!("{unchecked_others} other definitions with this name were not checked")
1387 });
1388 }
1389
1390 let group_definitions: Vec<&Definition> = definitions
1391 .iter()
1392 .filter(|d| group_ids.contains(d.symbol_id.as_str()))
1393 .collect();
1394 let rivals_of = |row: &NameRow| -> Vec<&Definition> {
1395 others
1396 .iter()
1397 .copied()
1398 .filter(|other| row.can_reach(other))
1399 .collect()
1400 };
1401 for index in undecided {
1402 let row = &rows[index];
1403 let rivals = rivals_of(row);
1404 let verdict = match claims.get(&row.key) {
1405 Some(claim) => claimed_verdict(row, claim, rivals.len(), spec.builds_class),
1406 None if self.out_of_time() => {
1407 unchecked.push(index);
1408 continue;
1409 }
1410 None => self.decide_late(row, &spec.group, &group_definitions, &rivals)?,
1411 };
1412 verdicts.insert(row.key.clone(), verdict);
1413 }
1414 for index in unchecked {
1415 let rivals = rivals_of(&rows[index]).len();
1416 let verdict = if rivals == 0 {
1417 Verdict::new(
1418 SiteLabel::Likely,
1419 format!("name only; {NOT_CHECKED}"),
1420 false,
1421 )
1422 } else {
1423 Verdict::new(
1424 SiteLabel::Unresolved,
1425 format!("{} definitions; {NOT_CHECKED}", rivals + 1),
1426 false,
1427 )
1428 };
1429 verdicts.insert(rows[index].key.clone(), verdict);
1430 }
1431
1432 for row in &rows {
1433 let Some(mut verdict) = verdicts.remove(&row.key) else {
1434 continue;
1435 };
1436 if matches!(verdict.label, SiteLabel::Likely | SiteLabel::Unresolved)
1437 && let Some(arity) = &spec.arity
1438 && let Some(count) = row.arg_count
1439 && let Some(basis) = arity.exclusion_basis(&spec.name, &row.language, count)
1440 {
1441 verdict = Verdict::new(SiteLabel::Excluded, basis, false);
1442 }
1443 if verdict.label == SiteLabel::Proven
1444 && NAME_RESOLVED_BASES.contains(&verdict.basis.as_str())
1445 && let Some(line) = self.js_binding_line(row, &spec.name)?
1446 {
1447 verdict.label = SiteLabel::Likely;
1448 verdict.basis = format!(
1449 "{}; line {line} binds the name as a callback parameter, loop variable, or catch parameter",
1450 verdict.basis
1451 );
1452 }
1453 out.push(site_row(row, verdict, spec));
1454 }
1455 if spec.scope == RowScope::All {
1456 for member in &spec.group {
1457 for grouped in
1458 grouped_member_access_rows(self.conn, &member.symbol_id, RESOLVER_ROW_CAP)?
1459 {
1460 out.push(SiteRow {
1461 end_line: None,
1462 arg_count: None,
1463 label: SiteLabel::Proven,
1464 basis: "qualified path".to_string(),
1465 claimed: true,
1466 site: grouped,
1467 });
1468 }
1469 }
1470 }
1471 Ok(())
1472 }
1473
1474 fn inheritance_labels(
1478 &mut self,
1479 owner: &Symbol,
1480 ) -> Result<HashMap<String, (SiteLabel, String)>, QueryError> {
1481 let spec = Spec {
1482 name: owner.name.clone(),
1483 group: vec![owner.clone()],
1484 scope: RowScope::Inheritance,
1485 builds_class: false,
1486 arity: None,
1487 local_to: None,
1488 };
1489 let mut rows = Vec::new();
1490 self.account_spec(&spec, &mut rows, &mut Vec::new())?;
1491 let mut labels: HashMap<String, (SiteLabel, String)> = HashMap::new();
1492 for row in rows {
1493 let better = labels
1494 .get(&row.site.from_symbol_id)
1495 .is_none_or(|(label, _)| row.label.rank() < label.rank());
1496 if better {
1497 labels.insert(row.site.from_symbol_id, (row.label, row.basis));
1498 }
1499 }
1500 Ok(labels)
1501 }
1502
1503 fn load_rows(
1504 &self,
1505 name: &str,
1506 definitions: &[Definition],
1507 scope: RowScope,
1508 ) -> Result<Vec<NameRow>, QueryError> {
1509 let docs = documentation_language_list();
1510 let inheritance = scope == RowScope::Inheritance;
1511 let kinds = if inheritance {
1512 "AND p.kind IN ('extends', 'implements')"
1513 } else {
1514 ""
1515 };
1516 let mut rows = Vec::new();
1517
1518 let mut stmt = self.conn.prepare(&format!(
1519 "SELECT p.rowid, p.from_symbol_id, COALESCE(s.name, ''), p.kind, p.path,
1520 COALESCE(f.language, s.language, ''), p.start_line, p.start_column,
1521 p.end_line, p.target_receiver, p.target_namespace_json, s.end_line
1522 FROM pending_relationships p
1523 LEFT JOIN symbols s ON s.symbol_id = p.from_symbol_id
1524 LEFT JOIN files f ON f.file_id = p.file_id
1525 WHERE p.target_terminal_name = ?1 AND p.kind != 'imports' {kinds}
1526 AND COALESCE(f.language, s.language, '') NOT IN ({docs})"
1527 ))?;
1528 let mut query = stmt.query(params![name])?;
1529 while let Some(row) = query.next()? {
1530 let namespace: Vec<String> = row
1531 .get::<_, Option<String>>(10)?
1532 .and_then(|json| serde_json::from_str(&json).ok())
1533 .unwrap_or_default();
1534 let receiver: Option<String> = row.get(9)?;
1535 let chain = namespace
1536 .iter()
1537 .chain(receiver.as_ref().filter(|r| !r.is_empty()))
1538 .cloned()
1539 .collect();
1540 rows.push(NameRow {
1541 key: format!("P:{}", row.get::<_, i64>(0)?),
1542 source: Source::Pending,
1543 caller_id: row.get(1)?,
1544 caller_name: row.get(2)?,
1545 caller_end_line: row.get::<_, Option<i64>>(11)?.map(|v| v as usize),
1546 path: row.get::<_, String>(4)?.replace('\\', "/"),
1547 language: row.get(5)?,
1548 kind: row.get(3)?,
1549 line: row.get::<_, i64>(6)? as usize,
1550 column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
1551 end_line: row.get::<_, Option<i64>>(8)?.map(|v| v as usize),
1552 receiver,
1553 namespace,
1554 to_symbol_id: None,
1555 site_line: row.get::<_, i64>(6)? as usize,
1556 site_column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
1557 consumed: false,
1558 paired: false,
1559 hidden_receiver: false,
1560 chain: Some(chain),
1561 arg_count: None,
1562 links_enclosing_class: false,
1563 });
1564 }
1565
1566 if !inheritance {
1567 let mut stmt = self.conn.prepare(&format!(
1568 "SELECT r.rowid, r.from_symbol_id, COALESCE(s.name, ''), r.kind, r.path,
1569 COALESCE(f.language, s.language, ''), r.start_line, r.start_column,
1570 r.end_line, r.to_symbol_id, {LINKS_ENCLOSING_CLASS_SQL}, s.end_line
1571 FROM relationships r
1572 JOIN symbols t ON t.symbol_id = r.to_symbol_id
1573 LEFT JOIN symbols s ON s.symbol_id = r.from_symbol_id
1574 LEFT JOIN files f ON f.file_id = r.file_id
1575 WHERE t.name = ?1 AND r.kind != 'imports'
1576 AND COALESCE(f.language, s.language, '') NOT IN ({docs})"
1577 ))?;
1578 let mut query = stmt.query(params![name])?;
1579 while let Some(row) = query.next()? {
1580 let line = row.get::<_, Option<i64>>(6)?.unwrap_or(0) as usize;
1581 rows.push(NameRow {
1582 key: format!("R:{}", row.get::<_, i64>(0)?),
1583 source: Source::Relationship,
1584 caller_id: row.get(1)?,
1585 caller_name: row.get(2)?,
1586 caller_end_line: row.get::<_, Option<i64>>(11)?.map(|v| v as usize),
1587 path: row.get::<_, String>(4)?.replace('\\', "/"),
1588 language: row.get(5)?,
1589 kind: row.get(3)?,
1590 line,
1591 column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
1592 end_line: row.get::<_, Option<i64>>(8)?.map(|v| v as usize),
1593 receiver: None,
1594 namespace: Vec::new(),
1595 to_symbol_id: row.get(9)?,
1596 site_line: line,
1597 site_column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
1598 consumed: false,
1599 paired: false,
1600 hidden_receiver: false,
1601 chain: None,
1602 arg_count: None,
1603 links_enclosing_class: row.get(10)?,
1604 });
1605 }
1606 }
1607
1608 if !inheritance && crate::queries::has_table(self.conn, "identifiers") {
1609 let mut stmt = self.conn.prepare(&format!(
1610 "SELECT i.identifier_id, i.containing_symbol_id, COALESCE(s.name, ''), i.kind,
1611 i.path, i.language, i.start_line, i.start_column,
1612 CASE WHEN json_valid(i.metadata_json)
1613 THEN json_extract(i.metadata_json, '$.receiver') END,
1614 CASE WHEN json_valid(i.metadata_json)
1615 THEN json_extract(i.metadata_json, '$.receiver_qualifier') END,
1616 CASE WHEN json_valid(i.metadata_json)
1617 THEN json_extract(i.metadata_json, '$.arg_count') END,
1618 CASE WHEN json_valid(i.metadata_json)
1619 THEN json_extract(i.metadata_json, '$.arg_spread') END IS NOT NULL
1620 FROM identifiers i
1621 LEFT JOIN symbols s ON s.symbol_id = i.containing_symbol_id
1622 WHERE i.name = ?1 AND COALESCE(s.kind, '') != 'import'
1623 AND i.language NOT IN ({docs})"
1624 ))?;
1625 let mut query = stmt.query(params![name])?;
1626 while let Some(row) = query.next()? {
1627 let line = row.get::<_, i64>(6)? as usize;
1628 let kind: String = row.get(3)?;
1629 let receiver: Option<String> = row.get(8)?;
1630 let qualifier: Option<String> = row.get(9)?;
1631 let arg_count: Option<u32> = row.get(10)?;
1632 let arg_spread: bool = row.get(11)?;
1633 let written = receiver.as_deref().filter(|r| !r.is_empty());
1634 let chain = match kind.as_str() {
1635 "type_usage" => None,
1636 "member_access" if written.is_none() => None,
1637 _ => Some(
1638 qualifier
1639 .iter()
1640 .flat_map(|q| q.split('.'))
1641 .chain(written)
1642 .map(str::to_string)
1643 .collect(),
1644 ),
1645 };
1646 rows.push(NameRow {
1647 key: format!("I:{}", row.get::<_, String>(0)?),
1648 source: Source::Identifier,
1649 caller_id: row.get(1)?,
1650 caller_name: row.get(2)?,
1651 caller_end_line: None,
1652 path: row.get::<_, String>(4)?.replace('\\', "/"),
1653 language: row.get(5)?,
1654 kind,
1655 line,
1656 column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
1657 end_line: None,
1658 receiver,
1659 namespace: Vec::new(),
1660 to_symbol_id: None,
1661 site_line: line,
1662 site_column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
1663 consumed: false,
1664 paired: false,
1665 hidden_receiver: false,
1666 chain,
1667 arg_count: arg_count.filter(|_| !arg_spread),
1668 links_enclosing_class: false,
1669 });
1670 }
1671 }
1672
1673 let import_spans = self.import_spans(name)?;
1674 let definition_spans = definition_spans(definitions, None);
1675 rows.retain(|row| {
1676 if scope == RowScope::Calls && !row.is_call() {
1677 return false;
1678 }
1679 if in_spans(&import_spans, &row.path, row.line) {
1680 return false;
1681 }
1682 !(row.source == Source::Identifier
1683 && !row.is_super_call()
1684 && in_spans(&definition_spans, &row.path, row.line))
1685 });
1686 Ok(rows)
1687 }
1688
1689 fn import_spans(&self, name: &str) -> Result<HashMap<String, Vec<(usize, usize)>>, QueryError> {
1690 let mut spans: HashMap<String, Vec<(usize, usize)>> = HashMap::new();
1691 let mut add = |path: String, start: i64, end: Option<i64>| {
1692 let start = start as usize;
1693 let end = end.map_or(start, |e| (e as usize).max(start));
1694 spans
1695 .entry(path.replace('\\', "/"))
1696 .or_default()
1697 .push((start, end));
1698 };
1699 let docs = documentation_language_list();
1700 let dotted = format!("\".{}\"", name.replace('"', "\"\""));
1703 let sql = if crate::queries::has_table(self.conn, "symbol_names_tri")
1704 && name.chars().count() >= 2
1705 {
1706 format!(
1707 "SELECT path, start_line, end_line FROM symbols
1708 WHERE kind = 'import' AND language NOT IN ({docs}) AND name = ?1
1709 UNION ALL
1710 SELECT s.path, s.start_line, s.end_line FROM symbol_names_tri
1711 CROSS JOIN symbols s ON s.rowid = symbol_names_tri.rowid
1712 WHERE symbol_names_tri MATCH ?2 AND s.kind = 'import'
1713 AND s.language NOT IN ({docs})
1714 AND substr(s.name, -length(?1) - 1) = '.' || ?1"
1715 )
1716 } else {
1717 format!(
1718 "SELECT path, start_line, end_line FROM symbols
1719 WHERE kind = 'import' AND language NOT IN ({docs})
1720 AND (name = ?1 OR substr(name, -length(?1) - 1) = '.' || ?1)
1721 AND ?2 = ?2"
1722 )
1723 };
1724 let mut stmt = self.conn.prepare(&sql)?;
1725 for row in stmt.query_map(params![name, dotted], |row| {
1726 Ok((
1727 row.get::<_, String>(0)?,
1728 row.get::<_, i64>(1)?,
1729 row.get::<_, Option<i64>>(2)?,
1730 ))
1731 })? {
1732 let (path, start, end) = row?;
1733 add(path, start, end);
1734 }
1735 let mut stmt = self.conn.prepare(
1736 "SELECT path, start_line, end_line FROM pending_relationships
1737 WHERE target_terminal_name = ?1 AND kind = 'imports'",
1738 )?;
1739 for row in stmt.query_map(params![name], |row| {
1740 Ok((
1741 row.get::<_, String>(0)?,
1742 row.get::<_, i64>(1)?,
1743 row.get::<_, Option<i64>>(2)?,
1744 ))
1745 })? {
1746 let (path, start, end) = row?;
1747 add(path, start, end);
1748 }
1749 let mut stmt = self.conn.prepare(
1750 "SELECT r.path, r.start_line, r.end_line FROM relationships r
1751 JOIN symbols t ON t.symbol_id = r.to_symbol_id
1752 WHERE t.name = ?1 AND r.kind = 'imports' AND r.start_line IS NOT NULL",
1753 )?;
1754 for row in stmt.query_map(params![name], |row| {
1755 Ok((
1756 row.get::<_, String>(0)?,
1757 row.get::<_, i64>(1)?,
1758 row.get::<_, Option<i64>>(2)?,
1759 ))
1760 })? {
1761 let (path, start, end) = row?;
1762 add(path, start, end);
1763 }
1764 Ok(spans)
1765 }
1766
1767 fn judge(
1769 &self,
1770 name: &str,
1771 target_id: &str,
1772 rows: &[&NameRow],
1773 ) -> Result<HashMap<String, Judged>, QueryError> {
1774 let ids_of = |source: Source| -> String {
1775 let ids: Vec<&str> = rows
1776 .iter()
1777 .filter(|row| row.source == source)
1778 .filter_map(|row| row.key.split_once(':').map(|(_, id)| id))
1779 .collect();
1780 serde_json::to_string(&ids).unwrap_or_else(|_| "[]".to_string())
1781 };
1782 let (pending, identifiers) = (ids_of(Source::Pending), ids_of(Source::Identifier));
1783 let absent = self
1784 .absent
1785 .as_ref()
1786 .filter(|symbol| symbol.symbol_id == target_id);
1787 let mut slot = if absent.is_some() {
1788 self.absent_judge_stmt.borrow_mut()
1789 } else {
1790 self.judge_stmt.borrow_mut()
1791 };
1792 if slot.is_none() {
1793 *slot = Some(self.conn.prepare(&judge_sql(self.conn, absent.is_some()))?);
1794 }
1795 let stmt = slot.as_mut().expect("judge statement was just prepared");
1796 let mut rows = match absent {
1797 Some(symbol) => stmt.query(params![
1798 name,
1799 target_id,
1800 pending,
1801 identifiers,
1802 symbol.name,
1803 symbol.kind,
1804 symbol.language,
1805 symbol.path,
1806 symbol.start_line as i64,
1807 symbol.parent_symbol_id
1808 ])?,
1809 None => stmt.query(params![name, target_id, pending, identifiers])?,
1810 };
1811 let mut judged = HashMap::new();
1812 while let Some(row) = rows.next()? {
1813 let source: String = row.get(0)?;
1814 let id: String = row.get(1)?;
1815 judged.insert(
1816 format!("{source}:{id}"),
1817 Judged {
1818 claimed: row.get::<_, i64>(2)? != 0,
1819 basis: row.get(3)?,
1820 },
1821 );
1822 }
1823 Ok(judged)
1824 }
1825
1826 fn claims(
1829 &self,
1830 spec: &Spec,
1831 rows: &[NameRow],
1832 order: &[usize],
1833 cannot_see: &mut Vec<String>,
1834 ) -> Result<(HashMap<String, Claim>, usize), QueryError> {
1835 let mut claims: HashMap<String, Claim> = HashMap::new();
1836 let mut resolvers = Vec::with_capacity(spec.group.len());
1837 for member in &spec.group {
1838 let resolver_ids: HashSet<String> = if spec.scope == RowScope::Inheritance {
1839 HashSet::new()
1840 } else {
1841 direct_identifier_rows(
1842 self.conn,
1843 &spec.name,
1844 Some(&member.symbol_id),
1845 RESOLVER_ROW_CAP,
1846 )?
1847 .into_iter()
1848 .map(|row| format!("I:{}", row.identifier_id))
1849 .collect()
1850 };
1851 if resolver_ids.len() >= RESOLVER_ROW_CAP {
1852 cannot_see.push(format!(
1853 "the resolver listed {RESOLVER_ROW_CAP} identifier rows and may hold more; rows past that limit have no resolver evidence"
1854 ));
1855 }
1856 resolvers.push(resolver_ids);
1857 }
1858 let mut judged_rows = 0;
1859 for chunk in order.chunks(JUDGE_CHUNK) {
1860 if Instant::now() >= self.judge_deadline {
1861 break;
1862 }
1863 let chunk_rows: Vec<&NameRow> = chunk
1864 .iter()
1865 .map(|&index| &rows[index])
1866 .filter(|row| !row.hidden_receiver)
1867 .collect();
1868 for (member, resolver_ids) in spec.group.iter().zip(&resolvers) {
1869 let judged = self.judge(&spec.name, &member.symbol_id, &chunk_rows)?;
1870 for row in chunk_rows.iter().copied() {
1871 let judged_row = judged.get(&row.key);
1872 let claimed = match row.source {
1873 Source::Relationship => row
1874 .to_symbol_id
1875 .as_deref()
1876 .is_some_and(|id| id == member.symbol_id),
1877 Source::Identifier
1878 if matches!(row.kind.as_str(), "type_usage" | "member_access") =>
1879 {
1880 resolver_ids.contains(&row.key)
1881 }
1882 _ => judged_row.is_some_and(|j| j.claimed),
1883 };
1884 if !claimed {
1885 continue;
1886 }
1887 let basis = match row.source {
1888 Source::Relationship if row.caller_id == row.to_symbol_id => None,
1890 Source::Relationship => Some("same file".to_string()),
1891 _ => judged_row.and_then(|j| j.basis.clone()),
1892 };
1893 let basis = match basis {
1894 Some(basis)
1895 if basis == IMPORT_BASIS
1896 && row.language == "python"
1897 && !self.python_import_reaches(
1898 row,
1899 &spec.name,
1900 &member.path,
1901 )? =>
1902 {
1903 None
1904 }
1905 basis => basis,
1906 };
1907 let entry = claims
1908 .entry(row.key.clone())
1909 .or_insert(Claim { basis: None });
1910 if entry.basis.is_none() {
1911 entry.basis = basis;
1912 }
1913 }
1914 }
1915 judged_rows += chunk.len();
1916 }
1917 Ok((claims, judged_rows))
1918 }
1919
1920 fn decide_cheap(
1921 &mut self,
1922 row: &NameRow,
1923 spec: &Spec,
1924 target: &Symbol,
1925 group_ids: &HashSet<&str>,
1926 claims: &HashMap<String, Claim>,
1927 ) -> Result<Option<Verdict>, QueryError> {
1928 if !languages_compatible(&row.language, &target.language) {
1929 return Ok(Some(Verdict::new(
1930 SiteLabel::Excluded,
1931 "other language",
1932 false,
1933 )));
1934 }
1935 if spec
1936 .group
1937 .iter()
1938 .all(|member| member.signature.as_deref().is_some_and(is_test_label))
1939 {
1940 return Ok(Some(Verdict::new(
1941 SiteLabel::Excluded,
1942 format!("`{}` is a test label, which code never names", spec.name),
1943 false,
1944 )));
1945 }
1946 if spec
1947 .group
1948 .iter()
1949 .all(|member| row.reads_a_module(&member.kind))
1950 {
1951 return Ok(Some(Verdict::new(
1952 SiteLabel::Excluded,
1953 format!(
1954 "`{}` is a module, which a read of a value never names",
1955 spec.name
1956 ),
1957 false,
1958 )));
1959 }
1960 if let Some(outer) = &spec.local_to
1961 && !(row.path == outer.path.replace('\\', "/")
1962 && (outer.start_line..=outer.end_line).contains(&row.site_line))
1963 {
1964 return Ok(Some(Verdict::new(
1965 SiteLabel::Excluded,
1966 format!("`{}` is local to `{}`", spec.name, outer.name),
1967 false,
1968 )));
1969 }
1970 if row.language == "java"
1973 && row.kind == "member_access"
1974 && spec
1975 .group
1976 .iter()
1977 .all(|member| member.language == "java" && member.kind == "method")
1978 {
1979 return Ok(Some(Verdict::new(
1980 SiteLabel::Excluded,
1981 format!(
1982 "a member access reads a field, not the method `{}`",
1983 spec.name
1984 ),
1985 false,
1986 )));
1987 }
1988 if let Some(shadow) = self.nearest_shadow(row, &spec.name, group_ids)? {
1989 let basis = match self.js_binding_line(row, &spec.name)? {
1990 Some(line) if line > shadow.start_line || shadow.start_line > row.site_line => {
1991 format!(
1992 "line {line} binds the name as a callback parameter, loop variable, or catch parameter"
1993 )
1994 }
1995 _ => shadow.resolves_to(),
1996 };
1997 return Ok(Some(Verdict::new(SiteLabel::Excluded, basis, false)));
1998 }
1999 if let Some(verdict) = self.java_static_import(row, spec)? {
2000 return Ok(Some(verdict));
2001 }
2002 if row.is_bare_use() && spec.group.iter().all(is_property_method) {
2003 return Ok(Some(self.bare_use_of_property_method(row, &spec.name)?));
2004 }
2005 if let Some(verdict) = self.js_verdict(row, spec)? {
2006 return Ok(Some(verdict));
2007 }
2008 if let Some(claim) = claims.get(&row.key)
2009 && let Some(basis) = &claim.basis
2010 {
2011 if basis == IMPORT_BASIS {
2012 if language_family(&row.language) == "js"
2013 && let Some(verdict) = self.external_import(row, &spec.name)?
2014 {
2015 return Ok(Some(verdict));
2016 }
2017 let others = self.other_import_matches(row, &spec.name, group_ids)?;
2018 if others > 0 {
2019 return Ok(Some(Verdict::new(
2020 SiteLabel::Unresolved,
2021 format!("the import matches {} definitions", others + 1),
2022 true,
2023 )));
2024 }
2025 }
2026 return Ok(Some(claimed_verdict(row, claim, 0, spec.builds_class)));
2027 }
2028 if self.crate_path_names_owner(row, &spec.group)? {
2029 return Ok(Some(Verdict::new(
2030 SiteLabel::Proven,
2031 "qualified path",
2032 false,
2033 )));
2034 }
2035 if let Some(to) = row.to_symbol_id.as_deref()
2036 && !row.hidden_receiver
2037 && !group_ids.contains(to)
2038 && let Some(other) = self.definition_by_id(to)?
2039 {
2040 return Ok(Some(Verdict::new(
2041 SiteLabel::Excluded,
2042 other.resolves_to(),
2043 false,
2044 )));
2045 }
2046 self.external_import(row, &spec.name)
2047 }
2048
2049 fn java_static_import(
2053 &self,
2054 row: &NameRow,
2055 spec: &Spec,
2056 ) -> Result<Option<Verdict>, QueryError> {
2057 if row.language != "java" || !row.is_call() || row.has_qualifier() {
2058 return Ok(None);
2059 }
2060 let signatures = self.with_statement(JAVA_STATIC_IMPORT_SQL, |stmt| {
2061 stmt.query_map(params![row.path, spec.name], |r| r.get::<_, String>(0))?
2062 .collect::<Result<Vec<_>, _>>()
2063 })?;
2064 let Some(class_path) = signatures.iter().find_map(|signature| {
2065 let imported = signature
2066 .strip_prefix("import static ")?
2067 .trim_end_matches(';');
2068 Some(imported.trim().rsplit_once('.')?.0.to_string())
2069 }) else {
2070 return Ok(None);
2071 };
2072 let class_name = class_path.rsplit('.').next().unwrap_or(&class_path);
2073 let mut chain = HashSet::new();
2074 if let Some(caller) = row.caller_id.as_deref()
2075 && let Some(class) = self.definition_by_id(caller)?.and_then(|d| d.owner_name)
2076 {
2077 chain = self.ancestor_names(&class)?;
2078 chain.insert(class);
2079 }
2080 let present = spec.group.iter().filter(|member| {
2081 self.absent
2082 .as_ref()
2083 .is_none_or(|absent| absent.symbol_id != member.symbol_id)
2084 });
2085 for member in present {
2086 if member.path.replace('\\', "/") == row.path {
2087 return Ok(None);
2088 }
2089 let owner = self
2090 .definition_by_id(&member.symbol_id)?
2091 .and_then(|d| d.owner_name);
2092 if owner.is_some_and(|owner| owner == class_name || chain.contains(&owner)) {
2093 return Ok(None);
2094 }
2095 }
2096 Ok(Some(Verdict::new(
2097 SiteLabel::Excluded,
2098 format!("`{}` is imported from `{class_path}`", spec.name),
2099 false,
2100 )))
2101 }
2102
2103 fn external_import(
2104 &mut self,
2105 row: &NameRow,
2106 name: &str,
2107 ) -> Result<Option<Verdict>, QueryError> {
2108 Ok(self.external_binding(row, name)?.map(|(binding, source)| {
2109 Verdict::new(
2110 SiteLabel::Excluded,
2111 format!("receiver `{binding}` is imported from `{source}`"),
2112 false,
2113 )
2114 }))
2115 }
2116
2117 fn crate_path_names_owner(&self, row: &NameRow, group: &[Symbol]) -> Result<bool, QueryError> {
2121 if row.language != "rust"
2122 || row.source != Source::Pending
2123 || row.kind != "calls"
2124 || row.namespace.len() < 2
2125 {
2126 return Ok(false);
2127 }
2128 let Some((owner_segment, leading)) = row.namespace.split_last() else {
2129 return Ok(false);
2130 };
2131 if matches!(leading[0].as_str(), "crate" | "self" | "super" | "Self") {
2132 return Ok(false);
2133 }
2134 for member in group.iter().filter(|member| member.language == "rust") {
2135 let Some(owner) = self
2136 .definition_by_id(&member.symbol_id)?
2137 .and_then(|definition| definition.owner_name)
2138 else {
2139 continue;
2140 };
2141 let path = member.path.replace('\\', "/");
2142 let Some((package, module_path)) = path.rsplit_once("/src/") else {
2143 continue;
2144 };
2145 let package_name = package
2146 .rsplit('/')
2147 .next()
2148 .unwrap_or(package)
2149 .replace('-', "_");
2150 let modules_match = leading[1..].iter().all(|segment| {
2151 module_path
2152 .split('/')
2153 .any(|part| part == segment || part.strip_suffix(".rs") == Some(segment))
2154 });
2155 if owner != *owner_segment || leading[0] != package_name || !modules_match {
2156 continue;
2157 }
2158 let in_package = format!("{}/%", crate::queries::escape_like(package));
2159 let owners = self.with_statement(OWNER_IN_PACKAGE_SQL, |stmt| {
2160 stmt.query_row(params![owner, in_package], |r| r.get::<_, i64>(0))
2161 })?;
2162 if owners == 1 {
2163 return Ok(true);
2164 }
2165 }
2166 Ok(false)
2167 }
2168
2169 fn decide_late(
2170 &mut self,
2171 row: &NameRow,
2172 group: &[Symbol],
2173 group_definitions: &[&Definition],
2174 rivals: &[&Definition],
2175 ) -> Result<Verdict, QueryError> {
2176 if let Some(verdict) = self.outside_ancestors(row, group)? {
2177 return Ok(verdict);
2178 }
2179 if let Some(receiver) = row.qualifier().filter(|_| row.namespace.len() <= 1)
2180 && let Some(type_name) = self.receiver_type(row, receiver)?
2181 && !self.owner_related(&type_name, group)?
2182 {
2183 return Ok(Verdict::new(
2184 SiteLabel::Excluded,
2185 format!("receiver `{receiver}` has type `{type_name}`"),
2186 false,
2187 ));
2188 }
2189 if let Some(head) = self.foreign_type_head(row, group)? {
2190 return Ok(Verdict::new(
2191 SiteLabel::Excluded,
2192 format!("receiver `{head}` has type `{head}`"),
2193 false,
2194 ));
2195 }
2196 if let Some(object) = self.object_assigned_on(row, group_definitions, rivals)? {
2197 return Ok(Verdict::new(
2198 SiteLabel::Likely,
2199 format!(
2200 "receiver `{object}` names the object `{}` is assigned on",
2201 group[0].name
2202 ),
2203 false,
2204 ));
2205 }
2206 if rivals.is_empty() {
2207 Ok(Verdict::new(SiteLabel::Likely, "name only", false))
2208 } else {
2209 Ok(Verdict::new(
2210 SiteLabel::Unresolved,
2211 format!("{} definitions", rivals.len() + 1),
2212 false,
2213 ))
2214 }
2215 }
2216
2217 fn outside_ancestors(
2221 &self,
2222 row: &NameRow,
2223 group: &[Symbol],
2224 ) -> Result<Option<Verdict>, QueryError> {
2225 let (Some(caller), Some(receiver)) = (row.caller_id.as_deref(), row.receiver.as_deref())
2226 else {
2227 return Ok(None);
2228 };
2229 if !row.links_enclosing_class {
2230 return Ok(None);
2231 }
2232 let Some(class) = self
2233 .definition_by_id(caller)?
2234 .and_then(|caller| caller.owner_name)
2235 else {
2236 return Ok(None);
2237 };
2238 let ancestors = self.ancestor_names(&class)?;
2239 for member in group {
2240 match self
2241 .definition_by_id(&member.symbol_id)?
2242 .and_then(|member| member.owner_name)
2243 {
2244 Some(owner) if !ancestors.contains(&owner) => {}
2245 _ => return Ok(None),
2246 }
2247 }
2248 Ok(Some(Verdict::new(
2249 SiteLabel::Excluded,
2250 format!("receiver `{receiver}` reaches only the ancestors of `{class}`"),
2251 false,
2252 )))
2253 }
2254
2255 fn object_assigned_on(
2258 &self,
2259 row: &NameRow,
2260 group: &[&Definition],
2261 rivals: &[&Definition],
2262 ) -> Result<Option<String>, QueryError> {
2263 let Some(receiver) = row
2264 .receiver
2265 .as_deref()
2266 .filter(|r| is_plain_identifier(r) && !SELF_RECEIVERS.contains(r))
2267 .filter(|_| matches!(row.kind.as_str(), "calls" | "call" | "member_access"))
2268 else {
2269 return Ok(None);
2270 };
2271 let assigned_on_receiver = |d: &&Definition| {
2272 language_family(&d.language) == "js"
2273 && matches!(d.kind.as_str(), "method" | "function")
2274 && d.top_level
2275 && d.class_name.as_deref() == Some(receiver)
2276 };
2277 if !group.iter().all(assigned_on_receiver)
2278 || rivals
2279 .iter()
2280 .any(|d| d.class_name.as_deref() == Some(receiver))
2281 {
2282 return Ok(None);
2283 }
2284 let Some(target) = group.first() else {
2285 return Ok(None);
2286 };
2287 let objects =
2288 self.rows_at_path(TOP_LEVEL_VARIABLE_SQL, &target.path, &[&receiver], |row| {
2289 row.get::<_, i64>(0)
2290 })?;
2291 Ok((!objects.is_empty()).then(|| receiver.to_string()))
2292 }
2293
2294 fn definition_by_id(&self, symbol_id: &str) -> Result<Option<Definition>, QueryError> {
2295 if let Some(absent) = self.absent.as_ref().filter(|s| s.symbol_id == symbol_id) {
2296 return self.absent_definition(absent).map(Some);
2297 }
2298 self.with_statement(DEFINITION_BY_ID_SQL, |stmt| {
2299 stmt.query_row(params![symbol_id], |row| {
2300 let parent_kind: Option<String> = row.get(9)?;
2301 let parent_name: Option<String> = row.get(8)?;
2302 Ok(Definition {
2303 symbol_id: row.get(0)?,
2304 name: row.get(1)?,
2305 path: row.get::<_, String>(4)?.replace('\\', "/"),
2306 start_line: row.get::<_, i64>(5)? as usize,
2307 decorated_start_line: None,
2308 body_start_line: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
2309 owner_name: parent_name.filter(|_| {
2310 parent_kind
2311 .as_deref()
2312 .is_some_and(|k| CLASS_LIKE_KINDS.contains(&k))
2313 }),
2314 local: false,
2315 kind: row.get(2)?,
2316 language: row.get(3)?,
2317 signature: String::new(),
2318 class_name: None,
2319 top_level: row.get::<_, Option<String>>(7)?.is_none(),
2320 callable: true,
2321 free: false,
2322 })
2323 })
2324 .optional()
2325 })
2326 }
2327
2328 fn absent_definition(&self, symbol: &Symbol) -> Result<Definition, QueryError> {
2331 let owner = match &symbol.parent_symbol_id {
2332 Some(parent) => crate::queries::get_symbol_by_id(self.conn, parent)?,
2333 None => None,
2334 };
2335 Ok(Definition {
2336 symbol_id: symbol.symbol_id.clone(),
2337 name: symbol.name.clone(),
2338 path: symbol.path.replace('\\', "/"),
2339 start_line: symbol.start_line,
2340 decorated_start_line: None,
2341 body_start_line: symbol.body_start_line,
2342 owner_name: owner
2343 .filter(|owner| CLASS_LIKE_KINDS.contains(&owner.kind.as_str()))
2344 .map(|owner| owner.name),
2345 local: false,
2346 kind: symbol.kind.clone(),
2347 language: symbol.language.clone(),
2348 signature: String::new(),
2349 class_name: None,
2350 top_level: symbol.parent_symbol_id.is_none(),
2351 callable: true,
2352 free: false,
2353 })
2354 }
2355
2356 fn nearest_shadow(
2361 &self,
2362 row: &NameRow,
2363 name: &str,
2364 group_ids: &HashSet<&str>,
2365 ) -> Result<Option<Definition>, QueryError> {
2366 let type_use = row.is_bare_type_use();
2367 if !row.is_bare_use() && !type_use {
2368 return Ok(None);
2369 }
2370 let mut hit = None;
2371 if let Some(caller) = row.caller_id.as_deref() {
2372 hit = self.first_real_definition(
2373 SHADOW_IN_CHAIN_SQL,
2374 params![caller, name, row.path, type_use, row.site_line as i64],
2375 row,
2376 )?;
2377 }
2378 if hit.is_none() {
2379 hit = self.first_real_definition(
2380 SHADOW_TOP_LEVEL_SQL,
2381 params![name, row.path, row.site_line as i64],
2382 row,
2383 )?;
2384 }
2385 match hit {
2386 Some(id) if !group_ids.contains(id.as_str()) => self.definition_by_id(&id),
2387 _ => Ok(None),
2388 }
2389 }
2390
2391 fn first_real_definition(
2394 &self,
2395 sql: &'static str,
2396 query: impl rusqlite::Params,
2397 row: &NameRow,
2398 ) -> Result<Option<String>, QueryError> {
2399 let rows = self.with_statement(sql, |stmt| {
2400 stmt.query_map(query, |r| {
2401 Ok((
2402 r.get::<_, String>(0)?,
2403 r.get::<_, String>(1)?,
2404 r.get::<_, String>(2)?,
2405 ))
2406 })?
2407 .collect::<Result<Vec<_>, _>>()
2408 })?;
2409 Ok(rows
2410 .into_iter()
2411 .find(|(_, signature, kind)| !is_test_label(signature) && !row.is_not_named_by(kind))
2412 .map(|(id, ..)| id))
2413 }
2414
2415 fn import_matches_of(
2418 &mut self,
2419 importer: &str,
2420 receiver: &str,
2421 name: &str,
2422 ) -> Result<&[ImportMatch], QueryError> {
2423 let key = (importer.to_string(), receiver.to_string(), name.to_string());
2424 if !self.import_matches.contains_key(&key) {
2425 if self.import_match_stmt.is_none() {
2426 self.import_match_stmt = Some(self.conn.prepare(&import_match_sql())?);
2427 }
2428 let stmt = self
2429 .import_match_stmt
2430 .as_mut()
2431 .expect("import match statement was just prepared");
2432 let matches = stmt
2433 .query_map(params![importer, receiver, name], |r| {
2434 Ok((
2435 r.get::<_, String>(0)?,
2436 r.get::<_, String>(1)?,
2437 r.get::<_, String>(2)?,
2438 r.get::<_, String>(3)?,
2439 ))
2440 })?
2441 .filter(|m| !matches!(m, Ok((_, _, signature, _)) if is_test_label(signature)))
2442 .map(|m| m.map(|(id, path, _, language)| (id, path, language)))
2443 .collect::<Result<Vec<_>, _>>()?;
2444 self.import_matches.insert(key.clone(), matches);
2445 }
2446 Ok(&self.import_matches[&key])
2447 }
2448
2449 fn other_import_matches(
2452 &mut self,
2453 row: &NameRow,
2454 name: &str,
2455 group_ids: &HashSet<&str>,
2456 ) -> Result<usize, QueryError> {
2457 let receiver = row.receiver.clone().unwrap_or_default();
2458 let matches = self.import_matches_of(&row.path, &receiver, name)?.to_vec();
2459 let mut count = 0;
2460 for (symbol_id, path, _) in &matches {
2461 if group_ids.contains(symbol_id.as_str()) {
2462 continue;
2463 }
2464 if row.language == "python"
2465 && !self.python_import_reaches(row, name, &path.replace('\\', "/"))?
2466 {
2467 continue;
2468 }
2469 count += 1;
2470 }
2471 Ok(count)
2472 }
2473
2474 fn imported_definitions(
2478 &mut self,
2479 importer: &str,
2480 language: &str,
2481 name: &str,
2482 ) -> Result<Vec<String>, QueryError> {
2483 let matches = self.import_matches_of(importer, "", name)?.to_vec();
2484 let mut reached = Vec::new();
2485 for (symbol_id, path, definition_language) in matches {
2486 if !languages_compatible(&definition_language, language) {
2487 continue;
2488 }
2489 if language == "python"
2490 && !self.python_binding_reaches(importer, name, &path.replace('\\', "/"))?
2491 {
2492 continue;
2493 }
2494 reached.push(symbol_id);
2495 }
2496 Ok(reached)
2497 }
2498
2499 fn bare_use_of_property_method(
2500 &self,
2501 row: &NameRow,
2502 name: &str,
2503 ) -> Result<Verdict, QueryError> {
2504 let sources = self.with_statement(IMPORT_SOURCES_SQL, |stmt| {
2505 stmt.query_map(params![row.path, name], |r| r.get::<_, String>(0))?
2506 .collect::<Result<Vec<_>, _>>()
2507 })?;
2508 let basis = match sources.iter().find(|source| !source.is_empty()) {
2509 Some(source) => format!("resolves to the import of `{name}` from `{source}`"),
2510 None => "a bare name cannot reach a property method".to_string(),
2511 };
2512 Ok(Verdict::new(SiteLabel::Excluded, basis, false))
2513 }
2514
2515 fn python_import_reaches(
2519 &self,
2520 row: &NameRow,
2521 name: &str,
2522 path: &str,
2523 ) -> Result<bool, QueryError> {
2524 self.python_binding_reaches(&row.path, row.qualifier().unwrap_or(name), path)
2525 }
2526
2527 fn python_binding_reaches(
2530 &self,
2531 importer: &str,
2532 binding: &str,
2533 path: &str,
2534 ) -> Result<bool, QueryError> {
2535 let sources = self.with_statement(IMPORT_SOURCES_SQL, |stmt| {
2536 stmt.query_map(params![importer, binding], |r| r.get::<_, String>(0))?
2537 .collect::<Result<Vec<_>, _>>()
2538 })?;
2539 if sources.is_empty() || sources.iter().any(|s| s.is_empty() || s.starts_with('.')) {
2540 return Ok(true);
2541 }
2542 let (folders, file) = match path.rsplit_once('/') {
2543 Some((dir, file)) => (dir.split('/').collect::<Vec<_>>(), file),
2544 None => (Vec::new(), path),
2545 };
2546 let mut top = folders.len();
2547 while top > 0 {
2548 let init = format!("{}/__init__.py", folders[..top].join("/"));
2549 let is_package = self.with_statement(FILE_EXISTS_SQL, |stmt| {
2550 stmt.exists(params![init, init.replace('/', "\\")])
2551 })?;
2552 if !is_package {
2553 break;
2554 }
2555 top -= 1;
2556 }
2557 let stem = file.strip_suffix(".py").unwrap_or(file);
2558 let reachable: Vec<&str> = if top == folders.len() {
2559 folders
2560 .iter()
2561 .copied()
2562 .chain(std::iter::once(stem))
2563 .collect()
2564 } else {
2565 folders[..=top].to_vec()
2566 };
2567 Ok(sources.iter().any(|source| {
2568 source
2569 .split('.')
2570 .next()
2571 .is_some_and(|first| reachable.contains(&first))
2572 }))
2573 }
2574
2575 fn external_binding(
2578 &mut self,
2579 row: &NameRow,
2580 name: &str,
2581 ) -> Result<Option<(String, String)>, QueryError> {
2582 let javascript = language_family(&row.language) == "js";
2583 let imported_head = match row.chain.as_ref() {
2584 Some(chain) if javascript => {
2585 let head = chain.first().map_or(name, String::as_str);
2586 self.js_binds_import(&row.path, head)?.then_some(head)
2587 }
2588 _ => None,
2589 };
2590 let Some(qualifier) = imported_head.or_else(|| row.qualifier()) else {
2591 return Ok(None);
2592 };
2593 if SELF_RECEIVERS.contains(&qualifier) || qualifier == "crate" {
2594 return Ok(None);
2595 }
2596 if row.language == "rust" && EXTERNAL_RUST_ROOTS.contains(&qualifier) {
2597 return Ok(Some((qualifier.to_string(), qualifier.to_string())));
2598 }
2599 let mut sources = self.with_statement(IMPORT_SOURCES_SQL, |stmt| {
2600 stmt.query_map(params![row.path, qualifier], |r| r.get::<_, String>(0))?
2601 .collect::<Result<Vec<_>, _>>()
2602 })?;
2603 let has_import = !sources.is_empty();
2604 sources.retain(|source| !source.is_empty());
2605 let pending_modules = self.with_statement(PENDING_IMPORT_MODULES_SQL, |stmt| {
2606 stmt.query_map(params![row.path, qualifier], |r| {
2607 r.get::<_, Option<String>>(0)
2608 })?
2609 .collect::<Result<Vec<_>, _>>()
2610 })?;
2611 for modules in pending_modules.into_iter().flatten() {
2612 if let Ok(parts) = serde_json::from_str::<Vec<String>>(&modules)
2613 && !parts.is_empty()
2614 {
2615 sources.push(parts.join("::"));
2616 }
2617 }
2618 if sources.is_empty() {
2619 return if has_import {
2620 Ok(None)
2621 } else {
2622 self.unindexed_path_head(row)
2623 };
2624 }
2625 for source in &sources {
2626 let external = if javascript {
2627 self.js_source_is_external(&row.path, source)?
2628 } else {
2629 self.source_is_external(source)?
2630 };
2631 if !external {
2632 return Ok(None);
2633 }
2634 }
2635 Ok(Some((qualifier.to_string(), sources[0].clone())))
2636 }
2637
2638 fn unindexed_path_head(
2641 &mut self,
2642 row: &NameRow,
2643 ) -> Result<Option<(String, String)>, QueryError> {
2644 let Some(head) = row
2645 .namespace
2646 .first()
2647 .filter(|head| is_plain_identifier(head) && !SELF_RECEIVERS.contains(&head.as_str()))
2648 .filter(|head| *head != "crate" && head.chars().next().is_some_and(char::is_lowercase))
2649 else {
2650 return Ok(None);
2651 };
2652 let kinds = self.with_statement(TYPE_HEAD_KINDS_SQL, |stmt| {
2653 stmt.query_map(params![head], |r| r.get::<_, String>(0))?
2654 .collect::<Result<Vec<_>, _>>()
2655 })?;
2656 if kinds.is_empty() && self.source_is_external(head)? {
2657 return Ok(Some((head.clone(), head.clone())));
2658 }
2659 Ok(None)
2660 }
2661
2662 fn source_is_external(&mut self, source: &str) -> Result<bool, QueryError> {
2663 if let Some(known) = self.external_sources.get(source) {
2664 return Ok(*known);
2665 }
2666 let runtime_library = !source.starts_with('.')
2667 && matches!(
2668 source.split([':', '.', '/']).find(|part| !part.is_empty()),
2669 Some("std" | "core" | "alloc")
2670 );
2671 let external = match import_segment(source).filter(|_| !runtime_library) {
2672 None => false,
2673 Some(segment) => {
2674 let escaped = crate::queries::escape_like(&segment);
2675 let (as_folder, as_file) = (format!("%/{escaped}/%"), format!("%/{escaped}.%"));
2676 !self.with_statement(SOURCE_FILES_SQL, |stmt| {
2677 stmt.exists(params![as_folder, as_file])
2678 })?
2679 }
2680 };
2681 let external = external || runtime_library;
2682 self.external_sources.insert(source.to_string(), external);
2683 Ok(external)
2684 }
2685
2686 fn receiver_type(&self, row: &NameRow, receiver: &str) -> Result<Option<String>, QueryError> {
2688 let Some(caller) = row.caller_id.as_deref() else {
2689 return Ok(None);
2690 };
2691 if SELF_RECEIVERS.contains(&receiver) {
2692 return Ok(None);
2693 }
2694 let types = self.with_statement(RECEIVER_TYPE_SQL, |stmt| {
2695 stmt.query_map(params![caller, receiver, row.path], |r| {
2696 r.get::<_, String>(0)
2697 })?
2698 .collect::<Result<Vec<_>, _>>()
2699 })?;
2700 let [only] = types.as_slice() else {
2701 return Ok(None);
2702 };
2703 if self.holds_any_value(row, only)?
2704 || self.is_type_parameter(caller, &row.language, only)?
2705 {
2706 return Ok(None);
2707 }
2708 Ok(Some(only.clone()))
2709 }
2710
2711 fn owner_related(&self, type_name: &str, group: &[Symbol]) -> Result<bool, QueryError> {
2714 let mut owners: Vec<String> = group
2715 .iter()
2716 .filter_map(|member| member.signature.as_deref().and_then(extended_type))
2717 .collect();
2718 for member in group {
2719 if let Some(definition) = self.definition_by_id(&member.symbol_id)? {
2720 match definition.owner_name {
2721 Some(owner) => owners.push(owner),
2722 None => return Ok(true),
2723 }
2724 }
2725 }
2726 if owners.iter().any(|owner| owner == type_name) {
2727 return Ok(true);
2728 }
2729 let type_ancestors = self.ancestor_names(type_name)?;
2730 for owner in &owners {
2731 if type_ancestors.contains(owner) || self.ancestor_names(owner)?.contains(type_name) {
2732 return Ok(true);
2733 }
2734 }
2735 Ok(false)
2736 }
2737
2738 fn ancestor_names(&self, class_name: &str) -> Result<HashSet<String>, QueryError> {
2739 let mut seen: HashSet<String> = HashSet::new();
2740 let mut frontier = vec![class_name.to_string()];
2741 for _ in 0..8 {
2742 let mut next = Vec::new();
2743 for name in frontier {
2744 for sql in [ANCESTORS_BY_RELATIONSHIP_SQL, ANCESTORS_BY_PENDING_SQL] {
2745 let bases = self.with_statement(sql, |stmt| {
2746 stmt.query_map(params![name], |r| r.get::<_, String>(0))?
2747 .collect::<Result<Vec<_>, _>>()
2748 })?;
2749 for base in bases {
2750 if seen.insert(base.clone()) {
2751 next.push(base);
2752 }
2753 }
2754 }
2755 }
2756 if next.is_empty() {
2757 break;
2758 }
2759 frontier = next;
2760 }
2761 Ok(seen)
2762 }
2763
2764 fn foreign_type_head(
2768 &self,
2769 row: &NameRow,
2770 group: &[Symbol],
2771 ) -> Result<Option<String>, QueryError> {
2772 let Some(head) = row
2773 .namespace
2774 .last()
2775 .map(String::as_str)
2776 .or(row.receiver.as_deref())
2777 .filter(|head| is_plain_identifier(head) && !SELF_RECEIVERS.contains(head))
2778 else {
2779 return Ok(None);
2780 };
2781 if !head.chars().next().is_some_and(char::is_uppercase) {
2782 return Ok(None);
2783 }
2784 let kinds = self.with_statement(TYPE_HEAD_KINDS_SQL, |stmt| {
2785 stmt.query_map(params![head], |r| r.get::<_, String>(0))?
2786 .collect::<Result<Vec<_>, _>>()
2787 })?;
2788 let foreign = if kinds.is_empty() {
2789 true
2790 } else {
2791 let type_like = kinds.iter().any(|k| CLASS_LIKE_KINDS.contains(&k.as_str()));
2792 let aliased = kinds
2793 .iter()
2794 .any(|k| matches!(k.as_str(), "type" | "type_alias" | "typedef"));
2795 type_like && !aliased && !self.owner_related(head, group)?
2796 };
2797 Ok(foreign.then(|| head.to_string()))
2798 }
2799
2800 fn text_outcome(
2801 &self,
2802 root: &Path,
2803 name: &str,
2804 sites: &[SiteRowOut],
2805 ) -> Result<TextOutcome, QueryError> {
2806 let hits = scan_word(root, name, TEXT_SCAN_BUDGET);
2807 let site_spans: HashMap<String, Vec<(usize, usize)>> = {
2808 let mut spans: HashMap<String, Vec<(usize, usize)>> = HashMap::new();
2809 for site in sites {
2810 let start = site.site.start_line.unwrap_or(0);
2811 spans
2812 .entry(site.site.path.clone())
2813 .or_default()
2814 .push((start, site.end_line.unwrap_or(start).max(start)));
2815 }
2816 spans
2817 };
2818 let definition_spans = definition_spans(&self.definitions(name)?, Some(&hits.hits));
2819 let import_spans = self.import_spans(name)?;
2820
2821 let mut counts = ReferenceCounts::default();
2822 let mut text_only: Vec<TextOnlyLines> = Vec::new();
2823 for (path, line) in &hits.hits {
2824 counts.text_lines += 1;
2825 if in_spans(&site_spans, path, *line) {
2826 counts.site_lines += 1;
2827 } else if in_spans(&definition_spans, path, *line) {
2828 counts.definition_lines += 1;
2829 } else if in_spans(&import_spans, path, *line) {
2830 counts.import_lines += 1;
2831 } else {
2832 counts.text_only_lines += 1;
2833 match text_only.last_mut() {
2834 Some(last) if last.path == *path => last.lines.push(*line),
2835 _ => text_only.push(TextOnlyLines {
2836 path: path.clone(),
2837 lines: vec![*line],
2838 }),
2839 }
2840 }
2841 }
2842 Ok(TextOutcome {
2843 counts,
2844 text_only,
2845 summary: TextScanSummary {
2846 files_scanned: hits.files_scanned,
2847 files_total: hits.files_total,
2848 complete: hits.complete,
2849 elapsed_ms: hits.elapsed.as_millis() as u64,
2850 skipped: None,
2851 },
2852 })
2853 }
2854}
2855
2856type SiteRowOut = LabeledSite;
2857
2858fn claimed_verdict(row: &NameRow, claim: &Claim, rivals: usize, builds_class: bool) -> Verdict {
2859 if let Some(basis) = &claim.basis {
2860 if builds_class {
2861 return Verdict::new(SiteLabel::Proven, "builds the class", true);
2862 }
2863 if row.kind == "variable_ref"
2864 && language_family(&row.language) == "js"
2865 && matches!(basis.as_str(), "same file" | "enclosing scope")
2866 {
2867 return Verdict::new(
2868 SiteLabel::Likely,
2869 format!("{basis}; the index does not record callback parameters"),
2870 true,
2871 );
2872 }
2873 return Verdict::new(SiteLabel::Proven, basis, true);
2874 }
2875 if rivals > 0 {
2876 return Verdict::new(
2877 SiteLabel::Unresolved,
2878 format!("{} definitions", rivals + 1),
2879 true,
2880 );
2881 }
2882 if row.has_qualifier() {
2883 return Verdict::new(SiteLabel::Likely, "receiver not typed", true);
2884 }
2885 Verdict::new(SiteLabel::Likely, "name only", true)
2886}
2887
2888fn site_row(row: &NameRow, verdict: Verdict, spec: &Spec) -> SiteRow {
2889 let end_line = row.end_line.filter(|end| !row.paired && *end > row.line);
2890 SiteRow {
2891 site: ReferenceSite {
2892 from_symbol_name: row.caller_name.clone(),
2893 from_symbol_id: row.caller_id.clone().unwrap_or_default(),
2894 to_symbol_name: spec.name.clone(),
2895 kind: row.display_kind(),
2896 path: row.path.clone(),
2897 start_line: Some(row.site_line),
2898 start_column: row.site_column,
2899 occurrences: None,
2900 target: None,
2901 enclosing_symbol_name: None,
2902 },
2903 label: verdict.label,
2904 basis: verdict.basis,
2905 claimed: verdict.claimed,
2906 end_line,
2907 arg_count: row.arg_count,
2908 }
2909}
2910
2911fn merge_site_rows(rows: Vec<SiteRow>) -> Vec<LabeledSite> {
2912 let mut merged: HashMap<(String, usize), SiteRow> = HashMap::new();
2913 let mut order: Vec<(String, usize)> = Vec::new();
2914 for row in rows {
2915 let key = (row.site.path.clone(), row.site.start_line.unwrap_or(0));
2916 match merged.get_mut(&key) {
2917 None => {
2918 order.push(key.clone());
2919 merged.insert(key, row);
2920 }
2921 Some(kept) => {
2922 let end = kept.end_line.max(row.end_line);
2923 let better = (row.label.rank(), !row.claimed, row.site.start_column)
2924 < (kept.label.rank(), !kept.claimed, kept.site.start_column);
2925 if better {
2926 *kept = row;
2927 }
2928 kept.end_line = end;
2929 }
2930 }
2931 }
2932 order
2933 .into_iter()
2934 .filter_map(|key| merged.remove(&key))
2935 .map(|row| LabeledSite {
2936 site: row.site,
2937 label: row.label,
2938 basis: row.basis,
2939 end_line: row.end_line,
2940 arg_count: row.arg_count,
2941 })
2942 .collect()
2943}
2944
2945fn mark_hidden_receivers(root: &Path, rows: &mut [NameRow]) {
2949 let mut by_path: HashMap<String, Vec<usize>> = HashMap::new();
2950 for (index, row) in rows.iter().enumerate() {
2951 if row.is_call() && !row.has_qualifier() && row.language != "php" {
2953 by_path.entry(row.path.clone()).or_default().push(index);
2954 }
2955 }
2956 for (path, indices) in by_path {
2957 let Ok(text) = std::fs::read_to_string(root.join(&path)) else {
2958 continue;
2959 };
2960 let lines: Vec<&str> = text.lines().collect();
2961 for index in indices {
2962 let row = &mut rows[index];
2963 let Some((before, _)) = row
2964 .site_column
2965 .and_then(|column| split_line(&lines, row.site_line, column))
2966 else {
2967 continue;
2968 };
2969 let before = before.trim_end();
2970 row.hidden_receiver = matches!(before.trim_start(), "." | "?.")
2971 || awaits_member(before)
2972 || before.is_empty() && continues_chain(&lines, row.site_line);
2973 }
2974 }
2975}
2976
2977fn awaits_member(text: &str) -> bool {
2980 text.strip_suffix('.').is_some_and(|result| {
2981 result
2982 .trim_end_matches(['?', '!'])
2983 .ends_with([')', ']', '\'', '"', '`'])
2984 })
2985}
2986
2987fn continues_chain(lines: &[&str], line: usize) -> bool {
2990 let above = lines
2991 .iter()
2992 .take(line.saturating_sub(1))
2993 .rev()
2994 .map(|text| text.trim())
2995 .find(|text| !text.is_empty());
2996 above.is_some_and(|text| {
2997 awaits_member(text)
2998 && !["//", "/*", "*", "#"]
2999 .iter()
3000 .any(|start| text.starts_with(start))
3001 })
3002}
3003
3004fn pair_identifiers(rows: &mut [NameRow]) {
3008 let mut by_caller: HashMap<String, (Vec<usize>, Vec<usize>)> = HashMap::new();
3009 for (index, row) in rows.iter().enumerate() {
3010 let Some(caller) = row.caller_id.clone() else {
3011 continue;
3012 };
3013 let entry = by_caller.entry(caller).or_default();
3014 match row.source {
3015 Source::Pending | Source::Relationship if row.kind == "calls" => entry.0.push(index),
3016 Source::Identifier if row.kind == "call" => entry.1.push(index),
3017 _ => {}
3018 }
3019 }
3020 for (calls, identifiers) in by_caller.values_mut() {
3021 calls.sort_by_key(|&i| (rows[i].line, rows[i].column, rows[i].source.tag(), i));
3022 identifiers.sort_by_key(|&i| (rows[i].line, rows[i].column, i));
3023 let mut used = vec![false; identifiers.len()];
3024 for &call in calls.iter() {
3025 let line = rows[call].line;
3026 let last = rows[call].caller_end_line.unwrap_or(usize::MAX).max(line);
3027 let Some(slot) = identifiers
3028 .iter()
3029 .enumerate()
3030 .position(|(slot, &i)| !used[slot] && (line..=last).contains(&rows[i].line))
3031 else {
3032 continue;
3033 };
3034 used[slot] = true;
3035 let identifier = identifiers[slot];
3036 let (site_line, site_column) = (rows[identifier].line, rows[identifier].column);
3037 let alone_on_line = identifiers
3038 .iter()
3039 .filter(|&&other| rows[other].line == site_line)
3040 .count()
3041 == 1;
3042 if alone_on_line && rows[call].links_enclosing_class && rows[identifier].is_super_call()
3043 {
3044 rows[call].consumed = true;
3045 rows[identifier].links_enclosing_class = true;
3046 continue;
3047 }
3048 rows[call].arg_count = rows[identifier].arg_count.filter(|_| alone_on_line);
3049 rows[identifier].consumed = true;
3050 rows[call].paired = true;
3051 rows[call].site_line = site_line;
3052 rows[call].site_column = site_column;
3053 }
3054 }
3055 attach_counts_by_line(rows);
3056}
3057
3058fn attach_counts_by_line(rows: &mut [NameRow]) {
3061 let mut identifiers: HashMap<(String, usize), Vec<Option<u32>>> = HashMap::new();
3062 for row in rows.iter() {
3063 if row.source == Source::Identifier && row.kind == "call" {
3064 identifiers
3065 .entry((row.path.clone(), row.line))
3066 .or_default()
3067 .push(row.arg_count);
3068 }
3069 }
3070 for row in rows.iter_mut() {
3071 if row.source != Source::Identifier
3072 && row.is_call()
3073 && row.arg_count.is_none()
3074 && let Some(counts) = identifiers.get(&(row.path.clone(), row.line))
3075 && let [count] = counts.as_slice()
3076 {
3077 row.arg_count = *count;
3078 }
3079 }
3080}
3081
3082const BODY_IS_FIRST_STATEMENT: &[&str] = &["python", "ruby", "lua", "fsharp", "vbnet"];
3085
3086fn definition_spans(
3090 definitions: &[Definition],
3091 name_lines: Option<&[(String, usize)]>,
3092) -> HashMap<String, Vec<(usize, usize)>> {
3093 let mut spans: HashMap<String, Vec<(usize, usize)>> = HashMap::new();
3094 for definition in definitions {
3095 let start = definition
3096 .decorated_start_line
3097 .unwrap_or(definition.start_line);
3098 let names_a_line_in = |first: usize, last: usize| {
3099 name_lines.is_none_or(|lines| {
3100 let from = lines.partition_point(|(path, line)| {
3101 (path.as_str(), *line) < (definition.path.as_str(), first)
3102 });
3103 lines
3104 .get(from)
3105 .is_some_and(|(path, line)| *path == definition.path && *line <= last)
3106 })
3107 };
3108 let end = definition
3109 .body_start_line
3110 .map_or(definition.start_line, |body| {
3111 if BODY_IS_FIRST_STATEMENT.contains(&definition.language.as_str())
3112 && names_a_line_in(start, body.saturating_sub(1))
3113 {
3114 body.saturating_sub(1)
3115 } else {
3116 body
3117 }
3118 })
3119 .max(definition.start_line);
3120 spans
3121 .entry(definition.path.clone())
3122 .or_default()
3123 .push((start, end));
3124 }
3125 spans
3126}
3127
3128fn in_spans(spans: &HashMap<String, Vec<(usize, usize)>>, path: &str, line: usize) -> bool {
3129 spans.get(path).is_some_and(|list| {
3130 list.iter()
3131 .any(|&(start, end)| start <= line && line <= end)
3132 })
3133}
3134
3135fn shared_leading_segments(a: &str, b: &str) -> usize {
3136 a.split('/')
3137 .zip(b.split('/'))
3138 .take_while(|(x, y)| x == y)
3139 .count()
3140}
3141
3142fn language_family(language: &str) -> &str {
3143 match language {
3144 "javascript" | "typescript" | "tsx" | "jsx" | "vue" => "js",
3145 "java" | "kotlin" | "scala" | "groovy" => "jvm",
3146 "c" | "cpp" | "objc" | "objcpp" | "qml" => "c",
3147 "csharp" | "razor" | "vbnet" | "fsharp" => "dotnet",
3148 other => other,
3149 }
3150}
3151
3152fn languages_compatible(row: &str, target: &str) -> bool {
3153 row.is_empty() || language_family(row) == language_family(target)
3154}
3155
3156fn import_segment(source: &str) -> Option<String> {
3158 let source = source.trim();
3159 let first_segment = source.split([':', '.']).next().unwrap_or("");
3160 if source.is_empty()
3161 || source.starts_with('.')
3162 || ["crate", "self", "super"].contains(&first_segment)
3163 {
3164 return None;
3165 }
3166 let normalized = source
3167 .replace("::", ".")
3168 .replace('/', ".")
3169 .replace('-', "_");
3170 normalized
3171 .rsplit('.')
3172 .find(|segment| !segment.is_empty())
3173 .map(str::to_string)
3174}
3175
3176fn judge_sql(conn: &Connection, absent_target: bool) -> String {
3179 let target = "s_target";
3180 let parent = "s_target_parent";
3181 let target_join = if absent_target {
3182 format!(
3183 "JOIN (SELECT ?2 AS symbol_id, ?5 AS name, ?6 AS kind, ?7 AS language, ?8 AS path,
3184 ?9 AS start_line, ?10 AS parent_symbol_id) {target}"
3185 )
3186 } else {
3187 format!("JOIN symbols {target} ON {target}.symbol_id = ?2")
3188 };
3189 let predicate = pending_target_predicate(conn, target, parent);
3190 let evidence = evidence_sql(conn, target, parent);
3191 format!(
3192 "SELECT p.src, p.row_id,
3193 CASE WHEN p.src = 'I' AND p.kind IN ('type_usage', 'member_access')
3194 THEN COALESCE(({predicate}), 0) ELSE 1 END AS claimed,
3195 {evidence} AS basis
3196 FROM (
3197 SELECT 'P' AS src, CAST(pr.rowid AS TEXT) AS row_id,
3198 pr.from_symbol_id AS from_symbol_id, pr.kind AS kind, pr.path AS path,
3199 pr.start_line AS start_line, pr.start_column AS start_column,
3200 pr.target_receiver AS target_receiver,
3201 pr.target_namespace_json AS target_namespace_json,
3202 COALESCE((SELECT language FROM files WHERE file_id = pr.file_id), '') AS language,
3203 NULL AS receiver_qualifier
3204 FROM pending_relationships pr
3205 WHERE pr.target_terminal_name = ?1 AND pr.kind != 'imports'
3206 AND CAST(pr.rowid AS TEXT) IN (SELECT value FROM json_each(?3))
3207 UNION ALL
3208 SELECT 'I', i.identifier_id, i.containing_symbol_id, i.kind, i.path, i.start_line,
3209 i.start_column,
3210 CASE WHEN json_valid(i.metadata_json)
3211 THEN json_extract(i.metadata_json, '$.receiver') END,
3212 '[]', i.language,
3213 CASE WHEN json_valid(i.metadata_json)
3214 THEN json_extract(i.metadata_json, '$.receiver_qualifier') END
3215 FROM identifiers i
3216 WHERE i.name = ?1 AND i.identifier_id IN (SELECT value FROM json_each(?4))
3217 ) p
3218 LEFT JOIN symbols s_from ON s_from.symbol_id = p.from_symbol_id
3219 {target_join}
3220 LEFT JOIN symbols {parent} ON {parent}.symbol_id = {target}.parent_symbol_id
3221 WHERE {predicate}
3222 OR (p.src = 'I' AND p.kind IN ('type_usage', 'member_access'))"
3223 )
3224}
3225
3226fn import_match_sql() -> String {
3229 let receiver = module_import_binding_sql("t", "p.target_receiver");
3230 let bare = module_import_binding_sql("t", "t.name");
3231 format!(
3232 "SELECT t.symbol_id, t.path, COALESCE(t.signature, ''), t.language
3233 FROM (SELECT ?1 AS path, ?2 AS target_receiver) p
3234 JOIN symbols t ON t.name = ?3
3235 WHERE t.parent_symbol_id IS NULL AND t.kind NOT IN ('import', 'export')
3236 AND t.language NOT IN ({docs})
3237 AND CASE WHEN p.target_receiver != '' THEN {receiver} ELSE {bare} END",
3238 docs = documentation_language_list()
3239 )
3240}
3241
3242fn evidence_sql(conn: &Connection, target: &str, parent: &str) -> String {
3244 let ns = "json_each(CASE WHEN json_valid(p.target_namespace_json) THEN p.target_namespace_json ELSE '[]' END)";
3245 let has_namespace = "COALESCE(p.target_namespace_json, '[]') NOT IN ('', '[]')";
3246 let has_receiver = "COALESCE(p.target_receiver, '') != ''";
3247 let self_receiver = "p.target_receiver IN ('self', 'this', 'cls', 'Self', 'super')";
3248 let names_self = format!("EXISTS (SELECT 1 FROM {ns} WHERE value = 'Self')");
3249 let receiver_chain = class_chain_sql(target, "p.target_receiver IS NOT 'super'");
3250 let inherited = class_chain_sql(target, &format!("{target}.kind IN ('variable', 'field')"));
3251 let enclosing = enclosing_scope_sql(target);
3252 let implicit_languages = implicit_receiver_language_list();
3253 let module_receiver = module_import_binding_sql(target, "p.target_receiver");
3254 let module_bare = module_import_binding_sql(target, &format!("{target}.name"));
3255 let receiver_type = receiver_type_sql(conn, parent);
3256 let qualifier_reaches_owner = module_import_binding_sql(parent, "p.receiver_qualifier");
3257 let foreign_qualifier = format!(
3258 "(p.receiver_qualifier IS NOT NULL
3259 AND EXISTS (SELECT 1 FROM symbols q WHERE q.kind = 'import' AND q.path = p.path
3260 AND q.name = p.receiver_qualifier)
3261 AND NOT {qualifier_reaches_owner})"
3262 );
3263 let package_proximity = path_proximity(&format!("{target}.path"), "p.path");
3264 let built = built_receiver_sql(conn, target)
3265 .map(|built| {
3266 format!(
3267 "WHEN {has_receiver} AND {target}.parent_symbol_id IS NOT NULL AND {built}
3268 THEN 'builds the class'"
3269 )
3270 })
3271 .unwrap_or_default();
3272 format!(
3273 "CASE
3274 WHEN {has_namespace} AND {parent}.name IS NOT NULL
3275 AND EXISTS (SELECT 1 FROM {ns} WHERE value = {parent}.name)
3276 THEN 'qualified path'
3277 WHEN {has_namespace} AND {target}.parent_symbol_id IS NULL THEN 'qualified path'
3278 WHEN {target}.parent_symbol_id IS NOT NULL AND {names_self}
3279 AND s_from.parent_symbol_id = {target}.parent_symbol_id THEN 'class chain'
3280 WHEN {target}.parent_symbol_id IS NOT NULL
3281 AND ({self_receiver} OR {names_self}) AND {receiver_chain} THEN 'class chain'
3282 WHEN {has_receiver} AND {parent}.name = p.target_receiver AND NOT {foreign_qualifier}
3283 THEN 'qualified path'
3284 WHEN {has_receiver} AND {target}.parent_symbol_id IS NULL AND {module_receiver}
3285 THEN 'import'
3286 WHEN {has_receiver} AND {target}.parent_symbol_id IS NOT NULL AND {receiver_type}
3287 THEN 'receiver type'
3288 {built}
3289 WHEN NOT ({has_receiver}) AND NOT ({has_namespace}) THEN
3290 CASE
3291 WHEN {target}.parent_symbol_id IS NULL AND {target}.path = p.path
3292 THEN 'same file'
3293 WHEN {target}.parent_symbol_id = p.from_symbol_id THEN 'enclosing scope'
3294 WHEN {target}.parent_symbol_id IS NOT NULL AND {enclosing}
3295 THEN 'enclosing scope'
3296 WHEN {target}.parent_symbol_id IS NOT NULL AND p.language IN ({implicit_languages})
3297 AND (s_from.parent_symbol_id = {target}.parent_symbol_id OR {inherited})
3298 THEN 'class chain'
3299 WHEN {target}.parent_symbol_id IS NULL AND {module_bare} THEN 'import'
3300 WHEN {target}.parent_symbol_id IS NULL AND {target}.language IN ('go', 'java')
3301 AND {target}.language = p.language AND ({package_proximity}) >= 1
3302 THEN 'same package'
3303 END
3304 END"
3305 )
3306}
3307
3308#[cfg(test)]
3309mod tests {
3310 use super::*;
3311 use crate::{
3312 Workspace, find_julie_extract_binary, open_read_only, safe_tempdir, scan_workspace,
3313 };
3314
3315 fn report_past_the_time_limit(
3316 files: &[(&str, &str)],
3317 name: &str,
3318 path: Option<&str>,
3319 ) -> ReferenceReport {
3320 find_julie_extract_binary().expect("julie-extract binary must be present for tests");
3321 let temp = safe_tempdir();
3322 let root = temp.path();
3323 for (relative, text) in files {
3324 let full = root.join(relative);
3325 std::fs::create_dir_all(full.parent().unwrap()).unwrap();
3326 std::fs::write(full, text).unwrap();
3327 }
3328 let db = root.join(".code-kb").join("artifact.db");
3329 scan_workspace(&Workspace::new(root.to_path_buf()), &db, true).unwrap();
3330 let conn = open_read_only(&db).unwrap();
3331 let target = select_reference_target(&conn, name, path).unwrap();
3332 report_within(&conn, root, &target, Duration::ZERO).unwrap()
3333 }
3334
3335 #[test]
3336 fn sites_past_the_time_limit_keep_the_label_their_name_gives() {
3337 let report = report_past_the_time_limit(
3338 &[(
3339 "a.py",
3340 "def helper():\n return 1\n\n\ndef use():\n return helper()\n",
3341 )],
3342 "helper",
3343 None,
3344 );
3345
3346 let labels: Vec<(SiteLabel, &str)> = report
3347 .sites
3348 .iter()
3349 .map(|site| (site.label, site.basis.as_str()))
3350 .collect();
3351 assert_eq!(
3352 labels,
3353 [(
3354 SiteLabel::Likely,
3355 "name only; not checked within the time limit"
3356 )]
3357 );
3358 assert!(
3359 report
3360 .cannot_see
3361 .iter()
3362 .any(|note| note == "1 of 1 sites were not checked within the 0 s time limit"),
3363 "{:?}",
3364 report.cannot_see
3365 );
3366 }
3367
3368 #[test]
3369 fn sites_past_the_time_limit_count_only_the_definitions_that_can_reach_them() {
3370 let report = report_past_the_time_limit(
3371 &[
3372 (
3373 "a/name.go",
3374 "package a\n\nfunc Name() string {\n\treturn \"a\"\n}\n",
3375 ),
3376 (
3377 "b/cmd.go",
3378 "package b\n\ntype Command struct {\n\tName string\n}\n",
3379 ),
3380 (
3381 "c/use.go",
3382 "package c\n\nfunc call(x any) string {\n\treturn x.Name()\n}\n\nfunc read(x any) string {\n\treturn x.Name\n}\n",
3383 ),
3384 ],
3385 "Name",
3386 Some("a/name.go"),
3387 );
3388
3389 let labels: Vec<(usize, SiteLabel, &str)> = report
3390 .sites
3391 .iter()
3392 .map(|site| {
3393 (
3394 site.site.start_line.unwrap_or(0),
3395 site.label,
3396 site.basis.as_str(),
3397 )
3398 })
3399 .collect();
3400 assert_eq!(
3401 labels,
3402 [
3403 (
3404 4,
3405 SiteLabel::Likely,
3406 "name only; not checked within the time limit"
3407 ),
3408 (
3409 8,
3410 SiteLabel::Unresolved,
3411 "2 definitions; not checked within the time limit"
3412 ),
3413 ]
3414 );
3415 }
3416}