use anyhow::{Context, Result};
use tree_sitter::{Parser, Query, QueryCursor, StreamingIterator};
use crate::engine::parse_tree::ParseTreeEngine;
use crate::engine::tokenizer::normalize;
#[derive(Debug, Clone, PartialEq)]
pub struct CodeMatch {
pub doc_id: String,
pub pattern: String,
pub start_byte: usize,
pub end_byte: usize,
pub snippet: String,
pub captures: std::collections::HashMap<String, String>,
}
impl CodeMatch {
#[must_use]
pub fn is_function(&self) -> bool {
self.pattern.contains("function") || self.pattern.contains("fn")
}
#[must_use]
pub fn is_error_handling(&self) -> bool {
self.pattern.contains("unwrap") || self.pattern.contains("expect") || self.pattern.contains("Result")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodePattern {
FunctionDefinition,
MethodCall,
ErrorHandling,
TodoMarker,
ImportStatement,
DeprecatedApi,
}
impl CodePattern {
#[must_use]
pub fn query_str_for(&self, language: &str) -> &'static str {
match (self, language) {
(CodePattern::FunctionDefinition, "rust") => r#"
(function_item name: (identifier) @name) @func
(function_signature_item name: (identifier) @name) @func
"#,
(CodePattern::FunctionDefinition, "python") => r#"
(function_definition name: (identifier) @name) @func
"#,
(CodePattern::FunctionDefinition, _) => r#"
(function_declaration name: (identifier) @name) @func
(method_declaration name: (identifier) @name) @func
(method_definition name: (identifier) @name) @func
"#,
(CodePattern::MethodCall, "rust") => r#"
(call_expression function: (identifier) @callee) @call
(call_expression function: (field_expression field: (field_identifier) @method)) @call
(macro_invocation macro: (identifier) @callee) @call
"#,
(CodePattern::MethodCall, "python") => r#"
(call function: (identifier) @callee) @call
(call function: (attribute attribute: (identifier) @method)) @call
"#,
(CodePattern::MethodCall, _) => r#"
(call_expression function: (identifier) @callee) @call
(call_expression function: (member_expression property: (property_identifier) @method)) @call
"#,
(CodePattern::ErrorHandling, "rust") => r#"
(call_expression function: (identifier) @name) @call
(call_expression function: (field_expression field: (field_identifier) @name)) @call
(macro_invocation macro: (identifier) @name) @call
(type_identifier) @type
"#,
(CodePattern::ErrorHandling, _) => r#"
(call_expression function: (identifier) @name) @call
"#,
(CodePattern::TodoMarker, "rust") => r#"
(line_comment) @comment
(block_comment) @comment
"#,
(CodePattern::TodoMarker, "python") => r#"
(comment) @comment
"#,
(CodePattern::TodoMarker, _) => r#"
(comment) @comment
(line_comment) @comment
"#,
(CodePattern::ImportStatement, "rust") => r#"
(use_declaration) @import
"#,
(CodePattern::ImportStatement, "python") => r#"
(import_statement) @import
(import_from_statement) @import
"#,
(CodePattern::ImportStatement, _) => r#"
(import_declaration) @import
"#,
(CodePattern::DeprecatedApi, _) => r#"
((identifier) @id (#match? @id "(?i)deprecated|obsolete|legacy"))
"#,
}
}
#[must_use]
pub fn query_str(&self) -> &'static str {
self.query_str_for("generic")
}
#[must_use]
pub fn name(&self) -> &'static str {
match self {
CodePattern::FunctionDefinition => "function_definition",
CodePattern::MethodCall => "method_call",
CodePattern::ErrorHandling => "error_handling",
CodePattern::TodoMarker => "todo_marker",
CodePattern::ImportStatement => "import_statement",
CodePattern::DeprecatedApi => "deprecated_api",
}
}
}
fn check_predicates(
m: &tree_sitter::QueryMatch,
query: &Query,
source: &str,
) -> bool {
let pattern_idx = m.pattern_index;
for pred in query.general_predicates(pattern_idx) {
match pred.operator.as_ref() {
"eq?" if pred.args.len() == 2 => {
if let tree_sitter::QueryPredicateArg::Capture(cap_idx) = pred.args[0]
&& let tree_sitter::QueryPredicateArg::String(ref expected) = pred.args[1]
{
let capture = m.captures().iter().find(|c| c.index == cap_idx);
if let Some(cap) = capture {
let actual = &source[cap.node.start_byte()..cap.node.end_byte()];
if actual != expected.as_ref() {
return false;
}
}
}
}
"match?" if pred.args.len() == 2 => {
if let tree_sitter::QueryPredicateArg::Capture(cap_idx) = pred.args[0]
&& let tree_sitter::QueryPredicateArg::String(ref pattern) = pred.args[1]
{
let capture = m.captures().iter().find(|c| c.index == cap_idx);
if let Some(cap) = capture {
let actual = &source[cap.node.start_byte()..cap.node.end_byte()];
if let Ok(re) = regex::Regex::new(pattern)
&& !re.is_match(actual)
{
return false;
}
}
}
}
_ => {}
}
}
true
}
pub fn match_pattern_filtered(
source: &str,
language: &str,
pattern: &str,
) -> Result<Vec<CodeMatch>> {
let normalized = normalize(source);
let lang = tree_sitter_language_pack::get_language(language)
.with_context(|| format!("load language `{language}`"))?;
let mut parser = Parser::new();
parser.set_language(&lang)
.with_context(|| format!("set parser language `{language}`"))?;
let tree = parser.parse(&normalized, None)
.with_context(|| format!("parse source ({} bytes)", normalized.len()))?;
let query = Query::new(&lang, pattern)
.with_context(|| format!("compile query pattern for `{language}`"))?;
let mut cursor = QueryCursor::new();
let mut matches = Vec::new();
let mut captures = cursor.captures(&query, tree.root_node(), normalized.as_bytes());
while let Some((m, _)) = captures.next() {
if !check_predicates(m, &query, &normalized) {
continue;
}
let node = m.captures()[0].node;
let start_byte = node.start_byte();
let end_byte = node.end_byte();
let snippet = normalized[start_byte..end_byte].to_string();
let mut captured = std::collections::HashMap::new();
for capture in m.captures() {
let name = query.capture_names()[capture.index as usize].to_string();
let text = normalized[capture.node.start_byte()..capture.node.end_byte()].to_string();
captured.insert(name, text);
}
matches.push(CodeMatch {
doc_id: String::new(),
pattern: pattern.to_string(),
start_byte,
end_byte,
snippet,
captures: captured,
});
}
Ok(matches)
}
pub fn match_pattern(
source: &str,
language: &str,
pattern: &str,
) -> Result<Vec<CodeMatch>> {
match_pattern_filtered(source, language, pattern)
}
pub fn extract_code_symbols(
source: &str,
language: &str,
) -> Result<Vec<CodeMatch>> {
let mut all_matches = Vec::new();
for pattern in [
CodePattern::FunctionDefinition,
CodePattern::MethodCall,
CodePattern::ErrorHandling,
CodePattern::TodoMarker,
CodePattern::ImportStatement,
CodePattern::DeprecatedApi,
] {
let mut matches = match_pattern(source, language, pattern.query_str_for(language))?;
for m in &mut matches {
m.pattern = pattern.name().to_string();
}
all_matches.extend(matches);
}
Ok(all_matches)
}
pub fn find_error_handling(source: &str, language: &str) -> Result<Vec<CodeMatch>> {
match_pattern(source, language, CodePattern::ErrorHandling.query_str_for(language))
}
pub fn find_todos(source: &str, language: &str) -> Result<Vec<CodeMatch>> {
match_pattern(source, language, CodePattern::TodoMarker.query_str_for(language))
}
pub fn find_imports(source: &str, language: &str) -> Result<Vec<CodeMatch>> {
match_pattern(source, language, CodePattern::ImportStatement.query_str_for(language))
}
pub fn extract_and_merge(
_engine: &ParseTreeEngine,
url: &str,
text: &str,
) -> Result<Vec<CodeMatch>> {
let normalized = normalize(text);
let language = tree_sitter_language_pack::detect_language_from_path(url)
.or_else(|| tree_sitter_language_pack::detect_language_from_content(&normalized))
.unwrap_or("text");
if language == "text" || !tree_sitter_language_pack::has_parser(language) {
return Ok(Vec::new());
}
extract_code_symbols(&normalized, language)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn finds_function_definitions_in_rust() {
let rust = r#"
fn main() {
println!("hello");
}
fn helper() -> i32 { 42 }
"#;
let matches = match_pattern(rust, "rust", CodePattern::FunctionDefinition.query_str_for("rust")).unwrap();
assert!(!matches.is_empty(), "must find function definitions");
assert!(matches.iter().any(|m| m.captures.contains_key("name")));
}
#[test]
fn finds_function_definitions_in_python() {
let py = "def hello():\n print('world')\n";
let matches = match_pattern(py, "python", CodePattern::FunctionDefinition.query_str_for("python")).unwrap();
assert!(!matches.is_empty(), "must find function definitions in python");
}
#[test]
fn finds_error_handling_patterns() {
let rust = r#"
fn risky() {
let x = some_result.unwrap();
let y = another.expect("failed");
}
"#;
let matches = find_error_handling(rust, "rust").unwrap();
assert!(!matches.is_empty(), "must find unwrap/expect");
assert!(matches.iter().any(|m| m.snippet.contains("unwrap")));
assert!(matches.iter().any(|m| m.snippet.contains("expect")));
}
#[test]
fn finds_todo_markers() {
let rust = r#"
// TODO: refactor this
fn foo() {}
// FIXME: handle edge case
"#;
let matches = find_todos(rust, "rust").unwrap();
assert_eq!(matches.len(), 2, "must find both TODO and FIXME");
}
#[test]
fn finds_import_statements() {
let rust = "use std::collections::HashMap;\nuse std::io::Read;\n";
let matches = find_imports(rust, "rust").unwrap();
assert_eq!(matches.len(), 2, "must find both use statements");
}
#[test]
fn extract_code_symbols_finds_multiple_kinds() {
let rust = r#"
use std::collections::HashMap;
// TODO: optimize
fn main() {
let x = compute().unwrap();
}
"#;
let matches = extract_code_symbols(rust, "rust").unwrap();
let patterns: Vec<&str> = matches.iter().map(|m| m.pattern.as_str()).collect();
assert!(patterns.contains(&"import_statement"), "must find imports");
assert!(patterns.contains(&"todo_marker"), "must find TODO");
assert!(patterns.contains(&"function_definition"), "must find function");
assert!(patterns.contains(&"error_handling"), "must find unwrap");
}
#[test]
fn no_match_returns_empty_vec() {
let rust = "fn main() {}";
let matches = match_pattern(rust, "rust", CodePattern::TodoMarker.query_str_for("rust")).unwrap();
assert!(matches.is_empty(), "no TODO must yield empty vec");
}
#[test]
fn pattern_with_no_match_does_not_error() {
let rust = "fn main() {}";
let result = match_pattern(rust, "rust", CodePattern::DeprecatedApi.query_str_for("rust"));
assert!(result.is_ok(), "no match must not error");
assert!(result.unwrap().is_empty());
}
#[test]
fn extract_and_merge_works_with_url() {
let engine = ParseTreeEngine::new();
let rust = "fn main() { let x = foo().unwrap(); }";
let matches = extract_and_merge(&engine, "https://example.com/main.rs", rust).unwrap();
assert!(!matches.is_empty(), "must extract code symbols from URL");
}
#[test]
fn extract_and_merge_falls_back_for_unknown_extension() {
let engine = ParseTreeEngine::new();
let text = "some plain text";
let matches = extract_and_merge(&engine, "https://example.com/file.xyz", text).unwrap();
assert!(matches.is_empty(), "unknown extension must yield empty");
}
#[test]
fn code_match_helpers_work() {
let m = CodeMatch {
doc_id: "doc".into(),
pattern: "function_definition".into(),
start_byte: 0,
end_byte: 10,
snippet: "fn main()".into(),
captures: std::collections::HashMap::new(),
};
assert!(m.is_function());
assert!(!m.is_error_handling());
}
#[test]
fn all_patterns_have_distinct_names() {
let patterns = [
CodePattern::FunctionDefinition,
CodePattern::MethodCall,
CodePattern::ErrorHandling,
CodePattern::TodoMarker,
CodePattern::ImportStatement,
CodePattern::DeprecatedApi,
];
let names: Vec<&str> = patterns.iter().map(|p| p.name()).collect();
let unique: std::collections::HashSet<_> = names.iter().cloned().collect();
assert_eq!(names.len(), unique.len(), "pattern names must be unique");
}
}