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
72 changes: 72 additions & 0 deletions crates/codegraph-cli/tests/sync_incremental.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,78 @@ fn sync_after_file_removal_equals_index_force() {
.expect("sync after file removal must equal a full index --force from scratch");
}

/// A JavaScript `add` that competes, by name alone, with `src/math.ts`'s `add`.
/// While both are indexed, `Counter.increment`'s same-file call to `add`
/// resolves by exact name at a lower confidence than when `add` is unique, so
/// the competitor arriving or leaving changes an edge inside an unchanged file.
fn write_competing_add(project: &Path) {
fs::create_dir_all(project.join("web")).unwrap();
fs::write(
project.join("web/bundle.js"),
"function add(a, b) {\n return a + b;\n}\n",
)
.unwrap();
}

#[test]
fn sync_after_removing_a_same_named_competitor_equals_index_force() {
assert_sync_equals_index_force("remove-competitor", write_competing_add, |project| {
fs::remove_file(project.join("web/bundle.js")).unwrap();
});
}

#[test]
fn sync_after_adding_a_same_named_competitor_equals_index_force() {
assert_sync_equals_index_force("add-competitor", |_| {}, write_competing_add);
}

#[test]
fn sync_after_exporting_a_same_named_candidate_equals_index_force() {
// An `export` added on the same line keeps the node's id, yet it lets
// `src/f.ts`'s `foo` tie with `src/h.ts`'s for `main`'s bare call.
assert_sync_equals_index_force(
"export-candidate",
|project| {
fs::write(
project.join("src/h.ts"),
"export function foo(): number {\n return 1;\n}\n",
)
.unwrap();
fs::write(
project.join("src/f.ts"),
"function foo(): number {\n return 2;\n}\n",
)
.unwrap();
fs::write(
project.join("src/g.ts"),
"function main(): number {\n return foo();\n}\n\nmain();\n",
)
.unwrap();
},
|project| {
fs::write(
project.join("src/f.ts"),
"export function foo(): number {\n return 2;\n}\n",
)
.unwrap();
},
);
}

#[test]
fn sync_after_excluding_a_same_named_competitor_equals_index_force() {
// `[indexing] exclude` takes the competitor out of scope while it stays on
// disk: the same removal as a delete, reached through the scan.
assert_sync_equals_index_force("exclude-competitor", write_competing_add, |project| {
fs::create_dir_all(project.join(".codegraph")).unwrap();
fs::write(
project.join(".codegraph/config.toml"),
"[app]\nname = \"mini\"\n\n[indexing]\nexclude = [\"web/\"]\n",
)
.unwrap();
});
}

fn prepend_game_flow_comment(project: &Path) {
let path = project.join("game_flow.gd");
let original = fs::read_to_string(&path).unwrap();
Expand Down
50 changes: 49 additions & 1 deletion crates/codegraph-store/src/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1613,6 +1613,32 @@ impl Store {
pub fn reference_sites_of_edges_to_named_targets(
&self,
names: &[String],
) -> rusqlite::Result<Vec<FileReferenceSite>> {
self.reference_sites_to_named_targets(names, "!=")
}

/// Same-file counterpart of
/// [`Self::reference_sites_of_edges_to_named_targets`]: surviving
/// non-`contains` edges whose source and target share a file and whose
/// target name is selected. An exact-name resolution's confidence counts every
/// node of that name in the project, so a competitor arriving in or leaving
/// another file changes an edge inside a file nobody touched. Sync selects
/// only names whose candidates changed, not names whose nodes merely moved: a
/// candidate outside the referencing file is scored without its position, and
/// the same-file target outranks it either way.
pub fn reference_sites_of_same_file_edges_to_named_targets(
&self,
names: &[String],
) -> rusqlite::Result<Vec<FileReferenceSite>> {
self.reference_sites_to_named_targets(names, "=")
}

/// The body of both named-target site queries; `file_relation` is the SQL
/// operator relating the edge's source and target files, `!=` or `=`.
fn reference_sites_to_named_targets(
&self,
names: &[String],
file_relation: &str,
) -> rusqlite::Result<Vec<FileReferenceSite>> {
if names.is_empty() {
return Ok(Vec::new());
Expand All @@ -1632,7 +1658,7 @@ impl Store {
JOIN nodes src ON src.id = e.source
WHERE tgt.name IN ({placeholders})
AND e.kind != 'contains'
AND src.file_path != tgt.file_path
AND src.file_path {file_relation} tgt.file_path
ORDER BY src.file_path, e.source, e.line, e.col"#
);
let params = chunk
Expand Down Expand Up @@ -3611,6 +3637,28 @@ mod tests {
file_path: "fallback.rs".to_string(),
site: None,
}));
assert!(
named.iter().all(|entry| entry.file_path != "b.rs"),
"the cross-file query leaves same-file edges out"
);
let same_file = store
.reference_sites_of_same_file_edges_to_named_targets(&[
"callee".to_string(),
"sibling".to_string(),
])
.unwrap();
assert_eq!(
same_file,
vec![FileReferenceSite {
file_path: "b.rs".to_string(),
site: Some(ReferenceSite {
from_node_id: "function:self".to_string(),
line: 10,
col: 2,
}),
}],
"the same-file query returns exactly the edge whose source shares its target's file"
);

let unresolved = |name: &str, line: i64| UnresolvedRef {
id: None,
Expand Down
Loading
Loading