#![allow(clippy::panic)]
use super::support::*;
use crate::indexer::{StructuralQueryAnchorSelection, search_structural_grouped_in_source};
#[test]
fn structural_search_rust_returns_deterministic_captures() -> FriggResult<()> {
let source = "pub fn first() {}\n\
pub fn second() {}\n";
let path = Path::new("fixtures/structural.rs");
let query = "(function_item) @function";
let first = search_structural_in_source(SymbolLanguage::Rust, path, source, query)?;
let second = search_structural_in_source(SymbolLanguage::Rust, path, source, query)?;
assert_eq!(first, second, "structural captures should be deterministic");
assert_eq!(first.len(), 2);
assert_eq!(
first
.iter()
.map(|matched| {
(
matched.path.clone(),
matched.span.start_line,
matched.span.start_column,
)
})
.collect::<Vec<_>>(),
vec![
(PathBuf::from("fixtures/structural.rs"), 1, 1),
(PathBuf::from("fixtures/structural.rs"), 2, 1),
]
);
Ok(())
}
#[test]
fn structural_search_typescript_tsx_uses_extension_aware_grammar() -> FriggResult<()> {
let matches = search_structural_in_source(
SymbolLanguage::TypeScript,
Path::new("fixtures/component.tsx"),
typescript_tsx_fixture(),
"(jsx_self_closing_element) @jsx",
)?;
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].path, PathBuf::from("fixtures/component.tsx"));
assert_eq!(matches[0].span.start_line, 1);
assert_eq!(matches[0].excerpt, "<Button />");
Ok(())
}
#[test]
fn structural_search_python_returns_deterministic_captures() -> FriggResult<()> {
let first = search_structural_in_source(
SymbolLanguage::Python,
Path::new("fixtures/python_symbols.py"),
python_symbols_fixture(),
"(function_definition) @fn",
)?;
let second = search_structural_in_source(
SymbolLanguage::Python,
Path::new("fixtures/python_symbols.py"),
python_symbols_fixture(),
"(function_definition) @fn",
)?;
assert_eq!(first, second);
assert_eq!(first.len(), 2);
assert_eq!(first[0].span.start_line, 3);
assert_eq!(first[1].span.start_line, 6);
Ok(())
}
#[test]
fn structural_search_go_returns_deterministic_captures() -> FriggResult<()> {
let first = search_structural_in_source(
SymbolLanguage::Go,
Path::new("fixtures/go_symbols.go"),
go_symbols_fixture(),
"(function_declaration) @fn",
)?;
let second = search_structural_in_source(
SymbolLanguage::Go,
Path::new("fixtures/go_symbols.go"),
go_symbols_fixture(),
"(function_declaration) @fn",
)?;
assert_eq!(first, second);
assert_eq!(first.len(), 1);
assert_eq!(first[0].span.start_line, 6);
Ok(())
}
#[test]
fn structural_search_kotlin_returns_deterministic_captures() -> FriggResult<()> {
let first = search_structural_in_source(
SymbolLanguage::Kotlin,
Path::new("fixtures/kotlin_symbols.kt"),
kotlin_symbols_fixture(),
"(function_declaration) @fn",
)?;
let second = search_structural_in_source(
SymbolLanguage::Kotlin,
Path::new("fixtures/kotlin_symbols.kt"),
kotlin_symbols_fixture(),
"(function_declaration) @fn",
)?;
assert_eq!(first, second);
assert_eq!(first.len(), 2);
assert_eq!(first[0].span.start_line, 4);
assert_eq!(first[1].span.start_line, 7);
Ok(())
}
#[test]
fn structural_search_java_returns_deterministic_captures() -> FriggResult<()> {
let first = search_structural_in_source(
SymbolLanguage::Java,
Path::new("fixtures/java_symbols.java"),
java_symbols_fixture(),
"(method_declaration) @fn",
)?;
let second = search_structural_in_source(
SymbolLanguage::Java,
Path::new("fixtures/java_symbols.java"),
java_symbols_fixture(),
"(method_declaration) @fn",
)?;
assert_eq!(first, second);
assert_eq!(first.len(), 2);
assert_eq!(first[0].span.start_line, 7);
assert_eq!(first[1].span.start_line, 10);
Ok(())
}
#[test]
fn structural_search_lua_returns_deterministic_captures() -> FriggResult<()> {
let first = search_structural_in_source(
SymbolLanguage::Lua,
Path::new("fixtures/lua_symbols.lua"),
lua_symbols_fixture(),
"(function_declaration) @fn",
)?;
let second = search_structural_in_source(
SymbolLanguage::Lua,
Path::new("fixtures/lua_symbols.lua"),
lua_symbols_fixture(),
"(function_declaration) @fn",
)?;
assert_eq!(first, second);
assert_eq!(first.len(), 2);
assert_eq!(first[0].span.start_line, 1);
assert_eq!(first[1].span.start_line, 4);
Ok(())
}
#[test]
fn structural_search_nim_returns_deterministic_captures() -> FriggResult<()> {
let first = search_structural_in_source(
SymbolLanguage::Nim,
Path::new("fixtures/nim_symbols.nim"),
nim_symbols_fixture(),
"(proc_declaration) @proc",
)?;
let second = search_structural_in_source(
SymbolLanguage::Nim,
Path::new("fixtures/nim_symbols.nim"),
nim_symbols_fixture(),
"(proc_declaration) @proc",
)?;
assert_eq!(first, second);
assert_eq!(first.len(), 1);
assert_eq!(first[0].span.start_line, 4);
Ok(())
}
#[test]
fn structural_search_roc_returns_deterministic_captures() -> FriggResult<()> {
let first = search_structural_in_source(
SymbolLanguage::Roc,
Path::new("fixtures/roc_symbols.roc"),
roc_symbols_fixture(),
"(value_declaration) @value",
)?;
let second = search_structural_in_source(
SymbolLanguage::Roc,
Path::new("fixtures/roc_symbols.roc"),
roc_symbols_fixture(),
"(value_declaration) @value",
)?;
assert_eq!(first, second);
assert_eq!(first.len(), 2);
assert_eq!(first[0].span.start_line, 2);
assert_eq!(first[1].span.start_line, 4);
Ok(())
}
#[test]
fn structural_search_rejects_invalid_query_with_typed_error() {
let error = search_structural_in_source(
SymbolLanguage::Rust,
Path::new("fixtures/structural.rs"),
"pub fn first() {}\n",
"(function_item @broken",
)
.expect_err("invalid query must return error");
match error {
FriggError::InvalidInput(message) => {
assert!(message.contains("invalid structural query"));
}
other => panic!("expected invalid input, got {other:?}"),
}
}
#[test]
fn structural_search_grouped_mode_groups_multi_capture_matches() -> FriggResult<()> {
let source = "pub fn first() {}\n\
pub fn second() {}\n";
let path = Path::new("fixtures/structural.rs");
let query = "(function_item name: (identifier) @name) @match";
let grouped =
search_structural_grouped_in_source(SymbolLanguage::Rust, path, source, query, None)?;
assert_eq!(grouped.len(), 2);
assert_eq!(grouped[0].anchor_capture_name.as_deref(), Some("match"));
assert_eq!(
grouped[0].anchor_selection,
StructuralQueryAnchorSelection::MatchCapture
);
assert_eq!(grouped[0].captures.len(), 2);
assert_eq!(grouped[0].captures[0].name, "match");
assert_eq!(grouped[0].captures[1].name, "name");
assert_eq!(grouped[0].excerpt, "pub fn first() {}");
Ok(())
}
#[test]
fn structural_search_grouped_mode_honors_primary_capture() -> FriggResult<()> {
let source = "pub fn first() {}\n";
let path = Path::new("fixtures/structural.rs");
let query = "(function_item name: (identifier) @name) @match";
let grouped = search_structural_grouped_in_source(
SymbolLanguage::Rust,
path,
source,
query,
Some("name"),
)?;
assert_eq!(grouped.len(), 1);
assert_eq!(grouped[0].anchor_capture_name.as_deref(), Some("name"));
assert_eq!(
grouped[0].anchor_selection,
StructuralQueryAnchorSelection::PrimaryCapture
);
assert_eq!(grouped[0].excerpt, "first");
Ok(())
}
#[test]
fn generated_follow_up_structural_prefers_useful_ancestors_deterministically() -> FriggResult<()> {
let source = "pub fn greet() {\n helper();\n}\n\nfn helper() {}\n";
let path = Path::new("fixtures/inspect.rs");
let (inspection, first) = inspect_syntax_tree_with_follow_up_in_source(
SymbolLanguage::Rust,
path,
"src/lib.rs",
source,
Some(2),
Some(6),
8,
4,
"repo-001",
)?;
let second = generated_follow_up_structural_at_location_in_source(
SymbolLanguage::Rust,
path,
"src/lib.rs",
source,
2,
6,
"repo-001",
)?;
assert_eq!(first, second);
assert_eq!(first.len(), 3);
assert_eq!(first[0].params.query, "(call_expression) @match");
assert_eq!(first[1].params.query, "(call_expression) @match");
assert_eq!(first[2].params.query, "(function_item) @match");
assert_eq!(
first[0].params.path_regex.as_deref(),
Some("^src/lib\\.rs$")
);
assert_eq!(inspection.focus.kind, "call_expression");
assert_eq!(
inspection.raw_focus.as_ref().map(|node| node.kind.as_str()),
Some("identifier")
);
assert_eq!(first[0].basis.raw_focus_kind.as_deref(), Some("identifier"));
Ok(())
}
#[test]
fn generated_follow_up_structural_omits_wrapper_only_candidates() -> FriggResult<()> {
let source = "pub fn greet() {}\n";
let path = Path::new("fixtures/wrapper_only.rs");
let (_inspection, follow_ups) = inspect_syntax_tree_with_follow_up_in_source(
SymbolLanguage::Rust,
path,
"src/lib.rs",
source,
None,
None,
8,
4,
"repo-001",
)?;
assert!(follow_ups.is_empty());
Ok(())
}
#[test]
fn heuristic_references_combines_graph_hints_and_lexical_fallback_deterministically()
-> FriggResult<()> {
let source = "pub struct User;\n\
pub fn create_user() -> User { User }\n\
pub fn use_user() { let _ = User; }\n\
pub fn marker() { let _ = User; }\n\
pub fn unrelated() { let _ = \"SuperUser\"; }\n";
let path = PathBuf::from("fixtures/heuristic.rs");
let symbols = extract_symbols_from_source(SymbolLanguage::Rust, &path, source)?;
let target =
find_symbol(&symbols, SymbolKind::Struct, "User", 1).expect("expected target symbol");
let create_user = find_symbol(&symbols, SymbolKind::Function, "create_user", 2)
.expect("expected create_user symbol");
let use_user = find_symbol(&symbols, SymbolKind::Function, "use_user", 3)
.expect("expected use_user symbol");
let mut graph = SymbolGraph::default();
register_symbol_definitions(&mut graph, "repo-001", &symbols);
assert!(
graph
.add_relation(
&create_user.stable_id,
&target.stable_id,
RelationKind::RefersTo
)
.expect("refers_to relation should be added")
);
assert!(
graph
.add_relation(&use_user.stable_id, &target.stable_id, RelationKind::Calls)
.expect("calls relation should be added")
);
let mut sources = BTreeMap::new();
sources.insert(path.clone(), source.to_owned());
let first =
resolve_heuristic_references("repo-001", &target.stable_id, &symbols, &graph, &sources);
let second =
resolve_heuristic_references("repo-001", &target.stable_id, &symbols, &graph, &sources);
assert_eq!(
first, second,
"heuristic references should be deterministic"
);
assert_eq!(
first
.iter()
.map(|reference| (reference.line, reference.confidence))
.collect::<Vec<_>>(),
vec![
(2, HeuristicReferenceConfidence::High),
(2, HeuristicReferenceConfidence::High),
(2, HeuristicReferenceConfidence::High),
(3, HeuristicReferenceConfidence::High),
(3, HeuristicReferenceConfidence::High),
(4, HeuristicReferenceConfidence::Low),
]
);
let line_two_columns = first
.iter()
.filter(|reference| reference.line == 2)
.map(|reference| reference.column)
.collect::<Vec<_>>();
assert_eq!(line_two_columns.len(), 3);
assert!(
line_two_columns
.windows(2)
.all(|window| window[0] <= window[1]),
"line-2 references should be ordered by column deterministically"
);
assert!(
matches!(
first[0].evidence,
HeuristicReferenceEvidence::GraphRelation { .. }
),
"highest-confidence hint should come from graph relation evidence"
);
assert!(
!first.iter().any(|reference| reference.line == 5),
"substring-only lexical tokens should not be returned"
);
Ok(())
}
#[test]
fn heuristic_references_false_positive_bound_for_substring_tokens() -> FriggResult<()> {
let source = "<?php\n\
class User {}\n\
function true_ref(): void { $x = new User(); }\n\
function noise(): void {\n\
$a = 'SuperUser';\n\
$b = 'UserService';\n\
}\n";
let path = PathBuf::from("fixtures/heuristic.php");
let symbols = extract_symbols_from_source(SymbolLanguage::Php, &path, source)?;
let target =
find_symbol(&symbols, SymbolKind::Class, "User", 2).expect("expected class symbol");
let true_ref = find_symbol(&symbols, SymbolKind::Function, "true_ref", 3)
.expect("expected true_ref symbol");
let mut graph = SymbolGraph::default();
register_symbol_definitions(&mut graph, "repo-001", &symbols);
assert!(
graph
.add_relation(
&true_ref.stable_id,
&target.stable_id,
RelationKind::RefersTo
)
.expect("refers_to relation should be added")
);
let mut sources = BTreeMap::new();
sources.insert(path, source.to_owned());
let references =
resolve_heuristic_references("repo-001", &target.stable_id, &symbols, &graph, &sources);
assert_eq!(
references
.iter()
.map(|reference| (reference.line, reference.confidence))
.collect::<Vec<_>>(),
vec![
(3, HeuristicReferenceConfidence::High),
(3, HeuristicReferenceConfidence::High),
],
"same-line heuristic references should preserve both graph and lexical hits"
);
let same_line_columns = references
.iter()
.map(|reference| reference.column)
.collect::<Vec<_>>();
assert_eq!(same_line_columns.len(), 2);
assert!(
same_line_columns
.windows(2)
.all(|window| window[0] <= window[1]),
"same-line references should be ordered by column"
);
let low_confidence = references
.iter()
.filter(|reference| reference.confidence == HeuristicReferenceConfidence::Low)
.count();
assert_eq!(
low_confidence, 0,
"false-positive lower bound violated: expected no low-confidence noise hits"
);
Ok(())
}
#[test]
fn heuristic_references_preserve_multiple_same_line_lexical_hits() -> FriggResult<()> {
let source = "pub struct User;\n\
pub fn use_user() { let _a = User; let _b = User; }\n";
let path = PathBuf::from("fixtures/heuristic-same-line.rs");
let symbols = extract_symbols_from_source(SymbolLanguage::Rust, &path, source)?;
let target =
find_symbol(&symbols, SymbolKind::Struct, "User", 1).expect("expected target symbol");
let sources = BTreeMap::from([(path, source.to_owned())]);
let references = resolve_heuristic_references(
"repo-001",
&target.stable_id,
&symbols,
&SymbolGraph::default(),
&sources,
);
assert_eq!(
references.len(),
2,
"same-line lexical references should retain both token hits"
);
assert_eq!(
references
.iter()
.map(|reference| (reference.line, reference.column, reference.confidence))
.collect::<Vec<_>>(),
vec![
(2, 30, HeuristicReferenceConfidence::Low),
(2, 45, HeuristicReferenceConfidence::Low),
]
);
assert!(
references.iter().all(|reference| matches!(
reference.evidence,
HeuristicReferenceEvidence::LexicalToken
)),
"same-line lexical hits should retain lexical evidence"
);
Ok(())
}