July 1, 2025
By: Kevin

将代码视为犯罪现场:一种使用 Babashka 的法证分析方法

  1. 核心理念:代码法证分析
  2. 工具包:Babashka 脚本
    1. 1. Git 日志分析 (git.clj)
    2. 2. 多语言圈复杂度分析
    3. 3. 自动化报告与示例
    4. 4. 配置:量身定制分析
      1. 作者别名合并 (:author-name-alias)
      2. 文件与提交排除 (:file-config -> :exclusions)
      3. 文档文件定义 (:file-config -> :docs)
  3. 扩展工具包
  4. 结论

Adam Tornhill 的《Your Code as a Crime Scene》一书提出了一个引人入胜的观点:将代码库视为一个考古遗址或犯罪现场。通过运用法证技术,我们可以揭示软件历史中隐藏的真相,识别出活动的热点区域,并最终“逮捕”那些导致缺陷、瓶颈和不良设计的元凶。

本文将探讨如何使用 Babashka(一个本地 Clojure 脚本环境)来构建一个轻量级但功能强大的代码法证分析工具包。我们的目标是创建一套能够分析任何代码库(包括 Clojure、ClojureScript、JavaScript、C#、Java 等)的工具,并为其演进和健康状况提供有价值的见解。

核心理念:代码法证分析

传统的代码分析通常侧重于代码的静态快照。我们进行语法检查、风格审查和复杂度测量。这些都是有价值的实践,但它们忽略了一个关键维度:时间

相比之下,法证分析方法利用版本控制历史(我们将使用 Git)来理解代码随时间的变化。这使我们能够回答以下问题:

  • “热点”在哪里? 哪些文件被修改得最频繁?这些通常是系统中最复杂、最容易出错的部分。
  • 主要贡献者是谁? 团队的开发焦点是如何随时间变化的?
  • 我们代码的真正复杂性是什么? 不仅仅是单个函数的复杂性,还包括变更之间的相互关联性。
  • 我们的代码是如何“野蛮生长”的? 我们是在不断累积技术债,还是在积极地偿还它?

通过回答这些问题,我们可以就重构工作的重点、如何引导新团队成员以及如何改进开发流程做出更明智的决策。

工具包:Babashka 脚本

我们的工具包是位于项目 @bb 目录中的一组 Babashka 脚本。每个脚本都设计用于执行特定的分析,并最终汇总到一份综合报告中。

1. Git 日志分析 (git.clj)

这是我们工具包的基石。它深入挖掘 git log,以提取关于每次提交的丰富信息。

主要功能:

  • 全面的提交数据: 解析所有分支的每次提交,提取作者、日期、提交信息以及详细的文件变更统计(增加和删除的行数)。
  • 作者别名处理: 现实世界的 Git 日志通常很混乱,开发者会使用不同的名称和电子邮件。我们使用一个 config.edn 文件将各种别名映��到一个规范的名称,确保贡献跟踪的准确性。
  • 文件过滤: 并非所有文件都同等重要。我们在分析中排除了自动生成的文件、文档和其他非代码资产,以避免噪音。
  • 时间维度分析: 该脚本可以按日期范围过滤提交,使我们能够生成特定时期(如每日、每周、每月)的报告。

分析示例:

  • 贡献排行榜: 我们可以生成表格,显示每位开发者贡献的提交次数和代码行数。
  • 热点识别: 通过跟踪文件修改频率,我们可以精确定位代码库中最不稳定的部分。这些是重构和增加测试覆盖率的首选目标。

2. 多语言圈复杂度分析

虽然 Git 历史告诉我们去哪里看,但我们仍然需要理解代码本身的内在复杂性。我们的工具包现在支持对 Clojure/ClojureScript、JavaScript 和 C# 的圈复杂度分析。

主要功能:

  • Clojure/ClojureScript (cc.clj): 使用 rewrite-clj 将源代码解析为抽象语法树(AST)并计算复杂度。
  • JavaScript (js_complexity.clj): 集成了 cyclomatic-complexity npm 包来分析 .js 文件。
  • C# (cs_complexity.clj): 使用 Metrics.NET.Console .NET 工具来分析 .csproj 项目。
  • 统一报告: 主脚本 git.clj 负责协调分析,并将结果汇总到最终报告中。

分析示例:

圈复杂度分数高的函数更难理解、测试和维护。通过在我们整个多语言代码库中识别这些函数,我们可以优先进行简化。

3. 自动化报告与示例

最后一步是生成一份综合报告。我们的 git.clj 脚本负责整个分析流程,并每天生成一个 git-xray-{date}.org 文件。这份报告为我们项目的健康状况和演进提供了一个持续的、数据驱动的概览。

以下是一份在一个包含 Clojure、C# 和 JavaScript 的真实项目中生成的报告节选:

报告示例

`#+SETUPFILE: https://gitee.com/zhaoyuKevin/org-theme/raw/master/org/theme-readtheorg.setup`

`* 项目代码分析 <2025-07-08>`

`** 项目中最复杂的函数, 前20个`

函数的圈复杂度 [[https://baike.baidu.com/item/圈复杂度/828737][~cyclomatic-complexity~]] 是衡量函数代码复杂度的方式,一般不要超过10!
| Func                                                                                                                       | Cc  |
| ---------------------------------------------------------------------------------------------------------------------+---- |     |
| ~src/clj-backend/src/clj/clj_backend/proj/recipe/service.clj:sync-update-recipe~                                           | 25  |
| ~src/clj-scada/src/clj/clj_scada/modules/file/service.clj:validate-field~                                                  | 20  |
| ~src/clj-backend/src/clj/clj_backend/proj/recipe/service.clj:get-phases-updated-msg~                                       | 17  |
| ...                                                                                                                        | ... |


`** 项目中的大文件(前20个)`
| File                                                                                                                | Size  |
|---------------------------------------------------------------------------------------------------------------------+-------|
| src/clj-scada/clj-scada.md                                                                                          | 28970 |
| doc/design/design.org                                                                                               | 12149 |
| .../20250106133925-update-all-stash-view.up.sql                                                                     | 9446  |
| ...                                                                                                                 | ...   |

`** 热点文件`
`*** 全项目热点`
| File                                                                                                                | Change Times |
|---------------------------------------------------------------------------------------------------------------------+--------------|
| doc/design/design.org                                                                                               | 239          |
| .../migrations/20231214063330-init.up.sql                                                                            | 175          |
| .../src/clj/clj_backend/proj/recipe/service.clj                                                                     | 167          |
| ...                                                                                                                 | ...          |

`** 提交次数`
`*** 
| Author           | Commit Count |
|------------------+--------------|
| LiuMeng          | 1            |
| ...              | ...          |
| LiMengjie        | 2175         |

从这份报告中,我们可以迅速得出结论:

  • recipe/service.clj 中的 sync-update-recipe 函数复杂度高达25,是进行代码简化的首要目标。
  • design.org 文件被修改了239次,是项目中最活跃的“热点”文件,说明设计文档的维护非常频繁。
  • 开发者 LiMengjieLiZhaoyu 是项目的主要贡献者。

4. 配置:量身定制分析

为了让分析结果更有意义,我们需要告诉工具如何处理项目中的特殊情况。所有配置都在 bb/config.edn 文件中完成。

作者别名合并 (:author-name-alias)

问题:在 Git 中,同一个人可能使用不同的名字和邮箱进行提交(例如 "Liza", "liza", "liza@example.com")。这会导致贡献度统计被分散。

解决方案:通过 :author-name-alias 映射,我们可以将这些不同的别名统一到一个规范的名称下。

;; bb/config.edn
{:author-name-alias
 {"Liza" "Liza Minnelli"
  "liza" "Liza Minnelli"}}

文件与提交排除 (:file-config -> :exclusions)

问题:自动生成的文件(如 package-lock.json)、庞大的第三方库或某些特殊的提交(如一次性格式化整个项目)会严重干扰分析结果的准确性。

解决方案

  • :files: 一个包含字符串的集合。任何文件路径如果包含其中任意一个字符串,就会被排除。这对于忽略 node_modules.lock 文件或自动生成的 svg 组件非常有用。
  • :commits: 一个包含提交哈希值(commit hash)前几位字符的集合。这对于忽略那些不代表真实开发工作的、大规模的自动化重构提交非常有用。
;; bb/config.edn
{:file-config
 {:exclusions
  {:files   #{"package-lock.json" "yarn.lock" "node_modules/"}
   :commits #{"6660858" "53d2b46"}}}}

文档文件定义 (:file-config -> :docs)

问题:我们希望区分对源代码的修改和对文档的修改,以便更准确地评估开发工作。

解决方案:docs 是一个包含文件扩展名的集合。任何以此扩展名结尾的文件都会被视为文档,其修改行数将被分开统计。

;; bb/config.edn
{:file-config
 {:docs #{".org" ".md" ".txt"}}}

扩展工具包

使用 Babashka 的美妙之处在于其可扩展性。我们可以轻松地为我们的法证工具包添加新工具。例如:

  • Java 复杂度分析: 我们可以集成像 pmdcheckstyle 这样的外部工具来分析 Java 代码的复杂性,并将结果整合到我们的报告中。
  • 依赖分析: 可以编写一个脚本来分析 project.cljpackage.json.csproj 文件,以跟踪依赖项随时间的变化。
  • 代码流失率 vs. 复杂度: 我们可以将代码流失率(文件变更的频率)与其圈复杂度关联起来,以发现真正高风险的区域。

结论

通过将我们的代码视为犯罪现场并应用法证技术,我们可以超越表面层次的度量,从而更深入地理解们的软件。Babashka 轻量级、可脚本化的特性使其成为构建定制化代码法证工具包的理想选择。

@bb 目录中的脚本是一个起点。它们展示了这种方法的强大之处,并为进一步探索提供了坚实的基础。我们鼓励去调整、扩展它们,并用它们来揭示你自己代码库中隐藏的故事。

Tags: clojure babashka