937 lines
40 KiB
Rust
937 lines
40 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 声明(如 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<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())
|
|
}
|
|
|
|
/// 递归遍历语法树,判断是否含任一 DEFINITION_KINDS 定义节点(contains_definition 的遍历内核)。
|
|
fn tree_has_definition(node: &Node<'_>) -> bool {
|
|
if DEFINITION_KINDS.contains(&node.kind()) {
|
|
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.kind() 在 DEFINITION_KINDS 内 + 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 DEFINITION_KINDS.contains(&node.kind()) {
|
|
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 DEFINITION_KINDS.contains(&node.kind()) {
|
|
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");
|
|
}
|
|
}
|