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