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 absent_condition: Option<&str>,
105) -> Result<ReferenceReport, QueryError> {
106 report_with(
107 conn,
108 root,
109 target,
110 ACCOUNTING_TIME_LIMIT,
111 ReportMode {
112 text_scan: false,
113 arity_exclusion: false,
114 absent_target: crate::queries::get_symbol_by_id(conn, &target.symbol_id)?.is_none(),
115 },
116 absent_condition,
117 )
118}
119
120#[derive(Clone, Copy)]
122struct ReportMode {
123 text_scan: bool,
124 arity_exclusion: bool,
125 absent_target: bool,
127}
128
129fn report_within(
130 conn: &Connection,
131 root: &Path,
132 target: &Symbol,
133 time_limit: Duration,
134) -> Result<ReferenceReport, QueryError> {
135 let mode = ReportMode {
136 text_scan: true,
137 arity_exclusion: true,
138 absent_target: false,
139 };
140 report_with(conn, root, target, time_limit, mode, None)
141}
142
143fn report_with(
144 conn: &Connection,
145 root: &Path,
146 target: &Symbol,
147 time_limit: Duration,
148 mode: ReportMode,
149 absent_condition: Option<&str>,
150) -> Result<ReferenceReport, QueryError> {
151 let class = constructor_class(conn, target)?;
152 let group = overload_group(conn, target)?;
153 let mut accounting = Accounting::new(conn, root, time_limit);
154 accounting.absent = mode.absent_target.then(|| target.clone());
155 accounting.target_condition = match absent_condition {
156 Some(condition) => Some(condition.to_string()),
157 None => preprocessor_condition(conn, &target.symbol_id)?,
158 };
159 let target_arity = match arity::group_arity(conn, root, &group, false)? {
160 Some(own) if mode.arity_exclusion => {
161 arity::widened_by_ancestors(&mut accounting, target, own)?
162 }
163 _ => None,
164 };
165 accounting.restart_clock(time_limit);
166 let mut specs = vec![Spec {
167 name: target.name.clone(),
168 arity: target_arity,
169 local_to: enclosing_function(conn, &group)?,
170 group,
171 scope: if class.is_some() {
172 RowScope::Calls
173 } else {
174 RowScope::All
175 },
176 builds_class: false,
177 }];
178 if let Some(class) = &class {
179 specs.push(Spec {
180 name: class.name.clone(),
181 group: vec![class.clone()],
182 scope: RowScope::Calls,
183 builds_class: true,
184 local_to: None,
185 arity: if mode.arity_exclusion {
186 arity::group_arity(conn, root, &specs[0].group, true)?
187 } else {
188 None
189 },
190 });
191 }
192
193 let mut site_rows = Vec::new();
194 let mut cannot_see = Vec::new();
195 for spec in &specs {
196 accounting.account_spec(spec, &mut site_rows, &mut cannot_see)?;
197 }
198 let definitions_with_name = accounting.definition_count(&target.name)?;
199
200 let mut sites = merge_site_rows(site_rows);
201 let unchecked = sites
202 .iter()
203 .filter(|site| site.basis.ends_with(NOT_CHECKED))
204 .count();
205 if unchecked > 0 {
206 cannot_see.push(format!(
207 "{unchecked} of {} sites were not checked within the {} s time limit",
208 sites.len(),
209 time_limit.as_secs()
210 ));
211 }
212 let mut report_sites: Vec<ReferenceSite> = sites.iter().map(|row| row.site.clone()).collect();
213 populate_lambda_enclosing_names(conn, &mut report_sites)?;
214 for (row, site) in sites.iter_mut().zip(report_sites) {
215 row.site.enclosing_symbol_name = site.enclosing_symbol_name;
216 }
217 sites.sort_by(|a, b| {
218 a.label
219 .rank()
220 .cmp(&b.label.rank())
221 .then_with(|| a.site.path.cmp(&b.site.path))
222 .then_with(|| a.site.start_line.cmp(&b.site.start_line))
223 .then_with(|| a.site.start_column.cmp(&b.site.start_column))
224 });
225
226 let text = match &class {
227 _ if !mode.text_scan => TextOutcome::not_run(),
228 Some(class) => TextOutcome::skipped(class),
229 None => accounting.text_outcome(root, &target.name, &sites)?,
230 };
231
232 let mut counts = ReferenceCounts {
233 sites: sites.len(),
234 ..text.counts
235 };
236 for site in &sites {
237 match site.label {
238 SiteLabel::Proven => counts.proven += 1,
239 SiteLabel::Likely => counts.likely += 1,
240 SiteLabel::Unresolved => counts.unresolved += 1,
241 SiteLabel::Excluded => counts.excluded += 1,
242 }
243 }
244
245 let reach = sections::reach_sections(&mut accounting, target, &sites)?;
246 cannot_see.splice(0..0, cannot_see_for_language(&target.language));
247 cannot_see.extend(reach.notes);
248 Ok(ReferenceReport {
249 target: report_target(conn, target, definitions_with_name)?,
250 counts,
251 sites,
252 text_only: text.text_only,
253 text_scan: text.summary,
254 cannot_see,
255 imports: match mode.absent_target {
256 true => import_sites_of_absent(
257 conn,
258 &target.name,
259 &target.path,
260 target.parent_symbol_id.is_some(),
261 )?,
262 false => import_sites(conn, &target.name, Some(&target.path))?,
263 },
264 overrides: reach.overrides,
265 overridden: reach.overridden,
266 constructions: reach.constructions,
267 variant_uses: reach.variant_uses,
268 registrations: reach.registrations,
269 tests: reach.tests,
270 })
271}
272
273fn cannot_see_for_language(language: &str) -> Vec<String> {
274 let mut items = Vec::new();
275 match language {
276 "python" => items.push("calls through getattr() or names built at runtime".to_string()),
277 "javascript" | "typescript" | "tsx" | "jsx" | "vue" => items
278 .push("calls through computed property names or values built at runtime".to_string()),
279 "rust" => items.push("calls inside macro bodies".to_string()),
280 "java" | "csharp" | "kotlin" => {
281 items.push("calls through reflection or dependency injection".to_string());
282 }
283 "go" => items.push(
284 "calls through a Go interface that the index does not record reach methods this list does not show"
285 .to_string(),
286 ),
287 _ => {}
288 }
289 items
290}
291
292fn report_target(
293 conn: &Connection,
294 target: &Symbol,
295 definitions_with_name: usize,
296) -> Result<ReferenceTarget, QueryError> {
297 let mut qualified = target.clone();
298 crate::queries::qualify_members(conn, std::iter::once(&mut qualified))?;
299 Ok(ReferenceTarget {
300 symbol_id: target.symbol_id.clone(),
301 name: target.name.clone(),
302 display_name: qualified.name,
303 kind: crate::formatters::display_kind(target).to_string(),
304 path: target.path.clone(),
305 line: target.start_line,
306 definitions_with_name,
307 })
308}
309
310fn is_constructor(target: &Symbol) -> bool {
311 target.kind == "constructor" || (target.language == "python" && target.name == "__init__")
312}
313
314fn constructor_class(conn: &Connection, target: &Symbol) -> Result<Option<Symbol>, QueryError> {
315 if !is_constructor(target) {
316 return Ok(None);
317 }
318 let class_id = match constructed_class(conn, &target.symbol_id)? {
319 Some((id, _)) => id,
320 None => match &target.parent_symbol_id {
321 Some(parent) => parent.clone(),
322 None => return Ok(None),
323 },
324 };
325 crate::queries::get_symbol_by_id(conn, &class_id)
326}
327
328pub(crate) fn inherited_group_arity(
332 conn: &Connection,
333 root: &Path,
334 target: &Symbol,
335) -> Result<Option<arity::Arity>, QueryError> {
336 let group = overload_group(conn, target)?;
337 let Some(own) = arity::group_arity(conn, root, &group, false)? else {
338 return Ok(None);
339 };
340 if !matches!(target.language.as_str(), "java" | "csharp" | "kotlin") {
341 return Ok(Some(own));
342 }
343 let mut accounting = Accounting::new(conn, root, ACCOUNTING_TIME_LIMIT);
344 Ok(arity::widened_by_ancestors(&mut accounting, target, own.clone())?.or(Some(own)))
345}
346
347pub(crate) fn overload_group(
349 conn: &Connection,
350 target: &Symbol,
351) -> Result<Vec<Symbol>, QueryError> {
352 let mut stmt = conn.prepare(
353 "SELECT symbol_id FROM symbols
354 WHERE name = ?1 AND path = ?2 AND kind = ?3 AND parent_symbol_id IS ?4
355 ORDER BY start_line, symbol_id",
356 )?;
357 let ids = stmt
358 .query_map(
359 params![
360 target.name,
361 target.path,
362 target.kind,
363 target.parent_symbol_id
364 ],
365 |row| row.get::<_, String>(0),
366 )?
367 .collect::<Result<Vec<_>, _>>()?;
368 let mut group = vec![target.clone()];
369 for id in ids.into_iter().filter(|id| *id != target.symbol_id) {
370 if let Some(symbol) = crate::queries::get_symbol_by_id(conn, &id)? {
371 group.push(symbol);
372 }
373 }
374 Ok(group)
375}
376
377const DEFINITION_BY_ID_SQL: &str =
378 "SELECT s.symbol_id, s.name, s.kind, s.language, s.path, s.start_line,
379 s.body_start_line, s.parent_symbol_id, par.name, par.kind, COALESCE(s.signature, ''),
380 COALESCE(par.signature, '')
381 FROM symbols s LEFT JOIN symbols par ON par.symbol_id = s.parent_symbol_id
382 WHERE s.symbol_id = ?1";
383
384const LINKS_ENCLOSING_CLASS_SQL: &str = "EXISTS (
387 WITH RECURSIVE enclosing(symbol_id, depth) AS (
388 SELECT r.from_symbol_id, 0
389 UNION ALL
390 SELECT s.parent_symbol_id, enclosing.depth + 1
391 FROM symbols s JOIN enclosing ON s.symbol_id = enclosing.symbol_id
392 WHERE s.parent_symbol_id IS NOT NULL AND enclosing.depth < 32
393 )
394 SELECT 1 FROM enclosing WHERE symbol_id = t.parent_symbol_id
395 )";
396
397const SHADOW_IN_CHAIN_SQL: &str = "WITH RECURSIVE chain(symbol_id, depth) AS (
398 SELECT ?1, 0
399 UNION ALL
400 SELECT s.parent_symbol_id, chain.depth + 1
401 FROM symbols s JOIN chain ON s.symbol_id = chain.symbol_id
402 WHERE s.parent_symbol_id IS NOT NULL AND chain.depth < 32
403 )
404 SELECT d.symbol_id, COALESCE(d.signature, ''), d.kind
405 FROM chain
406 JOIN symbols scope ON scope.symbol_id = chain.symbol_id
407 JOIN symbols d ON d.parent_symbol_id = chain.symbol_id
408 WHERE d.name = ?2 AND d.path = ?3
409 AND (d.kind IN ('variable', 'parameter', 'function', 'constant', 'class')
410 OR ?4 AND d.kind IN ('type', 'interface', 'enum'))
411 AND (chain.depth = 0 OR scope.kind IN ('function', 'method', 'constructor'))
412 ORDER BY chain.depth, d.start_line > ?5, abs(?5 - d.start_line)";
413
414const SHADOW_TOP_LEVEL_SQL: &str = "SELECT symbol_id, COALESCE(signature, ''), kind FROM symbols
416 WHERE name = ?1 AND path = ?2 AND parent_symbol_id IS NULL
417 AND kind NOT IN ('import', 'export', 'module', 'namespace')
418 AND NOT (kind = 'method' AND language IN ('javascript', 'typescript', 'tsx', 'jsx', 'vue'))
419 ORDER BY start_line > ?3, abs(?3 - start_line)";
420
421const IMPORT_SOURCES_SQL: &str = "SELECT COALESCE(json_extract(metadata_json, '$.source'), '')
422 FROM symbols
423 WHERE kind = 'import' AND path = ?1 AND json_valid(metadata_json)
424 AND (name = ?2 OR json_extract(metadata_json, '$.alias') = ?2
425 OR json_extract(metadata_json, '$.local_name') = ?2)";
426
427const IMPORT_SIGNATURES_SQL: &str = "SELECT COALESCE(signature, '') FROM symbols
428 WHERE kind = 'import' AND path = ?1 AND name = ?2";
429
430const PYTHON_IMPORT_ROWS_SQL: &str = "SELECT COALESCE(signature, ''),
431 COALESCE(json_extract(metadata_json, '$.source'), ''),
432 COALESCE(json_extract(metadata_json, '$.importedName'), '')
433 FROM symbols
434 WHERE kind = 'import' AND path = ?1 AND json_valid(metadata_json)
435 AND (name = ?2 OR json_extract(metadata_json, '$.alias') = ?2
436 OR json_extract(metadata_json, '$.local_name') = ?2)";
437
438const PENDING_IMPORT_MODULES_SQL: &str = "SELECT target_namespace_json FROM pending_relationships
439 WHERE kind = 'imports' AND path = ?1 AND target_terminal_name = ?2";
440
441const FILE_EXISTS_SQL: &str = "SELECT 1 FROM files WHERE path = ?1 OR path = ?2 LIMIT 1";
442
443const PYTHON_FILES_NAMED_SQL: &str = "SELECT replace(path, '\\', '/') FROM files
444 WHERE language = 'python'
445 AND ('/' || replace(path, '\\', '/') LIKE ?1 OR '/' || replace(path, '\\', '/') LIKE ?2)
446 LIMIT 200";
447
448const PYTHON_FILES_NAMED_CAP: usize = 200;
450
451const SOURCE_FILES_SQL: &str = "SELECT 1 FROM files
452 WHERE '/' || replace(replace(path, '\\', '/'), '-', '_') LIKE ?1 ESCAPE '\\'
453 OR '/' || replace(replace(path, '\\', '/'), '-', '_') LIKE ?2 ESCAPE '\\'
454 LIMIT 1";
455
456const RECEIVER_TYPE_SQL: &str = "WITH RECURSIVE chain(symbol_id, depth) AS (
457 SELECT ?1, 0
458 UNION ALL
459 SELECT s.parent_symbol_id, chain.depth + 1
460 FROM symbols s JOIN chain ON s.symbol_id = chain.symbol_id
461 WHERE s.parent_symbol_id IS NOT NULL AND chain.depth < 32
462 ), typed(depth, resolved_type) AS (
463 SELECT chain.depth, tf.resolved_type
464 FROM chain
465 JOIN symbols r ON r.parent_symbol_id = chain.symbol_id
466 JOIN type_facts tf ON tf.symbol_id = r.symbol_id
467 WHERE r.name = ?2 AND r.path = ?3
468 AND r.kind IN ('variable', 'parameter', 'field', 'property')
469 )
470 SELECT DISTINCT resolved_type FROM typed
471 WHERE depth = (SELECT MIN(depth) FROM typed)";
472
473const ANCESTORS_BY_RELATIONSHIP_SQL: &str = "SELECT DISTINCT base.name
474 FROM symbols child
475 JOIN relationships r ON r.from_symbol_id = child.symbol_id
476 AND r.kind IN ('extends', 'implements')
477 JOIN symbols base ON base.symbol_id = r.to_symbol_id
478 WHERE child.name = ?1";
479
480const ANCESTORS_BY_PENDING_SQL: &str = "SELECT DISTINCT pe.target_terminal_name
481 FROM symbols child
482 JOIN pending_relationships pe ON pe.from_symbol_id = child.symbol_id
483 AND pe.kind IN ('extends', 'implements')
484 WHERE child.name = ?1";
485
486const OWNER_IN_PACKAGE_SQL: &str = "SELECT COUNT(*) FROM symbols
487 WHERE name = ?1
488 AND kind IN ('class', 'struct', 'interface', 'trait', 'enum', 'record', 'object', 'protocol', 'union')
489 AND replace(path, '\\', '/') LIKE ?2 ESCAPE '\\'";
490
491const TYPE_HEAD_KINDS_SQL: &str =
492 "SELECT DISTINCT kind FROM symbols WHERE name = ?1 AND kind != 'parameter'";
493
494const RECEIVER_DECLARATIONS_SQL: &str = "WITH RECURSIVE chain(symbol_id, depth) AS (
495 SELECT ?1, 0
496 UNION ALL
497 SELECT s.parent_symbol_id, chain.depth + 1
498 FROM symbols s JOIN chain ON s.symbol_id = chain.symbol_id
499 WHERE s.parent_symbol_id IS NOT NULL AND chain.depth < 32
500 )
501 SELECT chain.depth, r.kind, r.start_line FROM chain
502 JOIN symbols r ON r.parent_symbol_id = chain.symbol_id
503 WHERE r.name = ?2 AND r.path = ?3 AND r.kind IN ('variable', 'constant', 'parameter')
504 UNION ALL
505 SELECT 99, r.kind, r.start_line FROM symbols r
506 WHERE r.parent_symbol_id IS NULL AND r.name = ?2 AND r.path = ?3
507 AND r.kind IN ('variable', 'constant')
508 ORDER BY 1, 3 DESC";
509
510const JS_IMPORTER_PATHS_SQL: &str = "SELECT DISTINCT replace(path, '\\', '/') FROM symbols
511 WHERE kind = 'import' AND name = ?1
512 AND language IN ('javascript', 'typescript', 'tsx', 'jsx', 'vue')";
513
514const MEMBER_ACCESS_PATHS_SQL: &str =
515 "SELECT DISTINCT path FROM identifiers WHERE name = ?1 AND kind = 'member_access'";
516
517const VARIABLE_REF_SITES_SQL: &str = "SELECT path, start_line, start_column FROM identifiers
518 WHERE name = ?1 AND kind = 'variable_ref' AND start_column IS NOT NULL";
519
520const TYPE_DECLARATIONS_SQL: &str = "SELECT COALESCE(signature, '') FROM symbols
521 WHERE name = ?1 AND kind = 'type' AND language = ?2";
522
523const SYMBOL_CHAIN_SQL: &str = "WITH RECURSIVE chain(symbol_id, depth) AS (
524 SELECT ?1, 0
525 UNION ALL
526 SELECT s.parent_symbol_id, chain.depth + 1
527 FROM symbols s JOIN chain ON s.symbol_id = chain.symbol_id
528 WHERE s.parent_symbol_id IS NOT NULL AND chain.depth < 32
529 )
530 SELECT s.kind, s.name FROM chain JOIN symbols s ON s.symbol_id = chain.symbol_id
531 ORDER BY chain.depth";
532
533const CSHARP_USINGS_SQL: &str = "SELECT COALESCE(signature, '') FROM symbols
534 WHERE kind = 'import' AND language = 'csharp'
535 AND (path = ?1 OR signature LIKE 'global using %')";
536
537const PREPROCESSOR_CONDITION_SQL: &str = "SELECT json_extract(metadata_json, '$.preprocessorIf')
538 FROM symbols WHERE symbol_id = ?1 AND json_valid(metadata_json)";
539
540const CSHARP_FILE_USINGS_SQL: &str = "SELECT COALESCE(signature, ''),
541 CASE WHEN json_valid(metadata_json)
542 THEN json_extract(metadata_json, '$.preprocessorIf') END
543 FROM symbols
544 WHERE kind = 'import' AND language = 'csharp' AND path = ?1";
545
546const CSHARP_CONDITIONAL_USINGS_SQL: &str = "SELECT COALESCE(signature, ''),
547 json_extract(metadata_json, '$.preprocessorIf')
548 FROM symbols
549 WHERE kind = 'import' AND language = 'csharp' AND json_valid(metadata_json)
550 AND json_extract(metadata_json, '$.preprocessorIf') IS NOT NULL";
551
552const MSBUILD_FILES_SQL: &str = "SELECT path FROM files
553 WHERE path LIKE '%.csproj' OR path LIKE '%.props' OR path LIKE '%.targets'";
554
555const CSHARP_TYPE_KINDS: &[&str] = &["class", "struct", "interface", "enum", "record", "delegate"];
556
557const JAVA_STATIC_IMPORTS_SQL: &str = "SELECT signature FROM symbols
558 WHERE kind = 'import' AND path = ?1 AND (name = ?2 OR signature LIKE '%.*')
559 AND signature LIKE 'import static %'";
560
561const CALLABLE_IN_FILE_SQL: &str = "SELECT 1 FROM symbols
562 WHERE path = ?1 AND name = ?2 AND kind IN ('method', 'function', 'constructor') LIMIT 1";
563
564const TOP_LEVEL_VARIABLE_SQL: &str = "SELECT 1 FROM symbols
565 WHERE path = ?1 AND name = ?2 AND parent_symbol_id IS NULL
566 AND kind IN ('variable', 'constant') LIMIT 1";
567
568const FN_TYPES: [&str; 5] = ["fn(", "fn (", "Fn(", "FnMut(", "FnOnce("];
569
570#[derive(Clone, Copy, PartialEq, Eq)]
572enum RowScope {
573 All,
574 Calls,
575 Inheritance,
577}
578
579struct Spec {
580 name: String,
581 group: Vec<Symbol>,
582 scope: RowScope,
583 builds_class: bool,
584 arity: Option<arity::Arity>,
586 local_to: Option<Symbol>,
588}
589
590#[derive(Clone, Copy, PartialEq, Eq, Hash)]
591enum Source {
592 Pending,
593 Relationship,
594 Identifier,
595}
596
597impl Source {
598 fn tag(self) -> &'static str {
599 match self {
600 Source::Pending => "P",
601 Source::Relationship => "R",
602 Source::Identifier => "I",
603 }
604 }
605}
606
607struct NameRow {
608 key: String,
609 source: Source,
610 caller_id: Option<String>,
611 caller_name: String,
612 caller_end_line: Option<usize>,
614 path: String,
615 language: String,
616 kind: String,
617 line: usize,
618 column: Option<usize>,
619 end_line: Option<usize>,
620 receiver: Option<String>,
621 namespace: Vec<String>,
622 to_symbol_id: Option<String>,
623 site_line: usize,
624 site_column: Option<usize>,
625 consumed: bool,
626 paired: bool,
627 hidden_receiver: bool,
630 result_receiver: bool,
633 chain: Option<Vec<String>>,
636 arg_count: Option<u32>,
639 links_enclosing_class: bool,
641}
642
643impl NameRow {
644 fn has_qualifier(&self) -> bool {
645 self.hidden_receiver
646 || self.receiver.as_deref().is_some_and(|r| !r.is_empty())
647 || !self.namespace.is_empty()
648 }
649
650 fn qualifier(&self) -> Option<&str> {
651 self.namespace
652 .first()
653 .map(String::as_str)
654 .or(self.receiver.as_deref())
655 .filter(|q| is_plain_identifier(q))
656 }
657
658 fn is_super_call(&self) -> bool {
659 self.receiver.as_deref().is_some_and(is_super_receiver)
660 }
661
662 fn is_call(&self) -> bool {
663 matches!(self.kind.as_str(), "calls" | "call" | "instantiates")
664 }
665
666 fn is_not_named_by(&self, kind: &str) -> bool {
669 let value_kind = matches!(
670 kind,
671 "field" | "property" | "variable" | "constant" | "parameter"
672 );
673 let java_call = self.language == "java" && matches!(self.kind.as_str(), "calls" | "call");
674 let type_use_of_value =
675 self.is_bare_type_use() && (value_kind || matches!(kind, "function" | "method"));
676 (java_call && value_kind) || type_use_of_value
677 }
678
679 fn is_bare_use(&self) -> bool {
680 !self.has_qualifier()
681 && matches!(
682 self.kind.as_str(),
683 "calls" | "call" | "variable_ref" | "references"
684 )
685 }
686
687 fn is_bare_type_use(&self) -> bool {
690 self.kind == "type_usage"
691 && language_family(&self.language) == "js"
692 && self.chain.as_ref().is_some_and(Vec::is_empty)
693 }
694
695 fn display_kind(&self) -> String {
696 match self.kind.as_str() {
697 "call" => "calls".to_string(),
698 other => other.to_string(),
699 }
700 }
701
702 fn can_reach(&self, definition: &Definition) -> bool {
705 let call_of_a_field = self.is_call()
706 && !definition.callable
707 && languages_compatible(&self.language, &definition.language);
708 let member_of_a_scoped_function =
709 definition.free && language_family(&self.language) == "js" && self.has_qualifier();
710 let call_on_a_result = self.result_receiver
711 && definition.module_value
712 && language_family(&self.language) == "js";
713 !call_of_a_field
714 && !member_of_a_scoped_function
715 && !call_on_a_result
716 && !self.reads_a_module(&definition.kind)
717 && !(self.is_bare_use() && definition.is_property_method())
718 && self.is_in_scope_of(definition)
719 && !(self.is_type_position() && definition.is_value())
720 }
721
722 fn is_type_position(&self) -> bool {
725 STATIC_TYPE_LANGUAGES.contains(&self.language.as_str())
726 && (matches!(
727 self.kind.as_str(),
728 "extends" | "implements" | "instantiates" | "type_usage"
729 ) || self.kind == "uses" && self.language == "csharp")
730 }
731
732 fn is_in_scope_of(&self, definition: &Definition) -> bool {
733 definition.scope.is_none_or(|(start, end)| {
734 self.path == definition.path && (start..=end).contains(&self.site_line)
735 })
736 }
737
738 fn reads_a_module(&self, kind: &str) -> bool {
740 self.language == "rust"
741 && self.kind == "variable_ref"
742 && matches!(kind, "namespace" | "module")
743 }
744}
745
746fn is_super_receiver(receiver: &str) -> bool {
749 matches!(receiver, "super" | "base") || receiver.starts_with("super<")
750}
751
752const STATIC_TYPE_LANGUAGES: &[&str] = &[
756 "csharp", "java", "kotlin", "scala", "swift", "dart", "rust", "go", "cpp", "c", "vbnet",
757];
758
759const LOCAL_FUNCTION_LANGUAGES: &[&str] = &[
760 "python", "rust", "kotlin", "csharp", "swift", "scala", "dart",
761];
762
763fn enclosing_function(conn: &Connection, group: &[Symbol]) -> Result<Option<Symbol>, QueryError> {
766 let Some(parent) = group
767 .first()
768 .and_then(|first| first.parent_symbol_id.as_deref())
769 else {
770 return Ok(None);
771 };
772 let local = group.iter().all(|member| {
773 member.kind == "function"
774 && LOCAL_FUNCTION_LANGUAGES.contains(&member.language.as_str())
775 && member.parent_symbol_id.as_deref() == Some(parent)
776 });
777 if !local {
778 return Ok(None);
779 }
780 Ok(crate::queries::get_symbol_by_id(conn, parent)?
781 .filter(|outer| matches!(outer.kind.as_str(), "function" | "method" | "constructor")))
782}
783
784fn is_property_method(symbol: &Symbol) -> bool {
787 symbol.kind == "method"
788 && symbol.parent_symbol_id.is_none()
789 && language_family(&symbol.language) == "js"
790}
791
792fn assigned_object(symbol: &Symbol) -> Option<String> {
795 if language_family(&symbol.language) != "js" {
796 return None;
797 }
798 let signature = symbol.signature.as_deref()?;
799 let (object, _) = signature.split_once(&format!(".{} =", symbol.name))?;
800 Some(object.trim().to_string()).filter(|object| is_plain_identifier(object))
801}
802
803pub(crate) fn preprocessor_condition(
806 conn: &Connection,
807 symbol_id: &str,
808) -> Result<Option<String>, QueryError> {
809 Ok(conn
810 .query_row(PREPROCESSOR_CONDITION_SQL, params![symbol_id], |r| {
811 r.get::<_, Option<String>>(0)
812 })
813 .optional()?
814 .flatten())
815}
816
817fn condition_terms(condition: &str) -> impl Iterator<Item = &str> {
820 condition.split(" && ").map(str::trim)
821}
822
823fn csharp_using_namespace(signature: &str) -> Option<&str> {
826 let signature = signature.trim();
827 let directive = signature.strip_prefix("global ").unwrap_or(signature);
828 let namespace = directive
829 .strip_prefix("using ")?
830 .trim()
831 .trim_end_matches(';')
832 .trim();
833 (!namespace.starts_with("static ") && !namespace.contains('=')).then_some(namespace)
834}
835
836fn extended_type(signature: &str) -> Option<String> {
838 let rest = signature
839 .match_indices('(')
840 .find_map(|(open, _)| arity::after_csharp_this(&signature[open + 1..]))?;
841 let written = rest.split_whitespace().next()?;
842 let unqualified = written.split('<').next()?.rsplit('.').next()?;
843 Some(unqualified.trim_end_matches('?').to_string()).filter(|t| is_plain_identifier(t))
844}
845
846fn is_test_label(signature: &str) -> bool {
849 signature.split_once('(').is_some_and(|(callee, rest)| {
850 !callee.is_empty()
851 && callee.split('.').all(is_plain_identifier)
852 && rest.trim_start().starts_with(['"', '\'', '`'])
853 })
854}
855
856fn is_plain_identifier(text: &str) -> bool {
857 let mut chars = text.chars();
858 chars
859 .next()
860 .is_some_and(|c| c.is_alphabetic() || c == '_' || c == '$')
861 && chars.all(is_word_char)
862}
863
864fn is_word_char(c: char) -> bool {
865 c.is_alphanumeric() || c == '_' || c == '$'
866}
867
868fn is_record(signature: &str) -> bool {
871 let declaration = outside_quotes_and_parentheses(signature);
872 declaration.match_indices("record").any(|(at, word)| {
873 let after = &declaration[at + word.len()..];
874 !declaration[..at].ends_with(is_word_char)
875 && after.starts_with(char::is_whitespace)
876 && after.trim_start().starts_with(is_word_char)
877 })
878}
879
880fn outside_quotes_and_parentheses(text: &str) -> String {
883 let mut outside = String::with_capacity(text.len());
884 let mut depth = 0usize;
885 let mut chars = text.chars();
886 while let Some(c) = chars.next() {
887 match c {
888 '"' | '\'' => {
889 while let Some(inner) = chars.next() {
890 match inner {
891 '\\' => {
892 chars.next();
893 }
894 _ if inner == c => break,
895 _ => {}
896 }
897 }
898 if depth == 0 {
899 outside.push(' ');
900 }
901 }
902 '(' => depth += 1,
903 ')' => {
904 depth = depth.saturating_sub(1);
905 if depth == 0 {
906 outside.push(' ');
907 }
908 }
909 _ if depth == 0 => outside.push(c),
910 _ => {}
911 }
912 }
913 outside
914}
915
916fn is_function_type(signature: &str, name: &str, language: &str) -> bool {
919 let Some(declared) = signature
920 .trim_start()
921 .strip_prefix("type")
922 .and_then(|rest| rest.trim_start().strip_prefix(name))
923 else {
924 return false;
925 };
926 let declared = declared.trim_start();
927 let declared = match declared.chars().next() {
928 Some(open @ ('[' | '<')) => {
929 let close = if open == '[' { ']' } else { '>' };
930 let mut depth = 0;
931 let end = declared.char_indices().find(|&(_, c)| {
932 depth += i32::from(c == open) - i32::from(c == close);
933 depth == 0
934 });
935 end.map_or("", |(end, _)| &declared[end + 1..])
936 }
937 _ => declared,
938 };
939 let declared = declared.trim_start().trim_start_matches('=').trim_start();
940 if language == "go" {
941 declared.starts_with("func")
942 } else {
943 FN_TYPES.iter().any(|fn_type| declared.contains(fn_type))
944 }
945}
946
947fn split_line<'a>(lines: &[&'a str], line: usize, column: usize) -> Option<(&'a str, &'a str)> {
949 lines.get(line.checked_sub(1)?)?.split_at_checked(column)
950}
951
952fn after_blanks_and_comments(mut text: &str) -> &str {
954 loop {
955 text = text.trim_start();
956 if text.starts_with("//") {
957 return "";
958 }
959 match text.strip_prefix("/*") {
960 Some(comment) => text = comment.split_once("*/").map_or("", |(_, rest)| rest),
961 None => return text,
962 }
963 }
964}
965
966fn before_blanks_and_comments(mut text: &str) -> &str {
968 loop {
969 text = text.trim_end();
970 match text
971 .strip_suffix("*/")
972 .and_then(|comment| comment.rsplit_once("/*"))
973 {
974 Some((rest, _)) => text = rest,
975 None => return text,
976 }
977 }
978}
979
980fn is_object_value(lines: &[&str], line: usize, column: usize, name: &str) -> bool {
985 let Some((before, rest)) = split_line(lines, line, column) else {
986 return false;
987 };
988 let after = rest.get(name.len()..).unwrap_or("");
989 before_blanks_and_comments(before)
990 .chars()
991 .next_back()
992 .is_none_or(|c| matches!(c, '{' | ',' | ':'))
993 && after_blanks_and_comments(after)
994 .chars()
995 .next()
996 .is_none_or(|c| matches!(c, ',' | '}'))
997}
998
999fn is_object_shorthand_site(text: &str, line: usize, column: usize, name: &str) -> bool {
1000 let Some(source_line) = source_line(text, line) else {
1001 return false;
1002 };
1003 let Some((_, after)) = source_line.split_at_checked(column) else {
1004 return false;
1005 };
1006 if !matches!(
1007 after_blanks_and_comments(after.get(name.len()..).unwrap_or(""))
1008 .chars()
1009 .next(),
1010 Some(',' | '}')
1011 ) {
1012 return false;
1013 }
1014 let Some(site) = line_start_offset(text, line).and_then(|start| start.checked_add(column))
1015 else {
1016 return false;
1017 };
1018 if site > 4096 {
1019 return false;
1020 }
1021 let Some(prefix) = text.get(..site) else {
1022 return false;
1023 };
1024 let Some(open) = js_bindings::unmatched_brace_before(prefix) else {
1025 return false;
1026 };
1027 let object_prefix = &prefix[open + 1..];
1028 if object_prefix.contains('{') || object_prefix.contains('}') {
1029 return false;
1030 }
1031 if before_blanks_and_comments(object_prefix)
1032 .chars()
1033 .next_back()
1034 .is_some_and(|c| !matches!(c, '{' | ','))
1035 {
1036 return false;
1037 }
1038 let before_open = prefix[..open].trim_end();
1039 let after_open = before_open.chars().next_back();
1040 if after_open == Some('=')
1041 && !before_open
1042 .chars()
1043 .rev()
1044 .nth(1)
1045 .is_some_and(|c| matches!(c, '=' | '!' | '<' | '>'))
1046 {
1047 return true;
1048 }
1049 if let Some(before_return) = before_open.strip_suffix("return")
1050 && (before_return.is_empty()
1051 || !is_word_char(before_return.chars().next_back().unwrap_or(' ')))
1052 && !prefix[before_return.len() + "return".len()..open]
1053 .chars()
1054 .any(|c| matches!(c, '\n' | '\r'))
1055 {
1056 return true;
1057 }
1058 let Some(call_head) = before_open.strip_suffix('(') else {
1059 return false;
1060 };
1061 let Some((before_new, constructor)) = call_head.rsplit_once("new ") else {
1062 return false;
1063 };
1064 !constructor.is_empty()
1065 && constructor.chars().all(|c| is_word_char(c) || c == '.')
1066 && !before_new.chars().next_back().is_some_and(is_word_char)
1067}
1068
1069fn line_start_offset(text: &str, line: usize) -> Option<usize> {
1070 let breaks = line.checked_sub(1)?;
1071 if breaks == 0 {
1072 return Some(0);
1073 }
1074 text.match_indices('\n')
1075 .nth(breaks - 1)
1076 .map(|(at, _)| at + 1)
1077}
1078
1079fn source_line(text: &str, line: usize) -> Option<&str> {
1080 let start = line_start_offset(text, line)?;
1081 text.get(start..)?
1082 .split_inclusive('\n')
1083 .next()
1084 .map(|line| line.strip_suffix('\n').unwrap_or(line))
1085 .map(|line| line.strip_suffix('\r').unwrap_or(line))
1086}
1087
1088fn caller_source_through_site(
1089 root: &Path,
1090 path: &str,
1091 scope_start_line: usize,
1092 site_line: usize,
1093) -> Option<String> {
1094 if scope_start_line == 0 || site_line < scope_start_line {
1095 return None;
1096 }
1097 const PREFIX_BYTE_CAP: u64 = 512 * 1024;
1098 const SNIPPET_BYTE_CAP: usize = 4096;
1099 const SNIPPET_LINE_CAP: usize = 64;
1100 if site_line - scope_start_line >= SNIPPET_LINE_CAP {
1101 return None;
1102 }
1103 let file = std::fs::File::open(root.join(path)).ok()?;
1104 let file = std::io::Read::take(file, PREFIX_BYTE_CAP);
1105 let mut source = std::io::BufReader::new(file);
1106 for _ in 1..scope_start_line {
1107 skip_source_line(&mut source)?;
1108 }
1109 let mut snippet = Vec::with_capacity(SNIPPET_BYTE_CAP);
1110 for line in scope_start_line..=site_line {
1111 append_source_line(
1112 &mut source,
1113 &mut snippet,
1114 SNIPPET_BYTE_CAP,
1115 line == site_line,
1116 )?;
1117 }
1118 String::from_utf8(snippet).ok()
1119}
1120
1121fn skip_source_line(reader: &mut impl std::io::BufRead) -> Option<()> {
1122 loop {
1123 let (consumed, newline) = {
1124 let bytes = std::io::BufRead::fill_buf(reader).ok()?;
1125 if bytes.is_empty() {
1126 return None;
1127 }
1128 match bytes.iter().position(|byte| *byte == b'\n') {
1129 Some(position) => (position + 1, true),
1130 None => (bytes.len(), false),
1131 }
1132 };
1133 std::io::BufRead::consume(reader, consumed);
1134 if newline {
1135 return Some(());
1136 }
1137 }
1138}
1139
1140fn append_source_line(
1141 reader: &mut std::io::BufReader<std::io::Take<std::fs::File>>,
1142 output: &mut Vec<u8>,
1143 byte_cap: usize,
1144 allow_unterminated: bool,
1145) -> Option<()> {
1146 let mut copied = false;
1147 loop {
1148 let bytes = std::io::BufRead::fill_buf(reader).ok()?;
1149 if bytes.is_empty() {
1150 return (copied && allow_unterminated && reader.get_ref().limit() > 0).then_some(());
1151 }
1152 let count = bytes
1153 .iter()
1154 .position(|byte| *byte == b'\n')
1155 .map_or(bytes.len(), |position| position + 1);
1156 if output.len().saturating_add(count) > byte_cap {
1157 return None;
1158 }
1159 let newline = bytes[count - 1] == b'\n';
1160 output.extend_from_slice(&bytes[..count]);
1161 std::io::BufRead::consume(reader, count);
1162 copied = true;
1163 if newline {
1164 return Some(());
1165 }
1166 }
1167}
1168
1169#[derive(Clone)]
1170struct Definition {
1171 symbol_id: String,
1172 name: String,
1173 path: String,
1174 start_line: usize,
1175 decorated_start_line: Option<usize>,
1177 body_start_line: Option<usize>,
1178 owner_name: Option<String>,
1179 owner_kind: Option<String>,
1180 local: bool,
1181 scope: Option<(usize, usize)>,
1184 kind: String,
1185 language: String,
1186 signature: String,
1187 owner_signature: String,
1188 class_name: Option<String>,
1190 top_level: bool,
1191 callable: bool,
1194 free: bool,
1197 module_value: bool,
1201}
1202
1203struct ShorthandSite {
1204 target_member: bool,
1205 shadow_id: Option<String>,
1206}
1207
1208impl Definition {
1209 fn is_js_shorthand_member_target(&self) -> bool {
1210 language_family(&self.language) == "js"
1211 && self
1212 .owner_kind
1213 .as_deref()
1214 .is_some_and(|kind| CLASS_LIKE_KINDS.contains(&kind))
1215 && (matches!(self.kind.as_str(), "field" | "property")
1216 || self.kind == "method" && self.owner_kind.as_deref() == Some("interface"))
1217 }
1218
1219 fn is_value(&self) -> bool {
1220 matches!(
1221 self.kind.as_str(),
1222 "field" | "constant" | "variable" | "property" | "parameter" | "enum_member"
1223 )
1224 }
1225
1226 fn is_python_class_annotation_without_value(&self) -> bool {
1227 if self.language != "python"
1228 || self.owner_name.is_none()
1229 || !matches!(self.kind.as_str(), "field" | "variable" | "constant")
1230 || arity::default_start(&self.signature).is_some()
1231 {
1232 return false;
1233 }
1234 let owner_signature = self.owner_signature.as_str();
1235 let dataclass = owner_signature.split_whitespace().any(|part| {
1236 let Some(decorator) = part.strip_prefix('@') else {
1237 return false;
1238 };
1239 let name = decorator
1240 .split_once('(')
1241 .map_or(decorator, |(name, _)| name);
1242 name.rsplit('.').next() == Some("dataclass")
1243 });
1244 let typed_dict = owner_signature
1245 .split(|c: char| !c.is_alphanumeric() && c != '_')
1246 .any(|word| word == "TypedDict");
1247 !dataclass && !typed_dict
1248 }
1249
1250 fn dynamic_import(&self, name: &str) -> Option<(String, String)> {
1253 (self.kind == "variable" && language_family(&self.language) == "js")
1254 .then(|| js_modules::dynamic_import_binding(&self.signature, name))
1255 .flatten()
1256 }
1257
1258 fn is_property_method(&self) -> bool {
1260 self.kind == "method" && self.top_level && language_family(&self.language) == "js"
1261 }
1262
1263 fn display(&self) -> String {
1264 match &self.owner_name {
1265 Some(owner) => format!("{owner}.{}", self.name),
1266 None => self.name.clone(),
1267 }
1268 }
1269
1270 fn resolves_to(&self) -> String {
1271 format!(
1272 "resolves to `{}` ({}:{})",
1273 self.display(),
1274 self.path,
1275 self.start_line
1276 )
1277 }
1278}
1279
1280#[derive(Clone)]
1281struct Verdict {
1282 label: SiteLabel,
1283 basis: String,
1284 claimed: bool,
1285}
1286
1287impl Verdict {
1288 fn new(label: SiteLabel, basis: impl Into<String>, claimed: bool) -> Self {
1289 Verdict {
1290 label,
1291 basis: basis.into(),
1292 claimed,
1293 }
1294 }
1295}
1296
1297struct SiteRow {
1298 site: ReferenceSite,
1299 label: SiteLabel,
1300 basis: String,
1301 claimed: bool,
1302 end_line: Option<usize>,
1303 arg_count: Option<u32>,
1304}
1305
1306struct Claim {
1307 basis: Option<String>,
1308}
1309
1310struct Judged {
1311 claimed: bool,
1312 basis: Option<String>,
1313}
1314
1315struct TextOutcome {
1316 counts: ReferenceCounts,
1317 text_only: Vec<TextOnlyLines>,
1318 summary: TextScanSummary,
1319}
1320
1321impl TextOutcome {
1322 fn not_run() -> Self {
1323 TextOutcome {
1324 counts: ReferenceCounts::default(),
1325 text_only: Vec::new(),
1326 summary: TextScanSummary {
1327 files_scanned: 0,
1328 files_total: 0,
1329 complete: true,
1330 elapsed_ms: 0,
1331 skipped: None,
1332 },
1333 }
1334 }
1335
1336 fn skipped(class: &Symbol) -> Self {
1337 TextOutcome {
1338 counts: ReferenceCounts::default(),
1339 text_only: Vec::new(),
1340 summary: TextScanSummary {
1341 files_scanned: 0,
1342 files_total: 0,
1343 complete: true,
1344 elapsed_ms: 0,
1345 skipped: Some(class.name.clone()),
1346 },
1347 }
1348 }
1349}
1350
1351type ImportMatch = (String, String, String);
1353
1354struct Accounting<'a> {
1355 conn: &'a Connection,
1356 root: &'a Path,
1357 judge_deadline: Instant,
1359 deadline: Instant,
1360 judge_stmt: RefCell<Option<Statement<'a>>>,
1361 absent: Option<Symbol>,
1364 absent_judge_stmt: RefCell<Option<Statement<'a>>>,
1365 target_condition: Option<String>,
1367 definitions_stmt: RefCell<Option<Statement<'a>>>,
1368 import_match_stmt: Option<Statement<'a>>,
1369 statements: RefCell<HashMap<&'static str, Statement<'a>>>,
1370 external_sources: HashMap<String, bool>,
1371 msbuild_usings: std::cell::OnceCell<bool>,
1372 import_matches: HashMap<(String, String, String), Vec<ImportMatch>>,
1374 js: js_modules::JsCaches,
1375 receivers: receiver_types::ReceiverCaches,
1376}
1377
1378impl<'a> Accounting<'a> {
1379 fn new(conn: &'a Connection, root: &'a Path, time_limit: Duration) -> Self {
1380 Accounting {
1381 conn,
1382 root,
1383 judge_deadline: Instant::now() + time_limit / 2,
1384 deadline: Instant::now() + time_limit,
1385 judge_stmt: RefCell::new(None),
1386 absent: None,
1387 absent_judge_stmt: RefCell::new(None),
1388 target_condition: None,
1389 definitions_stmt: RefCell::new(None),
1390 import_match_stmt: None,
1391 statements: RefCell::new(HashMap::new()),
1392 external_sources: HashMap::new(),
1393 msbuild_usings: std::cell::OnceCell::new(),
1394 import_matches: HashMap::new(),
1395 js: js_modules::JsCaches::default(),
1396 receivers: receiver_types::ReceiverCaches::default(),
1397 }
1398 }
1399
1400 fn out_of_time(&self) -> bool {
1401 Instant::now() >= self.deadline
1402 }
1403
1404 fn restart_clock(&mut self, time_limit: Duration) {
1407 self.judge_deadline = Instant::now() + time_limit / 2;
1408 self.deadline = Instant::now() + time_limit;
1409 }
1410
1411 fn with_statement<T>(
1414 &self,
1415 sql: &'static str,
1416 run: impl FnOnce(&mut Statement<'a>) -> rusqlite::Result<T>,
1417 ) -> Result<T, QueryError> {
1418 let mut statements = self.statements.borrow_mut();
1419 if !statements.contains_key(sql) {
1420 statements.insert(sql, self.conn.prepare(sql)?);
1421 }
1422 let stmt = statements
1423 .get_mut(sql)
1424 .expect("statement was just prepared");
1425 Ok(run(stmt)?)
1426 }
1427
1428 fn rows_at_path<T>(
1431 &self,
1432 sql: &'static str,
1433 path: &str,
1434 others: &[&dyn ToSql],
1435 read: fn(&Row) -> rusqlite::Result<T>,
1436 ) -> Result<Vec<T>, QueryError> {
1437 let windows = path.replace('/', "\\");
1438 let mut spellings = vec![path];
1439 if windows != path {
1440 spellings.push(&windows);
1441 }
1442 let mut rows = Vec::new();
1443 for spelling in spellings {
1444 let bound = std::iter::once(&spelling as &dyn ToSql).chain(others.iter().copied());
1445 rows.extend(self.with_statement(sql, |stmt| {
1446 stmt.query_map(params_from_iter(bound), read)?
1447 .collect::<Result<Vec<_>, _>>()
1448 })?);
1449 }
1450 Ok(rows)
1451 }
1452
1453 fn definitions_sql(&self) -> String {
1454 let decorated_start = if has_column(self.conn, "structural_facts", "end_byte")
1455 && has_column(self.conn, "symbols", "end_byte")
1456 {
1457 "(SELECT MIN(f.start_line) FROM structural_facts f
1458 WHERE f.path = s.path AND f.pattern_id LIKE '%.decorated_definition.%'
1459 AND f.end_byte = s.end_byte AND f.start_byte < s.start_byte)"
1460 } else {
1461 "NULL"
1462 };
1463 format!(
1464 "SELECT s.symbol_id, s.name, s.kind, s.language, s.path, s.start_line,
1465 s.body_start_line, s.parent_symbol_id, par.name, par.kind, {decorated_start},
1466 COALESCE(s.signature, ''), s.visibility,
1467 CASE WHEN json_valid(s.metadata_json) THEN
1468 json_extract(s.metadata_json, '$.className') END,
1469 CASE WHEN json_valid(s.metadata_json) THEN
1470 json_extract(s.metadata_json, '$.isCommonJSExport') END,
1471 COALESCE(par.signature, ''), par.start_line, par.end_line
1472 FROM symbols s LEFT JOIN symbols par ON par.symbol_id = s.parent_symbol_id
1473 WHERE s.name = ?1 AND s.kind NOT IN ('import', 'export')
1474 AND s.language NOT IN ({})",
1475 documentation_language_list()
1476 )
1477 }
1478
1479 fn definitions(&self, name: &str) -> Result<Vec<Definition>, QueryError> {
1480 let mut slot = self.definitions_stmt.borrow_mut();
1481 if slot.is_none() {
1482 *slot = Some(self.conn.prepare(&self.definitions_sql())?);
1483 }
1484 let stmt = slot
1485 .as_mut()
1486 .expect("definitions statement was just prepared");
1487 let rows = stmt.query_map(params![name], |row| {
1488 let signature: String = row.get(11)?;
1489 if is_test_label(&signature) {
1490 return Ok(None);
1491 }
1492 let kind: String = row.get(2)?;
1493 let language: String = row.get(3)?;
1494 let parent_id: Option<String> = row.get(7)?;
1495 let parent_kind: Option<String> = row.get(9)?;
1496 let parent_name: Option<String> = row.get(8)?;
1497 let owner_signature: String = row.get(15)?;
1498 let class_name: Option<String> = row.get(13)?;
1499 let in_function = parent_kind
1500 .as_deref()
1501 .is_some_and(|k| matches!(k, "function" | "method" | "constructor"));
1502 let local = kind == "parameter" || kind == "variable" && in_function;
1503 let scope = match (
1504 row.get::<_, Option<i64>>(16)?,
1505 row.get::<_, Option<i64>>(17)?,
1506 ) {
1507 (Some(start), Some(end))
1508 if in_function
1509 && kind == "function"
1510 && LOCAL_FUNCTION_LANGUAGES.contains(&language.as_str()) =>
1511 {
1512 Some((start as usize, end as usize))
1513 }
1514 _ => None,
1515 };
1516 let exported = row.get::<_, Option<String>>(12)?.as_deref() == Some("public")
1517 || row.get::<_, Option<i64>>(14)? == Some(1);
1518 let module_value = language_family(&language) == "js"
1519 && matches!(kind.as_str(), "function" | "variable" | "constant")
1520 && class_name.is_none()
1521 && parent_id.is_none();
1522 let free = language_family(&language) == "js"
1523 && matches!(kind.as_str(), "function" | "variable" | "constant")
1524 && class_name.is_none()
1525 && !exported
1526 && !local
1527 && (parent_id.is_none() || in_function);
1528 let record_component = language == "java"
1529 && kind == "property"
1530 && is_record(&owner_signature)
1531 && !signature.split_whitespace().any(|word| word == "static");
1532 let function_typed = language == "rust"
1533 && matches!(kind.as_str(), "variable" | "constant")
1534 && FN_TYPES.iter().any(|fn_type| signature.contains(fn_type));
1535 let callable = record_component
1536 || function_typed
1537 || !(matches!(language.as_str(), "go" | "java" | "rust")
1538 && matches!(
1539 kind.as_str(),
1540 "field" | "property" | "variable" | "constant"
1541 ));
1542 Ok(Some(Definition {
1543 symbol_id: row.get(0)?,
1544 name: row.get(1)?,
1545 path: row.get::<_, String>(4)?.replace('\\', "/"),
1546 start_line: row.get::<_, i64>(5)? as usize,
1547 decorated_start_line: row.get::<_, Option<i64>>(10)?.map(|v| v as usize),
1548 body_start_line: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
1549 owner_name: parent_name.filter(|_| {
1550 parent_kind
1551 .as_deref()
1552 .is_some_and(|k| CLASS_LIKE_KINDS.contains(&k))
1553 }),
1554 owner_kind: parent_kind.filter(|kind| CLASS_LIKE_KINDS.contains(&kind.as_str())),
1555 local,
1556 scope,
1557 top_level: parent_id.is_none(),
1558 kind,
1559 language,
1560 signature,
1561 owner_signature,
1562 class_name,
1563 callable,
1564 free,
1565 module_value,
1566 }))
1567 })?;
1568 let mut definitions: Vec<Definition> = rows
1569 .filter_map(Result::transpose)
1570 .collect::<Result<_, _>>()?;
1571 drop(slot);
1572 let writes =
1573 inherited_writes_among(self.conn, definitions.iter().map(|d| d.symbol_id.as_str()))?;
1574 definitions.retain(|d| !writes.contains(&d.symbol_id) && d.dynamic_import(name).is_none());
1575 let mut probed: HashSet<&str> = definitions
1576 .iter()
1577 .filter(|d| d.free || d.module_value)
1578 .map(|d| d.path.as_str())
1579 .collect();
1580 let importers = if definitions.iter().any(|d| d.module_value) {
1581 self.with_statement(JS_IMPORTER_PATHS_SQL, |stmt| {
1582 stmt.query_map(params![name], |r| r.get::<_, String>(0))?
1583 .collect::<Result<Vec<_>, _>>()
1584 })?
1585 } else {
1586 Vec::new()
1587 };
1588 probed.extend(importers.iter().map(String::as_str));
1589 let member_paths = if probed.is_empty() {
1590 HashSet::new()
1591 } else {
1592 self.paths_using_as_member(name, &probed)?
1593 };
1594 let held_by_importer = importers.iter().any(|path| member_paths.contains(path));
1595 for definition in &mut definitions {
1596 definition.free &= !member_paths.contains(&definition.path);
1597 definition.module_value &=
1598 !held_by_importer && !member_paths.contains(&definition.path);
1599 if !definition.callable {
1600 definition.callable = match definition.language.as_str() {
1601 "go" => self.holds_func(definition)?,
1602 "rust" => self.holds_fn_alias(definition)?,
1603 _ => false,
1604 };
1605 }
1606 }
1607 Ok(definitions)
1608 }
1609
1610 fn paths_using_as_member(
1613 &self,
1614 name: &str,
1615 paths: &HashSet<&str>,
1616 ) -> Result<HashSet<String>, QueryError> {
1617 if !crate::queries::has_table(self.conn, "identifiers") {
1618 return Ok(HashSet::new());
1619 }
1620 let normalized = |path: String| path.replace('\\', "/");
1621 let mut found: HashSet<String> = self.with_statement(MEMBER_ACCESS_PATHS_SQL, |stmt| {
1622 stmt.query_map(params![name], |r| r.get::<_, String>(0))?
1623 .map(|path| path.map(normalized))
1624 .collect()
1625 })?;
1626 let mut sites: HashMap<String, Vec<(usize, usize)>> = HashMap::new();
1627 let rows = self.with_statement(VARIABLE_REF_SITES_SQL, |stmt| {
1628 stmt.query_map(params![name], |r| {
1629 Ok((
1630 normalized(r.get::<_, String>(0)?),
1631 r.get::<_, i64>(1)? as usize,
1632 r.get::<_, i64>(2)? as usize,
1633 ))
1634 })?
1635 .collect::<Result<Vec<_>, _>>()
1636 })?;
1637 for (path, line, column) in rows {
1638 if paths.contains(path.as_str()) && !found.contains(&path) {
1639 sites.entry(path).or_default().push((line, column));
1640 }
1641 }
1642 for (path, sites) in sites {
1643 let Ok(text) = std::fs::read_to_string(self.root.join(&path)) else {
1644 continue;
1645 };
1646 let lines: Vec<&str> = text.lines().collect();
1647 if sites
1648 .iter()
1649 .any(|&(line, column)| is_object_value(&lines, line, column, name))
1650 {
1651 found.insert(path);
1652 }
1653 }
1654 Ok(found)
1655 }
1656
1657 fn holds_func(&self, definition: &Definition) -> Result<bool, QueryError> {
1660 if definition.kind == "constant" {
1661 return Ok(false);
1662 }
1663 let signature = definition.signature.trim_start();
1664 let signature = ["var ", "const "]
1665 .iter()
1666 .find_map(|keyword| signature.strip_prefix(keyword))
1667 .unwrap_or(signature);
1668 let declared = signature
1669 .trim_start()
1670 .strip_prefix(definition.name.as_str())
1671 .filter(|rest| !rest.starts_with(|c: char| c.is_alphanumeric() || c == '_'))
1672 .map_or("", str::trim_start);
1673 if declared.is_empty() || declared.starts_with('=') {
1674 return Ok(true);
1675 }
1676 let type_name: String = declared
1677 .chars()
1678 .take_while(|c| c.is_alphanumeric() || *c == '_')
1679 .collect();
1680 if type_name.is_empty() {
1681 return Ok(false);
1682 }
1683 if type_name == "func" || declared[type_name.len()..].starts_with('.') {
1684 return Ok(true);
1685 }
1686 self.declares_function_type(&type_name, "go")
1687 }
1688
1689 fn holds_fn_alias(&self, definition: &Definition) -> Result<bool, QueryError> {
1692 let declared = definition
1693 .signature
1694 .split_once(definition.name.as_str())
1695 .and_then(|(_, rest)| rest.trim_start().strip_prefix(':'))
1696 .and_then(|declared| declared.split(['=', ';']).next())
1697 .and_then(|declared| declared.rsplit("::").next())
1698 .map(str::trim);
1699 match declared {
1700 Some(type_name) if is_plain_identifier(type_name) => {
1701 self.declares_function_type(type_name, "rust")
1702 }
1703 _ => Ok(false),
1704 }
1705 }
1706
1707 fn declares_function_type(&self, type_name: &str, language: &str) -> Result<bool, QueryError> {
1709 let signatures = self.with_statement(TYPE_DECLARATIONS_SQL, |stmt| {
1710 stmt.query_map(params![type_name, language], |r| r.get::<_, String>(0))?
1711 .collect::<Result<Vec<_>, _>>()
1712 })?;
1713 Ok(signatures
1714 .iter()
1715 .any(|signature| is_function_type(signature, type_name, language)))
1716 }
1717
1718 fn definition_count(&self, name: &str) -> Result<usize, QueryError> {
1719 Ok(self.definitions(name)?.iter().filter(|d| !d.local).count())
1720 }
1721
1722 fn account_spec(
1723 &mut self,
1724 spec: &Spec,
1725 out: &mut Vec<SiteRow>,
1726 cannot_see: &mut Vec<String>,
1727 ) -> Result<(), QueryError> {
1728 let definitions = self.definitions(&spec.name)?;
1729 let group_ids: HashSet<&str> = spec.group.iter().map(|s| s.symbol_id.as_str()).collect();
1730 let target = &spec.group[0];
1731
1732 let mut rows = self.load_rows(&spec.name, &definitions, spec.scope)?;
1733 pair_identifiers(&mut rows);
1734 rows.retain(|row| !row.consumed);
1735 mark_hidden_receivers(self.root, &mut rows);
1736 self.mark_result_variables(&mut rows)?;
1737 js_modules::complete_js_chains(self.root, &mut rows);
1738
1739 let shorthand_targets: Vec<&Definition> = definitions
1740 .iter()
1741 .filter(|definition| group_ids.contains(definition.symbol_id.as_str()))
1742 .collect();
1743 let shorthand_member_target = !shorthand_targets.is_empty()
1744 && shorthand_targets
1745 .iter()
1746 .all(|definition| definition.is_js_shorthand_member_target());
1747 let mut shorthand_shadows = HashMap::new();
1748
1749 let mut order: Vec<usize> = (0..rows.len()).collect();
1750 order.sort_by(|&a, &b| {
1751 let (a, b) = (&rows[a], &rows[b]);
1752 shared_leading_segments(&b.path, &target.path)
1753 .cmp(&shared_leading_segments(&a.path, &target.path))
1754 .then_with(|| a.path.cmp(&b.path))
1755 .then_with(|| a.line.cmp(&b.line))
1756 });
1757 let (claims, judged) = self.claims(spec, &rows, &order, cannot_see)?;
1758 let mut group = definitions
1759 .iter()
1760 .filter(|d| group_ids.contains(d.symbol_id.as_str()))
1761 .peekable();
1762 let module_target = group.peek().is_some() && group.all(|d| d.module_value);
1763 let mut unchecked: Vec<usize> = order[judged..].to_vec();
1764 let mut verdicts: HashMap<String, Verdict> = HashMap::new();
1765 let mut undecided: Vec<usize> = Vec::new();
1766 for (position, &index) in order[..judged].iter().enumerate() {
1767 if self.out_of_time() {
1768 unchecked.extend_from_slice(&order[position..judged]);
1769 break;
1770 }
1771 let row = &rows[index];
1772 let mut shorthand_site = ShorthandSite {
1773 target_member: shorthand_member_target,
1774 shadow_id: None,
1775 };
1776 match self.decide_cheap(row, spec, target, &group_ids, &claims, &mut shorthand_site)? {
1777 Some(verdict) => {
1778 verdicts.insert(row.key.clone(), verdict);
1779 }
1780 None if module_target
1781 && row.result_receiver
1782 && language_family(&row.language) == "js" =>
1783 {
1784 verdicts.insert(
1785 row.key.clone(),
1786 Verdict::new(
1787 SiteLabel::Excluded,
1788 format!(
1789 "a call on a call result never reaches the top-level `{}`",
1790 spec.name
1791 ),
1792 false,
1793 ),
1794 );
1795 }
1796 None => undecided.push(index),
1797 }
1798 if let Some(shadow) = shorthand_site.shadow_id {
1799 shorthand_shadows.insert(row.key.clone(), shadow);
1800 }
1801 }
1802
1803 let mut others: Vec<&Definition> = definitions
1804 .iter()
1805 .filter(|d| !d.local && !group_ids.contains(d.symbol_id.as_str()))
1806 .filter(|d| !(spec.scope == RowScope::Inheritance && d.kind == "constructor"))
1807 .collect();
1808 others.sort_by_key(|d| {
1809 (
1810 std::cmp::Reverse(shared_leading_segments(&d.path, &target.path)),
1811 d.path.clone(),
1812 d.start_line,
1813 )
1814 });
1815 let mut checked = 0;
1816 for other in &others {
1817 if undecided.is_empty() || checked >= OTHER_DEFINITION_CAP || self.out_of_time() {
1818 break;
1819 }
1820 let mut kept = Vec::with_capacity(undecided.len());
1821 let mut complete = true;
1822 for chunk in undecided.chunks(JUDGE_CHUNK) {
1823 if self.out_of_time() {
1824 complete = false;
1825 kept.extend_from_slice(chunk);
1826 continue;
1827 }
1828 let chunk_rows: Vec<&NameRow> = chunk.iter().map(|&index| &rows[index]).collect();
1829 let judged = self.judge(&spec.name, &other.symbol_id, &chunk_rows)?;
1830 for &index in chunk {
1831 let row = &rows[index];
1832 if row.hidden_receiver
1833 || !row.is_in_scope_of(other)
1834 || row.is_type_position() && other.is_value()
1835 || row.language == "java" && !row.can_reach(other)
1836 || row.reads_a_module(&other.kind)
1837 || row.is_bare_use() && other.is_property_method()
1838 || row.language == "python"
1839 && other.is_python_class_annotation_without_value()
1840 {
1841 kept.push(index);
1842 continue;
1843 }
1844 let Some(Judged {
1845 claimed: true,
1846 basis: Some(basis),
1847 }) = judged.get(&row.key)
1848 else {
1849 kept.push(index);
1850 continue;
1851 };
1852 if basis == IMPORT_BASIS
1853 && row.language == "python"
1854 && !self.python_import_reaches(row, &spec.name, &other.path)?
1855 {
1856 kept.push(index);
1857 continue;
1858 }
1859 verdicts.insert(
1860 row.key.clone(),
1861 Verdict::new(SiteLabel::Excluded, other.resolves_to(), false),
1862 );
1863 }
1864 }
1865 undecided = kept;
1866 if !complete {
1867 break;
1868 }
1869 checked += 1;
1870 }
1871 let unchecked_others = others.len() - checked;
1872 if !undecided.is_empty() && unchecked_others > 0 {
1873 cannot_see.push(if unchecked_others == 1 {
1874 "1 other definition with this name was not checked".to_string()
1875 } else {
1876 format!("{unchecked_others} other definitions with this name were not checked")
1877 });
1878 }
1879
1880 let absent_definition = match self
1881 .absent
1882 .as_ref()
1883 .filter(|absent| group_ids.contains(absent.symbol_id.as_str()))
1884 {
1885 Some(absent) => Some(self.absent_definition(absent)?),
1886 None => None,
1887 };
1888 let group_definitions: Vec<&Definition> = definitions
1889 .iter()
1890 .filter(|d| group_ids.contains(d.symbol_id.as_str()))
1891 .chain(absent_definition.iter())
1892 .collect();
1893 for index in undecided {
1894 let row = &rows[index];
1895 let rivals = self.rivals(row, &others)?;
1896 let verdict = match claims.get(&row.key) {
1897 Some(claim) => claimed_verdict(row, claim, rivals.len(), spec.builds_class),
1898 None if self.out_of_time() => {
1899 unchecked.push(index);
1900 continue;
1901 }
1902 None => self.decide_late(
1903 row,
1904 &spec.group,
1905 &group_definitions,
1906 &rivals,
1907 shorthand_shadows.contains_key(&row.key),
1908 )?,
1909 };
1910 verdicts.insert(row.key.clone(), verdict);
1911 }
1912 for index in unchecked {
1913 let rivals = self.rivals(&rows[index], &others)?.len();
1914 let verdict = if rivals == 0 {
1915 Verdict::new(
1916 SiteLabel::Likely,
1917 format!("name only; {NOT_CHECKED}"),
1918 false,
1919 )
1920 } else {
1921 Verdict::new(
1922 SiteLabel::Unresolved,
1923 format!("{} definitions; {NOT_CHECKED}", rivals + 1),
1924 false,
1925 )
1926 };
1927 verdicts.insert(rows[index].key.clone(), verdict);
1928 }
1929
1930 for row in &rows {
1931 let Some(mut verdict) = verdicts.remove(&row.key) else {
1932 continue;
1933 };
1934 if matches!(verdict.label, SiteLabel::Likely | SiteLabel::Unresolved)
1935 && let Some(arity) = &spec.arity
1936 && let Some(count) = row.arg_count
1937 && let Some(basis) = arity.exclusion_basis(&spec.name, &row.language, count)
1938 {
1939 verdict = Verdict::new(SiteLabel::Excluded, basis, false);
1940 }
1941 if verdict.label == SiteLabel::Proven
1942 && NAME_RESOLVED_BASES.contains(&verdict.basis.as_str())
1943 && let Some(line) = self.js_binding_line(row, &spec.name)?
1944 {
1945 verdict.label = SiteLabel::Likely;
1946 verdict.basis = format!(
1947 "{}; line {line} binds the name as a callback parameter, loop variable, or catch parameter",
1948 verdict.basis
1949 );
1950 }
1951 out.push(site_row(row, verdict, spec));
1952 }
1953 if spec.scope == RowScope::All {
1954 for member in &spec.group {
1955 for grouped in
1956 grouped_member_access_rows(self.conn, &member.symbol_id, RESOLVER_ROW_CAP)?
1957 {
1958 out.push(SiteRow {
1959 end_line: None,
1960 arg_count: None,
1961 label: SiteLabel::Proven,
1962 basis: "qualified path".to_string(),
1963 claimed: true,
1964 site: grouped,
1965 });
1966 }
1967 }
1968 }
1969 Ok(())
1970 }
1971
1972 fn inheritance_labels(
1976 &mut self,
1977 owner: &Symbol,
1978 ) -> Result<HashMap<String, (SiteLabel, String)>, QueryError> {
1979 let spec = Spec {
1980 name: owner.name.clone(),
1981 group: vec![owner.clone()],
1982 scope: RowScope::Inheritance,
1983 builds_class: false,
1984 arity: None,
1985 local_to: None,
1986 };
1987 let mut rows = Vec::new();
1988 self.account_spec(&spec, &mut rows, &mut Vec::new())?;
1989 let mut labels: HashMap<String, (SiteLabel, String)> = HashMap::new();
1990 for row in rows {
1991 let better = labels
1992 .get(&row.site.from_symbol_id)
1993 .is_none_or(|(label, _)| row.label.rank() < label.rank());
1994 if better {
1995 labels.insert(row.site.from_symbol_id, (row.label, row.basis));
1996 }
1997 }
1998 Ok(labels)
1999 }
2000
2001 fn load_rows(
2002 &self,
2003 name: &str,
2004 definitions: &[Definition],
2005 scope: RowScope,
2006 ) -> Result<Vec<NameRow>, QueryError> {
2007 let docs = documentation_language_list();
2008 let inheritance = scope == RowScope::Inheritance;
2009 let kinds = if inheritance {
2010 "AND p.kind IN ('extends', 'implements')"
2011 } else {
2012 ""
2013 };
2014 let mut rows = Vec::new();
2015
2016 let mut stmt = self.conn.prepare(&format!(
2017 "SELECT p.rowid, p.from_symbol_id, COALESCE(s.name, ''), p.kind, p.path,
2018 COALESCE(f.language, s.language, ''), p.start_line, p.start_column,
2019 p.end_line, p.target_receiver, p.target_namespace_json, s.end_line
2020 FROM pending_relationships p
2021 LEFT JOIN symbols s ON s.symbol_id = p.from_symbol_id
2022 LEFT JOIN files f ON f.file_id = p.file_id
2023 WHERE p.target_terminal_name = ?1 AND p.kind != 'imports' {kinds}
2024 AND COALESCE(f.language, s.language, '') NOT IN ({docs})"
2025 ))?;
2026 let mut query = stmt.query(params![name])?;
2027 while let Some(row) = query.next()? {
2028 let namespace: Vec<String> = row
2029 .get::<_, Option<String>>(10)?
2030 .and_then(|json| serde_json::from_str(&json).ok())
2031 .unwrap_or_default();
2032 let receiver: Option<String> = row.get(9)?;
2033 let chain = namespace
2034 .iter()
2035 .chain(receiver.as_ref().filter(|r| !r.is_empty()))
2036 .cloned()
2037 .collect();
2038 rows.push(NameRow {
2039 key: format!("P:{}", row.get::<_, i64>(0)?),
2040 source: Source::Pending,
2041 caller_id: row.get(1)?,
2042 caller_name: row.get(2)?,
2043 caller_end_line: row.get::<_, Option<i64>>(11)?.map(|v| v as usize),
2044 path: row.get::<_, String>(4)?.replace('\\', "/"),
2045 language: row.get(5)?,
2046 kind: row.get(3)?,
2047 line: row.get::<_, i64>(6)? as usize,
2048 column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
2049 end_line: row.get::<_, Option<i64>>(8)?.map(|v| v as usize),
2050 receiver,
2051 namespace,
2052 to_symbol_id: None,
2053 site_line: row.get::<_, i64>(6)? as usize,
2054 site_column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
2055 consumed: false,
2056 paired: false,
2057 hidden_receiver: false,
2058 result_receiver: false,
2059 chain: Some(chain),
2060 arg_count: None,
2061 links_enclosing_class: false,
2062 });
2063 }
2064
2065 if !inheritance {
2066 let mut stmt = self.conn.prepare(&format!(
2067 "SELECT r.rowid, r.from_symbol_id, COALESCE(s.name, ''), r.kind, r.path,
2068 COALESCE(f.language, s.language, ''), r.start_line, r.start_column,
2069 r.end_line, r.to_symbol_id, {LINKS_ENCLOSING_CLASS_SQL}, s.end_line
2070 FROM relationships r
2071 JOIN symbols t ON t.symbol_id = r.to_symbol_id
2072 LEFT JOIN symbols s ON s.symbol_id = r.from_symbol_id
2073 LEFT JOIN files f ON f.file_id = r.file_id
2074 WHERE t.name = ?1 AND r.kind != 'imports'
2075 AND COALESCE(f.language, s.language, '') NOT IN ({docs})"
2076 ))?;
2077 let mut query = stmt.query(params![name])?;
2078 while let Some(row) = query.next()? {
2079 let line = row.get::<_, Option<i64>>(6)?.unwrap_or(0) as usize;
2080 rows.push(NameRow {
2081 key: format!("R:{}", row.get::<_, i64>(0)?),
2082 source: Source::Relationship,
2083 caller_id: row.get(1)?,
2084 caller_name: row.get(2)?,
2085 caller_end_line: row.get::<_, Option<i64>>(11)?.map(|v| v as usize),
2086 path: row.get::<_, String>(4)?.replace('\\', "/"),
2087 language: row.get(5)?,
2088 kind: row.get(3)?,
2089 line,
2090 column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
2091 end_line: row.get::<_, Option<i64>>(8)?.map(|v| v as usize),
2092 receiver: None,
2093 namespace: Vec::new(),
2094 to_symbol_id: row.get(9)?,
2095 site_line: line,
2096 site_column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
2097 consumed: false,
2098 paired: false,
2099 hidden_receiver: false,
2100 result_receiver: false,
2101 chain: None,
2102 arg_count: None,
2103 links_enclosing_class: row.get(10)?,
2104 });
2105 }
2106 }
2107
2108 if !inheritance && crate::queries::has_table(self.conn, "identifiers") {
2109 let mut stmt = self.conn.prepare(&format!(
2110 "SELECT i.identifier_id, i.containing_symbol_id, COALESCE(s.name, ''), i.kind,
2111 i.path, i.language, i.start_line, i.start_column,
2112 CASE WHEN json_valid(i.metadata_json)
2113 THEN json_extract(i.metadata_json, '$.receiver') END,
2114 CASE WHEN json_valid(i.metadata_json)
2115 THEN json_extract(i.metadata_json, '$.receiver_qualifier') END,
2116 CASE WHEN json_valid(i.metadata_json)
2117 THEN json_extract(i.metadata_json, '$.arg_count') END,
2118 CASE WHEN json_valid(i.metadata_json)
2119 THEN json_extract(i.metadata_json, '$.arg_spread') END IS NOT NULL
2120 FROM identifiers i
2121 LEFT JOIN symbols s ON s.symbol_id = i.containing_symbol_id
2122 WHERE i.name = ?1 AND COALESCE(s.kind, '') != 'import'
2123 AND i.language NOT IN ({docs})"
2124 ))?;
2125 let mut query = stmt.query(params![name])?;
2126 while let Some(row) = query.next()? {
2127 let line = row.get::<_, i64>(6)? as usize;
2128 let kind: String = row.get(3)?;
2129 let receiver: Option<String> = row.get(8)?;
2130 let qualifier: Option<String> = row.get(9)?;
2131 let arg_count: Option<u32> = row.get(10)?;
2132 let arg_spread: bool = row.get(11)?;
2133 let written = receiver.as_deref().filter(|r| !r.is_empty());
2134 let chain = match kind.as_str() {
2135 "type_usage" => None,
2136 "member_access" if written.is_none() => None,
2137 _ => Some(
2138 qualifier
2139 .iter()
2140 .flat_map(|q| q.split('.'))
2141 .chain(written)
2142 .map(str::to_string)
2143 .collect(),
2144 ),
2145 };
2146 rows.push(NameRow {
2147 key: format!("I:{}", row.get::<_, String>(0)?),
2148 source: Source::Identifier,
2149 caller_id: row.get(1)?,
2150 caller_name: row.get(2)?,
2151 caller_end_line: None,
2152 path: row.get::<_, String>(4)?.replace('\\', "/"),
2153 language: row.get(5)?,
2154 kind,
2155 line,
2156 column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
2157 end_line: None,
2158 receiver,
2159 namespace: Vec::new(),
2160 to_symbol_id: None,
2161 site_line: line,
2162 site_column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
2163 consumed: false,
2164 paired: false,
2165 hidden_receiver: false,
2166 result_receiver: false,
2167 chain,
2168 arg_count: arg_count.filter(|_| !arg_spread),
2169 links_enclosing_class: false,
2170 });
2171 }
2172 }
2173
2174 let import_spans = self.import_spans(name)?;
2175 let definition_spans = definition_spans(definitions, None);
2176 rows.retain(|row| {
2177 if scope == RowScope::Calls && !row.is_call() {
2178 return false;
2179 }
2180 if in_spans(&import_spans, &row.path, row.line) {
2181 return false;
2182 }
2183 !(row.source == Source::Identifier
2184 && !row.is_super_call()
2185 && in_spans(&definition_spans, &row.path, row.line))
2186 });
2187 Ok(rows)
2188 }
2189
2190 fn import_spans(&self, name: &str) -> Result<HashMap<String, Vec<(usize, usize)>>, QueryError> {
2191 let mut spans: HashMap<String, Vec<(usize, usize)>> = HashMap::new();
2192 let mut add = |path: String, start: i64, end: Option<i64>| {
2193 let start = start as usize;
2194 let end = end.map_or(start, |e| (e as usize).max(start));
2195 spans
2196 .entry(path.replace('\\', "/"))
2197 .or_default()
2198 .push((start, end));
2199 };
2200 let docs = documentation_language_list();
2201 let dotted = format!("\".{}\"", name.replace('"', "\"\""));
2204 let sql = if crate::queries::has_table(self.conn, "symbol_names_tri")
2205 && name.chars().count() >= 2
2206 {
2207 format!(
2208 "SELECT path, start_line, end_line FROM symbols
2209 WHERE kind = 'import' AND language NOT IN ({docs}) AND name = ?1
2210 UNION ALL
2211 SELECT s.path, s.start_line, s.end_line FROM symbol_names_tri
2212 CROSS JOIN symbols s ON s.rowid = symbol_names_tri.rowid
2213 WHERE symbol_names_tri MATCH ?2 AND s.kind = 'import'
2214 AND s.language NOT IN ({docs})
2215 AND substr(s.name, -length(?1) - 1) = '.' || ?1"
2216 )
2217 } else {
2218 format!(
2219 "SELECT path, start_line, end_line FROM symbols
2220 WHERE kind = 'import' AND language NOT IN ({docs})
2221 AND (name = ?1 OR substr(name, -length(?1) - 1) = '.' || ?1)
2222 AND ?2 = ?2"
2223 )
2224 };
2225 let mut stmt = self.conn.prepare(&sql)?;
2226 for row in stmt.query_map(params![name, dotted], |row| {
2227 Ok((
2228 row.get::<_, String>(0)?,
2229 row.get::<_, i64>(1)?,
2230 row.get::<_, Option<i64>>(2)?,
2231 ))
2232 })? {
2233 let (path, start, end) = row?;
2234 add(path, start, end);
2235 }
2236 let mut stmt = self.conn.prepare(
2237 "SELECT path, start_line, end_line FROM pending_relationships
2238 WHERE target_terminal_name = ?1 AND kind = 'imports'",
2239 )?;
2240 for row in stmt.query_map(params![name], |row| {
2241 Ok((
2242 row.get::<_, String>(0)?,
2243 row.get::<_, i64>(1)?,
2244 row.get::<_, Option<i64>>(2)?,
2245 ))
2246 })? {
2247 let (path, start, end) = row?;
2248 add(path, start, end);
2249 }
2250 let mut stmt = self.conn.prepare(
2251 "SELECT r.path, r.start_line, r.end_line FROM relationships r
2252 JOIN symbols t ON t.symbol_id = r.to_symbol_id
2253 WHERE t.name = ?1 AND r.kind = 'imports' AND r.start_line IS NOT NULL",
2254 )?;
2255 for row in stmt.query_map(params![name], |row| {
2256 Ok((
2257 row.get::<_, String>(0)?,
2258 row.get::<_, i64>(1)?,
2259 row.get::<_, Option<i64>>(2)?,
2260 ))
2261 })? {
2262 let (path, start, end) = row?;
2263 add(path, start, end);
2264 }
2265 let mut stmt = self.conn.prepare(
2266 "SELECT path, start_line, end_line, COALESCE(signature, '') FROM symbols
2267 WHERE name = ?1 AND kind = 'variable'
2268 AND language IN ('javascript', 'typescript', 'tsx', 'jsx', 'vue')",
2269 )?;
2270 for row in stmt.query_map(params![name], |row| {
2271 Ok((
2272 row.get::<_, String>(0)?,
2273 row.get::<_, i64>(1)?,
2274 row.get::<_, Option<i64>>(2)?,
2275 row.get::<_, String>(3)?,
2276 ))
2277 })? {
2278 let (path, start, end, signature) = row?;
2279 if js_modules::dynamic_import_binding(&signature, name).is_some() {
2280 add(path, start, end);
2281 }
2282 }
2283 Ok(spans)
2284 }
2285
2286 fn judge(
2288 &self,
2289 name: &str,
2290 target_id: &str,
2291 rows: &[&NameRow],
2292 ) -> Result<HashMap<String, Judged>, QueryError> {
2293 let ids_of = |source: Source| -> String {
2294 let ids: Vec<&str> = rows
2295 .iter()
2296 .filter(|row| row.source == source)
2297 .filter_map(|row| row.key.split_once(':').map(|(_, id)| id))
2298 .collect();
2299 serde_json::to_string(&ids).unwrap_or_else(|_| "[]".to_string())
2300 };
2301 let (pending, identifiers) = (ids_of(Source::Pending), ids_of(Source::Identifier));
2302 let absent = self
2303 .absent
2304 .as_ref()
2305 .filter(|symbol| symbol.symbol_id == target_id);
2306 let mut slot = if absent.is_some() {
2307 self.absent_judge_stmt.borrow_mut()
2308 } else {
2309 self.judge_stmt.borrow_mut()
2310 };
2311 if slot.is_none() {
2312 *slot = Some(self.conn.prepare(&judge_sql(self.conn, absent.is_some()))?);
2313 }
2314 let stmt = slot.as_mut().expect("judge statement was just prepared");
2315 let mut rows = match absent {
2316 Some(symbol) => stmt.query(params![
2317 name,
2318 target_id,
2319 pending,
2320 identifiers,
2321 symbol.name,
2322 symbol.kind,
2323 symbol.language,
2324 symbol.path,
2325 symbol.start_line as i64,
2326 symbol.parent_symbol_id
2327 ])?,
2328 None => stmt.query(params![name, target_id, pending, identifiers])?,
2329 };
2330 let mut judged = HashMap::new();
2331 while let Some(row) = rows.next()? {
2332 let source: String = row.get(0)?;
2333 let id: String = row.get(1)?;
2334 judged.insert(
2335 format!("{source}:{id}"),
2336 Judged {
2337 claimed: row.get::<_, i64>(2)? != 0,
2338 basis: row.get(3)?,
2339 },
2340 );
2341 }
2342 Ok(judged)
2343 }
2344
2345 fn claims(
2348 &self,
2349 spec: &Spec,
2350 rows: &[NameRow],
2351 order: &[usize],
2352 cannot_see: &mut Vec<String>,
2353 ) -> Result<(HashMap<String, Claim>, usize), QueryError> {
2354 let mut claims: HashMap<String, Claim> = HashMap::new();
2355 let mut resolvers = Vec::with_capacity(spec.group.len());
2356 for member in &spec.group {
2357 let resolver_ids: HashSet<String> = if spec.scope == RowScope::Inheritance {
2358 HashSet::new()
2359 } else {
2360 direct_identifier_rows(
2361 self.conn,
2362 &spec.name,
2363 Some(&member.symbol_id),
2364 RESOLVER_ROW_CAP,
2365 )?
2366 .into_iter()
2367 .map(|row| format!("I:{}", row.identifier_id))
2368 .collect()
2369 };
2370 if resolver_ids.len() >= RESOLVER_ROW_CAP {
2371 cannot_see.push(format!(
2372 "the resolver listed {RESOLVER_ROW_CAP} identifier rows and may hold more; rows past that limit have no resolver evidence"
2373 ));
2374 }
2375 resolvers.push(resolver_ids);
2376 }
2377 let mut judged_rows = 0;
2378 for chunk in order.chunks(JUDGE_CHUNK) {
2379 if Instant::now() >= self.judge_deadline {
2380 break;
2381 }
2382 let chunk_rows: Vec<&NameRow> = chunk
2383 .iter()
2384 .map(|&index| &rows[index])
2385 .filter(|row| !row.hidden_receiver)
2386 .collect();
2387 for (member, resolver_ids) in spec.group.iter().zip(&resolvers) {
2388 let judged = self.judge(&spec.name, &member.symbol_id, &chunk_rows)?;
2389 for row in chunk_rows.iter().copied() {
2390 let judged_row = judged.get(&row.key);
2391 let claimed = match row.source {
2392 Source::Relationship => row
2393 .to_symbol_id
2394 .as_deref()
2395 .is_some_and(|id| id == member.symbol_id),
2396 Source::Identifier
2397 if matches!(row.kind.as_str(), "type_usage" | "member_access") =>
2398 {
2399 resolver_ids.contains(&row.key)
2400 }
2401 _ => judged_row.is_some_and(|j| j.claimed),
2402 };
2403 if !claimed {
2404 continue;
2405 }
2406 let basis = match row.source {
2407 Source::Relationship if row.caller_id == row.to_symbol_id => None,
2409 Source::Relationship => Some("same file".to_string()),
2410 _ => judged_row.and_then(|j| j.basis.clone()),
2411 };
2412 let basis = match basis {
2413 Some(basis)
2414 if basis == IMPORT_BASIS
2415 && row.language == "python"
2416 && !self.python_import_reaches(
2417 row,
2418 &spec.name,
2419 &member.path,
2420 )? =>
2421 {
2422 None
2423 }
2424 basis => basis,
2425 };
2426 let entry = claims
2427 .entry(row.key.clone())
2428 .or_insert(Claim { basis: None });
2429 if entry.basis.is_none() {
2430 entry.basis = basis;
2431 }
2432 }
2433 }
2434 judged_rows += chunk.len();
2435 }
2436 Ok((claims, judged_rows))
2437 }
2438
2439 fn decide_cheap(
2440 &mut self,
2441 row: &NameRow,
2442 spec: &Spec,
2443 target: &Symbol,
2444 group_ids: &HashSet<&str>,
2445 claims: &HashMap<String, Claim>,
2446 shorthand_site: &mut ShorthandSite,
2447 ) -> Result<Option<Verdict>, QueryError> {
2448 if !languages_compatible(&row.language, &target.language) {
2449 return Ok(Some(Verdict::new(
2450 SiteLabel::Excluded,
2451 "other language",
2452 false,
2453 )));
2454 }
2455 if spec
2456 .group
2457 .iter()
2458 .all(|member| member.signature.as_deref().is_some_and(is_test_label))
2459 {
2460 return Ok(Some(Verdict::new(
2461 SiteLabel::Excluded,
2462 format!("`{}` is a test label, which code never names", spec.name),
2463 false,
2464 )));
2465 }
2466 if spec
2467 .group
2468 .iter()
2469 .all(|member| row.reads_a_module(&member.kind))
2470 {
2471 return Ok(Some(Verdict::new(
2472 SiteLabel::Excluded,
2473 format!(
2474 "`{}` is a module, which a read of a value never names",
2475 spec.name
2476 ),
2477 false,
2478 )));
2479 }
2480 if let Some(outer) = &spec.local_to
2481 && !(row.path == outer.path.replace('\\', "/")
2482 && (outer.start_line..=outer.end_line).contains(&row.site_line))
2483 {
2484 return Ok(Some(Verdict::new(
2485 SiteLabel::Excluded,
2486 format!("`{}` is local to `{}`", spec.name, outer.name),
2487 false,
2488 )));
2489 }
2490 if row.language == "java"
2493 && row.kind == "member_access"
2494 && spec
2495 .group
2496 .iter()
2497 .all(|member| member.language == "java" && member.kind == "method")
2498 {
2499 return Ok(Some(Verdict::new(
2500 SiteLabel::Excluded,
2501 format!(
2502 "a member access reads a field, not the method `{}`",
2503 spec.name
2504 ),
2505 false,
2506 )));
2507 }
2508 if let Some(shadow) = self.nearest_shadow(row, &spec.name, group_ids)? {
2509 let mut shorthand_candidate = false;
2510 if shorthand_site.target_member
2511 && row.kind == "variable_ref"
2512 && language_family(&row.language) == "js"
2513 && let Some(column) = row.site_column
2514 && shadow.local
2515 && matches!(shadow.kind.as_str(), "parameter" | "variable")
2516 && shadow.dynamic_import(&spec.name).is_none()
2517 && let Some(caller_id) = row.caller_id.as_deref()
2518 && let Some(caller) = self.definition_by_id(caller_id)?
2519 && caller.path == row.path
2520 && let Some(source) = caller_source_through_site(
2521 self.root,
2522 &row.path,
2523 caller.start_line,
2524 row.site_line,
2525 )
2526 && let Some(site_line) = row
2527 .site_line
2528 .checked_sub(caller.start_line)
2529 .and_then(|line| line.checked_add(1))
2530 {
2531 shorthand_candidate =
2532 is_object_shorthand_site(&source, site_line, column, &spec.name);
2533 }
2534 if shorthand_candidate {
2535 shorthand_site.shadow_id = Some(shadow.symbol_id.clone());
2536 }
2537 let dynamic = shadow.dynamic_import(&spec.name);
2538 if let Some((source, imported)) = &dynamic
2539 && let Some(verdict) =
2540 self.js_dynamic_import_verdict(row, spec, source, imported)?
2541 {
2542 return Ok(Some(verdict));
2543 }
2544 let follows_project_module = match &dynamic {
2545 Some((source, _)) => !self.js_source_is_external(&row.path, source)?,
2546 None => false,
2547 };
2548 if !follows_project_module && !shorthand_candidate {
2549 let basis = match self.js_binding_line(row, &spec.name)? {
2550 Some(line) if line > shadow.start_line || shadow.start_line > row.site_line => {
2551 format!(
2552 "line {line} binds the name as a callback parameter, loop variable, or catch parameter"
2553 )
2554 }
2555 _ => shadow.resolves_to(),
2556 };
2557 return Ok(Some(Verdict::new(SiteLabel::Excluded, basis, false)));
2558 }
2559 }
2560 if let Some(line) = self.js_enclosing_binding_line(row, &spec.name)? {
2561 let is_shorthand_value_binding = match shorthand_site.shadow_id.as_deref() {
2562 Some(shadow_id) => self
2563 .definition_by_id(shadow_id)?
2564 .is_some_and(|shadow| shadow.start_line == line),
2565 None => false,
2566 };
2567 if !is_shorthand_value_binding {
2568 return Ok(Some(Verdict::new(
2569 SiteLabel::Excluded,
2570 format!(
2571 "line {line} binds the name as a callback parameter, loop variable, or catch parameter"
2572 ),
2573 false,
2574 )));
2575 }
2576 }
2577 if let Some(verdict) = self.java_static_import(row, spec)? {
2578 return Ok(Some(verdict));
2579 }
2580 if let Some(verdict) = self.java_bare_call_outside_classes(row, spec)? {
2581 return Ok(Some(verdict));
2582 }
2583 if let Some(verdict) = self.csharp_type_outside_namespaces(row, spec)? {
2584 return Ok(Some(verdict));
2585 }
2586 if let Some(verdict) = self.csharp_conditional_fallback(row, &spec.name)? {
2587 return Ok(Some(verdict));
2588 }
2589 if row.is_bare_use() && spec.group.iter().all(is_property_method) {
2590 return Ok(Some(self.bare_use_of_property_method(row, &spec.name)?));
2591 }
2592 if let Some(verdict) = self.js_verdict(row, spec)?
2593 && (shorthand_site.shadow_id.is_none() || verdict.label != SiteLabel::Proven)
2594 {
2595 return Ok(Some(verdict));
2596 }
2597 if let Some(claim) = claims.get(&row.key)
2598 && let Some(basis) = &claim.basis
2599 {
2600 let skip_proven_claim = shorthand_site.shadow_id.is_some()
2601 && claimed_verdict(row, claim, 0, spec.builds_class).label == SiteLabel::Proven;
2602 if !skip_proven_claim {
2603 if basis == IMPORT_BASIS {
2604 if language_family(&row.language) == "js"
2605 && let Some(verdict) = self.external_import(row, &spec.name)?
2606 {
2607 return Ok(Some(verdict));
2608 }
2609 let others = self.other_import_matches(row, &spec.name, group_ids)?;
2610 if others > 0 {
2611 return Ok(Some(Verdict::new(
2612 SiteLabel::Unresolved,
2613 format!("the import matches {} definitions", others + 1),
2614 true,
2615 )));
2616 }
2617 }
2618 return Ok(Some(claimed_verdict(row, claim, 0, spec.builds_class)));
2619 }
2620 }
2621 if self.crate_path_names_owner(row, &spec.group)? {
2622 return Ok(Some(Verdict::new(
2623 SiteLabel::Proven,
2624 "qualified path",
2625 false,
2626 )));
2627 }
2628 if let Some(to) = row.to_symbol_id.as_deref()
2629 && !row.hidden_receiver
2630 && !group_ids.contains(to)
2631 && shorthand_site.shadow_id.as_deref() != Some(to)
2632 && let Some(other) = self.definition_by_id(to)?
2633 {
2634 return Ok(Some(Verdict::new(
2635 SiteLabel::Excluded,
2636 other.resolves_to(),
2637 false,
2638 )));
2639 }
2640 self.external_import(row, &spec.name)
2641 }
2642
2643 fn java_bare_call_outside_classes(
2651 &self,
2652 row: &NameRow,
2653 spec: &Spec,
2654 ) -> Result<Option<Verdict>, QueryError> {
2655 if row.language != "java" || !row.is_call() || !row.is_bare_use() {
2656 return Ok(None);
2657 }
2658 let Some(caller) = row.caller_id.as_deref() else {
2659 return Ok(None);
2660 };
2661 let mut owners = Vec::new();
2662 for member in &spec.group {
2663 let owner = (member.language == "java" && member.kind == "method")
2664 .then(|| self.definition_by_id(&member.symbol_id))
2665 .transpose()?
2666 .flatten()
2667 .and_then(|d| d.owner_name);
2668 match owner {
2669 Some(owner) => owners.push(owner),
2670 None => return Ok(None),
2671 }
2672 }
2673 let chain = self.with_statement(SYMBOL_CHAIN_SQL, |stmt| {
2674 stmt.query_map(params![caller], |r| {
2675 Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?))
2676 })?
2677 .collect::<Result<Vec<_>, _>>()
2678 })?;
2679 let is_class = |kind: &str| matches!(kind, "class" | "interface" | "enum" | "record");
2680 if chain.windows(2).any(|pair| {
2683 matches!(pair[0].0.as_str(), "method" | "constructor") && !is_class(&pair[1].0)
2684 }) {
2685 return Ok(None);
2686 }
2687 let enclosing: Vec<String> = chain
2688 .into_iter()
2689 .filter(|(kind, _)| is_class(kind))
2690 .map(|(_, name)| name)
2691 .collect();
2692 let Some(innermost) = enclosing.first().cloned() else {
2693 return Ok(None);
2694 };
2695 let mut reachable = HashSet::new();
2696 for class in enclosing {
2697 reachable.extend(self.ancestor_names(&class)?);
2698 reachable.insert(class);
2699 }
2700 let imported_from = self.with_statement(JAVA_STATIC_IMPORTS_SQL, |stmt| {
2701 stmt.query_map(params![row.path, spec.name], |r| r.get::<_, String>(0))?
2702 .collect::<Result<Vec<_>, _>>()
2703 })?;
2704 reachable.extend(imported_from.iter().filter_map(|signature| {
2705 let imported = signature
2706 .strip_prefix("import static ")?
2707 .trim_end_matches(';');
2708 let class_path = imported.trim().rsplit_once('.')?.0;
2709 Some(class_path.rsplit('.').next()?.to_string())
2710 }));
2711 if owners.iter().any(|owner| reachable.contains(owner)) {
2712 return Ok(None);
2713 }
2714 Ok(Some(Verdict::new(
2715 SiteLabel::Excluded,
2716 format!("a bare call reaches only the classes of `{innermost}` and its static imports"),
2717 false,
2718 )))
2719 }
2720
2721 fn java_static_import(
2722 &self,
2723 row: &NameRow,
2724 spec: &Spec,
2725 ) -> Result<Option<Verdict>, QueryError> {
2726 if row.language != "java" || !row.is_call() || row.has_qualifier() {
2727 return Ok(None);
2728 }
2729 let signatures = self.with_statement(JAVA_STATIC_IMPORTS_SQL, |stmt| {
2730 stmt.query_map(params![row.path, spec.name], |r| r.get::<_, String>(0))?
2731 .collect::<Result<Vec<_>, _>>()
2732 })?;
2733 let class_paths: Vec<String> = signatures
2734 .iter()
2735 .filter_map(|signature| {
2736 let imported = signature
2737 .strip_prefix("import static ")?
2738 .trim_end_matches(';');
2739 Some(imported.trim().rsplit_once('.')?.0.to_string())
2740 })
2741 .collect();
2742 let Some(class_path) = class_paths.first() else {
2743 return Ok(None);
2744 };
2745 let distinct_class_paths: HashSet<&str> = class_paths.iter().map(String::as_str).collect();
2746 let names_target_class = class_paths.iter().any(|class_path| {
2747 let class_file = format!("/{}.java", class_path.replace('.', "/"));
2748 spec.group
2749 .iter()
2750 .any(|member| format!("/{}", member.path.replace('\\', "/")).ends_with(&class_file))
2751 });
2752 if names_target_class && distinct_class_paths.len() > 1 {
2753 return Ok(None);
2754 }
2755 if names_target_class
2756 && self
2757 .rows_at_path(CALLABLE_IN_FILE_SQL, &row.path, &[&spec.name], |r| {
2758 r.get::<_, i64>(0)
2759 })?
2760 .is_empty()
2761 {
2762 return Ok(Some(Verdict::new(SiteLabel::Proven, IMPORT_BASIS, true)));
2763 }
2764 let class_name = class_path.rsplit('.').next().unwrap_or(class_path.as_str());
2765 let mut chain = HashSet::new();
2766 if let Some(caller) = row.caller_id.as_deref()
2767 && let Some(class) = self.definition_by_id(caller)?.and_then(|d| d.owner_name)
2768 {
2769 chain = self.ancestor_names(&class)?;
2770 chain.insert(class);
2771 }
2772 let present = spec.group.iter().filter(|member| {
2773 self.absent
2774 .as_ref()
2775 .is_none_or(|absent| absent.symbol_id != member.symbol_id)
2776 });
2777 for member in present {
2778 if member.path.replace('\\', "/") == row.path {
2779 return Ok(None);
2780 }
2781 let owner = self
2782 .definition_by_id(&member.symbol_id)?
2783 .and_then(|d| d.owner_name);
2784 if owner.is_some_and(|owner| owner == class_name || chain.contains(&owner)) {
2785 return Ok(None);
2786 }
2787 }
2788 Ok(Some(Verdict::new(
2789 SiteLabel::Excluded,
2790 format!("`{}` is imported from `{class_path}`", spec.name),
2791 false,
2792 )))
2793 }
2794
2795 fn external_import(
2796 &mut self,
2797 row: &NameRow,
2798 name: &str,
2799 ) -> Result<Option<Verdict>, QueryError> {
2800 Ok(self.external_binding(row, name)?.map(|(binding, source)| {
2801 Verdict::new(
2802 SiteLabel::Excluded,
2803 format!("receiver `{binding}` is imported from `{source}`"),
2804 false,
2805 )
2806 }))
2807 }
2808
2809 fn crate_path_names_owner(&self, row: &NameRow, group: &[Symbol]) -> Result<bool, QueryError> {
2813 if row.language != "rust"
2814 || row.source != Source::Pending
2815 || row.kind != "calls"
2816 || row.namespace.len() < 2
2817 {
2818 return Ok(false);
2819 }
2820 let Some((owner_segment, leading)) = row.namespace.split_last() else {
2821 return Ok(false);
2822 };
2823 if matches!(leading[0].as_str(), "crate" | "self" | "super" | "Self") {
2824 return Ok(false);
2825 }
2826 for member in group.iter().filter(|member| member.language == "rust") {
2827 let Some(owner) = self
2828 .definition_by_id(&member.symbol_id)?
2829 .and_then(|definition| definition.owner_name)
2830 else {
2831 continue;
2832 };
2833 let path = member.path.replace('\\', "/");
2834 let Some((package, module_path)) = path.rsplit_once("/src/") else {
2835 continue;
2836 };
2837 let package_name = package
2838 .rsplit('/')
2839 .next()
2840 .unwrap_or(package)
2841 .replace('-', "_");
2842 let modules_match = leading[1..].iter().all(|segment| {
2843 module_path
2844 .split('/')
2845 .any(|part| part == segment || part.strip_suffix(".rs") == Some(segment))
2846 });
2847 if owner != *owner_segment || leading[0] != package_name || !modules_match {
2848 continue;
2849 }
2850 let in_package = format!("{}/%", crate::queries::escape_like(package));
2851 let owners = self.with_statement(OWNER_IN_PACKAGE_SQL, |stmt| {
2852 stmt.query_row(params![owner, in_package], |r| r.get::<_, i64>(0))
2853 })?;
2854 if owners == 1 {
2855 return Ok(true);
2856 }
2857 }
2858 Ok(false)
2859 }
2860
2861 fn decide_late(
2862 &mut self,
2863 row: &NameRow,
2864 group: &[Symbol],
2865 group_definitions: &[&Definition],
2866 rivals: &[&Definition],
2867 shorthand_candidate: bool,
2868 ) -> Result<Verdict, QueryError> {
2869 if let Some(verdict) = self.outside_ancestors(row, group)? {
2870 return Ok(verdict);
2871 }
2872 if let Some(receiver) = row.qualifier().filter(|_| row.namespace.len() <= 1)
2873 && let Some(type_name) = self.receiver_type(row, receiver)?
2874 && !self.owner_related(&type_name, group)?
2875 {
2876 return Ok(Verdict::new(
2877 SiteLabel::Excluded,
2878 format!("receiver `{receiver}` has type `{type_name}`"),
2879 false,
2880 ));
2881 }
2882 if let Some(head) = self.foreign_type_head(row, group)? {
2883 return Ok(Verdict::new(
2884 SiteLabel::Excluded,
2885 format!("receiver `{head}` has type `{head}`"),
2886 false,
2887 ));
2888 }
2889 if let Some(object) = self.object_assigned_on(row, group_definitions, rivals)? {
2890 return Ok(Verdict::new(
2891 SiteLabel::Likely,
2892 format!(
2893 "receiver `{object}` names the object `{}` is assigned on",
2894 group[0].name
2895 ),
2896 false,
2897 ));
2898 }
2899 if shorthand_candidate {
2900 return Ok(Verdict::new(
2901 SiteLabel::Unresolved,
2902 format!("{} definitions", rivals.len() + 2),
2903 false,
2904 ));
2905 }
2906 if rivals.is_empty() {
2907 Ok(Verdict::new(SiteLabel::Likely, "name only", false))
2908 } else {
2909 Ok(Verdict::new(
2910 SiteLabel::Unresolved,
2911 format!("{} definitions", rivals.len() + 1),
2912 false,
2913 ))
2914 }
2915 }
2916
2917 fn outside_ancestors(
2921 &self,
2922 row: &NameRow,
2923 group: &[Symbol],
2924 ) -> Result<Option<Verdict>, QueryError> {
2925 let (Some(caller), Some(receiver)) = (row.caller_id.as_deref(), row.receiver.as_deref())
2926 else {
2927 return Ok(None);
2928 };
2929 if !row.links_enclosing_class {
2930 return Ok(None);
2931 }
2932 let Some(class) = self
2933 .definition_by_id(caller)?
2934 .and_then(|caller| caller.owner_name)
2935 else {
2936 return Ok(None);
2937 };
2938 let ancestors = self.ancestor_names(&class)?;
2939 for member in group {
2940 match self
2941 .definition_by_id(&member.symbol_id)?
2942 .and_then(|member| member.owner_name)
2943 {
2944 Some(owner) if !ancestors.contains(&owner) => {}
2945 _ => return Ok(None),
2946 }
2947 }
2948 Ok(Some(Verdict::new(
2949 SiteLabel::Excluded,
2950 format!("receiver `{receiver}` reaches only the ancestors of `{class}`"),
2951 false,
2952 )))
2953 }
2954
2955 fn object_assigned_on(
2958 &self,
2959 row: &NameRow,
2960 group: &[&Definition],
2961 rivals: &[&Definition],
2962 ) -> Result<Option<String>, QueryError> {
2963 let Some(receiver) = row
2964 .receiver
2965 .as_deref()
2966 .filter(|r| is_plain_identifier(r) && !SELF_RECEIVERS.contains(r))
2967 .filter(|_| matches!(row.kind.as_str(), "calls" | "call" | "member_access"))
2968 else {
2969 return Ok(None);
2970 };
2971 let assigned_on_receiver = |d: &&Definition| {
2972 language_family(&d.language) == "js"
2973 && matches!(d.kind.as_str(), "method" | "function")
2974 && d.top_level
2975 && d.class_name.as_deref() == Some(receiver)
2976 };
2977 if !group.iter().all(assigned_on_receiver)
2978 || rivals
2979 .iter()
2980 .any(|d| d.class_name.as_deref() == Some(receiver))
2981 {
2982 return Ok(None);
2983 }
2984 let Some(target) = group.first() else {
2985 return Ok(None);
2986 };
2987 let objects =
2988 self.rows_at_path(TOP_LEVEL_VARIABLE_SQL, &target.path, &[&receiver], |row| {
2989 row.get::<_, i64>(0)
2990 })?;
2991 Ok((!objects.is_empty()).then(|| receiver.to_string()))
2992 }
2993
2994 fn definition_by_id(&self, symbol_id: &str) -> Result<Option<Definition>, QueryError> {
2995 if let Some(absent) = self.absent.as_ref().filter(|s| s.symbol_id == symbol_id) {
2996 return self.absent_definition(absent).map(Some);
2997 }
2998 self.with_statement(DEFINITION_BY_ID_SQL, |stmt| {
2999 stmt.query_row(params![symbol_id], |row| {
3000 let parent_kind: Option<String> = row.get(9)?;
3001 let parent_name: Option<String> = row.get(8)?;
3002 let kind: String = row.get(2)?;
3003 let local = kind == "parameter"
3004 || kind == "variable"
3005 && parent_kind.as_deref().is_some_and(|kind| {
3006 matches!(kind, "function" | "method" | "constructor")
3007 });
3008 Ok(Definition {
3009 symbol_id: row.get(0)?,
3010 name: row.get(1)?,
3011 path: row.get::<_, String>(4)?.replace('\\', "/"),
3012 start_line: row.get::<_, i64>(5)? as usize,
3013 decorated_start_line: None,
3014 body_start_line: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
3015 owner_name: parent_name.filter(|_| {
3016 parent_kind
3017 .as_deref()
3018 .is_some_and(|k| CLASS_LIKE_KINDS.contains(&k))
3019 }),
3020 owner_kind: parent_kind
3021 .filter(|kind| CLASS_LIKE_KINDS.contains(&kind.as_str())),
3022 local,
3023 scope: None,
3024 kind,
3025 language: row.get(3)?,
3026 signature: row.get(10)?,
3027 owner_signature: row.get(11)?,
3028 class_name: None,
3029 top_level: row.get::<_, Option<String>>(7)?.is_none(),
3030 callable: true,
3031 free: false,
3032 module_value: false,
3033 })
3034 })
3035 .optional()
3036 })
3037 }
3038
3039 fn absent_definition(&self, symbol: &Symbol) -> Result<Definition, QueryError> {
3042 let owner = match &symbol.parent_symbol_id {
3043 Some(parent) => crate::queries::get_symbol_by_id(self.conn, parent)?,
3044 None => None,
3045 };
3046 let owner_signature = owner
3047 .as_ref()
3048 .and_then(|owner| owner.signature.clone())
3049 .unwrap_or_default();
3050 let owner_kind = owner
3051 .as_ref()
3052 .filter(|owner| CLASS_LIKE_KINDS.contains(&owner.kind.as_str()))
3053 .map(|owner| owner.kind.clone());
3054 let owner_name = owner
3055 .filter(|owner| CLASS_LIKE_KINDS.contains(&owner.kind.as_str()))
3056 .map(|owner| owner.name);
3057 Ok(Definition {
3058 symbol_id: symbol.symbol_id.clone(),
3059 name: symbol.name.clone(),
3060 path: symbol.path.replace('\\', "/"),
3061 start_line: symbol.start_line,
3062 decorated_start_line: None,
3063 body_start_line: symbol.body_start_line,
3064 owner_name,
3065 owner_kind,
3066 local: false,
3067 scope: None,
3068 kind: symbol.kind.clone(),
3069 language: symbol.language.clone(),
3070 signature: String::new(),
3071 owner_signature,
3072 class_name: assigned_object(symbol),
3073 top_level: symbol.parent_symbol_id.is_none(),
3074 callable: true,
3075 free: false,
3076 module_value: false,
3077 })
3078 }
3079
3080 fn nearest_shadow(
3085 &self,
3086 row: &NameRow,
3087 name: &str,
3088 group_ids: &HashSet<&str>,
3089 ) -> Result<Option<Definition>, QueryError> {
3090 let type_use = row.is_bare_type_use();
3091 if !row.is_bare_use() && !type_use {
3092 return Ok(None);
3093 }
3094 let mut hit = None;
3095 if let Some(caller) = row.caller_id.as_deref() {
3096 hit = self.first_real_definition(
3097 SHADOW_IN_CHAIN_SQL,
3098 params![caller, name, row.path, type_use, row.site_line as i64],
3099 row,
3100 )?;
3101 }
3102 if hit.is_none() {
3103 hit = self.first_real_definition(
3104 SHADOW_TOP_LEVEL_SQL,
3105 params![name, row.path, row.site_line as i64],
3106 row,
3107 )?;
3108 }
3109 match hit {
3110 Some(id) if !group_ids.contains(id.as_str()) => self.definition_by_id(&id),
3111 _ => Ok(None),
3112 }
3113 }
3114
3115 fn first_real_definition(
3118 &self,
3119 sql: &'static str,
3120 query: impl rusqlite::Params,
3121 row: &NameRow,
3122 ) -> Result<Option<String>, QueryError> {
3123 let rows = self.with_statement(sql, |stmt| {
3124 stmt.query_map(query, |r| {
3125 Ok((
3126 r.get::<_, String>(0)?,
3127 r.get::<_, String>(1)?,
3128 r.get::<_, String>(2)?,
3129 ))
3130 })?
3131 .collect::<Result<Vec<_>, _>>()
3132 })?;
3133 Ok(rows
3134 .into_iter()
3135 .find(|(_, signature, kind)| !is_test_label(signature) && !row.is_not_named_by(kind))
3136 .map(|(id, ..)| id))
3137 }
3138
3139 fn import_matches_of(
3142 &mut self,
3143 importer: &str,
3144 receiver: &str,
3145 name: &str,
3146 ) -> Result<&[ImportMatch], QueryError> {
3147 let key = (importer.to_string(), receiver.to_string(), name.to_string());
3148 if !self.import_matches.contains_key(&key) {
3149 if self.import_match_stmt.is_none() {
3150 self.import_match_stmt = Some(self.conn.prepare(&import_match_sql())?);
3151 }
3152 let stmt = self
3153 .import_match_stmt
3154 .as_mut()
3155 .expect("import match statement was just prepared");
3156 let matches = stmt
3157 .query_map(params![importer, receiver, name], |r| {
3158 Ok((
3159 r.get::<_, String>(0)?,
3160 r.get::<_, String>(1)?,
3161 r.get::<_, String>(2)?,
3162 r.get::<_, String>(3)?,
3163 ))
3164 })?
3165 .filter(|m| !matches!(m, Ok((_, _, signature, _)) if is_test_label(signature)))
3166 .map(|m| m.map(|(id, path, _, language)| (id, path, language)))
3167 .collect::<Result<Vec<_>, _>>()?;
3168 self.import_matches.insert(key.clone(), matches);
3169 }
3170 Ok(&self.import_matches[&key])
3171 }
3172
3173 fn other_import_matches(
3176 &mut self,
3177 row: &NameRow,
3178 name: &str,
3179 group_ids: &HashSet<&str>,
3180 ) -> Result<usize, QueryError> {
3181 let receiver = row.receiver.clone().unwrap_or_default();
3182 let matches = self.import_matches_of(&row.path, &receiver, name)?.to_vec();
3183 let mut count = 0;
3184 for (symbol_id, path, _) in &matches {
3185 if group_ids.contains(symbol_id.as_str()) {
3186 continue;
3187 }
3188 if row.language == "python"
3189 && !self.python_import_reaches(row, name, &path.replace('\\', "/"))?
3190 {
3191 continue;
3192 }
3193 count += 1;
3194 }
3195 Ok(count)
3196 }
3197
3198 fn imported_definitions(
3202 &mut self,
3203 importer: &str,
3204 language: &str,
3205 name: &str,
3206 ) -> Result<Vec<String>, QueryError> {
3207 let matches = self.import_matches_of(importer, "", name)?.to_vec();
3208 let mut reached = Vec::new();
3209 for (symbol_id, path, definition_language) in matches {
3210 if !languages_compatible(&definition_language, language) {
3211 continue;
3212 }
3213 if language == "python"
3214 && !self.python_binding_reaches(importer, name, &path.replace('\\', "/"))?
3215 {
3216 continue;
3217 }
3218 reached.push(symbol_id);
3219 }
3220 Ok(reached)
3221 }
3222
3223 fn bare_use_of_property_method(
3224 &self,
3225 row: &NameRow,
3226 name: &str,
3227 ) -> Result<Verdict, QueryError> {
3228 let sources = self.with_statement(IMPORT_SOURCES_SQL, |stmt| {
3229 stmt.query_map(params![row.path, name], |r| r.get::<_, String>(0))?
3230 .collect::<Result<Vec<_>, _>>()
3231 })?;
3232 let basis = match sources.iter().find(|source| !source.is_empty()) {
3233 Some(source) => format!("resolves to the import of `{name}` from `{source}`"),
3234 None => "a bare name cannot reach a property method".to_string(),
3235 };
3236 Ok(Verdict::new(SiteLabel::Excluded, basis, false))
3237 }
3238
3239 fn mark_result_variables(&self, rows: &mut [NameRow]) -> Result<(), QueryError> {
3243 let mut texts: HashMap<String, Option<String>> = HashMap::new();
3244 for row in rows.iter_mut() {
3245 if row.result_receiver
3246 || row.hidden_receiver
3247 || language_family(&row.language) != "js"
3248 || !row.is_call()
3249 || !row.namespace.is_empty()
3250 {
3251 continue;
3252 }
3253 let (Some(receiver), Some(caller)) = (
3254 row.receiver
3255 .clone()
3256 .filter(|r| is_plain_identifier(r) && !SELF_RECEIVERS.contains(&r.as_str())),
3257 row.caller_id.as_deref(),
3258 ) else {
3259 continue;
3260 };
3261 let declarations = self.with_statement(RECEIVER_DECLARATIONS_SQL, |stmt| {
3262 stmt.query_map(params![caller, receiver, row.path], |r| {
3263 Ok((r.get::<_, String>(1)?, r.get::<_, i64>(2)? as usize))
3264 })?
3265 .collect::<Result<Vec<_>, _>>()
3266 })?;
3267 let Some((kind, line)) = declarations
3268 .into_iter()
3269 .find(|(_, line)| *line <= row.site_line)
3270 else {
3271 continue;
3272 };
3273 if kind == "parameter" {
3274 continue;
3275 }
3276 let text = texts
3277 .entry(row.path.clone())
3278 .or_insert_with(|| std::fs::read_to_string(self.root.join(&row.path)).ok());
3279 row.result_receiver = text
3280 .as_deref()
3281 .and_then(|text| text.lines().nth(line.saturating_sub(1)))
3282 .is_some_and(|declaration| const_holds_result(declaration, &receiver));
3283 }
3284 Ok(())
3285 }
3286
3287 fn rivals<'d>(
3289 &self,
3290 row: &NameRow,
3291 others: &[&'d Definition],
3292 ) -> Result<Vec<&'d Definition>, QueryError> {
3293 let module_receiver = self.receiver_is_python_module(row)?;
3294 let bare_csharp = row.language == "csharp" && !row.has_qualifier();
3295 let mut rivals = Vec::new();
3296 for &other in others {
3297 if !row.can_reach(other) || module_receiver && other.owner_name.is_some() {
3298 continue;
3299 }
3300 let csharp_type = other.language == "csharp"
3301 && (other.kind == "constructor"
3302 || other.owner_name.is_none()
3303 && CSHARP_TYPE_KINDS.contains(&other.kind.as_str()));
3304 if bare_csharp && csharp_type && !self.csharp_type_in_scope(row, other)? {
3305 continue;
3306 }
3307 rivals.push(other);
3308 }
3309 Ok(rivals)
3310 }
3311
3312 fn csharp_type_outside_namespaces(
3315 &self,
3316 row: &NameRow,
3317 spec: &Spec,
3318 ) -> Result<Option<Verdict>, QueryError> {
3319 if row.language != "csharp" || row.has_qualifier() {
3320 return Ok(None);
3321 }
3322 let mut namespace = None;
3323 for member in &spec.group {
3324 let Some(definition) = self.definition_by_id(&member.symbol_id)? else {
3325 return Ok(None);
3326 };
3327 if definition.language != "csharp"
3328 || definition.owner_name.is_some()
3329 || !CSHARP_TYPE_KINDS.contains(&definition.kind.as_str())
3330 || self.csharp_type_in_scope(row, &definition)?
3331 {
3332 return Ok(None);
3333 }
3334 namespace = Some(self.csharp_namespace(&definition.symbol_id)?);
3335 }
3336 let usings = self.with_statement(CSHARP_USINGS_SQL, |stmt| {
3337 stmt.query_map(params![row.path], |r| r.get::<_, String>(0))?
3338 .collect::<Result<Vec<_>, _>>()
3339 })?;
3340 let alias = format!("using {} =", spec.name);
3341 if usings.iter().any(|using| using.contains(&alias)) {
3342 return Ok(None);
3343 }
3344 Ok(namespace.map(|namespace| {
3345 Verdict::new(
3346 SiteLabel::Excluded,
3347 format!("namespace `{namespace}` is not open here"),
3348 false,
3349 )
3350 }))
3351 }
3352
3353 fn csharp_conditional_fallback(
3358 &self,
3359 row: &NameRow,
3360 name: &str,
3361 ) -> Result<Option<Verdict>, QueryError> {
3362 if row.language != "csharp" || row.has_qualifier() {
3363 return Ok(None);
3364 }
3365 let Some(condition) = self.target_condition.as_deref() else {
3366 return Ok(None);
3367 };
3368 let negated: Vec<&str> = condition_terms(condition)
3369 .filter_map(|term| term.strip_prefix('!'))
3370 .map(str::trim)
3371 .collect();
3372 if negated.is_empty() {
3373 return Ok(None);
3374 }
3375 let usings = self.with_statement(CSHARP_FILE_USINGS_SQL, |stmt| {
3376 stmt.query_map(params![row.path], |r| {
3377 Ok((r.get::<_, String>(0)?, r.get::<_, Option<String>>(1)?))
3378 })?
3379 .collect::<Result<Vec<_>, _>>()
3380 })?;
3381 let under_negated = |condition: &str| {
3382 condition_terms(condition)
3383 .find_map(|term| negated.iter().copied().find(|symbol| *symbol == term))
3384 };
3385 if usings
3386 .iter()
3387 .any(|(_, condition)| condition.as_deref().and_then(under_negated).is_some())
3388 {
3389 return Ok(None);
3390 }
3391 let paired = self.with_statement(CSHARP_CONDITIONAL_USINGS_SQL, |stmt| {
3392 stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))?
3393 .collect::<Result<Vec<_>, _>>()
3394 })?;
3395 Ok(usings
3396 .iter()
3397 .filter(|(_, condition)| condition.is_none())
3398 .filter_map(|(signature, _)| csharp_using_namespace(signature))
3399 .find_map(|namespace| {
3400 let symbol = paired.iter().find_map(|(signature, condition)| {
3401 (csharp_using_namespace(signature) == Some(namespace))
3402 .then(|| under_negated(condition))
3403 .flatten()
3404 })?;
3405 Some(Verdict::new(
3406 SiteLabel::Unresolved,
3407 format!(
3408 "`{name}` is defined only without `{symbol}`, and this file always opens `{namespace}`, which other files open with it"
3409 ),
3410 false,
3411 ))
3412 }))
3413 }
3414
3415 fn csharp_type_in_scope(&self, row: &NameRow, other: &Definition) -> Result<bool, QueryError> {
3420 let namespace = self.csharp_namespace(&other.symbol_id)?;
3421 let Some(caller) = row.caller_id.as_deref().filter(|_| !namespace.is_empty()) else {
3422 return Ok(true);
3423 };
3424 if self.msbuild_usings()? {
3425 return Ok(true);
3426 }
3427 let enclosing = self.csharp_namespace(caller)?;
3428 if enclosing == namespace || enclosing.starts_with(&format!("{namespace}.")) {
3429 return Ok(true);
3430 }
3431 let usings = self.with_statement(CSHARP_USINGS_SQL, |stmt| {
3432 stmt.query_map(params![row.path], |r| r.get::<_, String>(0))?
3433 .collect::<Result<Vec<_>, _>>()
3434 })?;
3435 Ok(usings
3436 .iter()
3437 .any(|using| csharp_using_namespace(using) == Some(namespace.as_str())))
3438 }
3439
3440 fn msbuild_usings(&self) -> Result<bool, QueryError> {
3442 if let Some(known) = self.msbuild_usings.get() {
3443 return Ok(*known);
3444 }
3445 let paths = self.with_statement(MSBUILD_FILES_SQL, |stmt| {
3446 stmt.query_map([], |r| r.get::<_, String>(0))?
3447 .collect::<Result<Vec<_>, _>>()
3448 })?;
3449 let found = paths.iter().any(|path| {
3450 std::fs::read_to_string(self.root.join(path))
3451 .is_ok_and(|text| text.contains("<Using ") || text.contains("<Using\n"))
3452 });
3453 Ok(*self.msbuild_usings.get_or_init(|| found))
3454 }
3455
3456 fn csharp_namespace(&self, symbol_id: &str) -> Result<String, QueryError> {
3459 let start = match self.absent.as_ref().filter(|s| s.symbol_id == symbol_id) {
3460 Some(absent) => match absent.parent_symbol_id.as_deref() {
3461 Some(parent) => parent,
3462 None => return Ok(String::new()),
3463 },
3464 None => symbol_id,
3465 };
3466 let chain = self.with_statement(SYMBOL_CHAIN_SQL, |stmt| {
3467 stmt.query_map(params![start], |r| {
3468 Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?))
3469 })?
3470 .collect::<Result<Vec<_>, _>>()
3471 })?;
3472 let mut parts: Vec<String> = chain
3473 .into_iter()
3474 .filter(|(kind, _)| kind == "namespace")
3475 .map(|(_, name)| name)
3476 .collect();
3477 parts.reverse();
3478 Ok(parts.join("."))
3479 }
3480
3481 fn receiver_is_python_module(&self, row: &NameRow) -> Result<bool, QueryError> {
3484 if row.language != "python" || row.hidden_receiver {
3485 return Ok(false);
3486 }
3487 let chain: Vec<&str> = row
3488 .namespace
3489 .iter()
3490 .chain(row.receiver.iter())
3491 .flat_map(|part| part.split('.'))
3492 .filter(|part| !part.is_empty())
3493 .collect();
3494 let Some((head, rest)) = chain.split_first() else {
3495 return Ok(false);
3496 };
3497 let imports = self.with_statement(PYTHON_IMPORT_ROWS_SQL, |stmt| {
3498 stmt.query_map(params![row.path, head], |r| {
3499 Ok((
3500 r.get::<_, String>(0)?,
3501 r.get::<_, String>(1)?,
3502 r.get::<_, String>(2)?,
3503 ))
3504 })?
3505 .collect::<Result<Vec<_>, _>>()
3506 })?;
3507 let [(signature, source, imported)] = imports.as_slice() else {
3508 return Ok(false);
3509 };
3510 if source.is_empty() || source.starts_with('.') {
3511 return Ok(false);
3512 }
3513 let mut module: Vec<&str> = source.split('.').collect();
3514 if signature.trim_start().starts_with("from ") {
3515 module.push(imported);
3516 }
3517 module.extend(rest);
3518 let module = module.join("/");
3519 let package = format!(
3520 "%/{}",
3521 crate::queries::escape_like(&format!("{module}/__init__.py"))
3522 );
3523 let file = format!("%/{}", crate::queries::escape_like(&format!("{module}.py")));
3524 self.with_statement(SOURCE_FILES_SQL, |stmt| stmt.exists(params![package, file]))
3525 }
3526
3527 fn python_import_reaches(
3531 &self,
3532 row: &NameRow,
3533 name: &str,
3534 path: &str,
3535 ) -> Result<bool, QueryError> {
3536 self.python_binding_reaches(&row.path, row.qualifier().unwrap_or(name), path)
3537 }
3538
3539 fn python_binding_reaches(
3542 &self,
3543 importer: &str,
3544 binding: &str,
3545 path: &str,
3546 ) -> Result<bool, QueryError> {
3547 let sources = self.with_statement(IMPORT_SOURCES_SQL, |stmt| {
3548 stmt.query_map(params![importer, binding], |r| r.get::<_, String>(0))?
3549 .collect::<Result<Vec<_>, _>>()
3550 })?;
3551 if sources.is_empty() || sources.iter().any(|s| s.is_empty() || s.starts_with('.')) {
3552 return Ok(true);
3553 }
3554 let reachable = self.python_import_heads(path)?;
3555 Ok(sources.iter().any(|source| {
3556 source
3557 .split('.')
3558 .next()
3559 .is_some_and(|first| reachable.contains(&first))
3560 }))
3561 }
3562
3563 fn python_import_heads<'p>(&self, path: &'p str) -> Result<Vec<&'p str>, QueryError> {
3567 let (folders, file) = match path.rsplit_once('/') {
3568 Some((dir, file)) => (dir.split('/').collect::<Vec<_>>(), file),
3569 None => (Vec::new(), path),
3570 };
3571 let mut top = folders.len();
3572 while top > 0 {
3573 let init = format!("{}/__init__.py", folders[..top].join("/"));
3574 let is_package = self.with_statement(FILE_EXISTS_SQL, |stmt| {
3575 stmt.exists(params![init, init.replace('/', "\\")])
3576 })?;
3577 if !is_package {
3578 break;
3579 }
3580 top -= 1;
3581 }
3582 let stem = file.strip_suffix(".py").unwrap_or(file);
3583 Ok(if top == folders.len() {
3584 folders
3585 .iter()
3586 .copied()
3587 .chain(std::iter::once(stem))
3588 .collect()
3589 } else {
3590 folders[..=top].to_vec()
3591 })
3592 }
3593
3594 fn python_source_is_external(&mut self, source: &str) -> Result<bool, QueryError> {
3597 let key = format!("python:{source}");
3598 if let Some(known) = self.external_sources.get(&key) {
3599 return Ok(*known);
3600 }
3601 let first = source.split('.').next().unwrap_or_default();
3602 let external = if source.starts_with('.') || !is_plain_identifier(first) {
3603 false
3604 } else {
3605 let paths = self.with_statement(PYTHON_FILES_NAMED_SQL, |stmt| {
3606 stmt.query_map(
3607 params![format!("%/{first}/%"), format!("%/{first}.py")],
3608 |r| r.get::<_, String>(0),
3609 )?
3610 .collect::<Result<Vec<_>, _>>()
3611 })?;
3612 let mut reached = paths.len() >= PYTHON_FILES_NAMED_CAP;
3613 for path in &paths {
3614 if reached {
3615 break;
3616 }
3617 reached = self.python_import_heads(path)?.contains(&first);
3618 }
3619 !reached
3620 };
3621 self.external_sources.insert(key, external);
3622 Ok(external)
3623 }
3624
3625 fn external_binding(
3628 &mut self,
3629 row: &NameRow,
3630 name: &str,
3631 ) -> Result<Option<(String, String)>, QueryError> {
3632 let javascript = language_family(&row.language) == "js";
3633 let imported_head = match row.chain.as_ref() {
3634 Some(chain) if javascript => {
3635 let head = chain.first().map_or(name, String::as_str);
3636 self.js_binds_import(&row.path, head)?.then_some(head)
3637 }
3638 _ => None,
3639 };
3640 let Some(qualifier) = imported_head.or_else(|| row.qualifier()) else {
3641 return Ok(None);
3642 };
3643 if SELF_RECEIVERS.contains(&qualifier) || qualifier == "crate" {
3644 return Ok(None);
3645 }
3646 if row.language == "rust" && EXTERNAL_RUST_ROOTS.contains(&qualifier) {
3647 return Ok(Some((qualifier.to_string(), qualifier.to_string())));
3648 }
3649 let mut sources = self.with_statement(IMPORT_SOURCES_SQL, |stmt| {
3650 stmt.query_map(params![row.path, qualifier], |r| r.get::<_, String>(0))?
3651 .collect::<Result<Vec<_>, _>>()
3652 })?;
3653 let has_import = !sources.is_empty();
3654 sources.retain(|source| !source.is_empty());
3655 let pending_modules = self.with_statement(PENDING_IMPORT_MODULES_SQL, |stmt| {
3656 stmt.query_map(params![row.path, qualifier], |r| {
3657 r.get::<_, Option<String>>(0)
3658 })?
3659 .collect::<Result<Vec<_>, _>>()
3660 })?;
3661 for modules in pending_modules.into_iter().flatten() {
3662 if let Ok(parts) = serde_json::from_str::<Vec<String>>(&modules)
3663 && !parts.is_empty()
3664 {
3665 sources.push(parts.join("::"));
3666 }
3667 }
3668 if sources.is_empty() {
3669 return match has_import {
3670 true if row.language == "java" => self.java_import_outside(row, qualifier),
3671 true => Ok(None),
3672 false => self.unindexed_path_head(row),
3673 };
3674 }
3675 for source in &sources {
3676 let external = if javascript {
3677 self.js_source_is_external(&row.path, source)?
3678 } else if row.language == "python" {
3679 self.python_source_is_external(source)?
3680 } else {
3681 self.source_is_external(source)?
3682 };
3683 if !external {
3684 return Ok(None);
3685 }
3686 }
3687 Ok(Some((qualifier.to_string(), sources[0].clone())))
3688 }
3689
3690 fn java_import_outside(
3694 &self,
3695 row: &NameRow,
3696 qualifier: &str,
3697 ) -> Result<Option<(String, String)>, QueryError> {
3698 let signatures = self.with_statement(IMPORT_SIGNATURES_SQL, |stmt| {
3699 stmt.query_map(params![row.path, qualifier], |r| r.get::<_, String>(0))?
3700 .collect::<Result<Vec<_>, _>>()
3701 })?;
3702 let [signature] = signatures.as_slice() else {
3703 return Ok(None);
3704 };
3705 let imported = signature
3706 .trim()
3707 .trim_start_matches("import ")
3708 .trim()
3709 .trim_end_matches(';')
3710 .trim();
3711 let package: Vec<&str> = imported
3712 .split('.')
3713 .take_while(|segment| segment.starts_with(|c: char| c.is_ascii_lowercase()))
3714 .collect();
3715 if imported.starts_with("static ") || package.is_empty() {
3716 return Ok(None);
3717 }
3718 let folder = format!("%/{}/%", crate::queries::escape_like(&package.join("/")));
3719 let indexed = self.with_statement(SOURCE_FILES_SQL, |stmt| {
3720 stmt.exists(params![folder, folder])
3721 })?;
3722 Ok((!indexed).then(|| (qualifier.to_string(), imported.to_string())))
3723 }
3724
3725 fn unindexed_path_head(
3728 &mut self,
3729 row: &NameRow,
3730 ) -> Result<Option<(String, String)>, QueryError> {
3731 let Some(head) = row
3732 .namespace
3733 .first()
3734 .filter(|head| is_plain_identifier(head) && !SELF_RECEIVERS.contains(&head.as_str()))
3735 .filter(|head| *head != "crate" && head.chars().next().is_some_and(char::is_lowercase))
3736 else {
3737 return Ok(None);
3738 };
3739 let kinds = self.with_statement(TYPE_HEAD_KINDS_SQL, |stmt| {
3740 stmt.query_map(params![head], |r| r.get::<_, String>(0))?
3741 .collect::<Result<Vec<_>, _>>()
3742 })?;
3743 if kinds.is_empty() && self.source_is_external(head)? {
3744 return Ok(Some((head.clone(), head.clone())));
3745 }
3746 Ok(None)
3747 }
3748
3749 fn source_is_external(&mut self, source: &str) -> Result<bool, QueryError> {
3750 if let Some(known) = self.external_sources.get(source) {
3751 return Ok(*known);
3752 }
3753 let runtime_library = !source.starts_with('.')
3754 && matches!(
3755 source.split([':', '.', '/']).find(|part| !part.is_empty()),
3756 Some("std" | "core" | "alloc")
3757 );
3758 let external = match import_segment(source).filter(|_| !runtime_library) {
3759 None => false,
3760 Some(segment) => {
3761 let escaped = crate::queries::escape_like(&segment);
3762 let (as_folder, as_file) = (format!("%/{escaped}/%"), format!("%/{escaped}.%"));
3763 !self.with_statement(SOURCE_FILES_SQL, |stmt| {
3764 stmt.exists(params![as_folder, as_file])
3765 })?
3766 }
3767 };
3768 let external = external || runtime_library;
3769 self.external_sources.insert(source.to_string(), external);
3770 Ok(external)
3771 }
3772
3773 fn receiver_type(&self, row: &NameRow, receiver: &str) -> Result<Option<String>, QueryError> {
3775 let Some(caller) = row.caller_id.as_deref() else {
3776 return Ok(None);
3777 };
3778 if SELF_RECEIVERS.contains(&receiver) {
3779 return Ok(None);
3780 }
3781 let types = self.with_statement(RECEIVER_TYPE_SQL, |stmt| {
3782 stmt.query_map(params![caller, receiver, row.path], |r| {
3783 r.get::<_, String>(0)
3784 })?
3785 .collect::<Result<Vec<_>, _>>()
3786 })?;
3787 let [only] = types.as_slice() else {
3788 return Ok(None);
3789 };
3790 if self.holds_any_value(row, only)?
3791 || self.is_type_parameter(caller, &row.language, only)?
3792 {
3793 return Ok(None);
3794 }
3795 Ok(Some(only.clone()))
3796 }
3797
3798 fn owner_related(&self, type_name: &str, group: &[Symbol]) -> Result<bool, QueryError> {
3801 let mut owners: Vec<String> = group
3802 .iter()
3803 .filter_map(|member| member.signature.as_deref().and_then(extended_type))
3804 .collect();
3805 for member in group {
3806 if let Some(definition) = self.definition_by_id(&member.symbol_id)? {
3807 match definition.owner_name {
3808 Some(owner) => owners.push(owner),
3809 None => return Ok(true),
3810 }
3811 }
3812 }
3813 if owners.iter().any(|owner| owner == type_name) {
3814 return Ok(true);
3815 }
3816 let type_ancestors = self.ancestor_names(type_name)?;
3817 for owner in &owners {
3818 if type_ancestors.contains(owner) || self.ancestor_names(owner)?.contains(type_name) {
3819 return Ok(true);
3820 }
3821 }
3822 Ok(false)
3823 }
3824
3825 fn ancestor_names(&self, class_name: &str) -> Result<HashSet<String>, QueryError> {
3826 let mut seen: HashSet<String> = HashSet::new();
3827 let mut frontier = vec![class_name.to_string()];
3828 for _ in 0..8 {
3829 let mut next = Vec::new();
3830 for name in frontier {
3831 for sql in [ANCESTORS_BY_RELATIONSHIP_SQL, ANCESTORS_BY_PENDING_SQL] {
3832 let bases = self.with_statement(sql, |stmt| {
3833 stmt.query_map(params![name], |r| r.get::<_, String>(0))?
3834 .collect::<Result<Vec<_>, _>>()
3835 })?;
3836 for base in bases {
3837 if seen.insert(base.clone()) {
3838 next.push(base);
3839 }
3840 }
3841 }
3842 }
3843 if next.is_empty() {
3844 break;
3845 }
3846 frontier = next;
3847 }
3848 Ok(seen)
3849 }
3850
3851 fn foreign_type_head(
3855 &self,
3856 row: &NameRow,
3857 group: &[Symbol],
3858 ) -> Result<Option<String>, QueryError> {
3859 let Some(head) = row
3860 .namespace
3861 .last()
3862 .map(String::as_str)
3863 .or(row.receiver.as_deref())
3864 .filter(|head| is_plain_identifier(head) && !SELF_RECEIVERS.contains(head))
3865 else {
3866 return Ok(None);
3867 };
3868 if !head.chars().next().is_some_and(char::is_uppercase) {
3869 return Ok(None);
3870 }
3871 let kinds = self.with_statement(TYPE_HEAD_KINDS_SQL, |stmt| {
3872 stmt.query_map(params![head], |r| r.get::<_, String>(0))?
3873 .collect::<Result<Vec<_>, _>>()
3874 })?;
3875 let foreign = if kinds.is_empty() {
3876 true
3877 } else {
3878 let type_like = kinds.iter().any(|k| CLASS_LIKE_KINDS.contains(&k.as_str()));
3879 let aliased = kinds
3880 .iter()
3881 .any(|k| matches!(k.as_str(), "type" | "type_alias" | "typedef"));
3882 type_like && !aliased && !self.owner_related(head, group)?
3883 };
3884 Ok(foreign.then(|| head.to_string()))
3885 }
3886
3887 fn text_outcome(
3888 &self,
3889 root: &Path,
3890 name: &str,
3891 sites: &[SiteRowOut],
3892 ) -> Result<TextOutcome, QueryError> {
3893 let hits = scan_word(root, name, TEXT_SCAN_BUDGET);
3894 let site_spans: HashMap<String, Vec<(usize, usize)>> = {
3895 let mut spans: HashMap<String, Vec<(usize, usize)>> = HashMap::new();
3896 for site in sites {
3897 let start = site.site.start_line.unwrap_or(0);
3898 spans
3899 .entry(site.site.path.clone())
3900 .or_default()
3901 .push((start, site.end_line.unwrap_or(start).max(start)));
3902 }
3903 spans
3904 };
3905 let definition_spans = definition_spans(&self.definitions(name)?, Some(&hits.hits));
3906 let import_spans = self.import_spans(name)?;
3907
3908 let mut counts = ReferenceCounts::default();
3909 let mut text_only: Vec<TextOnlyLines> = Vec::new();
3910 for (path, line) in &hits.hits {
3911 counts.text_lines += 1;
3912 if in_spans(&site_spans, path, *line) {
3913 counts.site_lines += 1;
3914 } else if in_spans(&definition_spans, path, *line) {
3915 counts.definition_lines += 1;
3916 } else if in_spans(&import_spans, path, *line) {
3917 counts.import_lines += 1;
3918 } else {
3919 counts.text_only_lines += 1;
3920 match text_only.last_mut() {
3921 Some(last) if last.path == *path => last.lines.push(*line),
3922 _ => text_only.push(TextOnlyLines {
3923 path: path.clone(),
3924 lines: vec![*line],
3925 }),
3926 }
3927 }
3928 }
3929 Ok(TextOutcome {
3930 counts,
3931 text_only,
3932 summary: TextScanSummary {
3933 files_scanned: hits.files_scanned,
3934 files_total: hits.files_total,
3935 complete: hits.complete,
3936 elapsed_ms: hits.elapsed.as_millis() as u64,
3937 skipped: None,
3938 },
3939 })
3940 }
3941}
3942
3943type SiteRowOut = LabeledSite;
3944
3945fn claimed_verdict(row: &NameRow, claim: &Claim, rivals: usize, builds_class: bool) -> Verdict {
3946 if let Some(basis) = &claim.basis {
3947 if builds_class {
3948 return Verdict::new(SiteLabel::Proven, "builds the class", true);
3949 }
3950 if row.kind == "variable_ref"
3951 && language_family(&row.language) == "js"
3952 && matches!(basis.as_str(), "same file" | "enclosing scope")
3953 {
3954 return Verdict::new(
3955 SiteLabel::Likely,
3956 format!("{basis}; the index does not record callback parameters"),
3957 true,
3958 );
3959 }
3960 return Verdict::new(SiteLabel::Proven, basis, true);
3961 }
3962 if rivals > 0 {
3963 return Verdict::new(
3964 SiteLabel::Unresolved,
3965 format!("{} definitions", rivals + 1),
3966 true,
3967 );
3968 }
3969 if row.has_qualifier() {
3970 return Verdict::new(SiteLabel::Likely, "receiver not typed", true);
3971 }
3972 Verdict::new(SiteLabel::Likely, "name only", true)
3973}
3974
3975fn site_row(row: &NameRow, verdict: Verdict, spec: &Spec) -> SiteRow {
3976 let end_line = row.end_line.filter(|end| !row.paired && *end > row.line);
3977 SiteRow {
3978 site: ReferenceSite {
3979 from_symbol_name: row.caller_name.clone(),
3980 from_symbol_id: row.caller_id.clone().unwrap_or_default(),
3981 to_symbol_name: spec.name.clone(),
3982 kind: row.display_kind(),
3983 path: row.path.clone(),
3984 start_line: Some(row.site_line),
3985 start_column: row.site_column,
3986 occurrences: None,
3987 target: None,
3988 enclosing_symbol_name: None,
3989 },
3990 label: verdict.label,
3991 basis: verdict.basis,
3992 claimed: verdict.claimed,
3993 end_line,
3994 arg_count: row.arg_count,
3995 }
3996}
3997
3998fn merge_site_rows(rows: Vec<SiteRow>) -> Vec<LabeledSite> {
3999 let mut merged: HashMap<(String, usize), SiteRow> = HashMap::new();
4000 let mut order: Vec<(String, usize)> = Vec::new();
4001 for row in rows {
4002 let key = (row.site.path.clone(), row.site.start_line.unwrap_or(0));
4003 match merged.get_mut(&key) {
4004 None => {
4005 order.push(key.clone());
4006 merged.insert(key, row);
4007 }
4008 Some(kept) => {
4009 let end = kept.end_line.max(row.end_line);
4010 let better = (row.label.rank(), !row.claimed, row.site.start_column)
4011 < (kept.label.rank(), !kept.claimed, kept.site.start_column);
4012 if better {
4013 *kept = row;
4014 }
4015 kept.end_line = end;
4016 }
4017 }
4018 }
4019 order
4020 .into_iter()
4021 .filter_map(|key| merged.remove(&key))
4022 .map(|row| LabeledSite {
4023 site: row.site,
4024 label: row.label,
4025 basis: row.basis,
4026 end_line: row.end_line,
4027 arg_count: row.arg_count,
4028 })
4029 .collect()
4030}
4031
4032fn mark_hidden_receivers(root: &Path, rows: &mut [NameRow]) {
4036 let mut by_path: HashMap<String, Vec<usize>> = HashMap::new();
4037 for (index, row) in rows.iter().enumerate() {
4038 if row.is_call() && !row.has_qualifier() && row.language != "php" {
4040 by_path.entry(row.path.clone()).or_default().push(index);
4041 }
4042 }
4043 for (path, indices) in by_path {
4044 let Ok(text) = std::fs::read_to_string(root.join(&path)) else {
4045 continue;
4046 };
4047 let lines: Vec<&str> = text.lines().collect();
4048 for index in indices {
4049 let row = &mut rows[index];
4050 let Some((before, _)) = row
4051 .site_column
4052 .and_then(|column| split_line(&lines, row.site_line, column))
4053 else {
4054 continue;
4055 };
4056 let before = before.trim_end();
4057 let starts_line = matches!(before.trim_start(), "." | "?.")
4058 || before.is_empty() && continues_chain(&lines, row.site_line);
4059 row.hidden_receiver = starts_line || awaits_member(before);
4060 row.result_receiver = if starts_line {
4061 code_line_above(&lines, row.site_line).is_some_and(|above| {
4062 ends_with_result(above.trim_end_matches('.')) && !loads_module(above)
4063 })
4064 } else {
4065 awaits_member(before) && !loads_module(before)
4066 };
4067 }
4068 }
4069}
4070
4071fn awaits_member(text: &str) -> bool {
4074 text.strip_suffix('.').is_some_and(ends_with_result)
4075}
4076
4077fn ends_with_result(text: &str) -> bool {
4079 text.trim_end_matches(['?', '!'])
4080 .ends_with([')', ']', '\'', '"', '`'])
4081}
4082
4083fn loads_module(text: &str) -> bool {
4085 text.contains("require(") || text.contains("import(")
4086}
4087
4088fn const_holds_result(line: &str, name: &str) -> bool {
4091 let Some(at) = line.find(&format!("const {name}")) else {
4092 return false;
4093 };
4094 let rest = line[at + "const ".len() + name.len()..].trim_start();
4095 let initializer = match rest.strip_prefix(':') {
4096 Some(typed) => typed.split_once('=').map(|(_, value)| value),
4097 None => rest.strip_prefix('='),
4098 };
4099 let Some(initializer) = initializer.map(str::trim_start) else {
4100 return false;
4101 };
4102 if initializer.starts_with(['=', '>']) || loads_module(initializer) {
4103 return false;
4104 }
4105 let initializer = initializer.strip_prefix("await ").unwrap_or(initializer);
4106 let initializer = initializer.strip_prefix("new ").unwrap_or(initializer);
4107 let head = initializer
4108 .find(|c: char| !(c.is_alphanumeric() || matches!(c, '_' | '$' | '.' | '?')))
4109 .unwrap_or(initializer.len());
4110 head > 0 && initializer[head..].trim_start().starts_with(['(', '<'])
4111}
4112
4113fn continues_chain(lines: &[&str], line: usize) -> bool {
4116 code_line_above(lines, line).is_some_and(awaits_member)
4117}
4118
4119fn code_line_above<'l>(lines: &[&'l str], line: usize) -> Option<&'l str> {
4121 lines
4122 .iter()
4123 .take(line.saturating_sub(1))
4124 .rev()
4125 .map(|text| text.trim())
4126 .find(|text| !text.is_empty())
4127 .filter(|text| {
4128 !["//", "/*", "*", "#"]
4129 .iter()
4130 .any(|start| text.starts_with(start))
4131 })
4132}
4133
4134fn pair_identifiers(rows: &mut [NameRow]) {
4138 let mut by_caller: HashMap<String, (Vec<usize>, Vec<usize>)> = HashMap::new();
4139 for (index, row) in rows.iter().enumerate() {
4140 let Some(caller) = row.caller_id.clone() else {
4141 continue;
4142 };
4143 let entry = by_caller.entry(caller).or_default();
4144 match row.source {
4145 Source::Pending | Source::Relationship if row.kind == "calls" => entry.0.push(index),
4146 Source::Identifier if row.kind == "call" => entry.1.push(index),
4147 _ => {}
4148 }
4149 }
4150 for (calls, identifiers) in by_caller.values_mut() {
4151 calls.sort_by_key(|&i| (rows[i].line, rows[i].column, rows[i].source.tag(), i));
4152 identifiers.sort_by_key(|&i| (rows[i].line, rows[i].column, i));
4153 let mut used = vec![false; identifiers.len()];
4154 for &call in calls.iter() {
4155 let line = rows[call].line;
4156 let last = rows[call].caller_end_line.unwrap_or(usize::MAX).max(line);
4157 let Some(slot) = identifiers
4158 .iter()
4159 .enumerate()
4160 .position(|(slot, &i)| !used[slot] && (line..=last).contains(&rows[i].line))
4161 else {
4162 continue;
4163 };
4164 used[slot] = true;
4165 let identifier = identifiers[slot];
4166 let (site_line, site_column) = (rows[identifier].line, rows[identifier].column);
4167 let alone_on_line = identifiers
4168 .iter()
4169 .filter(|&&other| rows[other].line == site_line)
4170 .count()
4171 == 1;
4172 if alone_on_line && rows[call].links_enclosing_class && rows[identifier].is_super_call()
4173 {
4174 rows[call].consumed = true;
4175 rows[identifier].links_enclosing_class = true;
4176 continue;
4177 }
4178 rows[call].arg_count = rows[identifier].arg_count.filter(|_| alone_on_line);
4179 rows[identifier].consumed = true;
4180 rows[call].paired = true;
4181 rows[call].site_line = site_line;
4182 rows[call].site_column = site_column;
4183 }
4184 }
4185 attach_counts_by_line(rows);
4186}
4187
4188fn attach_counts_by_line(rows: &mut [NameRow]) {
4191 let mut identifiers: HashMap<(String, usize), Vec<Option<u32>>> = HashMap::new();
4192 for row in rows.iter() {
4193 if row.source == Source::Identifier && row.kind == "call" {
4194 identifiers
4195 .entry((row.path.clone(), row.line))
4196 .or_default()
4197 .push(row.arg_count);
4198 }
4199 }
4200 for row in rows.iter_mut() {
4201 if row.source != Source::Identifier
4202 && row.is_call()
4203 && row.arg_count.is_none()
4204 && let Some(counts) = identifiers.get(&(row.path.clone(), row.line))
4205 && let [count] = counts.as_slice()
4206 {
4207 row.arg_count = *count;
4208 }
4209 }
4210}
4211
4212const BODY_IS_FIRST_STATEMENT: &[&str] = &["python", "ruby", "lua", "fsharp", "vbnet"];
4215
4216fn definition_spans(
4220 definitions: &[Definition],
4221 name_lines: Option<&[(String, usize)]>,
4222) -> HashMap<String, Vec<(usize, usize)>> {
4223 let mut spans: HashMap<String, Vec<(usize, usize)>> = HashMap::new();
4224 for definition in definitions {
4225 let start = definition
4226 .decorated_start_line
4227 .unwrap_or(definition.start_line);
4228 let names_a_line_in = |first: usize, last: usize| {
4229 name_lines.is_none_or(|lines| {
4230 let from = lines.partition_point(|(path, line)| {
4231 (path.as_str(), *line) < (definition.path.as_str(), first)
4232 });
4233 lines
4234 .get(from)
4235 .is_some_and(|(path, line)| *path == definition.path && *line <= last)
4236 })
4237 };
4238 let end = definition
4239 .body_start_line
4240 .map_or(definition.start_line, |body| {
4241 if BODY_IS_FIRST_STATEMENT.contains(&definition.language.as_str())
4242 && names_a_line_in(start, body.saturating_sub(1))
4243 {
4244 body.saturating_sub(1)
4245 } else {
4246 body
4247 }
4248 })
4249 .max(definition.start_line);
4250 spans
4251 .entry(definition.path.clone())
4252 .or_default()
4253 .push((start, end));
4254 }
4255 spans
4256}
4257
4258fn in_spans(spans: &HashMap<String, Vec<(usize, usize)>>, path: &str, line: usize) -> bool {
4259 spans.get(path).is_some_and(|list| {
4260 list.iter()
4261 .any(|&(start, end)| start <= line && line <= end)
4262 })
4263}
4264
4265fn shared_leading_segments(a: &str, b: &str) -> usize {
4266 a.split('/')
4267 .zip(b.split('/'))
4268 .take_while(|(x, y)| x == y)
4269 .count()
4270}
4271
4272fn language_family(language: &str) -> &str {
4273 match language {
4274 "javascript" | "typescript" | "tsx" | "jsx" | "vue" => "js",
4275 "java" | "kotlin" | "scala" | "groovy" => "jvm",
4276 "c" | "cpp" | "objc" | "objcpp" | "qml" => "c",
4277 "csharp" | "razor" | "vbnet" | "fsharp" => "dotnet",
4278 other => other,
4279 }
4280}
4281
4282fn languages_compatible(row: &str, target: &str) -> bool {
4283 row.is_empty() || language_family(row) == language_family(target)
4284}
4285
4286fn import_segment(source: &str) -> Option<String> {
4288 let source = source.trim();
4289 let first_segment = source.split([':', '.']).next().unwrap_or("");
4290 if source.is_empty()
4291 || source.starts_with('.')
4292 || ["crate", "self", "super"].contains(&first_segment)
4293 {
4294 return None;
4295 }
4296 let normalized = source
4297 .replace("::", ".")
4298 .replace('/', ".")
4299 .replace('-', "_");
4300 normalized
4301 .rsplit('.')
4302 .find(|segment| !segment.is_empty())
4303 .map(str::to_string)
4304}
4305
4306fn judge_sql(conn: &Connection, absent_target: bool) -> String {
4309 let target = "s_target";
4310 let parent = "s_target_parent";
4311 let target_join = if absent_target {
4312 format!(
4313 "JOIN (SELECT ?2 AS symbol_id, ?5 AS name, ?6 AS kind, ?7 AS language, ?8 AS path,
4314 ?9 AS start_line, ?10 AS parent_symbol_id) {target}"
4315 )
4316 } else {
4317 format!("JOIN symbols {target} ON {target}.symbol_id = ?2")
4318 };
4319 let predicate = pending_target_predicate(conn, target, parent);
4320 let evidence = evidence_sql(conn, target, parent);
4321 format!(
4322 "SELECT p.src, p.row_id,
4323 CASE WHEN p.src = 'I' AND p.kind IN ('type_usage', 'member_access')
4324 THEN COALESCE(({predicate}), 0) ELSE 1 END AS claimed,
4325 {evidence} AS basis
4326 FROM (
4327 SELECT 'P' AS src, CAST(pr.rowid AS TEXT) AS row_id,
4328 pr.from_symbol_id AS from_symbol_id, pr.kind AS kind, pr.path AS path,
4329 pr.start_line AS start_line, pr.start_column AS start_column,
4330 pr.target_receiver AS target_receiver,
4331 pr.target_namespace_json AS target_namespace_json,
4332 COALESCE((SELECT language FROM files WHERE file_id = pr.file_id), '') AS language,
4333 NULL AS receiver_qualifier
4334 FROM pending_relationships pr
4335 WHERE pr.target_terminal_name = ?1 AND pr.kind != 'imports'
4336 AND CAST(pr.rowid AS TEXT) IN (SELECT value FROM json_each(?3))
4337 UNION ALL
4338 SELECT 'I', i.identifier_id, i.containing_symbol_id, i.kind, i.path, i.start_line,
4339 i.start_column,
4340 CASE WHEN json_valid(i.metadata_json)
4341 THEN json_extract(i.metadata_json, '$.receiver') END,
4342 '[]', i.language,
4343 CASE WHEN json_valid(i.metadata_json)
4344 THEN json_extract(i.metadata_json, '$.receiver_qualifier') END
4345 FROM identifiers i
4346 WHERE i.name = ?1 AND i.identifier_id IN (SELECT value FROM json_each(?4))
4347 ) p
4348 LEFT JOIN symbols s_from ON s_from.symbol_id = p.from_symbol_id
4349 {target_join}
4350 LEFT JOIN symbols {parent} ON {parent}.symbol_id = {target}.parent_symbol_id
4351 WHERE {predicate}
4352 OR (p.src = 'I' AND p.kind IN ('type_usage', 'member_access'))"
4353 )
4354}
4355
4356fn import_match_sql() -> String {
4359 let receiver = module_import_binding_sql("t", "p.target_receiver");
4360 let bare = module_import_binding_sql("t", "t.name");
4361 format!(
4362 "SELECT t.symbol_id, t.path, COALESCE(t.signature, ''), t.language
4363 FROM (SELECT ?1 AS path, ?2 AS target_receiver) p
4364 JOIN symbols t ON t.name = ?3
4365 WHERE t.parent_symbol_id IS NULL AND t.kind NOT IN ('import', 'export')
4366 AND t.language NOT IN ({docs})
4367 AND CASE WHEN p.target_receiver != '' THEN {receiver} ELSE {bare} END",
4368 docs = documentation_language_list()
4369 )
4370}
4371
4372fn evidence_sql(conn: &Connection, target: &str, parent: &str) -> String {
4374 let ns = "json_each(CASE WHEN json_valid(p.target_namespace_json) THEN p.target_namespace_json ELSE '[]' END)";
4375 let has_namespace = "COALESCE(p.target_namespace_json, '[]') NOT IN ('', '[]')";
4376 let has_receiver = "COALESCE(p.target_receiver, '') != ''";
4377 let self_receiver = "p.target_receiver IN ('self', 'this', 'cls', 'Self', 'super')";
4378 let names_self = format!("EXISTS (SELECT 1 FROM {ns} WHERE value = 'Self')");
4379 let receiver_chain = class_chain_sql(target, "p.target_receiver IS NOT 'super'");
4380 let inherited = class_chain_sql(target, &format!("{target}.kind IN ('variable', 'field')"));
4381 let enclosing = enclosing_scope_sql(target);
4382 let implicit_languages = implicit_receiver_language_list();
4383 let module_receiver = module_import_binding_sql(target, "p.target_receiver");
4384 let module_bare = module_import_binding_sql(target, &format!("{target}.name"));
4385 let receiver_type = receiver_type_sql(conn, parent);
4386 let qualifier_reaches_owner = module_import_binding_sql(parent, "p.receiver_qualifier");
4387 let foreign_qualifier = format!(
4388 "(p.receiver_qualifier IS NOT NULL
4389 AND EXISTS (SELECT 1 FROM symbols q WHERE q.kind = 'import' AND q.path = p.path
4390 AND q.name = p.receiver_qualifier)
4391 AND NOT {qualifier_reaches_owner})"
4392 );
4393 let package_proximity = path_proximity(&format!("{target}.path"), "p.path");
4394 let built = built_receiver_sql(conn, target)
4395 .map(|built| {
4396 format!(
4397 "WHEN {has_receiver} AND {target}.parent_symbol_id IS NOT NULL AND {built}
4398 THEN 'builds the class'"
4399 )
4400 })
4401 .unwrap_or_default();
4402 format!(
4403 "CASE
4404 WHEN {has_namespace} AND {parent}.name IS NOT NULL
4405 AND EXISTS (SELECT 1 FROM {ns} WHERE value = {parent}.name)
4406 THEN 'qualified path'
4407 WHEN {has_namespace} AND {target}.parent_symbol_id IS NULL THEN 'qualified path'
4408 WHEN {target}.parent_symbol_id IS NOT NULL AND {names_self}
4409 AND s_from.parent_symbol_id = {target}.parent_symbol_id THEN 'class chain'
4410 WHEN {target}.parent_symbol_id IS NOT NULL
4411 AND ({self_receiver} OR {names_self}) AND {receiver_chain} THEN 'class chain'
4412 WHEN {has_receiver} AND {parent}.name = p.target_receiver AND NOT {foreign_qualifier}
4413 THEN 'qualified path'
4414 WHEN {has_receiver} AND {target}.parent_symbol_id IS NULL AND {module_receiver}
4415 THEN 'import'
4416 WHEN {has_receiver} AND {target}.parent_symbol_id IS NOT NULL AND {receiver_type}
4417 THEN 'receiver type'
4418 {built}
4419 WHEN NOT ({has_receiver}) AND NOT ({has_namespace}) THEN
4420 CASE
4421 WHEN {target}.parent_symbol_id IS NULL AND {target}.path = p.path
4422 THEN 'same file'
4423 WHEN {target}.parent_symbol_id = p.from_symbol_id THEN 'enclosing scope'
4424 WHEN {target}.parent_symbol_id IS NOT NULL AND {enclosing}
4425 THEN 'enclosing scope'
4426 WHEN {target}.parent_symbol_id IS NOT NULL AND p.language IN ({implicit_languages})
4427 AND (s_from.parent_symbol_id = {target}.parent_symbol_id OR {inherited})
4428 THEN 'class chain'
4429 WHEN {target}.parent_symbol_id IS NULL AND {module_bare} THEN 'import'
4430 WHEN {target}.parent_symbol_id IS NULL AND {target}.language IN ('go', 'java')
4431 AND {target}.language = p.language AND ({package_proximity}) >= 1
4432 THEN 'same package'
4433 END
4434 END"
4435 )
4436}
4437
4438#[cfg(test)]
4439mod tests {
4440 use super::*;
4441 use crate::{
4442 Workspace, find_julie_extract_binary, open_read_only, safe_tempdir, scan_workspace,
4443 };
4444
4445 fn report_past_the_time_limit(
4446 files: &[(&str, &str)],
4447 name: &str,
4448 path: Option<&str>,
4449 ) -> ReferenceReport {
4450 find_julie_extract_binary().expect("julie-extract binary must be present for tests");
4451 let temp = safe_tempdir();
4452 let root = temp.path();
4453 for (relative, text) in files {
4454 let full = root.join(relative);
4455 std::fs::create_dir_all(full.parent().unwrap()).unwrap();
4456 std::fs::write(full, text).unwrap();
4457 }
4458 let db = root.join(".code-kb").join("artifact.db");
4459 scan_workspace(&Workspace::new(root.to_path_buf()), &db, true).unwrap();
4460 let conn = open_read_only(&db).unwrap();
4461 let target = select_reference_target(&conn, name, path).unwrap();
4462 report_within(&conn, root, &target, Duration::ZERO).unwrap()
4463 }
4464
4465 #[test]
4466 fn sites_past_the_time_limit_keep_the_label_their_name_gives() {
4467 let report = report_past_the_time_limit(
4468 &[(
4469 "a.py",
4470 "def helper():\n return 1\n\n\ndef use():\n return helper()\n",
4471 )],
4472 "helper",
4473 None,
4474 );
4475
4476 let labels: Vec<(SiteLabel, &str)> = report
4477 .sites
4478 .iter()
4479 .map(|site| (site.label, site.basis.as_str()))
4480 .collect();
4481 assert_eq!(
4482 labels,
4483 [(
4484 SiteLabel::Likely,
4485 "name only; not checked within the time limit"
4486 )]
4487 );
4488 assert!(
4489 report
4490 .cannot_see
4491 .iter()
4492 .any(|note| note == "1 of 1 sites were not checked within the 0 s time limit"),
4493 "{:?}",
4494 report.cannot_see
4495 );
4496 }
4497
4498 #[test]
4499 fn sites_past_the_time_limit_count_only_the_definitions_that_can_reach_them() {
4500 let report = report_past_the_time_limit(
4501 &[
4502 (
4503 "a/name.go",
4504 "package a\n\nfunc Name() string {\n\treturn \"a\"\n}\n",
4505 ),
4506 (
4507 "b/cmd.go",
4508 "package b\n\ntype Command struct {\n\tName string\n}\n",
4509 ),
4510 (
4511 "c/use.go",
4512 "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",
4513 ),
4514 ],
4515 "Name",
4516 Some("a/name.go"),
4517 );
4518
4519 let labels: Vec<(usize, SiteLabel, &str)> = report
4520 .sites
4521 .iter()
4522 .map(|site| {
4523 (
4524 site.site.start_line.unwrap_or(0),
4525 site.label,
4526 site.basis.as_str(),
4527 )
4528 })
4529 .collect();
4530 assert_eq!(
4531 labels,
4532 [
4533 (
4534 4,
4535 SiteLabel::Likely,
4536 "name only; not checked within the time limit"
4537 ),
4538 (
4539 8,
4540 SiteLabel::Unresolved,
4541 "2 definitions; not checked within the time limit"
4542 ),
4543 ]
4544 );
4545 }
4546}