Skip to content
Open
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
12 changes: 6 additions & 6 deletions crates/flow/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,8 +415,6 @@ async fn test_parse_rust_code() {
let output = result.unwrap();

let symbols = extract_symbols(&output);
// Note: Currently only extracts functions, not structs/classes
// TODO: Add struct/class extraction in future
if !symbols.is_empty() {
let symbol_names: Vec<String> = symbols
.iter()
Expand All @@ -426,15 +424,17 @@ async fn test_parse_rust_code() {
})
.collect();

// Look for functions that should be extracted
let found_function = symbol_names.iter().any(|name| {
// Look for functions and structs/classes that should be extracted
let found_function_or_struct = symbol_names.iter().any(|name| {
name.contains("main")
|| name.contains("process_user")
|| name.contains("calculate_total")
|| name.contains("User")
|| name.contains("Role")
});
assert!(
found_function,
"Should find at least one function (main, process_user, or calculate_total). Found: {:?}",
found_function_or_struct,
"Should find at least one function or struct (main, process_user, calculate_total, User, or Role). Found: {:?}",
symbol_names
);
Comment on lines +427 to 439
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Strengthen the test to explicitly assert that struct symbols (e.g., User and Role) are present, not just any one of the union of names.

Because the assertion passes if any of those names are present, the test can still succeed even if struct/class extraction is broken as long as functions are found. To verify the new behavior, add a distinct assertion that checks specifically for User/Role (e.g., keep the existing function check and add a separate any/contains just for struct names) so the test fails if struct extraction regresses.

Suggested change
// Look for functions and structs/classes that should be extracted
let found_function_or_struct = symbol_names.iter().any(|name| {
name.contains("main")
|| name.contains("process_user")
|| name.contains("calculate_total")
|| name.contains("User")
|| name.contains("Role")
});
assert!(
found_function,
"Should find at least one function (main, process_user, or calculate_total). Found: {:?}",
found_function_or_struct,
"Should find at least one function or struct (main, process_user, calculate_total, User, or Role). Found: {:?}",
symbol_names
);
// Look for functions that should be extracted
let found_function = symbol_names.iter().any(|name| {
name.contains("main")
|| name.contains("process_user")
|| name.contains("calculate_total")
});
assert!(
found_function,
"Should find at least one function (main, process_user, or calculate_total). Found: {:?}",
symbol_names
);
// Look for structs/classes that should be extracted
let found_struct = symbol_names.iter().any(|name| {
name.contains("User") || name.contains("Role")
});
assert!(
found_struct,
"Should find at least one struct or class (User or Role). Found: {:?}",
symbol_names
);

} else {
Expand Down
6 changes: 3 additions & 3 deletions crates/language/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1721,17 +1721,17 @@ pub fn from_extension(path: &Path) -> Option<SupportLang> {
}

// Handle extensionless files or files with unknown extensions
if let Some(_file_name) = path.file_name().and_then(|n| n.to_str()) {
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
// 1. Check if the full filename matches a known extension (e.g. .bashrc)
#[cfg(any(feature = "bash", feature = "all-parsers"))]
if constants::BASH_EXTS.contains(&_file_name) {
if constants::BASH_EXTS.contains(&file_name) {
return Some(SupportLang::Bash);
}

// 2. Check known extensionless file names
#[cfg(any(feature = "bash", feature = "all-parsers", feature = "ruby"))]
for (name, lang) in constants::LANG_RELATIONSHIPS_WITH_NO_EXTENSION {
if *name == _file_name {
if *name == file_name {
return Some(*lang);
}
}
Expand Down
44 changes: 44 additions & 0 deletions crates/services/src/conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ pub fn extract_basic_metadata<D: Doc>(
}
}

// Extract class and struct definitions
if let Ok(class_matches) = extract_classes(&root_node) {
for (name, info) in class_matches {
metadata.defined_symbols.insert(name, info);
}
}
Comment on lines +70 to +75
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

extract_classes is declared under #[cfg(feature = \"matching\")], but it’s called unconditionally here. This will fail to compile when the matching feature is disabled. Wrap this call-site with the same cfg(feature = \"matching\"), or provide a non-matching fallback implementation of extract_classes that returns an empty map.

Copilot uses AI. Check for mistakes.

// Extract import statements
if let Ok(imports) = extract_imports(&root_node, &document.language) {
for (name, info) in imports {
Expand Down Expand Up @@ -117,6 +124,43 @@ fn extract_functions<D: Doc>(root_node: &Node<D>) -> ServiceResult<RapidMap<Stri
Ok(functions)
}

/// Extract class and struct definitions using ast-grep patterns
#[cfg(feature = "matching")]
fn extract_classes<D: Doc>(root_node: &Node<D>) -> ServiceResult<RapidMap<String, SymbolInfo>> {
let mut classes = thread_utilities::get_map();

// Try different class/struct patterns based on common languages
let patterns = [
"struct $NAME { $$$BODY }", // Rust, C++, C#
"class $NAME { $$$BODY }", // TypeScript, JavaScript, Java, C#, C++
"class $NAME: $$$BODY", // Python
"class $NAME($$$PARAMS): $$$BODY", // Python
"type $NAME struct { $$$BODY }", // Go
"interface $NAME { $$$BODY }", // TypeScript, Java, C#
];
Comment on lines +132 to +140
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

These patterns are tried for every document regardless of language. root_node.find_all(...) is typically a full-tree scan, so scanning 6 patterns per file can become a noticeable cost at scale. Consider selecting patterns based on the detected SupportLang (similar to extract_imports) to reduce unnecessary traversals.

Copilot uses AI. Check for mistakes.

for pattern in &patterns {
for node_match in root_node.find_all(pattern) {
if let Some(name_node) = node_match.get_env().get_match("NAME") {
let class_name = name_node.text().to_string();
let position = name_node.start_pos();

let symbol_info = SymbolInfo {
name: class_name.clone(),
kind: SymbolKind::Class,
position,
scope: "global".to_string(), // Simplified for now
visibility: Visibility::Public, // Simplified for now
};
Comment on lines +148 to +154
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

All extracted symbols are labeled as SymbolKind::Class, even when matched from struct, interface, or Go type ... struct patterns. If downstream consumers rely on kind, this produces incorrect metadata. Consider deriving SymbolKind from the matched pattern (or capture a discriminator per pattern), and if the enum doesn’t support it yet, add distinct variants (e.g., Struct, Interface) or a more general Type kind.

Copilot uses AI. Check for mistakes.

classes.insert(class_name, symbol_info);
}
}
}

Ok(classes)
}

/// Extract import statements using language-specific patterns
#[cfg(feature = "matching")]
fn extract_imports<D: Doc>(
Expand Down
Loading