m1kapp/fixearly
GitHub: m1kapp/fixearly
一款 100% 本地运行的 JS/TS 静态分析工具,量化代码维护成本并给出等级评分,与 70 个开源项目基准线对比。
Stars: 0 | Forks: 0
# fixearly

**衡量的不是代码本身,而是下次修改它时产生的成本。** JS·TS 专用 · 静态分析 · 100% 本地 · LLM 0 · SSS ~ E 等级。
```
npx fixearly --dir=src --report
```
一行命令,零配置。代码绝不踏出你的计算机一步。`--report` 会生成一份**单页 HTML** —
包含在同级别开源项目中的排名、各维度坐标、**该从哪里开始修复(文件:行号)**,以及可直接粘贴给编码 AI 代理的
**任务指令**。
```
등급: B (73점) — 함수 11,000개, cog15+ 293개, 중복 5.5%, 200줄+ 173개
지난 측정(2026-07-20 09:00) 대비: C 69 → B 73 (+4점)
✓ 리포트 → fixearly-report.html
```
## 69 个知名开源项目的实际得分
用同一把尺子,毫无例外地进行评分。**基于 2026-07-31 · 评分规则 v11 全面重新测算。** 完整内容请见 [着陆页](https://m1kapp.github.io/fixearly)。
| 项目 | 等级 | 得分 | 核心指标 |
|----------|------|------|-----------|
| mitt | **SSS** | 100 | 1个文件 · 75行 · 重复率 0% |
| rxjs | **SSS** | 99 | 188个文件 · 6,536行 · 重复率 6.5% |
| execa | **SSS** | 98 | 103个文件 · 5,726行 · 重复率 0.7% |
| nestjs | **SS** | 95 | 483个文件 · 31,729行 · 重复率 7.3% |
| cypress | **SS** | 93 | 228个文件 · 26,700行 · 重复率 1% |
| express | **SS** | 93 | 6个文件 · 1,137行 · 重复率 0% |
| jotai | **SS** | 93 | 31个文件 · 3,036行 · 重复率 1.8% |
| playwright | **SS** | 93 | 302个文件 · 55,674行 · 重复率 6% |
| mongoose | **D+** | 51 | 长函数 6.1% · 重复率 5.5% |
| nocodb | **D+** | 49 | 长函数 8.0% · 重复率 11.9% |
| vue | **D** | 46 | 长函数 8.1% · 重复率 1.1% |
| react | **D** | 45 | 长函数 8.8% · 重复率 12.6% |
| payload | **D** | 36 | 长函数 15.4% · 重复率 5.3% |
## 衡量什么
### 6 个评分维度(决定等级 · 基于上限的分配分值 · 合计 87)
| 维度 | 分值 | 说明 |
|----|------|------|
| **认知复杂度** | 37 | 遵循 SonarSource `S3776` 规范(已验证与原值一致)。包含 cog15+·cog25+ 比例 + p90 + 前 10 名平均值。 |
| **函数长度** | 23 | 单次需要阅读的代码量。以排除嵌套函数和注释的*自有代码行数*为准,包含超过 40 行(JSX 为 60 行)的比例 + p90 + **前 10 名平均值**。由于拆分文件也不会改变,因此无法作弊。 |
| **重复率** | 9 | 以 token 为单位的复制粘贴密度。在 69 个实际测量结果中,与得分的相关性为 -0.11,因此分值从 16 降至 9 — 对大多数项目为 0 分,仅对少数项目影响显著。 |
| **文件大小** | 8 | 平均行数 + 大型文件占比。**从 27 降级至 8**:如果仅仅是将相同代码拆分为 6 个文件,旧公式就会给出 +27 分。 |
| **顺序 I/O** | 5 | 包含循环内逐行查询 (N+1) 以及排列互不相关的 `await`。由于属于同类缺陷,因此合并为一个维度。在重新测算的 37 个项目中,与现有分数的 **rho=−0.18**,保持独立,并在 **11 个项目 (30%)** 中触发。**v8 仅包含 N+1 但失败了** — 在 37 个项目中仅 1 个触发,且其值未达上限,导致该维度仅触及了一个仓库。用作升级依据的 12 个样本以 backend 为主,但语料库偏向于 library。**少于 3 处不计数**(重试和游标分页确属顺序操作)。宽限值为 3.0/1000 文件,斜率为 0.185(上限在 30.0 处触及 — 即 37 个项目中的 3 个)。设限 5 分:仍存在误报。 |
| **O(n²)** | 5 | 在行循环内访问外部数组。也就是使用 `.find/.some` 遍历外部数组的地方 — 如果是 Map/Set 则为 O(1)。**这是本工具产出 PR 最多的维度,却并未纳入评分**(提交 11 个 · 合并 1 个 · 批准 1 个)。这相当于在不影响等级的情况下,仅仅给出了修复建议。迟迟未加入的原因(“需要人工判断 n 是否很大”)通过三层防线解决:① 仅统计排除测试·前端区域、静态外部作用域 (const-outer)、循环内 I/O、截断 n 处 后的 **candidates** ② **3 处以下免责** ③ 设限 5 分 — 在判定的 6 个 O(n²) PR 中有 4 个被关闭(误报率约 36%)。按 **每 10k 行** 衡量:如果按文件计算,代码行数与 rho=+0.69 相关,无法消除规模影响。改为按行数衡量时,各类别内部的代码行数相关性均变为负数(工具链 −0.14·框架 −0.25·应用 −0.38) — 总体的 +0.64 是类别间的混淆。宽限值为 1.0/10k 行,斜率为 1.0(上限在 6.0 处)。37 个项目中有 17 个扣分,仅 2 个满分。 |
**使用前 10 名平均值的原因**:如果只衡量最差的 1 个值,那么“只修好最差的一个就收手”将成为最佳策略(实测:修复一个 +0.99 分,其后递减至约 0)。前 10 名平均值意味着必须实际减少 10 个问题才能持续降低得分。
### 18 种诊断(不影响等级 · 仅指出文件:行号)
**性能 6 项** — `循环依赖` · `循环内文件读取` · `渲染劫持` · `循环不变索引` ·
`展开累积 O(n²)` · `循环内 new RegExp`
**正确性 Bug 5 项** — `floating promise` · `await in forEach` · `共享引用 fill` · `数字排序(缺少比较函数的 sort)` · `全局正则状态`
**类型卫生 4 项** — `滥用 any` · `as any 断言` · `non-null (!)` · `@ts-ignore`
**基础卫生 3 项** — `空 catch` · `数组 for...in` · `死代码(--dead, knip)`
为何从分数中剔除:对于大多数情况,必须由人工确认 **n 是否真的很大** 才有意义。如果将需要人工判断的维度加入自动评分,小型仓库将面临全面崩溃的风险(1 个文件·2 处位置 = 满级扣分)。因此仅将其作为“待修复清单”列出。
**要成为评分维度,既需要参与评分,也必须能产出 PR。** 仅评分会沦为说教,仅产出 PR 则不会使报告分数变动。
v10 升级 `O(n²) 数组查询` 正是基于这个标准 — 已经产出了 11 个 PR,却一直未影响等级。
唯独将顺序 I/O 作为评分维度例外处理:按 **密度**(每 1000 文件)衡量,且 **少于 3 处不计数**,防止小型仓库因一两个问题而全面崩溃。即便如此,设限 5 分的原因依旧相同 — 仍存在误报。
**新增了耦合度(循环依赖) — 首先作为诊断项。** 其他维度衡量的全是“是否难以阅读”。然而,修改成本等于(理解难度)x(需要同时修改多少处)。第二项因素此前完全缺失。处于循环依赖中的模块无法单独阅读、测试或替换。仅用于类型的 import 并非 runtime 边,因此不计入 —
在 35 个项目的实测中,全部循环的 **37% 属于类型专用**。
暂不将其提升为评分维度。在 35 个项目中与现有分数的 rho=−0.24,大体独立,但在触发最多的
**8 个应用项目中 rho=−0.67**,重合度达一半。样本量单薄(应用 n=8,70 个语料库中仅有 35 个可重新测算),
如果增加第 6 个上限分,总分将从 82 升至 88,导致已发布的分数全部变动。该决定将在全部重新测算后作出。
**复杂度维度与函数长度维度相当重合 — 对此心知肚明。** 69 个项目的原始指标相关性:
`复杂度前10 ↔ 长度前10` **+0.88**,`复杂度最大值 ↔ 长度最大值` +0.78,`cog15+ ↔ 40行+` +0.70。
按扣分标准衡量也为 0.62~0.79。两者在 87 分的上限中占据了 60 分。
然而,如果逐一剔除这些指标并重新排名,`长度 p90`(rho 0.9963) · `长度前10`(0.9913) ·
`复杂度前10`(0.9928) **几乎不会改变语料库的排名。** 尽管如此,我们并不将其移除 — 这些指标的存在不是为了排名,而是为了**防止钻系统空子**。如果只衡量最差的值,“只修好最差的一个就收手”就会变成最佳策略,
而前 10 名平均值的导数永不为 0,意味着必须实际减少 10 个问题才能持续降低分数。静态的语料库排名无法衡量这种效果。我们将这一事实记录于此,并予以保留。
相反,`重复率` 上限分仅为 9,但对排名的影响却位居第二(rho 0.9509)。因为它与其他指标的相关性 ≤0.10,是唯一完全独立的维度。
**我们也评估了 4 种新的备选维度,但最终全部撤回**(2026-07-31,37 处实测 · `tools/axis-probe.mjs`)。
四项均通过了统计标准(独立性·辨识度·规模中立)。被否决的原因在于 **点位的实际依据** 和 **能否转化为机械性 PR**。
- `同步 I/O`(事件循环阻塞) — 找出了 245 处,但在 server 路径中仅有 **24 处 (6/37)**。
其余皆为 vite 依赖优化·vitest 脚手架·nuxt 构建·storybook builder,而 next.js 的 61 处全都是
`compiled/` 目录下的**打包好的 vendor 代码**。在这些场景下,同步 I/O 是正常行为。
- `无限制的 Promise.all 扇出` — 修复该问题需要并发限制器,会引入依赖并涉及设计决策。
- `重复 await` — 检测机制是通过比较调用**文本**,从而将仅值不同的调用判定为重复
(outline 迁移:重新赋值 `tableName` 导致 4 次相同文本)。也无法区分不同分支的调用。
- `JSON.parse(JSON.stringify())` — `structuredClone` 的行为与 JSON 往返不同 (Date·RegExp·Map)。
这不属于行为保持,作为 PR 提交存在风险,且信号微弱 (10/37)。
**模式在重复:统计数据很容易通过,但一旦打开实际点位就会暴露问题。** 与其增加新维度,
不如降低现有 6 个维度的误报率收益更高 — 实际上,通过两次人工验证,我们清除了 O(n²) 中 13% 和顺序 I/O 中 14% 的误报。
**评估升级后遭到搁置的两项**(37 处实测):
- `floating promise` — 触发率 12/37,与得分的相关性 rho=+0.01,统计数据堪称完美。然而,仅需添加 `void foo()` 一词
即可规避检测,而这正是 ESLint `no-floating-promises` **官方推荐的 opt-out 方式**。一旦成为评分维度,测试的就会变成“是否懂得利用 lint 的逃生口” — 被抛弃的错误依然存在,分数却上升了。
- `展开累积` — 触发率 18/37与得分相关性 rho=−0.30。这本身就属于 O(n²),基于上述同样原因被否决:
**严重程度取决于 runtime 的 n 值,而静态分析无法获知该 n 值。** 实证:`ky` 是一个拥有 27 个文件的 library,
其中 4 处全部集中在 `utils/merge.ts` 这一个文件中合并 HTTP 选项(n = 几个键)。按密度算是语料库最高(148/1000 文件),
如果纳入评分将面临满级扣分。密度归一化在小型仓库中会彻底崩溃。
在评估过程中,我们修补了 `展开累积` 检测器的召回率漏洞 — 原本只关注了 `[...acc, x]`,从而漏掉了同属 O(n²) 的
`acc.concat(x)` 和 `Object.assign({}, acc, …)`。此举在 10 个项目中额外找出了 38 处问题(angular 6→13,
typescript 4→10)。诊断准确度提升了,但分数不会改变。
## 为何可信 — 分数必须经得起辩护才算数
- **首先明确局限性** — **JS·TS 专用。** 因为解析器采用的是 TypeScript 编译器 API,所以只能读取 `.ts .tsx .js .jsx .mjs .cjs`。
此外,设计优雅度·测试·文档·安全性·性能·社区等指标 **完全不在衡量范围内**。
- **衡量并公开防作弊机制** — 将相同的代码在函数边界机械拆分为 6 个文件:旧公式 **+27分** → 现在 **+8分**(文件维度设限)。
通过将分析排除机制,阻止了注入大量 5 行文件的行为。**但并不声称“完全无法作弊”** — 在此记录下残留的漏洞(文件维度 8 分)是更为诚实的做法。
- **通过 69 个开源项目进行校准** — 宽限值并非随意的阈值,而是语料库的中位数,斜率设定为在 p90 处触及上限。(中位数为 81)
**我们对该规则进行了自我审查。** 7 个扣分项中有 6 个在 p90 ±5% 的范围内触及上限,唯独 `平均文件长度` 在 145 处触及上限
(而 p90 为 280)。这是在将上限从 14→5 缩减时,未修改除数导致的遗漏,使得触及点被拖至 190→145,69 个项目中有 29 个
贴在上限上,导致无法区分 150 行和 2,000 行的文件。v11 中将斜率调整为 0.03125 — 贴在上限上的项目由 29 个减至 8 个。
- **按代码行数划分体量** — 若按文件数划分,把文件拆分得更碎的一方只会虚增体量(实测:行数/文件存在 34~2,008 之间相差 59 倍的情况)。
- **公开不同类别的偏差** — library 87 · framework 85 · toolchain 84 · **应用 76 · 引擎/编译器 66.**
引擎放弃了抽象,而应用中通常按页面划分的函数会更长。这不是偏差,而是该维度的固有属性。因此,报告会同时展示**同类型基准线**。
- **在评分规则中固化版本号** — 更改宽限值或斜率将导致 `v7 → v8`。在历史比较中,若规则不同则会警告“分数比较无效”。
- **阴性对照组 · 自我验证闭环 · 模式目录** — [LOOP.md](./LOOP.md) · [PATTERNS.md](./PATTERNS.md)
- **实战验证** — 不仅仅是给出评级,而是精准指出需要实际修复的问题。向开源项目提交的 PR 记录:[IMPACT.md](./IMPACT.md)
## 使用方法
```
# 单页 리포트(推荐) — 我的位置 + 待修复列表 + AI 지시문
npx fixearly --dir=src --report
# 仅分数
npx fixearly --dir=src
# 优先修复文件排名(复杂度 × git churn)
npx fixearly --dir=src --hotspots
# 新增 데드코드 轴(knip 内置)
npx fixearly --dir=src --dead
# 在 모노레포 中排除非产出 패키지(需保留依据)
npx fixearly --dir=packages --exclude=packages/devtools,packages/examples
# README 배지
npx fixearly --dir=src --badge
```
每次测量时都会在 `.fixearly-history.json` 中保存一个快照,报告中会显示 **相较于上次** 的变化。
分数代表绝对位置,变化量则是你所付出的努力 — 这是你与自身历史的比较,所以仅靠修改指标是无法撼动分数的。
结果也会作为 `fixearly.json` 存放在 `--out` 目录下(用于 CI 追踪趋势)。
只有在使用 `--kit` 时才会沿用旧名称 `kit-stats.json` — 旨在兼容现有的消费者。
## 等级
| 等级 | 得分 | | 等级 | 得分 |
|------|------|---|------|------|
| **SSS** | ≥ 97 | | **C+** | ≥ 64 |
| **SS** | ≥ 93 | | **C** | ≥ 55 |
| **S** | ≥ 90 | | **D+** | ≥ 47 |
| **A+** | ≥ 86 | | **D** | ≥ 35 |
| **A** | ≥ 80 | | **E+** | ≥ 21 |
| **B+** | ≥ 76 | | **E** | ≥ 0 |
| **B** | ≥ 70 | | | |
底层等级的评分标准设定得较为宽松 — 大型服务和应用在结构指标上本就处于劣势。**实际上 E 级仅限于那些被荒废的项目。**
## 许可证
MIT
标签:CMS安全, JavaScript, MITM代理, SOC Prime, TypeScript, 云安全监控, 代码度量, 后端开发, 多模态安全, 安全专业人员, 安全插件, 开发工具, 技术债务, 数据可视化, 静态分析