Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 142 additions & 1 deletion crates/tracedecay-code-extraction/src/rust_extractor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{
time::Instant,
};

use tree_sitter::{Node as TsNode, Parser, Range, Tree};
use tree_sitter::{Node as TsNode, Parser, Point, Range, Tree};

use crate::common::local_node_id;
use crate::complexity::{RUST_COMPLEXITY, count_complexity};
Expand All @@ -28,6 +28,14 @@ struct ShadowedCallNames {
names: Vec<String>,
}

/// The names one block's `use` declarations bind, with the path each names.
struct BlockUseScope {
start: Point,
end: Point,
/// `None` binds the name to an item this file may itself define.
paths: BTreeMap<String, Option<String>>,
}

/// Receiver bindings whose type the function body states outright: typed
/// parameters, typed `let`s, `let`s initialised by a struct literal
/// (`T { .. }`, possibly behind `?`), and `self` in a method. A dotted
Expand Down Expand Up @@ -381,6 +389,7 @@ impl RustExtractor {
Self::collect_receiver_types(state, node, node, &mut receivers);
Self::extract_call_sites(state, node, &id, &receivers);
Self::suppress_shadowed_calls(state, node, &id);
Self::qualify_block_scoped_uses(state, node, &id);

Self::extract_annotations_from_modifiers(state, node, &id);

Expand Down Expand Up @@ -2333,6 +2342,138 @@ impl RustExtractor {
});
}

/// A `use` inside a block binds its names for that whole block, nested
/// blocks included, and shadows a module-scope import of the same name.
/// Import rows are file-scoped, so a call through a block `use` is
/// rewritten to the declared path here; the qualified resolver then binds
/// it exactly like a `crate::`/`super::`/extern path. A path that may
/// stay inside this file keeps its bare name, which binds same-file
/// items; the cross-file resolver never binds into the referencing file.
fn qualify_block_scoped_uses(
state: &mut ExtractionState<'_>,
function: TsNode<'_>,
fn_node_id: &str,
) {
let mut scopes = Vec::new();
Self::collect_block_use_scopes(state, function, function, &mut scopes);
if scopes.is_empty() {
return;
}
for reference in &mut state.unresolved_refs {
if reference.from_node_id != fn_node_id || reference.reference_name.contains('.') {
continue;
}
let head = reference
.reference_name
.split("::")
.next()
.unwrap_or_default();
let site = Point {
row: reference.line as usize,
column: reference.column as usize,
};
// Pre-order: an inner block follows the block that contains it.
let declared = scopes
.iter()
.rev()
.filter(|scope| scope.start <= site && site < scope.end)
.find_map(|scope| scope.paths.get(head));
if let Some(Some(path)) = declared {
reference.reference_name =
format!("{path}{}", &reference.reference_name[head.len()..]);
}
}
}

/// Whether a block `use` path names an item outside this file: `self::`
/// (and `crate::` from a crate root) only when it continues through a
/// root `mod name;` file module, and `super::` only from a file module.
fn block_use_path_leaves_file(
state: &ExtractionState<'_>,
function: TsNode<'_>,
path: &str,
) -> bool {
let crate_root = state.file_path == "lib.rs"
|| state.file_path == "main.rs"
|| state.file_path.ends_with("/lib.rs")
|| state.file_path.ends_with("/main.rs");
let mut segments = path.split("::");
let file_relative = match segments.next() {
Some("self") => true,
Some("crate") => crate_root,
Some("super") => {
return !Self::ancestors(function).any(|node| node.kind() == "mod_item");
}
_ => false,
};
if !file_relative {
return true;
}
let Some(module) = segments.next() else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Traverse nested modules before classifying imports as same-file

For a valid path such as crate::outer::disk::g where outer is an inline module but disk is declared as mod disk;, this examines only the first segment (outer) and concludes that the target remains in the current file. The call is consequently left bare and cannot resolve to outer/disk.rs::g. Classification needs to walk the complete module path, since a later segment can cross a file boundary.

Useful? React with 👍 / 👎.

return false;
};
let Some(root) = Self::ancestors(function).find(|node| node.kind() == "source_file") else {
return false;
};
let mut cursor = root.walk();
root.named_children(&mut cursor).any(|item| {
item.kind() == "mod_item"
&& item.child_by_field_name("body").is_none()
&& item
.child_by_field_name("name")
.is_some_and(|name| state.node_text(name) == module)
})
}

