Skip to content

Commit bcc9fba

Browse files
authored
feat(java/parser):support java for pr (#88)
1 parent e4ba257 commit bcc9fba

File tree

61 files changed

+4721
-103
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+4721
-103
lines changed

docs/system_architecture.md

Lines changed: 303 additions & 0 deletions
Large diffs are not rendered by default.

docs/tree-sitter_and_lsp_zh.md

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# 文档:如何使用 Tree-sitter 和 LSP 实现符号解析
2+
3+
## 1. 引言
4+
5+
本文档旨在深入解析 `abcoder` 项目中实现的符号解析(Symbol Resolution)机制。该机制采用了一种创新的混合模式,结合了 **Tree-sitter** 的快速语法分析能力和 **语言服务器协议(LSP)** 的深度语义理解能力。
6+
7+
通过阅读本文,开发人员可以理解以下内容:
8+
9+
- 符号信息收集的整体流程和核心调用链路。
10+
- Tree-sitter 如何用于快速构建代码的抽象语法树(AST)并识别基本代码结构。
11+
- LSP 如何在 Tree-sitter 的基础上提供精确的语义信息,如“跳转到定义”(Go to Definition)。
12+
- 这种混合模式为何能兼顾解析速度与语义准确性。
13+
14+
## 2. 核心组件与调用链路
15+
16+
符号解析的核心逻辑位于 `lang/collect/collect.go` 文件中,主要由 `Collector` 结构体及其方法驱动。
17+
18+
### 2.1. 调用链路概览
19+
20+
整个符号收集过程的调用链路如下:
21+
22+
1. **入口点**: 外部调用(例如 `lang/parse.go` 中的 `collectSymbol` 函数)是流程的起点。
23+
2. **初始化 Collector**: 通过 `collect.NewCollector(repoPath, cli)` 创建一个 `Collector` 实例。此实例包含了 LSP 客户端 `cli` 和一个针对特定语言的规约 `spec`
24+
3. **开始收集**: 调用 `Collector.Collect(ctx)` 方法,这是符号收集的主逻辑。
25+
4. **策略分支**: 在 `Collect` 方法内部,系统会根据语言做出策略选择:
26+
* **对于 Java 语言**: 调用 `ScannerByTreeSitter` 方法,进入 Tree-sitter + LSP 的混合解析模式。
27+
* **对于其他语言 (如 Rust)**: 调用 `ScannerFile` 方法,进入纯 LSP 的解析模式。
28+
29+
### 2.2. 核心组件
30+
31+
- **`Collector`**: 一个中心协调器,负责管理符号收集的整个生命周期,包括配置、文件扫描、符号提取和关系构建。
32+
- **`LSPClient`**: LSP 客户端的封装,用于与语言服务器(如 `jdt.ls``rust-analyzer`)通信,发送请求(如 `textDocument/definition`)并接收响应。
33+
- **`LanguageSpec`**: 定义了特定语言的行为和规则,例如如何解析导入语句、如何判断符号类型等。
34+
- **Tree-sitter Parser**: (主要在 `lang/java/parser` 中)用于将源代码字符串高效地解析成具体的语法树(CST/AST)。
35+
36+
## 3. Tree-sitter 语法解析 (AST Parsing)
37+
38+
当处理 Java 项目时,`ScannerByTreeSitter` 方法被触发,它首先利用 Tree-sitter 进行快速的语法结构分析。
39+
40+
### 3.1. 流程详解
41+
42+
1. **项目扫描**:
43+
* 首先,它会尝试解析项目根目录下的 `pom.xml` 文件,以获取所有 Maven 模块的路径。
44+
* 然后,它会遍历这些模块路径下的所有 `.java` 文件。
45+
46+
2. **文件解析**:
47+
* 对于每个 Java 文件,它会读取文件内容。
48+
* 调用 `javaparser.Parse(ctx, content)`,该函数内部使用 Tree-sitter 将文件内容解析成一棵完整的语法树(`sitter.Tree`)。
49+
* 通知 LSP 服务器文件已打开 (`c.cli.DidOpen(ctx, uri)`),以便 LSP 服务器建立对该文件的认知。
50+
51+
3. **AST 遍历与初步符号化**:
52+
* 获得语法树后,调用 `c.walk(tree.RootNode(), ...)` 方法,从根节点开始深度优先遍历 AST。
53+
* `walk` 方法通过一个 `switch node.Type()` 语句来识别不同类型的语法节点,例如:
54+
* `package_declaration` (包声明)
55+
* `import_declaration` (导入语句)
56+
* `class_declaration` (类定义)
57+
* `method_declaration` (方法定义)
58+
* `field_declaration` (字段定义)
59+
* 当匹配到类、方法等定义节点时,它会从节点中提取名称、范围等信息,并创建一个初步的 `DocumentSymbol` 对象。这个对象此时主要包含**语法信息**,其定义位置就是当前文件中的位置。
60+
61+
```go
62+
// Simplified version of the walk method in collect.go
63+
func (c *Collector) walk(node *sitter.Node, ...) {
64+
switch node.Type() {
65+
case "class_declaration":
66+
// 1. Extract class name from the node
67+
nameNode := javaparser.FindChildIdentifier(node)
68+
name := nameNode.Content(content)
69+
70+
// 2. Create a preliminary DocumentSymbol based on syntax info
71+
sym := &DocumentSymbol{
72+
Name: name,
73+
Kind: SKClass,
74+
Location: Location{URI: uri, Range: ...}, // Location within the current file
75+
Node: node, // Store the tree-sitter node
76+
Role: DEFINITION,
77+
}
78+
c.syms[sym.Location] = sym
79+
80+
// 3. Recursively walk into the class body
81+
bodyNode := node.ChildByFieldName("body")
82+
if bodyNode != nil {
83+
for i := 0; i < int(bodyNode.ChildCount()); i++ {
84+
c.walk(bodyNode.Child(i), ...)
85+
}
86+
}
87+
return
88+
89+
case "method_declaration":
90+
// ... similar logic for methods ...
91+
}
92+
}
93+
```
94+
95+
## 4. LSP 语义增强 (Semantic Enhancement)
96+
97+
仅靠 Tree-sitter 只能知道“这里有一个类定义”,但无法知道一个变量引用的具体类型定义在何处(尤其是在其他文件中)。这时,LSP 的作用就体现出来了。
98+
99+
`walk` 遍历和后续处理中,系统会利用 LSP 来“增强”由 Tree-sitter 创建的符号。
100+
101+
### 4.1. 流程详解
102+
103+
1. **获取精确的定义位置**:
104+
* 当 Tree-sitter 解析到一个引用(例如一个类继承 `extends MyBaseClass` 或一个字段声明 `private MyType myVar;`)时,它会创建一个代表该引用的临时符号。
105+
* 随后,系统调用 `c.findDefinitionLocation(ref)` 方法。该方法内部会向 LSP 服务器发送一个 `textDocument/definition` 请求。
106+
* LSP 服务器(已经索引了项目)会返回该符号的**真正定义位置**,可能在另一个文件,甚至在依赖的库中。
107+
* `Collector` 用 LSP 返回的权威位置更新符号的 `Location` 字段。
108+
109+
```go
110+
// Get the precise definition location from LSP
111+
func (c *Collector) findDefinitionLocation(ref *DocumentSymbol) Location {
112+
// Send a "go to definition" request to the LSP server
113+
defs, err := c.cli.Definition(context.Background(), ref.Location.URI, ref.Location.Range.Start)
114+
if err != nil || len(defs) == 0 {
115+
// If LSP can't find it (e.g., external library), return the reference location
116+
return ref.Location
117+
}
118+
// Return the authoritative location from LSP
119+
return defs[0]
120+
}
121+
```
122+
123+
2. **校准符号信息**:
124+
* 在 `walk` 方法中,当创建一个 `DocumentSymbol`(如类或方法)时,会调用 `c.findLocalLSPSymbol(uri)`
125+
* 此函数会向 LSP 请求当前文件的所有符号(`textDocument/documentSymbol`),并将其缓存。
126+
* 然后,它会用 LSP 返回的符号列表来校准 Tree-sitter 找到的符号。例如,LSP 提供的 `method` 符号名称通常包含完整的签名(如 `myMethod(String)`),这比 Tree-sitter 单纯提取的 `myMethod` 更精确。
127+
128+
## 5. 流程总结与图示
129+
130+
系统通过两阶段的过程实现高效而准确的符号解析:
131+
132+
1. **阶段一 (语法解析)**: 使用 Tree-sitter 快速扫描所有源文件,构建 AST,并识别出所有的定义和引用的基本语法结构。
133+
2. **阶段二 (语义链接)**: 遍历阶段一的成果,对每一个引用,利用 LSP`definition` 请求查询其权威定义位置,从而建立起跨文件的符号依赖关系图。
134+
135+
### Mermaid 架构图
136+
137+
```mermaid
138+
graph TD
139+
subgraph "Phase 1: Fast Syntax Parsing (Tree-sitter)"
140+
A[Start: ScannerByTreeSitter] --> B{Scan Project Files};
141+
B --> C[Read Java file content];
142+
C --> D[javaparser Parse];
143+
D --> E[Generate AST];
144+
E --> F[AST Traversal];
145+
F --> G[Identify syntax nodes: class, method, field];
146+
G --> H[Create Preliminary DocumentSymbol];
147+
end
148+
149+
subgraph "Phase 2: Semantic Linking (LSP)"
150+
H -. Reference Found .-> I{findDefinitionLocation};
151+
I --> J[Send definition request to LSP];
152+
J --> K[Receive Authoritative Location];
153+
K --> L[Update DocumentSymbol location];
154+
end
155+
156+
L --> M[Symbol with Full Semantic Info];
157+
158+
subgraph "Parallel LSP Interaction"
159+
F --> F_LSP{findLocalLSPSymbol};
160+
F_LSP --> F_LSP2[Request documentSymbol from LSP];
161+
F_LSP2 --> H_Update[Calibrate Symbol Name/Range];
162+
end
163+
164+
H --> H_Update;
165+
```
166+
167+
## 6. 结论
168+
169+
`abcoder` 采用的 Tree-sitter + LSP 混合符号解析模型是一个非常出色的工程实践。它结合了:
170+
171+
- **Tree-sitter 的优点**:
172+
- **极高的性能**: 无需预热或完整的项目索引,可以非常快速地解析单个文件。
173+
- **容错性强**: 即使代码有语法错误,也能生成部分可用的 AST
174+
- **纯粹的语法分析**: 专注于代码结构,不依赖复杂的构建环境。
175+
176+
- **LSP 的优点**:
177+
- **强大的语义理解**: 能够理解整个项目的上下文,包括依赖、继承关系和类型推断。
178+
- **准确性高**: 提供的是经过语言服务器深度分析后的权威信息。
179+
180+
通过让 Tree-sitter 完成粗粒度的结构化解析,再由 LSP 进行精确的语义“链接”,该系统在保证分析速度的同时,实现了高度准确的符号依赖关系构建,为上层代码理解和智能操作提供了坚实的基础。

docs/uast_conversion_guide.md

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
# UAST 结构与转换流程详解
2+
3+
## 1. 引言
4+
5+
本文档旨在详细解析项目中的 **UAST (Universal Abstract Syntax Tree, 统一抽象语法树)** 的核心数据结构、设计理念以及从特定语言的 CST (Concrete Syntax Tree, 具体语法树) 到 UAST 的转换流程。
6+
7+
**面向读者**:
8+
* **新加入的研发人员**: 快速理解项目核心的代码表示层。
9+
* **语言扩展开发者**: 在为项目支持新语言时,提供标准的 UAST 构建指南。
10+
* **架构师与代码分析工具开发者**: 深入了解 UAST 的设计,以便于上层应用的开发与集成。
11+
12+
**系统概览**: UAST 是本项目中用于表示多语言代码的统一中间表示。它将不同编程语言的语法结构抽象为一组通用的、包含丰富语义信息的图结构,是实现跨语言代码分析、转换和生成等功能的核心基础。
13+
14+
## 2. 核心概念与数据结构
15+
16+
UAST 的设计围绕着几个核心概念展开,它们共同构成了一个强大的代码表示模型。
17+
18+
### 2.1. 顶层结构
19+
20+
* `Repository`: 代码库的最高层级抽象,包含一个或多个 `Module`
21+
* `Module`: 代表一个独立的、特定语言的代码单元,例如一个 Go Module、一个 Java Maven 项目或一个 Python 包。
22+
* `Package`: 语言内部的命名空间,如 Go 的 `package` 或 Java 的 `package`
23+
* `File`: 代表一个物理源代码文件。
24+
25+
### 2.2. 核心实体:`Node`
26+
27+
`Node` 是 UAST 图模型中最基本的单元。每个 `Node` 代表代码中的一个具名实体。
28+
29+
* **`NodeType`**: 节点类型,主要分为三种:
30+
* `FUNC`: 代表函数或方法。
31+
* `TYPE`: 代表类、结构体、接口、枚举等类型定义。
32+
* `VAR`: 代表全局变量或常量。
33+
34+
* **`Identity`**: 全局唯一标识符,是链接不同 `Node` 的关键。它由三部分组成:
35+
* `ModPath`: 模块路径 (e.g., `github.com/your/project@v1.2.0`)
36+
* `PkgPath`: 包路径 (e.g., `github.com/your/project/internal/utils`)
37+
* `Name`: 实体名称 (e.g., `MyFunction`, `MyStruct.MyMethod`)
38+
* **完整形式**: `ModPath?PkgPath#Name`
39+
40+
### 2.3. 实体详情
41+
42+
每个 `Node` 都关联一个更详细的实体描述结构,存储了该实体的具体信息。
43+
44+
* **`Function`**: 存储函数的签名、参数、返回值、接收者(如果是方法)以及它调用的其他函数/方法列表。
45+
* **`Type`**: 存储类型的种类(`struct`, `interface` 等)、字段、内嵌/继承的类型、实现的方法和接口。
46+
* **`Var`**: 存储变量的类型、是否为常量/指针等信息。
47+
48+
### 2.4. 关系:`Relation`
49+
50+
`Relation` 用于描述两个 `Node` 之间的关系,是构建 UAST 图谱的边。
51+
52+
* **`RelationKind`**: 关系类型,主要包括:
53+
* `DEPENDENCY`: 表示一个节点依赖另一个节点(例如函数调用、类型使用)。
54+
* `IMPLEMENT`: 表示一个类型节点实现了一个接口节点。
55+
* `INHERIT`: 表示一个类型节点继承了另一个类型节点。
56+
* `GROUP`: 表示多个变量/常量在同一个声明块中定义。
57+
58+
### 2.5. UAST 核心结构图
59+
60+
下图展示了 UAST 核心数据结构之间的关系。
61+
62+
```mermaid
63+
graph TD
64+
subgraph Repository
65+
direction LR
66+
A[NodeGraph]
67+
M(Modules)
68+
end
69+
70+
subgraph Module
71+
direction LR
72+
P[Packages]
73+
F[Files]
74+
end
75+
76+
subgraph Package
77+
direction LR
78+
Funcs[Functions]
79+
Types[Types]
80+
Vars[Variables]
81+
end
82+
83+
subgraph Node
84+
direction TB
85+
ID[Identity]
86+
NodeType[Type: FUNC/TYPE/VAR]
87+
Rels[Relations]
88+
end
89+
90+
Repository -- Contains --> M
91+
M -- Contains --> Module
92+
Repository -- Contains --> A
93+
A -- "Maps ID to" --> Node
94+
95+
Module -- Contains --> P
96+
Module -- Contains --> F
97+
98+
Package -- Contains --> Funcs
99+
Package -- Contains --> Types
100+
Package -- Contains --> Vars
101+
102+
Node -- Has a --> ID
103+
Node -- Has a --> NodeType
104+
Node -- "Has multiple" --> Rels
105+
106+
Funcs -- Corresponds to --> Node
107+
Types -- Corresponds to --> Node
108+
Vars -- Corresponds to --> Node
109+
110+
style Repository fill:#f9f,stroke:#333,stroke-width:2px
111+
style Module fill:#ccf,stroke:#333,stroke-width:2px
112+
style Package fill:#cfc,stroke:#333,stroke-width:2px
113+
style Node fill:#fcf,stroke:#333,stroke-width:2px
114+
```
115+
116+
## 3. 从 CST 到 UAST 的转换流程
117+
118+
将特定语言的源代码转换为统一的 UAST,主要分为以下几个步骤。此流程的核心思想是 **“先收集实体,再建立关系”**
119+
120+
### 3.1. 流程概览
121+
122+
```mermaid
123+
graph TD
124+
A[1. 源代码] --> B{2. CST 解析};
125+
B --> C[3. 遍历 CST, 创建 UAST 实体];
126+
C --> D{4. 填充 Repository};
127+
D --> E[5. 构建 Node Graph];
128+
E --> F((6. UAST));
129+
130+
subgraph "Language Specific Parser (e.g., Tree-sitter)"
131+
B
132+
end
133+
134+
subgraph "UAST Converter (Go Code)"
135+
C
136+
D
137+
E
138+
end
139+
140+
style A fill:#lightgrey
141+
style F fill:#9f9
142+
```
143+
144+
### 3.2. 步骤详解
145+
146+
1. **CST 解析**:
147+
* 使用 `tree-sitter` 或其他特定语言的解析器,将输入的源代码字符串解析为一棵具体语法树 (CST)。CST 完整地保留了代码的所有语法细节,包括标点和空格。
148+
149+
2. **遍历 CST, 创建 UAST 实体**:
150+
* 编写一个针对该语言的 `Converter`。这个转换器会深度优先遍历 CST。
151+
* 当遇到代表函数、类、接口、变量声明等关键语法节点时,提取其核心信息(名称、位置、内容等)。
152+
* 为每个识别出的实体创建一个对应的 UAST 结构(`Function`, `Type`, `Var`),并为其生成一个全局唯一的 `Identity`
153+
* 在此阶段,也会初步解析实体内部的依赖关系,例如一个函数内部调用了哪些其他函数,这些信息会被临时存储在 `Function.FunctionCalls` 等字段中。
154+
155+
3. **填充 Repository**:
156+
* 将上一步创建的所有 `Function`, `Type`, `Var` 实体,按照其 `Identity` 中定义的模块和包路径,存入一个 `Repository` 对象中。此时,我们得到了一个包含所有代码实体信息但关系尚未连接的“半成品”。
157+
158+
4. **构建 Node Graph (`Repository.BuildGraph`)**:
159+
* 这是将离散的实体连接成图的关键一步。调用 `Repository.BuildGraph()` 方法。
160+
* 该方法会遍历 `Repository` 中的每一个 `Function`, `Type`, `Var`
161+
* 为每一个实体在 `Repository.Graph` 中创建一个 `Node`
162+
* 然后,它会检查每个实体的依赖字段(如 `Function.FunctionCalls`, `Type.Implements` 等)。
163+
* 根据这些依赖信息,在对应的 `Node` 之间创建 `Relation`,从而将整个图连接起来。例如,如果 `FunctionA` 调用了 `FunctionB`,那么在 `NodeA``NodeB` 之间就会建立一条 `DEPENDENCY` 关系的边。
164+
165+
### 3.3. 转换时序图示例
166+
167+
以下是一个简化的时序图,展示了从 Java 代码到 UAST 的转换过程。
168+
169+
```mermaid
170+
sequenceDiagram
171+
participant C as Converter
172+
participant T as TreeSitter
173+
participant R as Repository
174+
participant N as NodeGraph
175+
176+
C->>T: Parse("class A { void b() {} }")
177+
T-->>C: 返回 CST Root
178+
179+
C->>C: Traverse CST
180+
C->>R: Create/Get Module("my-java-project")
181+
C->>R: Create/Get Package("com.example")
182+
C->>R: SetType(Identity_A, Type_A)
183+
C->>R: SetFunction(Identity_B, Function_B)
184+
185+
Note right of C: 此时实体已收集, 但未连接
186+
187+
C->>R: BuildGraph()
188+
R->>N: SetNode(Identity_A, TYPE)
189+
R->>N: SetNode(Identity_B, FUNC)
190+
R->>N: AddRelation(Node_A, Node_B, DEPENDENCY)
191+
192+
Note right of R: 图关系建立完成
193+
194+
R-->>C: UAST Graph Ready
195+
```
196+
197+
## 4. 如何使用 UAST
198+
199+
一旦 UAST 构建完成,你就可以利用它进行各种强大的代码分析:
200+
201+
* **依赖分析**: 从任意一个 `Node` 出发,沿着 `Dependencies` 关系,可以找到它的所有依赖项。反之,沿着 `References` 可以找到所有引用它的地方。
202+
* **影响范围分析**: 当一个函数或类型发生变更时,可以通过 `References` 关系,快速定位到所有可能受影响的代码。
203+
* **代码导航**: 实现类似 IDE 的“跳转到定义”、“查找所有引用”等功能。
204+
* **重构**: 自动化地进行代码重构,例如重命名一个方法,并更新所有调用点。
205+
206+
## 5. FAQ 与开发建议
207+
208+
* **为什么不直接使用 CST?**
209+
* CST 过于具体且语言相关,直接使用它进行跨语言分析非常困难。UAST 提供了一个统一的、更高层次的抽象视图。
210+
* **如何添加对新语言的支持?**
211+
* 1. 找到或构建一个该语言的 `tree-sitter` 解析器。
212+
* 2. 实现一个新的 `Converter`,负责遍历该语言的 CST,并创建 UAST 实体。
213+
* 3. 确保 `Identity` 的生成规则与其他语言保持一致。
214+
* **LSP 的作用是什么?**
215+
* 虽然 `tree-sitter` 能提供语法结构,但很多语义信息(如一个变量的具体类型、一个函数调用到底解析到哪个定义)需要更复杂的类型推导。LSP 已经完成了这些工作,可以作为信息来源,极大地丰富 UAST 中 `Relation` 的准确性和语义信息。在转换流程中,可以集成 LSP 查询来辅助确定依赖关系。

0 commit comments

Comments
 (0)