use anyhow::Result;
const PARSE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2);
use std::path::Path;
use tree_sitter::{Language, Parser, Query, QueryCursor, StreamingIterator};
pub const KINDS: [&str; 6] = [
"defines",
"calls",
"imports",
"references",
"contains",
"aliases",
];
pub const EXTRACTED: &str = "extracted";
pub const INFERRED: &str = "inferred";
pub const RECEIVER: &str = ".";
pub const RESOLVED: &str = "resolved";
pub const AMBIGUOUS: &str = "ambiguous";
pub const CONFIDENCES: [&str; 4] = [EXTRACTED, RESOLVED, INFERRED, AMBIGUOUS];
pub fn confidence_rank(confidence: &str) -> usize {
CONFIDENCES
.iter()
.position(|c| *c == confidence)
.unwrap_or(CONFIDENCES.len())
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Symbol {
pub kind: String,
pub name: String,
pub qualified: String,
pub start_line: u32,
pub end_line: u32,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Edge {
pub from: String,
pub to: String,
pub kind: String,
pub confidence: String,
pub hint: Option<String>,
pub line: Option<u32>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub from_module: bool,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct Extraction {
pub symbols: Vec<Symbol>,
pub edges: Vec<Edge>,
}
pub const WITHOUT_GRAMMAR: &[(&str, &str)] = &[];
pub fn has_graph(lang: &str) -> bool {
grammar(lang).is_some()
}
pub fn languages() -> Vec<&'static str> {
crate::filter::LANGUAGES
.iter()
.map(|entry| entry.name)
.filter(|name| has_graph(name))
.collect()
}
pub fn language_of(path: &Path) -> Option<&'static str> {
let entry = crate::filter::language_of_path(&path.to_string_lossy())?;
has_graph(entry.name).then_some(entry.name)
}
fn supplement(lang: &str) -> &'static str {
match lang {
"rust" => {
r#"
(const_item name: (identifier) @name) @definition.constant
(static_item name: (identifier) @name) @definition.constant
(use_declaration argument: (_) @reference.import)
; `use store::Semlith as Store;` — the alias is a symbol of its own
; and the path it stands for is what it reaches. Without the edge a
; walk stops at `Store`, which is defined nowhere.
(use_as_clause
path: (_) @reference.alias
alias: (_) @name) @definition.alias
; A scoped call names the function and the thing it went through.
; Only the function earns an edge: `store::edges_out` is a call to
; `edges_out`, and `store` is what says *which* `edges_out`. Until
; 0.15.0 the path segment was emitted as a second `calls` edge,
; which recorded a call the source does not make and put a module
; name into every path the finder walked. It is a hint now.
(call_expression
function: (scoped_identifier
path: (identifier) @hint
name: (identifier) @name)) @reference.call
; A longer path, `crate::store::edges_out`: the hint is the segment
; nearest the name, because that is the one that names the module.
(call_expression
function: (scoped_identifier
path: (scoped_identifier name: (identifier) @hint)
name: (identifier) @name)) @reference.call
; `crate::search_parallel(args)`, `self::helper()`, `super::run()`:
; a path the grammar spells with a keyword, so `(identifier)` above
; never matched it and the call was missed (2026-10-14 audit, G2).
; The keyword names no file, so there is no hint.
(call_expression
function: (scoped_identifier
path: [(crate) (self) (super)]
name: (identifier) @name)) @reference.call
; A method call: `self.flush()`, `store.db()`, `self.index.search()`.
; The receiver is the hint, reduced to its type or last identifier,
; and marked as a receiver: a method call never means a free fn.
(call_expression
function: (field_expression
value: (_) @receiver
field: (field_identifier) @name)) @reference.call
; A call inside a macro's arguments: `json!({ "x": f(1) })`. The
; grammar leaves the arguments as a flat token tree, so none of the
; patterns above ever saw `f(1)` and the call had no edge — which
; is how a caller inside a `json!` slipped past the impact check
; (#244). The same three shapes are read off the tokens instead: a
; name, `a::name` with the segment as the hint, and `x.name` with
; `x` as the receiver, each followed by a parenthesised tree. A
; tree whose own child is `(` is one the parentheses delimit; an
; anchor on that first child misses `()`. An anchor also skips the
; `!` of a nested `vec![..]` or `format!(..)`, and a query cannot
; see whether the tree sits in a macro call or an attribute, so
; `collect` finishes the check; see `macro_argument_call`.
(token_tree
(identifier) @name . (token_tree "(") @reference.call)
(token_tree
(identifier) @hint . "::" . (identifier) @name
. (token_tree "(") @reference.call)
(token_tree
(_) @receiver . "." . (identifier) @name
. (token_tree "(") @reference.call)
"#
}
"typescript" => {
r#"
(import_statement source: (string) @reference.import)
; `import { search as find }` and `export { search as find }`.
(import_specifier
name: (_) @reference.alias
alias: (identifier) @name) @definition.alias
(export_specifier
name: (_) @reference.alias
alias: (identifier) @name) @definition.alias
(function_declaration name: (identifier) @name) @definition.function
(class_declaration name: (type_identifier) @name) @definition.class
(interface_declaration name: (type_identifier) @name) @definition.interface
(method_definition name: (property_identifier) @name) @definition.method
(variable_declarator
name: (identifier) @name
value: [(arrow_function) (function_expression)]) @definition.function
; `const MAX_HOLDERS = 4`. The value is spelled out rather than
; left as `(_)` so that the arrow function on the line above stays
; one symbol: a `const` bound to a function is a function, and
; tagging it twice would make `semlith symbol` report two
; definitions of it.
(lexical_declaration
"const"
(variable_declarator
name: (identifier) @name
value: [
(number) (string) (template_string) (true) (false) (null)
(object) (array) (identifier) (member_expression)
(unary_expression) (binary_expression)
(call_expression) (new_expression)
])) @definition.constant
(call_expression function: (identifier) @name) @reference.call
; `store.search(...)`, `this.index.search(...)`: the object is the
; hint, reduced to its last identifier.
(call_expression
function: (member_expression
object: (_) @hint
property: (property_identifier) @name)) @reference.call
"#
}
"python" => {
r#"
(import_statement name: (_) @reference.import)
(import_from_statement module_name: (_) @reference.import)
; `import numpy as np`, `from x import search as find`.
(aliased_import
name: (_) @reference.alias
alias: (identifier) @name) @definition.alias
; `json.loads(...)`, `self.store.search(...)`: the object names the
; module or the receiver the call went through. A receiver unless
; the file imports the name as a module; see `extract`.
(call
function: (attribute
object: (_) @receiver
attribute: (identifier) @name)) @reference.call
"#
}
"go" => {
r#"
; `const MaxHolders = 4`, and every name in a parenthesised
; `const (...)` block, which is one `const_spec` each.
(const_declaration (const_spec name: (identifier) @name) @definition.constant)
(import_spec path: (interpreted_string_literal) @reference.import)
; `import fp "path/filepath"` — the alias is what the file then
; writes, and the path is what it stands for.
(import_spec
name: (package_identifier) @name
path: (interpreted_string_literal) @reference.alias) @definition.alias
; `fmt.Errorf(...)`, `c.validate()`: the operand is the hint, which
; is what tells `fmt.Errorf` from a call into this corpus.
(call_expression
function: (selector_expression
operand: (identifier) @hint
field: (field_identifier) @name)) @reference.call
"#
}
"java" => {
r#"
(import_declaration (scoped_identifier) @reference.import)
; `static final int MAX_HOLDERS = 4;`. Java has no `const`, so a
; constant is a field that says `final`, and the modifiers are the
; only place that is written.
((field_declaration
(modifiers) @_modifiers
declarator: (variable_declarator name: (identifier) @name)) @definition.constant
(#match? @_modifiers "\\bfinal\\b"))
"#
}
"cpp" => {
r#"
(preproc_include path: (_) @reference.import)
(call_expression function: (identifier) @reference.call)
(function_definition
declarator: (function_declarator
declarator: (identifier) @name)) @definition.function
; `#define MAX_HOLDERS 4` and `constexpr int MAX_WAITERS = 8;`.
; C++ has both of C's macro and a real constant declaration, and
; the qualifier is what tells the second from a plain variable —
; `volatile` sits in the same node and is not one.
(preproc_def name: (identifier) @name) @definition.constant
((declaration
(type_qualifier) @_qualifier
declarator: (init_declarator declarator: (identifier) @name)) @definition.constant
(#any-of? @_qualifier "const" "constexpr" "consteval" "constinit"))
"#
}
"csharp" => {
r#"
(using_directive (identifier) @reference.import)
; `const int MaxHolders = 4;`. A field is a constant only when it
; says so, and `static`, `readonly` and the access modifiers all
; arrive in the same node.
((field_declaration
(modifier) @_const
(variable_declaration
(variable_declarator name: (identifier) @name))) @definition.constant
(#eq? @_const "const"))
(invocation_expression function: (identifier) @name) @reference.call
; `store.Search(...)`: the object is the hint.
(invocation_expression
function: (member_access_expression
expression: (_) @hint
name: (identifier) @name)) @reference.call
"#
}
"elm" => {
r#"
(value_declaration
functionDeclarationLeft: (function_declaration_left
(lower_case_identifier) @name)) @definition.function
(value_expr name: (value_qid (lower_case_identifier) @name)) @reference.call
(import_clause moduleName: (upper_case_qid) @reference.import)
"#
}
"javascript" => {
r#"
(import_statement source: (string) @reference.import)
(import_specifier
name: (_) @reference.alias
alias: (identifier) @name) @definition.alias
(export_specifier
name: (_) @reference.alias
alias: (identifier) @name) @definition.alias
; `const MAX_HOLDERS = 4`. The bundled query tags only the
; `export module.exports = ...` form, so an ordinary top-level
; constant reached the graph as nothing at all. The value is
; spelled out so that `const f = () => {}` stays a function.
(lexical_declaration
"const"
(variable_declarator
name: (identifier) @name
value: [
(number) (string) (template_string) (true) (false) (null)
(object) (array) (identifier) (member_expression)
(unary_expression) (binary_expression)
(call_expression) (new_expression)
])) @definition.constant
"#
}
"php" => {
r#"
(require_expression (string (string_content) @reference.import))
(include_expression (string (string_content) @reference.import))
(function_call_expression function: (name) @name) @reference.call
(member_call_expression
object: (_) @hint
name: (name) @name) @reference.call
; `const MAX_HOLDERS = 4;`, whether it stands alone or inside a
; class. `define('MAX_HOLDERS', 4)` is a function call and stays
; one: the name it binds is a string, not an identifier the
; grammar can hand back.
(const_declaration (const_element (name) @name)) @definition.constant
"#
}
"swift" => {
r#"
(import_declaration (identifier) @reference.import)
(call_expression (simple_identifier) @name) @reference.call
(call_expression
(navigation_expression
target: (_) @hint
suffix: (navigation_suffix (simple_identifier) @name))) @reference.call
"#
}
"c" => {
r#"
(preproc_include path: (_) @reference.import)
(call_expression function: (identifier) @reference.call)
; C's bundled query tags the declarator, so a function's span stops
; at its signature and every call inside its body was attributed to
; the file instead of to the function that makes it. This spans the
; whole definition; the narrower duplicate is dropped by the
; contained-definition rule below.
(function_definition
declarator: (function_declarator
declarator: (identifier) @name)) @definition.function
; `#define MAX_HOLDERS 4`. This is C's named constant, and not a
; stylistic alternative to one: a `const int` is a read-only
; object rather than a constant expression, and cannot size an
; array or label a case.
(preproc_def name: (identifier) @name) @definition.constant
"#
}
"ruby" => {
r#"
; Ruby has no `const` keyword. A name that begins with a capital
; is a constant, and the grammar says so by parsing it as
; `constant` rather than `identifier` — which is why this needs no
; predicate and no naming convention of our own.
(assignment left: (constant) @name) @definition.constant
"#
}
"elixir" => {
r#"
; `@max_holders 4`. A module attribute is the nearest thing
; Elixir has to a constant, and the grammar has no node for one:
; it is `@` applied to a call with no parentheses. The attributes
; the language reserves for documentation and typespecs go
; through the same syntax and are not constants.
((unary_operator
"@"
operand: (call target: (identifier) @name (arguments))) @definition.constant
(#not-any-of? @name
"moduledoc" "doc" "typedoc" "spec" "type" "typep" "opaque"
"callback" "macrocallback" "behaviour" "impl" "deprecated"
"derive" "enforce_keys" "compile" "dialyzer" "on_definition"
"before_compile" "after_compile" "external_resource"))
"#
}
"lua" => {
r#"
; `local MAX_HOLDERS <const> = 4`. Lua 5.4's attribute is the only
; thing in the language that makes a binding a constant; a local
; without one is reassignable and is not.
((variable_declaration
(assignment_statement
(variable_list
name: (identifier) @name
attribute: (attribute (identifier) @_attribute)))) @definition.constant
(#eq? @_attribute "const"))
"#
}
_ => "",
}
}
pub fn language_grammar(lang: &str) -> Option<Language> {
grammar(lang).map(|(language, _)| language)
}
fn grammar(lang: &str) -> Option<(Language, &'static str)> {
match lang {
"rust" => Some((
tree_sitter_rust::LANGUAGE.into(),
tree_sitter_rust::TAGS_QUERY,
)),
"typescript" => Some((
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
tree_sitter_typescript::TAGS_QUERY,
)),
"python" => Some((
tree_sitter_python::LANGUAGE.into(),
tree_sitter_python::TAGS_QUERY,
)),
"go" => Some((tree_sitter_go::LANGUAGE.into(), tree_sitter_go::TAGS_QUERY)),
"java" => Some((
tree_sitter_java::LANGUAGE.into(),
tree_sitter_java::TAGS_QUERY,
)),
"c" => Some((tree_sitter_c::LANGUAGE.into(), tree_sitter_c::TAGS_QUERY)),
"clojure" => Some((
tree_sitter_clojure_orchard::LANGUAGE.into(),
include_str!("../queries/clojure/tags.scm"),
)),
"cpp" => Some((
tree_sitter_cpp::LANGUAGE.into(),
tree_sitter_cpp::TAGS_QUERY,
)),
"csharp" => Some((
tree_sitter_c_sharp::LANGUAGE.into(),
tree_sitter_c_sharp::TAGS_QUERY,
)),
"dart" => Some((
tree_sitter_dart::LANGUAGE.into(),
tree_sitter_dart::TAGS_QUERY,
)),
"dockerfile" => Some((
tree_sitter_containerfile::LANGUAGE.into(),
include_str!("../queries/dockerfile/tags.scm"),
)),
"elixir" => Some((
tree_sitter_elixir::LANGUAGE.into(),
tree_sitter_elixir::TAGS_QUERY,
)),
"elm" => Some((
tree_sitter_elm::LANGUAGE.into(),
tree_sitter_elm::TAGS_QUERY,
)),
"erlang" => Some((
tree_sitter_erlang::LANGUAGE.into(),
include_str!("../queries/erlang/tags.scm"),
)),
"fortran" => Some((
tree_sitter_fortran::LANGUAGE.into(),
include_str!("../queries/fortran/tags.scm"),
)),
"graphql" => Some((
tree_sitter_graphql::LANGUAGE.into(),
include_str!("../queries/graphql/tags.scm"),
)),
"groovy" => Some((
tree_sitter_groovy::LANGUAGE.into(),
include_str!("../queries/groovy/tags.scm"),
)),
"javascript" => Some((
tree_sitter_javascript::LANGUAGE.into(),
tree_sitter_javascript::TAGS_QUERY,
)),
"lua" => Some((
tree_sitter_lua::LANGUAGE.into(),
tree_sitter_lua::TAGS_QUERY,
)),
"nix" => Some((
tree_sitter_nix::LANGUAGE.into(),
include_str!("../queries/nix/tags.scm"),
)),
"ocaml" => Some((
tree_sitter_ocaml::LANGUAGE_OCAML.into(),
tree_sitter_ocaml::TAGS_QUERY,
)),
"php" => Some((
tree_sitter_php::LANGUAGE_PHP.into(),
tree_sitter_php::TAGS_QUERY,
)),
"powershell" => Some((
tree_sitter_powershell::LANGUAGE.into(),
include_str!("../queries/powershell/tags.scm"),
)),
"proto" => Some((
tree_sitter_proto::LANGUAGE.into(),
include_str!("../queries/proto/tags.scm"),
)),
"r" => Some((tree_sitter_r::LANGUAGE.into(), tree_sitter_r::TAGS_QUERY)),
"ruby" => Some((
tree_sitter_ruby::LANGUAGE.into(),
tree_sitter_ruby::TAGS_QUERY,
)),
"sql" => Some((
tree_sitter_sequel::LANGUAGE.into(),
include_str!("../queries/sql/tags.scm"),
)),
"swift" => Some((
tree_sitter_swift::LANGUAGE.into(),
tree_sitter_swift::TAGS_QUERY,
)),
"vue" => Some((
tree_sitter_vue_next::LANGUAGE.into(),
include_str!("../queries/vue/tags.scm"),
)),
"css" => Some((
tree_sitter_css::LANGUAGE.into(),
include_str!("../queries/css/tags.scm"),
)),
"haskell" => Some((
tree_sitter_haskell::LANGUAGE.into(),
include_str!("../queries/haskell/tags.scm"),
)),
"html" => Some((
tree_sitter_html::LANGUAGE.into(),
include_str!("../queries/html/tags.scm"),
)),
"json" => Some((
tree_sitter_json::LANGUAGE.into(),
include_str!("../queries/json/tags.scm"),
)),
"julia" => Some((
tree_sitter_julia::LANGUAGE.into(),
include_str!("../queries/julia/tags.scm"),
)),
"kotlin" => Some((
tree_sitter_kotlin_ng::LANGUAGE.into(),
include_str!("../queries/kotlin/tags.scm"),
)),
"makefile" => Some((
tree_sitter_make::LANGUAGE.into(),
include_str!("../queries/makefile/tags.scm"),
)),
"markdown" => Some((
tree_sitter_md::LANGUAGE.into(),
include_str!("../queries/markdown/tags.scm"),
)),
"objective-c" => Some((
tree_sitter_objc::LANGUAGE.into(),
include_str!("../queries/objective-c/tags.scm"),
)),
"perl" => Some((
ts_parser_perl::LANGUAGE.into(),
include_str!("../queries/perl/tags.scm"),
)),
"scala" => Some((
tree_sitter_scala::LANGUAGE.into(),
include_str!("../queries/scala/tags.scm"),
)),
"shell" => Some((
tree_sitter_bash::LANGUAGE.into(),
include_str!("../queries/shell/tags.scm"),
)),
"svelte" => Some((
tree_sitter_svelte_ng::LANGUAGE.into(),
include_str!("../queries/svelte/tags.scm"),
)),
"terraform" => Some((
tree_sitter_hcl::LANGUAGE.into(),
include_str!("../queries/terraform/tags.scm"),
)),
"toml" => Some((
tree_sitter_toml_ng::LANGUAGE.into(),
include_str!("../queries/toml/tags.scm"),
)),
"yaml" => Some((
tree_sitter_yaml::LANGUAGE.into(),
include_str!("../queries/yaml/tags.scm"),
)),
"zig" => Some((
tree_sitter_zig::LANGUAGE.into(),
include_str!("../queries/zig/tags.scm"),
)),
_ => None,
}
}
fn queries(lang: &'static str, language: &Language, tags: &str) -> &'static [Query] {
static CACHE: std::sync::LazyLock<
std::sync::Mutex<std::collections::HashMap<&'static str, &'static [Query]>>,
> = std::sync::LazyLock::new(Default::default);
let mut cache = CACHE.lock().unwrap_or_else(|e| e.into_inner());
cache.entry(lang).or_insert_with(|| {
let compiled: Vec<Query> = [tags, supplement(lang)]
.iter()
.filter(|source| !source.is_empty())
.filter_map(|source| Query::new(language, source).ok())
.collect();
Box::leak(compiled.into_boxed_slice())
})
}
struct Def {
kind: String,
name: String,
owner: Option<String>,
start: usize,
end: usize,
start_line: u32,
end_line: u32,
}
struct Ref {
name: String,
kind: &'static str,
at: usize,
line: u32,
hint: Option<String>,
method: bool,
on_self: bool,
}
pub const MAX_BLOCK_NESTING: usize = 128;
pub fn ctype_safe<'a>(lang: &str, text: &'a str) -> std::borrow::Cow<'a, str> {
let triggers: fn(char) -> bool = match lang {
"markdown" => |c| c.is_ascii_digit(),
"bash" => |c| c.is_ascii_digit() || c == '{' || c == '.',
_ => return std::borrow::Cow::Borrowed(text),
};
let needs = |prev: Option<char>, c: char| (c as u32) > 0xFF && prev.is_some_and(triggers);
let mut prev = None;
if !text.chars().any(|c| {
let hit = needs(prev, c);
prev = Some(c);
hit
}) {
return std::borrow::Cow::Borrowed(text);
}
let mut out = String::with_capacity(text.len());
let mut prev = None;
for c in text.chars() {
if needs(prev, c) {
out.extend(std::iter::repeat_n('a', c.len_utf8()));
prev = Some('a');
} else {
out.push(c);
prev = Some(c);
}
}
std::borrow::Cow::Owned(out)
}
pub fn too_nested(lang: &str, text: &str) -> bool {
if !matches!(lang, "markdown" | "yaml") {
return false;
}
#[cfg(debug_assertions)]
if std::env::var_os("SEMLITH_NO_NESTING_CAP").is_some() {
return false;
}
text.lines().any(|line| {
let (mut depth, mut columns) = (0usize, 0usize);
let mut chars = line.chars().peekable();
while let Some(c) = chars.next() {
match c {
' ' => columns += 1,
'\t' => columns += 2,
'>' => depth += 1,
'-' | '*' | '+' if chars.peek().is_none_or(|n| *n == ' ' || *n == '\t') => {
depth += 1
}
c if c.is_control() => {}
_ => break,
}
if depth + columns / 2 > MAX_BLOCK_NESTING {
return true;
}
}
false
})
}
pub fn extract(path: &Path, text: &str) -> Result<Option<Extraction>> {
let Some(lang) = language_of(path) else {
return Ok(None);
};
if too_nested(lang, text) {
return Ok(None);
}
let Some((language, tags)) = grammar(lang) else {
return Ok(None);
};
let mut parser = Parser::new();
parser.set_language(&language)?;
let deadline = std::time::Instant::now() + PARSE_TIMEOUT;
let mut expired = |_: &tree_sitter::ParseState| {
if std::time::Instant::now() >= deadline {
std::ops::ControlFlow::Break(())
} else {
std::ops::ControlFlow::Continue(())
}
};
let options = tree_sitter::ParseOptions::default().progress_callback(&mut expired);
let parse = ctype_safe(lang, text);
let parsed =
parser.parse_with_options(&mut |at, _| &parse.as_bytes()[at..], None, Some(options));
let Some(tree) = parsed else {
return Ok(None);
};
let mut defs: Vec<Def> = Vec::new();
let mut refs: Vec<Ref> = Vec::new();
let rust = (lang == "rust").then(|| RustFields::of(tree.root_node(), text));
for query in queries(lang, &language, tags) {
collect(query, &tree, text, rust.as_ref(), &mut defs, &mut refs);
}
if lang == "markdown" {
defs.retain(|d| d.kind != "heading");
defs.extend(markdown_sections(text));
}
defs.sort_by(|a, b| {
(a.start, std::cmp::Reverse(a.end), &a.name).cmp(&(
b.start,
std::cmp::Reverse(b.end),
&b.name,
))
});
defs.dedup_by(|a, b| a.start == b.start && a.end == b.end && a.name == b.name);
let contained = contained(&defs);
let mut keep = contained.iter();
defs.retain(|_| !keep.next().copied().unwrap_or(false));
let module = module_name(path);
let mut symbols = vec![Symbol {
kind: "module".to_string(),
name: module.clone(),
qualified: path.to_string_lossy().to_string(),
start_line: 1,
end_line: text.lines().count().max(1) as u32,
}];
let mut edges: Vec<Edge> = Vec::new();
let parents = enclosing_each(&defs);
for (i, def) in defs.iter().enumerate() {
let parent = parents[i].map(|p| defs[p].name.clone());
let qualified = match (&def.owner, &parent) {
(Some(owner), _) => format!("{owner}::{}", def.name),
(None, Some(p)) => format!("{p}::{}", def.name),
(None, None) => format!("{module}::{}", def.name),
};
symbols.push(Symbol {
kind: def.kind.clone(),
name: def.name.clone(),
qualified,
start_line: def.start_line,
end_line: def.end_line,
});
if parent.as_deref().unwrap_or(&module) == def.name {
continue;
}
match parent {
Some(p) => edges.push(Edge {
from: p,
to: def.name.clone(),
kind: "contains".to_string(),
confidence: EXTRACTED.to_string(),
hint: None,
line: Some(def.start_line),
from_module: false,
}),
None => edges.push(Edge {
from: module.clone(),
to: def.name.clone(),
kind: "defines".to_string(),
confidence: EXTRACTED.to_string(),
hint: None,
line: Some(def.start_line),
from_module: true,
}),
}
}
let imported: Vec<String> = refs
.iter()
.filter(|r| r.kind == "imports")
.flat_map(|r| import_names(&r.name))
.collect();
let class_of = |at: Option<usize>| -> Option<String> {
let mut at = at;
if let Some(owner) = at.and_then(|i| defs[i].owner.clone()) {
return Some(owner);
}
for _ in 0..defs.len() {
let i = at?;
if defs[i].kind == "class" {
return Some(defs[i].name.clone());
}
at = parents[i];
}
None
};
let holders = innermost_at(&defs, refs.iter().map(|r| r.at), None);
for (reference, holder) in refs.iter().zip(holders) {
let enclosing = holder.map(|h| &defs[h]);
let from = match enclosing {
Some(def) => def.name.clone(),
None => module.clone(),
};
let owner = class_of(holder);
let lead = match (&reference.hint, reference.on_self) {
(Some(h), _) => Some(h.clone()),
(None, true) => owner.clone(),
(None, false) => None,
};
let method = reference.method
&& !(lang == "python"
&& !reference.on_self
&& lead.as_ref().is_some_and(|l| {
imported.contains(l) || defs.iter().any(|d| d.kind == "alias" && d.name == *l)
}));
let recursion = from == reference.name
&& match &lead {
None => true,
Some(l) => owner.as_ref() == Some(l) || (!method && l == "Self"),
};
if recursion {
continue;
}
let named_by_file = reference.kind == "imports"
|| imported.contains(&reference.name)
|| lead.as_ref().is_some_and(|h| imported.contains(h));
let confidence = if named_by_file { EXTRACTED } else { INFERRED };
let hint = if method {
Some(format!("{RECEIVER}{}", lead.unwrap_or_default()))
} else {
reference.hint.clone()
};
edges.push(Edge {
from,
to: reference.name.clone(),
kind: reference.kind.to_string(),
confidence: confidence.to_string(),
hint,
line: Some(reference.line),
from_module: enclosing.is_none(),
});
}
edges.sort_by(|a, b| {
(
&a.from,
&a.to,
&a.kind,
a.from_module,
confidence_rank(&a.confidence),
a.hint.is_none(),
&a.hint,
a.line.is_none(),
a.line,
)
.cmp(&(
&b.from,
&b.to,
&b.kind,
b.from_module,
confidence_rank(&b.confidence),
b.hint.is_none(),
&b.hint,
b.line.is_none(),
b.line,
))
});
edges.dedup_by(|a, b| {
(&a.from, &a.to, &a.kind, a.from_module) == (&b.from, &b.to, &b.kind, b.from_module)
});
Ok(Some(Extraction { symbols, edges }))
}
fn collect(
query: &Query,
tree: &tree_sitter::Tree,
text: &str,
rust: Option<&RustFields>,
defs: &mut Vec<Def>,
refs: &mut Vec<Ref>,
) {
let names = query.capture_names();
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(query, tree.root_node(), text.as_bytes());
while let Some(m) = matches.next() {
let mut span: Option<(&str, tree_sitter::Node)> = None;
let mut name: Option<tree_sitter::Node> = None;
let mut hint: Option<tree_sitter::Node> = None;
let mut method = false;
let mut references: Vec<(&str, tree_sitter::Node)> = Vec::new();
for capture in m.captures() {
let capture_name = names[capture.index as usize];
if let Some(kind) = capture_name.strip_prefix("definition.") {
span = Some((kind, capture.node));
} else if capture_name == "name" {
name = Some(capture.node);
} else if capture_name == "hint" {
hint = Some(capture.node);
} else if capture_name == "receiver" {
hint = Some(capture.node);
method = true;
} else if let Some(kind) = capture_name.strip_prefix("reference.") {
references.push((kind, capture.node));
}
}
for (kind, node) in references {
let kind = match kind {
"call" => "calls",
"import" => "imports",
"alias" => "aliases",
_ => "references",
};
let target = if kind == "imports" {
Some(trim_literal(&text[node.byte_range()]).to_string())
} else if kind == "aliases" {
last_segment(&text[node.byte_range()])
} else {
match name {
Some(n) => Some(text[n.byte_range()].to_string()),
None => reference_name(node, text),
}
};
let Some(target) = target.filter(|t| !t.is_empty()) else {
continue;
};
if node.kind() == "token_tree" && !macro_argument_call(node, name) {
continue;
}
let target = if in_macro(node) {
format!("{target}!")
} else {
target
};
refs.push(Ref {
name: target,
kind,
at: node.start_byte(),
line: node.start_position().row as u32 + 1,
hint: hint.and_then(|h| {
rust.and_then(|fields| receiver_type(h, text, fields))
.or_else(|| hint_text(h, text))
}),
method,
on_self: hint
.is_some_and(|h| matches!(&text[h.byte_range()], "self" | "this" | "cls")),
});
}
if let (Some((kind, node)), Some(name)) = (span, name) {
let span = if kind == "alias" {
node
} else {
definition_span(node, name)
};
let raw = &text[name.byte_range()];
defs.push(Def {
kind: kind.to_string(),
name: if kind == "heading" {
crate::chunk::heading_title(raw)
} else {
raw.to_string()
},
owner: rust.and_then(|_| rust_owner(span, text)),
start: span.start_byte(),
end: span.end_byte(),
start_line: span.start_position().row as u32 + 1,
end_line: span.end_position().row as u32 + 1,
});
}
}
}
fn markdown_sections(text: &str) -> Vec<Def> {
let mut starts = vec![0usize];
starts.extend(text.match_indices('\n').map(|(i, _)| i + 1));
let lines = text.lines().count();
let byte = |line: usize| starts.get(line).copied().unwrap_or(text.len());
let found = crate::chunk::headings(text);
found
.iter()
.enumerate()
.map(|(i, h)| {
let next = found[i + 1..]
.iter()
.find(|n| n.level <= h.level)
.map_or(lines + 1, |n| n.line as usize);
let end_line = (next - 1).max(h.line as usize);
Def {
kind: "heading".to_string(),
name: h.title.clone(),
owner: None,
start: byte(h.line as usize - 1),
end: byte(end_line)
.saturating_sub(1)
.max(byte(h.line as usize - 1) + 1),
start_line: h.line,
end_line: end_line as u32,
}
})
.collect()
}
#[derive(Debug, Clone, PartialEq)]
struct Ty {
name: String,
elem: Option<String>,
}
const STD_TYPES: [&str; 14] = [
"Vec", "VecDeque", "Option", "Result", "String", "str", "HashMap", "BTreeMap", "HashSet",
"BTreeSet", "Path", "PathBuf", "[]", "Self",
];
struct RustFields {
by_owner: std::collections::HashMap<(String, String), Ty>,
by_name: std::collections::HashMap<String, Option<Ty>>,
}
impl RustFields {
fn of(root: tree_sitter::Node, text: &str) -> RustFields {
let mut out = RustFields {
by_owner: Default::default(),
by_name: Default::default(),
};
let mut cursor = root.walk();
let mut stack = vec![root];
while let Some(node) = stack.pop() {
if node.kind() == "field_declaration" {
let owner = node
.parent()
.and_then(|list| list.parent())
.filter(|item| item.kind() == "struct_item")
.and_then(|item| item.child_by_field_name("name"))
.map(|n| text[n.byte_range()].to_string());
let name = node.child_by_field_name("name");
let ty = node
.child_by_field_name("type")
.and_then(|t| rust_ty(t, text));
if let (Some(name), Some(ty)) = (name, ty) {
let field = text[name.byte_range()].to_string();
if let Some(owner) = owner {
out.by_owner.insert((owner, field.clone()), ty.clone());
}
let entry = out.by_name.entry(field).or_insert_with(|| Some(ty.clone()));
if entry.as_ref() != Some(&ty) {
*entry = None;
}
}
continue;
}
stack.extend(node.named_children(&mut cursor));
}
out
}
}
fn rust_owner(node: tree_sitter::Node, text: &str) -> Option<String> {
let mut at = node.parent();
while let Some(n) = at {
match n.kind() {
"impl_item" => return rust_type_name(n.child_by_field_name("type")?, text),
"trait_item" => {
return n
.child_by_field_name("name")
.map(|t| text[t.byte_range()].to_string());
}
"function_item" | "mod_item" | "closure_expression" => return None,
_ => {}
}
at = n.parent();
}
None
}
fn rust_type_name(node: tree_sitter::Node, text: &str) -> Option<String> {
rust_ty(node, text).map(|t| t.name)
}
fn rust_ty(node: tree_sitter::Node, text: &str) -> Option<Ty> {
const THROUGH: [&str; 7] = ["Box", "Rc", "Arc", "RefCell", "Mutex", "RwLock", "Cell"];
let plain = |name: String| Some(Ty { name, elem: None });
match node.kind() {
"type_identifier" | "primitive_type" => plain(text[node.byte_range()].to_string()),
"reference_type" | "pointer_type" => rust_ty(node.child_by_field_name("type")?, text),
"scoped_type_identifier" => rust_ty(node.child_by_field_name("name")?, text),
"array_type" => Some(Ty {
name: "[]".to_string(),
elem: node
.child_by_field_name("element")
.and_then(|e| rust_type_name(e, text)),
}),
"generic_type" => {
let base = rust_type_name(node.child_by_field_name("type")?, text)?;
let first = node.child_by_field_name("type_arguments").and_then(|args| {
let mut cursor = args.walk();
args.named_children(&mut cursor).next()
});
if THROUGH.contains(&base.as_str()) {
return rust_ty(first?, text);
}
Some(Ty {
name: base,
elem: first.and_then(|f| rust_type_name(f, text)),
})
}
_ => None,
}
}
fn receiver_type(receiver: tree_sitter::Node, text: &str, fields: &RustFields) -> Option<String> {
expr_ty(receiver, text, fields, 0)
.map(|t| t.name)
.filter(|name| !STD_TYPES.contains(&name.as_str()))
}
fn expr_ty(node: tree_sitter::Node, text: &str, fields: &RustFields, depth: u8) -> Option<Ty> {
if depth > 8 {
return None;
}
match node.kind() {
"self" => {
let mut at = node.parent();
while let Some(n) = at {
if n.kind() == "function_item" {
return rust_owner(n, text).map(|name| Ty { name, elem: None });
}
at = n.parent();
}
None
}
"identifier" => binding_ty(node, text, fields, depth),
"field_expression" => {
let field = text[node.child_by_field_name("field")?.byte_range()].to_string();
let owner = node
.child_by_field_name("value")
.and_then(|v| expr_ty(v, text, fields, depth + 1));
if let Some(owner) = owner
&& let Some(ty) = fields.by_owner.get(&(owner.name, field.clone()))
{
return Some(ty.clone());
}
fields.by_name.get(&field).cloned().flatten()
}
"index_expression" => {
let container = expr_ty(node.named_child(0)?, text, fields, depth + 1)?;
container.elem.map(|name| Ty { name, elem: None })
}
"reference_expression" | "parenthesized_expression" | "try_expression" => {
let inner = node
.child_by_field_name("value")
.or_else(|| node.named_child(0))?;
expr_ty(inner, text, fields, depth + 1)
}
_ => None,
}
}
fn binding_ty(ident: tree_sitter::Node, text: &str, fields: &RustFields, depth: u8) -> Option<Ty> {
let name = &text[ident.byte_range()];
let mut function = ident.parent();
while let Some(f) = function {
if f.kind() == "function_item" {
break;
}
function = f.parent();
}
let function = function?;
let mut found: Option<(usize, Ty)> = None;
let mut cursor = function.walk();
let mut stack = vec![function];
while let Some(node) = stack.pop() {
if node.start_byte() >= ident.start_byte() {
continue;
}
let names_it = |field: &str| {
node.child_by_field_name(field)
.is_some_and(|p| p.kind() == "identifier" && &text[p.byte_range()] == name)
};
let ty = match node.kind() {
"parameter" | "let_declaration" if names_it("pattern") => node
.child_by_field_name("type")
.and_then(|t| rust_ty(t, text))
.or_else(|| {
node.child_by_field_name("value")
.and_then(|v| constructed_type(v, text))
.map(|name| Ty { name, elem: None })
}),
"for_expression" if names_it("pattern") => node
.child_by_field_name("value")
.and_then(|v| expr_ty(v, text, fields, depth + 1))
.and_then(|t| t.elem)
.map(|name| Ty { name, elem: None }),
_ => None,
};
if let Some(ty) = ty
&& found.as_ref().is_none_or(|(at, _)| node.start_byte() > *at)
{
found = Some((node.start_byte(), ty));
}
stack.extend(node.named_children(&mut cursor));
}
found.map(|(_, ty)| ty)
}
fn constructed_type(value: tree_sitter::Node, text: &str) -> Option<String> {
let upper = |t: &str| t.chars().next().is_some_and(char::is_uppercase);
match value.kind() {
"try_expression" => constructed_type(value.named_child(0)?, text),
"struct_expression" => {
rust_type_name(value.child_by_field_name("name")?, text).filter(|t| upper(t))
}
"call_expression" => {
let function = value.child_by_field_name("function")?;
match function.kind() {
"scoped_identifier" => {
let path = function.child_by_field_name("path")?;
let ty = match path.kind() {
"identifier" | "type_identifier" => text[path.byte_range()].to_string(),
"scoped_identifier" => {
text[path.child_by_field_name("name")?.byte_range()].to_string()
}
_ => return None,
};
upper(&ty).then_some(ty)
}
"field_expression" => {
let method = &text[function.child_by_field_name("field")?.byte_range()];
if matches!(method, "unwrap" | "expect") {
constructed_type(function.child_by_field_name("value")?, text)
} else {
None
}
}
_ => None,
}
}
_ => None,
}
}
fn in_macro(node: tree_sitter::Node) -> bool {
const MACRO_KINDS: [&str; 2] = ["macro_invocation", "macro_expression"];
if MACRO_KINDS.contains(&node.kind()) {
return true;
}
node.parent()
.is_some_and(|parent| MACRO_KINDS.contains(&parent.kind()))
}
fn macro_argument_call(args: tree_sitter::Node, name: Option<tree_sitter::Node>) -> bool {
if name.is_none() || args.prev_sibling() != name {
return false;
}
let mut up = args.parent();
while let Some(node) = up {
if node.kind() != "token_tree" {
return node.kind() == "macro_invocation";
}
up = node.parent();
}
false
}
fn hint_text(node: tree_sitter::Node, text: &str) -> Option<String> {
const USELESS: [&str; 5] = ["self", "this", "super", "crate", "cls"];
let name = reference_name(node, text)?;
if name.is_empty() || USELESS.contains(&name.as_str()) {
return None;
}
Some(name)
}
fn reference_name(node: tree_sitter::Node, text: &str) -> Option<String> {
if let Some(function) = node.child_by_field_name("function") {
return reference_name(function, text);
}
if let Some(property) = node.child_by_field_name("property") {
return reference_name(property, text);
}
let kind = node.kind();
if kind.ends_with("identifier") || kind == "type_identifier" || kind == "word" {
return Some(text[node.byte_range()].to_string());
}
if let Some(name) = node.child_by_field_name("name") {
return reference_name(name, text);
}
let mut cursor = node.walk();
let last = node
.named_children(&mut cursor)
.filter(|c| c.kind().ends_with("identifier"))
.last();
match last {
Some(child) => Some(text[child.byte_range()].to_string()),
None if node.child_count() == 0 => Some(text[node.byte_range()].to_string()),
None => None,
}
}
fn definition_span<'a>(
captured: tree_sitter::Node<'a>,
name: tree_sitter::Node<'a>,
) -> tree_sitter::Node<'a> {
if captured.start_byte() <= name.start_byte() && captured.end_byte() > name.end_byte() {
return captured;
}
let mut best = captured;
let mut node = name;
while let Some(parent) = node.parent().filter(|p| p.parent().is_some()) {
if declaration_shaped(parent.kind()) && parent.end_byte() > best.end_byte() {
best = parent;
}
node = parent;
}
best
}
fn declaration_shaped(kind: &str) -> bool {
const MARKERS: [&str; 10] = [
"declaration",
"definition",
"_item",
"specifier",
"class",
"function",
"method",
"struct",
"interface",
"module",
];
MARKERS.iter().any(|m| kind.contains(m))
}
fn contained(defs: &[Def]) -> Vec<bool> {
let mut out = vec![false; defs.len()];
let mut groups: std::collections::HashMap<(&str, &str), Vec<usize>> =
std::collections::HashMap::new();
for (i, d) in defs.iter().enumerate() {
groups.entry((&d.name, &d.kind)).or_default().push(i);
}
for mut group in groups.into_values() {
group.sort_by_key(|&i| (defs[i].start, std::cmp::Reverse(defs[i].end)));
let mut widest: Option<usize> = None;
let mut run = 0;
while run < group.len() {
let range = (defs[group[run]].start, defs[group[run]].end);
let mut next = run;
while next < group.len() && (defs[group[next]].start, defs[group[next]].end) == range {
next += 1;
}
let covered = widest.is_some_and(|end| end >= range.1);
for &i in &group[run..next] {
out[i] = covered;
}
widest = Some(widest.map_or(range.1, |end| end.max(range.1)));
run = next;
}
}
out
}
fn innermost_at(
defs: &[Def],
at: impl Iterator<Item = usize>,
not: Option<&dyn Fn(usize) -> usize>,
) -> Vec<Option<usize>> {
use std::cmp::Reverse;
use std::collections::{BTreeSet, BinaryHeap};
let at: Vec<usize> = at.collect();
let mut order: Vec<usize> = (0..at.len()).collect();
order.sort_by_key(|&q| at[q]);
let mut by_start: Vec<usize> = (0..defs.len()).collect();
by_start.sort_by_key(|&i| defs[i].start);
let mut open: BTreeSet<(usize, usize)> = BTreeSet::new();
let mut ends: BinaryHeap<Reverse<(usize, usize)>> = BinaryHeap::new();
let mut next = 0;
let mut out = vec![None; at.len()];
for q in order {
let pos = at[q];
while next < by_start.len() && defs[by_start[next]].start <= pos {
let d = &defs[by_start[next]];
open.insert((d.end - d.start, by_start[next]));
ends.push(Reverse((d.end, by_start[next])));
next += 1;
}
while let Some(Reverse((end, i))) = ends.peek().copied() {
if end > pos {
break;
}
ends.pop();
let d = &defs[i];
open.remove(&(d.end - d.start, i));
}
let skip = not.map(|f| f(q));
out[q] = open.iter().map(|&(_, i)| i).find(|&i| Some(i) != skip);
}
out
}
fn enclosing_each(defs: &[Def]) -> Vec<Option<usize>> {
innermost_at(defs, defs.iter().map(|d| d.start), Some(&|q| q))
}
fn module_name(path: &Path) -> String {
path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("module")
.to_string()
}
fn import_names(raw: &str) -> Vec<String> {
let cleaned = trim_literal(raw);
let mut out = vec![cleaned.to_string()];
if let Some(last) = cleaned
.rsplit(['/', '.', ':'])
.find(|s| !s.is_empty())
.filter(|s| *s != cleaned)
{
out.push(last.to_string());
}
out
}
fn last_segment(raw: &str) -> Option<String> {
let cleaned = trim_literal(raw);
cleaned
.rsplit(['/', '.', ':'])
.find(|s| !s.is_empty())
.map(|s| s.to_string())
.filter(|s| !s.is_empty())
}
fn trim_literal(raw: &str) -> &str {
raw.trim()
.trim_matches(|c| c == '"' || c == '\'' || c == '<' || c == '>')
.trim()
}
pub const MAX_NODES: usize = 2000;
pub const DEPENDENCY_KINDS: [&str; 4] = ["calls", "imports", "references", "aliases"];
pub fn dependency_kinds() -> Vec<String> {
DEPENDENCY_KINDS.iter().map(|k| k.to_string()).collect()
}
pub const NAVIGATIONAL_KINDS: [&str; 4] = ["heading", "key", "selector", "element"];
pub fn is_builtin(language: &str, name: &str, kind: &str, hint: Option<&str>) -> bool {
let hint = hint.map(|h| h.trim_start_matches(RECEIVER));
let list = |names: &[&str], n: &str| names.contains(&n);
match language {
"go" => {
const BUILTINS: &[&str] = &[
"append",
"cap",
"clear",
"close",
"complex",
"copy",
"delete",
"imag",
"len",
"make",
"max",
"min",
"new",
"panic",
"print",
"println",
"real",
"recover",
"bool",
"byte",
"error",
"float32",
"float64",
"int",
"int8",
"int16",
"int32",
"int64",
"rune",
"string",
"uint",
"uint8",
"uint16",
"uint32",
"uint64",
"uintptr",
"any",
"Errorf",
"Error",
"Fatal",
"Fatalf",
"Log",
"Logf",
"Helper",
"Run",
"Skip",
"Skipf",
"Cleanup",
"TempDir",
"Setenv",
"Parallel",
"Name",
"String",
"Close",
"Write",
"WriteString",
"Read",
"Len",
"Lock",
"Unlock",
"RLock",
"RUnlock",
"Add",
"Done",
"Wait",
"Unwrap",
"Is",
"As",
"Bytes",
"Reset",
"Sum",
"Contains",
"HasPrefix",
"Sprintf",
];
const PACKAGES: &[&str] = &[
"fmt",
"os",
"io",
"strings",
"strconv",
"errors",
"bytes",
"sort",
"sync",
"time",
"context",
"filepath",
"path",
"bufio",
"regexp",
"reflect",
"unicode",
"utf8",
"math",
"rand",
"json",
"http",
"url",
"log",
"flag",
"exec",
"runtime",
"atomic",
"template",
"testing",
"hex",
"base64",
"ioutil",
"signal",
"syscall",
"user",
"unsafe",
"slices",
"maps",
"cmp",
"iter",
"tabwriter",
"heap",
"list",
];
if kind == "imports" {
let path = name.trim_matches('"');
return !path.split('/').next().unwrap_or(path).contains('.');
}
list(BUILTINS, name) || hint.is_some_and(|h| list(PACKAGES, h))
}
"rust" => {
const BUILTINS: &[&str] = &[
"Ok",
"Err",
"Some",
"Box",
"drop",
"panic!",
"println!",
"print!",
"eprintln!",
"eprint!",
"format!",
"vec!",
"write!",
"writeln!",
"assert!",
"assert_eq!",
"assert_ne!",
"debug_assert!",
"debug_assert_eq!",
"unreachable!",
"todo!",
"unimplemented!",
"matches!",
"concat!",
"env!",
"include_str!",
"include_bytes!",
"stringify!",
"line!",
"file!",
"format_args!",
"dbg!",
"cfg!",
"thread_local!",
"compile_error!",
"clone",
"unwrap",
"expect",
"into",
"iter",
"into_iter",
"iter_mut",
"collect",
"map",
"to_string",
"to_owned",
"as_str",
"as_ref",
"as_deref",
"len",
"is_empty",
"push",
"push_str",
"pop",
"get",
"get_mut",
"insert",
"remove",
"contains",
"contains_key",
"unwrap_or",
"unwrap_or_default",
"unwrap_or_else",
"ok",
"err",
"ok_or",
"ok_or_else",
"and_then",
"filter",
"filter_map",
"next",
"join",
"split",
"trim",
"starts_with",
"ends_with",
"lock",
"borrow",
"borrow_mut",
"take",
"extend",
"enumerate",
"zip",
"chain",
"rev",
"sort",
"sort_by",
"sort_by_key",
"dedup",
"retain",
"entry",
"or_insert",
"or_default",
"chars",
"bytes",
"lines",
"find",
"position",
"any",
"all",
"count",
"sum",
"first",
"last",
"is_some",
"is_none",
"is_ok",
"is_err",
"from",
"default",
"fmt",
"eq",
"cmp",
"hash",
"with_capacity",
"map_err",
"flatten",
"flat_map",
"skip",
"copied",
"cloned",
"truncate",
"clear",
"to_vec",
"keys",
"values",
"display",
"load",
"store",
"send",
"recv",
"spawn",
"now",
"elapsed",
];
const NAMESPACES: &[&str] = &[
"std",
"core",
"alloc",
"String",
"Vec",
"Option",
"Result",
"Box",
"Rc",
"Arc",
"HashMap",
"HashSet",
"BTreeMap",
"BTreeSet",
"VecDeque",
"Path",
"PathBuf",
"File",
"fs",
"io",
"env",
"thread",
"mem",
"ptr",
"fmt",
"process",
"Duration",
"Instant",
"Mutex",
"RwLock",
"AtomicBool",
"AtomicUsize",
"AtomicU64",
"Ordering",
"Cow",
"Cell",
"RefCell",
"iter",
"str",
"char",
"u8",
"u32",
"u64",
"usize",
"i32",
"i64",
"f64",
"Self",
];
if kind == "imports" {
return ["std", "core", "alloc"]
.iter()
.any(|root| name == *root || name.starts_with(&format!("{root}::")));
}
list(BUILTINS, name) || hint.is_some_and(|h| list(NAMESPACES, h))
}
"python" => {
const BUILTINS: &[&str] = &[
"abs",
"all",
"any",
"ascii",
"bin",
"bool",
"breakpoint",
"bytearray",
"bytes",
"callable",
"chr",
"classmethod",
"compile",
"complex",
"delattr",
"dict",
"dir",
"divmod",
"enumerate",
"eval",
"exec",
"filter",
"float",
"format",
"frozenset",
"getattr",
"globals",
"hasattr",
"hash",
"help",
"hex",
"id",
"input",
"int",
"isinstance",
"issubclass",
"iter",
"len",
"list",
"locals",
"map",
"max",
"memoryview",
"min",
"next",
"object",
"oct",
"open",
"ord",
"pow",
"print",
"property",
"range",
"repr",
"reversed",
"round",
"set",
"setattr",
"slice",
"sorted",
"staticmethod",
"str",
"sum",
"super",
"tuple",
"type",
"vars",
"zip",
"__import__",
"Exception",
"ValueError",
"TypeError",
"KeyError",
"IndexError",
"RuntimeError",
"NotImplementedError",
"AttributeError",
"OSError",
"ImportError",
"StopIteration",
"AssertionError",
"LookupError",
"FileNotFoundError",
"PermissionError",
"UnicodeDecodeError",
"Warning",
"DeprecationWarning",
"UserWarning",
"append",
"extend",
"get",
"items",
"keys",
"values",
"join",
"split",
"strip",
"rstrip",
"lstrip",
"startswith",
"endswith",
"replace",
"lower",
"upper",
"pop",
"update",
"setdefault",
"encode",
"decode",
"add",
"remove",
"insert",
"sort",
"copy",
"count",
"index",
"discard",
"partition",
"rpartition",
"splitlines",
"isdigit",
"isidentifier",
];
const MODULES: &[&str] = &[
"os",
"sys",
"re",
"json",
"typing",
"t",
"collections",
"cabc",
"functools",
"itertools",
"pathlib",
"io",
"time",
"datetime",
"logging",
"subprocess",
"shutil",
"tempfile",
"threading",
"asyncio",
"inspect",
"contextlib",
"dataclasses",
"enum",
"abc",
"copy",
"math",
"random",
"string",
"textwrap",
"traceback",
"warnings",
"weakref",
"types",
"uuid",
"hashlib",
"base64",
"struct",
"socket",
"urllib",
"http",
"email",
"unittest",
"argparse",
"pickle",
"csv",
"glob",
"fnmatch",
"operator",
"queue",
"signal",
"platform",
"importlib",
"gzip",
"zipfile",
"decimal",
"statistics",
"secrets",
"hmac",
"ssl",
"errno",
"codecs",
"locale",
"pprint",
"heapq",
"bisect",
"ast",
"gc",
"atexit",
"mimetypes",
"numbers",
"__future__",
"path",
];
if kind == "imports" {
let module = name.split_whitespace().next().unwrap_or(name);
let top = module.split('.').next().unwrap_or(module);
return !module.starts_with('.') && list(MODULES, top);
}
list(BUILTINS, name) || hint.is_some_and(|h| list(MODULES, h))
}
"javascript" | "typescript" | "tsx" => {
const BUILTINS: &[&str] = &[
"require",
"parseInt",
"parseFloat",
"isNaN",
"isFinite",
"setTimeout",
"setInterval",
"clearTimeout",
"clearInterval",
"setImmediate",
"Promise",
"Object",
"Array",
"JSON",
"Math",
"Number",
"String",
"Boolean",
"Date",
"Error",
"TypeError",
"RangeError",
"Symbol",
"Map",
"Set",
"WeakMap",
"RegExp",
"encodeURIComponent",
"decodeURIComponent",
"encodeURI",
"decodeURI",
"fetch",
"structuredClone",
"queueMicrotask",
"push",
"pop",
"shift",
"unshift",
"map",
"filter",
"forEach",
"reduce",
"join",
"split",
"slice",
"splice",
"indexOf",
"includes",
"keys",
"values",
"entries",
"then",
"catch",
"finally",
"log",
"error",
"warn",
"info",
"debug",
"toString",
"hasOwnProperty",
"call",
"apply",
"bind",
"replace",
"trim",
"concat",
"find",
"some",
"every",
"sort",
"test",
"exec",
"match",
"startsWith",
"endsWith",
"toLowerCase",
"toUpperCase",
"stringify",
"parse",
"assign",
"freeze",
"isArray",
"resolve",
"reject",
"all",
"get",
"set",
"has",
"delete",
"emit",
"on",
];
const MODULES: &[&str] = &[
"fs",
"path",
"http",
"https",
"url",
"util",
"events",
"stream",
"os",
"crypto",
"child_process",
"assert",
"buffer",
"querystring",
"zlib",
"net",
"readline",
"process",
"console",
"Object",
"Array",
"JSON",
"Math",
"Promise",
"Number",
"String",
"Date",
"Reflect",
];
if kind == "imports" {
let module = name.trim_matches(|c| c == '"' || c == '\'');
return module.starts_with("node:") || list(MODULES, module);
}
list(BUILTINS, name) || hint.is_some_and(|h| list(MODULES, h))
}
_ => false,
}
}
pub fn is_relative_import(language: &str, name: &str) -> bool {
match language {
"rust" => {
["crate::", "super::", "self::"]
.iter()
.any(|p| name.starts_with(p))
|| matches!(name, "crate" | "super" | "self")
}
"python" => name.starts_with('.'),
"javascript" | "typescript" | "tsx" => {
let n = name.trim_matches(|c| c == '"' || c == '\'');
n.starts_with("./") || n.starts_with("../")
}
_ => false,
}
}
pub fn not_navigational(column: &str) -> String {
let holes = NAVIGATIONAL_KINDS
.iter()
.map(|k| format!("'{k}'"))
.collect::<Vec<_>>()
.join(", ");
format!("{column} NOT IN ({holes})")
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct Step {
pub from: String,
pub from_path: String,
pub from_line: u32,
pub to: String,
pub to_path: String,
pub to_line: u32,
pub kind: String,
pub confidence: String,
pub definitions: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub call_line: Option<u32>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct Chain {
pub steps: Vec<Step>,
pub summary: Summary,
}
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct Summary {
pub hops: usize,
pub extracted: usize,
pub resolved: usize,
pub inferred: usize,
pub ambiguous: usize,
pub seams: usize,
pub ambiguous_names: Vec<(String, usize)>,
pub hypothesis: bool,
}
impl Summary {
fn of(steps: &[Step]) -> Self {
let mut summary = Summary {
hops: steps.len(),
..Default::default()
};
for step in steps {
match step.confidence.as_str() {
EXTRACTED => summary.extracted += 1,
RESOLVED => summary.resolved += 1,
AMBIGUOUS => summary.ambiguous += 1,
_ => summary.inferred += 1,
}
}
for pair in steps.windows(2) {
let (before, after) = (&pair[0], &pair[1]);
if (&before.to_path, before.to_line) == (&after.from_path, after.from_line) {
continue;
}
summary.seams += 1;
let name = before.to.clone();
if !summary.ambiguous_names.iter().any(|(n, _)| *n == name) {
summary
.ambiguous_names
.push((name, before.definitions.max(2)));
}
}
summary.hypothesis = summary.seams > 0
|| (!steps.is_empty()
&& steps
.iter()
.all(|s| s.confidence == INFERRED || s.confidence == AMBIGUOUS));
summary
}
}
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct Neighbours {
pub callers: Vec<crate::store::EdgeEnd>,
pub callees: Vec<crate::store::EdgeEnd>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub unresolved: Vec<crate::store::Unresolved>,
#[serde(skip_serializing_if = "is_zero")]
pub hidden: usize,
}
fn is_zero(n: &usize) -> bool {
*n == 0
}
pub fn collapse(callees: Vec<crate::store::EdgeEnd>) -> Vec<crate::store::EdgeEnd> {
let mut seen: std::collections::HashSet<(String, String)> = std::collections::HashSet::new();
callees
.into_iter()
.filter(|end| {
if end.confidence != AMBIGUOUS {
return true;
}
seen.insert((end.symbol.name.clone(), end.kind.clone()))
})
.collect()
}
impl Chain {
pub fn render(&self, bold: &str, reset: &str, shorten: &dyn Fn(&str) -> String) -> String {
let mut out = String::new();
let width = self
.steps
.iter()
.map(|s| {
endpoint(&s.from, &shorten(&s.from_path), s.from_line)
.chars()
.count()
})
.max()
.unwrap_or(0);
for (i, step) in self.steps.iter().enumerate() {
let left = endpoint(&step.from, &shorten(&step.from_path), step.from_line);
let right = endpoint(&step.to, &shorten(&step.to_path), step.to_line);
let pad = " ".repeat(width.saturating_sub(left.chars().count()));
out.push_str(&format!(
"{bold}{}{reset} {left}{pad} → {right} {} · {}\n",
i + 1,
step.kind,
step.confidence,
));
if let Some(next) = self.steps.get(i + 1)
&& (&step.to_path, step.to_line) != (&next.from_path, next.from_line)
{
out.push_str(&format!(
" ── seam · {}: {} definitions · continues from {}\n",
step.to,
step.definitions.max(2),
endpoint(&next.from, &shorten(&next.from_path), next.from_line),
));
}
}
out.push_str(&self.trailer());
out.push('\n');
if self.summary.hypothesis {
out.push_str("A hypothesis, not a finding.\n");
}
out
}
pub fn trailer(&self) -> String {
let s = &self.summary;
let mut line = format!(
"{} hop{} · {} extracted · {} resolved · {} inferred · {} ambiguous",
s.hops,
if s.hops == 1 { "" } else { "s" },
s.extracted,
s.resolved,
s.inferred,
s.ambiguous,
);
if s.seams > 0 {
let names = s
.ambiguous_names
.iter()
.map(|(name, count)| format!("{name}: {count} definitions"))
.collect::<Vec<_>>()
.join(", ");
line.push_str(&format!(" · {} through ambiguous names ({names})", s.seams));
}
line
}
}
pub fn verbatim(path: &str) -> String {
path.to_string()
}
pub fn call_site(end: &crate::store::EdgeEnd, shorten: &dyn Fn(&str) -> String) -> String {
let means = match &end.means {
Some(m) => format!(" → {m}"),
None => String::new(),
};
let Some(line) = end.line else {
return means;
};
let what = if matches!(end.kind.as_str(), "defines" | "contains") {
("defined at", "defined on line")
} else {
("called at", "call at line")
};
match &end.from_path {
Some(path) => format!(" · {} {}:{line}{means}", what.0, shorten(path)),
None => format!(" · {} {line}{means}", what.1),
}
}
pub fn definitions_word(n: usize) -> String {
format!("{n} definition{}", if n == 1 { "" } else { "s" })
}
pub fn render_end(end: &crate::store::EdgeEnd, shorten: &dyn Fn(&str) -> String) -> String {
let name = if end.symbol.kind == "module" {
format!("{} (module)", end.symbol.name)
} else {
end.symbol.name.clone()
};
let site = call_site(end, shorten);
if end.confidence == AMBIGUOUS && end.from_path.is_some() {
return format!(
" {name} via {} ({}) · {}{site}",
end.kind,
end.confidence,
definitions_word(end.definitions),
);
}
let store = match &end.symbol.store {
Some(label) => format!("[{label}] "),
None => String::new(),
};
format!(
" {name} via {} ({}) {store}{}:{}{site}",
end.kind,
end.confidence,
shorten(&end.symbol.path),
end.symbol.start_line,
)
}
fn endpoint(name: &str, path: &str, line: u32) -> String {
if path.is_empty() {
return name.to_string();
}
format!("{name} @ {path}:{line}")
}
pub fn not_connected(from: &str, to: &str, depth: u32, all_edges: bool) -> String {
if all_edges {
format!("{from} and {to} are not connected within {depth} hops by any edge in the store.")
} else {
format!(
"{from} and {to} are not connected within {depth} hops by resolved edges. \
Ambiguous names were not crossed; --all-edges walks them and labels what it finds."
)
}
}
#[derive(Debug, Default, Clone)]
pub struct Scope {
groups: Vec<Vec<String>>,
paths: Option<std::collections::HashSet<String>>,
}
impl Scope {
pub fn all() -> Self {
Self::default()
}
pub fn of(db: &rusqlite::Connection, filter: &crate::filter::Filter) -> Result<Self> {
if filter.is_empty() {
return Ok(Self::all());
}
let paths = crate::store::filtered_paths(db, filter.groups())?;
Ok(Self {
groups: filter.groups().to_vec(),
paths: Some(paths.into_iter().collect()),
})
}
pub fn holds(&self, path: &str) -> bool {
self.paths.as_ref().is_none_or(|p| p.contains(path))
}
fn definitions(
&self,
db: &rusqlite::Connection,
name: &str,
limit: usize,
) -> Result<Vec<crate::store::SymbolRow>> {
Ok(self.definitions_counted(db, name, limit)?.0)
}
fn definitions_counted(
&self,
db: &rusqlite::Connection,
name: &str,
limit: usize,
) -> Result<(Vec<crate::store::SymbolRow>, usize)> {
if self.paths.is_none() {
return crate::store::symbols_named_counted(db, name, limit);
}
let mut rows = crate::store::symbols_by_names(
db,
std::slice::from_ref(&name.to_string()),
&self.groups,
)?;
rows.sort_by_key(|r| NAVIGATIONAL_KINDS.contains(&r.kind.as_str()));
let total = rows.len();
rows.truncate(limit);
Ok((rows, total))
}
}
pub fn neighbours(
db: &rusqlite::Connection,
name: &str,
kinds: &[String],
all: bool,
scope: &Scope,
) -> Result<Neighbours> {
let (qualifier, bare) = split_qualified(name);
let definitions = scope.definitions(db, bare, 64)?;
let scoped = scope.paths.is_some();
let defined: Vec<i64> = definitions.iter().map(|d| d.id).collect();
let owned: Vec<&crate::store::SymbolRow> = match qualifier {
Some(q) => definitions.iter().filter(|d| owned_by(d, q)).collect(),
None => Vec::new(),
};
let owned_ids: Vec<i64> = owned.iter().map(|d| d.id).collect();
let mut callers = Vec::new();
for incoming in crate::store::edges_in_resolved(db, bare, kinds)? {
if !owned_ids.is_empty() && !incoming.means.iter().any(|m| owned_ids.contains(m)) {
continue;
}
if scoped
&& (!scope.holds(&incoming.end.symbol.path)
|| !incoming.means.iter().any(|m| defined.contains(m)))
{
continue;
}
let mut end = incoming.end;
if end.definitions > 1 && incoming.means.len() == 1 {
end.means = definitions
.iter()
.find(|d| d.id == incoming.means[0])
.map(|d| d.qualified.clone());
}
callers.push(end);
}
let mut callees = crate::store::edges_out(db, bare, kinds)?;
if scoped {
callees.retain(|e| {
e.from_path.as_deref().is_some_and(|p| scope.holds(p)) && scope.holds(&e.symbol.path)
});
}
if !owned.is_empty() {
callees.retain(|e| {
owned.iter().any(|d| {
e.from_path.as_deref() == Some(d.path.as_str()) && e.from_line == Some(d.start_line)
})
});
}
let unresolved = crate::store::unresolved_out(db, bare, kinds)?;
Ok(Neighbours {
callers,
callees: if all { callees } else { collapse(callees) },
hidden: if all { 0 } else { unresolved.len() },
unresolved: if all { unresolved } else { Vec::new() },
})
}
pub const NAMES_LIMIT: usize = 20;
#[derive(Debug, Clone, serde::Serialize)]
pub struct Signature {
pub asked: String,
#[serde(flatten)]
pub symbol: crate::store::SymbolRow,
pub signature: String,
}
pub fn signatures(
db: &rusqlite::Connection,
names: &[String],
scope: &Scope,
) -> Result<Vec<Signature>> {
let mut out = Vec::new();
for asked in names.iter().take(NAMES_LIMIT) {
let (qualifier, bare) = split_qualified(asked);
let mut rows = scope.definitions(db, bare, 64)?;
if let Some(q) = qualifier
&& rows.iter().any(|r| owned_by(r, q))
{
rows.retain(|r| owned_by(r, q));
}
for symbol in rows {
let signature = crate::store::chunks_overlapping(
db,
&symbol.path,
symbol.start_line,
symbol.start_line,
)?
.iter()
.find_map(|c| {
let offset = symbol.start_line.checked_sub(c.start_line)? as usize;
c.text.lines().nth(offset).map(|l| l.trim().to_string())
})
.unwrap_or_default();
out.push(Signature {
asked: asked.clone(),
symbol,
signature,
});
}
}
Ok(out)
}
pub fn render_signatures(
rows: &[Signature],
asked: &[String],
shorten: &dyn Fn(&str) -> String,
) -> String {
const WIDTH: usize = 110;
let mut out = String::new();
for row in rows {
let mut signature = row.signature.clone();
if signature.chars().count() > WIDTH {
signature = signature.chars().take(WIDTH).collect::<String>() + "\u{2026}";
}
let store = match &row.symbol.store {
Some(s) => format!("{s} "),
None => String::new(),
};
out.push_str(&format!(
"{} {} {store}{}:{}-{} {}\n",
row.symbol.qualified,
row.symbol.kind,
shorten(&row.symbol.path),
row.symbol.start_line,
row.symbol.end_line,
signature
));
}
let missing: Vec<&str> = asked
.iter()
.filter(|n| !rows.iter().any(|r| r.asked == **n))
.map(String::as_str)
.collect();
if !missing.is_empty() {
out.push_str(&format!("no definition: {}\n", missing.join(", ")));
}
out.trim_end().to_string()
}
pub fn fit(text: String, narrow: &str) -> String {
if text.len() <= ANSWER_CHARS {
return text;
}
let lines: Vec<&str> = text.lines().collect();
let mut out = String::new();
let mut kept = 0;
for line in &lines {
if out.len() + line.len() + 1 + 2_000 > ANSWER_CHARS {
break;
}
out.push_str(line);
out.push('\n');
kept += 1;
}
let rest = &lines[kept..];
let mut by_file: Vec<(String, usize)> = Vec::new();
for line in rest {
let file = line
.split_whitespace()
.find_map(|word| {
let (path, number) = word.rsplit_once(':')?;
let number = number.split('-').next()?;
(!path.is_empty() && number.parse::<u32>().is_ok()).then(|| path.to_string())
})
.unwrap_or_else(|| "(no path)".to_string());
match by_file.iter_mut().find(|(f, _)| *f == file) {
Some((_, n)) => *n += 1,
None => by_file.push((file, 1)),
}
}
out.push_str(&format!("\n{} more lines, by file\n", rest.len()));
let mut left = by_file.len();
for (file, n) in &by_file {
let line = format!(" {file} · {n}\n");
if out.len() + line.len() + 300 > ANSWER_CHARS {
out.push_str(&format!(" … {left} more files\n"));
break;
}
out.push_str(&line);
left -= 1;
}
out.push_str(&format!(
"more: {} lines past the {ANSWER_CHARS}-character cap; {narrow}",
rest.len()
));
out
}
const EGO_LIMIT: usize = 12;
#[derive(Debug, Clone, serde::Serialize)]
pub struct Hop {
pub via: String,
pub name: String,
pub kind: String,
pub confidence: String,
pub direction: &'static str,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct Evidence {
pub name: String,
pub definitions: Vec<crate::store::SymbolRow>,
pub total: usize,
pub callers: Vec<crate::store::EdgeEnd>,
pub callees: Vec<crate::store::EdgeEnd>,
pub ego: Vec<Hop>,
}
pub fn evidence(
db: &rusqlite::Connection,
name: &str,
kinds: &[String],
limit: usize,
all: bool,
scope: &Scope,
) -> Result<Evidence> {
let (qualifier, bare) = split_qualified(name);
let (mut definitions, mut total) = scope.definitions_counted(db, bare, limit)?;
if let Some(q) = qualifier
&& definitions.iter().any(|d| owned_by(d, q))
{
let (every, _) = scope.definitions_counted(db, bare, usize::MAX)?;
let owned: Vec<_> = every.into_iter().filter(|d| owned_by(d, q)).collect();
total = owned.len();
definitions = owned.into_iter().take(limit).collect();
}
let ring = neighbours(db, name, kinds, all, scope)?;
let mut ego: Vec<Hop> = Vec::new();
let settled: Vec<(&str, &crate::store::EdgeEnd)> = ring
.callers
.iter()
.map(|e| ("caller", e))
.chain(ring.callees.iter().map(|e| ("callee", e)))
.filter(|(_, e)| e.confidence == EXTRACTED || e.confidence == RESOLVED)
.collect();
for (_, first) in &settled {
if ego.len() >= EGO_LIMIT {
break;
}
let out = crate::store::edges_out(db, &first.symbol.name, kinds)?;
let into = crate::store::edges_in(db, &first.symbol.name, kinds)?;
for (direction, end) in out
.into_iter()
.map(|e| ("callee", e))
.chain(into.into_iter().map(|e| ("caller", e)))
{
if ego.len() >= EGO_LIMIT {
break;
}
if !scope.holds(&end.symbol.path) {
continue;
}
if end.symbol.name == bare
|| ring
.callers
.iter()
.chain(ring.callees.iter())
.any(|e| e.symbol.name == end.symbol.name)
|| ego.iter().any(|h| h.name == end.symbol.name)
{
continue;
}
ego.push(Hop {
via: first.symbol.name.clone(),
name: end.symbol.name,
kind: end.kind,
confidence: end.confidence,
direction,
});
}
}
Ok(Evidence {
name: name.to_string(),
definitions,
total,
callers: ring.callers,
callees: ring.callees,
ego,
})
}
impl Evidence {
pub fn render(&self, bold: &str, reset: &str, shorten: &dyn Fn(&str) -> String) -> String {
let mut out = String::new();
for symbol in &self.definitions {
out.push_str(&format!(
"{bold}{}{reset} {} {}:{}-{}\n",
symbol.name,
symbol.kind,
shorten(&symbol.path),
symbol.start_line,
symbol.end_line,
));
}
let total = self.total.max(self.definitions.len());
if total > self.definitions.len() {
out.push_str(&format!(
"{total} definitions of this name, {} shown; k {total} lists every one\n",
self.definitions.len()
));
} else if total > 1 {
out.push_str(&format!("{total} definitions of this name\n"));
}
for (heading, ends) in [("callers", &self.callers), ("callees", &self.callees)] {
out.push_str(&format!("\n{bold}{heading}{reset} ({})\n", ends.len()));
if ends.is_empty() {
out.push_str(" none\n");
}
for end in ends.iter() {
out.push_str(&render_end(end, shorten));
out.push('\n');
}
}
if !self.ego.is_empty() {
out.push_str(&format!("\n{bold}two hops out{reset}\n"));
for hop in &self.ego {
out.push_str(&format!(
" {} · {} of {} ({})\n",
hop.name, hop.direction, hop.via, hop.confidence,
));
}
}
out.trim_end().to_string()
}
}
pub fn shortest_path(
db: &rusqlite::Connection,
from: &str,
to: &str,
depth: u32,
all_edges: bool,
) -> Result<Option<Chain>> {
if from == to {
return Ok(Some(Chain {
steps: Vec::new(),
summary: Summary::default(),
}));
}
let mut came_from: std::collections::HashMap<Node, Step> = std::collections::HashMap::new();
let mut seen: std::collections::HashSet<Node> = std::collections::HashSet::new();
let start = Node::any(from);
seen.insert(start.clone());
let mut frontier = vec![start];
let kinds = dependency_kinds();
for _ in 0..depth {
let mut level: Vec<(Node, crate::store::EdgeEnd)> = Vec::new();
for current in &frontier {
for edge in crate::store::edges_out(db, ¤t.name, &kinds)? {
if !all_edges {
if edge.confidence == AMBIGUOUS {
continue;
}
if !current.holds(edge.from_path.as_deref(), edge.from_line) {
continue;
}
}
level.push((current.clone(), edge));
}
}
level.sort_by_key(|(_, edge)| confidence_rank(&edge.confidence));
let mut next = Vec::new();
for (current, edge) in level {
let reached = if all_edges {
Node::any(&edge.symbol.name)
} else {
Node {
name: edge.symbol.name.clone(),
path: edge.symbol.path.clone(),
line: edge.symbol.start_line,
}
};
if !seen.insert(reached.clone()) {
continue;
}
came_from.insert(
reached.clone(),
Step {
from: current.name.clone(),
from_path: edge.from_path.unwrap_or_default(),
from_line: edge.from_line.unwrap_or(0),
to: reached.name.clone(),
to_path: edge.symbol.path,
to_line: edge.symbol.start_line,
kind: edge.kind,
confidence: edge.confidence,
definitions: edge.definitions,
call_line: edge.line,
},
);
if reached.name == to {
let steps = unwind(&came_from, from, &reached);
let summary = Summary::of(&steps);
return Ok(Some(Chain { steps, summary }));
}
next.push(reached);
if seen.len() >= MAX_NODES {
return Ok(None);
}
}
if next.is_empty() {
break;
}
frontier = next;
}
Ok(None)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Node {
name: String,
path: String,
line: u32,
}
impl Node {
fn any(name: &str) -> Self {
Node {
name: name.to_string(),
path: String::new(),
line: 0,
}
}
fn holds(&self, path: Option<&str>, line: Option<u32>) -> bool {
if self.path.is_empty() {
return true;
}
match (path, line) {
(Some(path), Some(line)) => path == self.path && line == self.line,
_ => true,
}
}
}
const SEED_CANDIDATES: usize = 40;
const DRAW_HUB: usize = 32;
fn busiest(stores: &[(&str, &rusqlite::Connection)]) -> Result<Option<String>> {
type Chosen = (String, (i64, i64), Option<String>);
static CHOSEN: std::sync::Mutex<Vec<Chosen>> = std::sync::Mutex::new(Vec::new());
let mut key = String::new();
let mut stats = (0i64, 0i64);
for (_, db) in stores {
key.push_str(db.path().unwrap_or(""));
key.push('|');
let (a, b) = crate::store::graph_stats(db)?;
stats = (stats.0 + a, stats.1 + b);
}
if let Some((_, _, name)) = CHOSEN
.lock()
.unwrap_or_else(|e| e.into_inner())
.iter()
.find(|(k, st, _)| *k == key && *st == stats)
{
return Ok(name.clone());
}
let name = busiest_uncached(stores)?;
let mut chosen = CHOSEN.lock().unwrap_or_else(|e| e.into_inner());
chosen.retain(|(k, _, _)| *k != key);
chosen.push((key, stats, name.clone()));
Ok(name)
}
fn busiest_uncached(stores: &[(&str, &rusqlite::Connection)]) -> Result<Option<String>> {
let mut best: Option<((usize, usize, usize), String)> = None;
for (_, db) in stores {
let db: &rusqlite::Connection = db;
for name in crate::store::busiest_names(db, SEED_CANDIDATES * 4)? {
let definitions = crate::store::symbols_named(db, &name, 2)?.len();
if definitions == 0 {
continue;
}
let (resolved, degree) = crate::store::edge_counts(db, &name)?;
if degree == 0 {
continue;
}
let unambiguous = usize::from(definitions <= 1);
let score = (unambiguous, resolved, degree);
if best.as_ref().is_none_or(|(seen, _)| score > *seen) {
best = Some((score, name.clone()));
}
}
}
Ok(best.map(|(_, name)| name))
}
pub fn scoped(
stores: &[(&str, &rusqlite::Connection)],
focus: Option<&str>,
prefix: Option<&str>,
limit: usize,
label_stores: bool,
) -> Result<serde_json::Value> {
let mut nodes: Vec<crate::store::SymbolRow> = Vec::new();
let mut index: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
let mut edges: Vec<serde_json::Value> = Vec::new();
let mut total = 0usize;
let chosen_focus = match (focus, prefix) {
(None, None) => busiest(stores)?,
(focus, _) => focus.map(str::to_string),
};
let focus = chosen_focus.as_deref();
for (label, db) in stores {
let db: &rusqlite::Connection = db;
let picked = match focus {
Some(name) => {
let mut around = crate::store::symbols_named(db, name, 4)?;
let mut ends: Vec<crate::store::EdgeEnd> =
crate::store::edges_in_first(db, name, limit * 2)?
.into_iter()
.chain(crate::store::edges_out_short_of_hubs(
db,
name,
&[],
DRAW_HUB,
)?)
.collect();
ends.sort_by_key(|end| match end.confidence.as_str() {
EXTRACTED | RESOLVED => 0,
AMBIGUOUS => 1,
_ => 2,
});
for end in ends {
if around.len() >= limit {
break;
}
around.push(end.symbol);
}
around
}
None => crate::store::symbols_scoped(db, prefix, limit)?,
};
total += crate::store::graph_stats(db)?.0 as usize;
for mut symbol in picked {
if nodes.len() >= limit {
break;
}
if index.contains_key(&symbol.name) {
continue;
}
if label_stores {
symbol.store = Some((*label).to_string());
}
index.insert(symbol.name.clone(), nodes.len());
nodes.push(symbol);
}
}
let mut seen: std::collections::HashSet<(usize, usize, String)> = Default::default();
let drawn: Vec<String> = index.keys().cloned().collect();
for (from_name, from_index) in &index {
for (_, db) in stores {
for end in crate::store::edges_out_to(db, from_name, &drawn)? {
let Some(to_index) = index.get(&end.symbol.name) else {
continue;
};
if from_index == to_index {
continue;
}
if !seen.insert((*from_index, *to_index, end.kind.clone())) {
continue;
}
edges.push(serde_json::json!({
"from": from_index,
"to": to_index,
"kind": end.kind,
"confidence": end.confidence,
}));
}
}
}
Ok(serde_json::json!({
"nodes": nodes,
"edges": edges,
"total": total,
"shown": nodes.len(),
}))
}
fn unwind(came_from: &std::collections::HashMap<Node, Step>, start: &str, end: &Node) -> Vec<Step> {
let mut chain = Vec::new();
let mut cursor = end.clone();
while cursor.name != start {
let Some(step) = came_from
.get(&cursor)
.or_else(|| came_from.get(&Node::any(&cursor.name)))
else {
break;
};
chain.push(step.clone());
cursor = Node {
name: step.from.clone(),
path: step.from_path.clone(),
line: step.from_line,
};
if cursor.name == start {
break;
}
}
chain.reverse();
chain
}
pub const IMPACT_LIMIT: usize = 400;
pub const ANSWER_CHARS: usize = 16_000;
#[derive(Debug, Clone, serde::Serialize)]
pub struct Reached {
pub name: String,
#[serde(serialize_with = "crate::serialize_plain")]
pub path: String,
pub line: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub at: Option<u32>,
pub hop: u32,
pub kind: String,
pub confidence: String,
pub via: String,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ReachedFile {
#[serde(serialize_with = "crate::serialize_plain")]
pub path: String,
pub symbols: usize,
pub nearest: u32,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct Impact {
pub name: String,
pub depth: u32,
pub all_edges: bool,
pub definitions: Vec<crate::store::SymbolRow>,
pub reached: Vec<Reached>,
pub files: Vec<ReachedFile>,
#[serde(skip_serializing_if = "is_zero")]
pub hidden: usize,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub unqualified: bool,
pub extracted: usize,
pub resolved: usize,
pub inferred: usize,
pub ambiguous: usize,
}
pub fn split_qualified(name: &str) -> (Option<&str>, &str) {
let ident = |t: &str| !t.is_empty() && t.chars().all(|c| c.is_alphanumeric() || c == '_');
let split = name.rsplit_once("::").or_else(|| name.rsplit_once('.'));
match split {
Some((owner, bare)) => {
let last = owner.rsplit("::").next().unwrap_or(owner);
if ident(last) && ident(bare) {
(Some(last), bare)
} else {
(None, name)
}
}
None => (None, name),
}
}
pub fn owned_by(row: &crate::store::SymbolRow, qualifier: &str) -> bool {
crate::store::owner_of(&row.qualified) == Some(qualifier)
|| module_name(Path::new(&row.path)) == qualifier
|| Path::new(&row.path)
.parent()
.is_some_and(|p| p.components().any(|c| c.as_os_str() == qualifier))
}
pub fn impact(
db: &rusqlite::Connection,
name: &str,
kinds: &[String],
depth: u32,
all_edges: bool,
scope: &Scope,
) -> Result<Impact> {
let dependencies = dependency_kinds();
let kinds = if kinds.is_empty() {
dependencies.as_slice()
} else {
kinds
};
let (qualifier, bare) = split_qualified(name);
let mut definitions = scope.definitions(db, bare, 64)?;
let mut unqualified = false;
if let Some(q) = qualifier {
let owned: Vec<_> = definitions
.iter()
.filter(|d| owned_by(d, q))
.cloned()
.collect();
if owned.is_empty() {
unqualified = !definitions.is_empty();
} else {
definitions = owned;
}
}
let mut answer = Impact {
name: name.to_string(),
depth,
all_edges,
definitions,
reached: Vec::new(),
files: Vec::new(),
hidden: 0,
unqualified,
extracted: 0,
resolved: 0,
inferred: 0,
ambiguous: 0,
};
let mut seen: std::collections::HashSet<Node> = std::collections::HashSet::new();
for row in &answer.definitions {
seen.insert(Node {
name: row.name.clone(),
path: row.path.clone(),
line: row.start_line,
});
}
let asked: std::collections::HashSet<i64> = answer.definitions.iter().map(|d| d.id).collect();
let mut incoming: std::collections::HashMap<String, Vec<crate::store::Incoming>> =
std::collections::HashMap::new();
let mut frontier: Vec<crate::store::SymbolRow> = answer.definitions.clone();
for hop in 1..=depth {
let mut next: Vec<crate::store::SymbolRow> = Vec::new();
for target in &frontier {
if !incoming.contains_key(&target.name) {
let found = crate::store::edges_in_resolved(db, &target.name, kinds)?;
incoming.insert(target.name.clone(), found);
}
for edge in &incoming[&target.name] {
if !edge.means.contains(&target.id) {
continue;
}
let within = hop == 1 && edge.means.iter().all(|id| asked.contains(id));
if !all_edges && edge.end.confidence == AMBIGUOUS && !within {
continue;
}
let caller = &edge.end.symbol;
if !scope.holds(&caller.path) {
continue;
}
let node = Node {
name: caller.name.clone(),
path: caller.path.clone(),
line: caller.start_line,
};
if !seen.insert(node) {
continue;
}
if answer.reached.len() >= IMPACT_LIMIT {
answer.hidden += 1;
continue;
}
match edge.end.confidence.as_str() {
EXTRACTED => answer.extracted += 1,
RESOLVED => answer.resolved += 1,
INFERRED => answer.inferred += 1,
_ => answer.ambiguous += 1,
}
next.push(caller.clone());
answer.reached.push(Reached {
name: caller.name.clone(),
path: caller.path.clone(),
line: caller.start_line,
at: edge.end.line,
hop,
kind: edge.end.kind.clone(),
confidence: edge.end.confidence.clone(),
via: target.name.clone(),
});
}
}
if next.is_empty() {
break;
}
frontier = next;
}
answer.files = files_of(&answer.reached);
Ok(answer)
}
pub fn files_of(reached: &[Reached]) -> Vec<ReachedFile> {
let mut by_file: std::collections::BTreeMap<String, (usize, u32)> =
std::collections::BTreeMap::new();
for row in reached {
let entry = by_file.entry(row.path.clone()).or_insert((0, row.hop));
entry.0 += 1;
entry.1 = entry.1.min(row.hop);
}
let mut files: Vec<ReachedFile> = by_file
.into_iter()
.map(|(path, (symbols, nearest))| ReachedFile {
path,
symbols,
nearest,
})
.collect();
files.sort_by(|a, b| {
a.nearest
.cmp(&b.nearest)
.then(b.symbols.cmp(&a.symbols))
.then(a.path.cmp(&b.path))
});
files
}
impl Impact {
pub fn render(&self, bold: &str, reset: &str, shorten: &dyn Fn(&str) -> String) -> String {
let mut out = self.headline();
if self.unqualified {
out.push_str(&format!(
"\nno definition of {} is owned by {}; this is every {}",
split_qualified(&self.name).1,
split_qualified(&self.name).0.unwrap_or_default(),
split_qualified(&self.name).1
));
}
if !self.definitions.is_empty() {
let defs: Vec<String> = self
.definitions
.iter()
.map(|d| format!("{}:{}", shorten(&d.path), d.start_line))
.collect();
out.push_str(&format!("\ndefined at {}", defs.join(", ")));
}
if self.reached.is_empty() {
return out;
}
let files_block = self.files_block(bold, reset, shorten);
let reserve = files_block.len().min(ANSWER_CHARS / 4) + 400;
let mut hop = 0;
let mut shown = 0usize;
let mut undated = 0usize;
for row in &self.reached {
let mut chunk = String::new();
if row.hop != hop {
chunk.push_str(&format!(
"\n\n{bold}{} hop{}{reset}",
row.hop,
if row.hop == 1 { "" } else { "s" }
));
}
let at = match row.at {
Some(at) => format!("{}:{at} in {}", shorten(&row.path), row.name),
None => {
undated += 1;
format!(
"{}:{} {} (definition)",
shorten(&row.path),
row.line,
row.name
)
}
};
chunk.push_str(&format!(
"\n {at} · {} {} · reaches {}",
row.kind, row.confidence, row.via
));
if out.len() + chunk.len() + reserve > ANSWER_CHARS {
break;
}
if row.hop != hop {
hop = row.hop;
}
out.push_str(&chunk);
shown += 1;
}
let collapsed = self.reached.len() - shown;
if collapsed > 0 {
let rest = files_of(&self.reached[shown..]);
out.push_str(&format!("\n\n{bold}{collapsed} more, by file{reset}"));
let mut left = rest.len();
for file in &rest {
let line = format!(
"\n {} · {} · nearest {} hop{}",
shorten(&file.path),
file.symbols,
file.nearest,
if file.nearest == 1 { "" } else { "s" }
);
if out.len() + line.len() + 300 > ANSWER_CHARS {
out.push_str(&format!("\n … {left} more files"));
break;
}
out.push_str(&line);
left -= 1;
}
let nearer = self.reached[shown].hop.saturating_sub(1).max(1);
out.push_str(&format!(
"\nmore: {collapsed} rows past the {ANSWER_CHARS}-character cap; depth: {nearer} lists them, or ask about one of the callers above"
));
} else if out.len() + files_block.len() < ANSWER_CHARS {
out.push_str(&files_block);
}
if undated > 0 {
out.push_str(&format!(
"\n{undated} row{} show{} the caller's definition: this store has no call-site lines for {}; a re-index adds them",
if undated == 1 { "" } else { "s" },
if undated == 1 { "s" } else { "" },
if undated == 1 { "it" } else { "them" }
));
}
if self.hidden > 0 {
out.push_str(&format!(
"\n\n{} more left out at the limit of {IMPACT_LIMIT}",
self.hidden
));
}
out
}
fn files_block(&self, bold: &str, reset: &str, shorten: &dyn Fn(&str) -> String) -> String {
let mut out = format!("\n\n{bold}files{reset}");
for file in &self.files {
out.push_str(&format!(
"\n {} · {} definition{} · nearest {} hop{}",
shorten(&file.path),
file.symbols,
if file.symbols == 1 { "" } else { "s" },
file.nearest,
if file.nearest == 1 { "" } else { "s" }
));
}
out
}
pub fn headline(&self) -> String {
if self.reached.is_empty() {
return format!(
"nothing in this store reaches {} within {} hop{}",
self.name,
self.depth,
if self.depth == 1 { "" } else { "s" }
);
}
let files = self.files.len();
format!(
"{} definition{} in {} file{} reach {} within {} hop{}",
self.reached.len(),
if self.reached.len() == 1 { "" } else { "s" },
files,
if files == 1 { "" } else { "s" },
self.name,
self.depth,
if self.depth == 1 { "" } else { "s" }
)
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct Support {
pub hop: String,
pub at: String,
pub code: String,
pub mark: &'static str,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct Trace {
pub from: String,
pub to: String,
pub depth: u32,
pub all_edges: bool,
pub answer: String,
pub chain: Option<Chain>,
pub lines: Vec<Support>,
}
pub fn support_mark(confidence: &str) -> &'static str {
match confidence {
EXTRACTED | RESOLVED => "supporting fact",
_ => "candidate — corroborate before use",
}
}
pub fn trace(
from: &str,
to: &str,
depth: u32,
all_edges: bool,
chain: Option<Chain>,
mut line_text: impl FnMut(&str, u32) -> Option<String>,
shorten: &dyn Fn(&str) -> String,
) -> Trace {
let Some(chain) = chain else {
return Trace {
from: from.to_string(),
to: to.to_string(),
depth,
all_edges,
answer: not_connected(from, to, depth, all_edges),
chain: None,
lines: Vec::new(),
};
};
let s = &chain.summary;
let answer = if chain.steps.is_empty() {
format!("{from} is {to}.")
} else if s.hypothesis && s.seams > 0 {
format!(
"Not connected by resolved edges. The nearest chain is {} hop{} long and crosses {} ambiguous name{} — treat it as a hypothesis.",
s.hops,
if s.hops == 1 { "" } else { "s" },
s.seams,
if s.seams == 1 { "" } else { "s" }
)
} else if s.hypothesis {
format!(
"Not connected by resolved edges. The nearest chain is {} hop{} long and every hop was matched by bare name — treat it as a hypothesis.",
s.hops,
if s.hops == 1 { "" } else { "s" }
)
} else {
format!(
"Connected in {} hop{}: {} extracted, {} resolved. Every hop names its target through something the extractor read or the ranking settled.",
s.hops,
if s.hops == 1 { "" } else { "s" },
s.extracted,
s.resolved
)
};
let lines = chain
.steps
.iter()
.map(|step| {
let (at, code) = match step.call_line {
Some(line) if !step.from_path.is_empty() => (
format!("{}:{line}", shorten(&step.from_path)),
line_text(&step.from_path, line).unwrap_or_default(),
),
_ => (String::new(), String::new()),
};
Support {
hop: format!("{} → {} · {}", step.from, step.to, step.kind),
at,
code,
mark: support_mark(&step.confidence),
}
})
.collect();
Trace {
from: from.to_string(),
to: to.to_string(),
depth,
all_edges,
answer,
chain: Some(chain),
lines,
}
}
impl Trace {
pub fn render(&self, bold: &str, reset: &str, shorten: &dyn Fn(&str) -> String) -> String {
let mut out = format!("{bold}answer{reset}\n{}\n", self.answer);
let Some(chain) = &self.chain else {
return out;
};
if chain.steps.is_empty() {
return out;
}
out.push_str(&format!("\n{bold}chain{reset}\n"));
out.push_str(&chain.render(bold, reset, shorten));
out.push_str(&format!("\n{bold}supporting lines{reset}\n"));
for line in &self.lines {
if line.at.is_empty() {
out.push_str(&format!(
" {} · no call line recorded [{}]\n",
line.hop, line.mark
));
continue;
}
out.push_str(&format!(" {} {}\n", line.at, line.code));
out.push_str(&format!(" {} [{}]\n", line.hop, line.mark));
}
out
}
pub fn evidence(&self, shorten: &dyn Fn(&str) -> String) -> String {
let mut out = format!("{} → {}\nanswer: {}\n", self.from, self.to, self.answer);
let Some(chain) = &self.chain else {
return out;
};
for (i, step) in chain.steps.iter().enumerate() {
out.push_str(&format!(
"{}. {} @ {}:{} -> {} @ {}:{} [{}]",
i + 1,
step.from,
shorten(&step.from_path),
step.from_line,
step.to,
shorten(&step.to_path),
step.to_line,
step.confidence,
));
if let Some(next) = chain.steps.get(i + 1)
&& (&step.to_path, step.to_line) != (&next.from_path, next.from_line)
{
out.push_str(&format!(
" seam: {}: {} definitions · continues from {} @ {}:{}",
step.to,
step.definitions.max(2),
next.from,
shorten(&next.from_path),
next.from_line,
));
}
out.push('\n');
}
if !self.lines.is_empty() {
out.push_str("\nsupporting lines\n");
for line in &self.lines {
let mark = if line.mark.starts_with("candidate") {
"candidate"
} else {
"supporting fact"
};
if line.at.is_empty() {
out.push_str(&format!(
"{} no call line recorded [{mark}]\n",
line.hop
));
} else {
out.push_str(&format!("{} {} [{mark}]\n", line.at, line.code));
}
}
}
out
}
}
pub const COMMUNITY_EDGES: usize = 20_000;
const LABEL_ROUNDS: usize = 8;
pub const COMMUNITIES_SHOWN: usize = 12;
#[derive(Debug, Clone, serde::Serialize)]
pub struct Hub {
pub name: String,
#[serde(serialize_with = "crate::serialize_plain")]
pub path: String,
pub line: u32,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct Community {
pub label: String,
pub size: usize,
pub hubs: Vec<Hub>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cross: Option<(String, usize)>,
}
pub fn communities(edges: &[(String, String)]) -> Vec<Community> {
use std::collections::{BTreeMap, BTreeSet};
let mut neighbours: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
for (from, to) in edges {
neighbours
.entry(from.as_str())
.or_default()
.insert(to.as_str());
neighbours
.entry(to.as_str())
.or_default()
.insert(from.as_str());
}
if neighbours.is_empty() {
return Vec::new();
}
let mut label: BTreeMap<&str, &str> = neighbours.keys().map(|n| (*n, *n)).collect();
for _ in 0..LABEL_ROUNDS {
let mut moved = false;
for (node, around) in &neighbours {
let mut tally: BTreeMap<&str, usize> = BTreeMap::new();
for other in around {
*tally.entry(label[other]).or_default() += 1;
}
let Some(&top) = tally.values().max() else {
continue;
};
let mine = label[node];
if tally.get(mine).copied().unwrap_or(0) == top {
continue;
}
let Some((&best, _)) = tally
.iter()
.filter(|(_, n)| **n == top)
.min_by_key(|(name, _)| **name)
else {
continue;
};
if mine != best {
label.insert(node, best);
moved = true;
}
}
if !moved {
break;
}
}
let mut members: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for (node, group) in &label {
members.entry(group).or_default().push(node);
}
let mut between: BTreeMap<(&str, &str), usize> = BTreeMap::new();
for (from, to) in edges {
let (a, b) = (label[from.as_str()], label[to.as_str()]);
if a != b {
*between.entry((a, b)).or_default() += 1;
}
}
let out: Vec<(&str, Community)> = members
.into_iter()
.map(|(group, names)| {
let inside: BTreeSet<&str> = names.iter().copied().collect();
let mut ranked: Vec<(usize, &str)> = names
.iter()
.map(|name| {
let degree = neighbours
.get(name)
.map(|around| around.iter().filter(|o| inside.contains(*o)).count())
.unwrap_or(0);
(degree, *name)
})
.collect();
ranked.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(b.1)));
let cross = between
.iter()
.filter(|((a, _), _)| *a == group)
.max_by(|a, b| a.1.cmp(b.1).then_with(|| b.0.1.cmp(a.0.1)))
.map(|((_, b), n)| ((*b).to_string(), *n));
(
group,
Community {
label: ranked
.first()
.map(|(_, name)| (*name).to_string())
.unwrap_or_else(|| group.to_string()),
size: names.len(),
hubs: ranked
.iter()
.take(3)
.map(|(_, name)| Hub {
name: (*name).to_string(),
path: String::new(),
line: 0,
})
.collect(),
cross,
},
)
})
.collect();
let names: std::collections::BTreeMap<&str, String> = out
.iter()
.map(|(group, community)| (*group, community.label.clone()))
.collect();
let mut out: Vec<Community> = out
.into_iter()
.map(|(_, mut community)| {
community.cross = community
.cross
.map(|(group, n)| (names.get(group.as_str()).cloned().unwrap_or(group), n));
community
})
.collect();
out.sort_by(|a, b| b.size.cmp(&a.size).then_with(|| a.label.cmp(&b.label)));
out
}
#[cfg(test)]
mod nesting_tests {
use super::*;
#[test]
fn the_four_crashing_shapes_are_too_nested_and_real_files_are_not() {
let d = 260;
let quotes = "> ".repeat(d) + "x\n";
let list: String = (0..d).map(|i| " ".repeat(i) + "- a\n").collect();
let seq: String = (0..d).map(|i| " ".repeat(i) + "-\n").collect();
let flat = "- ".repeat(d) + "x\n";
assert!(too_nested("markdown", "es));
assert!(too_nested("markdown", &list));
assert!(too_nested("yaml", &seq));
assert!(too_nested("yaml", &flat));
let list: String = (0..40).map(|i| " ".repeat(i) + "- a\n").collect();
assert!(!too_nested("markdown", &list));
assert!(!too_nested(
"markdown",
"> a quote\n>> a reply\n- item\n - sub\n"
));
assert!(
!too_nested("rust", "es),
"only the block grammars are capped"
);
}
}
#[cfg(test)]
mod tests {
#[test]
fn communities_are_the_same_twice_and_name_what_joins_them() {
let edges: Vec<(String, String)> = [
("store_open", "store_read"),
("store_read", "store_write"),
("store_write", "store_open"),
("http_get", "http_parse"),
("http_parse", "http_send"),
("http_send", "http_get"),
("http_parse", "store_read"),
]
.iter()
.map(|(a, b)| (a.to_string(), b.to_string()))
.collect();
let first = communities(&edges);
let second = communities(&edges);
assert_eq!(first.len(), 2, "two clusters, one bridge: {first:?}");
for (a, b) in first.iter().zip(second.iter()) {
assert_eq!(a.label, b.label, "the label moved between two runs");
assert_eq!(a.size, b.size);
let names =
|c: &Community| -> Vec<String> { c.hubs.iter().map(|h| h.name.clone()).collect() };
assert_eq!(names(a), names(b), "the hubs moved between two runs");
}
for community in &first {
assert_eq!(community.size, 3, "{community:?}");
assert!(!community.hubs.is_empty(), "a community with no hub");
}
let crossings: Vec<_> = first.iter().filter_map(|c| c.cross.clone()).collect();
assert_eq!(crossings.len(), 1, "{first:?}");
assert_eq!(crossings[0].1, 1, "one edge joins them");
assert!(
first.iter().any(|c| c.label == crossings[0].0),
"a cross edge must name another community's label: {first:?}"
);
}
#[test]
fn a_graph_with_no_edges_has_no_communities() {
assert!(communities(&[]).is_empty());
}
use super::*;
#[test]
fn a_wide_character_after_a_digit_is_kept_from_a_narrow_ctype_scanner() {
let md = " 1\u{5641A}\u{FFFD}";
let safe = ctype_safe("markdown", md);
assert_eq!(safe.len(), md.len());
assert_eq!(safe, " 1aaaa\u{FFFD}");
assert_eq!(
ctype_safe("bash", "echo {é..9} {\u{1F600}"),
"echo {é..9} {aaaa"
);
assert!(matches!(
ctype_safe("markdown", "1. café 2026 \u{1F600}"),
std::borrow::Cow::Borrowed(_)
));
assert!(matches!(
ctype_safe("rust", md),
std::borrow::Cow::Borrowed(_)
));
let g = extract(Path::new("a.md"), "# 1\u{1F600} heading\n\nbody\n")
.unwrap()
.unwrap();
assert!(
g.symbols.iter().any(|s| s.name.contains('\u{1F600}')),
"{:?}",
g.symbols
);
}
#[test]
fn nesting_that_continues_past_a_control_character_is_too_deep() {
let line = format!("{}{}{}", ">".repeat(100), "\0".repeat(100), ">".repeat(130));
assert!(too_nested("markdown", &line));
assert!(!too_nested("markdown", &format!("{}\0text", ">".repeat(5))));
}
fn contained_pairwise(defs: &[Def]) -> Vec<bool> {
defs.iter()
.map(|inner| {
defs.iter().any(|outer| {
outer.name == inner.name
&& outer.kind == inner.kind
&& (outer.start, outer.end) != (inner.start, inner.end)
&& outer.start <= inner.start
&& outer.end >= inner.end
})
})
.collect()
}
fn innermost_pairwise(defs: &[Def], skip: Option<usize>, at: usize) -> Option<usize> {
let mut best: Option<usize> = None;
for (i, d) in defs.iter().enumerate() {
if Some(i) == skip || d.start > at || d.end <= at {
continue;
}
if best.is_none_or(|b| d.end - d.start < defs[b].end - defs[b].start) {
best = Some(i);
}
}
best
}
#[test]
fn the_sweeps_answer_what_the_pairwise_passes_did() {
let mut seed = 0x9e37_79b9_7f4a_7c15_u64;
let mut next = |n: u64| {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed % n) as usize
};
for _ in 0..300 {
let count = 1 + next(40);
let defs: Vec<Def> = (0..count)
.map(|_| {
let start = next(200);
let end = start + 1 + next(80);
Def {
kind: ["function", "class"][next(2)].to_string(),
name: ["a", "b", "c"][next(3)].to_string(),
owner: None,
start,
end,
start_line: 1,
end_line: 1,
}
})
.collect();
assert_eq!(contained(&defs), contained_pairwise(&defs));
let each = enclosing_each(&defs);
for (i, d) in defs.iter().enumerate() {
assert_eq!(each[i], innermost_pairwise(&defs, Some(i), d.start));
}
let at: Vec<usize> = (0..60).map(|_| next(300)).collect();
let got = innermost_at(&defs, at.iter().copied(), None);
for (q, pos) in at.iter().enumerate() {
assert_eq!(got[q], innermost_pairwise(&defs, None, *pos));
}
}
}
use std::path::PathBuf;
fn run(name: &str, src: &str) -> Extraction {
extract(&PathBuf::from(name), src)
.expect("extraction does not fail")
.expect("the extension carries a grammar")
}
fn has_edge(e: &Extraction, from: &str, to: &str, kind: &str) -> bool {
e.edges
.iter()
.any(|x| x.from == from && x.to == to && x.kind == kind)
}
fn confidence_of(e: &Extraction, to: &str, kind: &str) -> String {
e.edges
.iter()
.find(|x| x.to == to && x.kind == kind)
.map(|x| x.confidence.clone())
.unwrap_or_else(|| panic!("no {kind} edge to {to} in {:?}", e.edges))
}
#[test]
fn a_macro_invocation_is_recorded_under_the_name_the_source_writes() {
let e = run(
"a.rs",
"fn go() { let s = format!(\"{}\", 1); println!(\"{s}\"); helper(); }\nfn helper() {}\n",
);
assert!(
!has_edge(&e, "go", "format", "calls"),
"a `format!` is not a call to `fn format`: {:?}",
e.edges
);
assert!(has_edge(&e, "go", "format!", "calls"), "{:?}", e.edges);
assert!(has_edge(&e, "go", "println!", "calls"), "{:?}", e.edges);
assert!(
has_edge(&e, "go", "helper", "calls"),
"an ordinary call is untouched: {:?}",
e.edges
);
}
#[test]
fn a_call_inside_a_macro_argument_is_a_call() {
let e = run(
"a.rs",
"#[cfg(not(test))]\nfn go(x: X) {\n let v = json!({ \"x\": f(1), \"y\": vec![1] });\n \
let s = format!(\"{}\", a::g(2));\n assert!(x.h());\n}\n",
);
let only = |to: &str| -> Vec<&Edge> {
e.edges
.iter()
.filter(|x| x.from == "go" && x.to == to && x.kind == "calls")
.collect()
};
assert_eq!(only("f").len(), 1, "{:?}", e.edges);
let g = only("g");
assert_eq!(g.len(), 1, "{:?}", e.edges);
assert_eq!(g[0].hint.as_deref(), Some("a"), "{:?}", e.edges);
let h = only("h");
assert_eq!(h.len(), 1, "{:?}", e.edges);
assert!(
h[0].hint
.as_deref()
.is_some_and(|x| x.starts_with(RECEIVER)),
"a method call inside a macro is still a method call: {:?}",
e.edges
);
for name in ["json", "vec", "format", "assert"] {
assert!(
!has_edge(&e, "go", name, "calls"),
"a macro name is not a call to `fn {name}`: {:?}",
e.edges
);
}
assert!(has_edge(&e, "go", "json!", "calls"), "{:?}", e.edges);
assert!(
!e.edges.iter().any(|x| x.to == "not"),
"an attribute's arguments call nothing: {:?}",
e.edges
);
}
#[test]
fn a_caller_inside_json_is_a_neighbour_and_an_impact() {
let db = rusqlite::Connection::open_in_memory().unwrap();
crate::store::prepare_for_tests(&db);
for (path, text) in [
(
"src/mcp.rs",
"pub fn tool_list_tokens(counter: &Counter) -> usize { 0 }\n",
),
(
"src/routes.rs",
"use crate::mcp;\nfn tools(counter: &Counter) -> Value {\n \
json!({ \"tokens\": mcp::tool_list_tokens(counter) })\n}\n",
),
] {
let file = crate::store::insert_file(&db, path, path, text.len() as u64, 0).unwrap();
let e = extract(Path::new(path), text).unwrap().unwrap();
crate::store::write_extraction(&db, &e, file, &[]).unwrap();
}
let n = neighbours(&db, "tool_list_tokens", &[], false, &Scope::all()).unwrap();
assert!(
n.callers.iter().any(|c| c.symbol.name == "tools"),
"{:?}",
n.callers
);
let reach = impact(&db, "tool_list_tokens", &[], 1, false, &Scope::all()).unwrap();
assert!(
reach.reached.iter().any(|r| r.name == "tools"),
"{:?}",
reach.reached
);
}
#[test]
fn a_file_with_no_grammar_yields_nothing_rather_than_failing() {
assert!(
extract(&PathBuf::from("notes.txt"), "the lock is acquired")
.unwrap()
.is_none()
);
}
#[test]
fn rust_constants_and_statics_are_symbols() {
let e = run(
"limits.rs",
"/// How many nodes a traversal may visit.\n\
pub const MAX_NODES: usize = 4000;\n\
static REGISTRY: &str = \"registry.json\";\n\
fn helper() {}\n",
);
let named = |n: &str| e.symbols.iter().find(|s| s.name == n);
let max_nodes = named("MAX_NODES")
.unwrap_or_else(|| panic!("MAX_NODES is not a symbol: {:?}", e.symbols));
assert_eq!(max_nodes.kind, "constant", "{max_nodes:?}");
let registry = named("REGISTRY")
.unwrap_or_else(|| panic!("REGISTRY is not a symbol: {:?}", e.symbols));
assert_eq!(registry.kind, "constant", "{registry:?}");
assert!(
named("helper").is_some(),
"the supplement broke the tags query"
);
}
#[test]
fn rust_carries_every_edge_kind_it_can_express() {
let e = run(
"lock.rs",
"use std::fs::File;\n\
struct Guard;\n\
impl Guard {\n fn open(&self) { File::create(\"x\"); helper(); }\n}\n\
fn helper() {}\n",
);
assert!(e.symbols.iter().any(|s| s.name == "Guard"));
assert!(e.symbols.iter().any(|s| s.name == "open"));
assert!(has_edge(&e, "lock", "helper", "defines"), "{:?}", e.edges);
assert!(has_edge(&e, "open", "helper", "calls"), "{:?}", e.edges);
assert!(
e.edges.iter().any(|x| x.kind == "imports"),
"the supplementary query supplies imports: {:?}",
e.edges
);
}
#[test]
fn an_imported_name_is_extracted_and_a_bare_one_is_inferred() {
let e = run(
"a.rs",
"use std::fs::File;\nfn go() { File::create(\"x\"); helper(); }\nfn helper() {}\n",
);
assert_eq!(confidence_of(&e, "create", "calls"), EXTRACTED);
assert_eq!(confidence_of(&e, "helper", "calls"), INFERRED);
}
fn hint_of(e: &Extraction, to: &str, kind: &str) -> Option<String> {
e.edges
.iter()
.find(|x| x.to == to && x.kind == kind)
.unwrap_or_else(|| panic!("no {kind} edge to {to} in {:?}", e.edges))
.hint
.clone()
}
#[test]
fn a_scoped_rust_call_hints_at_its_module_and_does_not_call_it() {
let e = run("a.rs", "fn go() { store::edges_out(db, name); }\n");
assert!(has_edge(&e, "go", "edges_out", "calls"), "{:?}", e.edges);
assert_eq!(hint_of(&e, "edges_out", "calls").as_deref(), Some("store"));
assert!(
!has_edge(&e, "go", "store", "calls"),
"the module is not called: {:?}",
e.edges
);
}
#[test]
fn a_longer_rust_path_hints_with_the_segment_nearest_the_name() {
let e = run("a.rs", "fn go() { crate::store::edges_out(db); }\n");
assert_eq!(hint_of(&e, "edges_out", "calls").as_deref(), Some("store"));
}
#[test]
fn a_rust_method_call_hints_with_its_receiver() {
let e = run(
"a.rs",
"fn go(&self) { self.index.search(v); self.flush(); }\n",
);
assert_eq!(hint_of(&e, "search", "calls").as_deref(), Some(".index"));
assert_eq!(hint_of(&e, "flush", "calls").as_deref(), Some("."));
}
#[test]
fn python_and_typescript_qualified_calls_carry_the_object_as_a_hint() {
let py = run(
"m.py",
"import json\n\nclass Store:\n def go(self):\n json.loads(raw)\n app.render()\n self.flush()\n",
);
assert_eq!(hint_of(&py, "loads", "calls").as_deref(), Some("json"));
assert_eq!(hint_of(&py, "render", "calls").as_deref(), Some(".app"));
assert_eq!(hint_of(&py, "flush", "calls").as_deref(), Some(".Store"));
let ts = run("app.ts", "function go(){ store.search(q); }\n");
assert_eq!(hint_of(&ts, "search", "calls").as_deref(), Some("store"));
}
#[test]
fn rust_receivers_name_their_type_and_methods_their_owner() {
let src = "struct Member { store: Semlith }\n\
struct Unreadable { store: String }\n\
struct Fleet { members: Vec<Member> }\n\
impl Fleet {\n\
fn search_in(&self) { self.search_preferring(); }\n\
fn search_preferring(&self) { self.members[0].store.search_preferring(); }\n\
}\n\
fn run(fleet: &mut Fleet) { fleet.open(); }\n\
fn build() { let s = Semlith::open(p)?; s.index(); let t = Other { a: 1 }; t.go(); }\n";
let e = run("fleet.rs", src);
let owner_of = |name: &str| {
e.symbols
.iter()
.find(|s| s.name == name)
.map(|s| s.qualified.clone())
};
assert_eq!(owner_of("search_in").as_deref(), Some("Fleet::search_in"));
assert_eq!(owner_of("run").as_deref(), Some("fleet::run"));
let hint = |from: &str, to: &str| {
e.edges
.iter()
.find(|x| x.from == from && x.to == to && x.kind == "calls")
.and_then(|x| x.hint.clone())
};
assert_eq!(
hint("search_in", "search_preferring").as_deref(),
Some(".Fleet")
);
assert_eq!(
hint("search_preferring", "search_preferring").as_deref(),
Some(".Semlith"),
"{:?}",
e.edges
);
assert_eq!(hint("run", "open").as_deref(), Some(".Fleet"));
assert_eq!(hint("build", "index").as_deref(), Some(".Semlith"));
assert_eq!(hint("build", "go").as_deref(), Some(".Other"));
let e = run(
"a.rs",
"struct Item; impl Item { fn go(&self) {} }\nstruct Bag { items: Vec<Item> }\nimpl Bag { fn all(&self) { for it in &self.items { it.go(); } } }\n",
);
let go = e
.edges
.iter()
.find(|x| x.from == "all" && x.to == "go")
.and_then(|x| x.hint.clone());
assert_eq!(go.as_deref(), Some(".Item"), "{:?}", e.edges);
}
#[test]
fn rust_recursion_on_self_adds_no_edge() {
let e = run(
"a.rs",
"struct A;\nimpl A { fn go(&self) { self.go(); } }\n",
);
assert!(!has_edge(&e, "go", "go", "calls"), "{:?}", e.edges);
}
#[test]
fn the_hinted_copy_of_a_doubly_captured_call_is_the_one_kept() {
let e = run("a.rs", "fn go() { store::edges_out(db); }\n");
let calls: Vec<&Edge> = e
.edges
.iter()
.filter(|x| x.to == "edges_out" && x.kind == "calls")
.collect();
assert_eq!(calls.len(), 1, "{:?}", e.edges);
assert_eq!(calls[0].hint.as_deref(), Some("store"));
}
#[test]
fn nesting_produces_contains_rather_than_defines() {
let e = run("m.py", "class Box:\n def open(self):\n pass\n");
assert!(has_edge(&e, "Box", "open", "contains"), "{:?}", e.edges);
assert!(has_edge(&e, "m", "Box", "defines"), "{:?}", e.edges);
}
#[test]
fn typescript_calls_come_from_the_supplement() {
let e = run(
"app.ts",
"import {a} from './m';\nfunction f(){ helper(); obj.go(); }\nfunction helper(){}\n",
);
assert!(has_edge(&e, "f", "helper", "calls"), "{:?}", e.edges);
assert!(has_edge(&e, "f", "go", "calls"), "{:?}", e.edges);
assert!(e.edges.iter().any(|x| x.kind == "imports"));
}
#[test]
fn c_calls_and_includes_come_from_the_supplement() {
let e = run(
"main.c",
"#include <stdio.h>\nint helper(){ return 1; }\nint main(){ return helper(); }\n",
);
assert!(has_edge(&e, "main", "helper", "calls"), "{:?}", e.edges);
assert!(
e.edges
.iter()
.any(|x| x.kind == "imports" && x.to == "stdio.h"),
"{:?}",
e.edges
);
}
#[test]
fn python_go_and_java_each_yield_symbols_and_edges() {
let py = run(
"m.py",
"import os\ndef go():\n helper()\ndef helper():\n pass\n",
);
assert!(has_edge(&py, "go", "helper", "calls"), "{:?}", py.edges);
assert!(py.edges.iter().any(|x| x.kind == "imports" && x.to == "os"));
let go = run(
"m.go",
"package m\nimport \"fmt\"\nfunc helper() {}\nfunc Run() { helper() }\n",
);
assert!(has_edge(&go, "Run", "helper", "calls"), "{:?}", go.edges);
assert!(
go.edges
.iter()
.any(|x| x.kind == "imports" && x.to == "fmt")
);
let java = run(
"A.java",
"import java.util.List;\nclass A { void go(){ helper(); } void helper(){} }\n",
);
assert!(has_edge(&java, "go", "helper", "calls"), "{:?}", java.edges);
assert!(java.edges.iter().any(|x| x.kind == "imports"));
assert!(has_edge(&java, "A", "go", "contains"), "{:?}", java.edges);
}
struct Fixture {
language: &'static str,
file: &'static str,
symbols: &'static [(&'static str, &'static str)],
edges: &'static [(&'static str, &'static str, &'static str)],
}
const FIXTURES: &[Fixture] = &[
Fixture {
language: "rust",
file: "rust/lock.rs",
symbols: &[
("MAX_HOLDERS", "constant"),
("helper", "function"),
("acquire", "function"),
],
edges: &[
("acquire", "helper", "calls"),
("lock", "std::fs::File", "imports"),
],
},
Fixture {
language: "typescript",
file: "typescript/app.ts",
symbols: &[
("MAX_HOLDERS", "constant"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls"), ("app", "./m", "imports")],
},
Fixture {
language: "python",
file: "python/run.py",
symbols: &[
("MAX_HOLDERS", "constant"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls"), ("run", "os", "imports")],
},
Fixture {
language: "go",
file: "go/serve.go",
symbols: &[
("MaxHolders", "constant"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls"), ("serve", "fmt", "imports")],
},
Fixture {
language: "java",
file: "java/Main.java",
symbols: &[
("MAX_HOLDERS", "constant"),
("Main", "class"),
("helper", "method"),
("acquire", "method"),
],
edges: &[
("acquire", "helper", "calls"),
("Main", "helper", "contains"),
],
},
Fixture {
language: "c",
file: "c/main.c",
symbols: &[
("MAX_HOLDERS", "constant"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "clojure",
file: "clojure/lock.clj",
symbols: &[
("max-holders", "constant"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "cpp",
file: "cpp/lock.cpp",
symbols: &[
("MAX_HOLDERS", "constant"),
("MAX_WAITERS", "constant"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "csharp",
file: "csharp/Lock.cs",
symbols: &[
("MaxHolders", "constant"),
("Lock", "class"),
("Helper", "method"),
("Acquire", "method"),
],
edges: &[("Acquire", "Helper", "calls")],
},
Fixture {
language: "css",
file: "css/lock.css",
symbols: &[("acquire", "selector"), ("lock", "selector")],
edges: &[("lock", "base.css", "imports")],
},
Fixture {
language: "dart",
file: "dart/lock.dart",
symbols: &[
("maxHolders", "variable"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "dockerfile",
file: "dockerfile/Dockerfile",
symbols: &[("build", "stage"), ("runtime", "stage")],
edges: &[
("build", "debian", "imports"),
("runtime", "build", "imports"),
],
},
Fixture {
language: "elixir",
file: "elixir/lock.ex",
symbols: &[
("max_holders", "constant"),
("Lock", "module"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "elm",
file: "elm/Lock.elm",
symbols: &[("helper", "function"), ("acquire", "function")],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "erlang",
file: "erlang/lock.erl",
symbols: &[
("MAX_HOLDERS", "constant"),
("lock", "module"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "fortran",
file: "fortran/lock.f90",
symbols: &[
("max_holders", "constant"),
("lock", "module"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "graphql",
file: "graphql/lock.graphql",
symbols: &[
("Lock", "type"),
("Holder", "type"),
("Acquire", "operation"),
],
edges: &[("Lock", "holder", "contains")],
},
Fixture {
language: "groovy",
file: "groovy/lock.groovy",
symbols: &[
("MAX_HOLDERS", "constant"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "haskell",
file: "haskell/Lock.hs",
symbols: &[("helper", "function"), ("acquire", "function")],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "html",
file: "html/lock.html",
symbols: &[("acquire", "element")],
edges: &[
("lock", "base.css", "imports"),
("lock", "app.js", "imports"),
],
},
Fixture {
language: "javascript",
file: "javascript/app.js",
symbols: &[
("MAX_HOLDERS", "constant"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls"), ("app", "./m", "imports")],
},
Fixture {
language: "json",
file: "json/lock.json",
symbols: &[("name", "key"), ("acquire", "key"), ("timeout", "key")],
edges: &[("acquire", "timeout", "contains")],
},
Fixture {
language: "julia",
file: "julia/lock.jl",
symbols: &[
("MAX_HOLDERS", "constant"),
("Lock", "module"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "kotlin",
file: "kotlin/Lock.kt",
symbols: &[
("MAX_HOLDERS", "property"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "lua",
file: "lua/lock.lua",
symbols: &[
("MAX_HOLDERS", "constant"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "makefile",
file: "makefile/Makefile",
symbols: &[("helper", "target"), ("acquire", "target")],
edges: &[
("acquire", "helper", "references"),
("Makefile", "config.mk", "imports"),
],
},
Fixture {
language: "markdown",
file: "markdown/notes.md",
symbols: &[("Lock", "heading"), ("Acquire", "heading")],
edges: &[("Lock", "Acquire", "contains")],
},
Fixture {
language: "nix",
file: "nix/lock.nix",
symbols: &[("helper", "binding"), ("acquire", "binding")],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "objective-c",
file: "objective-c/Lock.m",
symbols: &[
("MAX_HOLDERS", "constant"),
("Lock", "class"),
("helper", "method"),
("acquire", "method"),
],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "ocaml",
file: "ocaml/lock.ml",
symbols: &[("helper", "function"), ("acquire", "function")],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "perl",
file: "perl/lock.pl",
symbols: &[
("MAX_HOLDERS", "constant"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "php",
file: "php/lock.php",
symbols: &[
("MAX_HOLDERS", "constant"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "powershell",
file: "powershell/Lock.ps1",
symbols: &[("Get-Helper", "function"), ("Get-Acquire", "function")],
edges: &[("Get-Acquire", "Get-Helper", "calls")],
},
Fixture {
language: "proto",
file: "proto/lock.proto",
symbols: &[("Lock", "message"), ("Acquire", "service"), ("Hold", "rpc")],
edges: &[("lock", "base.proto", "imports")],
},
Fixture {
language: "r",
file: "r/lock.r",
symbols: &[("helper", "function"), ("acquire", "function")],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "ruby",
file: "ruby/lock.rb",
symbols: &[
("MAX_HOLDERS", "constant"),
("helper", "method"),
("acquire", "method"),
],
edges: &[("lock", "helper", "defines")],
},
Fixture {
language: "scala",
file: "scala/Lock.scala",
symbols: &[
("MaxHolders", "constant"),
("Lock", "object"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "shell",
file: "shell/lock.sh",
symbols: &[
("MAX_HOLDERS", "constant"),
("helper", "function"),
("acquire", "function"),
],
edges: &[
("acquire", "helper", "calls"),
("lock", "./config.sh", "imports"),
],
},
Fixture {
language: "sql",
file: "sql/lock.sql",
symbols: &[("lock_holder", "table"), ("acquire", "view")],
edges: &[("acquire", "lock_holder", "references")],
},
Fixture {
language: "svelte",
file: "svelte/Lock.svelte",
symbols: &[("lock", "element")],
edges: &[("Lock", "lock", "defines")],
},
Fixture {
language: "swift",
file: "swift/Lock.swift",
symbols: &[
("maxHolders", "property"),
("helper", "function"),
("acquire", "function"),
],
edges: &[("acquire", "helper", "calls")],
},
Fixture {
language: "terraform",
file: "terraform/lock.tf",
symbols: &[("region", "block"), ("base", "block"), ("lock", "block")],
edges: &[("lock", "region", "defines")],
},
Fixture {
language: "toml",
file: "toml/lock.toml",
symbols: &[("name", "key"), ("acquire", "table")],
edges: &[("acquire", "timeout", "contains")],
},
Fixture {
language: "vue",
file: "vue/Lock.vue",
symbols: &[("lock", "element")],
edges: &[("Lock", "lock", "defines")],
},
Fixture {
language: "yaml",
file: "yaml/lock.yaml",
symbols: &[("name", "key"), ("acquire", "key"), ("timeout", "key")],
edges: &[("acquire", "timeout", "contains")],
},
Fixture {
language: "zig",
file: "zig/lock.zig",
symbols: &[
("MAX_HOLDERS", "variable"),
("helper", "function"),
("acquire", "function"),
("std", "variable"),
],
edges: &[("acquire", "helper", "calls")],
},
];
const WITHOUT_CONSTANTS: &[(&str, &str)] = &[
(
"css",
"a custom property is a variable a later rule can redefine",
),
(
"dart",
"already a symbol; the bundled query calls it a variable",
),
(
"dockerfile",
"ARG and ENV are build variables, overridable at build time",
),
(
"elm",
"every binding is immutable, so none of them is the constant",
),
("graphql", "a schema names types and fields, never a value"),
(
"haskell",
"every binding is immutable, so none of them is the constant",
),
("html", "markup, with no declaration of any kind"),
(
"json",
"data; a member is a key and a value, not a declaration",
),
(
"kotlin",
"already a symbol; `const val` is a property to the grammar",
),
(
"makefile",
"a variable can be overridden on the command line",
),
("markdown", "prose"),
(
"nix",
"a `let` or attribute binding is already a `binding` symbol",
),
(
"ocaml",
"every `let` binding is immutable, so none of them is the constant",
),
(
"powershell",
"a constant is `Set-Variable -Option Constant`, a call and not syntax",
),
(
"proto",
"an enum value is a member of an enum, not a declared constant",
),
(
"r",
"the language has no constant construct; `<-` binds and rebinds",
),
(
"sql",
"there is no `CREATE CONSTANT`; a literal is written where it is used",
),
(
"svelte",
"the grammar reads the markup and hands back the script as raw text",
),
(
"swift",
"already a symbol; the bundled query calls `let` a property",
),
(
"terraform",
"a local or a variable is a value HCL evaluates, not a constant",
),
("toml", "data; a key and a value, not a declaration"),
(
"vue",
"the grammar reads the template and hands back the script as raw text",
),
("yaml", "data; a key and a value, not a declaration"),
(
"zig",
"already a symbol; our own query calls `const` a variable",
),
];
fn fixture_root() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/graph")
}
#[test]
#[ignore = "a tool for writing queries, not a gate"]
fn dump_fixture_trees() {
let root = fixture_root();
let mut languages: Vec<_> = crate::filter::LANGUAGES.iter().map(|e| e.name).collect();
languages.sort_unstable();
for name in languages {
let dir = root.join(name);
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries {
let path = entry.unwrap().path();
let text = std::fs::read_to_string(&path).unwrap();
let Some((language, _)) = grammar(name) else {
continue;
};
let mut parser = Parser::new();
parser.set_language(&language).unwrap();
let tree = parser.parse(&text, None).unwrap();
println!(
"===== {name} :: {} =====\n{}",
path.file_name().unwrap().to_string_lossy(),
tree.root_node().to_sexp()
);
}
}
}
#[test]
#[ignore = "a tool for writing queries, not a gate"]
fn dump_fixture_extractions() {
let root = fixture_root();
for fixture in FIXTURES {
let text = std::fs::read_to_string(root.join(fixture.file)).unwrap();
let e = extract(std::path::Path::new(fixture.file), &text)
.unwrap()
.unwrap();
println!("===== {} =====", fixture.language);
for s in &e.symbols {
println!(
" symbol {:14} {:24} {}-{}",
s.kind, s.name, s.start_line, s.end_line
);
}
for x in &e.edges {
println!(
" edge {:14} {} -> {} (line {:?})",
x.kind, x.from, x.to, x.line
);
}
}
}
#[test]
fn every_advertised_language_has_a_fixture() {
for entry in crate::filter::LANGUAGES {
if WITHOUT_GRAMMAR.iter().any(|(name, _)| *name == entry.name) {
continue;
}
assert!(
FIXTURES.iter().any(|f| f.language == entry.name),
"{} has a grammar but no fixture asserting what it extracts",
entry.name
);
}
}
#[test]
fn every_language_with_a_constant_construct_extracts_one() {
for entry in crate::filter::LANGUAGES {
let fixture = FIXTURES
.iter()
.find(|f| f.language == entry.name)
.unwrap_or_else(|| panic!("{} has no fixture", entry.name));
let declares = fixture.symbols.iter().any(|(_, kind)| *kind == "constant");
match WITHOUT_CONSTANTS
.iter()
.find(|(name, _)| *name == entry.name)
{
Some((_, why)) => assert!(
!declares,
"{} is listed as extracting no constant ({why}) but its fixture declares one",
entry.name
),
None => assert!(
declares,
"{} has a constant construct but no fixture proves one is extracted",
entry.name
),
}
}
}
#[test]
fn every_fixture_yields_what_it_declares() {
let root = fixture_root();
let mut wrong: Vec<String> = Vec::new();
for fixture in FIXTURES {
let path = root.join(fixture.file);
let text = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("{} is missing: {e}", path.display()));
let extraction = extract(std::path::Path::new(fixture.file), &text)
.unwrap_or_else(|e| panic!("{}: extraction failed: {e}", fixture.language))
.unwrap_or_else(|| {
panic!(
"{}: no extractor reached {} — the extension is not in the table",
fixture.language, fixture.file
)
});
for (name, kind) in fixture.symbols {
if !extraction
.symbols
.iter()
.any(|s| s.name == *name && s.kind == *kind)
{
wrong.push(format!(
"{}: no {kind} named {name}; found {:?}",
fixture.language,
extraction
.symbols
.iter()
.map(|s| (&s.kind, &s.name))
.collect::<Vec<_>>()
));
}
}
for (from, to, kind) in fixture.edges {
if !extraction
.edges
.iter()
.any(|e| e.from == *from && e.to == *to && e.kind == *kind)
{
wrong.push(format!(
"{}: no {kind} edge {from} -> {to}; found {:?}",
fixture.language,
extraction
.edges
.iter()
.map(|e| (&e.from, &e.kind, &e.to))
.collect::<Vec<_>>()
));
}
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}
#[test]
fn every_advertised_language_has_a_working_grammar() {
let mut broken: Vec<String> = Vec::new();
for entry in crate::filter::LANGUAGES {
let lang = entry.name;
if let Some((_, why)) = WITHOUT_GRAMMAR.iter().find(|(name, _)| *name == lang) {
assert!(
grammar(lang).is_none(),
"{lang} is listed as having no grammar ({why}) but has one"
);
continue;
}
let Some((language, tags)) = grammar(lang) else {
broken.push(format!("{lang}: advertised but has no grammar"));
continue;
};
let mut parser = Parser::new();
if let Err(e) = parser.set_language(&language) {
broken.push(format!("{lang}: grammar is ABI-incompatible: {e}"));
continue;
}
if let Err(e) = Query::new(&language, tags) {
broken.push(format!("{lang}: tags query does not compile: {e:?}"));
}
let supplement = supplement(lang);
if !supplement.is_empty()
&& let Err(e) = Query::new(&language, supplement)
{
broken.push(format!("{lang}: supplement does not compile: {e:?}"));
}
}
assert!(broken.is_empty(), "{}", broken.join("\n"));
}
#[test]
fn a_file_that_does_not_parse_yields_what_it_could_read() {
let e = run("a.rs", "fn good() {}\nfn broken( {\n");
assert!(e.symbols.iter().any(|s| s.name == "good"));
}
#[test]
fn a_reference_edge_carries_the_line_the_call_was_written_on() {
let e = run(
"a.rs",
"use other::helper;\n\nfn caller() {\n let x = 1;\n let _ = x;\n helper();\n}\n",
);
let call = e
.edges
.iter()
.find(|x| x.kind == "calls" && x.to == "helper")
.unwrap_or_else(|| panic!("no call edge: {:?}", e.edges));
assert_eq!(call.line, Some(6), "{:?}", e.edges);
let import = e
.edges
.iter()
.find(|x| x.kind == "imports")
.unwrap_or_else(|| panic!("no import edge: {:?}", e.edges));
assert_eq!(import.line, Some(1), "{:?}", e.edges);
assert!(e.edges.iter().all(|x| x.line.is_some()), "{:?}", e.edges);
}
#[test]
fn an_alias_becomes_a_symbol_and_an_edge_to_what_it_stands_for() {
let rust = run("a.rs", "pub use store::Semlith as Store;\n");
assert!(
rust.symbols.iter().any(|s| s.name == "Store"),
"{:?}",
rust.symbols
);
assert!(
has_edge(&rust, "Store", "Semlith", "aliases"),
"{:?}",
rust.edges
);
let py = run("a.py", "from x import search as find\n");
assert!(has_edge(&py, "find", "search", "aliases"), "{:?}", py.edges);
let ts = run("a.ts", "import { search as find } from './x';\n");
assert!(has_edge(&ts, "find", "search", "aliases"), "{:?}", ts.edges);
let go = run("a.go", "package m\nimport fp \"path/filepath\"\n");
assert!(has_edge(&go, "fp", "filepath", "aliases"), "{:?}", go.edges);
}
#[test]
fn a_re_export_that_does_not_rename_adds_no_alias_edge() {
let e = run("a.rs", "pub use store::Semlith;\n");
assert!(
!e.edges.iter().any(|x| x.kind == "aliases"),
"{:?}",
e.edges
);
}
#[test]
fn recursion_does_not_produce_a_self_edge() {
let e = run("a.rs", "fn loops() { loops(); }\n");
assert!(!has_edge(&e, "loops", "loops", "calls"), "{:?}", e.edges);
}
fn chain() -> rusqlite::Connection {
let db = rusqlite::Connection::open_in_memory().unwrap();
crate::store::prepare_for_tests(&db);
let file = crate::store::insert_file(&db, "a.rs", "h", 1, 0).unwrap();
let mut id = std::collections::HashMap::new();
for name in ["a", "b", "c", "d", "e", "lonely"] {
let symbol = Symbol {
kind: "function".to_string(),
name: name.to_string(),
qualified: name.to_string(),
start_line: 1,
end_line: 2,
};
id.insert(
name,
crate::store::insert_symbol(&db, file, None, &symbol).unwrap(),
);
}
for (from, to) in [("a", "b"), ("b", "c"), ("c", "d"), ("e", "d")] {
crate::store::insert_edge(&db, id[from], to, "calls", EXTRACTED, None, None).unwrap();
}
db
}
#[test]
fn a_scope_keeps_a_namesake_in_another_repository_out() {
let db = rusqlite::Connection::open_in_memory().unwrap();
crate::store::prepare_for_tests(&db);
for repo in ["one", "two"] {
let file =
crate::store::insert_file(&db, &format!("/w/{repo}/x.rs"), "h", 1, 0).unwrap();
let mut ids = Vec::new();
for (name, line) in [("run", 1), (format!("go_{repo}").as_str(), 5)] {
let symbol = Symbol {
kind: "function".to_string(),
name: name.to_string(),
qualified: name.to_string(),
start_line: line,
end_line: line + 2,
};
ids.push(crate::store::insert_symbol(&db, file, None, &symbol).unwrap());
}
crate::store::insert_edge(&db, ids[1], "run", "calls", EXTRACTED, None, None).unwrap();
}
let filter = crate::filter::Filter::new(&["one/**".to_string()], &[], &[]).unwrap();
let scope = Scope::of(&db, &filter).unwrap();
let names = |ends: &[crate::store::EdgeEnd]| {
let mut n: Vec<String> = ends.iter().map(|e| e.symbol.name.clone()).collect();
n.sort();
n
};
let around = neighbours(&db, "run", &[], true, &scope).unwrap();
assert_eq!(names(&around.callers), ["go_one"]);
let everywhere = neighbours(&db, "run", &[], true, &Scope::all()).unwrap();
assert_eq!(names(&everywhere.callers), ["go_one", "go_two"]);
let reach = impact(&db, "run", &[], 3, false, &scope).unwrap();
assert!(
reach.definitions.iter().all(|d| d.path.contains("/one/")),
"{reach:?}"
);
assert!(
reach.reached.iter().all(|r| r.path.contains("/one/")),
"{reach:?}"
);
assert!(
reach.reached.iter().any(|r| r.name == "go_one"),
"{reach:?}"
);
let rows = signatures(&db, &["run".to_string()], &scope).unwrap();
assert_eq!(rows.len(), 1, "{rows:?}");
assert!(rows[0].symbol.path.contains("/one/"));
let found = evidence(&db, "run", &[], 20, true, &scope).unwrap();
assert!(found.definitions.iter().all(|d| d.path.contains("/one/")));
assert_eq!(names(&found.callers), ["go_one"]);
}
#[test]
fn the_evidence_block_carries_the_definition_both_rings_and_no_ambiguity() {
let db = chain();
let found = evidence(&db, "b", &dependency_kinds(), 20, false, &Scope::all()).unwrap();
assert_eq!(found.name, "b");
assert!(!found.definitions.is_empty(), "{found:?}");
assert!(
found.callers.iter().any(|e| e.symbol.name == "a"),
"{:?}",
found.callers
);
assert!(
found.callees.iter().any(|e| e.symbol.name == "c"),
"{:?}",
found.callees
);
assert!(
found.ego.iter().any(|h| h.name == "d" && h.via == "c"),
"{:?}",
found.ego
);
assert!(
found
.ego
.iter()
.all(|h| h.name != "b" && h.name != "a" && h.name != "c"),
"{:?}",
found.ego
);
assert!(
found.ego.len() <= EGO_LIMIT,
"the ring is context, not an answer: {:?}",
found.ego
);
let text = found.render("", "", &verbatim);
assert!(text.contains("callers"), "{text}");
assert!(text.contains("callees"), "{text}");
assert!(text.contains("two hops out"), "{text}");
}
#[test]
fn neighbours_separates_the_two_directions() {
let db = chain();
let n = neighbours(&db, "d", &[], false, &Scope::all()).unwrap();
let mut callers: Vec<&str> = n.callers.iter().map(|e| e.symbol.name.as_str()).collect();
callers.sort_unstable();
assert_eq!(callers, ["c", "e"], "both callers of d");
assert!(n.callees.is_empty(), "d calls nothing");
}
#[test]
fn a_cycle_terminates_instead_of_looping() {
let db = chain();
let file = crate::store::insert_file(&db, "b.rs", "h", 1, 0).unwrap();
let symbol = Symbol {
kind: "function".to_string(),
name: "d".to_string(),
qualified: "d".to_string(),
start_line: 1,
end_line: 2,
};
let d = crate::store::insert_symbol(&db, file, None, &symbol).unwrap();
crate::store::insert_edge(&db, d, "a", "calls", EXTRACTED, None, None).unwrap();
let path = shortest_path(&db, "a", "d", 50, false).unwrap();
assert!(path.is_some(), "a still reaches d");
assert!(
path.unwrap().steps.len() <= 4,
"a cycle inflated the shortest path"
);
}
#[test]
fn the_shortest_path_is_the_short_one_and_names_every_edge() {
let db = chain();
let path = shortest_path(&db, "a", "d", 10, false)
.unwrap()
.expect("a reaches d");
let hops: Vec<(&str, &str)> = path
.steps
.iter()
.map(|s| (s.from.as_str(), s.to.as_str()))
.collect();
assert_eq!(hops, [("a", "b"), ("b", "c"), ("c", "d")]);
assert!(path.steps.iter().all(|s| s.kind == "calls"));
assert!(path.steps.iter().all(|s| s.confidence == EXTRACTED));
assert!(path.steps.iter().all(|s| !s.to_path.is_empty()));
assert_eq!(path.summary.extracted, 3);
assert_eq!(path.summary.seams, 0);
assert!(!path.summary.hypothesis, "every hop is extracted");
}
fn forked_name() -> rusqlite::Connection {
let db = rusqlite::Connection::open_in_memory().unwrap();
crate::store::prepare_for_tests(&db);
let define = |db: &rusqlite::Connection, path: &str, name: &str, line: u32| {
let file = crate::store::insert_file(db, path, path, 1, 0).unwrap_or_else(|_| {
db.query_row("SELECT id FROM files WHERE path = ?1", [path], |r| r.get(0))
.unwrap()
});
let symbol = Symbol {
kind: "function".to_string(),
name: name.to_string(),
qualified: name.to_string(),
start_line: line,
end_line: line + 1,
};
crate::store::insert_symbol(db, file, None, &symbol).unwrap()
};
let start = define(&db, "src/start.rs", "start", 10);
define(&db, "src/one.rs", "record", 20);
let second = define(&db, "src/two.rs", "record", 30);
define(&db, "src/finish.rs", "finish", 40);
crate::store::insert_edge(&db, start, "record", "calls", INFERRED, None, None).unwrap();
crate::store::insert_edge(&db, second, "finish", "calls", INFERRED, None, None).unwrap();
db
}
#[test]
fn a_chain_through_an_ambiguous_name_is_not_walked_by_default() {
let db = forked_name();
assert!(
shortest_path(&db, "start", "finish", 6, false)
.unwrap()
.is_none(),
"the only route crosses a name with two definitions"
);
}
#[test]
fn the_same_chain_with_all_edges_is_labelled_rather_than_asserted() {
let db = forked_name();
let chain = shortest_path(&db, "start", "finish", 6, true)
.unwrap()
.expect("all_edges walks the ambiguous name");
assert_eq!(chain.steps.len(), 2);
assert_eq!(chain.steps[0].confidence, AMBIGUOUS);
assert_eq!(chain.steps[0].definitions, 2);
assert_eq!(chain.summary.seams, 1, "{:?}", chain.steps);
assert_eq!(
chain.summary.ambiguous_names,
vec![("record".to_string(), 2)]
);
assert!(chain.summary.hypothesis);
let rendered = chain.render("", "", &verbatim);
assert!(rendered.contains("seam"), "{rendered}");
assert!(rendered.contains("record: 2 definitions"), "{rendered}");
assert!(
rendered.contains("A hypothesis, not a finding."),
"{rendered}"
);
assert!(
rendered.contains("src/start.rs:10"),
"every hop shows both endpoints: {rendered}"
);
}
#[test]
fn an_all_extracted_chain_carries_no_hypothesis_line() {
let db = chain();
let rendered = shortest_path(&db, "a", "d", 10, false)
.unwrap()
.expect("a reaches d")
.render("", "", &verbatim);
assert!(!rendered.contains("hypothesis"), "{rendered}");
assert!(!rendered.contains("seam"), "{rendered}");
assert!(rendered.contains("3 hops"), "{rendered}");
assert!(rendered.contains("3 extracted"), "{rendered}");
}
#[test]
fn the_refusal_names_the_flag_that_widens_the_question() {
let strict = not_connected("a", "b", 6, false);
assert!(strict.contains("by resolved edges"), "{strict}");
assert!(strict.contains("--all-edges"), "{strict}");
let walked = not_connected("a", "b", 6, true);
assert!(walked.contains("any edge in the store"), "{walked}");
assert!(!walked.contains("--all-edges"), "{walked}");
}
#[test]
fn an_unconnected_pair_returns_nothing_rather_than_erroring() {
let db = chain();
assert!(
shortest_path(&db, "a", "lonely", 10, false)
.unwrap()
.is_none()
);
assert!(
shortest_path(&db, "a", "d", 2, false).unwrap().is_none(),
"d is three hops away, so a depth of two does not reach it"
);
}
}
#[cfg(test)]
mod audit_tests {
use super::*;
fn fixture(name: &str) -> String {
let root =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/graph-audit");
std::fs::read_to_string(root.join(name)).unwrap()
}
fn store(files: &[(&str, &str)]) -> rusqlite::Connection {
let db = rusqlite::Connection::open_in_memory().unwrap();
crate::store::prepare_for_tests(&db);
for (path, name) in files {
let text = fixture(name);
let file = crate::store::insert_file(&db, path, path, text.len() as u64, 0).unwrap();
let e = extract(Path::new(path), &text).unwrap().unwrap();
crate::store::write_extraction(&db, &e, file, &[]).unwrap();
}
db
}
fn ripgrep() -> rusqlite::Connection {
store(&[
("crates/core/main.rs", "rust/ripgrep_main.rs"),
("crates/ignore/src/walk.rs", "rust/ripgrep_walk.rs"),
("crates/core/index/enabled.rs", "rust/ripgrep_enabled.rs"),
("crates/core/search.rs", "rust/ripgrep_search.rs"),
])
}
fn flask() -> rusqlite::Connection {
store(&[
("src/flask/app.py", "python/flask_app.py"),
("src/flask/helpers.py", "python/flask_helpers.py"),
("src/flask/__init__.py", "python/flask_init.py"),
("tests/test_basic.py", "python/flask_test_basic.py"),
])
}
#[test]
fn a_method_call_is_never_resolved_to_a_free_function() {
let db = ripgrep();
for from in ["search_parallel", "files_parallel"] {
let out = crate::store::edges_out(&db, from, &dependency_kinds()).unwrap();
for to in ["run", "clone", "clear", "unwrap"] {
for e in out.iter().filter(|e| e.symbol.name == to) {
assert_eq!(
e.confidence, AMBIGUOUS,
"{from} -> {to} settled on {}:{} ({})",
e.symbol.path, e.symbol.start_line, e.symbol.qualified
);
assert!(
e.symbol.qualified != "main::run",
"{from} -> {to} names the free fn main::run"
);
}
}
assert!(
out.iter().all(|e| e.symbol.kind != "module"),
"{from}: {out:?}"
);
}
}
#[test]
fn no_path_or_trace_crosses_an_untyped_method_call() {
let db = ripgrep();
let chain = shortest_path(&db, "files_parallel", "search_parallel", 6, false).unwrap();
assert!(chain.is_none(), "{chain:?}");
let t = trace(
"files_parallel",
"search_parallel",
6,
false,
chain,
|_, _| None,
&verbatim,
);
assert!(
t.lines.iter().all(|l| l.mark != "supporting fact"),
"{:?}",
t.lines
);
}
#[test]
fn a_call_never_means_a_module() {
let db = ripgrep();
let out = crate::store::edges_out(&db, "run", &dependency_kinds()).unwrap();
let search: Vec<_> = out.iter().filter(|e| e.symbol.name == "search").collect();
assert_eq!(search.len(), 1, "{search:?}");
assert_eq!(search[0].confidence, RESOLVED, "{search:?}");
assert_eq!(search[0].symbol.kind, "function", "{search:?}");
let n = neighbours(&db, "search", &[], false, &Scope::all()).unwrap();
assert!(
n.callers
.iter()
.any(|c| c.symbol.name == "run" && c.confidence == RESOLVED),
"{:?}",
n.callers
);
}
#[test]
fn a_call_through_crate_is_a_caller() {
let db = ripgrep();
let n = neighbours(&db, "search_parallel", &[], false, &Scope::all()).unwrap();
assert!(
n.callers.iter().any(|c| c.symbol.name == "read"
&& c.symbol.path == "crates/core/index/enabled.rs"
&& c.line == Some(14)),
"{:?}",
n.callers
);
}
#[test]
fn a_namesake_called_on_another_receiver_is_not_recursion() {
let db = flask();
let n = neighbours(&db, "Flask.make_response", &[], false, &Scope::all()).unwrap();
assert!(
n.callers
.iter()
.any(|c| c.symbol.path == "src/flask/helpers.py"
&& c.symbol.name == "make_response"
&& c.line == Some(27)),
"{:?}",
n.callers
);
}
#[test]
fn a_module_edge_sits_on_the_module_and_is_not_a_call() {
let db = ripgrep();
let into =
crate::store::edges_in(&db, "search_parallel", &["defines".to_string()]).unwrap();
assert!(!into.is_empty());
for e in &into {
assert_eq!(e.symbol.kind, "module", "{e:?}");
let site = call_site(e, &verbatim);
assert!(!site.contains("call"), "{site}");
}
let main = crate::store::edges_out(&db, "main", &["defines".to_string()]).unwrap();
assert!(!main.is_empty());
assert!(main.iter().all(|e| e.from_line == Some(1)), "{main:?}");
}
#[test]
fn neighbors_and_impact_give_one_edge_one_confidence() {
let db = flask();
let n = neighbours(&db, "make_response", &[], false, &Scope::all()).unwrap();
let reach = impact(&db, "make_response", &[], 1, false, &Scope::all()).unwrap();
let caller = n
.callers
.iter()
.find(|c| c.symbol.name == "test_make_response")
.expect("test_make_response calls make_response");
let row = reach
.reached
.iter()
.find(|r| r.name == "test_make_response")
.expect("impact reaches test_make_response");
assert_eq!(caller.confidence, row.confidence);
}
#[test]
fn a_python_definition_is_not_owned_by_an_import_alias() {
let e = extract(
Path::new("src/flask/app.py"),
&fixture("python/flask_app.py"),
)
.unwrap()
.unwrap();
let flask = e.symbols.iter().find(|s| s.name == "Flask").unwrap();
assert_eq!(flask.qualified, "app::Flask");
let method = e
.symbols
.iter()
.find(|s| s.name == "make_response")
.unwrap();
assert_eq!(method.qualified, "Flask::make_response");
for s in &e.symbols {
for alias in ["BaseResponse::", "t::", "ft::", "cabc::", "_url_quote::"] {
assert!(!s.qualified.starts_with(alias), "{s:?}");
}
}
assert!(
!e.edges
.iter()
.any(|x| x.kind == "contains"
&& ["BaseResponse", "t", "ft"].contains(&x.from.as_str())),
"{:?}",
e.edges
);
}
#[test]
fn an_alias_row_is_its_import_line() {
let e = extract(
Path::new("src/flask/__init__.py"),
&fixture("python/flask_init.py"),
)
.unwrap()
.unwrap();
for (name, line) in [("Flask", 2), ("make_response", 17), ("url_for", 22)] {
let s = e
.symbols
.iter()
.find(|s| s.name == name && s.kind == "alias")
.unwrap_or_else(|| panic!("no alias {name}"));
assert_eq!((s.start_line, s.end_line), (line, line), "{s:?}");
}
}
#[test]
fn setext_headings_define_their_own_sections() {
let text = fixture("markdown/ripgrep_changelog.md");
let lines = text.lines().count() as u32;
let e = extract(Path::new("CHANGELOG.md"), &text).unwrap().unwrap();
let headings: Vec<&Symbol> = e.symbols.iter().filter(|s| s.kind == "heading").collect();
let titles: Vec<&str> = headings.iter().map(|s| s.name.as_str()).collect();
assert_eq!(
titles,
[
"TBD",
"15.2.0 (2026-07-15)",
"15.1.0 (2025-10-22)",
"15.0.0 (2025-10-15)",
"12.1.1 (2020-05-29)"
]
);
let spans: Vec<(u32, u32)> = headings
.iter()
.map(|s| (s.start_line, s.end_line))
.collect();
assert_eq!(spans, [(1, 5), (6, 35), (36, 53), (54, 65), (66, lines)]);
for h in &headings {
assert_eq!(h.qualified, format!("CHANGELOG::{}", h.name), "{h:?}");
}
}
#[test]
fn the_census_counts_an_untyped_method_call_as_ambiguous() {
let dir = tempfile::tempdir().unwrap();
let db = crate::store::open(&dir.path().join("store.db")).unwrap();
crate::store::read_only(&db, false).unwrap();
for (path, text) in [
("src/a.rs", "fn caller(t: Thing) { t.go(); }\n"),
(
"src/b.rs",
"struct Other;\nimpl Other {\n fn go(&self) {}\n}\n",
),
] {
let file = crate::store::insert_file(&db, path, path, 1, 0).unwrap();
let e = extract(Path::new(path), text).unwrap().unwrap();
crate::store::write_extraction(&db, &e, file, &[]).unwrap();
}
let rust = crate::store::coverage_by_language(&db)
.unwrap()
.into_iter()
.find(|r| r.language == "rust")
.unwrap();
assert_eq!((rust.resolved, rust.ambiguous), (0, 1), "{rust:?}");
let out = crate::store::edges_out(&db, "caller", &[]).unwrap();
assert!(out.iter().all(|e| e.confidence == AMBIGUOUS), "{out:?}");
}
#[test]
fn a_folder_both_files_sit_in_names_no_candidate() {
let db = store(&[
("/w/flask/src/flask/app.py", "python/flask_app.py"),
("/w/flask/src/flask/__init__.py", "python/flask_init.py"),
(
"/w/flask/tests/test_config.py",
"python/flask_test_config.py",
),
("/w/flask/tests/test_basic.py", "python/flask_test_basic.py"),
]);
for from in ["test_response_type_errors", "test_config_from_object"] {
let out = crate::store::edges_out(&db, from, &[]).unwrap();
let flask: Vec<_> = out.iter().filter(|e| e.symbol.name == "Flask").collect();
assert!(!flask.is_empty(), "{from}");
for e in flask {
assert!(
e.confidence == AMBIGUOUS || e.symbol.path.contains("/src/flask/"),
"{from}: {e:?}"
);
}
}
let out = crate::store::edges_out(&db, "test_custom_config_class", &[]).unwrap();
let own: Vec<_> = out
.iter()
.filter(|e| e.symbol.name == "Flask" && e.kind == "calls")
.collect();
assert_eq!(own.len(), 1, "{own:?}");
assert_eq!(own[0].symbol.path, "/w/flask/tests/test_config.py");
}
#[test]
fn a_structural_edge_means_its_own_file() {
let db = store(&[
(".github/workflows/ci.yml", "yaml/ripgrep_ci.yml"),
("crates/core/main.rs", "rust/ripgrep_main.rs"),
("crates/ignore/src/walk.rs", "rust/ripgrep_walk.rs"),
]);
let n = neighbours(&db, "run", &[], false, &Scope::all()).unwrap();
let structural: Vec<_> = n
.callers
.iter()
.filter(|c| c.kind == "contains" || c.kind == "defines")
.collect();
assert!(!structural.is_empty());
for c in structural {
assert_eq!(c.confidence, EXTRACTED, "{c:?}");
}
let out = crate::store::edges_out(&db, "steps", &["contains".to_string()]).unwrap();
assert!(
out.iter()
.all(|e| e.symbol.path == ".github/workflows/ci.yml"),
"{out:?}"
);
}
#[test]
fn symbol_counts_every_definition_and_lists_code_first() {
let db = store(&[
(".github/workflows/ci.yml", "yaml/ripgrep_ci.yml"),
("crates/core/main.rs", "rust/ripgrep_main.rs"),
("crates/ignore/src/walk.rs", "rust/ripgrep_walk.rs"),
]);
let total = 7 + 2;
let found = evidence(&db, "run", &dependency_kinds(), 3, false, &Scope::all()).unwrap();
let kinds: Vec<&str> = found.definitions.iter().map(|d| d.kind.as_str()).collect();
assert_eq!(
&kinds[..2],
["function", "method"],
"{:?}",
found.definitions
);
let text = found.render("", "", &verbatim);
assert!(
text.contains(&format!("{total} definitions of this name")),
"{text}"
);
assert!(text.contains("3 shown"), "{text}");
let all = evidence(&db, "run", &dependency_kinds(), 20, false, &Scope::all()).unwrap();
assert_eq!(all.definitions.len(), total, "{:?}", all.definitions);
assert!(!all.render("", "", &verbatim).contains("shown"));
}
#[test]
fn several_names_answer_with_the_rows_one_name_does() {
let db = store(&[
("CHANGELOG.md", "markdown/ripgrep_changelog.md"),
("crates/core/main.rs", "rust/ripgrep_main.rs"),
]);
let one = evidence(&db, "15.2.0 (2026-07-15)", &[], 20, false, &Scope::all()).unwrap();
assert_eq!(one.definitions.len(), 1);
let names = [
"15.2.0 (2026-07-15)".to_string(),
"search_parallel".to_string(),
];
let rows = signatures(&db, &names, &Scope::all()).unwrap();
let asked: Vec<&str> = rows.iter().map(|r| r.asked.as_str()).collect();
assert_eq!(
asked,
["15.2.0 (2026-07-15)", "search_parallel"],
"{rows:?}"
);
}
#[test]
fn an_edge_end_serialises_one_kind_key() {
let db = ripgrep();
let found = evidence(
&db,
"search_parallel",
&dependency_kinds(),
20,
false,
&Scope::all(),
)
.unwrap();
assert!(!found.callers.is_empty());
let json = serde_json::to_string(&found.callers[0]).unwrap();
assert_eq!(json.matches("\"kind\":").count(), 1, "{json}");
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(value["symbol_kind"], "function", "{value}");
assert_eq!(value["kind"], "calls", "{value}");
}
#[test]
fn a_go_const_block_gives_each_name_its_own_line() {
let text = "package cobra\n\nconst (\n\t// ShellCompRequestCmd is the name of the hidden command.\n\tShellCompRequestCmd = \"__complete\"\n\t// ShellCompNoDescRequestCmd is the name of the other one.\n\tShellCompNoDescRequestCmd = \"__completeNoDesc\"\n)\n\nfunc (c *Command) initCompleteCmd() {\n\tc.AddCommand(ShellCompRequestCmd)\n}\n";
let e = extract(Path::new("completions.go"), text).unwrap().unwrap();
for (name, line) in [("ShellCompRequestCmd", 5), ("ShellCompNoDescRequestCmd", 7)] {
let s = e.symbols.iter().find(|s| s.name == name).unwrap();
assert_eq!(s.qualified, format!("completions::{name}"), "{s:?}");
assert_eq!((s.start_line, s.end_line), (line, line), "{s:?}");
}
}
#[test]
fn called_and_not_found_leaves_out_builtins_and_the_standard_library() {
let db = store(&[("command.go", "go/cobra_command.go")]);
let (targets, distinct) = crate::store::unresolved_targets(&db, 20).unwrap();
let names: Vec<&str> = targets.iter().map(|(n, _)| n.as_str()).collect();
assert!(names.contains(&"validateRequiredFlags"), "{names:?}");
for builtin in [
"Errorf",
"len",
"append",
"os",
"fmt",
"strings",
"TrimSpace",
"Fprintln",
] {
assert!(!names.contains(&builtin), "{builtin} in {names:?}");
}
assert_eq!(distinct, names.len() as i64);
assert!(crate::store::builtin_targets(&db).unwrap() >= 7);
}
}