fn ancestors(node: TsNode<'_>) -> impl Iterator<Item = TsNode<'_>> {
std::iter::successors(node.parent(), TsNode::parent)
}

fn collect_block_use_scopes(
state: &mut ExtractionState<'_>,
node: TsNode<'_>,
function: TsNode<'_>,
scopes: &mut Vec<BlockUseScope>,
) {
if node != function && node.kind() == "function_item" {
return;
}
if node.kind() == "block" {
let mut paths = BTreeMap::new();
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if child.kind() != "use_declaration" {
continue;
}
let Some(argument) = child.child_by_field_name("argument") else {
continue;
};
let first = state.imports.len();
Self::extract_use_bindings(state, argument, None, false, None);
let bindings = state.imports.drain(first..).collect::<Vec<_>>();
for import in bindings {
if let (Some(local), Some(imported)) = (import.local_name, import.imported_name)
{
let path = format!("{}::{imported}", import.module_specifier);
let leaves_file = Self::block_use_path_leaves_file(state, function, &path);
paths.insert(local, leaves_file.then_some(path));
Comment on lines +2457 to +2459

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain the target of same-file aliased imports

When a block import aliases an item defined in the same file, such as use self::inner::x as y, then_some(path) stores None and the call remains named y. The same-file resolver therefore cannot select inner::x; worse, if another same-file function is actually named y, it emits a false call edge to that function even though the block import shadows it. Preserve enough path information to resolve aliases rather than treating every same-file import as safely bindable by its local name.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not overwrite cfg-selected block imports

Valid platform-selected imports can bind the same local name in one block, for example #[cfg(unix)] use crate::unix::g; followed by #[cfg(windows)] use crate::windows::g;. This unconditional map insertion discards the first binding without inspecting either cfg, so every call is attributed to whichever declaration appears last, even when that declaration is inactive for the indexed platform. Retain all conditional alternatives or abstain instead of producing a deterministic false caller edge.

Useful? React with 👍 / 👎.

}
}
}
if !paths.is_empty() {
scopes.push(BlockUseScope {
start: node.start_position(),
end: node.end_position(),
paths,
});
}
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
Self::collect_block_use_scopes(state, child, function, scopes);
}
}

fn collect_shadowed_names(
state: &ExtractionState<'_>,
node: TsNode<'_>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,7 @@ fn retained_stale_rust_extractor_generation_is_refused_and_rebuilt() {
.iter()
.find(|(language, _)| language.as_str() == "rust")
.map(|(_, revision)| revision.as_str()),
Some("extractor.rust.v14")
Some("extractor.rust.v15")
);
}

Expand Down
7 changes: 4 additions & 3 deletions crates/tracedecay-code-index/src/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -761,12 +761,13 @@ mod tests {
// `self` from the enclosing impl or trait; v12 binds a receiver typed
// by a type parameter with one trait bound to `Trait::method`; v13
// retains parser-positioned receiver-call evidence; v14 extracts
// item-list macro bodies and binds `#[cfg]` variants. The revision is
// item-list macro bodies and binds `#[cfg]` variants; v15 qualifies
// calls through block-scoped `use` declarations. The revision is
// part of the batch identity, so the pinned digest moves with it.
assert_eq!(descriptor.extractor_revision.as_str(), "extractor.rust.v14");
assert_eq!(descriptor.extractor_revision.as_str(), "extractor.rust.v15");
assert_eq!(
extraction.batch().rows_digest.as_str(),
"sha256:ee93409e76028857423e575422b01262d32c61a001213ca71d4cd06f7bf34162"
"sha256:c7f72d389d8521307a60ca4373df3f9551c15e6527b1b5f68b34e80db87b38af"
);
}

