关于 awesome-code-standards
一项用于在项目和模块中统一编码约定的技能。它帮助开发者发现现有做法,并据此规范命名、结构、错误处理、类型、并发和文档。
主要功能
- 01发现项目现有约定
- 02命名与结构指导
- 03不可变性与错误处理规范
- 04类型安全与并发指导
- 05注释和文档规范
适用场景
- 01启动项目或模块
- 02让重构符合团队约定
- 03设置 Lint、格式化或类型检查规则
- 04为新成员整理编码约定
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ khasky/ awesome-agent-skills --skill awesome-code-standards安装后在 Claude Code 中输入/awesome-code-standards确认已加载。
附带文件evals/eval_queries.json
详细介绍
编码规范
遵循统一的命名、结构和模式,让团队中的代码更易读、更易维护。
适用场景
- 启动新项目或模块
- 重构以符合团队规范
- 设置或更新 lint、格式化或类型检查规则
- 确定代码库应遵循哪些规范(审查特定差异应使用 awesome-code-review)
- 新人入职:记录或应用编码规范
- 强制保持命名、格式或结构一致
核心原则
- 可读性优先——代码被阅读的次数多于编写次数;清晰的命名和结构胜过巧妙的技巧。
- KISS——选择可行的最简单方案;避免过度设计和过早优化。
- DRY——将通用逻辑提取到函数或模块中;避免复制粘贴。
- YAGNI——不要为推测的未来需求构建功能;有需要时再增加复杂性。
- 不可变性——优先使用 const;避免修改参数或共享状态;必要时使用展开或复制。
工作流程(应用规范时)
- 了解项目规范——检查现有代码:命名方式(camelCase 还是 snake_case)、文件布局、导入风格、测试模式。查看 CONTRIBUTING、.eslintrc、.prettierrc 或 editorconfig。按维度统计(引号、缩进、命名格式):占多数的变体即为规范,少数变体记为问题;若票数相同,则询问而不是自行决定。
- 识别违规项——将已更改或新增的代码与这些规范及下方规则进行比较。
- 提出具体修复方案——重命名符号、提取函数、添加类型、修正格式。每条建议尽量只包含一项逻辑编辑。
- 记录例外情况——如果项目有例外规定(例如“此处为兼容旧代码而使用 any”),应注明,而不是不加说明地“修复”。
命名规范
变量和函数
// GOOD: Descriptive, verb-noun for functions
const marketSearchQuery = 'election';
const isUserAuthenticated = true;
async function fetchMarketData(marketId: string) {}
function calculateSimilarity(a: number[], b: number[]) {}
// BAD: Unclear or noun-only for actions
const q = 'election';
const flag = true;
async function market(id: string) {}
function similarity(a, b) {}
常量
- 真正常量使用 UPPER_SNAKE(例如
MAX_RETRIES、API_BASE_URL)。 - 或遵循项目规范(某些代码库对配置对象使用 camelCase)。
类型和接口
- 使用 PascalCase:
User、OrderItem、ApiResponse<T>。 - 如有助于说明用途,可添加角色后缀:
CreateUserRequest、UserResponse。
文件
- 组件:PascalCase(
Button.tsx、UserProfile.tsx)。 - 工具函数/钩子:camelCase(
formatDate.ts、useAuth.ts)。 - 类型:根据项目使用 camelCase 搭配
.types或.d(market.types.ts)。
其他语言
以上示例使用 TypeScript;规则是“遵循该语言自身发布的规范”,且各语言的规范各不相同。各语言的风格指南优先于此处展示的大小写格式:
- Python(PEP 8)——函数、变量和模块使用
snake_case;类使用PascalCase;常量使用UPPER_SNAKE;前导_表示非公开。 - Go(Effective Go)——使用
MixedCaps,绝不使用下划线;首字母大写 即为 导出标记。包名是名称的一部分:使用chi.NewRouter,而不是chi.NewChiRouter。 - Rust(RFC 430)——函数、变量和模块使用
snake_case;类型和 trait 使用UpperCamelCase;const 和静态变量使用SCREAMING_SNAKE_CASE。 - Java / Kotlin——方法和字段使用
camelCase,类型使用PascalCase;每个文件放置一个公开类型,文件名与之相同。 - C#——方法、属性和类型使用
PascalCase;局部变量和参数使用camelCase;接口名称以I为前缀。
不可变性(关键)
// GOOD: Spread and new references
const updatedUser = { ...user, name: 'New Name' };
const updatedArray = [...items, newItem];
// BAD: Direct mutation
user.name = 'New Name';
items.push(newItem);
其他语言——规则相同,机制不同:
- Python — 切勿修改调用方仍持有的参数,也切勿使用可变默认值(
def f(xs=[])会让多次调用共用一个列表;应使用None并在函数内创建)。固定集合优先使用元组和frozenset;修改副本可使用dataclasses.replace(obj, field=…)。 - Go — 存储切片时,它仍使用调用方的底层数组,因此后续
append可能会写入该数组;保留切片前先复制(slices.Clone)。对于不应修改数据的方法,使用值接收者。 - Rust — 绑定默认不可变,借用检查器会强制执行;
mut是需要说明理由的例外,而不是默认写法。 - Java — 数据载体使用
record,在边界处使用List.copyOf/Map.copyOf创建防御性副本。 - C# — 使用
record类型和with表达式;共享集合使用ImmutableArray/ImmutableList。
错误处理
// GOOD: add context, keep the cause; log once at the boundary
async function fetchData(url: string) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
throw new Error(`Failed to fetch ${url}`, { cause: error });
}
}
// BAD: No handling or swallowed errors
async function fetchData(url: string) {
const response = await fetch(url);
return response.json();
}
其他语言——形式各不相同,但“补充上下文,绝不吞掉错误”的原则不变:
- Python — 抛出特定的异常类型,绝不使用裸
except:(它也会捕获KeyboardInterrupt)。重新抛出时保留异常链:raise ParseError(...) from err。 - Go — 错误是值:检查每个错误,并使用
%w添加上下文,以便errors.Is/errors.As仍能正常工作(fmt.Errorf("fetch %s: %w", url, err))。使用_ =丢弃错误时,必须添加注释说明为何安全。 - Rust — 返回
Result<T, E>并使用?传播错误;在库边界使用带类型的错误枚举(或thiserror),仅在二进制程序中使用anyhow。在库代码中使用unwrap()/expect(),就等于发布了一个会触发 panic 的程序。 - Java — 每种失败模式使用一种异常类型,绝不留下空的
catch;保留原因(throw new X(msg, err))。 - C# — 捕获特定异常,并使用裸
throw;重新抛出(不要使用throw ex;,它会重置堆栈跟踪)。
异步与并发
// GOOD: Parallel when independent
const [users, markets, stats] = await Promise.all([
fetchUsers(),
fetchMarkets(),
fetchStats(),
]);
// BAD: Sequential when unnecessary
const users = await fetchUsers();
const markets = await fetchMarkets();
const stats = await fetchStats();
其他语言——让相互独立的工作并发执行,并确保每个并发单元都有取消机制:
- Python — 对相互独立的 await 使用
asyncio.gather(*coros);当某项失败时应取消其他任务,则使用asyncio.TaskGroup(3.11+)。阻塞调用应放入run_in_executor,绝不在协程中内联执行。 - Go — 对必须作为整体失败的扇出任务使用
errgroup.Group(golang.org/x/sync),对无需作为整体失败的任务使用sync.WaitGroup。任何可能阻塞的函数都要将context.Context作为第一个参数,并将其向下传递,而不是存储在结构体中。 - Rust — 对相互独立的 future 使用
tokio::join!,当第一个错误应中止其他任务时使用try_join!;拆除任务时使用CancellationToken或丢弃JoinHandle。 - Java — 使用
CompletableFuture.allOf(...),或在可用时使用结构化并发(StructuredTaskScope)。 - C# — 使用
Task.WhenAll(...),并在每个异步签名中传递CancellationToken。
类型安全
// GOOD: Explicit types, no any
interface Market {
id: string;
name: string;
status: 'active' | 'resolved' | 'closed';
}
function getMarket(id: string): Promise<Market> { /* ... */ }
// BAD: any or untyped
function getMarket(id: any): Promise<any> { /* ... */ }
其他语言——目标相同:让无效值无法表示,并在边界处检查,而不是在每个调用点反复检查。
- Python — 为所有公共签名添加注解,并在 CI 中以严格模式运行
mypy/pyright;如果没有任何检查器检查类型提示,那它就只是一条注释。在 API 和解析边界使用TypedDict/dataclass/Pydantic 模型,而不是原始的dict[str, Any]。 - Go — 优先使用具体类型而非
any;接受接口,返回结构体。签名中的any表示解析边界,必须立即使用类型分支或errors.As处理。 - Rust — 使用新类型而非裸原始类型(
struct UserId(u64),而不是u64),让编译器捕获参数顺序错误;使用枚举而非依赖字符串表示的状态。 - Java / Kotlin — 使用 Kotlin 的可空类型,或在 Java 中使用
Optional和可空性注解;公共签名中不要使用Object。 - C# — 在整个项目中启用可空引用类型(
<Nullable>enable</Nullable>),并将警告视为错误。
对于没有类型检查器的动态类型语言(纯 JS、Ruby、PHP、Lua),运行时也要完成同样的工作:在信任边界使用 schema 验证,内部调用则无需重复防护。
注释和文档
- 解释原因,而非内容——写“使用指数退避以避免给 API 造成过大压力”,而不是“增加重试计数”。只复述代码的注释是噪声;删除它,或重命名代码,让注释变得多余。
- 使用纯 ASCII 标点——像开发者平时输入那样写注释:用
-,而不是—;用...,而不是…;使用直引号,不用装饰性符号或表情符号。注释中出现排版用符号是 AI 生成的迹象,不符合项目风格。(仅限注释文本——绝不适用于字符串字面量、标识符或数据。) - 注释文本使用朴素动词——注释中绝不使用一份封闭清单里的借用措辞:有朴素动词可用时,不用那些煞有介事的动词。清单及其替代词见
awesome-humanize-en/references/lexicon.md的 Borrowed diction 部分,其中还列出了某些词属于产品自有词汇的领域。即使没有该技能,这条规则仍然适用:有更朴素的动词时,注释就用更朴素的动词。(仅限注释文本——在字符串字面量或产品数据中,这些词属于内容。) - 注释不要复述代码声明的值——注释应说明某个值的用途,而不是它是什么:不要写
border-radius简写各长度的值、带引号的max-height、颜色、超时、与常量相同的裸数字,也不要写源码中已有的选择器、类名或存储键。代码重命名时,注释是唯一不会随之更新的副本。仓库中未声明的值(例如供应商限制或协议固定宽度)可以写明,并说明其固定依据。 - 不要使用分节分隔线——例如
// --- HTTP、// ==== Helpers ====及类似写法:用空行分隔代码块;如果文件需要目录,就应该拆分。 - 注释中的列表顶格书写——标记后空一格,换行后从标记所在位置开始,不要用
//制造悬挂缩进。反映所记录内容实际嵌套关系的缩进属于内容,应保留。 - 不要声称某种选择是有意为之——
deliberately、intentionally、by design只是在断言意图,只有原因才能证明这一点;写明原因,删掉这些词(lexicon.md,Borrowed diction)。 - 文档注释应说明类型未表达的契约——包括引发的错误、副作用、单位、顺序、特殊值(
null、0或空值的含义),以及调用方式不明显时的示例。对于已发布的库或 SDK 接口,应提供更多文档;对于内部辅助函数或普通数据类型,几乎无需文档。只有当@param和@returns补充了名称和类型之外的信息时才使用;重复签名的文档块是噪声。使用语言自身的格式,并遵循项目风格:JSDoc/TSDoc、Python 文档字符串(PEP 257,采用项目使用的 Google/NumPy/reST 风格)、以符号名称开头的 Go 文档注释、带# Examples部分的 Rust///、Javadoc、C# XML 文档注释。 - 不要保留注释掉的代码——删除它,或在工单中说明原因;用版本控制保存历史。
这就是现在编写或修改代码时应达到的标准。若要全面检查仓库中的现有注释——决定删除、精简或修正哪些注释,并处理误报边界及保持行为不变的验证关卡——请使用负责该流程的 awesome-code-cleanup。
文件和项目结构
- 遵循现有布局(例如
src/app/、src/components/、src/lib/)。 - 每个文件最多导出一个主要对象,除非项目使用 barrel 文件或 index 再导出。
- 对导入分组:标准库 → 第三方 → 本地;按字母顺序或项目规定的路径顺序排列。
后端分层与边界(适用时)
- 三模型拆分——将 DTO/API 模型、领域模型和持久化/ORM 模型分开。让同一个
User对象流经传输层、业务逻辑和存储层,会导致 API 结构与表结构耦合,任何重构都要改动每一层。 - 层级放置启发法——需要 HTTP 状态码 → 边界/控制器;需要业务规则 → 服务;需要表/索引/ORM → 仓库。流程保持单向:
controller -> service -> repository -> gateway。 - 在边界处统一处理横切关注点——身份验证、校验、速率限制、请求 ID、日志都应放在 HTTP 流水线中(全局中间件/钩子或路由级设置),不要在每个处理器中手动复制。原则:不要在每个端点中手动重复策略。
- 错误不应了解传输层——服务和仓库抛出领域错误;由一个全局处理器将其映射为状态码。
- 契约优先——OpenAPI(或同等规范)是请求/响应结构的唯一真实来源;据此生成类型化客户端,并在 CI 中检测规范偏差,发现偏差即失败。
// GOOD: service throws a domain error; the global handler maps NotFoundError -> 404
throw new NotFoundError('market', id);
// BAD: business logic reaches into HTTP transport
return res.status(404).json({ error: 'not found' });
本节只负责放置各层。错误封装格式、HTTP 状态码映射和重试策略的深入设计属于 awesome-error-standards 的工作——如果任务是设计错误契约本身,请前往那里。
需要修复的代码异味
| 异味 | 操作 |
|---|---|
| 函数超过约 50 行 | 拆分为名称清晰的较小函数 |
| 嵌套过深(5 层以上) | 使用提前返回或提取函数 |
| 魔法数字 | 提取为命名常量(例如 MAX_RETRIES、DEBOUNCE_MS) |
| 参数列表过长 | 使用选项对象或拆分为较小类型 |
| 两处存在重复逻辑 | 提取为共享函数或模块 |
上述代码异味适用于任何语言。数值阈值只是起点,并非铁律;始终以仓库中有文档记录的规范为准,也不要重复标记 linter 或类型检查器已强制执行的问题。在引入新模式前,先搜索代码库——问题往往已在某处解决;应复用现有方案,而不是再添加一种解决同一问题的方式。
框架特有的正确性(适用时)
这与上述通用代码异味不同:每个框架都有一小份容易踩的陷阱清单,这些写法看似正确,运行时却会失败。应了解当前框架的清单,不要假设其他框架的清单也适用——权威来源是该框架自己的文档(React 的 Hooks 规则和“You Might Not Need an Effect”、Vue 的响应性注意事项、Angular 的变更检测指南、Svelte 的 store 和响应性说明)。
以 React 为例:
| 异味 | 操作 |
|---|---|
JSX 中的 {count && <X/>} | 值为假时会渲染字面量 0/NaN——请使用显式三元表达式 count > 0 ? <X/> : null |
| 在另一个组件内部定义组件 | 将其提取到外部——嵌套定义每次渲染都会创建新类型并重新挂载,导致状态丢失 |
将可从 props/state 推导的状态保存在 useState+useEffect 中 | 在渲染期间推导(或使用带 key 的重置)——无需使用 effect |
可迁移的部分是这类问题的形态,而不是表格中的这三行:假值渲染为可见输出、标识每次渲染都会变化并悄然丢弃状态,以及状态被重复保存而非推导。无论项目使用什么框架,都要留意这类问题。
前端渲染与动效(适用时)
- 只对合成器属性进行动画处理——
transform和opacity。绝不要对布局属性(width、height、top、margin)做动画处理——它们会在每一帧触发回流;位置变化请使用 FLIP 技术。 - 不要在同一帧中交替读取和写入布局——先批量读取(
getBoundingClientRect、offsetWidth),再进行写入,避免布局抖动。 - 对于与滚动关联的动画,优先使用
animation-timeline: view()/scroll(),而不是 JS 滚动事件监听器;谨慎使用will-change,并在之后移除。 - 合理的默认设置:标题使用
text-balance,正文使用text-pretty,数字列使用tabular-nums,优先使用h-dvh而非h-screen,交互响应时间控制在约 200ms 以内,每个视图使用一种强调色,并采用固定的 z-index 层级。
前端架构(适用时)
- 按功能划分文件夹——产品代码(页面、功能组件、状态、功能范围内的 API 适配器、测试)应放在同一个功能/模块文件夹中。共享文件夹只放跨应用的基础组件:设计系统、应用外壳、路由引导、全局配置、国际化。反模式:没有边界的
shared/common/utils大杂烩;庞大的全局components/;为一个一次性文件单独建文件夹。 - 就近放置——重要组件或 hook 应将其测试、故事、样式和
index.ts再导出文件放在旁边。 - 路由级代码拆分是默认的性能优化——延迟加载路由代码块;如果落地页不需要仪表盘级依赖,就不要将它们放进初始包中。手动优化组件前先进行性能分析。(这与上文的合成器/动画规则不同——这里关注的是包结构,而非帧预算。)
- 使用 API 契约,不要重新手写类型——从后端维护的同一份 OpenAPI 规范生成 TypeScript 类型,而不是手动重复定义请求/响应结构。使用服务端状态库(例如 TanStack Query),统一处理加载、错误和重试。集中映射错误(使用单个
parseApiError),并在错误界面中显示服务端的request_id,以便用户报告能与服务端日志对应起来。
检查清单(强制执行时)
- 命名符合项目规范(camelCase/PascalCase/snake_case)
- 不直接修改参数或共享状态
- 处理错误并附带上下文传递
- 避免不必要的
any;在边界处显式声明类型 - 为公共 API 编写文档(使用该语言的文档注释格式或项目标准)
- 文件和结构符合现有布局
- 不使用魔法数字;为常量命名
- 通过 lint 和格式化规则检查(如果项目有相关规则)
反模式
| 反模式 | 更好的做法 |
|---|---|
| “这只是个小脚本” | 使用同样的命名和结构;未来的维护者会感谢你 |
| 将代码注释掉“留待以后” | 删除;使用 git 历史或工单 |
| 只修复范围内的文件 | 如果涉及某种模式,应建议在整个项目中统一规范,或安排后续跟进 |
| 添加没有工具支持的样式规则 | 将规则放入语言的格式化工具和 linter 中,确保格式自动统一(gofmt、ruff/black、rustfmt+clippy、ktlint、dotnet format、ESLint/Prettier),并使用 .editorconfig 处理跨语言的设置 |
集成
- 如果项目有风格指南、CONTRIBUTING 或 lint/格式化配置,请优先遵循。只有在用户明确要求时才覆盖。
- 提出具体的修改建议(重命名、提取函数、添加类型),不要只罗列规则。