use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::time::{Duration, Instant};
use rusqlite::types::ToSql;
use rusqlite::{Connection, OptionalExtension, Row, Statement, params, params_from_iter};
use crate::models::{
LabeledSite, ReferenceCounts, ReferenceReport, ReferenceSite, ReferenceTarget, SiteLabel,
Symbol, TextOnlyLines, TextScanSummary,
};
use crate::queries::{
QueryError, built_receiver_sql, class_chain_sql, constructed_class, direct_identifier_rows,
documentation_language_list, enclosing_scope_sql, get_symbol_by_name,
grouped_member_access_rows, has_column, implicit_receiver_language_list, import_sites,
import_sites_of_absent, inherited_writes_among, module_import_binding_sql, path_proximity,
pending_target_predicate, populate_lambda_enclosing_names, receiver_type_sql,
symbol_not_found_parts,
};
use crate::text_scan::scan_word;
pub(crate) mod arity;
mod js_bindings;
mod js_modules;
mod receiver_types;
mod sections;
pub(crate) mod variant_names;
pub(crate) use sections::{destructurings, enum_variant_names, uses_of_variants};
const RESOLVER_ROW_CAP: usize = 5_000;
const OTHER_DEFINITION_CAP: usize = 12;
const ACCOUNTING_TIME_LIMIT: Duration = Duration::from_secs(3);
const JUDGE_CHUNK: usize = 2_000;
const NOT_CHECKED: &str = "not checked within the time limit";
const TEXT_SCAN_BUDGET: Duration = Duration::from_secs(3);
const CLASS_LIKE_KINDS: &[&str] = &[
"class",
"struct",
"interface",
"trait",
"enum",
"record",
"object",
"protocol",
"union",
];
const SELF_RECEIVERS: &[&str] = &["self", "this", "cls", "Self", "super"];
const EXTERNAL_RUST_ROOTS: &[&str] = &["std", "core", "alloc"];
const IMPORT_BASIS: &str = "import";
const NAME_RESOLVED_BASES: &[&str] = &[
IMPORT_BASIS,
"same file",
"enclosing scope",
"qualified path",
];
pub fn select_reference_target(
conn: &Connection,
name: &str,
path_filter: Option<&str>,
) -> Result<Symbol, QueryError> {
match get_symbol_by_name(conn, name, path_filter)? {
Some(target) => Ok(target),
None => {
let (workspace, hint) = symbol_not_found_parts(conn, name, path_filter);
Err(QueryError::SymbolNotFound {
name: name.to_string(),
workspace,
hint,
})
}
}
}
pub fn reference_report(
conn: &Connection,
root: &Path,
target: &Symbol,
_include_external: bool,
) -> Result<ReferenceReport, QueryError> {
report_within(conn, root, target, ACCOUNTING_TIME_LIMIT)
}
pub fn reference_report_for_edit(
conn: &Connection,
root: &Path,
target: &Symbol,
absent_condition: Option<&str>,
) -> Result<ReferenceReport, QueryError> {
report_with(
conn,
root,
target,
ACCOUNTING_TIME_LIMIT,
ReportMode {
text_scan: false,
arity_exclusion: false,
absent_target: crate::queries::get_symbol_by_id(conn, &target.symbol_id)?.is_none(),
},
absent_condition,
)
}
#[derive(Clone, Copy)]
struct ReportMode {
text_scan: bool,
arity_exclusion: bool,
absent_target: bool,
}
fn report_within(
conn: &Connection,
root: &Path,
target: &Symbol,
time_limit: Duration,
) -> Result<ReferenceReport, QueryError> {
let mode = ReportMode {
text_scan: true,
arity_exclusion: true,
absent_target: false,
};
report_with(conn, root, target, time_limit, mode, None)
}
fn report_with(
conn: &Connection,
root: &Path,
target: &Symbol,
time_limit: Duration,
mode: ReportMode,
absent_condition: Option<&str>,
) -> Result<ReferenceReport, QueryError> {
let class = constructor_class(conn, target)?;
let group = overload_group(conn, target)?;
let mut accounting = Accounting::new(conn, root, time_limit);
accounting.absent = mode.absent_target.then(|| target.clone());
accounting.target_condition = match absent_condition {
Some(condition) => Some(condition.to_string()),
None => preprocessor_condition(conn, &target.symbol_id)?,
};
let target_arity = match arity::group_arity(conn, root, &group, false)? {
Some(own) if mode.arity_exclusion => {
arity::widened_by_ancestors(&mut accounting, target, own)?
}
_ => None,
};
accounting.restart_clock(time_limit);
let mut specs = vec![Spec {
name: target.name.clone(),
arity: target_arity,
local_to: enclosing_function(conn, &group)?,
group,
scope: if class.is_some() {
RowScope::Calls
} else {
RowScope::All
},
builds_class: false,
}];
if let Some(class) = &class {
specs.push(Spec {
name: class.name.clone(),
group: vec![class.clone()],
scope: RowScope::Calls,
builds_class: true,
local_to: None,
arity: if mode.arity_exclusion {
arity::group_arity(conn, root, &specs[0].group, true)?
} else {
None
},
});
}
let mut site_rows = Vec::new();
let mut cannot_see = Vec::new();
for spec in &specs {
accounting.account_spec(spec, &mut site_rows, &mut cannot_see)?;
}
let definitions_with_name = accounting.definition_count(&target.name)?;
let mut sites = merge_site_rows(site_rows);
let unchecked = sites
.iter()
.filter(|site| site.basis.ends_with(NOT_CHECKED))
.count();
if unchecked > 0 {
cannot_see.push(format!(
"{unchecked} of {} sites were not checked within the {} s time limit",
sites.len(),
time_limit.as_secs()
));
}
let mut report_sites: Vec<ReferenceSite> = sites.iter().map(|row| row.site.clone()).collect();
populate_lambda_enclosing_names(conn, &mut report_sites)?;
for (row, site) in sites.iter_mut().zip(report_sites) {
row.site.enclosing_symbol_name = site.enclosing_symbol_name;
}
sites.sort_by(|a, b| {
a.label
.rank()
.cmp(&b.label.rank())
.then_with(|| a.site.path.cmp(&b.site.path))
.then_with(|| a.site.start_line.cmp(&b.site.start_line))
.then_with(|| a.site.start_column.cmp(&b.site.start_column))
});
let text = match &class {
_ if !mode.text_scan => TextOutcome::not_run(),
Some(class) => TextOutcome::skipped(class),
None => accounting.text_outcome(root, &target.name, &sites)?,
};
let mut counts = ReferenceCounts {
sites: sites.len(),
..text.counts
};
for site in &sites {
match site.label {
SiteLabel::Proven => counts.proven += 1,
SiteLabel::Likely => counts.likely += 1,
SiteLabel::Unresolved => counts.unresolved += 1,
SiteLabel::Excluded => counts.excluded += 1,
}
}
let reach = sections::reach_sections(&mut accounting, target, &sites)?;
cannot_see.splice(0..0, cannot_see_for_language(&target.language));
cannot_see.extend(reach.notes);
Ok(ReferenceReport {
target: report_target(conn, target, definitions_with_name)?,
counts,
sites,
text_only: text.text_only,
text_scan: text.summary,
cannot_see,
imports: match mode.absent_target {
true => import_sites_of_absent(
conn,
&target.name,
&target.path,
target.parent_symbol_id.is_some(),
)?,
false => import_sites(conn, &target.name, Some(&target.path))?,
},
overrides: reach.overrides,
overridden: reach.overridden,
constructions: reach.constructions,
variant_uses: reach.variant_uses,
registrations: reach.registrations,
tests: reach.tests,
})
}
fn cannot_see_for_language(language: &str) -> Vec<String> {
let mut items = Vec::new();
match language {
"python" => items.push("calls through getattr() or names built at runtime".to_string()),
"javascript" | "typescript" | "tsx" | "jsx" | "vue" => items
.push("calls through computed property names or values built at runtime".to_string()),
"rust" => items.push("calls inside macro bodies".to_string()),
"java" | "csharp" | "kotlin" => {
items.push("calls through reflection or dependency injection".to_string());
}
"go" => items.push(
"calls through a Go interface that the index does not record reach methods this list does not show"
.to_string(),
),
_ => {}
}
items
}
fn report_target(
conn: &Connection,
target: &Symbol,
definitions_with_name: usize,
) -> Result<ReferenceTarget, QueryError> {
let mut qualified = target.clone();
crate::queries::qualify_members(conn, std::iter::once(&mut qualified))?;
Ok(ReferenceTarget {
symbol_id: target.symbol_id.clone(),
name: target.name.clone(),
display_name: qualified.name,
kind: crate::formatters::display_kind(target).to_string(),
path: target.path.clone(),
line: target.start_line,
definitions_with_name,
})
}
fn is_constructor(target: &Symbol) -> bool {
target.kind == "constructor" || (target.language == "python" && target.name == "__init__")
}
fn constructor_class(conn: &Connection, target: &Symbol) -> Result<Option<Symbol>, QueryError> {
if !is_constructor(target) {
return Ok(None);
}
let class_id = match constructed_class(conn, &target.symbol_id)? {
Some((id, _)) => id,
None => match &target.parent_symbol_id {
Some(parent) => parent.clone(),
None => return Ok(None),
},
};
crate::queries::get_symbol_by_id(conn, &class_id)
}
pub(crate) fn inherited_group_arity(
conn: &Connection,
root: &Path,
target: &Symbol,
) -> Result<Option<arity::Arity>, QueryError> {
let group = overload_group(conn, target)?;
let Some(own) = arity::group_arity(conn, root, &group, false)? else {
return Ok(None);
};
if !matches!(target.language.as_str(), "java" | "csharp" | "kotlin") {
return Ok(Some(own));
}
let mut accounting = Accounting::new(conn, root, ACCOUNTING_TIME_LIMIT);
Ok(arity::widened_by_ancestors(&mut accounting, target, own.clone())?.or(Some(own)))
}
pub(crate) fn overload_group(
conn: &Connection,
target: &Symbol,
) -> Result<Vec<Symbol>, QueryError> {
let mut stmt = conn.prepare(
"SELECT symbol_id FROM symbols
WHERE name = ?1 AND path = ?2 AND kind = ?3 AND parent_symbol_id IS ?4
ORDER BY start_line, symbol_id",
)?;
let ids = stmt
.query_map(
params![
target.name,
target.path,
target.kind,
target.parent_symbol_id
],
|row| row.get::<_, String>(0),
)?
.collect::<Result<Vec<_>, _>>()?;
let mut group = vec![target.clone()];
for id in ids.into_iter().filter(|id| *id != target.symbol_id) {
if let Some(symbol) = crate::queries::get_symbol_by_id(conn, &id)? {
group.push(symbol);
}
}
Ok(group)
}
const DEFINITION_BY_ID_SQL: &str =
"SELECT s.symbol_id, s.name, s.kind, s.language, s.path, s.start_line,
s.body_start_line, s.parent_symbol_id, par.name, par.kind, COALESCE(s.signature, '')
FROM symbols s LEFT JOIN symbols par ON par.symbol_id = s.parent_symbol_id
WHERE s.symbol_id = ?1";
const LINKS_ENCLOSING_CLASS_SQL: &str = "EXISTS (
WITH RECURSIVE enclosing(symbol_id, depth) AS (
SELECT r.from_symbol_id, 0
UNION ALL
SELECT s.parent_symbol_id, enclosing.depth + 1
FROM symbols s JOIN enclosing ON s.symbol_id = enclosing.symbol_id
WHERE s.parent_symbol_id IS NOT NULL AND enclosing.depth < 32
)
SELECT 1 FROM enclosing WHERE symbol_id = t.parent_symbol_id
)";
const SHADOW_IN_CHAIN_SQL: &str = "WITH RECURSIVE chain(symbol_id, depth) AS (
SELECT ?1, 0
UNION ALL
SELECT s.parent_symbol_id, chain.depth + 1
FROM symbols s JOIN chain ON s.symbol_id = chain.symbol_id
WHERE s.parent_symbol_id IS NOT NULL AND chain.depth < 32
)
SELECT d.symbol_id, COALESCE(d.signature, ''), d.kind
FROM chain
JOIN symbols scope ON scope.symbol_id = chain.symbol_id
JOIN symbols d ON d.parent_symbol_id = chain.symbol_id
WHERE d.name = ?2 AND d.path = ?3
AND (d.kind IN ('variable', 'parameter', 'function', 'constant', 'class')
OR ?4 AND d.kind IN ('type', 'interface', 'enum'))
AND (chain.depth = 0 OR scope.kind IN ('function', 'method', 'constructor'))
ORDER BY chain.depth, d.start_line > ?5, abs(?5 - d.start_line)";
const SHADOW_TOP_LEVEL_SQL: &str = "SELECT symbol_id, COALESCE(signature, ''), kind FROM symbols
WHERE name = ?1 AND path = ?2 AND parent_symbol_id IS NULL
AND kind NOT IN ('import', 'export', 'module', 'namespace')
AND NOT (kind = 'method' AND language IN ('javascript', 'typescript', 'tsx', 'jsx', 'vue'))
ORDER BY start_line > ?3, abs(?3 - start_line)";
const JAVA_STATIC_IMPORT_SQL: &str = "SELECT signature FROM symbols
WHERE kind = 'import' AND path = ?1 AND name = ?2 AND signature LIKE 'import static %'";
const IMPORT_SOURCES_SQL: &str = "SELECT COALESCE(json_extract(metadata_json, '$.source'), '')
FROM symbols
WHERE kind = 'import' AND path = ?1 AND json_valid(metadata_json)
AND (name = ?2 OR json_extract(metadata_json, '$.alias') = ?2
OR json_extract(metadata_json, '$.local_name') = ?2)";
const IMPORT_SIGNATURES_SQL: &str = "SELECT COALESCE(signature, '') FROM symbols
WHERE kind = 'import' AND path = ?1 AND name = ?2";
const PYTHON_IMPORT_ROWS_SQL: &str = "SELECT COALESCE(signature, ''),
COALESCE(json_extract(metadata_json, '$.source'), ''),
COALESCE(json_extract(metadata_json, '$.importedName'), '')
FROM symbols
WHERE kind = 'import' AND path = ?1 AND json_valid(metadata_json)
AND (name = ?2 OR json_extract(metadata_json, '$.alias') = ?2
OR json_extract(metadata_json, '$.local_name') = ?2)";
const PENDING_IMPORT_MODULES_SQL: &str = "SELECT target_namespace_json FROM pending_relationships
WHERE kind = 'imports' AND path = ?1 AND target_terminal_name = ?2";
const FILE_EXISTS_SQL: &str = "SELECT 1 FROM files WHERE path = ?1 OR path = ?2 LIMIT 1";
const PYTHON_FILES_NAMED_SQL: &str = "SELECT replace(path, '\\', '/') FROM files
WHERE language = 'python'
AND ('/' || replace(path, '\\', '/') LIKE ?1 OR '/' || replace(path, '\\', '/') LIKE ?2)
LIMIT 200";
const PYTHON_FILES_NAMED_CAP: usize = 200;
const SOURCE_FILES_SQL: &str = "SELECT 1 FROM files
WHERE '/' || replace(replace(path, '\\', '/'), '-', '_') LIKE ?1 ESCAPE '\\'
OR '/' || replace(replace(path, '\\', '/'), '-', '_') LIKE ?2 ESCAPE '\\'
LIMIT 1";
const RECEIVER_TYPE_SQL: &str = "WITH RECURSIVE chain(symbol_id, depth) AS (
SELECT ?1, 0
UNION ALL
SELECT s.parent_symbol_id, chain.depth + 1
FROM symbols s JOIN chain ON s.symbol_id = chain.symbol_id
WHERE s.parent_symbol_id IS NOT NULL AND chain.depth < 32
), typed(depth, resolved_type) AS (
SELECT chain.depth, tf.resolved_type
FROM chain
JOIN symbols r ON r.parent_symbol_id = chain.symbol_id
JOIN type_facts tf ON tf.symbol_id = r.symbol_id
WHERE r.name = ?2 AND r.path = ?3
AND r.kind IN ('variable', 'parameter', 'field', 'property')
)
SELECT DISTINCT resolved_type FROM typed
WHERE depth = (SELECT MIN(depth) FROM typed)";
const ANCESTORS_BY_RELATIONSHIP_SQL: &str = "SELECT DISTINCT base.name
FROM symbols child
JOIN relationships r ON r.from_symbol_id = child.symbol_id
AND r.kind IN ('extends', 'implements')
JOIN symbols base ON base.symbol_id = r.to_symbol_id
WHERE child.name = ?1";
const ANCESTORS_BY_PENDING_SQL: &str = "SELECT DISTINCT pe.target_terminal_name
FROM symbols child
JOIN pending_relationships pe ON pe.from_symbol_id = child.symbol_id
AND pe.kind IN ('extends', 'implements')
WHERE child.name = ?1";
const OWNER_IN_PACKAGE_SQL: &str = "SELECT COUNT(*) FROM symbols
WHERE name = ?1
AND kind IN ('class', 'struct', 'interface', 'trait', 'enum', 'record', 'object', 'protocol', 'union')
AND replace(path, '\\', '/') LIKE ?2 ESCAPE '\\'";
const TYPE_HEAD_KINDS_SQL: &str =
"SELECT DISTINCT kind FROM symbols WHERE name = ?1 AND kind != 'parameter'";
const RECEIVER_DECLARATIONS_SQL: &str = "WITH RECURSIVE chain(symbol_id, depth) AS (
SELECT ?1, 0
UNION ALL
SELECT s.parent_symbol_id, chain.depth + 1
FROM symbols s JOIN chain ON s.symbol_id = chain.symbol_id
WHERE s.parent_symbol_id IS NOT NULL AND chain.depth < 32
)
SELECT chain.depth, r.kind, r.start_line FROM chain
JOIN symbols r ON r.parent_symbol_id = chain.symbol_id
WHERE r.name = ?2 AND r.path = ?3 AND r.kind IN ('variable', 'constant', 'parameter')
UNION ALL
SELECT 99, r.kind, r.start_line FROM symbols r
WHERE r.parent_symbol_id IS NULL AND r.name = ?2 AND r.path = ?3
AND r.kind IN ('variable', 'constant')
ORDER BY 1, 3 DESC";
const JS_IMPORTER_PATHS_SQL: &str = "SELECT DISTINCT replace(path, '\\', '/') FROM symbols
WHERE kind = 'import' AND name = ?1
AND language IN ('javascript', 'typescript', 'tsx', 'jsx', 'vue')";
const MEMBER_ACCESS_PATHS_SQL: &str =
"SELECT DISTINCT path FROM identifiers WHERE name = ?1 AND kind = 'member_access'";
const VARIABLE_REF_SITES_SQL: &str = "SELECT path, start_line, start_column FROM identifiers
WHERE name = ?1 AND kind = 'variable_ref' AND start_column IS NOT NULL";
const TYPE_DECLARATIONS_SQL: &str = "SELECT COALESCE(signature, '') FROM symbols
WHERE name = ?1 AND kind = 'type' AND language = ?2";
const SYMBOL_CHAIN_SQL: &str = "WITH RECURSIVE chain(symbol_id, depth) AS (
SELECT ?1, 0
UNION ALL
SELECT s.parent_symbol_id, chain.depth + 1
FROM symbols s JOIN chain ON s.symbol_id = chain.symbol_id
WHERE s.parent_symbol_id IS NOT NULL AND chain.depth < 32
)
SELECT s.kind, s.name FROM chain JOIN symbols s ON s.symbol_id = chain.symbol_id
ORDER BY chain.depth";
const CSHARP_USINGS_SQL: &str = "SELECT COALESCE(signature, '') FROM symbols
WHERE kind = 'import' AND language = 'csharp'
AND (path = ?1 OR signature LIKE 'global using %')";
const PREPROCESSOR_CONDITION_SQL: &str = "SELECT json_extract(metadata_json, '$.preprocessorIf')
FROM symbols WHERE symbol_id = ?1 AND json_valid(metadata_json)";
const CSHARP_FILE_USINGS_SQL: &str = "SELECT COALESCE(signature, ''),
CASE WHEN json_valid(metadata_json)
THEN json_extract(metadata_json, '$.preprocessorIf') END
FROM symbols
WHERE kind = 'import' AND language = 'csharp' AND path = ?1";
const CSHARP_CONDITIONAL_USINGS_SQL: &str = "SELECT COALESCE(signature, ''),
json_extract(metadata_json, '$.preprocessorIf')
FROM symbols
WHERE kind = 'import' AND language = 'csharp' AND json_valid(metadata_json)
AND json_extract(metadata_json, '$.preprocessorIf') IS NOT NULL";
const MSBUILD_FILES_SQL: &str = "SELECT path FROM files
WHERE path LIKE '%.csproj' OR path LIKE '%.props' OR path LIKE '%.targets'";
const CSHARP_TYPE_KINDS: &[&str] = &["class", "struct", "interface", "enum", "record", "delegate"];
const JAVA_STATIC_IMPORTS_SQL: &str = "SELECT signature FROM symbols
WHERE kind = 'import' AND path = ?1 AND (name = ?2 OR signature LIKE '%.*')
AND signature LIKE 'import static %'";
const CALLABLE_IN_FILE_SQL: &str = "SELECT 1 FROM symbols
WHERE path = ?1 AND name = ?2 AND kind IN ('method', 'function', 'constructor') LIMIT 1";
const TOP_LEVEL_VARIABLE_SQL: &str = "SELECT 1 FROM symbols
WHERE path = ?1 AND name = ?2 AND parent_symbol_id IS NULL
AND kind IN ('variable', 'constant') LIMIT 1";
const FN_TYPES: [&str; 5] = ["fn(", "fn (", "Fn(", "FnMut(", "FnOnce("];
#[derive(Clone, Copy, PartialEq, Eq)]
enum RowScope {
All,
Calls,
Inheritance,
}
struct Spec {
name: String,
group: Vec<Symbol>,
scope: RowScope,
builds_class: bool,
arity: Option<arity::Arity>,
local_to: Option<Symbol>,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum Source {
Pending,
Relationship,
Identifier,
}
impl Source {
fn tag(self) -> &'static str {
match self {
Source::Pending => "P",
Source::Relationship => "R",
Source::Identifier => "I",
}
}
}
struct NameRow {
key: String,
source: Source,
caller_id: Option<String>,
caller_name: String,
caller_end_line: Option<usize>,
path: String,
language: String,
kind: String,
line: usize,
column: Option<usize>,
end_line: Option<usize>,
receiver: Option<String>,
namespace: Vec<String>,
to_symbol_id: Option<String>,
site_line: usize,
site_column: Option<usize>,
consumed: bool,
paired: bool,
hidden_receiver: bool,
result_receiver: bool,
chain: Option<Vec<String>>,
arg_count: Option<u32>,
links_enclosing_class: bool,
}
impl NameRow {
fn has_qualifier(&self) -> bool {
self.hidden_receiver
|| self.receiver.as_deref().is_some_and(|r| !r.is_empty())
|| !self.namespace.is_empty()
}
fn qualifier(&self) -> Option<&str> {
self.namespace
.first()
.map(String::as_str)
.or(self.receiver.as_deref())
.filter(|q| is_plain_identifier(q))
}
fn is_super_call(&self) -> bool {
self.receiver.as_deref().is_some_and(is_super_receiver)
}
fn is_call(&self) -> bool {
matches!(self.kind.as_str(), "calls" | "call" | "instantiates")
}
fn is_not_named_by(&self, kind: &str) -> bool {
let value_kind = matches!(
kind,
"field" | "property" | "variable" | "constant" | "parameter"
);
let java_call = self.language == "java" && matches!(self.kind.as_str(), "calls" | "call");
let type_use_of_value =
self.is_bare_type_use() && (value_kind || matches!(kind, "function" | "method"));
(java_call && value_kind) || type_use_of_value
}
fn is_bare_use(&self) -> bool {
!self.has_qualifier()
&& matches!(
self.kind.as_str(),
"calls" | "call" | "variable_ref" | "references"
)
}
fn is_bare_type_use(&self) -> bool {
self.kind == "type_usage"
&& language_family(&self.language) == "js"
&& self.chain.as_ref().is_some_and(Vec::is_empty)
}
fn display_kind(&self) -> String {
match self.kind.as_str() {
"call" => "calls".to_string(),
other => other.to_string(),
}
}
fn can_reach(&self, definition: &Definition) -> bool {
let call_of_a_field = self.is_call()
&& !definition.callable
&& languages_compatible(&self.language, &definition.language);
let member_of_a_scoped_function =
definition.free && language_family(&self.language) == "js" && self.has_qualifier();
let call_on_a_result = self.result_receiver
&& definition.module_value
&& language_family(&self.language) == "js";
!call_of_a_field
&& !member_of_a_scoped_function
&& !call_on_a_result
&& !self.reads_a_module(&definition.kind)
&& !(self.is_bare_use() && definition.is_property_method())
&& self.is_in_scope_of(definition)
&& !(self.is_type_position() && definition.is_value())
}
fn is_type_position(&self) -> bool {
STATIC_TYPE_LANGUAGES.contains(&self.language.as_str())
&& (matches!(
self.kind.as_str(),
"extends" | "implements" | "instantiates" | "type_usage"
) || self.kind == "uses" && self.language == "csharp")
}
fn is_in_scope_of(&self, definition: &Definition) -> bool {
definition.scope.is_none_or(|(start, end)| {
self.path == definition.path && (start..=end).contains(&self.site_line)
})
}
fn reads_a_module(&self, kind: &str) -> bool {
self.language == "rust"
&& self.kind == "variable_ref"
&& matches!(kind, "namespace" | "module")
}
}
fn is_super_receiver(receiver: &str) -> bool {
matches!(receiver, "super" | "base") || receiver.starts_with("super<")
}
const STATIC_TYPE_LANGUAGES: &[&str] = &[
"csharp", "java", "kotlin", "scala", "swift", "dart", "rust", "go", "cpp", "c", "vbnet",
];
const LOCAL_FUNCTION_LANGUAGES: &[&str] = &[
"python", "rust", "kotlin", "csharp", "swift", "scala", "dart",
];
fn enclosing_function(conn: &Connection, group: &[Symbol]) -> Result<Option<Symbol>, QueryError> {
let Some(parent) = group
.first()
.and_then(|first| first.parent_symbol_id.as_deref())
else {
return Ok(None);
};
let local = group.iter().all(|member| {
member.kind == "function"
&& LOCAL_FUNCTION_LANGUAGES.contains(&member.language.as_str())
&& member.parent_symbol_id.as_deref() == Some(parent)
});
if !local {
return Ok(None);
}
Ok(crate::queries::get_symbol_by_id(conn, parent)?
.filter(|outer| matches!(outer.kind.as_str(), "function" | "method" | "constructor")))
}
fn is_property_method(symbol: &Symbol) -> bool {
symbol.kind == "method"
&& symbol.parent_symbol_id.is_none()
&& language_family(&symbol.language) == "js"
}
fn assigned_object(symbol: &Symbol) -> Option<String> {
if language_family(&symbol.language) != "js" {
return None;
}
let signature = symbol.signature.as_deref()?;
let (object, _) = signature.split_once(&format!(".{} =", symbol.name))?;
Some(object.trim().to_string()).filter(|object| is_plain_identifier(object))
}
pub(crate) fn preprocessor_condition(
conn: &Connection,
symbol_id: &str,
) -> Result<Option<String>, QueryError> {
Ok(conn
.query_row(PREPROCESSOR_CONDITION_SQL, params![symbol_id], |r| {
r.get::<_, Option<String>>(0)
})
.optional()?
.flatten())
}
fn condition_terms(condition: &str) -> impl Iterator<Item = &str> {
condition.split(" && ").map(str::trim)
}
fn csharp_using_namespace(signature: &str) -> Option<&str> {
let signature = signature.trim();
let directive = signature.strip_prefix("global ").unwrap_or(signature);
let namespace = directive
.strip_prefix("using ")?
.trim()
.trim_end_matches(';')
.trim();
(!namespace.starts_with("static ") && !namespace.contains('=')).then_some(namespace)
}
fn extended_type(signature: &str) -> Option<String> {
let rest = signature
.match_indices('(')
.find_map(|(open, _)| arity::after_csharp_this(&signature[open + 1..]))?;
let written = rest.split_whitespace().next()?;
let unqualified = written.split('<').next()?.rsplit('.').next()?;
Some(unqualified.trim_end_matches('?').to_string()).filter(|t| is_plain_identifier(t))
}
fn is_test_label(signature: &str) -> bool {
signature.split_once('(').is_some_and(|(callee, rest)| {
!callee.is_empty()
&& callee.split('.').all(is_plain_identifier)
&& rest.trim_start().starts_with(['"', '\'', '`'])
})
}
fn is_plain_identifier(text: &str) -> bool {
let mut chars = text.chars();
chars
.next()
.is_some_and(|c| c.is_alphabetic() || c == '_' || c == '$')
&& chars.all(is_word_char)
}
fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '$'
}
fn is_record(signature: &str) -> bool {
let declaration = outside_quotes_and_parentheses(signature);
declaration.match_indices("record").any(|(at, word)| {
let after = &declaration[at + word.len()..];
!declaration[..at].ends_with(is_word_char)
&& after.starts_with(char::is_whitespace)
&& after.trim_start().starts_with(is_word_char)
})
}
fn outside_quotes_and_parentheses(text: &str) -> String {
let mut outside = String::with_capacity(text.len());
let mut depth = 0usize;
let mut chars = text.chars();
while let Some(c) = chars.next() {
match c {
'"' | '\'' => {
while let Some(inner) = chars.next() {
match inner {
'\\' => {
chars.next();
}
_ if inner == c => break,
_ => {}
}
}
if depth == 0 {
outside.push(' ');
}
}
'(' => depth += 1,
')' => {
depth = depth.saturating_sub(1);
if depth == 0 {
outside.push(' ');
}
}
_ if depth == 0 => outside.push(c),
_ => {}
}
}
outside
}
fn is_function_type(signature: &str, name: &str, language: &str) -> bool {
let Some(declared) = signature
.trim_start()
.strip_prefix("type")
.and_then(|rest| rest.trim_start().strip_prefix(name))
else {
return false;
};
let declared = declared.trim_start();
let declared = match declared.chars().next() {
Some(open @ ('[' | '<')) => {
let close = if open == '[' { ']' } else { '>' };
let mut depth = 0;
let end = declared.char_indices().find(|&(_, c)| {
depth += i32::from(c == open) - i32::from(c == close);
depth == 0
});
end.map_or("", |(end, _)| &declared[end + 1..])
}
_ => declared,
};
let declared = declared.trim_start().trim_start_matches('=').trim_start();
if language == "go" {
declared.starts_with("func")
} else {
FN_TYPES.iter().any(|fn_type| declared.contains(fn_type))
}
}
fn split_line<'a>(lines: &[&'a str], line: usize, column: usize) -> Option<(&'a str, &'a str)> {
lines.get(line.checked_sub(1)?)?.split_at_checked(column)
}
fn after_blanks_and_comments(mut text: &str) -> &str {
loop {
text = text.trim_start();
if text.starts_with("//") {
return "";
}
match text.strip_prefix("/*") {
Some(comment) => text = comment.split_once("*/").map_or("", |(_, rest)| rest),
None => return text,
}
}
}
fn before_blanks_and_comments(mut text: &str) -> &str {
loop {
text = text.trim_end();
match text
.strip_suffix("*/")
.and_then(|comment| comment.rsplit_once("/*"))
{
Some((rest, _)) => text = rest,
None => return text,
}
}
}
fn is_object_value(lines: &[&str], line: usize, column: usize, name: &str) -> bool {
let Some((before, rest)) = split_line(lines, line, column) else {
return false;
};
let after = rest.get(name.len()..).unwrap_or("");
before_blanks_and_comments(before)
.chars()
.next_back()
.is_none_or(|c| matches!(c, '{' | ',' | ':'))
&& after_blanks_and_comments(after)
.chars()
.next()
.is_none_or(|c| matches!(c, ',' | '}'))
}
#[derive(Clone)]
struct Definition {
symbol_id: String,
name: String,
path: String,
start_line: usize,
decorated_start_line: Option<usize>,
body_start_line: Option<usize>,
owner_name: Option<String>,
local: bool,
scope: Option<(usize, usize)>,
kind: String,
language: String,
signature: String,
class_name: Option<String>,
top_level: bool,
callable: bool,
free: bool,
module_value: bool,
}
impl Definition {
fn is_value(&self) -> bool {
matches!(
self.kind.as_str(),
"field" | "constant" | "variable" | "property" | "parameter" | "enum_member"
)
}
fn dynamic_import(&self, name: &str) -> Option<(String, String)> {
(self.kind == "variable" && language_family(&self.language) == "js")
.then(|| js_modules::dynamic_import_binding(&self.signature, name))
.flatten()
}
fn is_property_method(&self) -> bool {
self.kind == "method" && self.top_level && language_family(&self.language) == "js"
}
fn display(&self) -> String {
match &self.owner_name {
Some(owner) => format!("{owner}.{}", self.name),
None => self.name.clone(),
}
}
fn resolves_to(&self) -> String {
format!(
"resolves to `{}` ({}:{})",
self.display(),
self.path,
self.start_line
)
}
}
#[derive(Clone)]
struct Verdict {
label: SiteLabel,
basis: String,
claimed: bool,
}
impl Verdict {
fn new(label: SiteLabel, basis: impl Into<String>, claimed: bool) -> Self {
Verdict {
label,
basis: basis.into(),
claimed,
}
}
}
struct SiteRow {
site: ReferenceSite,
label: SiteLabel,
basis: String,
claimed: bool,
end_line: Option<usize>,
arg_count: Option<u32>,
}
struct Claim {
basis: Option<String>,
}
struct Judged {
claimed: bool,
basis: Option<String>,
}
struct TextOutcome {
counts: ReferenceCounts,
text_only: Vec<TextOnlyLines>,
summary: TextScanSummary,
}
impl TextOutcome {
fn not_run() -> Self {
TextOutcome {
counts: ReferenceCounts::default(),
text_only: Vec::new(),
summary: TextScanSummary {
files_scanned: 0,
files_total: 0,
complete: true,
elapsed_ms: 0,
skipped: None,
},
}
}
fn skipped(class: &Symbol) -> Self {
TextOutcome {
counts: ReferenceCounts::default(),
text_only: Vec::new(),
summary: TextScanSummary {
files_scanned: 0,
files_total: 0,
complete: true,
elapsed_ms: 0,
skipped: Some(class.name.clone()),
},
}
}
}
type ImportMatch = (String, String, String);
struct Accounting<'a> {
conn: &'a Connection,
root: &'a Path,
judge_deadline: Instant,
deadline: Instant,
judge_stmt: RefCell<Option<Statement<'a>>>,
absent: Option<Symbol>,
absent_judge_stmt: RefCell<Option<Statement<'a>>>,
target_condition: Option<String>,
definitions_stmt: RefCell<Option<Statement<'a>>>,
import_match_stmt: Option<Statement<'a>>,
statements: RefCell<HashMap<&'static str, Statement<'a>>>,
external_sources: HashMap<String, bool>,
msbuild_usings: std::cell::OnceCell<bool>,
import_matches: HashMap<(String, String, String), Vec<ImportMatch>>,
js: js_modules::JsCaches,
receivers: receiver_types::ReceiverCaches,
}
impl<'a> Accounting<'a> {
fn new(conn: &'a Connection, root: &'a Path, time_limit: Duration) -> Self {
Accounting {
conn,
root,
judge_deadline: Instant::now() + time_limit / 2,
deadline: Instant::now() + time_limit,
judge_stmt: RefCell::new(None),
absent: None,
absent_judge_stmt: RefCell::new(None),
target_condition: None,
definitions_stmt: RefCell::new(None),
import_match_stmt: None,
statements: RefCell::new(HashMap::new()),
external_sources: HashMap::new(),
msbuild_usings: std::cell::OnceCell::new(),
import_matches: HashMap::new(),
js: js_modules::JsCaches::default(),
receivers: receiver_types::ReceiverCaches::default(),
}
}
fn out_of_time(&self) -> bool {
Instant::now() >= self.deadline
}
fn restart_clock(&mut self, time_limit: Duration) {
self.judge_deadline = Instant::now() + time_limit / 2;
self.deadline = Instant::now() + time_limit;
}
fn with_statement<T>(
&self,
sql: &'static str,
run: impl FnOnce(&mut Statement<'a>) -> rusqlite::Result<T>,
) -> Result<T, QueryError> {
let mut statements = self.statements.borrow_mut();
if !statements.contains_key(sql) {
statements.insert(sql, self.conn.prepare(sql)?);
}
let stmt = statements
.get_mut(sql)
.expect("statement was just prepared");
Ok(run(stmt)?)
}
fn rows_at_path<T>(
&self,
sql: &'static str,
path: &str,
others: &[&dyn ToSql],
read: fn(&Row) -> rusqlite::Result<T>,
) -> Result<Vec<T>, QueryError> {
let windows = path.replace('/', "\\");
let mut spellings = vec![path];
if windows != path {
spellings.push(&windows);
}
let mut rows = Vec::new();
for spelling in spellings {
let bound = std::iter::once(&spelling as &dyn ToSql).chain(others.iter().copied());
rows.extend(self.with_statement(sql, |stmt| {
stmt.query_map(params_from_iter(bound), read)?
.collect::<Result<Vec<_>, _>>()
})?);
}
Ok(rows)
}
fn definitions_sql(&self) -> String {
let decorated_start = if has_column(self.conn, "structural_facts", "end_byte")
&& has_column(self.conn, "symbols", "end_byte")
{
"(SELECT MIN(f.start_line) FROM structural_facts f
WHERE f.path = s.path AND f.pattern_id LIKE '%.decorated_definition.%'
AND f.end_byte = s.end_byte AND f.start_byte < s.start_byte)"
} else {
"NULL"
};
format!(
"SELECT s.symbol_id, s.name, s.kind, s.language, s.path, s.start_line,
s.body_start_line, s.parent_symbol_id, par.name, par.kind, {decorated_start},
COALESCE(s.signature, ''), s.visibility,
CASE WHEN json_valid(s.metadata_json) THEN
json_extract(s.metadata_json, '$.className') END,
CASE WHEN json_valid(s.metadata_json) THEN
json_extract(s.metadata_json, '$.isCommonJSExport') END,
COALESCE(par.signature, ''), par.start_line, par.end_line
FROM symbols s LEFT JOIN symbols par ON par.symbol_id = s.parent_symbol_id
WHERE s.name = ?1 AND s.kind NOT IN ('import', 'export')
AND s.language NOT IN ({})",
documentation_language_list()
)
}
fn definitions(&self, name: &str) -> Result<Vec<Definition>, QueryError> {
let mut slot = self.definitions_stmt.borrow_mut();
if slot.is_none() {
*slot = Some(self.conn.prepare(&self.definitions_sql())?);
}
let stmt = slot
.as_mut()
.expect("definitions statement was just prepared");
let rows = stmt.query_map(params![name], |row| {
let signature: String = row.get(11)?;
if is_test_label(&signature) {
return Ok(None);
}
let kind: String = row.get(2)?;
let language: String = row.get(3)?;
let parent_id: Option<String> = row.get(7)?;
let parent_kind: Option<String> = row.get(9)?;
let parent_name: Option<String> = row.get(8)?;
let class_name: Option<String> = row.get(13)?;
let in_function = parent_kind
.as_deref()
.is_some_and(|k| matches!(k, "function" | "method" | "constructor"));
let local = kind == "parameter" || kind == "variable" && in_function;
let scope = match (
row.get::<_, Option<i64>>(16)?,
row.get::<_, Option<i64>>(17)?,
) {
(Some(start), Some(end))
if in_function
&& kind == "function"
&& LOCAL_FUNCTION_LANGUAGES.contains(&language.as_str()) =>
{
Some((start as usize, end as usize))
}
_ => None,
};
let exported = row.get::<_, Option<String>>(12)?.as_deref() == Some("public")
|| row.get::<_, Option<i64>>(14)? == Some(1);
let module_value = language_family(&language) == "js"
&& matches!(kind.as_str(), "function" | "variable" | "constant")
&& class_name.is_none()
&& parent_id.is_none();
let free = language_family(&language) == "js"
&& matches!(kind.as_str(), "function" | "variable" | "constant")
&& class_name.is_none()
&& !exported
&& !local
&& (parent_id.is_none() || in_function);
let record_component = language == "java"
&& kind == "property"
&& is_record(&row.get::<_, String>(15)?)
&& !signature.split_whitespace().any(|word| word == "static");
let function_typed = language == "rust"
&& matches!(kind.as_str(), "variable" | "constant")
&& FN_TYPES.iter().any(|fn_type| signature.contains(fn_type));
let callable = record_component
|| function_typed
|| !(matches!(language.as_str(), "go" | "java" | "rust")
&& matches!(
kind.as_str(),
"field" | "property" | "variable" | "constant"
));
Ok(Some(Definition {
symbol_id: row.get(0)?,
name: row.get(1)?,
path: row.get::<_, String>(4)?.replace('\\', "/"),
start_line: row.get::<_, i64>(5)? as usize,
decorated_start_line: row.get::<_, Option<i64>>(10)?.map(|v| v as usize),
body_start_line: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
owner_name: parent_name.filter(|_| {
parent_kind
.as_deref()
.is_some_and(|k| CLASS_LIKE_KINDS.contains(&k))
}),
local,
scope,
top_level: parent_id.is_none(),
kind,
language,
signature,
class_name,
callable,
free,
module_value,
}))
})?;
let mut definitions: Vec<Definition> = rows
.filter_map(Result::transpose)
.collect::<Result<_, _>>()?;
drop(slot);
let writes =
inherited_writes_among(self.conn, definitions.iter().map(|d| d.symbol_id.as_str()))?;
definitions.retain(|d| !writes.contains(&d.symbol_id) && d.dynamic_import(name).is_none());
let mut probed: HashSet<&str> = definitions
.iter()
.filter(|d| d.free || d.module_value)
.map(|d| d.path.as_str())
.collect();
let importers = if definitions.iter().any(|d| d.module_value) {
self.with_statement(JS_IMPORTER_PATHS_SQL, |stmt| {
stmt.query_map(params![name], |r| r.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()
})?
} else {
Vec::new()
};
probed.extend(importers.iter().map(String::as_str));
let member_paths = if probed.is_empty() {
HashSet::new()
} else {
self.paths_using_as_member(name, &probed)?
};
let held_by_importer = importers.iter().any(|path| member_paths.contains(path));
for definition in &mut definitions {
definition.free &= !member_paths.contains(&definition.path);
definition.module_value &=
!held_by_importer && !member_paths.contains(&definition.path);
if !definition.callable {
definition.callable = match definition.language.as_str() {
"go" => self.holds_func(definition)?,
"rust" => self.holds_fn_alias(definition)?,
_ => false,
};
}
}
Ok(definitions)
}
fn paths_using_as_member(
&self,
name: &str,
paths: &HashSet<&str>,
) -> Result<HashSet<String>, QueryError> {
if !crate::queries::has_table(self.conn, "identifiers") {
return Ok(HashSet::new());
}
let normalized = |path: String| path.replace('\\', "/");
let mut found: HashSet<String> = self.with_statement(MEMBER_ACCESS_PATHS_SQL, |stmt| {
stmt.query_map(params![name], |r| r.get::<_, String>(0))?
.map(|path| path.map(normalized))
.collect()
})?;
let mut sites: HashMap<String, Vec<(usize, usize)>> = HashMap::new();
let rows = self.with_statement(VARIABLE_REF_SITES_SQL, |stmt| {
stmt.query_map(params![name], |r| {
Ok((
normalized(r.get::<_, String>(0)?),
r.get::<_, i64>(1)? as usize,
r.get::<_, i64>(2)? as usize,
))
})?
.collect::<Result<Vec<_>, _>>()
})?;
for (path, line, column) in rows {
if paths.contains(path.as_str()) && !found.contains(&path) {
sites.entry(path).or_default().push((line, column));
}
}
for (path, sites) in sites {
let Ok(text) = std::fs::read_to_string(self.root.join(&path)) else {
continue;
};
let lines: Vec<&str> = text.lines().collect();
if sites
.iter()
.any(|&(line, column)| is_object_value(&lines, line, column, name))
{
found.insert(path);
}
}
Ok(found)
}
fn holds_func(&self, definition: &Definition) -> Result<bool, QueryError> {
if definition.kind == "constant" {
return Ok(false);
}
let signature = definition.signature.trim_start();
let signature = ["var ", "const "]
.iter()
.find_map(|keyword| signature.strip_prefix(keyword))
.unwrap_or(signature);
let declared = signature
.trim_start()
.strip_prefix(definition.name.as_str())
.filter(|rest| !rest.starts_with(|c: char| c.is_alphanumeric() || c == '_'))
.map_or("", str::trim_start);
if declared.is_empty() || declared.starts_with('=') {
return Ok(true);
}
let type_name: String = declared
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if type_name.is_empty() {
return Ok(false);
}
if type_name == "func" || declared[type_name.len()..].starts_with('.') {
return Ok(true);
}
self.declares_function_type(&type_name, "go")
}
fn holds_fn_alias(&self, definition: &Definition) -> Result<bool, QueryError> {
let declared = definition
.signature
.split_once(definition.name.as_str())
.and_then(|(_, rest)| rest.trim_start().strip_prefix(':'))
.and_then(|declared| declared.split(['=', ';']).next())
.and_then(|declared| declared.rsplit("::").next())
.map(str::trim);
match declared {
Some(type_name) if is_plain_identifier(type_name) => {
self.declares_function_type(type_name, "rust")
}
_ => Ok(false),
}
}
fn declares_function_type(&self, type_name: &str, language: &str) -> Result<bool, QueryError> {
let signatures = self.with_statement(TYPE_DECLARATIONS_SQL, |stmt| {
stmt.query_map(params![type_name, language], |r| r.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()
})?;
Ok(signatures
.iter()
.any(|signature| is_function_type(signature, type_name, language)))
}
fn definition_count(&self, name: &str) -> Result<usize, QueryError> {
Ok(self.definitions(name)?.iter().filter(|d| !d.local).count())
}
fn account_spec(
&mut self,
spec: &Spec,
out: &mut Vec<SiteRow>,
cannot_see: &mut Vec<String>,
) -> Result<(), QueryError> {
let definitions = self.definitions(&spec.name)?;
let group_ids: HashSet<&str> = spec.group.iter().map(|s| s.symbol_id.as_str()).collect();
let target = &spec.group[0];
let mut rows = self.load_rows(&spec.name, &definitions, spec.scope)?;
pair_identifiers(&mut rows);
rows.retain(|row| !row.consumed);
mark_hidden_receivers(self.root, &mut rows);
self.mark_result_variables(&mut rows)?;
js_modules::complete_js_chains(self.root, &mut rows);
let mut order: Vec<usize> = (0..rows.len()).collect();
order.sort_by(|&a, &b| {
let (a, b) = (&rows[a], &rows[b]);
shared_leading_segments(&b.path, &target.path)
.cmp(&shared_leading_segments(&a.path, &target.path))
.then_with(|| a.path.cmp(&b.path))
.then_with(|| a.line.cmp(&b.line))
});
let (claims, judged) = self.claims(spec, &rows, &order, cannot_see)?;
let mut group = definitions
.iter()
.filter(|d| group_ids.contains(d.symbol_id.as_str()))
.peekable();
let module_target = group.peek().is_some() && group.all(|d| d.module_value);
let mut unchecked: Vec<usize> = order[judged..].to_vec();
let mut verdicts: HashMap<String, Verdict> = HashMap::new();
let mut undecided: Vec<usize> = Vec::new();
for (position, &index) in order[..judged].iter().enumerate() {
if self.out_of_time() {
unchecked.extend_from_slice(&order[position..judged]);
break;
}
let row = &rows[index];
match self.decide_cheap(row, spec, target, &group_ids, &claims)? {
Some(verdict) => {
verdicts.insert(row.key.clone(), verdict);
}
None if module_target
&& row.result_receiver
&& language_family(&row.language) == "js" =>
{
verdicts.insert(
row.key.clone(),
Verdict::new(
SiteLabel::Excluded,
format!(
"a call on a call result never reaches the top-level `{}`",
spec.name
),
false,
),
);
}
None => undecided.push(index),
}
}
let mut others: Vec<&Definition> = definitions
.iter()
.filter(|d| !d.local && !group_ids.contains(d.symbol_id.as_str()))
.filter(|d| !(spec.scope == RowScope::Inheritance && d.kind == "constructor"))
.collect();
others.sort_by_key(|d| {
(
std::cmp::Reverse(shared_leading_segments(&d.path, &target.path)),
d.path.clone(),
d.start_line,
)
});
let mut checked = 0;
for other in &others {
if undecided.is_empty() || checked >= OTHER_DEFINITION_CAP || self.out_of_time() {
break;
}
let mut kept = Vec::with_capacity(undecided.len());
let mut complete = true;
for chunk in undecided.chunks(JUDGE_CHUNK) {
if self.out_of_time() {
complete = false;
kept.extend_from_slice(chunk);
continue;
}
let chunk_rows: Vec<&NameRow> = chunk.iter().map(|&index| &rows[index]).collect();
let judged = self.judge(&spec.name, &other.symbol_id, &chunk_rows)?;
for &index in chunk {
let row = &rows[index];
if row.hidden_receiver
|| !row.is_in_scope_of(other)
|| row.is_type_position() && other.is_value()
|| row.language == "java" && !row.can_reach(other)
|| row.reads_a_module(&other.kind)
|| row.is_bare_use() && other.is_property_method()
{
kept.push(index);
continue;
}
let Some(Judged {
claimed: true,
basis: Some(basis),
}) = judged.get(&row.key)
else {
kept.push(index);
continue;
};
if basis == IMPORT_BASIS
&& row.language == "python"
&& !self.python_import_reaches(row, &spec.name, &other.path)?
{
kept.push(index);
continue;
}
verdicts.insert(
row.key.clone(),
Verdict::new(SiteLabel::Excluded, other.resolves_to(), false),
);
}
}
undecided = kept;
if !complete {
break;
}
checked += 1;
}
let unchecked_others = others.len() - checked;
if !undecided.is_empty() && unchecked_others > 0 {
cannot_see.push(if unchecked_others == 1 {
"1 other definition with this name was not checked".to_string()
} else {
format!("{unchecked_others} other definitions with this name were not checked")
});
}
let absent_definition = match self
.absent
.as_ref()
.filter(|absent| group_ids.contains(absent.symbol_id.as_str()))
{
Some(absent) => Some(self.absent_definition(absent)?),
None => None,
};
let group_definitions: Vec<&Definition> = definitions
.iter()
.filter(|d| group_ids.contains(d.symbol_id.as_str()))
.chain(absent_definition.iter())
.collect();
for index in undecided {
let row = &rows[index];
let rivals = self.rivals(row, &others)?;
let verdict = match claims.get(&row.key) {
Some(claim) => claimed_verdict(row, claim, rivals.len(), spec.builds_class),
None if self.out_of_time() => {
unchecked.push(index);
continue;
}
None => self.decide_late(row, &spec.group, &group_definitions, &rivals)?,
};
verdicts.insert(row.key.clone(), verdict);
}
for index in unchecked {
let rivals = self.rivals(&rows[index], &others)?.len();
let verdict = if rivals == 0 {
Verdict::new(
SiteLabel::Likely,
format!("name only; {NOT_CHECKED}"),
false,
)
} else {
Verdict::new(
SiteLabel::Unresolved,
format!("{} definitions; {NOT_CHECKED}", rivals + 1),
false,
)
};
verdicts.insert(rows[index].key.clone(), verdict);
}
for row in &rows {
let Some(mut verdict) = verdicts.remove(&row.key) else {
continue;
};
if matches!(verdict.label, SiteLabel::Likely | SiteLabel::Unresolved)
&& let Some(arity) = &spec.arity
&& let Some(count) = row.arg_count
&& let Some(basis) = arity.exclusion_basis(&spec.name, &row.language, count)
{
verdict = Verdict::new(SiteLabel::Excluded, basis, false);
}
if verdict.label == SiteLabel::Proven
&& NAME_RESOLVED_BASES.contains(&verdict.basis.as_str())
&& let Some(line) = self.js_binding_line(row, &spec.name)?
{
verdict.label = SiteLabel::Likely;
verdict.basis = format!(
"{}; line {line} binds the name as a callback parameter, loop variable, or catch parameter",
verdict.basis
);
}
out.push(site_row(row, verdict, spec));
}
if spec.scope == RowScope::All {
for member in &spec.group {
for grouped in
grouped_member_access_rows(self.conn, &member.symbol_id, RESOLVER_ROW_CAP)?
{
out.push(SiteRow {
end_line: None,
arg_count: None,
label: SiteLabel::Proven,
basis: "qualified path".to_string(),
claimed: true,
site: grouped,
});
}
}
}
Ok(())
}
fn inheritance_labels(
&mut self,
owner: &Symbol,
) -> Result<HashMap<String, (SiteLabel, String)>, QueryError> {
let spec = Spec {
name: owner.name.clone(),
group: vec![owner.clone()],
scope: RowScope::Inheritance,
builds_class: false,
arity: None,
local_to: None,
};
let mut rows = Vec::new();
self.account_spec(&spec, &mut rows, &mut Vec::new())?;
let mut labels: HashMap<String, (SiteLabel, String)> = HashMap::new();
for row in rows {
let better = labels
.get(&row.site.from_symbol_id)
.is_none_or(|(label, _)| row.label.rank() < label.rank());
if better {
labels.insert(row.site.from_symbol_id, (row.label, row.basis));
}
}
Ok(labels)
}
fn load_rows(
&self,
name: &str,
definitions: &[Definition],
scope: RowScope,
) -> Result<Vec<NameRow>, QueryError> {
let docs = documentation_language_list();
let inheritance = scope == RowScope::Inheritance;
let kinds = if inheritance {
"AND p.kind IN ('extends', 'implements')"
} else {
""
};
let mut rows = Vec::new();
let mut stmt = self.conn.prepare(&format!(
"SELECT p.rowid, p.from_symbol_id, COALESCE(s.name, ''), p.kind, p.path,
COALESCE(f.language, s.language, ''), p.start_line, p.start_column,
p.end_line, p.target_receiver, p.target_namespace_json, s.end_line
FROM pending_relationships p
LEFT JOIN symbols s ON s.symbol_id = p.from_symbol_id
LEFT JOIN files f ON f.file_id = p.file_id
WHERE p.target_terminal_name = ?1 AND p.kind != 'imports' {kinds}
AND COALESCE(f.language, s.language, '') NOT IN ({docs})"
))?;
let mut query = stmt.query(params![name])?;
while let Some(row) = query.next()? {
let namespace: Vec<String> = row
.get::<_, Option<String>>(10)?
.and_then(|json| serde_json::from_str(&json).ok())
.unwrap_or_default();
let receiver: Option<String> = row.get(9)?;
let chain = namespace
.iter()
.chain(receiver.as_ref().filter(|r| !r.is_empty()))
.cloned()
.collect();
rows.push(NameRow {
key: format!("P:{}", row.get::<_, i64>(0)?),
source: Source::Pending,
caller_id: row.get(1)?,
caller_name: row.get(2)?,
caller_end_line: row.get::<_, Option<i64>>(11)?.map(|v| v as usize),
path: row.get::<_, String>(4)?.replace('\\', "/"),
language: row.get(5)?,
kind: row.get(3)?,
line: row.get::<_, i64>(6)? as usize,
column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
end_line: row.get::<_, Option<i64>>(8)?.map(|v| v as usize),
receiver,
namespace,
to_symbol_id: None,
site_line: row.get::<_, i64>(6)? as usize,
site_column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
consumed: false,
paired: false,
hidden_receiver: false,
result_receiver: false,
chain: Some(chain),
arg_count: None,
links_enclosing_class: false,
});
}
if !inheritance {
let mut stmt = self.conn.prepare(&format!(
"SELECT r.rowid, r.from_symbol_id, COALESCE(s.name, ''), r.kind, r.path,
COALESCE(f.language, s.language, ''), r.start_line, r.start_column,
r.end_line, r.to_symbol_id, {LINKS_ENCLOSING_CLASS_SQL}, s.end_line
FROM relationships r
JOIN symbols t ON t.symbol_id = r.to_symbol_id
LEFT JOIN symbols s ON s.symbol_id = r.from_symbol_id
LEFT JOIN files f ON f.file_id = r.file_id
WHERE t.name = ?1 AND r.kind != 'imports'
AND COALESCE(f.language, s.language, '') NOT IN ({docs})"
))?;
let mut query = stmt.query(params![name])?;
while let Some(row) = query.next()? {
let line = row.get::<_, Option<i64>>(6)?.unwrap_or(0) as usize;
rows.push(NameRow {
key: format!("R:{}", row.get::<_, i64>(0)?),
source: Source::Relationship,
caller_id: row.get(1)?,
caller_name: row.get(2)?,
caller_end_line: row.get::<_, Option<i64>>(11)?.map(|v| v as usize),
path: row.get::<_, String>(4)?.replace('\\', "/"),
language: row.get(5)?,
kind: row.get(3)?,
line,
column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
end_line: row.get::<_, Option<i64>>(8)?.map(|v| v as usize),
receiver: None,
namespace: Vec::new(),
to_symbol_id: row.get(9)?,
site_line: line,
site_column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
consumed: false,
paired: false,
hidden_receiver: false,
result_receiver: false,
chain: None,
arg_count: None,
links_enclosing_class: row.get(10)?,
});
}
}
if !inheritance && crate::queries::has_table(self.conn, "identifiers") {
let mut stmt = self.conn.prepare(&format!(
"SELECT i.identifier_id, i.containing_symbol_id, COALESCE(s.name, ''), i.kind,
i.path, i.language, i.start_line, i.start_column,
CASE WHEN json_valid(i.metadata_json)
THEN json_extract(i.metadata_json, '$.receiver') END,
CASE WHEN json_valid(i.metadata_json)
THEN json_extract(i.metadata_json, '$.receiver_qualifier') END,
CASE WHEN json_valid(i.metadata_json)
THEN json_extract(i.metadata_json, '$.arg_count') END,
CASE WHEN json_valid(i.metadata_json)
THEN json_extract(i.metadata_json, '$.arg_spread') END IS NOT NULL
FROM identifiers i
LEFT JOIN symbols s ON s.symbol_id = i.containing_symbol_id
WHERE i.name = ?1 AND COALESCE(s.kind, '') != 'import'
AND i.language NOT IN ({docs})"
))?;
let mut query = stmt.query(params![name])?;
while let Some(row) = query.next()? {
let line = row.get::<_, i64>(6)? as usize;
let kind: String = row.get(3)?;
let receiver: Option<String> = row.get(8)?;
let qualifier: Option<String> = row.get(9)?;
let arg_count: Option<u32> = row.get(10)?;
let arg_spread: bool = row.get(11)?;
let written = receiver.as_deref().filter(|r| !r.is_empty());
let chain = match kind.as_str() {
"type_usage" => None,
"member_access" if written.is_none() => None,
_ => Some(
qualifier
.iter()
.flat_map(|q| q.split('.'))
.chain(written)
.map(str::to_string)
.collect(),
),
};
rows.push(NameRow {
key: format!("I:{}", row.get::<_, String>(0)?),
source: Source::Identifier,
caller_id: row.get(1)?,
caller_name: row.get(2)?,
caller_end_line: None,
path: row.get::<_, String>(4)?.replace('\\', "/"),
language: row.get(5)?,
kind,
line,
column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
end_line: None,
receiver,
namespace: Vec::new(),
to_symbol_id: None,
site_line: line,
site_column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
consumed: false,
paired: false,
hidden_receiver: false,
result_receiver: false,
chain,
arg_count: arg_count.filter(|_| !arg_spread),
links_enclosing_class: false,
});
}
}
let import_spans = self.import_spans(name)?;
let definition_spans = definition_spans(definitions, None);
rows.retain(|row| {
if scope == RowScope::Calls && !row.is_call() {
return false;
}
if in_spans(&import_spans, &row.path, row.line) {
return false;
}
!(row.source == Source::Identifier
&& !row.is_super_call()
&& in_spans(&definition_spans, &row.path, row.line))
});
Ok(rows)
}
fn import_spans(&self, name: &str) -> Result<HashMap<String, Vec<(usize, usize)>>, QueryError> {
let mut spans: HashMap<String, Vec<(usize, usize)>> = HashMap::new();
let mut add = |path: String, start: i64, end: Option<i64>| {
let start = start as usize;
let end = end.map_or(start, |e| (e as usize).max(start));
spans
.entry(path.replace('\\', "/"))
.or_default()
.push((start, end));
};
let docs = documentation_language_list();
let dotted = format!("\".{}\"", name.replace('"', "\"\""));
let sql = if crate::queries::has_table(self.conn, "symbol_names_tri")
&& name.chars().count() >= 2
{
format!(
"SELECT path, start_line, end_line FROM symbols
WHERE kind = 'import' AND language NOT IN ({docs}) AND name = ?1
UNION ALL
SELECT s.path, s.start_line, s.end_line FROM symbol_names_tri
CROSS JOIN symbols s ON s.rowid = symbol_names_tri.rowid
WHERE symbol_names_tri MATCH ?2 AND s.kind = 'import'
AND s.language NOT IN ({docs})
AND substr(s.name, -length(?1) - 1) = '.' || ?1"
)
} else {
format!(
"SELECT path, start_line, end_line FROM symbols
WHERE kind = 'import' AND language NOT IN ({docs})
AND (name = ?1 OR substr(name, -length(?1) - 1) = '.' || ?1)
AND ?2 = ?2"
)
};
let mut stmt = self.conn.prepare(&sql)?;
for row in stmt.query_map(params![name, dotted], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, i64>(1)?,
row.get::<_, Option<i64>>(2)?,
))
})? {
let (path, start, end) = row?;
add(path, start, end);
}
let mut stmt = self.conn.prepare(
"SELECT path, start_line, end_line FROM pending_relationships
WHERE target_terminal_name = ?1 AND kind = 'imports'",
)?;
for row in stmt.query_map(params![name], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, i64>(1)?,
row.get::<_, Option<i64>>(2)?,
))
})? {
let (path, start, end) = row?;
add(path, start, end);
}
let mut stmt = self.conn.prepare(
"SELECT r.path, r.start_line, r.end_line FROM relationships r
JOIN symbols t ON t.symbol_id = r.to_symbol_id
WHERE t.name = ?1 AND r.kind = 'imports' AND r.start_line IS NOT NULL",
)?;
for row in stmt.query_map(params![name], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, i64>(1)?,
row.get::<_, Option<i64>>(2)?,
))
})? {
let (path, start, end) = row?;
add(path, start, end);
}
let mut stmt = self.conn.prepare(
"SELECT path, start_line, end_line, COALESCE(signature, '') FROM symbols
WHERE name = ?1 AND kind = 'variable'
AND language IN ('javascript', 'typescript', 'tsx', 'jsx', 'vue')",
)?;
for row in stmt.query_map(params![name], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, i64>(1)?,
row.get::<_, Option<i64>>(2)?,
row.get::<_, String>(3)?,
))
})? {
let (path, start, end, signature) = row?;
if js_modules::dynamic_import_binding(&signature, name).is_some() {
add(path, start, end);
}
}
Ok(spans)
}
fn judge(
&self,
name: &str,
target_id: &str,
rows: &[&NameRow],
) -> Result<HashMap<String, Judged>, QueryError> {
let ids_of = |source: Source| -> String {
let ids: Vec<&str> = rows
.iter()
.filter(|row| row.source == source)
.filter_map(|row| row.key.split_once(':').map(|(_, id)| id))
.collect();
serde_json::to_string(&ids).unwrap_or_else(|_| "[]".to_string())
};
let (pending, identifiers) = (ids_of(Source::Pending), ids_of(Source::Identifier));
let absent = self
.absent
.as_ref()
.filter(|symbol| symbol.symbol_id == target_id);
let mut slot = if absent.is_some() {
self.absent_judge_stmt.borrow_mut()
} else {
self.judge_stmt.borrow_mut()
};
if slot.is_none() {
*slot = Some(self.conn.prepare(&judge_sql(self.conn, absent.is_some()))?);
}
let stmt = slot.as_mut().expect("judge statement was just prepared");
let mut rows = match absent {
Some(symbol) => stmt.query(params![
name,
target_id,
pending,
identifiers,
symbol.name,
symbol.kind,
symbol.language,
symbol.path,
symbol.start_line as i64,
symbol.parent_symbol_id
])?,
None => stmt.query(params![name, target_id, pending, identifiers])?,
};
let mut judged = HashMap::new();
while let Some(row) = rows.next()? {
let source: String = row.get(0)?;
let id: String = row.get(1)?;
judged.insert(
format!("{source}:{id}"),
Judged {
claimed: row.get::<_, i64>(2)? != 0,
basis: row.get(3)?,
},
);
}
Ok(judged)
}
fn claims(
&self,
spec: &Spec,
rows: &[NameRow],
order: &[usize],
cannot_see: &mut Vec<String>,
) -> Result<(HashMap<String, Claim>, usize), QueryError> {
let mut claims: HashMap<String, Claim> = HashMap::new();
let mut resolvers = Vec::with_capacity(spec.group.len());
for member in &spec.group {
let resolver_ids: HashSet<String> = if spec.scope == RowScope::Inheritance {
HashSet::new()
} else {
direct_identifier_rows(
self.conn,
&spec.name,
Some(&member.symbol_id),
RESOLVER_ROW_CAP,
)?
.into_iter()
.map(|row| format!("I:{}", row.identifier_id))
.collect()
};
if resolver_ids.len() >= RESOLVER_ROW_CAP {
cannot_see.push(format!(
"the resolver listed {RESOLVER_ROW_CAP} identifier rows and may hold more; rows past that limit have no resolver evidence"
));
}
resolvers.push(resolver_ids);
}
let mut judged_rows = 0;
for chunk in order.chunks(JUDGE_CHUNK) {
if Instant::now() >= self.judge_deadline {
break;
}
let chunk_rows: Vec<&NameRow> = chunk
.iter()
.map(|&index| &rows[index])
.filter(|row| !row.hidden_receiver)
.collect();
for (member, resolver_ids) in spec.group.iter().zip(&resolvers) {
let judged = self.judge(&spec.name, &member.symbol_id, &chunk_rows)?;
for row in chunk_rows.iter().copied() {
let judged_row = judged.get(&row.key);
let claimed = match row.source {
Source::Relationship => row
.to_symbol_id
.as_deref()
.is_some_and(|id| id == member.symbol_id),
Source::Identifier
if matches!(row.kind.as_str(), "type_usage" | "member_access") =>
{
resolver_ids.contains(&row.key)
}
_ => judged_row.is_some_and(|j| j.claimed),
};
if !claimed {
continue;
}
let basis = match row.source {
Source::Relationship if row.caller_id == row.to_symbol_id => None,
Source::Relationship => Some("same file".to_string()),
_ => judged_row.and_then(|j| j.basis.clone()),
};
let basis = match basis {
Some(basis)
if basis == IMPORT_BASIS
&& row.language == "python"
&& !self.python_import_reaches(
row,
&spec.name,
&member.path,
)? =>
{
None
}
basis => basis,
};
let entry = claims
.entry(row.key.clone())
.or_insert(Claim { basis: None });
if entry.basis.is_none() {
entry.basis = basis;
}
}
}
judged_rows += chunk.len();
}
Ok((claims, judged_rows))
}
fn decide_cheap(
&mut self,
row: &NameRow,
spec: &Spec,
target: &Symbol,
group_ids: &HashSet<&str>,
claims: &HashMap<String, Claim>,
) -> Result<Option<Verdict>, QueryError> {
if !languages_compatible(&row.language, &target.language) {
return Ok(Some(Verdict::new(
SiteLabel::Excluded,
"other language",
false,
)));
}
if spec
.group
.iter()
.all(|member| member.signature.as_deref().is_some_and(is_test_label))
{
return Ok(Some(Verdict::new(
SiteLabel::Excluded,
format!("`{}` is a test label, which code never names", spec.name),
false,
)));
}
if spec
.group
.iter()
.all(|member| row.reads_a_module(&member.kind))
{
return Ok(Some(Verdict::new(
SiteLabel::Excluded,
format!(
"`{}` is a module, which a read of a value never names",
spec.name
),
false,
)));
}
if let Some(outer) = &spec.local_to
&& !(row.path == outer.path.replace('\\', "/")
&& (outer.start_line..=outer.end_line).contains(&row.site_line))
{
return Ok(Some(Verdict::new(
SiteLabel::Excluded,
format!("`{}` is local to `{}`", spec.name, outer.name),
false,
)));
}
if row.language == "java"
&& row.kind == "member_access"
&& spec
.group
.iter()
.all(|member| member.language == "java" && member.kind == "method")
{
return Ok(Some(Verdict::new(
SiteLabel::Excluded,
format!(
"a member access reads a field, not the method `{}`",
spec.name
),
false,
)));
}
if let Some(shadow) = self.nearest_shadow(row, &spec.name, group_ids)? {
let dynamic = shadow.dynamic_import(&spec.name);
if let Some((source, imported)) = &dynamic
&& let Some(verdict) =
self.js_dynamic_import_verdict(row, spec, source, imported)?
{
return Ok(Some(verdict));
}
let follows_project_module = match &dynamic {
Some((source, _)) => !self.js_source_is_external(&row.path, source)?,
None => false,
};
if !follows_project_module {
let basis = match self.js_binding_line(row, &spec.name)? {
Some(line) if line > shadow.start_line || shadow.start_line > row.site_line => {
format!(
"line {line} binds the name as a callback parameter, loop variable, or catch parameter"
)
}
_ => shadow.resolves_to(),
};
return Ok(Some(Verdict::new(SiteLabel::Excluded, basis, false)));
}
}
if let Some(line) = self.js_enclosing_binding_line(row, &spec.name)? {
return Ok(Some(Verdict::new(
SiteLabel::Excluded,
format!(
"line {line} binds the name as a callback parameter, loop variable, or catch parameter"
),
false,
)));
}
if let Some(verdict) = self.java_static_import(row, spec)? {
return Ok(Some(verdict));
}
if let Some(verdict) = self.java_bare_call_outside_classes(row, spec)? {
return Ok(Some(verdict));
}
if let Some(verdict) = self.csharp_type_outside_namespaces(row, spec)? {
return Ok(Some(verdict));
}
if let Some(verdict) = self.csharp_conditional_fallback(row, &spec.name)? {
return Ok(Some(verdict));
}
if row.is_bare_use() && spec.group.iter().all(is_property_method) {
return Ok(Some(self.bare_use_of_property_method(row, &spec.name)?));
}
if let Some(verdict) = self.js_verdict(row, spec)? {
return Ok(Some(verdict));
}
if let Some(claim) = claims.get(&row.key)
&& let Some(basis) = &claim.basis
{
if basis == IMPORT_BASIS {
if language_family(&row.language) == "js"
&& let Some(verdict) = self.external_import(row, &spec.name)?
{
return Ok(Some(verdict));
}
let others = self.other_import_matches(row, &spec.name, group_ids)?;
if others > 0 {
return Ok(Some(Verdict::new(
SiteLabel::Unresolved,
format!("the import matches {} definitions", others + 1),
true,
)));
}
}
return Ok(Some(claimed_verdict(row, claim, 0, spec.builds_class)));
}
if self.crate_path_names_owner(row, &spec.group)? {
return Ok(Some(Verdict::new(
SiteLabel::Proven,
"qualified path",
false,
)));
}
if let Some(to) = row.to_symbol_id.as_deref()
&& !row.hidden_receiver
&& !group_ids.contains(to)
&& let Some(other) = self.definition_by_id(to)?
{
return Ok(Some(Verdict::new(
SiteLabel::Excluded,
other.resolves_to(),
false,
)));
}
self.external_import(row, &spec.name)
}
fn java_bare_call_outside_classes(
&self,
row: &NameRow,
spec: &Spec,
) -> Result<Option<Verdict>, QueryError> {
if row.language != "java" || !row.is_call() || !row.is_bare_use() {
return Ok(None);
}
let Some(caller) = row.caller_id.as_deref() else {
return Ok(None);
};
let mut owners = Vec::new();
for member in &spec.group {
let owner = (member.language == "java" && member.kind == "method")
.then(|| self.definition_by_id(&member.symbol_id))
.transpose()?
.flatten()
.and_then(|d| d.owner_name);
match owner {
Some(owner) => owners.push(owner),
None => return Ok(None),
}
}
let chain = self.with_statement(SYMBOL_CHAIN_SQL, |stmt| {
stmt.query_map(params![caller], |r| {
Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?))
})?
.collect::<Result<Vec<_>, _>>()
})?;
let is_class = |kind: &str| matches!(kind, "class" | "interface" | "enum" | "record");
if chain.windows(2).any(|pair| {
matches!(pair[0].0.as_str(), "method" | "constructor") && !is_class(&pair[1].0)
}) {
return Ok(None);
}
let enclosing: Vec<String> = chain
.into_iter()
.filter(|(kind, _)| is_class(kind))
.map(|(_, name)| name)
.collect();
let Some(innermost) = enclosing.first().cloned() else {
return Ok(None);
};
let mut reachable = HashSet::new();
for class in enclosing {
reachable.extend(self.ancestor_names(&class)?);
reachable.insert(class);
}
let imported_from = self.with_statement(JAVA_STATIC_IMPORTS_SQL, |stmt| {
stmt.query_map(params![row.path, spec.name], |r| r.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()
})?;
reachable.extend(imported_from.iter().filter_map(|signature| {
let imported = signature
.strip_prefix("import static ")?
.trim_end_matches(';');
let class_path = imported.trim().rsplit_once('.')?.0;
Some(class_path.rsplit('.').next()?.to_string())
}));
if owners.iter().any(|owner| reachable.contains(owner)) {
return Ok(None);
}
Ok(Some(Verdict::new(
SiteLabel::Excluded,
format!("a bare call reaches only the classes of `{innermost}` and its static imports"),
false,
)))
}
fn java_static_import(
&self,
row: &NameRow,
spec: &Spec,
) -> Result<Option<Verdict>, QueryError> {
if row.language != "java" || !row.is_call() || row.has_qualifier() {
return Ok(None);
}
let signatures = self.with_statement(JAVA_STATIC_IMPORT_SQL, |stmt| {
stmt.query_map(params![row.path, spec.name], |r| r.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()
})?;
let Some(class_path) = signatures.iter().find_map(|signature| {
let imported = signature
.strip_prefix("import static ")?
.trim_end_matches(';');
Some(imported.trim().rsplit_once('.')?.0.to_string())
}) else {
return Ok(None);
};
let class_file = format!("/{}.java", class_path.replace('.', "/"));
let names_target_class = spec
.group
.iter()
.any(|member| format!("/{}", member.path.replace('\\', "/")).ends_with(&class_file));
if names_target_class
&& self
.rows_at_path(CALLABLE_IN_FILE_SQL, &row.path, &[&spec.name], |r| {
r.get::<_, i64>(0)
})?
.is_empty()
{
return Ok(Some(Verdict::new(SiteLabel::Proven, IMPORT_BASIS, true)));
}
let class_name = class_path.rsplit('.').next().unwrap_or(&class_path);
let mut chain = HashSet::new();
if let Some(caller) = row.caller_id.as_deref()
&& let Some(class) = self.definition_by_id(caller)?.and_then(|d| d.owner_name)
{
chain = self.ancestor_names(&class)?;
chain.insert(class);
}
let present = spec.group.iter().filter(|member| {
self.absent
.as_ref()
.is_none_or(|absent| absent.symbol_id != member.symbol_id)
});
for member in present {
if member.path.replace('\\', "/") == row.path {
return Ok(None);
}
let owner = self
.definition_by_id(&member.symbol_id)?
.and_then(|d| d.owner_name);
if owner.is_some_and(|owner| owner == class_name || chain.contains(&owner)) {
return Ok(None);
}
}
Ok(Some(Verdict::new(
SiteLabel::Excluded,
format!("`{}` is imported from `{class_path}`", spec.name),
false,
)))
}
fn external_import(
&mut self,
row: &NameRow,
name: &str,
) -> Result<Option<Verdict>, QueryError> {
Ok(self.external_binding(row, name)?.map(|(binding, source)| {
Verdict::new(
SiteLabel::Excluded,
format!("receiver `{binding}` is imported from `{source}`"),
false,
)
}))
}
fn crate_path_names_owner(&self, row: &NameRow, group: &[Symbol]) -> Result<bool, QueryError> {
if row.language != "rust"
|| row.source != Source::Pending
|| row.kind != "calls"
|| row.namespace.len() < 2
{
return Ok(false);
}
let Some((owner_segment, leading)) = row.namespace.split_last() else {
return Ok(false);
};
if matches!(leading[0].as_str(), "crate" | "self" | "super" | "Self") {
return Ok(false);
}
for member in group.iter().filter(|member| member.language == "rust") {
let Some(owner) = self
.definition_by_id(&member.symbol_id)?
.and_then(|definition| definition.owner_name)
else {
continue;
};
let path = member.path.replace('\\', "/");
let Some((package, module_path)) = path.rsplit_once("/src/") else {
continue;
};
let package_name = package
.rsplit('/')
.next()
.unwrap_or(package)
.replace('-', "_");
let modules_match = leading[1..].iter().all(|segment| {
module_path
.split('/')
.any(|part| part == segment || part.strip_suffix(".rs") == Some(segment))
});
if owner != *owner_segment || leading[0] != package_name || !modules_match {
continue;
}
let in_package = format!("{}/%", crate::queries::escape_like(package));
let owners = self.with_statement(OWNER_IN_PACKAGE_SQL, |stmt| {
stmt.query_row(params![owner, in_package], |r| r.get::<_, i64>(0))
})?;
if owners == 1 {
return Ok(true);
}
}
Ok(false)
}
fn decide_late(
&mut self,
row: &NameRow,
group: &[Symbol],
group_definitions: &[&Definition],
rivals: &[&Definition],
) -> Result<Verdict, QueryError> {
if let Some(verdict) = self.outside_ancestors(row, group)? {
return Ok(verdict);
}
if let Some(receiver) = row.qualifier().filter(|_| row.namespace.len() <= 1)
&& let Some(type_name) = self.receiver_type(row, receiver)?
&& !self.owner_related(&type_name, group)?
{
return Ok(Verdict::new(
SiteLabel::Excluded,
format!("receiver `{receiver}` has type `{type_name}`"),
false,
));
}
if let Some(head) = self.foreign_type_head(row, group)? {
return Ok(Verdict::new(
SiteLabel::Excluded,
format!("receiver `{head}` has type `{head}`"),
false,
));
}
if let Some(object) = self.object_assigned_on(row, group_definitions, rivals)? {
return Ok(Verdict::new(
SiteLabel::Likely,
format!(
"receiver `{object}` names the object `{}` is assigned on",
group[0].name
),
false,
));
}
if rivals.is_empty() {
Ok(Verdict::new(SiteLabel::Likely, "name only", false))
} else {
Ok(Verdict::new(
SiteLabel::Unresolved,
format!("{} definitions", rivals.len() + 1),
false,
))
}
}
fn outside_ancestors(
&self,
row: &NameRow,
group: &[Symbol],
) -> Result<Option<Verdict>, QueryError> {
let (Some(caller), Some(receiver)) = (row.caller_id.as_deref(), row.receiver.as_deref())
else {
return Ok(None);
};
if !row.links_enclosing_class {
return Ok(None);
}
let Some(class) = self
.definition_by_id(caller)?
.and_then(|caller| caller.owner_name)
else {
return Ok(None);
};
let ancestors = self.ancestor_names(&class)?;
for member in group {
match self
.definition_by_id(&member.symbol_id)?
.and_then(|member| member.owner_name)
{
Some(owner) if !ancestors.contains(&owner) => {}
_ => return Ok(None),
}
}
Ok(Some(Verdict::new(
SiteLabel::Excluded,
format!("receiver `{receiver}` reaches only the ancestors of `{class}`"),
false,
)))
}
fn object_assigned_on(
&self,
row: &NameRow,
group: &[&Definition],
rivals: &[&Definition],
) -> Result<Option<String>, QueryError> {
let Some(receiver) = row
.receiver
.as_deref()
.filter(|r| is_plain_identifier(r) && !SELF_RECEIVERS.contains(r))
.filter(|_| matches!(row.kind.as_str(), "calls" | "call" | "member_access"))
else {
return Ok(None);
};
let assigned_on_receiver = |d: &&Definition| {
language_family(&d.language) == "js"
&& matches!(d.kind.as_str(), "method" | "function")
&& d.top_level
&& d.class_name.as_deref() == Some(receiver)
};
if !group.iter().all(assigned_on_receiver)
|| rivals
.iter()
.any(|d| d.class_name.as_deref() == Some(receiver))
{
return Ok(None);
}
let Some(target) = group.first() else {
return Ok(None);
};
let objects =
self.rows_at_path(TOP_LEVEL_VARIABLE_SQL, &target.path, &[&receiver], |row| {
row.get::<_, i64>(0)
})?;
Ok((!objects.is_empty()).then(|| receiver.to_string()))
}
fn definition_by_id(&self, symbol_id: &str) -> Result<Option<Definition>, QueryError> {
if let Some(absent) = self.absent.as_ref().filter(|s| s.symbol_id == symbol_id) {
return self.absent_definition(absent).map(Some);
}
self.with_statement(DEFINITION_BY_ID_SQL, |stmt| {
stmt.query_row(params![symbol_id], |row| {
let parent_kind: Option<String> = row.get(9)?;
let parent_name: Option<String> = row.get(8)?;
Ok(Definition {
symbol_id: row.get(0)?,
name: row.get(1)?,
path: row.get::<_, String>(4)?.replace('\\', "/"),
start_line: row.get::<_, i64>(5)? as usize,
decorated_start_line: None,
body_start_line: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
owner_name: parent_name.filter(|_| {
parent_kind
.as_deref()
.is_some_and(|k| CLASS_LIKE_KINDS.contains(&k))
}),
local: false,
scope: None,
kind: row.get(2)?,
language: row.get(3)?,
signature: row.get(10)?,
class_name: None,
top_level: row.get::<_, Option<String>>(7)?.is_none(),
callable: true,
free: false,
module_value: false,
})
})
.optional()
})
}
fn absent_definition(&self, symbol: &Symbol) -> Result<Definition, QueryError> {
let owner = match &symbol.parent_symbol_id {
Some(parent) => crate::queries::get_symbol_by_id(self.conn, parent)?,
None => None,
};
Ok(Definition {
symbol_id: symbol.symbol_id.clone(),
name: symbol.name.clone(),
path: symbol.path.replace('\\', "/"),
start_line: symbol.start_line,
decorated_start_line: None,
body_start_line: symbol.body_start_line,
owner_name: owner
.filter(|owner| CLASS_LIKE_KINDS.contains(&owner.kind.as_str()))
.map(|owner| owner.name),
local: false,
scope: None,
kind: symbol.kind.clone(),
language: symbol.language.clone(),
signature: String::new(),
class_name: assigned_object(symbol),
top_level: symbol.parent_symbol_id.is_none(),
callable: true,
free: false,
module_value: false,
})
}
fn nearest_shadow(
&self,
row: &NameRow,
name: &str,
group_ids: &HashSet<&str>,
) -> Result<Option<Definition>, QueryError> {
let type_use = row.is_bare_type_use();
if !row.is_bare_use() && !type_use {
return Ok(None);
}
let mut hit = None;
if let Some(caller) = row.caller_id.as_deref() {
hit = self.first_real_definition(
SHADOW_IN_CHAIN_SQL,
params![caller, name, row.path, type_use, row.site_line as i64],
row,
)?;
}
if hit.is_none() {
hit = self.first_real_definition(
SHADOW_TOP_LEVEL_SQL,
params![name, row.path, row.site_line as i64],
row,
)?;
}
match hit {
Some(id) if !group_ids.contains(id.as_str()) => self.definition_by_id(&id),
_ => Ok(None),
}
}
fn first_real_definition(
&self,
sql: &'static str,
query: impl rusqlite::Params,
row: &NameRow,
) -> Result<Option<String>, QueryError> {
let rows = self.with_statement(sql, |stmt| {
stmt.query_map(query, |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
))
})?
.collect::<Result<Vec<_>, _>>()
})?;
Ok(rows
.into_iter()
.find(|(_, signature, kind)| !is_test_label(signature) && !row.is_not_named_by(kind))
.map(|(id, ..)| id))
}
fn import_matches_of(
&mut self,
importer: &str,
receiver: &str,
name: &str,
) -> Result<&[ImportMatch], QueryError> {
let key = (importer.to_string(), receiver.to_string(), name.to_string());
if !self.import_matches.contains_key(&key) {
if self.import_match_stmt.is_none() {
self.import_match_stmt = Some(self.conn.prepare(&import_match_sql())?);
}
let stmt = self
.import_match_stmt
.as_mut()
.expect("import match statement was just prepared");
let matches = stmt
.query_map(params![importer, receiver, name], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
r.get::<_, String>(3)?,
))
})?
.filter(|m| !matches!(m, Ok((_, _, signature, _)) if is_test_label(signature)))
.map(|m| m.map(|(id, path, _, language)| (id, path, language)))
.collect::<Result<Vec<_>, _>>()?;
self.import_matches.insert(key.clone(), matches);
}
Ok(&self.import_matches[&key])
}
fn other_import_matches(
&mut self,
row: &NameRow,
name: &str,
group_ids: &HashSet<&str>,
) -> Result<usize, QueryError> {
let receiver = row.receiver.clone().unwrap_or_default();
let matches = self.import_matches_of(&row.path, &receiver, name)?.to_vec();
let mut count = 0;
for (symbol_id, path, _) in &matches {
if group_ids.contains(symbol_id.as_str()) {
continue;
}
if row.language == "python"
&& !self.python_import_reaches(row, name, &path.replace('\\', "/"))?
{
continue;
}
count += 1;
}
Ok(count)
}
fn imported_definitions(
&mut self,
importer: &str,
language: &str,
name: &str,
) -> Result<Vec<String>, QueryError> {
let matches = self.import_matches_of(importer, "", name)?.to_vec();
let mut reached = Vec::new();
for (symbol_id, path, definition_language) in matches {
if !languages_compatible(&definition_language, language) {
continue;
}
if language == "python"
&& !self.python_binding_reaches(importer, name, &path.replace('\\', "/"))?
{
continue;
}
reached.push(symbol_id);
}
Ok(reached)
}
fn bare_use_of_property_method(
&self,
row: &NameRow,
name: &str,
) -> Result<Verdict, QueryError> {
let sources = self.with_statement(IMPORT_SOURCES_SQL, |stmt| {
stmt.query_map(params![row.path, name], |r| r.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()
})?;
let basis = match sources.iter().find(|source| !source.is_empty()) {
Some(source) => format!("resolves to the import of `{name}` from `{source}`"),
None => "a bare name cannot reach a property method".to_string(),
};
Ok(Verdict::new(SiteLabel::Excluded, basis, false))
}
fn mark_result_variables(&self, rows: &mut [NameRow]) -> Result<(), QueryError> {
let mut texts: HashMap<String, Option<String>> = HashMap::new();
for row in rows.iter_mut() {
if row.result_receiver
|| row.hidden_receiver
|| language_family(&row.language) != "js"
|| !row.is_call()
|| !row.namespace.is_empty()
{
continue;
}
let (Some(receiver), Some(caller)) = (
row.receiver
.clone()
.filter(|r| is_plain_identifier(r) && !SELF_RECEIVERS.contains(&r.as_str())),
row.caller_id.as_deref(),
) else {
continue;
};
let declarations = self.with_statement(RECEIVER_DECLARATIONS_SQL, |stmt| {
stmt.query_map(params![caller, receiver, row.path], |r| {
Ok((r.get::<_, String>(1)?, r.get::<_, i64>(2)? as usize))
})?
.collect::<Result<Vec<_>, _>>()
})?;
let Some((kind, line)) = declarations
.into_iter()
.find(|(_, line)| *line <= row.site_line)
else {
continue;
};
if kind == "parameter" {
continue;
}
let text = texts
.entry(row.path.clone())
.or_insert_with(|| std::fs::read_to_string(self.root.join(&row.path)).ok());
row.result_receiver = text
.as_deref()
.and_then(|text| text.lines().nth(line.saturating_sub(1)))
.is_some_and(|declaration| const_holds_result(declaration, &receiver));
}
Ok(())
}
fn rivals<'d>(
&self,
row: &NameRow,
others: &[&'d Definition],
) -> Result<Vec<&'d Definition>, QueryError> {
let module_receiver = self.receiver_is_python_module(row)?;
let bare_csharp = row.language == "csharp" && !row.has_qualifier();
let mut rivals = Vec::new();
for &other in others {
if !row.can_reach(other) || module_receiver && other.owner_name.is_some() {
continue;
}
let csharp_type = other.language == "csharp"
&& (other.kind == "constructor"
|| other.owner_name.is_none()
&& CSHARP_TYPE_KINDS.contains(&other.kind.as_str()));
if bare_csharp && csharp_type && !self.csharp_type_in_scope(row, other)? {
continue;
}
rivals.push(other);
}
Ok(rivals)
}
fn csharp_type_outside_namespaces(
&self,
row: &NameRow,
spec: &Spec,
) -> Result<Option<Verdict>, QueryError> {
if row.language != "csharp" || row.has_qualifier() {
return Ok(None);
}
let mut namespace = None;
for member in &spec.group {
let Some(definition) = self.definition_by_id(&member.symbol_id)? else {
return Ok(None);
};
if definition.language != "csharp"
|| definition.owner_name.is_some()
|| !CSHARP_TYPE_KINDS.contains(&definition.kind.as_str())
|| self.csharp_type_in_scope(row, &definition)?
{
return Ok(None);
}
namespace = Some(self.csharp_namespace(&definition.symbol_id)?);
}
let usings = self.with_statement(CSHARP_USINGS_SQL, |stmt| {
stmt.query_map(params![row.path], |r| r.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()
})?;
let alias = format!("using {} =", spec.name);
if usings.iter().any(|using| using.contains(&alias)) {
return Ok(None);
}
Ok(namespace.map(|namespace| {
Verdict::new(
SiteLabel::Excluded,
format!("namespace `{namespace}` is not open here"),
false,
)
}))
}
fn csharp_conditional_fallback(
&self,
row: &NameRow,
name: &str,
) -> Result<Option<Verdict>, QueryError> {
if row.language != "csharp" || row.has_qualifier() {
return Ok(None);
}
let Some(condition) = self.target_condition.as_deref() else {
return Ok(None);
};
let negated: Vec<&str> = condition_terms(condition)
.filter_map(|term| term.strip_prefix('!'))
.map(str::trim)
.collect();
if negated.is_empty() {
return Ok(None);
}
let usings = self.with_statement(CSHARP_FILE_USINGS_SQL, |stmt| {
stmt.query_map(params![row.path], |r| {
Ok((r.get::<_, String>(0)?, r.get::<_, Option<String>>(1)?))
})?
.collect::<Result<Vec<_>, _>>()
})?;
let under_negated = |condition: &str| {
condition_terms(condition)
.find_map(|term| negated.iter().copied().find(|symbol| *symbol == term))
};
if usings
.iter()
.any(|(_, condition)| condition.as_deref().and_then(under_negated).is_some())
{
return Ok(None);
}
let paired = self.with_statement(CSHARP_CONDITIONAL_USINGS_SQL, |stmt| {
stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))?
.collect::<Result<Vec<_>, _>>()
})?;
Ok(usings
.iter()
.filter(|(_, condition)| condition.is_none())
.filter_map(|(signature, _)| csharp_using_namespace(signature))
.find_map(|namespace| {
let symbol = paired.iter().find_map(|(signature, condition)| {
(csharp_using_namespace(signature) == Some(namespace))
.then(|| under_negated(condition))
.flatten()
})?;
Some(Verdict::new(
SiteLabel::Unresolved,
format!(
"`{name}` is defined only without `{symbol}`, and this file always opens `{namespace}`, which other files open with it"
),
false,
))
}))
}
fn csharp_type_in_scope(&self, row: &NameRow, other: &Definition) -> Result<bool, QueryError> {
let namespace = self.csharp_namespace(&other.symbol_id)?;
let Some(caller) = row.caller_id.as_deref().filter(|_| !namespace.is_empty()) else {
return Ok(true);
};
if self.msbuild_usings()? {
return Ok(true);
}
let enclosing = self.csharp_namespace(caller)?;
if enclosing == namespace || enclosing.starts_with(&format!("{namespace}.")) {
return Ok(true);
}
let usings = self.with_statement(CSHARP_USINGS_SQL, |stmt| {
stmt.query_map(params![row.path], |r| r.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()
})?;
Ok(usings
.iter()
.any(|using| csharp_using_namespace(using) == Some(namespace.as_str())))
}
fn msbuild_usings(&self) -> Result<bool, QueryError> {
if let Some(known) = self.msbuild_usings.get() {
return Ok(*known);
}
let paths = self.with_statement(MSBUILD_FILES_SQL, |stmt| {
stmt.query_map([], |r| r.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()
})?;
let found = paths.iter().any(|path| {
std::fs::read_to_string(self.root.join(path))
.is_ok_and(|text| text.contains("<Using ") || text.contains("<Using\n"))
});
Ok(*self.msbuild_usings.get_or_init(|| found))
}
fn csharp_namespace(&self, symbol_id: &str) -> Result<String, QueryError> {
let start = match self.absent.as_ref().filter(|s| s.symbol_id == symbol_id) {
Some(absent) => match absent.parent_symbol_id.as_deref() {
Some(parent) => parent,
None => return Ok(String::new()),
},
None => symbol_id,
};
let chain = self.with_statement(SYMBOL_CHAIN_SQL, |stmt| {
stmt.query_map(params![start], |r| {
Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?))
})?
.collect::<Result<Vec<_>, _>>()
})?;
let mut parts: Vec<String> = chain
.into_iter()
.filter(|(kind, _)| kind == "namespace")
.map(|(_, name)| name)
.collect();
parts.reverse();
Ok(parts.join("."))
}
fn receiver_is_python_module(&self, row: &NameRow) -> Result<bool, QueryError> {
if row.language != "python" || row.hidden_receiver {
return Ok(false);
}
let chain: Vec<&str> = row
.namespace
.iter()
.chain(row.receiver.iter())
.flat_map(|part| part.split('.'))
.filter(|part| !part.is_empty())
.collect();
let Some((head, rest)) = chain.split_first() else {
return Ok(false);
};
let imports = self.with_statement(PYTHON_IMPORT_ROWS_SQL, |stmt| {
stmt.query_map(params![row.path, head], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
))
})?
.collect::<Result<Vec<_>, _>>()
})?;
let [(signature, source, imported)] = imports.as_slice() else {
return Ok(false);
};
if source.is_empty() || source.starts_with('.') {
return Ok(false);
}
let mut module: Vec<&str> = source.split('.').collect();
if signature.trim_start().starts_with("from ") {
module.push(imported);
}
module.extend(rest);
let module = module.join("/");
let package = format!(
"%/{}",
crate::queries::escape_like(&format!("{module}/__init__.py"))
);
let file = format!("%/{}", crate::queries::escape_like(&format!("{module}.py")));
self.with_statement(SOURCE_FILES_SQL, |stmt| stmt.exists(params![package, file]))
}
fn python_import_reaches(
&self,
row: &NameRow,
name: &str,
path: &str,
) -> Result<bool, QueryError> {
self.python_binding_reaches(&row.path, row.qualifier().unwrap_or(name), path)
}
fn python_binding_reaches(
&self,
importer: &str,
binding: &str,
path: &str,
) -> Result<bool, QueryError> {
let sources = self.with_statement(IMPORT_SOURCES_SQL, |stmt| {
stmt.query_map(params![importer, binding], |r| r.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()
})?;
if sources.is_empty() || sources.iter().any(|s| s.is_empty() || s.starts_with('.')) {
return Ok(true);
}
let reachable = self.python_import_heads(path)?;
Ok(sources.iter().any(|source| {
source
.split('.')
.next()
.is_some_and(|first| reachable.contains(&first))
}))
}
fn python_import_heads<'p>(&self, path: &'p str) -> Result<Vec<&'p str>, QueryError> {
let (folders, file) = match path.rsplit_once('/') {
Some((dir, file)) => (dir.split('/').collect::<Vec<_>>(), file),
None => (Vec::new(), path),
};
let mut top = folders.len();
while top > 0 {
let init = format!("{}/__init__.py", folders[..top].join("/"));
let is_package = self.with_statement(FILE_EXISTS_SQL, |stmt| {
stmt.exists(params![init, init.replace('/', "\\")])
})?;
if !is_package {
break;
}
top -= 1;
}
let stem = file.strip_suffix(".py").unwrap_or(file);
Ok(if top == folders.len() {
folders
.iter()
.copied()
.chain(std::iter::once(stem))
.collect()
} else {
folders[..=top].to_vec()
})
}
fn python_source_is_external(&mut self, source: &str) -> Result<bool, QueryError> {
let key = format!("python:{source}");
if let Some(known) = self.external_sources.get(&key) {
return Ok(*known);
}
let first = source.split('.').next().unwrap_or_default();
let external = if source.starts_with('.') || !is_plain_identifier(first) {
false
} else {
let paths = self.with_statement(PYTHON_FILES_NAMED_SQL, |stmt| {
stmt.query_map(
params![format!("%/{first}/%"), format!("%/{first}.py")],
|r| r.get::<_, String>(0),
)?
.collect::<Result<Vec<_>, _>>()
})?;
let mut reached = paths.len() >= PYTHON_FILES_NAMED_CAP;
for path in &paths {
if reached {
break;
}
reached = self.python_import_heads(path)?.contains(&first);
}
!reached
};
self.external_sources.insert(key, external);
Ok(external)
}
fn external_binding(
&mut self,
row: &NameRow,
name: &str,
) -> Result<Option<(String, String)>, QueryError> {
let javascript = language_family(&row.language) == "js";
let imported_head = match row.chain.as_ref() {
Some(chain) if javascript => {
let head = chain.first().map_or(name, String::as_str);
self.js_binds_import(&row.path, head)?.then_some(head)
}
_ => None,
};
let Some(qualifier) = imported_head.or_else(|| row.qualifier()) else {
return Ok(None);
};
if SELF_RECEIVERS.contains(&qualifier) || qualifier == "crate" {
return Ok(None);
}
if row.language == "rust" && EXTERNAL_RUST_ROOTS.contains(&qualifier) {
return Ok(Some((qualifier.to_string(), qualifier.to_string())));
}
let mut sources = self.with_statement(IMPORT_SOURCES_SQL, |stmt| {
stmt.query_map(params![row.path, qualifier], |r| r.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()
})?;
let has_import = !sources.is_empty();
sources.retain(|source| !source.is_empty());
let pending_modules = self.with_statement(PENDING_IMPORT_MODULES_SQL, |stmt| {
stmt.query_map(params![row.path, qualifier], |r| {
r.get::<_, Option<String>>(0)
})?
.collect::<Result<Vec<_>, _>>()
})?;
for modules in pending_modules.into_iter().flatten() {
if let Ok(parts) = serde_json::from_str::<Vec<String>>(&modules)
&& !parts.is_empty()
{
sources.push(parts.join("::"));
}
}
if sources.is_empty() {
return match has_import {
true if row.language == "java" => self.java_import_outside(row, qualifier),
true => Ok(None),
false => self.unindexed_path_head(row),
};
}
for source in &sources {
let external = if javascript {
self.js_source_is_external(&row.path, source)?
} else if row.language == "python" {
self.python_source_is_external(source)?
} else {
self.source_is_external(source)?
};
if !external {
return Ok(None);
}
}
Ok(Some((qualifier.to_string(), sources[0].clone())))
}
fn java_import_outside(
&self,
row: &NameRow,
qualifier: &str,
) -> Result<Option<(String, String)>, QueryError> {
let signatures = self.with_statement(IMPORT_SIGNATURES_SQL, |stmt| {
stmt.query_map(params![row.path, qualifier], |r| r.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()
})?;
let [signature] = signatures.as_slice() else {
return Ok(None);
};
let imported = signature
.trim()
.trim_start_matches("import ")
.trim()
.trim_end_matches(';')
.trim();
let package: Vec<&str> = imported
.split('.')
.take_while(|segment| segment.starts_with(|c: char| c.is_ascii_lowercase()))
.collect();
if imported.starts_with("static ") || package.is_empty() {
return Ok(None);
}
let folder = format!("%/{}/%", crate::queries::escape_like(&package.join("/")));
let indexed = self.with_statement(SOURCE_FILES_SQL, |stmt| {
stmt.exists(params![folder, folder])
})?;
Ok((!indexed).then(|| (qualifier.to_string(), imported.to_string())))
}
fn unindexed_path_head(
&mut self,
row: &NameRow,
) -> Result<Option<(String, String)>, QueryError> {
let Some(head) = row
.namespace
.first()
.filter(|head| is_plain_identifier(head) && !SELF_RECEIVERS.contains(&head.as_str()))
.filter(|head| *head != "crate" && head.chars().next().is_some_and(char::is_lowercase))
else {
return Ok(None);
};
let kinds = self.with_statement(TYPE_HEAD_KINDS_SQL, |stmt| {
stmt.query_map(params![head], |r| r.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()
})?;
if kinds.is_empty() && self.source_is_external(head)? {
return Ok(Some((head.clone(), head.clone())));
}
Ok(None)
}
fn source_is_external(&mut self, source: &str) -> Result<bool, QueryError> {
if let Some(known) = self.external_sources.get(source) {
return Ok(*known);
}
let runtime_library = !source.starts_with('.')
&& matches!(
source.split([':', '.', '/']).find(|part| !part.is_empty()),
Some("std" | "core" | "alloc")
);
let external = match import_segment(source).filter(|_| !runtime_library) {
None => false,
Some(segment) => {
let escaped = crate::queries::escape_like(&segment);
let (as_folder, as_file) = (format!("%/{escaped}/%"), format!("%/{escaped}.%"));
!self.with_statement(SOURCE_FILES_SQL, |stmt| {
stmt.exists(params![as_folder, as_file])
})?
}
};
let external = external || runtime_library;
self.external_sources.insert(source.to_string(), external);
Ok(external)
}
fn receiver_type(&self, row: &NameRow, receiver: &str) -> Result<Option<String>, QueryError> {
let Some(caller) = row.caller_id.as_deref() else {
return Ok(None);
};
if SELF_RECEIVERS.contains(&receiver) {
return Ok(None);
}
let types = self.with_statement(RECEIVER_TYPE_SQL, |stmt| {
stmt.query_map(params![caller, receiver, row.path], |r| {
r.get::<_, String>(0)
})?
.collect::<Result<Vec<_>, _>>()
})?;
let [only] = types.as_slice() else {
return Ok(None);
};
if self.holds_any_value(row, only)?
|| self.is_type_parameter(caller, &row.language, only)?
{
return Ok(None);
}
Ok(Some(only.clone()))
}
fn owner_related(&self, type_name: &str, group: &[Symbol]) -> Result<bool, QueryError> {
let mut owners: Vec<String> = group
.iter()
.filter_map(|member| member.signature.as_deref().and_then(extended_type))
.collect();
for member in group {
if let Some(definition) = self.definition_by_id(&member.symbol_id)? {
match definition.owner_name {
Some(owner) => owners.push(owner),
None => return Ok(true),
}
}
}
if owners.iter().any(|owner| owner == type_name) {
return Ok(true);
}
let type_ancestors = self.ancestor_names(type_name)?;
for owner in &owners {
if type_ancestors.contains(owner) || self.ancestor_names(owner)?.contains(type_name) {
return Ok(true);
}
}
Ok(false)
}
fn ancestor_names(&self, class_name: &str) -> Result<HashSet<String>, QueryError> {
let mut seen: HashSet<String> = HashSet::new();
let mut frontier = vec![class_name.to_string()];
for _ in 0..8 {
let mut next = Vec::new();
for name in frontier {
for sql in [ANCESTORS_BY_RELATIONSHIP_SQL, ANCESTORS_BY_PENDING_SQL] {
let bases = self.with_statement(sql, |stmt| {
stmt.query_map(params![name], |r| r.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()
})?;
for base in bases {
if seen.insert(base.clone()) {
next.push(base);
}
}
}
}
if next.is_empty() {
break;
}
frontier = next;
}
Ok(seen)
}
fn foreign_type_head(
&self,
row: &NameRow,
group: &[Symbol],
) -> Result<Option<String>, QueryError> {
let Some(head) = row
.namespace
.last()
.map(String::as_str)
.or(row.receiver.as_deref())
.filter(|head| is_plain_identifier(head) && !SELF_RECEIVERS.contains(head))
else {
return Ok(None);
};
if !head.chars().next().is_some_and(char::is_uppercase) {
return Ok(None);
}
let kinds = self.with_statement(TYPE_HEAD_KINDS_SQL, |stmt| {
stmt.query_map(params![head], |r| r.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()
})?;
let foreign = if kinds.is_empty() {
true
} else {
let type_like = kinds.iter().any(|k| CLASS_LIKE_KINDS.contains(&k.as_str()));
let aliased = kinds
.iter()
.any(|k| matches!(k.as_str(), "type" | "type_alias" | "typedef"));
type_like && !aliased && !self.owner_related(head, group)?
};
Ok(foreign.then(|| head.to_string()))
}
fn text_outcome(
&self,
root: &Path,
name: &str,
sites: &[SiteRowOut],
) -> Result<TextOutcome, QueryError> {
let hits = scan_word(root, name, TEXT_SCAN_BUDGET);
let site_spans: HashMap<String, Vec<(usize, usize)>> = {
let mut spans: HashMap<String, Vec<(usize, usize)>> = HashMap::new();
for site in sites {
let start = site.site.start_line.unwrap_or(0);
spans
.entry(site.site.path.clone())
.or_default()
.push((start, site.end_line.unwrap_or(start).max(start)));
}
spans
};
let definition_spans = definition_spans(&self.definitions(name)?, Some(&hits.hits));
let import_spans = self.import_spans(name)?;
let mut counts = ReferenceCounts::default();
let mut text_only: Vec<TextOnlyLines> = Vec::new();
for (path, line) in &hits.hits {
counts.text_lines += 1;
if in_spans(&site_spans, path, *line) {
counts.site_lines += 1;
} else if in_spans(&definition_spans, path, *line) {
counts.definition_lines += 1;
} else if in_spans(&import_spans, path, *line) {
counts.import_lines += 1;
} else {
counts.text_only_lines += 1;
match text_only.last_mut() {
Some(last) if last.path == *path => last.lines.push(*line),
_ => text_only.push(TextOnlyLines {
path: path.clone(),
lines: vec![*line],
}),
}
}
}
Ok(TextOutcome {
counts,
text_only,
summary: TextScanSummary {
files_scanned: hits.files_scanned,
files_total: hits.files_total,
complete: hits.complete,
elapsed_ms: hits.elapsed.as_millis() as u64,
skipped: None,
},
})
}
}
type SiteRowOut = LabeledSite;
fn claimed_verdict(row: &NameRow, claim: &Claim, rivals: usize, builds_class: bool) -> Verdict {
if let Some(basis) = &claim.basis {
if builds_class {
return Verdict::new(SiteLabel::Proven, "builds the class", true);
}
if row.kind == "variable_ref"
&& language_family(&row.language) == "js"
&& matches!(basis.as_str(), "same file" | "enclosing scope")
{
return Verdict::new(
SiteLabel::Likely,
format!("{basis}; the index does not record callback parameters"),
true,
);
}
return Verdict::new(SiteLabel::Proven, basis, true);
}
if rivals > 0 {
return Verdict::new(
SiteLabel::Unresolved,
format!("{} definitions", rivals + 1),
true,
);
}
if row.has_qualifier() {
return Verdict::new(SiteLabel::Likely, "receiver not typed", true);
}
Verdict::new(SiteLabel::Likely, "name only", true)
}
fn site_row(row: &NameRow, verdict: Verdict, spec: &Spec) -> SiteRow {
let end_line = row.end_line.filter(|end| !row.paired && *end > row.line);
SiteRow {
site: ReferenceSite {
from_symbol_name: row.caller_name.clone(),
from_symbol_id: row.caller_id.clone().unwrap_or_default(),
to_symbol_name: spec.name.clone(),
kind: row.display_kind(),
path: row.path.clone(),
start_line: Some(row.site_line),
start_column: row.site_column,
occurrences: None,
target: None,
enclosing_symbol_name: None,
},
label: verdict.label,
basis: verdict.basis,
claimed: verdict.claimed,
end_line,
arg_count: row.arg_count,
}
}
fn merge_site_rows(rows: Vec<SiteRow>) -> Vec<LabeledSite> {
let mut merged: HashMap<(String, usize), SiteRow> = HashMap::new();
let mut order: Vec<(String, usize)> = Vec::new();
for row in rows {
let key = (row.site.path.clone(), row.site.start_line.unwrap_or(0));
match merged.get_mut(&key) {
None => {
order.push(key.clone());
merged.insert(key, row);
}
Some(kept) => {
let end = kept.end_line.max(row.end_line);
let better = (row.label.rank(), !row.claimed, row.site.start_column)
< (kept.label.rank(), !kept.claimed, kept.site.start_column);
if better {
*kept = row;
}
kept.end_line = end;
}
}
}
order
.into_iter()
.filter_map(|key| merged.remove(&key))
.map(|row| LabeledSite {
site: row.site,
label: row.label,
basis: row.basis,
end_line: row.end_line,
arg_count: row.arg_count,
})
.collect()
}
fn mark_hidden_receivers(root: &Path, rows: &mut [NameRow]) {
let mut by_path: HashMap<String, Vec<usize>> = HashMap::new();
for (index, row) in rows.iter().enumerate() {
if row.is_call() && !row.has_qualifier() && row.language != "php" {
by_path.entry(row.path.clone()).or_default().push(index);
}
}
for (path, indices) in by_path {
let Ok(text) = std::fs::read_to_string(root.join(&path)) else {
continue;
};
let lines: Vec<&str> = text.lines().collect();
for index in indices {
let row = &mut rows[index];
let Some((before, _)) = row
.site_column
.and_then(|column| split_line(&lines, row.site_line, column))
else {
continue;
};
let before = before.trim_end();
let starts_line = matches!(before.trim_start(), "." | "?.")
|| before.is_empty() && continues_chain(&lines, row.site_line);
row.hidden_receiver = starts_line || awaits_member(before);
row.result_receiver = if starts_line {
code_line_above(&lines, row.site_line).is_some_and(|above| {
ends_with_result(above.trim_end_matches('.')) && !loads_module(above)
})
} else {
awaits_member(before) && !loads_module(before)
};
}
}
}
fn awaits_member(text: &str) -> bool {
text.strip_suffix('.').is_some_and(ends_with_result)
}
fn ends_with_result(text: &str) -> bool {
text.trim_end_matches(['?', '!'])
.ends_with([')', ']', '\'', '"', '`'])
}
fn loads_module(text: &str) -> bool {
text.contains("require(") || text.contains("import(")
}
fn const_holds_result(line: &str, name: &str) -> bool {
let Some(at) = line.find(&format!("const {name}")) else {
return false;
};
let rest = line[at + "const ".len() + name.len()..].trim_start();
let initializer = match rest.strip_prefix(':') {
Some(typed) => typed.split_once('=').map(|(_, value)| value),
None => rest.strip_prefix('='),
};
let Some(initializer) = initializer.map(str::trim_start) else {
return false;
};
if initializer.starts_with(['=', '>']) || loads_module(initializer) {
return false;
}
let initializer = initializer.strip_prefix("await ").unwrap_or(initializer);
let initializer = initializer.strip_prefix("new ").unwrap_or(initializer);
let head = initializer
.find(|c: char| !(c.is_alphanumeric() || matches!(c, '_' | '$' | '.' | '?')))
.unwrap_or(initializer.len());
head > 0 && initializer[head..].trim_start().starts_with(['(', '<'])
}
fn continues_chain(lines: &[&str], line: usize) -> bool {
code_line_above(lines, line).is_some_and(awaits_member)
}
fn code_line_above<'l>(lines: &[&'l str], line: usize) -> Option<&'l str> {
lines
.iter()
.take(line.saturating_sub(1))
.rev()
.map(|text| text.trim())
.find(|text| !text.is_empty())
.filter(|text| {
!["//", "/*", "*", "#"]
.iter()
.any(|start| text.starts_with(start))
})
}
fn pair_identifiers(rows: &mut [NameRow]) {
let mut by_caller: HashMap<String, (Vec<usize>, Vec<usize>)> = HashMap::new();
for (index, row) in rows.iter().enumerate() {
let Some(caller) = row.caller_id.clone() else {
continue;
};
let entry = by_caller.entry(caller).or_default();
match row.source {
Source::Pending | Source::Relationship if row.kind == "calls" => entry.0.push(index),
Source::Identifier if row.kind == "call" => entry.1.push(index),
_ => {}
}
}
for (calls, identifiers) in by_caller.values_mut() {
calls.sort_by_key(|&i| (rows[i].line, rows[i].column, rows[i].source.tag(), i));
identifiers.sort_by_key(|&i| (rows[i].line, rows[i].column, i));
let mut used = vec![false; identifiers.len()];
for &call in calls.iter() {
let line = rows[call].line;
let last = rows[call].caller_end_line.unwrap_or(usize::MAX).max(line);
let Some(slot) = identifiers
.iter()
.enumerate()
.position(|(slot, &i)| !used[slot] && (line..=last).contains(&rows[i].line))
else {
continue;
};
used[slot] = true;
let identifier = identifiers[slot];
let (site_line, site_column) = (rows[identifier].line, rows[identifier].column);
let alone_on_line = identifiers
.iter()
.filter(|&&other| rows[other].line == site_line)
.count()
== 1;
if alone_on_line && rows[call].links_enclosing_class && rows[identifier].is_super_call()
{
rows[call].consumed = true;
rows[identifier].links_enclosing_class = true;
continue;
}
rows[call].arg_count = rows[identifier].arg_count.filter(|_| alone_on_line);
rows[identifier].consumed = true;
rows[call].paired = true;
rows[call].site_line = site_line;
rows[call].site_column = site_column;
}
}
attach_counts_by_line(rows);
}
fn attach_counts_by_line(rows: &mut [NameRow]) {
let mut identifiers: HashMap<(String, usize), Vec<Option<u32>>> = HashMap::new();
for row in rows.iter() {
if row.source == Source::Identifier && row.kind == "call" {
identifiers
.entry((row.path.clone(), row.line))
.or_default()
.push(row.arg_count);
}
}
for row in rows.iter_mut() {
if row.source != Source::Identifier
&& row.is_call()
&& row.arg_count.is_none()
&& let Some(counts) = identifiers.get(&(row.path.clone(), row.line))
&& let [count] = counts.as_slice()
{
row.arg_count = *count;
}
}
}
const BODY_IS_FIRST_STATEMENT: &[&str] = &["python", "ruby", "lua", "fsharp", "vbnet"];
fn definition_spans(
definitions: &[Definition],
name_lines: Option<&[(String, usize)]>,
) -> HashMap<String, Vec<(usize, usize)>> {
let mut spans: HashMap<String, Vec<(usize, usize)>> = HashMap::new();
for definition in definitions {
let start = definition
.decorated_start_line
.unwrap_or(definition.start_line);
let names_a_line_in = |first: usize, last: usize| {
name_lines.is_none_or(|lines| {
let from = lines.partition_point(|(path, line)| {
(path.as_str(), *line) < (definition.path.as_str(), first)
});
lines
.get(from)
.is_some_and(|(path, line)| *path == definition.path && *line <= last)
})
};
let end = definition
.body_start_line
.map_or(definition.start_line, |body| {
if BODY_IS_FIRST_STATEMENT.contains(&definition.language.as_str())
&& names_a_line_in(start, body.saturating_sub(1))
{
body.saturating_sub(1)
} else {
body
}
})
.max(definition.start_line);
spans
.entry(definition.path.clone())
.or_default()
.push((start, end));
}
spans
}
fn in_spans(spans: &HashMap<String, Vec<(usize, usize)>>, path: &str, line: usize) -> bool {
spans.get(path).is_some_and(|list| {
list.iter()
.any(|&(start, end)| start <= line && line <= end)
})
}
fn shared_leading_segments(a: &str, b: &str) -> usize {
a.split('/')
.zip(b.split('/'))
.take_while(|(x, y)| x == y)
.count()
}
fn language_family(language: &str) -> &str {
match language {
"javascript" | "typescript" | "tsx" | "jsx" | "vue" => "js",
"java" | "kotlin" | "scala" | "groovy" => "jvm",
"c" | "cpp" | "objc" | "objcpp" | "qml" => "c",
"csharp" | "razor" | "vbnet" | "fsharp" => "dotnet",
other => other,
}
}
fn languages_compatible(row: &str, target: &str) -> bool {
row.is_empty() || language_family(row) == language_family(target)
}
fn import_segment(source: &str) -> Option<String> {
let source = source.trim();
let first_segment = source.split([':', '.']).next().unwrap_or("");
if source.is_empty()
|| source.starts_with('.')
|| ["crate", "self", "super"].contains(&first_segment)
{
return None;
}
let normalized = source
.replace("::", ".")
.replace('/', ".")
.replace('-', "_");
normalized
.rsplit('.')
.find(|segment| !segment.is_empty())
.map(str::to_string)
}
fn judge_sql(conn: &Connection, absent_target: bool) -> String {
let target = "s_target";
let parent = "s_target_parent";
let target_join = if absent_target {
format!(
"JOIN (SELECT ?2 AS symbol_id, ?5 AS name, ?6 AS kind, ?7 AS language, ?8 AS path,
?9 AS start_line, ?10 AS parent_symbol_id) {target}"
)
} else {
format!("JOIN symbols {target} ON {target}.symbol_id = ?2")
};
let predicate = pending_target_predicate(conn, target, parent);
let evidence = evidence_sql(conn, target, parent);
format!(
"SELECT p.src, p.row_id,
CASE WHEN p.src = 'I' AND p.kind IN ('type_usage', 'member_access')
THEN COALESCE(({predicate}), 0) ELSE 1 END AS claimed,
{evidence} AS basis
FROM (
SELECT 'P' AS src, CAST(pr.rowid AS TEXT) AS row_id,
pr.from_symbol_id AS from_symbol_id, pr.kind AS kind, pr.path AS path,
pr.start_line AS start_line, pr.start_column AS start_column,
pr.target_receiver AS target_receiver,
pr.target_namespace_json AS target_namespace_json,
COALESCE((SELECT language FROM files WHERE file_id = pr.file_id), '') AS language,
NULL AS receiver_qualifier
FROM pending_relationships pr
WHERE pr.target_terminal_name = ?1 AND pr.kind != 'imports'
AND CAST(pr.rowid AS TEXT) IN (SELECT value FROM json_each(?3))
UNION ALL
SELECT 'I', i.identifier_id, i.containing_symbol_id, i.kind, i.path, i.start_line,
i.start_column,
CASE WHEN json_valid(i.metadata_json)
THEN json_extract(i.metadata_json, '$.receiver') END,
'[]', i.language,
CASE WHEN json_valid(i.metadata_json)
THEN json_extract(i.metadata_json, '$.receiver_qualifier') END
FROM identifiers i
WHERE i.name = ?1 AND i.identifier_id IN (SELECT value FROM json_each(?4))
) p
LEFT JOIN symbols s_from ON s_from.symbol_id = p.from_symbol_id
{target_join}
LEFT JOIN symbols {parent} ON {parent}.symbol_id = {target}.parent_symbol_id
WHERE {predicate}
OR (p.src = 'I' AND p.kind IN ('type_usage', 'member_access'))"
)
}
fn import_match_sql() -> String {
let receiver = module_import_binding_sql("t", "p.target_receiver");
let bare = module_import_binding_sql("t", "t.name");
format!(
"SELECT t.symbol_id, t.path, COALESCE(t.signature, ''), t.language
FROM (SELECT ?1 AS path, ?2 AS target_receiver) p
JOIN symbols t ON t.name = ?3
WHERE t.parent_symbol_id IS NULL AND t.kind NOT IN ('import', 'export')
AND t.language NOT IN ({docs})
AND CASE WHEN p.target_receiver != '' THEN {receiver} ELSE {bare} END",
docs = documentation_language_list()
)
}
fn evidence_sql(conn: &Connection, target: &str, parent: &str) -> String {
let ns = "json_each(CASE WHEN json_valid(p.target_namespace_json) THEN p.target_namespace_json ELSE '[]' END)";
let has_namespace = "COALESCE(p.target_namespace_json, '[]') NOT IN ('', '[]')";
let has_receiver = "COALESCE(p.target_receiver, '') != ''";
let self_receiver = "p.target_receiver IN ('self', 'this', 'cls', 'Self', 'super')";
let names_self = format!("EXISTS (SELECT 1 FROM {ns} WHERE value = 'Self')");
let receiver_chain = class_chain_sql(target, "p.target_receiver IS NOT 'super'");
let inherited = class_chain_sql(target, &format!("{target}.kind IN ('variable', 'field')"));
let enclosing = enclosing_scope_sql(target);
let implicit_languages = implicit_receiver_language_list();
let module_receiver = module_import_binding_sql(target, "p.target_receiver");
let module_bare = module_import_binding_sql(target, &format!("{target}.name"));
let receiver_type = receiver_type_sql(conn, parent);
let qualifier_reaches_owner = module_import_binding_sql(parent, "p.receiver_qualifier");
let foreign_qualifier = format!(
"(p.receiver_qualifier IS NOT NULL
AND EXISTS (SELECT 1 FROM symbols q WHERE q.kind = 'import' AND q.path = p.path
AND q.name = p.receiver_qualifier)
AND NOT {qualifier_reaches_owner})"
);
let package_proximity = path_proximity(&format!("{target}.path"), "p.path");
let built = built_receiver_sql(conn, target)
.map(|built| {
format!(
"WHEN {has_receiver} AND {target}.parent_symbol_id IS NOT NULL AND {built}
THEN 'builds the class'"
)
})
.unwrap_or_default();
format!(
"CASE
WHEN {has_namespace} AND {parent}.name IS NOT NULL
AND EXISTS (SELECT 1 FROM {ns} WHERE value = {parent}.name)
THEN 'qualified path'
WHEN {has_namespace} AND {target}.parent_symbol_id IS NULL THEN 'qualified path'
WHEN {target}.parent_symbol_id IS NOT NULL AND {names_self}
AND s_from.parent_symbol_id = {target}.parent_symbol_id THEN 'class chain'
WHEN {target}.parent_symbol_id IS NOT NULL
AND ({self_receiver} OR {names_self}) AND {receiver_chain} THEN 'class chain'
WHEN {has_receiver} AND {parent}.name = p.target_receiver AND NOT {foreign_qualifier}
THEN 'qualified path'
WHEN {has_receiver} AND {target}.parent_symbol_id IS NULL AND {module_receiver}
THEN 'import'
WHEN {has_receiver} AND {target}.parent_symbol_id IS NOT NULL AND {receiver_type}
THEN 'receiver type'
{built}
WHEN NOT ({has_receiver}) AND NOT ({has_namespace}) THEN
CASE
WHEN {target}.parent_symbol_id IS NULL AND {target}.path = p.path
THEN 'same file'
WHEN {target}.parent_symbol_id = p.from_symbol_id THEN 'enclosing scope'
WHEN {target}.parent_symbol_id IS NOT NULL AND {enclosing}
THEN 'enclosing scope'
WHEN {target}.parent_symbol_id IS NOT NULL AND p.language IN ({implicit_languages})
AND (s_from.parent_symbol_id = {target}.parent_symbol_id OR {inherited})
THEN 'class chain'
WHEN {target}.parent_symbol_id IS NULL AND {module_bare} THEN 'import'
WHEN {target}.parent_symbol_id IS NULL AND {target}.language IN ('go', 'java')
AND {target}.language = p.language AND ({package_proximity}) >= 1
THEN 'same package'
END
END"
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
Workspace, find_julie_extract_binary, open_read_only, safe_tempdir, scan_workspace,
};
fn report_past_the_time_limit(
files: &[(&str, &str)],
name: &str,
path: Option<&str>,
) -> ReferenceReport {
find_julie_extract_binary().expect("julie-extract binary must be present for tests");
let temp = safe_tempdir();
let root = temp.path();
for (relative, text) in files {
let full = root.join(relative);
std::fs::create_dir_all(full.parent().unwrap()).unwrap();
std::fs::write(full, text).unwrap();
}
let db = root.join(".code-kb").join("artifact.db");
scan_workspace(&Workspace::new(root.to_path_buf()), &db, true).unwrap();
let conn = open_read_only(&db).unwrap();
let target = select_reference_target(&conn, name, path).unwrap();
report_within(&conn, root, &target, Duration::ZERO).unwrap()
}
#[test]
fn sites_past_the_time_limit_keep_the_label_their_name_gives() {
let report = report_past_the_time_limit(
&[(
"a.py",
"def helper():\n return 1\n\n\ndef use():\n return helper()\n",
)],
"helper",
None,
);
let labels: Vec<(SiteLabel, &str)> = report
.sites
.iter()
.map(|site| (site.label, site.basis.as_str()))
.collect();
assert_eq!(
labels,
[(
SiteLabel::Likely,
"name only; not checked within the time limit"
)]
);
assert!(
report
.cannot_see
.iter()
.any(|note| note == "1 of 1 sites were not checked within the 0 s time limit"),
"{:?}",
report.cannot_see
);
}
#[test]
fn sites_past_the_time_limit_count_only_the_definitions_that_can_reach_them() {
let report = report_past_the_time_limit(
&[
(
"a/name.go",
"package a\n\nfunc Name() string {\n\treturn \"a\"\n}\n",
),
(
"b/cmd.go",
"package b\n\ntype Command struct {\n\tName string\n}\n",
),
(
"c/use.go",
"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",
),
],
"Name",
Some("a/name.go"),
);
let labels: Vec<(usize, SiteLabel, &str)> = report
.sites
.iter()
.map(|site| {
(
site.site.start_line.unwrap_or(0),
site.label,
site.basis.as_str(),
)
})
.collect();
assert_eq!(
labels,
[
(
4,
SiteLabel::Likely,
"name only; not checked within the time limit"
),
(
8,
SiteLabel::Unresolved,
"2 definitions; not checked within the time limit"
),
]
);
}
}