From 7d6b7ea876a70ce76433a852a90b5db1565110ca Mon Sep 17 00:00:00 2001 From: "Anaz S. Aji" Date: Fri, 24 Jul 2026 13:53:45 +0700 Subject: [PATCH 1/5] feat: add Dart symbol indexing (#374) * feat: add Dart symbol indexing (closes #373) - Add regex-based Dart extraction: classes, mixins, enums, extensions, functions, getters, setters, typedefs - Add Dart to context extraction (imports, calls, types) - Add Dart to scope tracking and function entry detection - Add Dart-specific builtins to filter list - Add 2 tests: comprehensive + issue #373 reproduction - Update CHANGELOG.md * style: cargo fmt --------- Co-authored-by: ajianaz --- CHANGELOG.md | 4 + Cargo.lock | 2 +- src/engine/context/extraction.rs | 4 +- src/index/extract.rs | 157 ++++++++++++++++++++++++++++++- 4 files changed, 163 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e99633..fd03efd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **Dart symbol indexing.** `cora index` now extracts classes, mixins, enums, extensions, functions, getters, and typedefs from `.dart` files. Closes #373. + ## [0.8.1] - 2026-07-24 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 6b4f939..57eab0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -297,7 +297,7 @@ dependencies = [ [[package]] name = "cora-code" -version = "0.7.0" +version = "0.8.1" dependencies = [ "anyhow", "assert_cmd", diff --git a/src/engine/context/extraction.rs b/src/engine/context/extraction.rs index 2bc332c..3a801b6 100644 --- a/src/engine/context/extraction.rs +++ b/src/engine/context/extraction.rs @@ -228,7 +228,7 @@ pub fn extract_symbols_from_line(line: &str, language: &str) -> Vec } } } - "java" | "kt" | "kts" => { + "java" | "kt" | "kts" | "dart" => { for cap in RE_JAVA_IMPORT.captures_iter(line) { add_unique( &mut symbols, @@ -387,7 +387,7 @@ pub fn extract_definitions_from_diff( (Some(&RE_DEF_JS_FN), Some(&RE_DEF_JS_TYPE)) } "go" => (Some(&RE_DEF_GO_FN), Some(&RE_DEF_GO_TYPE)), - "java" | "kt" | "kts" => (None, Some(&RE_DEF_JAVA_TYPE)), + "java" | "kt" | "kts" | "dart" => (None, Some(&RE_DEF_JAVA_TYPE)), _ => (None, None), }; diff --git a/src/index/extract.rs b/src/index/extract.rs index ff78506..4ebfdc4 100644 --- a/src/index/extract.rs +++ b/src/index/extract.rs @@ -144,6 +144,29 @@ static RE_ZIG_FN: LazyLock = static RE_ZIG_CONST: LazyLock = LazyLock::new(|| Regex::new(r"^\s*(?:pub\s+)?const\s+(\w+)").unwrap()); +// ─── Dart ─── + +static RE_DART_CLASS: LazyLock = + LazyLock::new(|| Regex::new(r"^\s*(?:abstract\s+)?(?:class|mixin)\s+(\w+)").unwrap()); + +static RE_DART_ENUM: LazyLock = LazyLock::new(|| Regex::new(r"^\s*enum\s+(\w+)").unwrap()); + +static RE_DART_EXTENDS: LazyLock = + LazyLock::new(|| Regex::new(r"^\s*extension\s+(?:type\s+)?(\w+)\s+on\s+\w+").unwrap()); +static RE_DART_EXTENDS_UNNAMED: LazyLock = + LazyLock::new(|| Regex::new(r"^\s*extension\s+(?:type\s+)?on\s+(\w+)").unwrap()); + +static RE_DART_FN: LazyLock = LazyLock::new(|| { + Regex::new(r"^\s*(?:static\s+)?(?:external\s+)?(?:@[\w()<>]+\s+)*(?:covariant\s+)?(?:late\s+)?(?:final\s+)?(?:const\s+)?(?:[A-Z]\w*\s+)?(?:Future<[^>]+>\s+)?(?:void|[A-Z]\w*<[^>]+>|[a-zA-Z]\w*(?:<[^>]+>)?)\s+(\w+)\s*\(").unwrap() +}); + +static RE_DART_TYPEDEF: LazyLock = + LazyLock::new(|| Regex::new(r"^\s*typedef\s+(\w+)").unwrap()); + +static RE_DART_GETTER: LazyLock = LazyLock::new(|| { + Regex::new(r"^\s*(?:[A-Z]\w*\s+)?(?:[a-zA-Z]\w*(?:<[^>]+>)?)\s+get\s+(\w+)").unwrap() +}); + /// Extract symbols from source code. /// /// When the `tree-sitter` feature is enabled, uses AST-based extraction for @@ -182,6 +205,7 @@ pub fn extract_symbols(content: &str, language: &str, file_path: &str) -> Vec extract_scala(line, line_no, file_path, line, &mut symbols), "lua" => extract_lua(line, line_no, file_path, line, &mut symbols), "zig" => extract_zig(line, line_no, file_path, line, &mut symbols), + "dart" => extract_dart(line, line_no, file_path, line, &mut symbols), _ => {} } } @@ -362,6 +386,29 @@ fn extract_zig(line: &str, line_no: u32, file: &str, raw: &str, out: &mut Vec) { + if let Some(cap) = RE_DART_CLASS.captures(line) { + out.push(def(cap, SymbolKind::Struct, line_no, file, raw)); + } + if let Some(cap) = RE_DART_ENUM.captures(line) { + out.push(def(cap, SymbolKind::Enum, line_no, file, raw)); + } + if let Some(cap) = RE_DART_EXTENDS.captures(line) { + out.push(def(cap, SymbolKind::Trait, line_no, file, raw)); + } else if let Some(cap) = RE_DART_EXTENDS_UNNAMED.captures(line) { + out.push(def(cap, SymbolKind::Trait, line_no, file, raw)); + } + if let Some(cap) = RE_DART_FN.captures(line) { + out.push(def(cap, SymbolKind::Function, line_no, file, raw)); + } + if let Some(cap) = RE_DART_TYPEDEF.captures(line) { + out.push(def(cap, SymbolKind::TypeAlias, line_no, file, raw)); + } + if let Some(cap) = RE_DART_GETTER.captures(line) { + out.push(def(cap, SymbolKind::Function, line_no, file, raw)); + } +} + /// Extract function call sites from source code. /// /// When the `tree-sitter` feature is enabled, uses AST-based extraction for @@ -398,6 +445,8 @@ pub fn extract_calls(content: &str, language: &str, file_path: &str) -> Vec Option { RE.captures(trimmed) .map(|c| c.get(1).unwrap().as_str().to_string()) } - "java" | "kt" => { + "java" | "kt" | "dart" => { static RE: LazyLock = LazyLock::new(|| Regex::new(r"\b(\w+)\s*\([^)]*\)\s*\{").unwrap()); RE.captures(trimmed) @@ -564,6 +613,29 @@ fn is_builtin(name: &str) -> bool { | "send" | "sync" | "main" + // Dart builtins + | "debugPrint" + | "throw" + | "rethrow" + | "late" + | "required" + | "covariant" + | "show" + | "hide" + | "Future" + | "Stream" + | "Completer" + | "List" + | "Map" + | "Set" + | "int" + | "double" + | "num" + | "bool" + | "dynamic" + | "Object" + | "Null" + | "Never" ) } @@ -811,4 +883,87 @@ const MAX_RETRIES: u32 = 3; assert!(names.contains(&"main")); assert!(names.contains(&"MAX_RETRIES")); } + + #[test] + fn test_extract_dart() { + let code = r#" +class UserService { + String? name; + void login(String email) { + print(email); + } + + static Future fetchData() async { + return Future.value(); + } + + String get displayName => name ?? ''; + + set displayName(String value) {} +} + +enum Status { active, inactive } + +extension UserServiceHelpers on UserService { + String greet() => "Hello"; +} + +typedef UserCallback = void Function(User); + +mixin Validator { + bool validate(String input) => true; +} + +abstract class Shape { + double area(); +} +"#; + let symbols = extract_symbols(code, "dart", "lib/models/user.dart"); + let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect(); + let kinds: Vec<(String, &str)> = symbols + .iter() + .map(|s| (s.name.clone(), s.kind.as_str())) + .collect(); + eprintln!("Dart symbols: {:?}", kinds); + + assert!(names.contains(&"UserService"), "UserService class"); + assert!(names.contains(&"Shape"), "Shape abstract class"); + assert!(names.contains(&"Validator"), "Validator mixin"); + assert!(names.contains(&"login"), "login method"); + assert!(names.contains(&"fetchData"), "fetchData async method"); + assert!(names.contains(&"displayName"), "displayName getter"); + assert!(names.contains(&"greet"), "extension method greet"); + assert!(names.contains(&"validate"), "mixin method validate"); + assert!(names.contains(&"area"), "abstract method area"); + assert!(names.contains(&"Status"), "Status enum"); + assert!( + names.contains(&"UserServiceHelpers"), + "extension on UserService" + ); + assert!(names.contains(&"UserCallback"), "typedef UserCallback"); + } + + #[test] + fn test_extract_dart_issue_373_repro() { + let code = r#"class UserService { + String? name; + void login(String email) { + print(email); + } +} + +Future fetchData() async { + return Future.value(); +} + +enum Status { active, inactive }"#; + let symbols = extract_symbols(code, "dart", "sample.dart"); + let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect(); + + assert!(!symbols.is_empty(), "Should extract at least 1 Dart symbol"); + assert!(names.contains(&"UserService"), "UserService class"); + assert!(names.contains(&"login"), "login method"); + assert!(names.contains(&"fetchData"), "fetchData function"); + assert!(names.contains(&"Status"), "Status enum"); + } } From 41d05742e3bbad182f0c3ed16eaa5054cc278b64 Mon Sep 17 00:00:00 2001 From: "Anaz S. Aji" Date: Fri, 24 Jul 2026 14:50:01 +0700 Subject: [PATCH 2/5] feat: add Svelte symbol indexing (#375) (#375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Component name derived from filename (kebab-case → PascalCase) - Extract ", + ) + .unwrap() +}); +static RE_SVELTE_EXPORT_PROP: LazyLock = + LazyLock::new(|| Regex::new(r"^\s*export\s+let\s+(\w+)").unwrap()); +static RE_SVELTE_STATE: LazyLock = + LazyLock::new(|| Regex::new(r"^\s*let\s+(\w+)\s*=\s*\$state(?:<[^>]*>)?\s*\(").unwrap()); +static RE_SVELTE_DERIVED: LazyLock = + LazyLock::new(|| Regex::new(r"^\s*let\s+(\w+)\s*=\s*\$derived(?:<[^>]*>)?\s*\(").unwrap()); +static RE_SVELTE_EFFECT: LazyLock = + LazyLock::new(|| Regex::new(r"^\s*\$effect\s*\(\s*(?:async\s+)?\(\s*\)\s*=>").unwrap()); /// Extract symbols from source code. /// @@ -206,6 +224,7 @@ pub fn extract_symbols(content: &str, language: &str, file_path: &str) -> Vec extract_lua(line, line_no, file_path, line, &mut symbols), "zig" => extract_zig(line, line_no, file_path, line, &mut symbols), "dart" => extract_dart(line, line_no, file_path, line, &mut symbols), + "svelte" => extract_svelte(content, file_path, &mut symbols), _ => {} } } @@ -409,6 +428,79 @@ fn extract_dart(line: &str, line_no: u32, file: &str, raw: &str, out: &mut Vec` blocks and delegate to TS/JS extractor +/// 3. Extract Svelte-specific: export props, $state, $derived +fn extract_svelte(content: &str, file: &str, out: &mut Vec) { + // 1. Component name from filename + let component_name = std::path::Path::new(file) + .file_stem() + .and_then(|s| s.to_str()) + .map(|s| { + let mut result = String::new(); + let mut capitalize_next = true; + for ch in s.chars() { + if ch == '-' || ch == '_' { + capitalize_next = true; + } else if capitalize_next { + result.push(ch.to_ascii_uppercase()); + capitalize_next = false; + } else { + result.push(ch); + } + } + result + }) + .unwrap_or_default(); + + if !component_name.is_empty() { + out.push(ExtractedDef { + name: component_name.clone(), + kind: SymbolKind::Struct, + file: file.to_string(), + line: 1, + signature: format!("", component_name), + }); + } + + // 2. Extract + + + +{#if count > 0} +

Counting!

+{/if} + + +"#; + + let symbols = extract_symbols(content, "svelte", "src/lib/Counter.svelte"); + let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect(); + println!("Svelte symbols: {:?}", names); + + assert!(names.contains(&"Counter"), "missing component name"); + assert!(names.contains(&"name"), "missing prop: name"); + assert!(names.contains(&"count"), "missing prop: count"); + assert!( + names.iter().any(|n| n.starts_with("doubled")), + "missing $derived: doubled" + ); + assert!( + names.iter().any(|n| n.starts_with("items")), + "missing $state: items" + ); + assert!( + names.contains(&"handleClick"), + "missing function: handleClick" + ); + assert!(names.contains(&"fetchData"), "missing function: fetchData"); + } } From 372bb996907084a06ad3f83db76456ea1dc27492 Mon Sep 17 00:00:00 2001 From: ajianaz Date: Fri, 24 Jul 2026 16:55:41 +0700 Subject: [PATCH 3/5] feat: add tree-sitter support for 8 additional languages Add AST-based symbol extraction for Java, C, C++, C#, Ruby, PHP, Scala, and JavaScript using tree-sitter grammars compatible with runtime 0.26. New grammars: - tree-sitter-java 0.23 - tree-sitter-c 0.24 - tree-sitter-cpp 0.23 - tree-sitter-c-sharp 0.23 - tree-sitter-ruby 0.23 - tree-sitter-php 0.24 - tree-sitter-scala 3 - tree-sitter-javascript 0.25 Symbol kinds: Function, Method, Class, Struct, Interface, Trait, Enum, Module, Variable, TypeAlias, Constant. Edge kinds: Calls, Inherits, Implements, Imports. Closes #376 --- Cargo.lock | 88 ++++ Cargo.toml | 16 + src/index/ast.rs | 1063 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 1163 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 57eab0b..f9fb789 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -328,9 +328,17 @@ dependencies = [ "tracing", "tracing-subscriber", "tree-sitter", + "tree-sitter-c", + "tree-sitter-c-sharp", + "tree-sitter-cpp", "tree-sitter-go", + "tree-sitter-java", + "tree-sitter-javascript", + "tree-sitter-php", "tree-sitter-python", + "tree-sitter-ruby", "tree-sitter-rust", + "tree-sitter-scala", "tree-sitter-typescript", "usearch", "which", @@ -2088,6 +2096,36 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "tree-sitter-c" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b2eb57a55fed6b00812912e730b7a275cf4fe98bfd6a5d76263d4438371728" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-c-sharp" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1aac67f1ad71de1d6d39708d34811081c26dfa495658de6c14c34200849357c" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-cpp" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2196ea9d47b4ab4a31b9297eaa5a5d19a0b121dceb9f118f6790ad0ab94743" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-go" version = "0.25.0" @@ -2098,12 +2136,42 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "tree-sitter-java" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa6cbcdc8c679b214e616fd3300da67da0e492e066df01bcf5a5921a71e90d6" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-javascript" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68204f2abc0627a90bdf06e605f5c470aa26fdcb2081ea553a04bdad756693f5" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-language" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" +[[package]] +name = "tree-sitter-php" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c17c3ab69052c5eeaa7ff5cd972dd1bc25d1b97ee779fec391ad3b5df5592" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-python" version = "0.25.0" @@ -2114,6 +2182,16 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "tree-sitter-ruby" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0484ea4ef6bb9c575b4fdabde7e31340a8d2dbc7d52b321ac83da703249f95" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-rust" version = "0.24.2" @@ -2124,6 +2202,16 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "tree-sitter-scala" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de5a4a7ff23a55474ce6a741d52aaeca7a82fe9421bb982b86e98c6ac8629397" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-typescript" version = "0.23.2" diff --git a/Cargo.toml b/Cargo.toml index d1a64fe..33e9049 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,14 @@ tree-sitter-rust = { version = "0.24", optional = true } tree-sitter-go = { version = "0.25", optional = true } tree-sitter-python = { version = "0.25", optional = true } tree-sitter-typescript = { version = "0.23", optional = true } +tree-sitter-java = { version = "0.23", optional = true } +tree-sitter-c = { version = "0.24", optional = true } +tree-sitter-cpp = { version = "0.23", optional = true } +tree-sitter-c-sharp = { version = "0.23", optional = true } +tree-sitter-ruby = { version = "0.23", optional = true } +tree-sitter-php = { version = "0.24", optional = true } +tree-sitter-scala = { version = "0.26", optional = true } +tree-sitter-javascript = { version = "0.25", optional = true } [features] default = [] @@ -78,6 +86,14 @@ tree-sitter = [ "dep:tree-sitter-go", "dep:tree-sitter-python", "dep:tree-sitter-typescript", + "dep:tree-sitter-java", + "dep:tree-sitter-c", + "dep:tree-sitter-cpp", + "dep:tree-sitter-c-sharp", + "dep:tree-sitter-ruby", + "dep:tree-sitter-php", + "dep:tree-sitter-scala", + "dep:tree-sitter-javascript", ] [dev-dependencies] diff --git a/src/index/ast.rs b/src/index/ast.rs index e97c04a..abe06e4 100644 --- a/src/index/ast.rs +++ b/src/index/ast.rs @@ -100,11 +100,21 @@ impl From for CallSite { /// Returns `None` for unsupported languages. pub fn get_language(ext: &str) -> Option { match ext { + // Original 5 "rs" => Some(tree_sitter_rust::LANGUAGE.into()), "go" => Some(tree_sitter_go::LANGUAGE.into()), "py" | "pyi" => Some(tree_sitter_python::LANGUAGE.into()), "ts" => Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()), "tsx" => Some(tree_sitter_typescript::LANGUAGE_TSX.into()), + // Batch 2 — 8 new languages + "java" => Some(tree_sitter_java::LANGUAGE.into()), + "c" | "h" => Some(tree_sitter_c::LANGUAGE.into()), + "cpp" | "cc" | "cxx" | "hpp" | "hxx" => Some(tree_sitter_cpp::LANGUAGE.into()), + "cs" => Some(tree_sitter_c_sharp::LANGUAGE.into()), + "rb" => Some(tree_sitter_ruby::LANGUAGE.into()), + "php" => Some(tree_sitter_php::LANGUAGE_PHP.into()), + "scala" | "sc" => Some(tree_sitter_scala::LANGUAGE.into()), + "js" | "mjs" | "cjs" => Some(tree_sitter_javascript::LANGUAGE.into()), _ => None, } } @@ -135,7 +145,16 @@ pub fn extract(content: &str, language: &str, file_path: &str) -> (Vec, "rs" => extract_rust(&tree.root_node(), content, file_path), "go" => extract_go(&tree.root_node(), content, file_path), "py" | "pyi" => extract_python(&tree.root_node(), content, file_path), - "ts" | "tsx" => extract_typescript(&tree.root_node(), content, file_path), + "ts" | "tsx" | "js" | "mjs" | "cjs" => { + extract_typescript(&tree.root_node(), content, file_path) + } + "java" => extract_java(&tree.root_node(), content, file_path), + "c" | "h" => extract_c(&tree.root_node(), content, file_path), + "cpp" | "cc" | "cxx" | "hpp" | "hxx" => extract_cpp(&tree.root_node(), content, file_path), + "cs" => extract_csharp(&tree.root_node(), content, file_path), + "rb" => extract_ruby(&tree.root_node(), content, file_path), + "php" => extract_php(&tree.root_node(), content, file_path), + "scala" | "sc" => extract_scala(&tree.root_node(), content, file_path), _ => (Vec::new(), Vec::new()), } } @@ -846,6 +865,1037 @@ fn extract_typescript( (nodes, edges) } + +// ─── Java ───────────────────────────────────────────────────────── + +fn extract_java( + root: &tree_sitter::Node, + source: &str, + file_path: &str, +) -> (Vec, Vec) { + let mut nodes = Vec::new(); + let mut edges = Vec::new(); + let mut cursor = root.walk(); + + fn walk_java( + node: &tree_sitter::Node, + source: &str, + file_path: &str, + nodes: &mut Vec, + edges: &mut Vec, + ) { + match node.kind() { + "class_declaration" => { + let name = node_name(node, source); + if !name.is_empty() { + let line = (node.start_position().row + 1) as u32; + // Inheritance + if let Some(superclass) = node.child_by_field_name("superclass") { + let parent = node_text(&superclass, source); + if !parent.is_empty() { + edges.push(AstEdge { + source: name.clone(), + kind: EdgeKind::Inherits, + target: parent, + file: file_path.to_string(), + line, + }); + } + } + // Interfaces + if let Some(interfaces) = node.child_by_field_name("interfaces") { + let mut ic = interfaces.walk(); + if ic.goto_first_child() { + loop { + let iface = node_text(&ic.node(), source); + if !iface.is_empty() { + edges.push(AstEdge { + source: name.clone(), + kind: EdgeKind::Implements, + target: iface, + file: file_path.to_string(), + line, + }); + } + if !ic.goto_next_sibling() { + break; + } + } + } + } + nodes.push(AstNode { + name: name.clone(), + kind: SymbolKind::Class, + file: file_path.to_string(), + line, + signature: signature_for_node(node, source), + parent: None, + }); + // Methods inside class body + if let Some(body) = find_child_by_kind(node, &["class_body", "block"]) { + let mut mc = body.walk(); + if mc.goto_first_child() { + loop { + let mn = mc.node(); + if mn.kind() == "method_declaration" + || mn.kind() == "constructor_declaration" + { + let mname = node_name(&mn, source); + if !mname.is_empty() { + nodes.push(AstNode { + name: mname, + kind: SymbolKind::Method, + file: file_path.to_string(), + line: (mn.start_position().row + 1) as u32, + signature: signature_for_node(&mn, source), + parent: Some(name.clone()), + }); + } + } else if mn.kind() == "field_declaration" { + let mut fc = mn.walk(); + if fc.goto_first_child() { + loop { + if fc.node().kind() == "variable_declarator" { + let fname = node_name(&fc.node(), source); + if !fname.is_empty() { + nodes.push(AstNode { + name: fname, + kind: SymbolKind::Variable, + file: file_path.to_string(), + line: (fc.node().start_position().row + 1) + as u32, + signature: signature_for_node( + &fc.node(), + source, + ), + parent: Some(name.clone()), + }); + } + } + if !fc.goto_next_sibling() { + break; + } + } + } + } + if !mc.goto_next_sibling() { + break; + } + } + } + } + } + } + "interface_declaration" => { + let name = node_name(node, source); + if !name.is_empty() { + nodes.push(AstNode { + name, + kind: SymbolKind::Interface, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + signature: signature_for_node(node, source), + parent: None, + }); + } + } + "method_declaration" => { + let name = node_name(node, source); + if !name.is_empty() { + nodes.push(AstNode { + name: name.clone(), + kind: SymbolKind::Function, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + signature: signature_for_node(node, source), + parent: None, + }); + extract_calls_from_node(node, source, file_path, &name, edges); + } + } + "import_declaration" => { + edges.push(AstEdge { + source: file_path.to_string(), + kind: EdgeKind::Imports, + target: signature_for_node(node, source), + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + }); + } + "enum_declaration" => { + let name = node_name(node, source); + if !name.is_empty() { + nodes.push(AstNode { + name, + kind: SymbolKind::Enum, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + signature: signature_for_node(node, source), + parent: None, + }); + } + } + _ => {} + } + } + + for child in root.children(&mut cursor) { + walk_java(&child, source, file_path, &mut nodes, &mut edges); + } + (nodes, edges) +} + +// ─── C ───────────────────────────────────────────────────────────── + +fn extract_c( + root: &tree_sitter::Node, + source: &str, + file_path: &str, +) -> (Vec, Vec) { + let mut nodes = Vec::new(); + let mut edges = Vec::new(); + let mut cursor = root.walk(); + + for child in root.children(&mut cursor) { + match child.kind() { + "function_definition" => { + let name = node_name(&child, source); + if !name.is_empty() { + nodes.push(AstNode { + name: name.clone(), + kind: SymbolKind::Function, + file: file_path.to_string(), + line: (child.start_position().row + 1) as u32, + signature: signature_for_node(&child, source), + parent: None, + }); + extract_calls_from_node(&child, source, file_path, &name, &mut edges); + } + } + "struct_specifier" => { + let name = node_name(&child, source); + if !name.is_empty() { + nodes.push(AstNode { + name, + kind: SymbolKind::Struct, + file: file_path.to_string(), + line: (child.start_position().row + 1) as u32, + signature: signature_for_node(&child, source), + parent: None, + }); + } + } + "enum_specifier" => { + let name = node_name(&child, source); + if !name.is_empty() { + nodes.push(AstNode { + name, + kind: SymbolKind::Enum, + file: file_path.to_string(), + line: (child.start_position().row + 1) as u32, + signature: signature_for_node(&child, source), + parent: None, + }); + } + } + "type_definition" => { + let name = node_name(&child, source); + if !name.is_empty() { + nodes.push(AstNode { + name, + kind: SymbolKind::TypeAlias, + file: file_path.to_string(), + line: (child.start_position().row + 1) as u32, + signature: signature_for_node(&child, source), + parent: None, + }); + } + } + "preproc_include" => { + let mut pc = child.walk(); + if pc.goto_first_child() { + loop { + if pc.node().kind() == "string_literal" + || pc.node().kind() == "system_lib_string" + { + let inc = node_text(&pc.node(), source).trim_matches('"').to_string(); + if !inc.is_empty() { + edges.push(AstEdge { + source: file_path.to_string(), + kind: EdgeKind::Imports, + target: inc, + file: file_path.to_string(), + line: (child.start_position().row + 1) as u32, + }); + } + break; + } + if !pc.goto_next_sibling() { + break; + } + } + } + } + "declaration" => { + // Top-level const/typedef etc + if let Some(decl) = child.child_by_field_name("declarator") { + // Only capture if it looks like a function pointer or named constant + let mut dc = child.walk(); + if dc.goto_first_child() { + loop { + if dc.node().kind() == "type_qualifier" { + // const — skip for now (too noisy) + } + if !dc.goto_next_sibling() { + break; + } + } + } + } + } + _ => {} + } + } + (nodes, edges) +} + +// ─── C++ ─────────────────────────────────────────────────────────── + +fn extract_cpp( + root: &tree_sitter::Node, + source: &str, + file_path: &str, +) -> (Vec, Vec) { + let mut nodes = Vec::new(); + let mut edges = Vec::new(); + let mut cursor = root.walk(); + + fn walk_cpp( + node: &tree_sitter::Node, + source: &str, + file_path: &str, + nodes: &mut Vec, + edges: &mut Vec, + ) { + match node.kind() { + "function_definition" => { + let name = node_name(node, source); + if !name.is_empty() { + nodes.push(AstNode { + name: name.clone(), + kind: SymbolKind::Function, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + signature: signature_for_node(node, source), + parent: None, + }); + extract_calls_from_node(node, source, file_path, &name, edges); + } + } + "class_specifier" => { + let name = node_name(node, source); + if !name.is_empty() { + let line = (node.start_position().row + 1) as u32; + // Inheritance + if let Some(bases) = node.child_by_field_name("base_list_clause") { + let mut bc = bases.walk(); + if bc.goto_first_child() { + loop { + if bc.node().kind() == "type_identifier" { + let parent = node_text(&bc.node(), source); + if !parent.is_empty() { + edges.push(AstEdge { + source: name.clone(), + kind: EdgeKind::Inherits, + target: parent, + file: file_path.to_string(), + line, + }); + } + } + if !bc.goto_next_sibling() { + break; + } + } + } + } + nodes.push(AstNode { + name: name.clone(), + kind: SymbolKind::Class, + file: file_path.to_string(), + line, + signature: signature_for_node(node, source), + parent: None, + }); + // Methods inside class body + if let Some(body) = + find_child_by_kind(node, &["field_declaration_list", "class_body"]) + { + let mut mc = body.walk(); + if mc.goto_first_child() { + loop { + let mn = mc.node(); + if mn.kind() == "function_definition" { + let mname = node_name(&mn, source); + if !mname.is_empty() { + nodes.push(AstNode { + name: mname, + kind: SymbolKind::Method, + file: file_path.to_string(), + line: (mn.start_position().row + 1) as u32, + signature: signature_for_node(&mn, source), + parent: Some(name.clone()), + }); + } + } + if !mc.goto_next_sibling() { + break; + } + } + } + } + } + } + "struct_specifier" => { + let name = node_name(node, source); + if !name.is_empty() { + nodes.push(AstNode { + name, + kind: SymbolKind::Struct, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + signature: signature_for_node(node, source), + parent: None, + }); + } + } + "enum_specifier" => { + let name = node_name(node, source); + if !name.is_empty() { + nodes.push(AstNode { + name, + kind: SymbolKind::Enum, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + signature: signature_for_node(node, source), + parent: None, + }); + } + } + "namespace_definition" => { + let name = node_name(node, source); + if !name.is_empty() { + nodes.push(AstNode { + name, + kind: SymbolKind::Module, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + signature: signature_for_node(node, source), + parent: None, + }); + // Recurse into namespace body + if let Some(body) = find_child_by_kind(node, &["declaration_list"]) { + let mut nc = body.walk(); + if nc.goto_first_child() { + loop { + walk_cpp(&nc.node(), source, file_path, nodes, edges); + if !nc.goto_next_sibling() { + break; + } + } + } + } + } + } + "template_declaration" => { + // Unwrap template and recurse into inner declaration + let mut tc = node.walk(); + if tc.goto_first_child() { + loop { + let inner = tc.node(); + if inner.kind() != "template" && inner.kind() != ">" { + walk_cpp(&inner, source, file_path, nodes, edges); + } + if !tc.goto_next_sibling() { + break; + } + } + } + } + "preproc_include" => { + let mut pc = node.walk(); + if pc.goto_first_child() { + loop { + if pc.node().kind() == "string_literal" + || pc.node().kind() == "system_lib_string" + { + let inc = node_text(&pc.node(), source).trim_matches('"').to_string(); + if !inc.is_empty() { + edges.push(AstEdge { + source: file_path.to_string(), + kind: EdgeKind::Imports, + target: inc, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + }); + } + break; + } + if !pc.goto_next_sibling() { + break; + } + } + } + } + _ => {} + } + } + + for child in root.children(&mut cursor) { + walk_cpp(&child, source, file_path, &mut nodes, &mut edges); + } + (nodes, edges) +} + +// ─── C# ──────────────────────────────────────────────────────────── + +fn extract_csharp( + root: &tree_sitter::Node, + source: &str, + file_path: &str, +) -> (Vec, Vec) { + let mut nodes = Vec::new(); + let mut edges = Vec::new(); + let mut cursor = root.walk(); + + fn walk_csharp( + node: &tree_sitter::Node, + source: &str, + file_path: &str, + nodes: &mut Vec, + edges: &mut Vec, + ) { + match node.kind() { + "class_declaration" + | "struct_declaration" + | "record_declaration" + | "interface_declaration" => { + let name = node_name(node, source); + if !name.is_empty() { + let line = (node.start_position().row + 1) as u32; + let kind = match node.kind() { + "interface_declaration" => SymbolKind::Interface, + "struct_declaration" => SymbolKind::Struct, + "record_declaration" => SymbolKind::Struct, + _ => SymbolKind::Class, + }; + // Inheritance / implementation + if let Some(bases) = node.child_by_field_name("bases") { + let mut bc = bases.walk(); + if bc.goto_first_child() { + loop { + let base = node_text(&bc.node(), source); + if !base.is_empty() { + let edge_kind = if kind == SymbolKind::Interface { + EdgeKind::Inherits + } else { + EdgeKind::Implements + }; + // Heuristic: base type starting with 'I' is likely an interface + let ek = if base.starts_with('I') + && base + .chars() + .nth(1) + .map_or(false, |c| c.is_ascii_uppercase()) + { + EdgeKind::Implements + } else if kind == SymbolKind::Interface { + EdgeKind::Inherits + } else { + EdgeKind::Inherits + }; + edges.push(AstEdge { + source: name.clone(), + kind: ek, + target: base, + file: file_path.to_string(), + line, + }); + } + if !bc.goto_next_sibling() { + break; + } + } + } + } + nodes.push(AstNode { + name: name.clone(), + kind, + file: file_path.to_string(), + line, + signature: signature_for_node(node, source), + parent: None, + }); + // Members + if let Some(body) = find_child_by_kind(node, &["declaration_list"]) { + let mut mc = body.walk(); + if mc.goto_first_child() { + loop { + let mn = mc.node(); + if mn.kind() == "method_declaration" + || mn.kind() == "constructor_declaration" + { + let mname = node_name(&mn, source); + if !mname.is_empty() { + nodes.push(AstNode { + name: mname, + kind: SymbolKind::Method, + file: file_path.to_string(), + line: (mn.start_position().row + 1) as u32, + signature: signature_for_node(&mn, source), + parent: Some(name.clone()), + }); + } + } else if mn.kind() == "property_declaration" { + let pname = node_name(&mn, source); + if !pname.is_empty() { + nodes.push(AstNode { + name: pname, + kind: SymbolKind::Variable, + file: file_path.to_string(), + line: (mn.start_position().row + 1) as u32, + signature: signature_for_node(&mn, source), + parent: Some(name.clone()), + }); + } + } + if !mc.goto_next_sibling() { + break; + } + } + } + } + } + } + "enum_declaration" => { + let name = node_name(node, source); + if !name.is_empty() { + nodes.push(AstNode { + name, + kind: SymbolKind::Enum, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + signature: signature_for_node(node, source), + parent: None, + }); + } + } + "method_declaration" => { + let name = node_name(node, source); + if !name.is_empty() { + nodes.push(AstNode { + name: name.clone(), + kind: SymbolKind::Function, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + signature: signature_for_node(node, source), + parent: None, + }); + extract_calls_from_node(node, source, file_path, &name, edges); + } + } + "namespace_declaration" => { + let name = node_name(node, source); + if !name.is_empty() { + nodes.push(AstNode { + name, + kind: SymbolKind::Module, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + signature: signature_for_node(node, source), + parent: None, + }); + if let Some(body) = find_child_by_kind(node, &["declaration_list"]) { + let mut nc = body.walk(); + if nc.goto_first_child() { + loop { + walk_csharp(&nc.node(), source, file_path, nodes, edges); + if !nc.goto_next_sibling() { + break; + } + } + } + } + } + } + "using_directive" => { + edges.push(AstEdge { + source: file_path.to_string(), + kind: EdgeKind::Imports, + target: signature_for_node(node, source), + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + }); + } + _ => {} + } + } + + for child in root.children(&mut cursor) { + walk_csharp(&child, source, file_path, &mut nodes, &mut edges); + } + (nodes, edges) +} + +// ─── Ruby ────────────────────────────────────────────────────────── + +fn extract_ruby( + root: &tree_sitter::Node, + source: &str, + file_path: &str, +) -> (Vec, Vec) { + let mut nodes = Vec::new(); + let mut edges = Vec::new(); + let mut cursor = root.walk(); + + fn walk_ruby( + node: &tree_sitter::Node, + source: &str, + file_path: &str, + nodes: &mut Vec, + edges: &mut Vec, + ) { + match node.kind() { + "module" => { + let name = if let Some(n) = node.child_by_field_name("name") { + node_text(&n, source) + } else { + String::new() + }; + if !name.is_empty() { + nodes.push(AstNode { + name, + kind: SymbolKind::Module, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + signature: signature_for_node(node, source), + parent: None, + }); + } + } + "class" => { + let name = if let Some(n) = node.child_by_field_name("name") { + node_text(&n, source) + } else { + String::new() + }; + if !name.is_empty() { + let line = (node.start_position().row + 1) as u32; + // Parent class + if let Some(superclass) = node.child_by_field_name("superclass") { + let parent = node_text(&superclass, source); + if !parent.is_empty() { + edges.push(AstEdge { + source: name.clone(), + kind: EdgeKind::Inherits, + target: parent, + file: file_path.to_string(), + line, + }); + } + } + nodes.push(AstNode { + name: name.clone(), + kind: SymbolKind::Class, + file: file_path.to_string(), + line, + signature: signature_for_node(node, source), + parent: None, + }); + // Methods inside class + let mut mc = node.walk(); + if mc.goto_first_child() { + loop { + if mc.node().kind() == "method" { + let mname = if let Some(n) = mc.node().child_by_field_name("name") { + node_text(&n, source) + } else { + String::new() + }; + if !mname.is_empty() { + nodes.push(AstNode { + name: mname, + kind: SymbolKind::Method, + file: file_path.to_string(), + line: (mc.node().start_position().row + 1) as u32, + signature: signature_for_node(&mc.node(), source), + parent: Some(name.clone()), + }); + } + } + if !mc.goto_next_sibling() { + break; + } + } + } + } + } + "method" => { + let name = if let Some(n) = node.child_by_field_name("name") { + node_text(&n, source) + } else { + String::new() + }; + if !name.is_empty() { + nodes.push(AstNode { + name: name.clone(), + kind: SymbolKind::Function, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + signature: signature_for_node(node, source), + parent: None, + }); + extract_calls_from_node(node, source, file_path, &name, edges); + } + } + "require" => { + edges.push(AstEdge { + source: file_path.to_string(), + kind: EdgeKind::Imports, + target: signature_for_node(node, source), + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + }); + } + _ => {} + } + } + + for child in root.children(&mut cursor) { + walk_ruby(&child, source, file_path, &mut nodes, &mut edges); + } + (nodes, edges) +} + +// ─── PHP ─────────────────────────────────────────────────────────── + +fn extract_php( + root: &tree_sitter::Node, + source: &str, + file_path: &str, +) -> (Vec, Vec) { + let mut nodes = Vec::new(); + let mut edges = Vec::new(); + let mut cursor = root.walk(); + + fn walk_php( + node: &tree_sitter::Node, + source: &str, + file_path: &str, + nodes: &mut Vec, + edges: &mut Vec, + ) { + match node.kind() { + "class_declaration" | "anonymous_class_creation_expression" => { + let name = node_name(node, source); + if !name.is_empty() { + let line = (node.start_position().row + 1) as u32; + nodes.push(AstNode { + name: name.clone(), + kind: SymbolKind::Class, + file: file_path.to_string(), + line, + signature: signature_for_node(node, source), + parent: None, + }); + // Methods + if let Some(body) = + find_child_by_kind(node, &["declaration_list", "class_body"]) + { + let mut mc = body.walk(); + if mc.goto_first_child() { + loop { + if mc.node().kind() == "method_declaration" { + let mname = node_name(&mc.node(), source); + if !mname.is_empty() { + nodes.push(AstNode { + name: mname, + kind: SymbolKind::Method, + file: file_path.to_string(), + line: (mc.node().start_position().row + 1) as u32, + signature: signature_for_node(&mc.node(), source), + parent: Some(name.clone()), + }); + } + } + if !mc.goto_next_sibling() { + break; + } + } + } + } + } + } + "interface_declaration" => { + let name = node_name(node, source); + if !name.is_empty() { + nodes.push(AstNode { + name, + kind: SymbolKind::Interface, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + signature: signature_for_node(node, source), + parent: None, + }); + } + } + "function_definition" | "method_declaration" => { + let name = node_name(node, source); + if !name.is_empty() { + nodes.push(AstNode { + name: name.clone(), + kind: SymbolKind::Function, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + signature: signature_for_node(node, source), + parent: None, + }); + extract_calls_from_node(node, source, file_path, &name, edges); + } + } + "namespace_definition" => { + let name = if let Some(n) = node.child_by_field_name("name") { + node_text(&n, source) + } else { + String::new() + }; + if !name.is_empty() { + nodes.push(AstNode { + name, + kind: SymbolKind::Module, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + signature: signature_for_node(node, source), + parent: None, + }); + } + } + "use_declaration" => { + edges.push(AstEdge { + source: file_path.to_string(), + kind: EdgeKind::Imports, + target: signature_for_node(node, source), + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + }); + } + _ => {} + } + } + + for child in root.children(&mut cursor) { + walk_php(&child, source, file_path, &mut nodes, &mut edges); + } + (nodes, edges) +} + +// ─── Scala ───────────────────────────────────────────────────────── + +fn extract_scala( + root: &tree_sitter::Node, + source: &str, + file_path: &str, +) -> (Vec, Vec) { + let mut nodes = Vec::new(); + let mut edges = Vec::new(); + let mut cursor = root.walk(); + + fn walk_scala( + node: &tree_sitter::Node, + source: &str, + file_path: &str, + nodes: &mut Vec, + edges: &mut Vec, + ) { + match node.kind() { + "class_definition" | "object_definition" => { + let name = node_name(node, source); + if !name.is_empty() { + let line = (node.start_position().row + 1) as u32; + let kind = if node.kind() == "object_definition" { + SymbolKind::Module + } else { + SymbolKind::Class + }; + nodes.push(AstNode { + name: name.clone(), + kind, + file: file_path.to_string(), + line, + signature: signature_for_node(node, source), + parent: None, + }); + } + } + "trait_definition" => { + let name = node_name(node, source); + if !name.is_empty() { + nodes.push(AstNode { + name, + kind: SymbolKind::Trait, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + signature: signature_for_node(node, source), + parent: None, + }); + } + } + "function_definition" => { + let name = node_name(node, source); + if !name.is_empty() { + nodes.push(AstNode { + name: name.clone(), + kind: SymbolKind::Function, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + signature: signature_for_node(node, source), + parent: None, + }); + extract_calls_from_node(node, source, file_path, &name, edges); + } + } + "import_declaration" => { + edges.push(AstEdge { + source: file_path.to_string(), + kind: EdgeKind::Imports, + target: signature_for_node(node, source), + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + }); + } + "enum_definition" => { + let name = node_name(node, source); + if !name.is_empty() { + nodes.push(AstNode { + name, + kind: SymbolKind::Enum, + file: file_path.to_string(), + line: (node.start_position().row + 1) as u32, + signature: signature_for_node(node, source), + parent: None, + }); + } + } + _ => {} + } + } + + for child in root.children(&mut cursor) { + walk_scala(&child, source, file_path, &mut nodes, &mut edges); + } + (nodes, edges) +} + #[cfg(test)] mod tests { use super::*; @@ -862,8 +1912,12 @@ mod tests { #[test] fn test_get_language_unsupported() { - assert!(get_language("rb").is_none()); - assert!(get_language("java").is_none()); + // Previously unsupported, now all supported via tree-sitter + assert!(get_language("rb").is_some()); + assert!(get_language("java").is_some()); + assert!(get_language("cs").is_some()); + assert!(get_language("scala").is_some()); + assert!(get_language("php").is_some()); assert!(get_language("").is_none()); } @@ -987,7 +2041,8 @@ export function getUser(id: string): User { #[test] fn test_extract_unsupported_fallback() { - let (nodes, edges) = extract("fn test() {}", "rb", "test.rb"); + // "rb" is now supported; use a truly unsupported language + let (nodes, edges) = extract("fn test() {}", "xyz", "test.xyz"); assert!(nodes.is_empty()); assert!(edges.is_empty()); } From 2e58106ef16c40819a5c1302e6b61c0184f92a45 Mon Sep 17 00:00:00 2001 From: ajianaz Date: Fri, 24 Jul 2026 18:29:56 +0700 Subject: [PATCH 4/5] docs: update README, changelog, roadmap, cli-reference, code-intelligence - README: fix scanner counts, update tree-sitter language line - Changelog: add v0.8.0 (Brain Mode, tree-sitter, VitePress) and v0.8.1 entries with Unreleased section for PR 374-376 - Roadmap: add v0.8 section with all completed items, remove from Future - CLI reference: add index --watch/--stats/--prune, callers, impact, affected commands with examples - Code intelligence: rewrite Supported Languages table (18 langs, regex vs AST tiers), update MCP tools table, fix tree-sitter instructions --- README.md | 7 ++-- docs/changelog.md | 72 +++++++++++++++++++++++++++++++++++++-- docs/cli-reference.md | 15 ++++++++ docs/code-intelligence.md | 46 +++++++++++++++++++++---- docs/roadmap.md | 16 ++++++--- 5 files changed, 139 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index db3b8df..4874fd7 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ - ⚡ **Native Rust** — fast binary, no runtime dependencies, cross-platform - 🪝 **Pre-commit hooks** — catch issues before they reach CI - 📋 **SARIF output** — upload to GitHub Code Scanning -- 🛡️ **Deterministic scanners** — 11 security patterns + 12 secret detection patterns that run without LLM +- 🛡️ **Deterministic scanners** — 12 built-in rules + 13 security patterns + 15 secret detection patterns that run without LLM - 🧠 **Language-specific analysis** — tailored review guidance for Dart/Flutter, Svelte, TypeScript, Go, Rust, Python - 🚧 **Quality gate** — configurable pass/fail thresholds for CI enforcement - 📐 **Quality profiles** — strict, balanced, or lax presets for different project needs @@ -31,8 +31,9 @@ - 🔍 **Code Intelligence** — index symbols across 15 languages, call graph, trace, impact analysis - 🧠 **Brain Mode** — hybrid semantic search (FTS5 + vector KNN + graph) with RRF fusion - 🗄️ **Multi-project database** — one global index, search across all your repos at once -- 🌳 **tree-sitter** (opt-in) — AST-based call graph extraction for Rust, Go, Python, TypeScript -- 🔌 **MCP server** — 15 tools for AI agents (review, search, brain, debt, trace, ...) +- 🌳 **Tree-sitter** (opt-in) — AST-based symbol extraction for 12 languages: Rust, Go, Python, TypeScript/TSX, Java, C, C++, C#, Ruby, PHP, Scala, JavaScript +- 📄 **Svelte support** — review Svelte components with specialized analysis +- 🔌 **MCP server** — 15 tools for AI coding agents (review, search, brain, debt, trace, ...) - 💾 **Diff-hash caching** — skip repeat reviews automatically - 🔧 **Configurable** — per-project `.cora.yaml`, global `~/.cora/config.yaml`, or env vars diff --git a/docs/changelog.md b/docs/changelog.md index 9e21e75..c08ef54 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,6 +7,71 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added — Code Intelligence + +- **Tree-sitter AST extraction for 8 additional languages** (#376) + - Java, C, C++, C#, Ruby, PHP, Scala, JavaScript — full AST-based symbol extraction. + - Tree-sitter now covers **12 languages** total (previously: Rust, Go, Python, TypeScript/TSX). + - AST path takes priority when tree-sitter is compiled; regex extractors remain as fallback. + +- **Svelte symbol indexing** (#375) + - Svelte components are now indexed (scripts, props, stores, imports). + - Uses TypeScript tree-sitter parser for script blocks. + +- **Dart symbol indexing** (#374) + - Classes, mixins, extensions, methods, top-level functions, and enums. + +## [0.8.1] - 2026-07-24 + +### Changed + +- **Version bump for crates.io publish** — no functional changes. + +## [0.8.0] - 2026-07-16 + +### Highlights + +- **Brain Mode** — hybrid semantic search combining FTS5, vector KNN (usearch), and call-graph BFS with Reciprocal Rank Fusion (RRF). All local, no model download. +- **Tree-sitter AST extraction** — opt-in `--features tree-sitter` builds get accurate symbol extraction and call graph edges for Rust, Go, Python, TypeScript/TSX. +- **Architecture commands** — `cora trace` for call chain tracing, `cora arch` for module/edge overview. +- **VitePress documentation** — migrated from SvelteKit to VitePress at `codecora.dev`. + +### Added + +- **Brain Mode** — hybrid semantic search (#362) + - Three search signals fused via RRF (k=60): FTS5 keyword, usearch HNSW vector KNN, call-graph BFS. + - `cora brain ` command with `--json` and `--limit` flags. + - `cora.brain_search` MCP tool. + +- **Static token embedding engine** (#354) + - Zero-dependency bag-of-tokens hashing producing 256d vectors. + - No model download, no GPU, no external service. + - Integrated into `cora index` for vector index population. + +- **Tree-sitter AST extraction** (#356) + - Opt-in via `--features tree-sitter` at build time. + - Initial 4 languages: Rust, Go, Python, TypeScript/TSX. + - Schema v3 `edges` table for AST-derived call graph relationships. + +- **`cora trace` and `cora arch`** (#358) + - `cora trace ` — depth-limited BFS call chain tracing (outgoing/incoming). + - `cora arch` — architecture overview showing modules, edge types, and top connectors. + +- **Global index database** (#355) + - Migrated from `.cora/index.db` (per-project) to `~/.codecora/cora-code/graph.db` (global). + - Multi-project search: index multiple repos, search across all of them. + +- **Code Intelligence documentation** (#365) + - New `docs/code-intelligence.md` covering indexing, search, call graph, and MCP tools. + +### Changed + +- **Renamed `cora-cli` → `cora-code`** (#338) — crate name, repo description, all references. +- **Security scanner false positives suppressed** (#369) — `sec-hardcoded-url` and `sec-hardcoded-secret/crypto` patterns refined to reduce noise (#357, #364). +- **Uteke memory integration hidden from user-facing docs** (#367) — still functional, just not advertised externally. +- **VitePress docs** — adopted `@codecora-theme`, retired SvelteKit `website/` directory. +- **CI** — decoupled GitHub Release from crates.io publish (#371). + ## [0.7.0] - 2026-07-16 ### Highlights @@ -596,9 +661,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Cross-platform** — Linux (x86_64, ARM64), macOS (Apple Silicon), Windows (x86_64) - **MIT License** — fully open source -[Unreleased]: https://github.com/codecoradev/cora-code/compare/v0.6.1...develop +[Unreleased]: https://github.com/codecoradev/cora-code/compare/v0.8.1...develop +[0.8.1]: https://github.com/codecoradev/cora-code/compare/v0.8.0...v0.8.1 +[0.8.0]: https://github.com/codecoradev/cora-code/compare/v0.7.0...v0.8.0 +[0.7.0]: https://github.com/codecoradev/cora-code/compare/v0.6.2...v0.7.0 +[0.6.2]: https://github.com/codecoradev/cora-code/compare/v0.6.1...v0.6.2 [0.6.1]: https://github.com/codecoradev/cora-code/compare/v0.6.0...v0.6.1 -[0.6.0]: https://github.com/codecoradev/cora-code/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/codecoradev/cora-code/compare/v0.4.6...v0.5.0 [0.4.6]: https://github.com/codecoradev/cora-code/compare/v0.4.5...v0.4.6 [0.4.5]: https://github.com/codecoradev/cora-code/compare/v0.4.4...v0.4.5 diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 0f4fe16..3de3962 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -66,6 +66,14 @@ Complete command reference for the cora CLI. | `cora brain` `--limit N` | Max results (default: 20) | | `cora index` | Index project symbols into SQLite + usearch | | `cora index --rebuild` | Rebuild index from scratch | +| `cora index --watch` | Auto-sync file watcher (2s poll interval) | +| `cora index --stats` | Show index statistics (symbol count, languages, DB size) | +| `cora index --prune` | Remove stale entries for deleted files | +| `cora callers` `` | Find all callers of a symbol (reverse call graph) | +| `cora callers` `--limit N` | Max callers to return (default: 50) | +| `cora impact` `` | Analyze blast radius of changing a symbol | +| `cora impact` `--depth N` | Traversal depth (default: 3) | +| `cora affected` `` | Find test files affected by source changes | | `cora completion` `` | Generate shell completions (bash/zsh/fish) | | `cora mcp` | Start MCP server for AI coding agents (Claude Code, Cursor, Windsurf) | @@ -111,3 +119,10 @@ $ cora brain "TokenEmbedding" --json --limit 5 $ cora trace main $ cora arch ``` + +```bash +# Find callers and analyze impact +$ cora callers handle_request +$ cora impact process_order --depth 5 +$ cora affected src/order.rs src/payment.rs +``` diff --git a/docs/code-intelligence.md b/docs/code-intelligence.md index 90f7e85..3e83961 100644 --- a/docs/code-intelligence.md +++ b/docs/code-intelligence.md @@ -32,10 +32,11 @@ cora arch Scans your project, extracts symbol definitions (functions, structs, enums, traits, etc.), and builds: | Component | Technology | Storage | ------------|-----------|---------| -| Symbol table | Regex extractors (15 languages) | SQLite FTS5 | +|-----------|-----------|---------| +| Symbol table (regex) | Regex extractors (15 languages) | SQLite FTS5 | +| Symbol table (AST) | Tree-sitter grammars (12 languages, opt-in) | SQLite FTS5 | | Vector embeddings | Static token hashing (256d) | usearch HNSW index | -| Call graph | Regex scope tracking (+ tree-sitter opt-in) | SQLite `edges` table | +| Call graph | Regex scope tracking + tree-sitter AST edges | SQLite `edges` table | ```bash cora index # Index current project (incremental) @@ -47,7 +48,34 @@ cora index --prune # Remove symbols from deleted files ### Supported Languages -15 language extractors: Rust, Python, TypeScript/TSX, Go, Java, C, Ruby, PHP, Swift, Scala, Lua, Zig, Dart, Kotlin, JavaScript. +Cora extracts symbols using two strategies — **regex** (always available) and **tree-sitter AST** (opt-in via `--features tree-sitter` at build time): + +| | Regex Extraction | AST (Tree-sitter) | +|--|-----------------|-------------------| +| **Rust** | ✅ | ✅ | +| **Go** | ✅ | ✅ | +| **Python** | ✅ | ✅ | +| **TypeScript/TSX** | ✅ | ✅ | +| **Svelte** | ✅ | ✅ (via TypeScript) | +| **Java** | ✅ | ✅ | +| **C** | ✅ | ✅ | +| **C++** | ✅ | ✅ | +| **C#** | ✅ | ✅ | +| **Ruby** | ✅ | ✅ | +| **PHP** | ✅ | ✅ | +| **Scala** | ✅ | ✅ | +| **JavaScript** | ✅ | ✅ | +| **Swift** | ✅ | — | +| **Kotlin** | ✅ | — | +| **Dart** | ✅ | — | +| **Lua** | ✅ | — | +| **Zig** | ✅ | — | + +**Regex-only** languages work out of the box. For **AST-accurate** extraction (full call graph, precise imports/exports), build with tree-sitter: + +```bash +cargo install --git https://github.com/codecoradev/cora-code --features tree-sitter +``` ### Multi-Project Index @@ -167,10 +195,12 @@ cora trace "process" --depth 4 # Limit traversal depth cora trace --json # JSON output ``` -Requires schema v3 edges table. Enable tree-sitter for AST-based edge extraction: +Requires schema v3 edges table. When built with `--features tree-sitter`, Cora uses AST-based edge extraction for more accurate call graphs. Regex-only builds still generate edges via scope tracking. ```bash -cora index --rebuild # With tree-sitter feature compiled +# Build with tree-sitter for best call graph accuracy +cargo install --git https://github.com/codecoradev/cora-code --features tree-sitter +cora index --rebuild # Re-index to get AST edges ``` ### `cora arch` — Architecture Overview @@ -202,13 +232,15 @@ cora affected --json # JSON output All code intelligence features are available as MCP tools for AI coding agents: | Tool | Description | -------|-------------| +|------|-------------| | `cora.search_symbols` | FTS5 symbol search | | `cora.find_callers` | Find callers of a symbol | | `cora.find_impact` | Impact analysis | | `cora.find_affected_tests` | Test impact analysis | | `cora.index_status` | Index statistics | | `cora.brain_search` | Hybrid semantic search | +| `cora.get_project_info` | Project metadata & config | +| `cora.get_memory` | Review memory recall | ## Data Directory diff --git a/docs/roadmap.md b/docs/roadmap.md index a6a8765..acfc057 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -102,16 +102,22 @@ Query layer built on top of the v0.6 index — call graph traversal, test impact - [#268](https://github.com/codecoradev/cora-code/issues/268) Language expansion — 6 → 15+ language support — ✓ Done - [#269](https://github.com/codecoradev/cora-code/issues/269) Auto-sync file watcher daemon — ✓ Done -## Future — What's Next +## v0.8 — Brain Mode & Tree-sitter Expansion -### Brain Mode Roadmap +Hybrid semantic search and deep language support via AST parsing. - [Phase 1](https://github.com/codecoradev/cora-code/pull/354) — Static token embedding engine (256d) — ✓ Done -- [Phase 2](https://github.com/codecoradev/cora-code/pull/356) — tree-sitter AST + schema v3 edges table — ✓ Done +- [Phase 2](https://github.com/codecoradev/cora-code/pull/356) — Tree-sitter AST + schema v3 edges table — ✓ Done - [Phase 2C](https://github.com/codecoradev/cora-code/pull/358) — `cora trace` + `cora arch` — ✓ Done - [Phase 3](https://github.com/codecoradev/cora-code/pull/362) — Brain Mode hybrid search (usearch + RRF) — ✓ Done -- [Phase 4](https://github.com/codecoradev/cora-code/issues/360) — Uteke integration upgrade → Planned -- [Phase 5](https://github.com/codecoradev/cora-code/issues/361) — Voyage-4-Nano ONNX (opt-in, 1024d) → Deferred +- [#374](https://github.com/codecoradev/cora-code/pull/374) Dart symbol indexing — ✓ Done +- [#375](https://github.com/codecoradev/cora-code/pull/375) Svelte symbol indexing — ✓ Done +- [#376](https://github.com/codecoradev/cora-code/pull/376) Tree-sitter expansion: 4 → 12 languages (Java, C, C++, C#, Ruby, PHP, Scala, JS) — ✓ Done +- [#338](https://github.com/codecoradev/cora-code/pull/338) Renamed `cora-cli` → `cora-code` — ✓ Done +- [#369](https://github.com/codecoradev/cora-code/pull/369) Security scanner false positive suppression — ✓ Done +- [#355](https://github.com/codecoradev/cora-code/pull/355) Global index database (`~/.codecora/cora-code/graph.db`) — ✓ Done + +## Future — What's Next ### Other From c4ddc13a7c49bb15414fa978c2296eb1539afda0 Mon Sep 17 00:00:00 2001 From: ajianaz Date: Fri, 24 Jul 2026 19:10:12 +0700 Subject: [PATCH 5/5] chore: bump v0.8.2 for release --- Cargo.toml | 2 +- docs/changelog.md | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 33e9049..699f4e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cora-code" -version = "0.8.1" +version = "0.8.2" edition = "2024" description = "CLI-first AI code review — BYOK, diff/scan/branch, pre-commit hooks" license = "MIT" diff --git a/docs/changelog.md b/docs/changelog.md index c08ef54..9dfd162 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,7 +5,7 @@ All notable changes to cora-code are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.8.2] - 2026-07-24 ### Added — Code Intelligence @@ -21,6 +21,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Dart symbol indexing** (#374) - Classes, mixins, extensions, methods, top-level functions, and enums. +### Changed — Documentation + +- Updated README, changelog (v0.8.0 + v0.8.1 entries), roadmap (v0.8 section), CLI reference, and code intelligence docs. + +## [Unreleased] + ## [0.8.1] - 2026-07-24 ### Changed @@ -661,7 +667,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Cross-platform** — Linux (x86_64, ARM64), macOS (Apple Silicon), Windows (x86_64) - **MIT License** — fully open source -[Unreleased]: https://github.com/codecoradev/cora-code/compare/v0.8.1...develop +[Unreleased]: https://github.com/codecoradev/cora-code/compare/v0.8.2...develop +[0.8.2]: https://github.com/codecoradev/cora-code/compare/v0.8.1...v0.8.2 [0.8.1]: https://github.com/codecoradev/cora-code/compare/v0.8.0...v0.8.1 [0.8.0]: https://github.com/codecoradev/cora-code/compare/v0.7.0...v0.8.0 [0.7.0]: https://github.com/codecoradev/cora-code/compare/v0.6.2...v0.7.0