chore: 新增 backend-code-lint 变更计划,更新 .gitignore
This commit is contained in:
174
openspec/changes/backend-code-lint/specs/code-lint/spec.md
Normal file
174
openspec/changes/backend-code-lint/specs/code-lint/spec.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Code Lint
|
||||
|
||||
## Purpose
|
||||
|
||||
定义后端 Go 代码静态分析规则,将编码规范从人工约定升级为机器可检查的硬约束,通过 golangci-lint 在开发和提交阶段自动拦截违规代码。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: golangci-lint 配置
|
||||
|
||||
系统 SHALL 通过 `.golangci.yml` 配置 golangci-lint,启用 13 个 linter。
|
||||
|
||||
#### Scenario: 配置文件位置
|
||||
|
||||
- **WHEN** 配置 lint 规则
|
||||
- **THEN** 配置文件 SHALL 位于 `backend/.golangci.yml`
|
||||
|
||||
#### Scenario: 启用的 linter 列表
|
||||
|
||||
- **WHEN** 运行 golangci-lint
|
||||
- **THEN** SHALL 启用以下 linter:forbidigo、errorlint、errcheck、staticcheck、revive、gocritic、gosec、bodyclose、noctx、nilerr、gofumpt、goimports、gocyclo
|
||||
|
||||
### Requirement: forbidigo 日志输出约束
|
||||
|
||||
系统 SHALL 通过 forbidigo 禁止在正式代码中使用直接输出函数。
|
||||
|
||||
#### Scenario: 禁止 fmt.Print 系列
|
||||
|
||||
- **WHEN** 正式代码中调用 fmt.Print、fmt.Println、fmt.Printf
|
||||
- **THEN** lint SHALL 报错,提示使用 zap logger
|
||||
|
||||
#### Scenario: 禁止 fmt.Fprint 到 Stdout/Stderr
|
||||
|
||||
- **WHEN** 正式代码中调用 fmt.Fprintf(os.Stdout, ...) 或 fmt.Fprintf(os.Stderr, ...)
|
||||
- **THEN** lint SHALL 报错,提示使用 zap logger
|
||||
|
||||
#### Scenario: 禁止标准库 log
|
||||
|
||||
- **WHEN** 正式代码中调用 log.Print、log.Fatal、log.Panic、log.Printf 等
|
||||
- **THEN** lint SHALL 报错,提示使用 zap logger
|
||||
|
||||
#### Scenario: 禁止 zap.L() 全局 logger
|
||||
|
||||
- **WHEN** 正式代码中调用 zap.L()
|
||||
- **THEN** lint SHALL 报错,提示通过 DI 注入 *zap.Logger
|
||||
|
||||
#### Scenario: 禁止 zap.S() Sugar logger
|
||||
|
||||
- **WHEN** 代码中调用 zap.S()
|
||||
- **THEN** lint SHALL 报错,不使用 Sugar logger
|
||||
|
||||
#### Scenario: 允许 fmt.Sprintf 和 fmt.Errorf
|
||||
|
||||
- **WHEN** 代码中使用 fmt.Sprintf 或 fmt.Errorf
|
||||
- **THEN** lint SHALL NOT 报错
|
||||
|
||||
#### Scenario: 测试代码放宽
|
||||
|
||||
- **WHEN** 测试文件(*_test.go)或 tests/ 目录中使用 fmt.Print 系列
|
||||
- **THEN** forbidigo SHALL NOT 报错
|
||||
|
||||
### Requirement: errorlint 错误比较约束
|
||||
|
||||
系统 SHALL 通过 errorlint 强制使用类型安全的错误比较方式。
|
||||
|
||||
#### Scenario: 禁止 err == sentinel 比较
|
||||
|
||||
- **WHEN** 代码中使用 `err == someError` 直接比较错误
|
||||
- **THEN** lint SHALL 报错,要求使用 errors.Is()
|
||||
|
||||
#### Scenario: 禁止直接类型断言
|
||||
|
||||
- **WHEN** 代码中使用 `err.(SomeType)` 直接类型断言
|
||||
- **THEN** lint SHALL 报错,要求使用 errors.As()
|
||||
|
||||
### Requirement: errcheck 错误返回值检查
|
||||
|
||||
系统 SHALL 通过 errcheck 禁止忽略函数返回的错误。
|
||||
|
||||
#### Scenario: 启用 check-blank
|
||||
|
||||
- **WHEN** 代码中使用 `_ = someFuncReturnsError()`
|
||||
- **THEN** lint SHALL 报错(除非排除列表中的函数)
|
||||
|
||||
#### Scenario: 启用 check-type-assertions
|
||||
|
||||
- **WHEN** 代码中使用未检查的类型断言 `v := x.(Type)`
|
||||
- **THEN** lint SHALL 报错
|
||||
|
||||
#### Scenario: 排除 fmt.Fprintf
|
||||
|
||||
- **WHEN** 代码中忽略 fmt.Fprintf 的返回值
|
||||
- **THEN** errcheck SHALL NOT 报错(io.Writer 场景合理)
|
||||
|
||||
#### Scenario: 测试代码放宽
|
||||
|
||||
- **WHEN** 测试文件中忽略错误返回值
|
||||
- **THEN** errcheck 的 check-blank SHALL 放宽
|
||||
|
||||
### Requirement: revive 代码风格规则
|
||||
|
||||
系统 SHALL 通过 revive 启用精选的 8 条代码风格规则。
|
||||
|
||||
#### Scenario: 启用的规则
|
||||
|
||||
- **WHEN** 运行 revive
|
||||
- **THEN** SHALL 启用:exported、var-naming、indent-error-flow、error-strings、error-return、blank-imports、context-as-argument、unexported-return
|
||||
|
||||
#### Scenario: 测试代码排除 exported
|
||||
|
||||
- **WHEN** 测试文件中的导出符号缺少文档注释
|
||||
- **THEN** revive SHALL NOT 报错
|
||||
|
||||
### Requirement: gosec 安全检查
|
||||
|
||||
系统 SHALL 通过 gosec 检查常见安全问题。
|
||||
|
||||
#### Scenario: 正式代码全部启用
|
||||
|
||||
- **WHEN** 正式代码中存在安全隐患(硬编码凭证、SQL 注入等)
|
||||
- **THEN** gosec SHALL 报错
|
||||
|
||||
#### Scenario: 测试代码排除部分规则
|
||||
|
||||
- **WHEN** 测试文件中触发 G101(硬编码密钥)、G401/G501(弱密码算法)
|
||||
- **THEN** gosec SHALL NOT 报错
|
||||
|
||||
### Requirement: gocyclo 圈复杂度控制
|
||||
|
||||
系统 SHALL 通过 gocyclo 控制函数复杂度。
|
||||
|
||||
#### Scenario: 正式代码复杂度阈值
|
||||
|
||||
- **WHEN** 正式代码中函数圈复杂度超过 10
|
||||
- **THEN** gocyclo SHALL 报错
|
||||
|
||||
#### Scenario: 测试代码复杂度阈值
|
||||
|
||||
- **WHEN** 测试代码中函数圈复杂度超过 20
|
||||
- **THEN** gocyclo SHALL 报错
|
||||
|
||||
### Requirement: goimports import 排序
|
||||
|
||||
系统 SHALL 通过 goimports 统一 import 分组排序。
|
||||
|
||||
#### Scenario: 三组格式
|
||||
|
||||
- **WHEN** 格式化 import
|
||||
- **THEN** SHALL 按标准库、第三方库、本地包(nex/backend)三组排序
|
||||
- **THEN** local-prefixes SHALL 配置为 nex/backend
|
||||
|
||||
### Requirement: 生成代码排除
|
||||
|
||||
系统 SHALL 排除自动生成的代码的 lint 检查。
|
||||
|
||||
#### Scenario: mocks 目录排除
|
||||
|
||||
- **WHEN** lint 扫描 tests/mocks/ 目录
|
||||
- **THEN** SHALL 排除该目录(由 mockgen 生成的代码)
|
||||
|
||||
#### Scenario: Code generated 标记自动检测
|
||||
|
||||
- **WHEN** 文件包含 `// Code generated by` 标记
|
||||
- **THEN** golangci-lint SHALL 自动排除该文件
|
||||
|
||||
### Requirement: embedfs 编译兼容
|
||||
|
||||
系统 SHALL 确保 golangci-lint 能正常加载 embedfs 模块。
|
||||
|
||||
#### Scenario: 空目录占位
|
||||
|
||||
- **WHEN** embedfs 模块的 assets/ 和 frontend-dist/ 目录不存在
|
||||
- **THEN** SHALL 通过 .gitkeep 文件确保目录存在
|
||||
- **THEN** go:embed 指令 SHALL 能正常匹配
|
||||
@@ -0,0 +1,45 @@
|
||||
# Error Handling — Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 使用类型安全错误判断
|
||||
|
||||
系统 SHALL 使用类型安全方式判断错误类型,并通过 lint 工具强制执行。
|
||||
|
||||
#### Scenario: 数据库错误判断
|
||||
|
||||
- **WHEN** 判断数据库唯一约束错误
|
||||
- **THEN** SHALL 使用 errors.Is(err, gorm.ErrDuplicatedKey)
|
||||
- **THEN** SHALL NOT 使用字符串匹配 err.Error()
|
||||
|
||||
#### Scenario: 网络错误判断
|
||||
|
||||
- **WHEN** 判断网络错误
|
||||
- **THEN** SHALL 使用 errors.As(err, &net.Error) 判断网络错误
|
||||
- **THEN** SHALL 使用 errors.As(err, &net.OpError) 判断操作错误
|
||||
- **THEN** SHALL 使用 errors.Is(opErr.Err, syscall.ECONNRESET) 判断连接重置
|
||||
- **THEN** SHALL NOT 使用字符串匹配判断错误类型
|
||||
|
||||
#### Scenario: 错误链判断
|
||||
|
||||
- **WHEN** 判断错误链中的特定错误
|
||||
- **THEN** SHALL 使用 errors.Is 进行链式判断
|
||||
- **THEN** SHALL 使用 errors.As 提取特定类型错误
|
||||
|
||||
#### Scenario: lint 自动拦截错误比较
|
||||
|
||||
- **WHEN** 代码中使用 `err == someError` 直接比较
|
||||
- **THEN** errorlint SHALL 检测并报错
|
||||
- **THEN** SHALL 改用 errors.Is()
|
||||
|
||||
#### Scenario: lint 自动拦截类型断言
|
||||
|
||||
- **WHEN** 代码中使用 `err.(SomeType)` 直接类型断言
|
||||
- **THEN** errorlint SHALL 检测并报错
|
||||
- **THEN** SHALL 改用 errors.As()
|
||||
|
||||
#### Scenario: lint 自动拦截忽略错误返回值
|
||||
|
||||
- **WHEN** 代码中使用 `_ = funcReturnsError()` 忽略错误
|
||||
- **THEN** errcheck SHALL 检测并报错
|
||||
- **THEN** SHALL 正确处理错误或添加 //nolint:errcheck 注释(仅在有意忽略时)
|
||||
@@ -0,0 +1,32 @@
|
||||
# Module Logging — Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 禁止全局 logger
|
||||
|
||||
系统 SHALL 禁止在业务代码中使用全局 logger,并通过 lint 工具强制执行。
|
||||
|
||||
#### Scenario: 移除 zap.L() 调用
|
||||
|
||||
- **WHEN** 重构现有代码
|
||||
- **THEN** SHALL 移除所有 `zap.L()` 调用
|
||||
- **THEN** SHALL 通过构造函数注入 logger
|
||||
- **THEN** 允许仅在测试代码中使用 `zap.L()` 或 `zap.NewNop()`
|
||||
|
||||
#### Scenario: 移除 zap.L() fallback
|
||||
|
||||
- **WHEN** 构造函数 logger 参数为 nil
|
||||
- **THEN** SHALL NOT 使用 `zap.L()` 作为默认值
|
||||
- **THEN** 调用方 SHALL 必须传入有效的 logger
|
||||
|
||||
#### Scenario: lint 自动拦截 zap.L()
|
||||
|
||||
- **WHEN** 正式代码中新增 `zap.L()` 调用
|
||||
- **THEN** forbidigo SHALL 检测并报错
|
||||
- **THEN** git commit SHALL 被拒绝
|
||||
|
||||
#### Scenario: 禁止 fmt/os.Stderr 直接输出
|
||||
|
||||
- **WHEN** 正式代码中使用 fmt.Print*、fmt.Fprintf(os.Stderr, ...) 等直接输出
|
||||
- **THEN** forbidigo SHALL 检测并报错
|
||||
- **THEN** SHALL 使用注入的 zap logger 替代
|
||||
@@ -0,0 +1,54 @@
|
||||
# Pre-commit Hook
|
||||
|
||||
## Purpose
|
||||
|
||||
定义基于 lefthook 的 pre-commit hook 配置,在 git commit 时自动运行 lint 检查,拦截违规代码提交。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: lefthook 配置
|
||||
|
||||
系统 SHALL 通过 `lefthook.yml` 配置 pre-commit hook。
|
||||
|
||||
#### Scenario: 配置文件位置
|
||||
|
||||
- **WHEN** 配置 lefthook
|
||||
- **THEN** 配置文件 SHALL 位于项目根目录 `lefthook.yml`
|
||||
|
||||
#### Scenario: pre-commit hook 安装
|
||||
|
||||
- **WHEN** 开发者首次克隆项目
|
||||
- **THEN** 运行 `lefthook install` SHALL 安装 git hooks
|
||||
- **THEN** hooks SHALL 自动注册到 .git/hooks/
|
||||
|
||||
### Requirement: Go 文件变更触发 lint
|
||||
|
||||
系统 SHALL 在 Go 文件变更时自动运行 golangci-lint。
|
||||
|
||||
#### Scenario: 检测到 Go 文件变更
|
||||
|
||||
- **WHEN** git commit 中包含 backend/**/*.go 文件的变更
|
||||
- **THEN** SHALL 自动运行 golangci-lint
|
||||
|
||||
#### Scenario: 增量检查
|
||||
|
||||
- **WHEN** 运行 lint
|
||||
- **THEN** SHALL 只检查 staged 文件(使用 --new-from-rev HEAD)
|
||||
- **THEN** SHALL NOT 检查整个代码库
|
||||
|
||||
#### Scenario: lint 通过
|
||||
|
||||
- **WHEN** golangci-lint 检查通过
|
||||
- **THEN** commit SHALL 正常完成
|
||||
|
||||
#### Scenario: lint 失败
|
||||
|
||||
- **WHEN** golangci-lint 检查发现违规
|
||||
- **THEN** commit SHALL 被拒绝
|
||||
- **THEN** SHALL 显示具体的违规信息和修复建议
|
||||
|
||||
#### Scenario: 无 Go 文件变更
|
||||
|
||||
- **WHEN** git commit 不包含 Go 文件变更
|
||||
- **THEN** SHALL NOT 运行 golangci-lint
|
||||
- **THEN** commit SHALL 正常完成
|
||||
@@ -0,0 +1,30 @@
|
||||
# Structured Logging — Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 字段标准化
|
||||
|
||||
系统 SHALL 使用标准化字段定义,并通过 lint 工具强制执行错误字段规范。
|
||||
|
||||
#### Scenario: 标准字段常量
|
||||
|
||||
- **WHEN** 记录日志字段
|
||||
- **THEN** SHALL 使用 `pkg/logger/field.go` 中定义的常量
|
||||
- **THEN** 字段名 SHALL 包括:`request_id`、`provider_id`、`model_name`、`method`、`path`、`status`、`latency`
|
||||
|
||||
#### Scenario: 错误字段统一
|
||||
|
||||
- **WHEN** 记录错误日志
|
||||
- **THEN** SHALL 使用 `zap.Error(err)`
|
||||
- **THEN** SHALL NOT 使用 `zap.String("error", err.Error())`
|
||||
|
||||
#### Scenario: lint 强化错误字段约束
|
||||
|
||||
- **WHEN** 存量代码中使用 `zap.String("error", err.Error())` 记录错误
|
||||
- **THEN** SHALL 修改为 `zap.Error(err)`
|
||||
|
||||
#### Scenario: 字段构造函数
|
||||
|
||||
- **WHEN** 构造日志字段
|
||||
- **THEN** SHALL 优先使用 `pkg/logger` 提供的辅助函数
|
||||
- **THEN** 辅助函数 SHALL 返回 `zap.Field` 类型
|
||||
Reference in New Issue
Block a user