关于 code-quality
为 AI Agent 提供统一的测试、代码结构、错误处理和命名规范,适用于编码与重构,并以清单和详细规则指导执行。
主要功能
- 01四层测试验证框架
- 02测试驱动开发循环
- 03代码结构与命名规则
- 04具体错误处理指导
- 05编码与重构清单
适用场景
- 01实现前规划测试
- 02检查代码质量问题
- 03重构不一致的代码
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ alexanderstephenthompson/ claude-hub --skill code-quality安装后在 Claude Code 中输入/code-quality确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/docstring-templates.mdtdd-checklist.mdcode-review-checklist.mdreferences/naming-reference.mderror-handling-reference.mdtesting-reference.mdscripts/check_naming.pycheck_complexity.py详细介绍
代码质量标准
不容妥协的代码质量标准。这些不是偏好,而是要求。
问题
AI Agent 会生成能运行但风格不一致的代码。每次会话都会选择略有不同的命名、嵌套模式和错误处理方式——这并非有意为之,而是各自独立的结果。随着时间推移,这种偏差会不断累积:代码库中会出现三种错误处理方式、四套命名约定,以及散落各处、几乎一致却又不完全匹配的魔法数字。这些标准会将偏差降至零,让重构保持小规模且不频繁。
使用方式
- 构建者: 编写代码前先阅读
## Builder Checklist。如果检查清单中的条目含义不明确,请参阅叙述性章节。 - 重构者: 将
## Enforced Rules用作问题清单。阅读叙述性章节以了解修复指导。 - 两者: 叙述性章节是权威标准。检查清单和规则表是对相同内容的精简呈现。
测试驱动开发:4 层验证框架
第 1 层 — 单元测试(逻辑是否正确?)
单独测试函数、计算、状态转换和数据格式化。纯输入/输出——给定此输入,能否得到此输出?对被测代码周围的一切进行模拟。这些测试快速、成本低,数量也应该最多。覆盖:正常路径、当前要实现的特定场景,以及边界情况。
第 2 层 — 集成测试(各部分是否衔接?)
测试多个部分如何协同工作。API 调用是否返回预期的数据结构,组件是否使用正确的 props 渲染,数据库查询是否返回正确数据,钩子和状态管理是否正确连接,各模块之间是否正确传递数据。这些测试能发现“单独运行正常,组合起来就出问题”的情况。
第 3 层 — 行为测试(流程是否正常?)
使用 Playwright 或类似自动化工具,端到端测试完整的面向用户流程。模拟真实交互——点击此处、确认出现预期结果、跳转至此处、表单是否正确提交、数据是否显示在正确位置。同时测试相邻流程,确保没有引入回归。这些测试速度较慢,也更脆弱,但能发现其他层级遗漏的问题。
第 4 层 — 人工验证(实际体验是否合适?)
验证自动化测试无法判断的所有内容:视觉效果是否正确、动画是否流畅、用户体验是否直观、屏幕上的数据是否符合现实预期、在实际设备和屏幕尺寸上是否能正常运行。第 1 至第 3 层通过后,根据变更内容生成具体的人工检查清单,而不是使用通用检查项。如果工作涉及图表组件,请人工检查图表;如果涉及 API 集成,请验证真实数据是否顺利流转;如果涉及布局,请在实际屏幕宽度下检查。
TDD 原则
真正的 TDD 会在编写任何实现代码之前,先编写第 1 至第 3 层的测试。所有测试一开始都应该失败——这正是目的。测试定义了要构建的内容。然后编写代码,直到测试通过。第 4 层是所有其他测试通过后的最终关卡。
每次变更都遵循以下循环:
- 红 — 编写一个失败的测试
- 绿 — 编写通过该测试所需的最少代码
- 重构 — 在不改变行为的前提下清理代码
- 在第 1 层重复,直到单元完成;然后进行第 2 层,再进行第 3 层
- 所有自动化测试通过后,第 4 层是最终关卡
拿不准时: 放慢脚步,编写测试,并做出尽可能小的变更。
什么是好的测试
每个测试都按 Arrange-Act-Assert 结构编写:
def test_apply_discount_reduces_total():
# Arrange — set up the scenario
cart = Cart(items=[Item(price=100)])
discount = Discount(percent=20)
# Act — perform the action under test
cart.apply_discount(discount)
# Assert — verify the outcome
assert cart.total == 80
每个测试只验证一个概念。 如果测试名称中有“和”,就把它拆分成两个测试。
测试名称应描述行为,而非实现:
| 不佳 | 良好 |
|---|---|
test_calculate | test_calculate_total_sums_item_prices |
test_error | test_negative_quantity_raises_validation_error |
test_user_service | test_deactivated_user_cannot_place_order |
测试必须:
- 相互隔离 — 测试之间不能依赖彼此的状态或执行顺序
- 结果确定 — 相同输入得到相同结果。不能依赖随机性、时钟或网络调用。
- 运行快速 — 单元测试应在几毫秒内完成。如果运行缓慢,它就不是单元测试。
- 易于阅读 — 无需阅读测试主体,仅凭失败的测试名称就应能知道哪里出了问题
代码结构
优先使用提前返回,避免嵌套
先写保护子句,简化控制流。
不佳 — 嵌套过多,难以理解:
def process(order):
if order:
if order.items:
if order.is_valid:
return calculate_total(order)
return None
良好 — 结构平坦,清晰明了:
def process(order):
if not order:
return None
if not order.items:
return None
if not order.is_valid:
return None
return calculate_total(order)
嵌套深度最多为 3 层。 如果更深,就提取为函数。
函数长度
函数应只做一件事。如果需要用注释在函数内划分“部分”,就应该把这些部分拆成独立函数。
参考指南:
- 如果函数超过约 30 行,考虑是否可以提取其中的逻辑
- 如果函数的参数超过 3-4 个,它很可能承担了太多职责
- 如果无法为函数起一个清晰的名称,它承担的职责就太多了
单一职责
每个函数、类和模块都应该只有一个变更原因。
异味:“这个函数同时处理验证、格式化和保存。”
**修正:**拆分为三个函数——validate、format、save。
导入规范
- 不保留未使用的导入 — 删除它们,不要注释掉
- 不使用通配符导入 —
from x import *会掩盖名称的来源 - 对导入分组 — 先是标准库,再是外部依赖,最后是内部模块,组间用空行分隔
# Bad — unordered, wildcard, unused
from utils import *
import os
import requests
from collections import OrderedDict # unused
# Good — grouped, explicit, no dead weight
import os
import requests
from app.models import User
from app.services import send_welcome_email
死代码
没有被调用的代码就不存在。删除它——版本控制会保留记录。
- 不保留被注释掉的代码 —
// old version、# was: ...、/* removed */都是杂乱内容 - 不保留无法到达的分支 — 无条件返回后的无用
else、未使用的变量 - 不保留占位注释 —
// TODO: removed或// no longer needed,直接删除整行即可
明确胜过巧妙
可读性胜过简洁。把操作拆分成清晰的步骤。
不佳 — 写法巧妙,但难以调试:
names = [u.name for u in users if u.is_active and u.role in allowed]
良好 — 意图清晰,便于调试:
active_users = filter_active(users)
authorized_users = filter_by_role(active_users, allowed)
names = extract_names(authorized_users)
如果单行代码需要费心理解,就把它拆开。优化代码时要优先考虑读者,而非编写者。
错误处理
尽早失败
在边界处验证输入。不要让错误数据深入系统。
def create_user(email, name):
if not email:
raise ValidationError("Email is required")
if not is_valid_email(email):
raise ValidationError("Invalid email format")
return save_user(email, name)
使用具体错误,而非通用错误
捕获预期的错误。对未预期的错误重新抛出。绝不要写 except Exception — 先识别实际的失败类型。
根据操作选择匹配的异常类型:
| 操作 | 捕获 | 原因 |
|---|---|---|
| 打开、读取或写入文件 | OSError | 涵盖 FileNotFoundError、PermissionError、IsADirectoryError |
| 读取文件并解析内容 | (OSError, UnicodeDecodeError) | 文件可能存在,但包含无效编码 |
| 解析 JSON/YAML | (json.JSONDecodeError, ValueError) | 内容格式错误 |
| 将字符串转换为数字 | ValueError | 格式无效 |
| 访问字典或列表 | (KeyError, IndexError) | 缺少键或索引越界 |
| 网络请求 | (ConnectionError, TimeoutError) | 网络相关故障 |
| 执行子进程 | (subprocess.SubprocessError, OSError) | 进程启动或执行失败 |
| 正则表达式操作 | re.error | 模式无效 |
不佳 — 吞掉所有错误:
try:
do_risky_thing()
except Exception:
pass
良好 — 处理能够理解的错误:
try:
do_risky_thing()
except NetworkError:
return retry()
except ValidationError as e:
return error_response(e.message)
# Unexpected errors propagate up
可以使用宽泛捕获的情况: 仅限应用程序的顶层边界(CLI main()、API 请求处理程序),此时替代做法是让程序因未处理的错误而崩溃。即便如此,也要在继续执行前记录完整异常。
绝不吞掉错误
如果捕获了错误,必须执行以下操作之一:
- 处理错误——采取有意义的恢复措施
- 记录并重新抛出——让失败清晰可见
- 转换错误——为调用方包装成更具体的错误
空的 catch / except 块就是 bug。
命名约定
名称必须清楚地表达:
- 谁在执行操作——执行操作的主体
- 正在执行什么操作——描述行为的动词
- 数据或所有权的流向——数据流向何处或来自何处
明确流向
使用介词(to、from、into、onto)或具名参数。
不佳——含糊不清:
shop.buy_item(item_id, buyer) # Who is buying?
transfer(amount, account) # Transfer to or from?
良好——清晰明确:
shop.sell_item_to(item_id, buyer) # Shop sells TO buyer
shop.sell(item_id, to=buyer) # Named parameter clarifies
transfer_from(account, amount) # Direction explicit
account.transfer_to(other, amount) # Direction in method name
大声朗读测试
如果一个方法调用读起来不自然,那么它的名称就不合适。
# "shop buy item buyer" — confusing
shop.buy_item(item_id, buyer)
# "shop sell item to buyer" — clear
shop.sell_item_to(item_id, buyer)
布尔值命名
布尔值名称始终以 is、has、should、can、will 或 did 开头:
# Bad — ambiguous (is it a noun? a verb? a state?)
active = True
permission = True
refresh = True
# Good — clearly a yes/no question
is_active = True
has_permission = True
should_refresh = True
命名模式
| 模式 | 适用场景 | 示例 |
|---|---|---|
verb_noun_to(target) | 操作流向目标 | send_message_to(user) |
verb_noun_from(source) | 操作来自来源 | receive_payment_from(customer) |
noun.verb_to(target) | 对象向目标执行操作 | cart.transfer_to(order) |
verb(noun, to=target) | 具名参数让含义更清晰 | assign(task, to=developer) |
绝不使用缩写
每次都写出完整单词。唯一可接受的缩写是广泛认可的技术术语:id、url、api、db、io。
**禁止使用单字符变量。**循环计数器也不例外。i 和 j 会掩盖你正在遍历的内容:
# Bad — what is i? what is j?
for i in range(len(rows)):
for j in range(len(columns)):
grid[i][j] = calculate(i, j)
# Good — names describe the iteration
for row_index in range(len(rows)):
for column_index in range(len(columns)):
grid[row_index][column_index] = calculate(row_index, column_index)
常见违规——这些问题经常出现,必须始终展开:
| 写这个 | 不要写这个 |
|---|---|
dependency | dep |
index / position | idx |
source | src |
destination | dst |
description | desc |
threshold | thresh |
config / configuration | cfg |
message | msg |
request | req |
response | res |
context | ctx |
error | err |
value | val |
count | cnt |
button | btn |
user | usr |
callback | cb |
function | fn |
manager | mgr |
service | svc |
repository | repo |
implementation | impl |
password | pwd |
temporary | tmp |
number | num |
完整列表:references/naming-reference.md
避免
| 不要这样 | 应改为 |
|---|---|
单字符名称(i、x、e) | 描述性名称(row_index、coordinate、error) |
通用名称(data、list、temp) | 具体名词(user_data、order_list) |
否定式布尔值(is_not_disabled) | 肯定形式(is_enabled) |
常量与清晰度
不要使用魔法值
每个数字和字符串字面量都应该有名称。编写任何字面量前,先进行提取测试:
**提取测试:**如果字面量不是 0、1、-1、True、False、None 或 "",就需要为其定义具名常量。
包括:
- 阈值和限制——
MAX_RETRIES = 3、HIGH_COUPLING_THRESHOLD = 10 - 大小和度量值——
MIN_FONT_SIZE_PX = 12、MASK_VISIBLE_CHARACTERS = 4 - 字符串模式——
DEFAULT_ENCODING = "utf-8"、CSV_DELIMITER = "," - 配置值——
TOP_RESULTS_DISPLAY_LIMIT = 20、SCAN_DEPTH = 3
根据常量的含义命名,而不是根据它的值命名。THREE = 3 毫无意义。MAX_RETRIES = 3 则表达了意图。
不佳:
if retry_count > 3:
sleep(60)
if len(results) > 20:
results = results[:20]
良好:
MAX_RETRIES = 3
RETRY_DELAY_SECONDS = 60
TOP_RESULTS_DISPLAY_LIMIT = 20
if retry_count > MAX_RETRIES:
sleep(RETRY_DELAY_SECONDS)
if len(results) > TOP_RESULTS_DISPLAY_LIMIT:
results = results[:TOP_RESULTS_DISPLAY_LIMIT]
将常量放在模块顶部,按用途分组,并置于所有函数定义之前。
布尔参数
布尔参数会让调用处的含义变得模糊。
不佳——True 表示什么?
create_user(data, True, False)
良好——使用具名参数或选项:
create_user(data, send_welcome=True, require_verification=False)
如果语言不支持具名参数,请使用选项对象/结构体。
默认保持不可变
默认使用不可变数据。只有确实需要重新赋值时,才使用可变绑定。
- 使用
const(JS/TS)、final(Java/Dart)、readonly(C#)或等效方式 - 仅当值必须变化时才使用
let/var(循环计数器、累加器) - 不要修改函数参数——改为返回新值
# Bad — mutates the input
def apply_discount(cart):
cart["total"] *= 0.8
return cart
# Good — returns a new value
def apply_discount(cart):
return {**cart, "total": cart["total"] * 0.8}
文档
文档字符串
文档字符串是持续更新的文档。 公共 API 必须能够自我说明,无需阅读实现。
必需元素
每个公共函数、方法和类都必须包含:
- 用途 — 它的功能(一行)
- 参数 — 每个参数的类型和含义
- 返回值 — 返回什么,以及在什么情况下返回
- 副作用 — 任何状态变化、I/O 或修改
- 错误 — 可能出现的异常/错误
- 示例 — 展示常见场景的实际用法
文档字符串示例
def sell_item_to(self, item_id: str, buyer: Customer) -> Receipt:
"""Sell an item from shop inventory to a customer.
Transfers ownership of the item from the shop to the buyer,
processes payment, and updates inventory.
Args:
item_id: Unique identifier of the item to sell.
buyer: Customer purchasing the item. Must have sufficient balance.
Returns:
Receipt containing transaction details and timestamp.
Raises:
ItemNotFoundError: If item_id doesn't exist in inventory.
InsufficientBalanceError: If buyer can't afford the item.
ItemAlreadySoldError: If item was sold between check and purchase.
Examples:
Basic sale:
>>> shop = Shop(inventory=[item])
>>> buyer = Customer(balance=100)
>>> receipt = shop.sell_item_to(item.id, buyer)
>>> assert receipt.amount == item.price
>>> assert item.id not in shop.inventory
Handling insufficient balance:
>>> poor_buyer = Customer(balance=0)
>>> shop.sell_item_to(item.id, poor_buyer)
Raises InsufficientBalanceError
"""
文档字符串规则
- 示例应与实际测试场景一致
- 行为变化时更新文档字符串
- 将文档字符串视为一等代码,而非装饰
注释
写任何注释之前,先进行删除测试: 在脑海中删除这条注释。是否会遗漏任何信息?如果代码通过命名和结构已经传达了相同信息,就不要写注释。
| 应该注释 | 不要注释 |
|---|---|
| 原因 — 意图、业务原因、不明显的背景信息 | 内容 — 代码已经说明了这一点 |
| 不明显的注意事项或边缘情况 | 显而易见的操作 |
| 复杂算法概要 | 用来解释糟糕代码的注释(应改进代码) |
| 带有工单/问题引用的 TODO | 缺少背景信息的 TODO |
| 正则表达式模式说明(该模式匹配什么) | 重述函数调用(# Send the email) |
不好 — 重述代码:
# Get the users
users = get_users()
# Filter active users
active_users = filter_active(users)
# Count the results
count = len(active_users)
好 — 无需注释(代码本身已说明):
users = get_users()
active_users = filter_active(users)
count = len(active_users)
好 — 解释原因:
# Offset by 1 because CSS cascade position is 1-indexed but array is 0-indexed
cascade_position = file_index + 1
如果需要注释来解释代码的作用,就说明代码应该更清晰。 重命名变量、提取函数、简化逻辑——这样注释就不再必要。
构建者检查清单
在编写受本规范约束的代码之前,请根据这些约束检查你的计划。这是上文详细说明的相同指导原则,经过精简以便在构建前核对。构建者应在编写代码前阅读本节;重构者则应使用“强制规则”表和完整说明。
- 在实现之前编写第 1–3 层(先让测试失败,再编写代码使其通过)
- 第 1 层:单元测试覆盖逻辑、正常路径和边缘情况
- 第 2 层:集成测试验证各部分正确连接
- 第 3 层:行为测试端到端模拟真实用户流程
- 第 4 层:根据变更内容生成由人工验证的检查清单
- 每个测试只验证一个概念,并采用 Arrange-Act-Assert 结构
- 测试彼此隔离、结果确定且运行快速
- 函数简短且职责单一
- 嵌套最多 3 层,并使用提前返回
- 在边界处快速失败,并使用具体类型
- 异常类型与操作相匹配(不得使用裸
except Exception) - 不得有空的 catch/except 块
- 名称通过朗读测试
- 不得使用单字符变量——应使用描述性名称
- 不得使用缩写名称——写出完整单词(使用
dependency,而非dep) - 方法名称应清楚表达方向(to/from)
- 布尔值名称以 is/has/should/can 开头
- 每个字面值都通过提取测试——若不是 0/1/True/False/None/"",就定义具名常量
- 常量放在模块顶部,并按用途分组
- 布尔参数使用具名参数或选项
- 变量默认不可变(
const/final/readonly) - 不得有未使用或通配符导入
- 不得有注释掉的代码或死代码
- 所有公共 API 都有完整的文档字符串
- 注释通过删除测试——只解释原因,绝不解释内容
强制规则
这些规则由 check.js(clean-team)确定性地检查。更新这些标准时,请同步更新对应的 check.js 规则,反之亦然。
| 规则 ID | 严重级别 | 检查内容 |
|---|---|---|
no-debugger | 错误 | 代码中残留的 debugger 语句 |
no-var | 错误 | var 声明(请使用 const/let) |
no-empty-catch | 错误 | 没有处理逻辑的空 catch 块 |
no-console | 警告 | console.log/warn/error 语句 |
no-double-equals | 警告 | 使用 ==/!= 而非严格相等运算符(允许 == null) |
参考资料
references/testing-reference.md— 深入了解测试金字塔、模拟指南和反模式references/naming-reference.md— 完整的命名约定、缩写规则和领域命名references/error-handling-reference.md— 错误层级、重试/回退模式和错误边界
资源文件
assets/tdd-checklist.md— 逐步 TDD 工作流检查清单assets/docstring-templates.md— 可直接复制粘贴的文档字符串模板(Python、JS/TS、C#、Rust、Go)assets/code-review-checklist.md— 全面的代码审查检查清单
模板
特定领域的代码质量扩展。每个文件都针对一种项目类型,提供相同类别(结构、命名、测试、错误处理、性能)的脚手架:
templates/web-development.md— Web 专属质量规则templates/data.md— 数据管道和分析质量规则templates/world-building.md— Unity 和 VRChat 质量规则
脚本
scripts/check_naming.py— 验证任何代码库中的命名约定scripts/check_complexity.py— 检查函数长度、嵌套深度和参数数量