chore: 新增 backend-code-lint 变更计划,更新 .gitignore
This commit is contained in:
130
openspec/changes/backend-code-lint/design.md
Normal file
130
openspec/changes/backend-code-lint/design.md
Normal file
@@ -0,0 +1,130 @@
|
||||
## Context
|
||||
|
||||
后端项目使用 Go 1.26.2 开发,已集成 `golangci-lint v1.64.8` 作为 tool dependency(`go.mod` 中声明),并通过 `make backend-lint` 调用。但当前**没有 `.golangci.yml` 配置文件**,lint 以默认配置运行(仅启用极少量 linter),且存在 embedfs 模块加载错误导致 lint 实际无法执行。
|
||||
|
||||
代码审计发现以下存量问题:
|
||||
- 8 处 `err == sentinel` 应使用 `errors.Is()`
|
||||
- 7 处 `_ = json.Marshal(...)` 忽略错误返回值
|
||||
- 13 处 `zap.String("error", err.Error())` 应使用 `zap.Error(err)`
|
||||
- 2 处 `fmt.Fprintf(os.Stderr, ...)` 应走 logger
|
||||
- 1 处 `_ = io.ReadAll(...)` 忽略错误
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 配置 golangci-lint,将 README 中的编码规范转化为机器可检查的硬约束
|
||||
- 引入 lefthook 实现 pre-commit 自动 lint,AI 提交代码时自动拦截违规
|
||||
- 修复存量代码中的规范违规
|
||||
- 解决 embedfs 导致 lint 无法运行的阻塞问题
|
||||
|
||||
**Non-Goals:**
|
||||
- 不引入自定义 linter 插件(开发成本过高)
|
||||
- 不配置 CI pipeline lint 门禁(仅本地)
|
||||
- 不改变现有错误响应策略(允许 err.Error() 暴露在 HTTP 响应中)
|
||||
- 不引入 funlen linter(gocyclo 已控制复杂度,funlen 误报率高)
|
||||
- 不引入 unparam linter(项目 interface 密集,unparam 误报率高)
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: Linter 选型 — 13 个 linter 分四层
|
||||
|
||||
```
|
||||
🔒 硬约束层(项目规范 → 机器检查)
|
||||
├── forbidigo 禁止 fmt.Print*/log.*/zap.L()/zap.S()
|
||||
├── errorlint 强制 errors.Is/As,禁止 err == 比较
|
||||
└── errcheck 禁止忽略错误返回值(check-blank: true)
|
||||
|
||||
🏗️ 质量基线层
|
||||
├── staticcheck Go 团队官方综合静态分析
|
||||
├── revive golint 替代品,精选 8 条规则
|
||||
│ (exported, var-naming, indent-error-flow, error-strings,
|
||||
│ error-return, blank-imports, context-as-argument,
|
||||
│ unexported-return)
|
||||
├── gocritic 100+ 代码质量规则
|
||||
└── gosec 安全检查
|
||||
|
||||
🛡️ 资源安全层
|
||||
├── bodyclose HTTP 响应 Body 关闭检查
|
||||
├── noctx HTTP 请求必须携带 context
|
||||
└── nilerr 检查 if err != nil { return nil } 遗漏
|
||||
|
||||
📐 格式层(可自动修复)
|
||||
├── gofumpt gofmt 的严格版
|
||||
└── goimports import 排序(local-prefixes: nex/backend)
|
||||
|
||||
📊 复杂度
|
||||
└── gocyclo 正式代码 ≤10 / 测试代码 ≤20
|
||||
```
|
||||
|
||||
**替代方案**: 使用 revive 自定义规则检查 zap.String("error", err.Error()) → 决定暂不实施,先修复存量 13 处 + README 约定 + code review,如果后续频繁违规再投入开发自定义规则。
|
||||
|
||||
### D2: forbidigo 配置策略
|
||||
|
||||
禁止列表:
|
||||
- `fmt\.Print*` — 必须使用 zap logger
|
||||
- `fmt\.Fprint*(os\.(Stdout|Stderr)` — 必须使用 zap logger
|
||||
- `log\.(Print|Fatal|Panic|Printf)*` — 必须使用 zap logger
|
||||
- `zap\.L()` — 必须通过 DI 注入 *zap.Logger
|
||||
- `zap\.S()` — 不使用 Sugar logger
|
||||
|
||||
**不禁止**:
|
||||
- `zap.Logger.Fatal()` — main() 中的 Fatal 是合理的启动终止模式,且 forbidigo 按函数匹配不会拦截 zap.Logger 的方法调用
|
||||
- `fmt.Sprintf` — 格式化字符串是合法用途
|
||||
- `fmt.Errorf` — 创建带格式的错误是标准用法
|
||||
|
||||
### D3: 测试代码 vs 正式代码差异化规则
|
||||
|
||||
| 规则 | 正式代码 | 测试代码 |
|
||||
|---|---|---|
|
||||
| forbidigo | 全部启用 | 放宽(fmt.Sprintf 等合理) |
|
||||
| errcheck | check-blank: true | 放宽 check-blank |
|
||||
| revive (exported) | 启用 | 排除 |
|
||||
| gosec | 启用 | 排除 G101/G401/G501 |
|
||||
| gocyclo | ≤10 | ≤20 |
|
||||
|
||||
通过 `issues.exclude-rules` 按路径 `*_test.go` 和 `tests/` 配置排除。
|
||||
|
||||
### D4: 生成代码排除
|
||||
|
||||
`tests/mocks/` 目录下的 8 个 mock 文件由 mockgen 生成,需要排除。使用 `issues.exclude-dirs` 配合 `issues.exclude-generated: true`(golangci-lint 自动检测 `Code generated by` 标记)。
|
||||
|
||||
### D5: embedfs 阻塞问题修复
|
||||
|
||||
embedfs 模块使用 `//go:embed assets/*` 和 `//go:embed frontend-dist/*`,但这些目录在未构建时不存在,导致 golangci-lint 无法加载该模块。
|
||||
|
||||
**方案**: 在 `embedfs/assets/` 和 `embedfs/frontend-dist/` 中添加 `.gitkeep` 文件,使 `go:embed` 指令能匹配到内容。这是最小侵入的解决方案,不影响正常构建流程(desktop-build 会覆盖这些目录)。
|
||||
|
||||
### D6: lefthook pre-commit 配置
|
||||
|
||||
```yaml
|
||||
# lefthook.yml
|
||||
pre-commit:
|
||||
commands:
|
||||
backend-lint:
|
||||
glob: "backend/**/*.go"
|
||||
run: cd backend && go tool golangci-lint run --new-from-rev HEAD {staged_files}
|
||||
```
|
||||
|
||||
关键设计:
|
||||
- 只检查 staged 文件(`--new-from-rev HEAD`),速度快
|
||||
- 只在 Go 文件变更时触发(`glob: "backend/**/*.go"`)
|
||||
- AI commit 时自动触发,lint 不过则 commit 被拒绝,形成自动反馈循环
|
||||
|
||||
### D7: 存量修复策略
|
||||
|
||||
按优先级分批修复:
|
||||
1. **P0 — embedfs 阻塞修复**:创建 .gitkeep 文件
|
||||
2. **P1 — err == sentinel → errors.Is()**:8 处,分布在 handler 和 client
|
||||
3. **P2 — 忽略错误返回值**:7 处 json.Marshal + 1 处 io.ReadAll + 2 处 stats Record(加 nolint 注释)
|
||||
4. **P3 — zap.String("error", err.Error()) → zap.Error(err)**:13 处
|
||||
5. **P4 — fmt.Fprintf(os.Stderr) → logger**:2 处,在 cmd/desktop/
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**[lint 速度影响 commit 体验]** → lefthook 只检查 staged Go 文件,增量检查通常 <5 秒,可接受。如果仍慢,可加 `--timeout` 限制。
|
||||
|
||||
**[lefthook 是新依赖]** → lefthook 是开发工具依赖,不影响生产代码。作为单二进制分发,安装简单(`go install` 或从 GitHub release 下载)。首次需要开发者手动安装。
|
||||
|
||||
**[存量修复可能引入新 bug]** → 所有修复都是机械性替换(errors.Is、zap.Error 等),不改变逻辑。修复后运行 `make test` 确认无回归。
|
||||
|
||||
**[forbidigo 可能误拦合理用法]** → 通过仔细配置允许列表(允许 fmt.Sprintf、fmt.Errorf),并在发现误报时调整规则。
|
||||
Reference in New Issue
Block a user