将代码视为犯罪现场:一种使用 Babashka 的法证分析方法
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-complexitynpm 包来分析.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次,是项目中最活跃的“热点”文件,说明设计文档的维护非常频繁。- 开发者
LiMengjie和LiZhaoyu是项目的主要贡献者。
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 复杂度分析: 我们可以集成像
pmd或checkstyle这样的外部工具来分析 Java 代码的复杂性,并将结果整合到我们的报告中。 - 依赖分析: 可以编写一个脚本来分析
project.clj、package.json或.csproj文件,以跟踪依赖项随时间的变化。 - 代码流失率 vs. 复杂度: 我们可以将代码流失率(文件变更的频率)与其圈复杂度关联起来,以发现真正高风险的区域。
结论
通过将我们的代码视为犯罪现场并应用法证技术,我们可以超越表面层次的度量,从而更深入地理解们的软件。Babashka 轻量级、可脚本化的特性使其成为构建定制化代码法证工具包的理想选择。
@bb 目录中的脚本是一个起点。它们展示了这种方法的强大之处,并为进一步探索提供了坚实的基础。我们鼓励去调整、扩展它们,并用它们来揭示你自己代码库中隐藏的故事。