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嵌入, 自动化资产收集, 规范化匹配, 语法树分析, 重复代码分析, 重构建议, 错误基检测, 静态代码分析