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