Arbiter——静态分析Agent的实现

项目开发日记:静态分析Agent的实现

基于tree-sitter构建代码智能分析工具

在代码审查流程中,需要自动分析代码质量、发现潜在问题。我开发了这个静态分析Agent,它能解析代码AST,提取函数签名、调用图、复杂度指标,并输出结构化的issues列表供下游消费。

为什么是tree-sitter?

在对比了正则表达式和Python AST后,我选择了tree-sitter,原因是:

  • 多语言支持:不只限于Python,还能支持JavaScript、Go等多种语言
  • 错误容忍:即使代码有语法错误,也能生成部分AST
  • 精确定位:能精确获取每个节点的行列位置
  • 增量解析:支持增量更新,适合大型项目

我甚至考虑过LLM,这个能理解上下文,提出建议,但考虑成本,延迟,结果不可靠,以及我这个项目的目的

架构设计

核心模块

复制代码
StaticAnalyzerAgent (主控制器)
    ↓
TreeSitterParser (解析封装)
    ↓
分析引擎 (AST分析 + 正则降级)
    ↓
结构化输出

数据模型

我定义了以下几个核心数据结构:

Issue:发现的问题

  • severity: error/warning/info
  • line: 行号
  • description: 问题描述
  • suggestion: 修复建议

FunctionInfo:函数信息

  • name: 函数名
  • start_line/end_line: 起止行号
  • parameters: 参数列表
  • complexity: 圈复杂度
  • is_recursive: 是否递归
  • calls: 调用的函数列表

AnalysisResult:分析结果

  • success: 是否成功
  • failure_category: 失败类型
  • issues: 问题列表
  • functions: 函数列表
  • call_graph: 调用图
  • metrics: 代码指标
  • degraded: 是否降级

核心功能实现

1. 代码解析

我封装了TreeSitterParser类来处理解析逻辑:

python 复制代码
class TreeSitterParser:
    def __init__(self):
        self.python_language = Language(tspython.language())
        self.parser = Parser(self.python_language)
    
    async def parse(self, code: str, language: str):
        if language != "python":
            raise ValueError(f"Unsupported language: {language}")
        
        tree = await asyncio.to_thread(
            self.parser.parse,
            code.encode("utf-8"),
        )
        return tree

这里使用异步执行避免阻塞事件循环,同时支持超时控制。

2. 函数提取

遍历AST提取所有顶层函数定义:

python 复制代码
def _extract_functions(self, root_node, code):
    functions = []
    
    def traverse(node):
        if node.type == "function_definition":
            func_info = self._parse_function_node(node, code)
            if func_info:
                functions.append(func_info)
            return  # 不进入函数体内部,只提取顶层
        if node.type == "class_definition":
            return  # 类方法由_extract_classes处理
        for child in node.children:
            traverse(child)
    
    traverse(root_node)
    return functions

函数信息解析包括:

  • name字段获取函数名
  • parameters子节点提取参数列表
  • 计算圈复杂度
  • 检测是否递归调用
  • 提取函数内部的调用关系

3. 类信息提取

类似地提取类定义及其方法:

python 复制代码
def _extract_classes(self, root_node):
    classes = []
    
    def traverse(node):
        if node.type == "class_definition":
            class_info = self._parse_class_node(node)
            if class_info:
                classes.append(class_info)
        for child in node.children:
            traverse(child)
    
    traverse(root_node)
    return classes

4. 圈复杂度计算

遍历函数AST,统计控制流语句数量:

python 复制代码
def _calculate_complexity(self, node):
    complexity = 1
    
    def count_branches(n):
        nonlocal complexity
        if n.type in ("if_statement", "for_statement", 
                      "while_statement", "except_clause"):
            complexity += 1
        for child in n.children:
            count_branches(child)
    
    count_branches(node)
    return complexity

5. 调用图构建

从函数信息中提取调用关系:

python 复制代码
def _build_call_graph(self, functions):
    return {
        func.name: func.calls
        for func in functions
        if func.calls
    }

6. 代码指标计算

统计代码行数、注释行数和空行:

python 复制代码
def _calculate_metrics(self, code):
    lines = code.split("\n")
    total_lines = len(lines)
    code_lines = comment_lines = blank_lines = 0
    
    for line in lines:
        stripped = line.strip()
        if not stripped:
            blank_lines += 1
        elif stripped.startswith("#"):
            comment_lines += 1
        else:
            code_lines += 1
    
    return CodeMetrics(
        total_lines=total_lines,
        code_lines=code_lines,
        comment_lines=comment_lines,
        blank_lines=blank_lines
    )

