elixir-vibe/ex_dna
GitHub: elixir-vibe/ex_dna
基于 Elixir AST 语义分析的代码重复检测器,支持三种克隆类型识别并提供智能重构建议。
Stars: 95 | Forks: 4
# ExDNA 🧬
Elixir 的代码重复检测器,受
[jscpd](https://github.com/kucherenko/jscpd) 启发,但基于 Elixir 原生 AST
而非 token 匹配构建。
由于 ExDNA 理解代码结构——而不仅仅是文本——
`fn(a, b) -> a + b end` 和 `fn(x, y) -> x + y end` 会被识别为
相同的代码。它还会告诉你*如何*修复每个克隆:提取一个函数、
一个宏,或一个 behaviour 回调。
## 功能
- **三种克隆类型** —— 完全相同的副本 (I),重命名变量/更改
字面量 (II),以及通过结构相似性识别的近似克隆 (III)
- **多子句感知** —— 具有相同名称/元数的连续 `def`/`defp` 子句会作为
单个单元进行分析,从而捕获由于单个子句过小而无法标记的重复模式匹配
函数
- **委托模式检测** —— `def foo(x), do: foo(x, [])` 及其后的
`def foo(x, opts)` 被分组为一个单元,可捕获跨模块的重复包装器+主体
对
- **兄弟窗口检测** —— 即使周围代码不同,也能捕获模块间复制的
相邻函数
- **重构建议** —— 提取函数、提取宏、使用 `@callback` 提取
behaviour
- **智能命名** —— 建议根据主导的结构体、调用
或模式(如 `build_changeset`、`contact_step`)命名,而不是
命名为 `extracted_function`
- **管道规范化** —— `x |> f()` 和 `f(x)` 匹配为相同代码
- **字段顺序规范化** —— `%User{name: x, age: y}` 和
`%User{age: y, name: x}` 在 Type-II 模式下匹配
- **Guard 规范化** —— `when is_binary(x)` 和 `when is_atom(x)` 在
Type-II 模式下匹配(涵盖 Kernel guards、`defguard`、库 guards)
- **布尔运算符规范化** —— `&&`/`||`/`!` 与 `and`/`or`/`not` 匹配
- **Sigil 展开** —— `~w(foo bar)a` 与 `[:foo, :bar]` 匹配
- **跨文件分组** —— `actions/ ↔ tools/ (6 clones, 298 nodes)` 而
不是列出每一对
- **`@no_clone` 注解** —— 抑制已知/故意的重复项
- **增量 `Mix.Task.Compiler`** —— 仅重新分析更改的文件
- **LSP 服务器** —— 将克隆诊断信息与
[Expert](https://github.com/elixir-lang/expert) 或 ElixirLS 一起推送到您的编辑器中
- **Credo 集成** —— `DuplicatedCode` 的直接替代品,可复用
Credo 解析的 AST
- **支持 CI** —— 发现克隆时以代码 1 退出,或使用
`--max-clones` 设定克隆预算
- **四种输出格式** —— Credo 风格的控制台、JSON、独立 HTML
以及用于 GitHub Code Scanning 的 [SARIF](https://sarifweb.azurewebsites.net/)
- **快速** —— 并行文件解析,Plausible (465 个文件) 约 1 秒完成,
Ash (554 个文件) 约 6 秒完成完整的 Type-I/II/III 检测
## 安装
```
def deps do
[{:ex_dna, "~> 1.5", only: [:dev, :test], runtime: false}]
end
```
## 使用方法
```
mix ex_dna # scan lib/
mix ex_dna lib/accounts lib/admin # specific paths
mix ex_dna --literal-mode abstract # enable Type-II (renamed vars)
mix ex_dna --min-similarity 0.85 # enable Type-III (near-miss)
mix ex_dna --min-mass 50 # fewer, larger clones
mix ex_dna --max-clones 10 # fail only above budget
mix ex_dna --format json # machine-readable
mix ex_dna --format html # browsable report
mix ex_dna --format sarif # GitHub Code Scanning
```
深入了解特定的克隆:
```
mix ex_dna.explain 3
```
显示完整的反合一分解——公共结构、分歧点
以及带有调用位置的建议提取。
### 编程 API
```
report = ExDNA.analyze("lib/")
report = ExDNA.analyze(["lib/", "test/"])
report = ExDNA.analyze(paths: ["lib/"], min_mass: 20, literal_mode: :abstract)
report.clones #=> [%ExDNA.Detection.Clone{}, ...]
report.stats #=> %{files_analyzed: 42, total_clones: 3, ...}
```
## 配置
选项是分层的:**默认值 → `.ex_dna.exs` → CLI 标志**。
在项目根目录中创建 `.ex_dna.exs`:
```
%{
min_mass: 25,
min_occurrences: 3,
ignore: ["lib/my_app_web/templates/**"],
excluded_macros: [:schema, :pipe_through, :plug],
normalize_pipes: true
}
```
| 选项 | CLI 标志 | 默认值 | 描述 |
|--------|----------|---------|-------------|
| `min_mass` | `--min-mass` | `30` | 片段的最小 AST 节点数 |
| `min_occurrences` | `--min-occurrences` | `2` | 标记为克隆的代码最低出现次数 |
| `min_similarity` | `--min-similarity` | `1.0` | Type-III 的阈值(设置为 < 1.0 以启用) |
| `literal_mode` | `--literal-mode` | `keep` | `keep` = 仅 Type-I,`abstract` = 也包括 Type-II |
| `normalize_pipes` | `--normalize-pipes` | `false` | 将 `x \|> f()` 视为与 `f(x)` 相同 |
| `excluded_macros` | `--exclude-macro` | `[]` | 完全跳过的宏调用 |
| `ignored_attributes` | `--ignore-attribute` | *(见下文)* | 要跳过的模块属性名称 |
| `parse_timeout` | — | `5000` | 每个文件的最大毫秒数(终止挂起的解析) |
| `ignore` | `--ignore` | `[]` | 要排除的全局匹配模式 |
| — | `--max-clones` | — | 克隆预算(仅当超过此值时以代码 1 退出) |
| — | `--format` | `console` | `console`、`json`、`html` 或 `sarif` |
**默认忽略的属性:** Elixir 的所有
[保留属性](https://hexdocs.pm/elixir/Module.html#reserved_attributes/0)
(`moduledoc`、`doc`、`spec`、`type`、`impl`、`behaviour`、`derive` 等)
以及 `no_clone`。
自定义模块属性,如 `@extensions`、`@timeout` 或 `@fields`,**会**
被计算指纹,并在多个模块中以相同值出现时
被报告为重复项。
## 抑制克隆
```
@no_clone true
def validate(params) do
# intentional duplication, won't be flagged
end
```
## max_clones / min_occurrences
* `min_occurrences` → 仅报告出现在 3 个或更多位置的克隆组
* `max_clones` → 如果剩余可报告的克隆组超过 10 个,则返回非零退出码
请注意,`max_clones` 会在诸如 `min_occurrences` 等报告过滤器之后应用,因此
由于 `min_occurrences` 而未报告的克隆不会计入 `max_clones` 预算。
## 增量检测
将 ExDNA 添加为编译器,以便在 `mix compile` 时自动检测:
```
def project do
[compilers: Mix.compilers() ++ [:ex_dna]]
end
```
仅重新分析更改的文件。缓存存储在 `.ex_dna_cache` 中(请将其添加到
`.gitignore`)。
## 编辑器集成
ExDNA 附带一个 LSP 服务器,可在每次保存时内联推送警告。它可以
与您的主要 Elixir LSP 并行运行。
```
mix ex_dna.lsp
```
### Neovim
```
vim.lsp.config('ex_dna', {
cmd = { 'mix', 'ex_dna.lsp' },
root_markers = { 'mix.exs' },
filetypes = { 'elixir' },
})
```
## Credo 集成
ExDNA 附带一个 Credo 检查,用于替代内置的 `DuplicatedCode`,并
提供完整的 Type-I/II/III 检测和重构建议。它复用了 Credo
已解析的 AST——无需重复解析。
作为 Credo 插件使用(推荐)——会自动注册检查并
禁用内置的 `DuplicatedCode`:
```
# .credo.exs
%{
configs: [
%{
name: "default",
plugins: [{ExDNA.Credo, []}]
}
]
}
```
或者直接添加到 `:enabled` 检查列表中:
```
{ExDNA.Credo, []}
```
并禁用内置检查:
```
{Credo.Check.Design.DuplicatedCode, false}
```
所有 ExDNA 选项均可作为检查/插件参数使用。默认情况下,Credo 检查使用与 `mix ex_dna` 相同的路径范围 (`lib/`);如果您希望 Credo 也包含测试文件,请传入 `paths: ["lib/", "test/"]`。
```
{ExDNA.Credo, [
paths: ["lib/", "test/"],
min_mass: 40,
literal_mode: :abstract,
excluded_macros: [:schema, :pipe_through],
normalize_pipes: true,
min_similarity: 0.85
]}
```
## 工作原理
1. **解析** —— 对每个 `.ex`/`.exs` 文件执行 `Code.string_to_quoted/2`(并行,
带有每个文件的超时设置)
2. **规范化** —— 剥离行/列元数据 → 将变量重命名为位置
占位符(`$0`、`$1`) → 可选地抽象字面量 → 可选地
展平管道 → 排序结构体/映射字段
3. **计算指纹** —— 遍历每个超过 `min_mass` 个节点的子树,使用
BLAKE2b 进行哈希;还在模块级兄弟序列上生成滑动窗口,
并为模糊候选修剪计算结构子哈希
4. **检测** —— 按哈希分组 (Type I/II);对子哈希使用倒排索引 +
Jaccard 相似度 + 树编辑距离进行 Type III 检测
5. **过滤** —— 修剪嵌套克隆,保留每个位置的最大匹配项
6. **建议** —— 对每个克隆对进行反合一以计算公共结构,
生成提取函数/宏/behaviour的建议
## 许可证
[MIT](LICENSE)
标签:Elixir, ExDNA, jscpd替代, Linux安全, odt, 代码克隆检测, 代码审查工具, 代码相似度检测, 代码结构分析, 代码规范, 代码重构, 多子句分析, 威胁情报, 开发者工具, 技术债务治理, 抽象语法树, 模式匹配, 自动化payload嵌入, 自动化资产收集, 规范化匹配, 语法树分析, 重复代码分析, 重构建议, 错误基检测, 静态代码分析