Skip to main content

code_kb_core/
reference_report.rs

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;
34/// How long the per-site checks of one report may run; later sites keep the label their name gives.
35const ACCOUNTING_TIME_LIMIT: Duration = Duration::from_secs(3);
36/// Rows per judge query, so the time limit is checked between queries.
37const 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"];
54/// The basis of a claim that rests on an import; a package import can match several files.
55const IMPORT_BASIS: &str = "import";
56/// The bases that rest on the written name alone, which a callback parameter of that name hides.
57const NAME_RESOLVED_BASES: &[&str] = &[
58    IMPORT_BASIS,
59    "same file",
60    "enclosing scope",
61    "qualified path",
62];
63
64/// Selects the callers target by name exactly as `find_references_scoped` does: the same
65/// `QueryError::SymbolNotFound` (built with `symbol_not_found_parts`) or ambiguity error.
66pub 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
84/// The labeled sites of the selected target and a whole-word text count of its name.
85/// `root` is the workspace root the text scan walks. `include_external` only matters for callees.
86pub 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
95/// The report the post-edit check reads: the labeled sites and the reach sections of `target`,
96/// without the whole-word text scan and without the exclusion of a call whose argument count the
97/// target does not accept, so a call that still passes the old count keeps its label. `target`
98/// may be a symbol the index no longer holds; the report then judges the sites of its name as if
99/// the symbol were still there, with its owner (`parent_symbol_id`) taken from the index.
100pub 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/// What a report computes besides the labeled sites.
121#[derive(Clone, Copy)]
122struct ReportMode {
123    text_scan: bool,
124    arity_exclusion: bool,
125    /// The index does not hold the target, so its row cannot be joined by id.
126    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
328/// The range of the overloads of `target` joined with the same-named methods of its base types,
329/// in Java, C#, and Kotlin, where a call sees the overloads it inherits (C++ and TypeScript hide
330/// them, and a Python override replaces the method). The own range when the walk was cut.
331pub(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
347/// The selected symbol and its overloads: the symbols with its name, owner, kind, and file.
348pub(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
383/// Whether the relationship row `r` links its call to a member of a class that encloses the
384/// caller, the caller included; `t` is the linked symbol.
385const 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
413/// An ownerless JavaScript method is an object-literal member or a property, which binds no name.
414const 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
450/// At this many candidate files an import counts as reaching the project, unchecked.
451const 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/// Which indexed rows of a name a spec accounts for.
573#[derive(Clone, Copy, PartialEq, Eq)]
574enum RowScope {
575    All,
576    Calls,
577    /// Only the pending `extends` and `implements` rows, which tie a type to its base by name.
578    Inheritance,
579}
580
581struct Spec {
582    name: String,
583    group: Vec<Symbol>,
584    scope: RowScope,
585    builds_class: bool,
586    /// The arguments a call to the group may pass; `None` when the index cannot tell.
587    arity: Option<arity::Arity>,
588    /// The function that holds a group of local functions, which no site outside it can name.
589    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    /// The last line of the caller; julie can name a caller that ends above the row.
615    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    /// The call is written on an expression (`getAsSingleElement().getAsInt()`), so it has a
630    /// receiver that julie did not record.
631    hidden_receiver: bool,
632    /// That receiver is the result of a call or an index, or a string literal, and loads no
633    /// module (`require(…)`, `import(…)`).
634    result_receiver: bool,
635    /// The names written before the used name (`["z", "core"]` for `z.core.f()`), `None` when
636    /// the row does not say.
637    chain: Option<Vec<String>>,
638    /// The arguments the call passes, `None` when julie recorded no count or the call spreads an
639    /// argument, so the count is unknown.
640    arg_count: Option<u32>,
641    /// julie linked the call to a member of a class that encloses the caller, the caller included.
642    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    /// Whether a definition of `kind` never hides the name from this row: a Java call names a
669    /// method, so a field, variable, or parameter of that name is another thing.
670    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    /// A JavaScript type usage written without a qualifier (`output<T>`), which a type of that
690    /// name in an enclosing scope shadows.
691    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    /// Whether the row can reach `definition`, another definition of the name: a call cannot
705    /// reach a field, and a member access cannot reach a function that only its scope names.
706    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    /// A place where a statically typed language names a type: a base type, a built type, a
725    /// written type, or the type of a C# member (julie's `uses`).
726    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    /// A Rust read of a value (`git.diff()`) never names a module (`mod git;`), which holds none.
741    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
748/// Whether `receiver` makes a call reach an ancestor of the caller's class, never the class
749/// itself: `super`, `base`, and the Kotlin form that names the ancestor (`super<Greeter>`).
750fn is_super_receiver(receiver: &str) -> bool {
751    matches!(receiver, "super" | "base") || receiver.starts_with("super<")
752}
753
754/// Languages where a function defined inside a function is named only inside it. JavaScript is
755/// not one here: such a function can be placed on an object the outer function returns.
756/// Languages where a type and a value are apart, so a place that names a type never names a value.
757const 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
765/// The function, method, or constructor that holds every member of `group` when each one is a
766/// function defined inside it in a language of [`LOCAL_FUNCTION_LANGUAGES`].
767fn 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
786/// A JavaScript method assigned to a property (`req.accepts = function`): no scope binds its name,
787/// so a bare use of that name reads an import or a global.
788fn 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
794/// The object a JavaScript method is assigned on (`req` for `req.param = function`), which julie
795/// keeps as `className` in metadata that a removed symbol no longer has.
796fn 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
805/// The `#if` condition of the branch that julie kept a symbol from (`preprocessorIf`), nested
806/// conditions joined with ` && `.
807pub(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
819/// The terms of a `preprocessorIf` condition: the conditions of nested branches, joined with
820/// ` && `.
821fn condition_terms(condition: &str) -> impl Iterator<Item = &str> {
822    condition.split(" && ").map(str::trim)
823}
824
825/// The namespace a C# `using` directive opens (`A.B` for `using A.B;` or `global using A.B;`),
826/// none for a `using static` or an alias.
827fn 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
838/// The type a C# extension method extends: `IEnumerable` for `Foo(this IEnumerable<T> source)`.
839fn 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
848/// A test-framework call that julie names after its label (`test("strictObject")`): code
849/// never calls the label by name.
850fn 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
870/// Whether a Java type signature declares a record (`@Deprecated(since = "1") public record
871/// Point(int x, int y)`): the word `record` followed by the type name.
872fn 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
882/// `text` without string literals and without the text inside parentheses, each replaced by a
883/// space (`@Schema(description = "a record") public class T` is `@Schema  public class T`).
884fn 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
918/// Whether the declaration `type Name func(...)`, `type Name = func(...)`, `type Name[T any]
919/// func(T)`, or Rust `type Name = fn(...)` declares a function type.
920fn 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
949/// `line` (1-based) split at the byte `column`.
950fn 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
954/// `text` without the blanks and `/* */` comments before it; a `//` comment ends the line.
955fn 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
968/// `text` without the blanks and `/* */` comments at its end.
969fn 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
982/// Whether `name` written at `line` and `column` stands where an object literal holds a value
983/// (`{ render }`, `{ key: render, other }`): `{`, `,`, `:`, or nothing before it, and `,`, `}`, or
984/// nothing after it, blanks and comments aside. A call argument (`f(a, render)`) and a receiver
985/// (`render.use()`) do not.
986fn 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    /// The first decorator line above the definition, when it has decorators.
1008    decorated_start_line: Option<usize>,
1009    body_start_line: Option<usize>,
1010    owner_name: Option<String>,
1011    local: bool,
1012    /// The lines of the function that holds this function, which alone names it in a language of
1013    /// [`LOCAL_FUNCTION_LANGUAGES`].
1014    scope: Option<(usize, usize)>,
1015    kind: String,
1016    language: String,
1017    signature: String,
1018    /// The object a JavaScript function is assigned on (`res.redirect = function`).
1019    class_name: Option<String>,
1020    top_level: bool,
1021    /// A call can reach it: false for a field, property, variable, or constant in Go, Java, or
1022    /// Rust, except a Go one that holds a func.
1023    callable: bool,
1024    /// A JavaScript function, variable, or constant that only its own scope names: not exported,
1025    /// not a member of an object, and never written as `object.name` in its file.
1026    free: bool,
1027    /// A top-level JavaScript function, variable, or constant that no object holds: never written
1028    /// as `object.name` in its file and never placed in an object literal there or in a file that
1029    /// imports the name. Only a module object can hold it, and no call result is one.
1030    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    /// The module source and imported name when this is a JavaScript variable that a
1042    /// destructured dynamic import binds: a binding, not a definition.
1043    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    /// See [`is_property_method`].
1050    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
1142/// A definition that an import reaches: its symbol id, path, and language.
1143type ImportMatch = (String, String, String);
1144
1145struct Accounting<'a> {
1146    conn: &'a Connection,
1147    root: &'a Path,
1148    /// The judge stops here, so the cheaper checks of the judged rows still fit in the limit.
1149    judge_deadline: Instant,
1150    deadline: Instant,
1151    judge_stmt: RefCell<Option<Statement<'a>>>,
1152    /// The target when the index no longer holds it, and the judge that reads its columns from
1153    /// the symbol instead of joining its row.
1154    absent: Option<Symbol>,
1155    absent_judge_stmt: RefCell<Option<Statement<'a>>>,
1156    /// The C# `#if` condition of the branch that julie kept the target from.
1157    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    /// Definitions an import reaches, by caller file, receiver, and name.
1164    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    /// Starts a new time limit and keeps every prepared statement and cache. The judge statement
1196    /// is large, so a later phase of the report reuses it instead of preparing its own.
1197    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    /// Runs `run` on the statement for `sql`, preparing it on first use: preparing the larger
1203    /// queries costs more than running them once per row.
1204    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    /// Runs `sql` with `path` as `?1` and `others` after it, once for each spelling of the path
1220    /// the index can hold (`/` and `\`), and reads every row.
1221    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    /// The files among `paths` that use `name` as a member of an object: written `object.name`,
1399    /// or placed in an object literal (`{ name }`, `{ key: name }`).
1400    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    /// Whether a Go field or variable may hold a func: it has no written type, a qualified type
1446    /// (`http.HandlerFunc`), a func type, or a project type declared as `type X func(...)`.
1447    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    /// Whether a Rust field or static is declared with a project type alias of a fn type
1478    /// (`static HOOK: Callback` with `type Callback = fn(i32)`).
1479    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    /// Whether a project type named `type_name` is declared as a func (Go) or fn (Rust) type.
1496    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    /// The label and basis the report gives each pending `extends` or `implements` row that names
1736    /// `owner`, by the id of the type that holds the row; the best label wins when a type has
1737    /// several rows.
1738    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        // A full-path import row (`com.squareup.moshi.Json`) ends in `.name`; the trigram
1965        // index finds those rows without a scan of every import row.
1966        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    /// Judges `rows` (pending and identifier rows; relationship rows need no judge) for `target_id`.
2050    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    /// The claims of the target group on the rows of `order`, judged in chunks until the time
2109    /// limit, and how many rows at the start of `order` were judged.
2110    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                        // julie links a call inside a method to that method by name alone.
2171                        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        // julie writes a Java method reference (`parser::next`) as a call, so a member access
2253        // without parentheses reads a field.
2254        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    /// A bare Java call whose file imports the name statically from a class that owns no
2363    /// target (`import static java.lang.Math.toIntExact;`). Java takes such a call from the import
2364    /// unless the caller's file or class chain defines the name. A removed target defines nothing.
2365    /// A bare Java call names a method of the caller's class, of a class that encloses it, of
2366    /// their ancestors, or of a class that the file imports it from statically (`import static
2367    /// C.name;` or `import static C.*;`). An external base class cannot extend a project class,
2368    /// so an ancestor that is not indexed does not change this.
2369    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        // julie hangs the methods of an anonymous class on the field or method that builds it,
2400        // so its supertype, whose methods a bare call also reaches, is unknown.
2401        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    /// A Rust path call `krate::module::Owner::name()` whose leading segments name the crate and
2520    /// the modules that hold the target. The resolver maps crate names to folders only for
2521    /// functions without an owner, so it leaves such a method call unclaimed.
2522    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    /// A `super` or `base` call whose julie link into the caller's own class was dropped reaches
2620    /// an ancestor of that class. A target none of whose owners is such an ancestor is out of
2621    /// reach.
2622    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    /// The receiver of a JavaScript call that names the object the target is assigned on
2658    /// (`res.redirect = function`), when no rival is assigned on an object of that name.
2659    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    /// The definition of the target the index no longer holds, with the owner its
2733    /// `parent_symbol_id` names.
2734    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    /// The nearest definition of the name in a scope that holds the call, when it is not part of
2763    /// the target group: a parameter, local variable, or nested function of the caller or an
2764    /// enclosing function, or a top-level definition of the caller's file. In one scope the last
2765    /// definition above the call wins, since julie does not record block scopes.
2766    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    /// The first row of `sql` that `row` can name and that is not a test label
2798    /// (`describe("mix")`), which julie indexes as a function but code never reads by name.
2799    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    /// The definitions that the import binding of `receiver` (or of `name` itself when the
2822    /// receiver is empty) in file `importer` reaches, as `(symbol_id, path, language)` rows.
2823    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    /// How many top-level definitions outside the target group the import behind the row also
2856    /// reaches: a package import (`import * as z from "zod"`) can match every file of the package.
2857    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    /// The top-level definitions named `name`, in the language of the importer, that the import
2881    /// binding of `name` in file `importer` reaches. A Python absolute import reaches only the
2882    /// files its first segment starts at.
2883    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    /// Marks the JavaScript calls whose receiver is a `const` of the caller's scope or file that
2922    /// holds a call result or a new object (`const schema = z.object({…})`), the nearest
2923    /// declaration above the call.
2924    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    /// The other definitions that can reach `row`; a module receiver reaches no member of a class.
2970    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    /// A bare C# use of a top-level type whose namespace the use's file neither holds nor opens,
2995    /// and that no `using Name = …;` alias names.
2996    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    /// A bare C# use of a target that julie kept from an `#if !X` branch, in a file that always
3036    /// opens a namespace that other files open under `#if X`: the file builds against that
3037    /// namespace, which defines the name where `X` holds. A file that opens it under `#if X`
3038    /// itself pairs its use with the target, which a build without `X` needs, so it stays.
3039    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    /// Whether a bare C# use can name the type of `other`: its namespace is the global one, one
3098    /// that holds the use, or one that a `using` of the use's file or a `global using` opens. A
3099    /// project file with an MSBuild `<Using …/>` item opens namespaces the index does not see, so
3100    /// every namespace counts as open there.
3101    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    /// Whether a `.csproj`, `.props`, or `.targets` file of the project holds a `<Using …/>` item.
3123    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    /// The dotted namespace that holds `symbol_id`, read from its `namespace` ancestors; empty
3139    /// for the global namespace. A removed target starts from its owner in the index.
3140    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    /// Whether the receiver chain of a Python row names a module of the project: `flask.json`
3164    /// after `import flask`, or `json` after `from flask import json`.
3165    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    /// Whether the absolute import that binds the row's qualifier, or the name itself, starts at
3210    /// the top package of `path` or at a folder above it: `import json` does not reach
3211    /// `src/flask/json/provider.py`, whose top package is `flask`.
3212    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    /// Whether the absolute import of `binding` in file `importer` starts at the top package of
3222    /// `path` or at a folder above it.
3223    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    /// The first segments of an absolute import that reach the Python file `path`: its top package
3246    /// (the outermost folder with `__init__.py`) and the plain folders above it, or, outside a
3247    /// package, its folders and its stem.
3248    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    /// Whether the absolute Python import `source` reaches no file of the project: `import json`
3277    /// beside `src/flask/json/`, whose top package is `flask`, names the standard library.
3278    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    /// The import that binds the qualifier of the call to a module outside the index; for a
3308    /// JavaScript use, the first name written (`z` of `z.core.f()`, or `name` itself).
3309    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    /// A Java import of the receiver from a package that no project folder holds (`Assert` after
3373    /// `import org.junit.Assert;`). julie writes no source for a Java import, so the package is
3374    /// read from the signature: the segments before the first capitalized one.
3375    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    /// A lowercase head of a path call (`dunce::simplified()`) that no symbol, folder, or file of
3408    /// the project carries: a crate or package outside the index.
3409    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    /// The type of the receiver variable nearest to the caller, when its type facts agree.
3456    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    /// True when `type_name` is an owner of the target group or shares an inheritance chain
3481    /// with one, so a call through it may still reach the target.
3482    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    /// The capitalized type a call is written through (`Vec::new()`, `Command::new()`,
3534    /// `Math.max()`) when that type cannot own the target: no project symbol has its name, or it
3535    /// is a project class that is not an owner of the target, nor related to one, nor an alias.
3536    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
3714/// Marks the calls without a recorded receiver whose name follows a `.` after a call or index
3715/// result in the source (`single().getAsInt()`, `items[0]?.name()`), starts a line that
3716/// continues a chain (`    .readonly()`), or starts the line below a `.` (`make().` then `foo()`).
3717fn 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        // PHP writes string concatenation with `.`.
3721        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
3753/// Whether `text` ends with a call result, an index result, or a string literal and a `.`
3754/// (`make().`, `items[0]?.`, `make()!.`, `'abc'.`) that waits for a member name.
3755fn awaits_member(text: &str) -> bool {
3756    text.strip_suffix('.').is_some_and(ends_with_result)
3757}
3758
3759/// Whether `text` ends with a call result, an index result, or a string literal.
3760fn ends_with_result(text: &str) -> bool {
3761    text.trim_end_matches(['?', '!'])
3762        .ends_with([')', ']', '\'', '"', '`'])
3763}
3764
3765/// Whether `text` loads a module, whose result can be a module object.
3766fn loads_module(text: &str) -> bool {
3767    text.contains("require(") || text.contains("import(")
3768}
3769
3770/// Whether `line` declares `name` as a `const` that holds a call result or a new object
3771/// (`const schema = z.object({`) and loads no module.
3772fn 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
3795/// Whether the last line of code above `line` (1-based) ends with a call or index result and a
3796/// `.` that waits for the name on `line`.
3797fn continues_chain(lines: &[&str], line: usize) -> bool {
3798    code_line_above(lines, line).is_some_and(awaits_member)
3799}
3800
3801/// The last non-blank line above `line` (1-based), trimmed, unless it is a comment.
3802fn 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
3816/// Gives each pending or relationship call the first unused call identifier of the same caller
3817/// and name on or after its line and inside the caller, so a statement that starts above its
3818/// call reports the line of the call.
3819fn 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
3870/// Gives each pending or relationship call that has no count (a `new Foo(a)` construction) the
3871/// count of the one call identifier on its line.
3872fn 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
3894/// Languages where julie starts a body at its first statement, which is code and not part of the
3895/// definition. Elsewhere a body starts at its `{` or `=`, which can share the line of the name.
3896const BODY_IS_FIRST_STATEMENT: &[&str] = &["python", "ruby", "lua", "fsharp", "vbnet"];
3897
3898/// The lines of each definition. With `name_lines`, the sorted lines that hold the name, a
3899/// definition whose head lines hold no such line is a one-line definition (`@d` then
3900/// `def f(): return 1`), so its span keeps the first body line, which holds the name.
3901fn 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
3968/// The last segment of an import source, `None` for a relative path or a path inside the crate.
3969fn 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
3988/// SQL that returns, for every pending or identifier row with the name `?1`, whether the
3989/// predicate of the resolver ties it to the symbol `?2` and the positive evidence for it.
3990fn 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
4038/// The top-level definitions named `?3` that the import binding of receiver `?2` (or of the name
4039/// itself when `?2` is empty) in file `?1` reaches, as `(symbol_id, path)` rows.
4040fn 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
4054/// SQL `CASE` that names the positive evidence tying row `p` to `target`, or yields `NULL`.
4055fn 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}