7. 问题生成规则

基于AST分析结果生成issues:

python 复制代码
def _generate_issues(self, functions, code):
    issues = []
    
    for func in functions:
        # 高复杂度检查
        if func.complexity > 10:
            issues.append(Issue(
                severity="warning",
                line=func.start_line,
                description=f"函数'{func.name}'圈复杂度为{func.complexity},建议拆分",
                suggestion="将复杂逻辑拆分为多个职责单一的子函数"
            ))
        
        # 参数过多检查
        if len(func.parameters) > 5:
            issues.append(Issue(
                severity="warning",
                line=func.start_line,
                description=f"函数有{len(func.parameters)}个参数,建议不超过5个",
                suggestion="考虑使用dataclass或dict封装参数"
            ))
        
        # 函数过长检查
        func_length = func.end_line - func.start_line
        if func_length > 50:
            issues.append(Issue(
                severity="info",
                line=func.start_line,
                description=f"函数有{func_length}行,建议不超过50行",
                suggestion="提取子函数以降低函数长度"
            ))
    
    return issues

8. 正则降级策略

当tree-sitter解析失败时,自动降级为正则模式匹配:

python 复制代码
_REGEX_PATTERNS = [
    (re.compile(r"eval\s*\(", re.MULTILINE), 
     "warning", "使用eval()存在代码注入风险"),
    (re.compile(r"except\s*:", re.MULTILINE),
     "warning", "裸except会捕获所有异常"),
    # ... 更多模式
]

def _regex_fallback(self, code):
    issues = []
    for pattern, severity, description in _REGEX_PATTERNS:
        for match in pattern.finditer(code):
            line_num = code[:match.start()].count("\n") + 1
            issues.append(Issue(
                severity=severity,
                line=line_num,
                description=description,
                suggestion=description
            ))
    
    return AnalysisResult(
        success=True,
        failure_category=FailureCategory.PARSE_ERROR,
        error_message="tree-sitter解析失败,降级为正则匹配",
        issues=issues,
        metrics=self._calculate_metrics(code),
        degraded=True
    )

输出格式

Agent输出JSON格式的结构化结果:

json 复制代码
{
  "issues": [
    {
      "severity": "warning",
      "line": 42,
      "description": "函数'process_data'圈复杂度为15,建议拆分",
      "suggestion": "将复杂逻辑拆分为多个职责单一的子函数"
    }
  ],
  "static_findings": "found",
  "degraded": false,
  "metrics": {
    "total_lines": 200,
    "code_lines": 150,
    "comment_lines": 30,
    "blank_lines": 20
  }
}

当没有发现问题时,标记static_findings: none

json 复制代码
{
  "issues": [],
  "static_findings": "none",
  "degraded": false,
  "metrics": {...}
}

故障处理

1. 解析失败降级

当tree-sitter解析失败时,自动切换为正则模式匹配,标记degraded为true。

2. 超时处理

设置超时控制,超时后返回部分结果并标记:

python 复制代码
try:
    async with asyncio.timeout(self.config.agent_timeout):
        tree = await self.parser.parse(code, language)
        # ... 分析逻辑
except TimeoutError:
    return AnalysisResult(
        success=True,
        failure_category=FailureCategory.UPSTREAM_TIMEOUT,
        degraded=True,
        error_message="分析超时,返回部分结果"
    )

3. 不支持的语言

遇到不支持的语言时,自动降级为正则匹配。

使用示例

基本用法

python 复制代码
agent = StaticAnalyzerAgent(config)

# 分析代码
result = await agent.analyze_code("""
def complex_function(x):
    if x > 0:
        for i in range(x):
            if i % 2 == 0:
                print(i)
    return x
""", "python")

# 输出结果
print(f"发现 {len(result.issues)} 个问题")
for issue in result.issues:
    print(f"[{issue.severity}] Line {issue.line}: {issue.description}")

作为Agent运行

python 复制代码
# 通过run方法执行
task = json.dumps({
    "diff": code_content,
    "language": "python",
    "files": ["example.py"]
})

result = await agent.run(task)
output = json.loads(result.output)

性能优化

异步执行

所有耗时操作使用asyncio.to_thread避免阻塞事件循环。

解析器复用

解析器实例在Agent生命周期内复用,避免重复初始化开销。

正则预编译

所有正则模式在模块加载时预编译,提高匹配效率。