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