跳到主要内容

RepoProfile 与多语言实体抽取(支撑层)

前两章的造 bug 和验证,都站在两个支柱上:一个仓库怎么「装/测/解析日志」(RepoProfile),源码怎么切成可改的「函数/类」(实体抽取)。本章讲清这两根柱子,它们也是 SWE-smith 能横跨 11 种语言的关键。

1. 它要解决的小问题

不同语言、不同仓库的「装环境、跑测试、读测试结果」千差万别:Python 用 conda+pytest,Go 用 go test,日志格式各不相同。流水线如果到处写 if language == ... 会失控。

SWE-smith 的做法是把每个仓库的所有差异收进一个类 RepoProfile,流水线只跟这个统一接口打交道。

2. RepoProfile:一个仓库的「档案」

RepoProfile(swesmith/profiles/base.py:80)是个 @dataclass 抽象基类,封装了一个仓库在流水线里需要的一切:

关切接口说明
身份owner/repo/commit锁定到具体 commit;repo_name = owner__repo.<commit前8位>
镜像image_namebuild_imagepull_imageDocker 执行环境
镜像仓库create_mirrorclonemirror_name在 swesmith 组织下建冻结快照
抽实体extract_entities走文件树,按扩展名分派给适配器
跑测试test_cmdget_test_cmdget_test_files测试命令 + 相关测试定位
读日志log_parser(抽象)把测试输出解析成 {测试名: 状态}

语言层(如 PythonProfile,profiles/python.py)给出该语言的默认 test_cmd(conda+pytest)、exts([".py"])、log_parser(正则扫 pytest 输出)、build_image(conda env 装法)。每个具体仓库再继承语言层、只填 owner/repo/commit:

# 示意,改编自 profiles/python.py —— 一个具体仓库档案极简
@dataclass
class Addict75284f95(PythonProfile):
owner: str = "mewwts"
repo: str = "addict"
commit: str = "75284f9593dfb929cadd900aff9e35e7c7aec54b"

仓库 addict 的全部「个性」就这三行——其余全继承自 PythonProfile。这就是为什么仓库覆盖能扩到几百个。

3. 注册表 + 单例:profile 怎么被找到

所有 profile 子类被注册进全局 registry(profiles/base.py:680Registry,实例在 :737)。注册时按 repo_namemirror_name 双键登记(register_profile,:687)。流水线各处通过 registry.get(repo)registry.get_from_inst(instance)(:714,从实例 id 反查)拿到 profile。

为什么用单例? SingletonMeta(profiles/base.py:70)让每个 profile 类全局只有一个实例。这样跨模块共享缓存(分支列表、测试路径、镜像是否存在)才有意义,也避免给几百个 profile 各分配一把锁。clone/缓存初始化用 threading.Lock 保护并发(:123_lock,_get_cached_test_paths:258with self._lock)。

4. 实体抽取:把源码切成 CodeEntity

extract_entities(profiles/base.py:438)走整个仓库文件树,跳过测试文件(_is_test_path,:549),按扩展名把每个文件交给对应的语言适配器:get_entities_from_file[ext](分派表在 bug_gen/adapters/__init__.py:13,覆盖 .py .js .ts .go .java .rb .rs .c .cpp .cs .php)。

两种后端,殊途同归:

  • Python 用标准库 ast(adapters/python.py):get_entities_from_file_py(:152)ast.walk 找出所有 FunctionDef/ClassDef,_build_entity(:171)算出起止行、缩进、并 dedent 源码,包成 PythonEntity
  • 其它语言用 tree-sitter(如 adapters/golang.pyadapters/rust.py):同样产出统一的 CodeEntity,只是解析器换成 tree-sitter 语法树。

4.1 属性标签:bug 匹配的依据

每个 entity 在 _analyze_properties 里被打上 CodeProperty 标签(Python 版 adapters/python.py:10),这是 procedural 变异器 can_change 的判断依据(见 01 章 §4.2)。逻辑就是「AST 里有没有某种节点」:

# 示意,改编自 adapters/python.py —— 打属性标签
if any(isinstance(n, (ast.For, ast.While)) for n in ast.walk(node)):
self._tags.add(CodeProperty.HAS_LOOP)
if any(isinstance(n, ast.BinOp) for n in ast.walk(node)):
self._tags.add(CodeProperty.HAS_BINARY_OP)

CodeProperty 枚举的全集在 constants.py:47(控制流 / 操作 / 实体类型三大类)。元类 CodeEntityMeta(constants.py:80)把每个枚举值变成 entity 上的只读属性(entity.has_loop 即查 _tags),让外部读起来像普通字段。

4.2 complexity:过滤太简单的目标

PythonEntity.complexity(adapters/python.py:72)是简化版圈复杂度:基础 1,每个 if/for/while+1、每个布尔运算/异常处理/比较运算按数量加。变异器用 min_complexity(默认 3)挡掉过于简单的函数——太简单的函数改了也造不出有意义的 bug。

4.3 stub:LLM-rewrite 的「挖空」靠它

CodeEntity.stub(adapters/python.py:115)用 ast.NodeTransformer 删掉函数体、只留签名+docstring+TODO: Implement this function 占位(常量 TODO_REWRITE,constants.py:37)。这正是 01 章 LLM-rewrite 策略「挖空再让模型重写」的那一步。

5. 建环境:mirror + Docker

造 bug 前要先有可复现环境,两步:

  1. create_mirror(profiles/base.py:340):clone 源仓库 → checkout 到指定 commit → 抹掉 .git 重新 init(冻结成一个纯净快照,去掉历史和 CI 配置)→ push 到 swesmith/<repo_name>。所有后续 clone 都从这个 mirror 来,不再碰原仓库。
  2. build_image(语言层实现,如 profiles/python.pybuild_image):生成 Dockerfile + setup 脚本,在镜像里 clone mirror、建 conda 环境、装依赖。_prepare_dockerfile(base.py:288)还会自动给每个 RUN 注入 SSH mount,方便装私有依赖。

6. 本章代码地图

主题文件关键符号
profile 基类swesmith/profiles/base.pyRepoProfileextract_entitiesget_test_cmdcreate_mirror
单例 + 注册表swesmith/profiles/base.pySingletonMetaRegistryregistryget_from_inst
Python 语言层swesmith/profiles/python.pyPythonProfilelog_parserbuild_imageAddict75284f95
实体数据结构swesmith/constants.pyCodeEntityCodePropertyCodeEntityMeta
Python 实体抽取swesmith/bug_gen/adapters/python.pyget_entities_from_file_py_build_entityPythonEntitystub
适配器分派表swesmith/bug_gen/adapters/__init__.pyget_entities_from_fileSUPPORTED_EXTS