容易混淆的问题
这些概念名字相近,误用不会立刻报错,只会在某次 review 或线上事故里暴露。下面按检查链、测试、正则、重构分组,每组先给对照表,再补判断依据。
格式化与静态检查
格式化器只重写 token 之间的空白与标点,输出确定、可逆;linter 读控制流和数据流,输出是「可能有问题」,允许误报。前者全自动,后者只有一部分能自动修。
判断方法只有一条:这条建议能不能用「改完一定等价」来证明。能,就是格式化或安全修复;不能,就只能报告,由人决定。
| 对照 | 区别 | 判断方法 |
|---|---|---|
| 格式化 vs lint | 排版 vs 语义模式 | 问「改完是否等价」 |
--check | 只返回状态码,不改文件 | CI 用 |
--write | 直接写回文件 | 本地用 |
noqa | 单行或单文件豁免 | 补上具体规则编号与理由 |
ignore | 全局关闭一条规则 | 只用于确认不适用的规则 |
ruff format --check . # CI:不合规就红
ruff format . # 本地:直接改
ruff check . # 只报告
ruff check --fix . # 修可证明等价的部分--check 与 --fix 必须读同一份配置。配置漂移时,本地全绿、CI 全红,排查方向会被引到错误的地方。
提交钩子与 CI
钩子跑在本地,跟着工作副本走,能被 --no-verify 跳过,也会因为新机器没装依赖而静默失效。CI 跑在干净容器里,每次从零开始,是唯一无法绕过的关卡。
所以钩子负责把反馈提前,CI 负责裁决。把整套测试塞进钩子,结果是人开始习惯性跳过它。
| 对照 | 运行位置 | 能否绕过 | 适合 |
|---|---|---|---|
| pre-commit | 本地 | 能,且可卸载 | 秒级:格式化、lint |
| pre-push | 本地 | 能 | 分钟级:测试 |
| CI | 服务端 | 不能 | 全量验证与构建 |
测试
pytest -k 匹配的是用例名,表达式支持 and、or、not;go test -run 匹配的是测试函数名,用的是正则,不加锚点时按子串匹配。
pytest -k "login and not slow"
go test -run 'TestLogin|TestSignup'覆盖率是「执行过的行」占比,和断言质量无关。100% 覆盖的测试可能一条断言都没写,把断言删掉覆盖率也不会掉。
| 对照 | 匹配或衡量对象 | 常见误用 |
|---|---|---|
pytest -k | 用例名字符串表达式 | 当成正则写 |
go test -run | 测试函数名的正则 | 以为必须从开头匹配 |
| 行覆盖 | 执行过的行 | 当成正确性证明 |
| 分支覆盖 | 条件的两侧 | 以为等价于行覆盖 |
正则
re.match 只从字符串开头尝试,re.search 会扫描中间,re.fullmatch 要求整串被消耗完。多数校验 bug 来自用了 match 却以为在整串匹配,于是末尾多出来的内容被放过。
贪婪与懒惰决定量词停在哪里:.* 吃到最后一个可行位置,.*? 停在第一个。回溯决定代价:嵌套量词会让失败尝试指数增长。
| 对照 | 行为 |
|---|---|
re.match | 只匹配开头,不要求整串 |
re.fullmatch | 整串必须匹配 |
.* | 贪婪,尽量多吃 |
.*? | 懒惰,尽早停下 |
$1 | VS Code、sed 的替换模板 |
\1 | Python、Vim、PCRE 的反向引用 |
| grep | 字符层,注释与字符串也会命中 |
| semgrep | AST 层,跳过注释、跨行也能匹配 |
替换引用写错不会报错,只会得到一段看似合理的新文本。改完先 git diff 看一遍,再跑测试。
重构与重写
重构保持外部行为不变,靠测试证明;重写允许改变行为,需要新的验收标准。前者可以随时停下,每一步都能发布;后者必须一次做到可用状态,中途停下就是半成品。
| 对照 | 外部行为 | 测试 | 提交粒度 |
|---|---|---|---|
| 重构 | 不变 | 改前改后都绿 | 小步、可回滚 |
| 重写 | 可能变 | 需要新基线 | 整体切换 |
| 自动修复 | 不变 | 修完仍需验证 | 单独提交 |
判断自己在做哪一种,看有没有一条能同时跑在旧代码和新代码上的测试。没有,就是在重写,风险要按重写来估。