Expand Down
8 changes: 5 additions & 3 deletions crates/tracedecay-code-index/src/languages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,9 @@ impl StaticLanguageRegistry {
// parse as item lists (`cfg_rt! { ... }`), records other bodies as
// unexpanded `name!` macro nodes, binds calls to every `#[cfg]`
// variant of one definition, and retains qualified calls through a
// declared module whose name is also a blocklisted std name.
// declared module whose name is also a blocklisted std name. Rust
// v15 rewrites a call through a `use` declared in a block to the
// path that `use` names, so it binds like a module-scope import.
// TypeScript v7 records `export … from` forwarding as public
// import evidence and retains explicitly imported ubiquitous names
// as cross-file candidates, so barrels and workspace packages bind.
Expand All @@ -238,7 +240,7 @@ impl StaticLanguageRegistry {
// comments and `///` lost its stray `/`; QBasic dialects moved when
// CONST names stopped losing their text before an underscore.
let extractor_revision = match language.as_str() {
"rust" => 14,
"rust" => 15,
"typescript" => 9,
"protobuf" => 7,
"sql" => 6,
Expand Down Expand Up @@ -443,7 +445,7 @@ mod tests {
assert!(rust.stable_member_spans);
assert!(rust.capabilities.extraction);
assert_eq!(rust.root_markers, vec!["Cargo.toml".to_owned()]);
assert_eq!(rust.extractor_revision.as_str(), "extractor.rust.v14");
assert_eq!(rust.extractor_revision.as_str(), "extractor.rust.v15");

assert_eq!(
registry
Expand Down
4 changes: 2 additions & 2 deletions crates/tracedecay-code-index/src/production/worker_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ fn extractor_revision_change_reextracts_before_validating_retained_import_rows()

assert_eq!(
rebuilt.files[0].extraction.extractor_revision.as_str(),
"extractor.rust.v14"
"extractor.rust.v15"
);
assert_ne!(
rebuilt.files[0].extraction.parser_import_rows_digest,
Expand Down Expand Up @@ -490,7 +490,7 @@ fn physical_artifact_reuse_rejects_a_stale_extractor_revision() {

assert_eq!(
rebuilt.files[0].extraction.extractor_revision.as_str(),
"extractor.rust.v14"
"extractor.rust.v15"
);
assert!(
rebuilt.files[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1227,3 +1227,97 @@ fn rust_items_inside_item_list_macros_are_symbols_with_resolved_callers() {
]
);
}

fn resolved_callers<'a>(
generation: &'a CodeIndexPublishedGenerationV1,
target: &SymbolOccurrenceId,
) -> Vec<&'a str> {
let mut callers = generation
.edges()
.iter()
.filter(|edge| edge.to_occurrence == *target && edge.kind == RelationEdgeKindV1::Calls)
.filter_map(|edge| {
generation
.symbols()
.symbols
.iter()
.find(|symbol| symbol.occurrence == edge.from_occurrence)
.map(|symbol| symbol.qualified_name.as_str())
})
.collect::<Vec<_>>();
callers.sort_unstable();
callers
}

#[test]
fn rust_calls_bind_through_a_use_declared_in_the_calling_block() {
let generation = published_rust_workspace(&[
(
"file.block-use.lib",
"crates/app/src/lib.rs",
"mod m;\nmod n;\nmod runtime;\nuse crate::n::g;\n\
pub fn f() { use crate::m::g; g(); }\n\
pub fn module_scope() { g(); }\n\
pub fn spawn_inner() {\n use crate::runtime::{context, task};\n context::with_current();\n task::schedule();\n}\n\
pub fn timer() {\n #[cfg(feature = \"rt\")]\n {\n use crate::runtime::context;\n context::with_current();\n }\n}\n\
mod inner { pub fn x() {} }\n\
pub fn same_file() { use self::inner::x; x(); }\n",
),
("file.block-use.m", "crates/app/src/m.rs", "pub fn g() {}\n"),
("file.block-use.n", "crates/app/src/n.rs", "pub fn g() {}\n"),
(
"file.block-use.runtime",
"crates/app/src/runtime/mod.rs",
"pub mod context;\npub mod task;\n",
),
(
"file.block-use.context",
"crates/app/src/runtime/context.rs",
"mod current;\npub(crate) use current::with_current;\n",
),
(
"file.block-use.current",
"crates/app/src/runtime/context/current.rs",
"pub(crate) fn with_current() {}\n",
),
(
"file.block-use.task",
"crates/app/src/runtime/task.rs",
"pub fn schedule() {}\n",
),
]);
let block_g = symbol_occurrence(&generation, "crates/app/src/m.rs::g");
let module_g = symbol_occurrence(&generation, "crates/app/src/n.rs::g");
let with_current = symbol_occurrence(
&generation,
"crates/app/src/runtime/context/current.rs::with_current",
);
let schedule = symbol_occurrence(&generation, "crates/app/src/runtime/task.rs::schedule");
let inner_x = symbol_occurrence(&generation, "crates/app/src/lib.rs::inner::x");

// The block's `use` shadows the module-scope import only inside `f`.
assert_eq!(
resolved_callers(&generation, &block_g),
["crates/app/src/lib.rs::f"]
);
assert_eq!(
resolved_callers(&generation, &module_g),
["crates/app/src/lib.rs::module_scope"]
);
assert_eq!(
resolved_callers(&generation, &with_current),
[
"crates/app/src/lib.rs::spawn_inner",
"crates/app/src/lib.rs::timer"
]
);
assert_eq!(
resolved_callers(&generation, &schedule),
["crates/app/src/lib.rs::spawn_inner"]
);
// A block `use` of this file's own item keeps binding it in-file.
assert_eq!(
resolved_callers(&generation, &inner_x),
["crates/app/src/lib.rs::same_file"]
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2984,22 +2984,22 @@ fn partitioned_codec_fixture() -> (
}

const PARTITIONED_FORMAT_STATE_DIGEST: &str =
"sha256:1cf2d05997094d9e01c88c7482eef7e405c5c30b6e6bc3ff7d223105fab33c0b";
"sha256:824e5692181d785e3722fc0a4465b2d21aaac106bcc45f684bd0350693558149";
const PARTITIONED_FORMAT_SEGMENTS: &[(&str, u64)] = &[
(
"sha256:65f5ee94b70747310ed41f45b6a525ad30f554b8060a388830959adac394347f",
"sha256:810c283c12aeeae815061185f2936982b31666488763adbf3b1212ca68a5b2a9",
1_734,
),
(
"sha256:e2e4ff0252168f278eb5270b4b05b6463fa6ecc961b03638e1fa1e5a9e3f2050",
1_262,
"sha256:ff6dc0319b2f147a5fb56aa00248462133db05f78252309f0a1dfdfac051335b",
1_263,
),
(
"sha256:304facbe2ba45ce8ec684842962237cd235b7ee6ee7327d8bd64b35d796fa5d7",
"sha256:cde12a92d48833fd210770a1e3da3fab56015801ca376cf920bbcc8441fc3d31",
1_310,
),
(
"sha256:0735457fdd43120b1cea3f13d983e0ff583e8d0802e96fd506f991aac6dfa1c3",
"sha256:f2fd9edd26bb0724336d5e288fd9d33beed496a8ef543fbffbd76bdcd77c9d5d",
2_837,
),
];
Expand Down
Loading