//! AST 代码智能 — tree-sitter 符号解析(read_symbol 三态) //! //! 治 aichat read_file 全文回灌 prompt 爆(信息密度驱动 ≠ 压缩,见设计文档): //! read_symbol 按语义层级返符号 **骨架**(默认)/ **全文**(full),替代物理读全文件。 //! LLM 默认拿骨架(签名 + 行范围 + 内部调用)→ 要细节再 full 取完整定义体 → 进 prompt 的都是主动要的,天然高密度。 //! 设计:docs/02-架构设计/专项设计/AST符号解析-设计-2026-06-24.md //! //! **不报错底线**:无 grammar / 解析失败 / symbol 未找到 / Vue 无 script / 任何大小函数 → //! 返兜底提示(grep + read_file),不 Err / 不 panic / 不阻断。 use std::collections::HashSet; use tree_sitter::{Language, Node, Parser}; /// Phase1 支持的定义节点 kind(Rust + TS/JS 通用,Vue 借 TS)。 /// /// tree-sitter 各 grammar 的定义节点 kind 基本一致: /// - Rust:function_item / struct_item / impl_item 等(`_item` 后缀) /// - TS/JS:function_declaration / class_declaration / interface_declaration / method_definition 等 /// 统一放一张表(find_definition 命中即提取),跨语言零分支。 const DEFINITION_KINDS: &[&str] = &[ // Rust "function_item", "function_signature_item", "struct_item", "enum_item", "impl_item", "trait_item", "mod_item", "const_item", "static_item", "type_item", "macro_definition", // TS / JS(与 Rust kind 不冲突) "function_declaration", "generator_function_declaration", "class_declaration", "abstract_class_declaration", "interface_declaration", "enum_declaration", "method_definition", // TS/JS const/let/var 声明(如 const fn = () => {}) "variable_declarator", // Go "method_declaration", // Go method(receiver func) "type_declaration", // Go type Xxx struct/interface/func // Java "constructor_declaration", // Java constructor "annotation_type_declaration", // @interface "record_declaration", // Java record(Java 16+) // Python(tree-sitter-python 用 _definition 后缀) "function_definition", "class_definition", ]; /// 文件后缀 → tree-sitter grammar 集中 lookup(静态编译,不动态加载 / 不抽 trait)。 /// /// 设计(grammar 策略 2026-06-24 定稿,详见插件机制设计文档): /// **6 个 grammar 覆盖 9 类文件**,架构成本最低。 /// - `.rs` → tree-sitter-rust /// - `.ts` → tree-sitter-typescript(language_typescript) /// - `.tsx` → tree-sitter-typescript(language_tsx) /// - `.js` / `.jsx` → tree-sitter-javascript /// - `.go` → tree-sitter-go /// - `.java` → tree-sitter-java /// - `.py` → tree-sitter-python /// - `.vue` → 返回 None 标记"需切 script 段"(Vue 借 TS,逻辑在 script,read_symbol 内 extract_vue_script 切段喂 ts) /// /// 未来加语言 = 此处加一行 + 可能补 DEFINITION_KINDS(表驱动,~几行),不返工调用方。 /// tree-sitter 动态加载技术上可行(Helix/Neovim 先例)但跨平台分发 + ABI 坑,当前不值(见插件设计)。 pub fn grammar_for(ext: &str) -> Option { match ext { "rs" => Some(Language::from(tree_sitter_rust::LANGUAGE)), "ts" => Some(Language::from(tree_sitter_typescript::LANGUAGE_TYPESCRIPT)), "tsx" => Some(Language::from(tree_sitter_typescript::LANGUAGE_TSX)), "js" | "jsx" => Some(Language::from(tree_sitter_javascript::LANGUAGE)), "go" => Some(Language::from(tree_sitter_go::LANGUAGE)), "java" => Some(Language::from(tree_sitter_java::LANGUAGE)), "py" => Some(Language::from(tree_sitter_python::LANGUAGE)), // Vue 借 TS:返 None 标记需切 script 段(read_symbol 内 extract_vue_script + 喂 ts) "vue" => None, _ => None, } } /// 判断 ext 是否为支持语言(含 Vue 借 TS)。file_outline 据此提前判 ext(不支持不读文件)。 pub fn is_supported_ext(ext: &str) -> bool { matches!(ext, "rs" | "ts" | "tsx" | "js" | "jsx" | "vue" | "go" | "java" | "py") } /// 判断 source 内容是否真的含可提取的函数/类/结构体定义(AC-4 read_file 符号级命中提示)。 /// /// read_file 读取成功后据此决定是否追加「改用 read_symbol」提示:仅当内容里真的存在 /// DEFINITION_KINDS 定义节点才提示——比旧按扩展名判断更准,无定义的文件不瞎提示。 /// 与 read_symbol 同语法树逻辑(grammar_for + DEFINITION_KINDS + Vue 切 ` 内容喂 tree-sitter-typescript: /// - 省 1 个 grammar(tree-sitter-vue 不引,降依赖体积) /// - 避 SFC 嵌套复杂度(tree-sitter-vue 解析 template/style/script 混合,符号提取只关心 script) /// /// 多个 script 段取首个(`")?; Some((source[tag_close..tag_close + end].to_string(), tag_close)) } /// read_symbol 三态读取(信息密度驱动)。 /// /// **不按物理行返、不截断**,按语义结构层级返(消解"截断多少行"争论): /// - `full=false`(默认 骨架态):返签名 + kind + 行范围 + 内部调用列表 + 总行数(极小,高密度) /// - `full=true`(全文态):返完整定义体(语法边界 100% 准) /// /// **不报错底线**:无 grammar / 解析失败 / symbol 未找到 / Vue 无 script → 返兜底提示(grep + read_file),不 panic。 /// /// 参数: /// - `source`:文件全文(由 handler 读 + 算 hash 后传入,纯函数不碰 fs) /// - `file_hash`:文件 hash(handler 算,回传 LLM 供后续定位 / 兜底回取) /// - `path`:路径(取 ext + 兜底消息展示) /// - `symbol`:目标符号名(精确匹配定义节点的 name field) /// - `full`:true=完整定义体,false=骨架 /// - `kind_hint`:可选 kind 过滤(如 "function"/"struct"),None 匹配任意定义 kind /// - `drill`:可选子定义名(如 "parse");Some(name)=在目标符号节点子树内递归找名为 name 的 /// 嵌套定义(局部函数/impl 方法),命中即返该子定义的 full 态;未命中走兜底(reason=drill_not_found)。 /// None=不下钻,走原 full/kind_hint 三态逻辑。 pub fn read_symbol( source: &str, file_hash: &str, path: &str, symbol: &str, full: bool, kind_hint: Option<&str>, drill: Option<&str>, ) -> serde_json::Value { let ext = path.rsplit('.').next().unwrap_or("").to_lowercase(); let is_vue = ext == "vue"; // Vue 借 TS:切 script 段(Vue 逻辑在 script,template/style 不做符号) let (parse_source, parse_ext, line_offset): (String, &str, usize) = if is_vue { match extract_vue_script(source) { Some((script, byte_off)) => { // Vue 切段后节点行号是 script 内相对行,加原文件中 script 起始行偏移映射回原行号 let off_lines = source[..byte_off].matches('\n').count(); (script, "ts", off_lines) } None => return fallback(symbol, path, file_hash, "vue_no_script", "Vue 文件无 "#; let r = read_symbol(vue, "h", "App.vue", "greet", false, None, None); assert_eq!(r["fallback"], serde_json::Value::Null); assert_eq!(r["symbol"], "greet"); assert_eq!(r["kind"], "function_declaration"); // Vue 切段后行号映射回原文件(greet 在原文件第 5 行附近) let start = r["line_start"].as_u64().unwrap(); assert!(start >= 4, "Vue 行号应映射回原文件(script 段偏移后),实际 {}", start); let calls = r["calls"].as_array().unwrap(); // Phase2:calls 每项是 {name, line} 对象 assert!(calls.iter().any(|c| c["name"] == "helper")); } #[test] fn test_read_symbol_vue_no_script_fallback() { let vue = "\n"; let r = read_symbol(vue, "h", "App.vue", "greet", false, None, None); assert_eq!(r["fallback"], true); assert_eq!(r["reason"], "vue_no_script"); } /// Phase2 drill:下钻目标符号(read_symbol)内的嵌套子定义(parse)。 /// RUST_FIXTURE 中 read_symbol 函数体内调用了 parse,但 parse 是顶层函数(非 read_symbol 子树内), /// 故用带嵌套局部函数的 fixture 验证:outer 内定义 inner,drill="inner" 应命中并返 full 态。 #[test] fn test_read_symbol_drill_hits_nested() { let src = r#" fn outer() { fn inner(x: i32) -> i32 { x + 1 } inner(42) } "#; // 外层骨架态,确认 outer 能解析到 let skeleton = read_symbol(src, "h", "t.rs", "outer", false, None, None); assert_eq!(skeleton["symbol"], "outer"); // 下钻 inner:应在 outer 子树内命中,返 full 态 let drill = read_symbol(src, "h", "t.rs", "outer", false, None, Some("inner")); assert_eq!(drill["fallback"], serde_json::Value::Null, "drill 命中应返符号非兜底"); assert_eq!(drill["symbol"], "inner"); assert_eq!(drill["mode"], "full", "drill 命中走 full 态(完整定义体)"); let content = drill["content"].as_str().unwrap(); assert!(content.contains("fn inner"), "drill content 应含 inner 定义体"); } /// Phase2 drill 未命中兜底:下钻外层符号内不存在的子定义名 → fallback reason=drill_not_found。 #[test] fn test_read_symbol_drill_not_found_fallback() { let src = "fn outer() {\n fn inner() {}\n}\n"; let drill = read_symbol(src, "h", "t.rs", "outer", false, None, Some("nonexistent")); assert_eq!(drill["fallback"], true); assert_eq!(drill["reason"], "drill_not_found"); assert!(drill["suggestion"].as_str().unwrap().contains("nonexistent")); } #[test] fn test_extract_vue_script() { let vue = "a\nb\n"; let (script, off) = extract_vue_script(vue).unwrap(); assert_eq!(script, "let x = 1;"); // script 在第 3 行(0-indexed offset 后),行偏移应 = 2(\n 数) assert_eq!(vue[..off].matches('\n').count(), 2); } #[test] fn test_extract_signature() { // function 带 body let s = extract_signature("pub fn foo(x: i32) -> i32 {\n x + 1\n}"); assert_eq!(s, "pub fn foo(x: i32) -> i32"); // struct 带 body let s = extract_signature("struct Config {\n name: String,\n}"); assert_eq!(s, "struct Config"); // 声明以 ; 结尾 let s = extract_signature("fn foo();"); assert_eq!(s, "fn foo()"); } #[test] fn test_collect_calls_dedup() { let src = "fn a() { b(); b(); c(); }"; let mut parser = Parser::new(); parser.set_language(&Language::from(tree_sitter_rust::LANGUAGE)).unwrap(); let tree = parser.parse(src, None).unwrap(); let calls = collect_calls(tree.root_node(), src.as_bytes(), 0); // b 去重只出现一次(Phase2:每项是 {name, line} 对象) assert!(calls.iter().any(|c| c["name"] == "b")); assert_eq!(calls.iter().filter(|c| c["name"] == "b").count(), 1); assert!(calls.iter().any(|c| c["name"] == "c")); // line 应为调用所在行(src 单行,a 在第 1 行) assert_eq!(calls[0]["line"].as_u64().unwrap(), 1); } /// 多角度验证(用户要求:实现后看效果 + 多角度测试): /// 解析 devflow 真实源码 code_intel.rs 自身,验骨架态密度提升(设计目标:降一个量级)。 /// 对比 read_symbol 骨架(单符号签名+calls)vs read_file 全文(整个文件所有符号)的体积。 #[test] fn test_read_symbol_real_devflow_token_density() { let manifest = env!("CARGO_MANIFEST_DIR"); // src-tauri/ let path_str = format!("{}/src/commands/ai/code_intel.rs", manifest); let source = std::fs::read_to_string(&path_str) .expect("应能读到 devflow 自身 code_intel.rs"); let full_bytes = source.len(); // 骨架态:解析自身 read_symbol 函数(只给该符号,非整个文件) let skeleton = read_symbol(&source, "h0", "code_intel.rs", "read_symbol", false, None, None); assert_ne!(skeleton["fallback"], true, "真实文件应解析出 read_symbol 符号"); assert_eq!(skeleton["symbol"], "read_symbol"); assert_eq!(skeleton["kind"], "function_item"); let skeleton_str = serde_json::to_string(&skeleton).unwrap(); let skeleton_bytes = skeleton_str.len(); // 密度断言:read_symbol 骨架(单符号)<< read_file 全文(整个文件)。 // 正是治 prompt 爆的核心:LLM 要理解 read_symbol,工具只给该符号骨架,不给全文件。 let ratio = full_bytes as f64 / skeleton_bytes.max(1) as f64; eprintln!( "[密度效果] code_intel.rs 全文 {} 字节 vs read_symbol 骨架 {} 字节 → 降 {:.1}x", full_bytes, skeleton_bytes, ratio ); assert!(ratio > 5.0, "骨架密度应显著高于全文(降 >5x),实际 {:.1}x", ratio); // full 态:完整定义体(语法边界 100% 准) let full_sym = read_symbol(&source, "h1", "code_intel.rs", "read_symbol", true, None, None); let content = full_sym["content"].as_str().expect("full 态应返 content"); assert!(content.contains("pub fn read_symbol")); // calls 应含 read_symbol 内部调用的函数(grammar_for/extract_symbol/fallback 等) let calls = skeleton["calls"].as_array().expect("骨架应含 calls"); assert!(!calls.is_empty(), "应提取出内部调用"); } // ============================================================ // contains_definition — AC-4 read_file 符号级命中检测 // ============================================================ #[test] fn test_contains_definition_hits_definitions() { // 含 fn/struct 定义 → true(应提示改用 read_symbol) assert!(contains_definition(RUST_FIXTURE, "rs")); assert!(contains_definition("pub fn main() {}\n", "rs")); assert!(contains_definition("struct Config { name: String }\n", "rs")); // TS/JS assert!(contains_definition("export function greet() { return 1; }", "ts")); assert!(contains_definition("const handler = () => {};", "js")); // Go / Java / Python assert!(contains_definition("package main\nfunc main() {}\n", "go")); assert!(contains_definition("class Foo { void bar() {} }", "java")); assert!(contains_definition("def foo():\n pass\n", "py")); } #[test] fn test_contains_definition_plain_text_false() { // 无定义节点的纯文本/纯注释 → false(符号级命中,不瞎提示) assert!(!contains_definition("just some text\nwith no definitions\n", "rs")); assert!(!contains_definition("// 只有注释\n/* 多行注释 */\n", "rs")); // 字符串字面量里有 fn 字样但不是定义 assert!(!contains_definition("let msg = \"fn main() {}\";\nconsole.log(msg);", "js")); } #[test] fn test_contains_definition_unsupported_ext_or_parse_fail() { // Ruby 无 grammar → false assert!(!contains_definition("def foo()\n puts 'hi'\nend\n", "rb")); // 无扩展名 → false assert!(!contains_definition("any content", "")); // 大小写不敏感("RS" 等价 "rs") assert!(contains_definition("pub fn main() {}\n", "RS")); } #[test] fn test_contains_definition_vue() { // Vue 含 \n"; assert!(contains_definition(vue, "vue")); // 无 \n"; let symbols = extract_outline(vue, "vue"); assert_eq!(symbols.len(), 2); assert_eq!(symbols[0]["name"], "greet"); assert_eq!(symbols[0]["kind"], "function_declaration"); // script 内容以 \n 开头(第 3 行