Files
DevFlow/src-tauri/src/commands/ai/code_intel.rs
T
lxy bacec20c3c 修复: 变量定义判定误伤函数调用值
子树包含判定改直取 value 节点 kind
const x = arr.map(...) 不再误判为符号定义
subtree_contains_kind 无其他使用方一并删除
2026-10-01 08:27:49 +08:00

960 lines
41 KiB
Rust

//! 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 声明,仅当初始化为函数/类值才算定义(收窄判定见 is_definition_node)
"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<Language> {
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 切 <script> 段借 ts),
/// 但不提取符号只探测存在性,开销低。**复用现有函数,不新写正则**。
/// 无 grammar / 解析失败 / Vue 无 script / 无定义节点 → false(不提示)。
pub(crate) fn contains_definition(source: &str, ext: &str) -> bool {
let ext_lower = ext.to_lowercase();
let (parse_source, parse_ext): (String, &str) = if ext_lower == "vue" {
// Vue 逻辑在 <script> 段,切段借 ts(与 read_symbol 的 extract_vue_script 同源)
match extract_vue_script(source) {
Some((script, _)) => (script, "ts"),
None => return false, // 纯 template/style 无可提取符号
}
} else {
(source.to_string(), ext_lower.as_str())
};
let grammar = match grammar_for(parse_ext) {
Some(g) => g,
None => return false, // 不支持的语言不提示
};
let mut parser = Parser::new();
if parser.set_language(&grammar).is_err() {
return false; // grammar 加载失败(ABI 不兼容)不提示
}
let tree = match parser.parse(&parse_source, None) {
Some(t) => t,
None => return false, // 解析失败不提示
};
tree_has_definition(&tree.root_node())
}
/// JS/TS 函数/类值表达式 kind:variable_declarator 是否算定义据此判定
const FUNCTION_VALUE_KINDS: &[&str] = &["arrow_function", "function", "generator_function", "class"];
/// 节点是否为可提取定义:kind 在表内,variable_declarator 需初始化为函数/类值。
///
/// variable_declarator 单看 kind 过宽:`let msg = "fn main() {}"` 这类普通声明也命中,
/// contains_definition 会对几乎任何 JS/TS 文件误报提示。收窄为 `const fn = () => {}`
/// 形态(初始化值本身是函数/类表达式)才算定义,contains_definition / read_symbol /
/// outline 三处共用同一判定。判定看 value 节点自身 kind 而非子树包含——
/// `const x = arr.map(n => ...)` 子树含 arrow_function,但 x 不是函数定义。
fn is_definition_node(node: &Node<'_>) -> bool {
if !DEFINITION_KINDS.contains(&node.kind()) {
return false;
}
if node.kind() != "variable_declarator" {
return true;
}
match node.child_by_field_name("value") {
Some(v) => FUNCTION_VALUE_KINDS.contains(&v.kind()),
None => false,
}
}
/// 递归遍历语法树,判断是否含任一定义节点(contains_definition 的遍历内核)。
fn tree_has_definition(node: &Node<'_>) -> bool {
if is_definition_node(node) {
return true;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if tree_has_definition(&child) {
return true;
}
}
false
}
/// 提取 Vue SFC 的 `<script>` 段内容(借 TS 解析)。
///
/// Vue 单文件组件逻辑在 `<script setup lang="ts">` 内,template/style 不做符号提取。
/// 切出 `<script>...</script>` 内容喂 tree-sitter-typescript:
/// - 省 1 个 grammar(tree-sitter-vue 不引,降依赖体积)
/// - 避 SFC 嵌套复杂度(tree-sitter-vue 解析 template/style/script 混合,符号提取只关心 script)
///
/// 多个 script 段取首个(`<script setup>` 主逻辑);无 script 返 None(走兜底)。
/// 返回 (script 内容, script 在原 source 中的字节起始偏移)用于行号映射。
fn extract_vue_script(source: &str) -> Option<(String, usize)> {
let start_tag = source.find("<script")?;
// 跳过 <script ...> 的属性(到 >),取标签体开始
let tag_close = source[start_tag..].find('>')? + start_tag + 1;
let end = source[tag_close..].find("</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 文件无 <script> 段(纯 template/style 无可提取符号)"),
}
} else {
(source.to_string(), ext.as_str(), 0)
};
let grammar = match grammar_for(parse_ext) {
Some(g) => g,
None => {
return fallback(symbol, path, file_hash, "no_grammar",
&format!("不支持 {} 文件符号解析(支持 .rs/.ts/.tsx/.js/.jsx/.vue/.go/.java/.py)", ext));
}
};
let mut parser = Parser::new();
if parser.set_language(&grammar).is_err() {
return fallback(symbol, path, file_hash, "parse_failed",
"grammar 加载失败(tree-sitter 版本/ABI 不兼容,疑似环境问题)");
}
let tree = match parser.parse(&parse_source, None) {
Some(t) => t,
None => return fallback(symbol, path, file_hash, "parse_failed", "解析失败(源码可能有语法错误)"),
};
let root = tree.root_node();
let mut found: Option<Node> = None;
find_definition(&root, symbol, kind_hint, parse_source.as_bytes(), &mut found);
// drill:在目标符号节点子树内递归找名为 drill 的嵌套定义(局部函数/impl 方法)。
// 命中即返该子定义 full 态;未命中走兜底(reason=drill_not_found,不 panic)。
if let Some(drill_name) = drill {
if let Some(sym_node) = found {
let mut drill_found: Option<Node> = None;
find_definition(&sym_node, drill_name, None, parse_source.as_bytes(), &mut drill_found);
return match drill_found {
Some(dn) => extract_symbol(dn, parse_source.as_bytes(), source, path, file_hash, true, line_offset),
None => fallback(drill_name, path, file_hash, "drill_not_found",
&format!("符号「{}」内未找到下钻子定义「{}」(可能非嵌套定义/作用域差异)", symbol, drill_name)),
};
}
// found 本身为 None 时 drill 无意义,落入下方 not_found 兜底
}
match found {
Some(node) => extract_symbol(node, parse_source.as_bytes(), source, path, file_hash, full, line_offset),
None => fallback(symbol, path, file_hash, "not_found",
&format!("文件内未找到符号「{}」(可能拼写/作用域差异,试 grep 搜定义)", symbol)),
}
}
/// 递归遍历找匹配 symbol 名的定义节点。
///
/// 命中条件:node 为定义节点(is_definition_node)+ name field 文本 == symbol + kind_hint(若传)匹配。
/// 首个命中即返(find_definition 调用方短路)。
fn find_definition<'a>(
node: &Node<'a>,
symbol: &str,
kind_hint: Option<&str>,
source: &[u8],
found: &mut Option<Node<'a>>,
) {
if found.is_some() {
return;
}
if is_definition_node(node) {
let kind_ok = kind_hint.map_or(true, |h| kind_matches(node.kind(), h));
if kind_ok {
if let Some(name_node) = node.child_by_field_name("name") {
let name = node_text(name_node, source);
if name == symbol {
*found = Some(*node);
return;
}
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
find_definition(&child, symbol, kind_hint, source, found);
if found.is_some() {
return;
}
}
}
/// kind_hint 宽松匹配:user 传 "function" 命中 function_item/function_declaration/method_definition 等。
fn kind_matches(node_kind: &str, hint: &str) -> bool {
let h = hint.to_lowercase();
node_kind.contains(&h)
|| (h == "function" && (node_kind.contains("function") || node_kind == "method_definition"))
|| (h == "struct" && node_kind.contains("struct"))
|| (h == "class" && node_kind.contains("class"))
|| (h == "enum" && node_kind.contains("enum"))
}
/// 取节点文本(字节切片避免 utf8_text 的 lifetime/Result 坑)。
/// 返 owned String(符号体不大,避免借用 lifetime 纠缠调用方)。
fn node_text(node: Node<'_>, source: &[u8]) -> String {
std::str::from_utf8(&source[node.start_byte()..node.end_byte()])
.unwrap_or("")
.to_string()
}
/// 提取符号三态信息,组装返回 Value。
fn extract_symbol(
node: Node<'_>,
source: &[u8],
_orig_source: &str,
path: &str,
file_hash: &str,
full: bool,
line_offset: usize,
) -> serde_json::Value {
let name = node
.child_by_field_name("name")
.map(|n| node_text(n, source).to_string())
.unwrap_or_default();
let kind = node.kind().to_string();
// 行号:script 内相对行(0-based)+ Vue 偏移 → 原文件 1-based 行
let start_row = node.start_position().row + 1 + line_offset;
let end_row = node.end_position().row + 1 + line_offset;
let body_lines = end_row - start_row + 1;
let node_src = node_text(node, source);
let signature = extract_signature(&node_src);
let calls = collect_calls(node, source, line_offset);
if full {
serde_json::json!({
"symbol": name,
"kind": kind,
"path": path,
"file_hash": file_hash,
"line_start": start_row,
"line_end": end_row,
"body_lines": body_lines,
"signature": signature,
"calls": calls,
"mode": "full",
"content": node_src,
})
} else {
serde_json::json!({
"symbol": name,
"kind": kind,
"path": path,
"file_hash": file_hash,
"line_start": start_row,
"line_end": end_row,
"body_lines": body_lines,
"signature": signature,
"calls": calls,
"mode": "skeleton",
"hint": "骨架态:传 full=true 取完整定义体(语法边界精准);传 drill=\"子定义名\" 下钻符号内嵌套定义(局部函数/impl 方法)",
})
}
}
/// 提取签名:节点文本到首个 `{`(函数/结构体体开始)或 `;`(声明)之前的部分。
/// 无 `{`/`;` 取首行。trim 收尾空白 + 注释。
fn extract_signature(node_src: &str) -> String {
let cut = node_src
.find('{')
.or_else(|| node_src.find(';'))
.unwrap_or_else(|| node_src.len());
let sig = node_src[..cut].trim();
if sig.is_empty() {
node_src.lines().next().unwrap_or("").trim().to_string()
} else {
sig.to_string()
}
}
/// 收集节点内所有 call_expression 的被调用者(同文件静态调用点)。
///
/// Phase1:扁平收集去重(Phase2 按分支结构组织调用图)。
/// Rust/TS/JS 的 call_expression 都有 "function" field(callee),统一提取。
/// Phase2:每项带行号(call_expression 所在行,1-based + Vue 偏移,与 start_row 同口径)。
fn collect_calls(node: Node, source: &[u8], line_offset: usize) -> Vec<serde_json::Value> {
let mut calls = Vec::new();
let mut seen = HashSet::new();
collect_calls_inner(node, source, line_offset, &mut calls, &mut seen);
calls
}
fn collect_calls_inner(
node: Node,
source: &[u8],
line_offset: usize,
out: &mut Vec<serde_json::Value>,
seen: &mut HashSet<String>,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "call_expression" {
if let Some(func) = child.child_by_field_name("function") {
let name = node_text(func, source).to_string();
if !name.is_empty() && seen.insert(name.clone()) {
// 行号:调用语句所在行(call_expression 节点本身的 start_position),1-based + Vue 偏移
let line = child.start_position().row + 1 + line_offset;
out.push(serde_json::json!({ "name": name, "line": line }));
}
}
}
collect_calls_inner(child, source, line_offset, out, seen);
}
}
/// 文件符号概览(outline)— 提取**顶层**定义符号列表,供前端预览面板做符号树。
///
/// 与 read_symbol 复用同一套解析(grammar_for + DEFINITION_KINDS + Vue 切 script 段),
/// 但只返精简元数据 `{ name, kind, line, signature }`(不含全文/调用,轻量)。
/// 遍历语义:命中 DEFINITION_KINDS 的节点记入并**不下钻其子树** —— impl 内方法、
/// 函数/类内嵌套定义不算顶层(与 IDE 符号树顶层视图一致)。
///
/// **不报错底线**:无 grammar / 解析失败 / 无符号 / Vue 无 script → 空 Vec,不 Err 不 panic。
pub fn extract_outline(source: &str, ext: &str) -> Vec<serde_json::Value> {
let ext_lower = ext.to_lowercase();
// Vue 借 TS:切 script 段,行号偏移映射回原文件(与 read_symbol 同源)。
let (parse_source, parse_ext, line_offset): (String, &str, usize) = if ext_lower == "vue" {
match extract_vue_script(source) {
Some((script, byte_off)) => {
let off_lines = source[..byte_off].matches('\n').count();
(script, "ts", off_lines)
}
None => return Vec::new(), // 纯 template/style 无可提取符号
}
} else {
(source.to_string(), ext_lower.as_str(), 0)
};
let grammar = match grammar_for(parse_ext) {
Some(g) => g,
None => return Vec::new(), // 不支持的语言
};
let mut parser = Parser::new();
if parser.set_language(&grammar).is_err() {
return Vec::new(); // grammar 加载失败(ABI 不兼容)
}
let tree = match parser.parse(&parse_source, None) {
Some(t) => t,
None => return Vec::new(), // 解析失败
};
let bytes = parse_source.as_bytes();
let mut symbols = Vec::new();
collect_top_level_symbols(tree.root_node(), bytes, line_offset, &mut symbols);
symbols
}
/// 收集顶层定义符号(记入 + 不下钻:命中定义节点后不再进其子树)。
fn collect_top_level_symbols(
node: Node<'_>,
source: &[u8],
line_offset: usize,
out: &mut Vec<serde_json::Value>,
) {
if is_definition_node(&node) {
let name = symbol_name(node, source);
let kind = node.kind().to_string();
let line = node.start_position().row + 1 + line_offset;
// Python 等无 `{`/`;` 终结符的语言,extract_signature 会含函数体:outline 只取首行(头行即签名)。
let signature = extract_signature(&node_text(node, source))
.lines()
.next()
.unwrap_or("")
.trim()
.to_string();
out.push(serde_json::json!({
"name": name,
"kind": kind,
"line": line,
"signature": signature,
}));
return; // 不下钻(顶层符号语义)
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_top_level_symbols(child, source, line_offset, out);
}
}
/// 取定义节点展示名:name 字段 → type 字段(Rust impl_item)→ 首个 identifier 子节点
/// (Go type_declaration 等 name 藏在一层子节点内)。全空返空串(前端显示签名兜底)。
fn symbol_name(node: Node<'_>, source: &[u8]) -> String {
if let Some(n) = node.child_by_field_name("name") {
let t = node_text(n, source);
if !t.is_empty() {
return t;
}
}
if let Some(n) = node.child_by_field_name("type") {
let t = node_text(n, source);
if !t.is_empty() {
return t;
}
}
find_first_identifier(node, source).unwrap_or_default()
}
/// DFS 找首个 identifier 类子节点(Go type_declaration → type_spec → type_identifier 隔两层)。
fn find_first_identifier(node: Node<'_>, source: &[u8]) -> Option<String> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
let kind = child.kind();
if kind == "identifier" || kind.ends_with("_identifier") {
let t = node_text(child, source);
if !t.is_empty() {
return Some(t);
}
}
}
for child in node.children(&mut cursor) {
if let Some(t) = find_first_identifier(child, source) {
return Some(t);
}
}
None
}
/// 兜底返回(不报错底线):任何失败路径统一返此结构,LLM 据 fallback/suggestion 回退 grep/read_file。
fn fallback(symbol: &str, path: &str, file_hash: &str, reason: &str, suggestion: &str) -> serde_json::Value {
serde_json::json!({
"symbol": symbol,
"path": path,
"file_hash": file_hash,
"fallback": true,
"reason": reason,
"suggestion": suggestion,
"tip": "回退:用 grep(pattern=symbol) 搜定义位置 → read_file(path, offset=行号) 精准读取",
})
}
// ============================================================
// 单测:三态提取 + 兜底 + grammar lookup + Vue 切段
// ============================================================
#[cfg(test)]
mod tests {
use super::*;
const RUST_FIXTURE: &str = r#"use std::io;
pub fn read_symbol(path: &str) -> Result<(), io::Error> {
let content = load(path)?;
parse(&content);
validate(path);
Ok(())
}
fn parse(content: &str) {
helper(content);
}
fn helper(_x: &str) {}
struct Config {
name: String,
}
"#;
#[test]
fn test_grammar_for_all_exts() {
assert!(grammar_for("rs").is_some());
assert!(grammar_for("ts").is_some());
assert!(grammar_for("tsx").is_some());
assert!(grammar_for("js").is_some());
assert!(grammar_for("jsx").is_some());
// Phase1 扩展:Go / Java / Python(静态编译,表驱动加表项)
assert!(grammar_for("go").is_some());
assert!(grammar_for("java").is_some());
assert!(grammar_for("py").is_some());
// Vue 借 TS:grammar 本身返 None(切段后用 ts)
assert!(grammar_for("vue").is_none());
// 不支持语言
assert!(grammar_for("").is_none());
}
#[test]
fn test_is_supported_ext() {
assert!(is_supported_ext("rs"));
assert!(is_supported_ext("go"));
assert!(is_supported_ext("java"));
assert!(is_supported_ext("py"));
assert!(is_supported_ext("vue"));
// 不支持语言
assert!(!is_supported_ext("rb"));
assert!(!is_supported_ext("md"));
}
#[test]
fn test_read_symbol_rust_skeleton() {
let r = read_symbol(RUST_FIXTURE, "hash1", "test.rs", "read_symbol", false, None, None);
assert_eq!(r["fallback"], serde_json::Value::Null);
assert_eq!(r["symbol"], "read_symbol");
assert_eq!(r["kind"], "function_item");
assert_eq!(r["mode"], "skeleton");
// 骨架态不返 content,返 signature + calls
assert!(r["content"].is_null());
assert!(r["signature"].as_str().unwrap().contains("read_symbol"));
// calls 含同文件静态调用(parse/validate)+ load(跨文件也命中名,Phase1 扁平)
// Phase2:每项是 {name, line} 对象
let calls = r["calls"].as_array().unwrap();
assert!(calls.iter().any(|c| c["name"] == "parse"));
assert!(calls.iter().any(|c| c["name"] == "validate"));
assert_eq!(r["body_lines"].as_u64().unwrap(), 6);
}
#[test]
fn test_read_symbol_rust_full() {
let r = read_symbol(RUST_FIXTURE, "hash1", "test.rs", "read_symbol", true, None, None);
assert_eq!(r["mode"], "full");
let content = r["content"].as_str().unwrap();
// full 态返完整定义体(含函数体 {})
assert!(content.contains("pub fn read_symbol"));
assert!(content.contains("let content = load(path)?;"));
assert!(content.trim_end().ends_with('}'));
}
#[test]
fn test_read_symbol_struct() {
let r = read_symbol(RUST_FIXTURE, "h", "test.rs", "Config", false, None, None);
assert_eq!(r["symbol"], "Config");
assert_eq!(r["kind"], "struct_item");
}
#[test]
fn test_read_symbol_kind_hint_filter() {
// kind_hint="struct" 时,"read_symbol"(function)不应被误匹配为 struct
let r = read_symbol(RUST_FIXTURE, "h", "test.rs", "Config", false, Some("struct"), None);
assert_eq!(r["symbol"], "Config");
assert_eq!(r["kind"], "struct_item");
}
#[test]
fn test_read_symbol_not_found_fallback() {
let r = read_symbol(RUST_FIXTURE, "h", "test.rs", "nonexistent_fn", false, None, None);
assert_eq!(r["fallback"], true);
assert_eq!(r["reason"], "not_found");
assert!(r["suggestion"].as_str().unwrap().contains("grep"));
}
#[test]
fn test_read_symbol_no_grammar_fallback() {
// Ruby 无 grammar(.rb 未集成 tree-sitter-ruby),走兜底。
// 注:原用 .py 做"无 grammar"样例,Phase1 扩展加 Python 支持后失效。
let src = "def hello\n puts 'hi'\nend\n";
let r = read_symbol(src, "h", "test.rb", "hello", false, None, None);
assert_eq!(r["fallback"], true);
assert_eq!(r["reason"], "no_grammar");
assert!(r["suggestion"].as_str().unwrap().contains("不支持"));
}
#[test]
fn test_read_symbol_vue_script_extraction() {
let vue = r#"<template><div>{{ msg }}</div></template>
<script setup lang="ts">
function greet(name: string) {
console.log(name);
helper();
}
function helper() {}
</script>
<style scoped></style>
"#;
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 = "<template><div>hi</div></template>\n<style></style>";
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<script>let x = 1;</script>";
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 含 <script> 函数 → true
let vue = "<template><div>{{ msg }}</div></template>\n\n<script setup lang=\"ts\">\nfunction greet() {}\n</script>\n";
assert!(contains_definition(vue, "vue"));
// 无 <script> 段(纯 template/style)→ false
let no_script = "<template><div>hi</div></template>\n<style></style>";
assert!(!contains_definition(no_script, "vue"));
}
// ============================================================
// extract_outline — 文件符号概览(顶层符号列表)
// ============================================================
#[test]
fn test_extract_outline_rust_top_level() {
let src = "use std::io;\n\npub fn read_symbol(path: &str) {\n helper(path);\n}\n\nfn helper(_x: &str) {}\n\nstruct Config {\n name: String,\n}\n\nimpl Config {\n fn new() -> Self { Config { name: String::new() } }\n}\n";
let symbols = extract_outline(src, "rs");
// 顶层:read_symbol / helper / Config(struct) / Config(impl);impl 内 new 不下钻
let names: Vec<&str> = symbols.iter().map(|s| s["name"].as_str().unwrap()).collect();
assert_eq!(names, vec!["read_symbol", "helper", "Config", "Config"]);
assert_eq!(symbols[0]["kind"], "function_item");
assert_eq!(symbols[2]["kind"], "struct_item");
// impl Config 的 name 来自 type 字段
assert_eq!(symbols[3]["kind"], "impl_item");
assert_eq!(symbols[3]["name"], "Config");
// 行号 1-based
assert_eq!(symbols[0]["line"].as_u64().unwrap(), 3);
assert_eq!(symbols[1]["line"].as_u64().unwrap(), 7);
// signature 只到首个 {,不含函数体
assert_eq!(symbols[0]["signature"].as_str().unwrap(), "pub fn read_symbol(path: &str)");
assert!(!symbols[0]["signature"].as_str().unwrap().contains("helper(path)"));
}
#[test]
fn test_extract_outline_vue_line_offset() {
let vue = "<template><div>{{ msg }}</div></template>\n\n<script setup lang=\"ts\">\nfunction greet() {\n helper();\n}\n\nfunction helper() {}\n</script>\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 行 <script> 标签后),greet 在原文件第 4 行、helper 第 8 行
assert_eq!(symbols[0]["line"].as_u64().unwrap(), 4);
assert_eq!(symbols[1]["line"].as_u64().unwrap(), 8);
}
#[test]
fn test_extract_outline_ts_export_and_arrow() {
let src = "export function greet() { return 1; }\nexport const handler = () => {};\ninterface Foo { bar: string }\n";
let symbols = extract_outline(src, "ts");
let kinds: Vec<&str> = symbols.iter().map(|s| s["kind"].as_str().unwrap()).collect();
// export 包装函数仍命中(export_statement 非定义,下钻到 function_declaration)
assert_eq!(kinds, vec!["function_declaration", "variable_declarator", "interface_declaration"]);
assert_eq!(symbols[0]["name"], "greet");
assert_eq!(symbols[1]["name"], "handler");
assert_eq!(symbols[1]["signature"].as_str().unwrap(), "handler = () =>");
}
#[test]
fn test_extract_outline_go_type_declaration() {
let src = "package main\n\nfunc main() {}\n\ntype User struct {\n Name string\n}\n";
let symbols = extract_outline(src, "go");
assert_eq!(symbols[0]["name"], "main");
assert_eq!(symbols[0]["kind"], "function_declaration");
// Go type_declaration 的 name 藏在一层子节点(type_spec)内,走 identifier 回退
assert_eq!(symbols[1]["name"], "User");
assert_eq!(symbols[1]["kind"], "type_declaration");
assert_eq!(symbols[1]["line"].as_u64().unwrap(), 5);
}
#[test]
fn test_extract_outline_java_class_only_top() {
let src = "public class Foo {\n private int x;\n public void bar() {}\n}\n";
let symbols = extract_outline(src, "java");
// 类内 method 不下钻:仅 class Foo 一个顶层符号
assert_eq!(symbols.len(), 1);
assert_eq!(symbols[0]["name"], "Foo");
assert_eq!(symbols[0]["kind"], "class_declaration");
}
#[test]
fn test_extract_outline_python() {
let src = "import os\n\ndef foo(a):\n return a\n\nclass Bar:\n def method(self):\n pass\n";
let symbols = extract_outline(src, "py");
// class 内 method 不下钻
assert_eq!(symbols.len(), 2);
assert_eq!(symbols[0]["name"], "foo");
assert_eq!(symbols[0]["kind"], "function_definition");
assert_eq!(symbols[1]["name"], "Bar");
assert_eq!(symbols[1]["kind"], "class_definition");
}
#[test]
fn test_extract_outline_unsupported_or_fail_returns_empty() {
// 不支持语言
assert!(extract_outline("# Title\nplain text", "md").is_empty());
// 无 script 的 Vue
assert!(extract_outline("<template><div>hi</div></template>", "vue").is_empty());
// 纯文本(无定义节点)
assert!(extract_outline("just some text\nno definitions", "rs").is_empty());
// 残缺语法(tree-sitter 容错仍产出节点,不 panic 即可,不强行断言空)
let _ = extract_outline("fn broken(", "rs");
}
}