fix: 将 code-review-skill 从子模块转为普通目录
This commit is contained in:
Submodule .claude/skills/code-review-skill deleted from 17267ff26b
@@ -0,0 +1,25 @@
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editor files
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Local config
|
||||
.env
|
||||
.env.local
|
||||
@@ -0,0 +1,348 @@
|
||||
# Contributing to AI Code Review Guide
|
||||
|
||||
Thank you for your interest in contributing! This document provides guidelines for contributing to this Claude Code Skill project.
|
||||
|
||||
## Claude Code Skill 开发规范
|
||||
|
||||
本项目是一个 Claude Code Skill,贡献者需要遵循以下规范。
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
code-review-skill/
|
||||
├── SKILL.md # Required: main file (always loaded)
|
||||
├── README.md
|
||||
├── CONTRIBUTING.md
|
||||
├── LICENSE
|
||||
├── reference/ # On-demand language/framework guides
|
||||
│ ├── react.md # React 19 / Next.js / TanStack Query v5
|
||||
│ ├── vue.md # Vue 3.5 Composition API
|
||||
│ ├── angular.md # Angular 17+, Signals, Standalone, RxJS
|
||||
│ ├── svelte.md # Svelte 5 / SvelteKit, runes, SSR boundary
|
||||
│ ├── rust.md # Ownership, async, unsafe, cancellation
|
||||
│ ├── typescript.md # Type safety, generics, strict mode
|
||||
│ ├── nestjs.md # NestJS DI, modules, Guards/Pipes, DTOs
|
||||
│ ├── python.md # Type hints, async, testing
|
||||
│ ├── django.md # Django / DRF, N+1, serializers, async views
|
||||
│ ├── fastapi.md # FastAPI, Depends, Pydantic v2, async
|
||||
│ ├── java.md # Java 17/21, Spring Boot 3, virtual threads
|
||||
│ ├── kotlin.md # Kotlin / Android, coroutines, Flow, Compose
|
||||
│ ├── go.md # Error handling, goroutines, context
|
||||
│ ├── csharp.md # C# / .NET 8, async, EF Core, ASP.NET Core
|
||||
│ ├── php.md # PHP 8.x, types, PDO, security, Composer
|
||||
│ ├── c.md # Memory safety, UB, error handling
|
||||
│ ├── cpp.md # RAII, move semantics, exception safety
|
||||
│ ├── qt.md # Object model, signals/slots, GUI perf
|
||||
│ ├── css-less-sass.md # Variables, responsive, performance
|
||||
│ ├── architecture-review-guide.md # SOLID, anti-patterns, coupling
|
||||
│ ├── performance-review-guide.md # Web Vitals, N+1, complexity
|
||||
│ ├── security-review-guide.md # OWASP Top 10, JWT, validation
|
||||
│ ├── common-bugs-checklist.md # Quick-reference bug patterns
|
||||
│ ├── code-quality-universal.md # Language-agnostic quality anti-patterns
|
||||
│ └── code-review-best-practices.md # Communication & process
|
||||
├── assets/ # Templates and quick reference
|
||||
│ ├── review-checklist.md
|
||||
│ └── pr-review-template.md
|
||||
└── scripts/
|
||||
└── pr-analyzer.py # PR complexity analyzer
|
||||
```
|
||||
|
||||
### Frontmatter 规范
|
||||
|
||||
SKILL.md 必须包含 YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: skill-name
|
||||
description: |
|
||||
功能描述。触发条件说明。
|
||||
Use when [具体使用场景]。
|
||||
allowed-tools: ["Read", "Grep", "Glob"] # 可选:限制工具访问
|
||||
---
|
||||
```
|
||||
|
||||
#### 必需字段
|
||||
|
||||
| 字段 | 说明 | 约束 |
|
||||
|------|------|------|
|
||||
| `name` | Skill 标识符 | 小写字母、数字、连字符;最多 64 字符 |
|
||||
| `description` | 功能和激活条件 | 最多 1024 字符;必须包含 "Use when" |
|
||||
|
||||
#### 可选字段
|
||||
|
||||
| 字段 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `allowed-tools` | 限制工具访问 | `["Read", "Grep", "Glob"]` |
|
||||
|
||||
### 命名约定
|
||||
|
||||
**Skill 名称规则**:
|
||||
- 仅使用小写字母、数字和连字符(kebab-case)
|
||||
- 最多 64 个字符
|
||||
- 避免下划线或大写字母
|
||||
|
||||
```
|
||||
✅ 正确:code-review-skill, typescript-advanced-types
|
||||
❌ 错误:CodeReview, code_review, TYPESCRIPT
|
||||
```
|
||||
|
||||
**文件命名规则**:
|
||||
- reference 文件使用小写:`react.md`, `vue.md`
|
||||
- 多词文件使用连字符:`common-bugs-checklist.md`
|
||||
|
||||
### Description 写法规范
|
||||
|
||||
Description 必须包含两部分:
|
||||
|
||||
1. **功能陈述**:具体说明 Skill 能做什么
|
||||
2. **触发条件**:以 "Use when" 开头,说明何时激活
|
||||
|
||||
```yaml
|
||||
# ✅ 正确示例
|
||||
description: |
|
||||
Provides comprehensive code review guidance for React 19, Vue 3, Rust,
|
||||
TypeScript, Java, Python, and C/C++.
|
||||
Helps catch bugs, improve code quality, and give constructive feedback.
|
||||
Use when reviewing pull requests, conducting PR reviews, establishing
|
||||
review standards, or mentoring developers through code reviews.
|
||||
|
||||
# ❌ 错误示例(太模糊,缺少触发条件)
|
||||
description: |
|
||||
Helps with code review.
|
||||
```
|
||||
|
||||
### Progressive Disclosure(渐进式披露)
|
||||
|
||||
Claude 只在需要时加载支持文件,不会一次性加载所有内容。
|
||||
|
||||
#### 文件职责划分
|
||||
|
||||
| 文件 | 加载时机 | 内容 |
|
||||
|------|----------|------|
|
||||
| `SKILL.md` | 始终加载 | 核心原则、快速索引、何时使用 |
|
||||
| `reference/*.md` | 按需加载 | 语言/框架的详细指南 |
|
||||
| `assets/*.md` | 明确需要时 | 模板、清单 |
|
||||
| `scripts/*.py` | 明确指引时 | 工具脚本 |
|
||||
|
||||
#### 内容组织原则
|
||||
|
||||
**SKILL.md**(~200 行以内):
|
||||
- 简述:2-3 句话说明用途
|
||||
- 核心原则和方法论
|
||||
- 语言/框架索引表(链接到 reference/)
|
||||
- 何时使用此 Skill
|
||||
|
||||
**reference/*.md**(详细内容):
|
||||
- 完整的代码示例
|
||||
- 所有最佳实践
|
||||
- Review Checklist
|
||||
- 边界情况和陷阱
|
||||
|
||||
### 文件引用规范
|
||||
|
||||
在 SKILL.md 中引用其他文件时:
|
||||
|
||||
```markdown
|
||||
# ✅ 正确:使用 Markdown 链接格式
|
||||
| **React** | [React Guide](reference/react.md) | Hooks, React 19, RSC |
|
||||
| **Vue 3** | [Vue Guide](reference/vue.md) | Composition API |
|
||||
|
||||
详见 [React Guide](reference/react.md) 获取完整指南。
|
||||
|
||||
# ❌ 错误:使用代码块格式
|
||||
参考 `reference/react.md` 文件。
|
||||
```
|
||||
|
||||
**路径规则**:
|
||||
- 使用相对路径(相对于 Skill 目录)
|
||||
- 使用正斜杠 `/`,不使用反斜杠
|
||||
- 不需要 `./` 前缀
|
||||
|
||||
### 约定(Conventions)
|
||||
|
||||
**严重级别(severity)**:审查意见统一使用 SKILL.md「Technique 4」的标记方案,三档由红到绿表示优先级:
|
||||
|
||||
- 🔴 `[blocking]` - 合并前必须修复
|
||||
- 🟡 `[important]` - 应当修复,有异议可讨论
|
||||
- 🟢 `[nit]` - 可选优化,不阻塞合并
|
||||
|
||||
新增 reference 指南时请沿用这套标记,不要自创等价的名称(如 critical/warning/suggestion)。
|
||||
|
||||
**语言策略**:现有指南是中英混合的——部分通篇中文,部分(如 fastapi.md、php.md)以英文为主。新增内容时**跟随同一领域既有指南的语言**:改某个指南就用它的语言;新建指南可自行选择中文或英文,但单个文件内部保持一致。
|
||||
|
||||
---
|
||||
|
||||
## 贡献类型
|
||||
|
||||
### 添加新语言支持
|
||||
|
||||
1. 在 `reference/` 目录创建新文件(如 `go.md`)
|
||||
2. 遵循以下结构:
|
||||
|
||||
```markdown
|
||||
# [Language] Code Review Guide
|
||||
|
||||
> 简短描述,一句话说明覆盖内容。
|
||||
|
||||
## 目录
|
||||
- [主题1](#主题1)
|
||||
- [主题2](#主题2)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 主题1
|
||||
|
||||
### 子主题
|
||||
|
||||
```[language]
|
||||
// ❌ Bad pattern - 说明为什么不好
|
||||
bad_code_example()
|
||||
|
||||
// ✅ Good pattern - 说明为什么好
|
||||
good_code_example()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### 类别1
|
||||
- [ ] 检查项 1
|
||||
- [ ] 检查项 2
|
||||
```
|
||||
|
||||
3. 在 `SKILL.md` 的索引表中添加链接
|
||||
4. 更新 `README.md` 的统计信息
|
||||
|
||||
### 添加框架模式
|
||||
|
||||
1. 确保引用官方文档
|
||||
2. 包含版本号(如 "React 19", "Vue 3.5+")
|
||||
3. 提供可运行的代码示例
|
||||
4. 添加对应的 checklist 项
|
||||
|
||||
### 改进现有内容
|
||||
|
||||
- 修复拼写或语法错误
|
||||
- 更新过时的模式(注明版本变化)
|
||||
- 添加边界情况示例
|
||||
- 改进代码示例的清晰度
|
||||
|
||||
---
|
||||
|
||||
## 代码示例规范
|
||||
|
||||
### 格式要求
|
||||
|
||||
```markdown
|
||||
// ❌ 问题描述 - 解释为什么这样做不好
|
||||
problematic_code()
|
||||
|
||||
// ✅ 推荐做法 - 解释为什么这样做更好
|
||||
recommended_code()
|
||||
```
|
||||
|
||||
### 质量标准
|
||||
|
||||
- 示例应基于真实场景,避免人为构造
|
||||
- 同时展示问题和解决方案
|
||||
- 保持示例简洁聚焦
|
||||
- 包含必要的上下文(import 语句等)
|
||||
|
||||
---
|
||||
|
||||
## 提交流程
|
||||
|
||||
### Issue 报告
|
||||
|
||||
- 使用 GitHub Issues 报告问题或建议
|
||||
- 提供清晰的描述和示例
|
||||
- 标注相关的语言/框架
|
||||
|
||||
### Pull Request 流程
|
||||
|
||||
1. Fork 仓库
|
||||
2. 创建功能分支:`git checkout -b feature/add-go-support`
|
||||
3. 进行修改
|
||||
4. 提交(见下文 commit 格式)
|
||||
5. 推送到 fork:`git push origin feature/add-go-support`
|
||||
6. 创建 Pull Request
|
||||
|
||||
### Commit 消息格式
|
||||
|
||||
```
|
||||
类型: 简短描述
|
||||
|
||||
详细说明(如需要)
|
||||
|
||||
- 具体变更 1
|
||||
- 具体变更 2
|
||||
```
|
||||
|
||||
**类型**:
|
||||
- `feat`: 新功能或新内容
|
||||
- `fix`: 修复错误
|
||||
- `docs`: 仅文档变更
|
||||
- `refactor`: 重构(不改变功能)
|
||||
- `chore`: 维护性工作
|
||||
|
||||
**示例**:
|
||||
```
|
||||
feat: 添加 Go 语言代码审查指南
|
||||
|
||||
- 新增 reference/go.md
|
||||
- 覆盖错误处理、并发、接口设计
|
||||
- 更新 SKILL.md 索引表
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Skill 设计原则
|
||||
|
||||
### 单一职责
|
||||
|
||||
每个 Skill 专注一个核心能力。本 Skill 专注于**代码审查**,不应扩展到:
|
||||
- 代码生成
|
||||
- 项目初始化
|
||||
- 部署配置
|
||||
|
||||
### 版本管理
|
||||
|
||||
- 在 reference 文件中标注框架/语言版本
|
||||
- 更新时在 commit 中说明版本变化
|
||||
- 过时内容应更新而非删除(除非完全废弃)
|
||||
|
||||
### 内容质量
|
||||
|
||||
- 所有建议应有依据(官方文档、最佳实践)
|
||||
- 避免主观偏好(如代码风格),专注于客观问题
|
||||
- 优先覆盖常见陷阱和安全问题
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何测试我的更改?
|
||||
|
||||
将修改后的 Skill 复制到 `~/.claude/skills/` 目录,然后在 Claude Code 中测试:
|
||||
```bash
|
||||
cp -r code-review-skill ~/.claude/skills/code-review-skill
|
||||
```
|
||||
|
||||
### Q: 我应该更新 SKILL.md 还是 reference 文件?
|
||||
|
||||
- **SKILL.md**:只修改索引表或核心原则
|
||||
- **reference/*.md**:添加/更新具体的语言或框架内容
|
||||
|
||||
### Q: 如何处理过时的内容?
|
||||
|
||||
1. 标注版本变化(如 "React 18 → React 19")
|
||||
2. 保留旧版本内容(如果仍有用户使用)
|
||||
3. 在 checklist 中更新相关项
|
||||
|
||||
---
|
||||
|
||||
## 问题咨询
|
||||
|
||||
如有任何问题,欢迎在 GitHub Issues 中提问。
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 tt-a1i
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,658 @@
|
||||
<div align="center">
|
||||
|
||||
<h1>🔍 Code Review Skill</h1>
|
||||
|
||||
<p>
|
||||
<strong>A comprehensive, modular code review skill for Claude Code</strong><br/>
|
||||
<strong>面向 Claude Code 的全面模块化代码审查技能</strong>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/awesome-skills/code-review-skill/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/badge/License-MIT-22c55e?style=flat-square" alt="License: MIT"/>
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/Claude_Code-Skill-7c3aed?style=flat-square&logo=anthropic&logoColor=white" alt="Claude Code Skill"/>
|
||||
<img src="https://img.shields.io/badge/Total_Lines-16%2C000%2B-3b82f6?style=flat-square" alt="16000+ lines"/>
|
||||
<img src="https://img.shields.io/badge/Languages-20%2B-f59e0b?style=flat-square" alt="20+ languages"/>
|
||||
<img src="https://img.shields.io/badge/PRs-Welcome-ec4899?style=flat-square" alt="PRs Welcome"/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="#english">English</a>
|
||||
·
|
||||
<a href="#chinese">中文</a>
|
||||
·
|
||||
<a href="./CONTRIBUTING.md">Contributing</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
<a name="english"></a>
|
||||
|
||||
## English
|
||||
|
||||
### What is this?
|
||||
|
||||
**Code Review Skill** is a production-ready skill for [Claude Code](https://claude.ai/code) that transforms AI-assisted code review from vague suggestions into a **structured, consistent, and expert-level** process.
|
||||
|
||||
It covers **20+ languages and frameworks** with over **16,000 lines** of carefully curated review guidelines — loaded progressively to minimize context window usage.
|
||||
|
||||
---
|
||||
|
||||
### ✨ Key Features
|
||||
|
||||
- **Progressive Disclosure** — Core skill is ~190 lines; language guides (~200–1,000 lines each) load only when needed.
|
||||
- **Four-Phase Review Process** — Structured workflow from understanding scope to delivering clear feedback.
|
||||
- **Severity Labeling** — Every finding is categorized: `blocking` · `important` · `nit` · `suggestion` · `learning` · `praise`
|
||||
- **Security-First** — Dedicated security checklists per language ecosystem.
|
||||
- **Collaborative Tone** — Questions over commands, suggestions over mandates.
|
||||
- **Automation Awareness** — Clearly separates what human review should catch vs. what linters handle.
|
||||
|
||||
---
|
||||
|
||||
### 🌐 Supported Languages & Frameworks
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Category</th>
|
||||
<th>Technology</th>
|
||||
<th>Guide</th>
|
||||
<th>Lines</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="6"><strong>Frontend</strong></td>
|
||||
<td>⚛️ React 19 / Next.js / TanStack Query v5</td>
|
||||
<td><code>reference/react.md</code></td>
|
||||
<td>~870</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>💚 Vue 3.5 + Composition API</td>
|
||||
<td><code>reference/vue.md</code></td>
|
||||
<td>~920</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🔮 Angular 17+ / Signals / Zoneless</td>
|
||||
<td><code>reference/angular.md</code></td>
|
||||
<td>~420</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🔥 Svelte 5 / SvelteKit</td>
|
||||
<td><code>reference/svelte.md</code></td>
|
||||
<td>~1,060</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🎨 CSS / Less / Sass</td>
|
||||
<td><code>reference/css-less-sass.md</code></td>
|
||||
<td>~660</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🔷 TypeScript</td>
|
||||
<td><code>reference/typescript.md</code></td>
|
||||
<td>~540</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="9"><strong>Backend</strong></td>
|
||||
<td>☕ Java 17/21 + Spring Boot 3</td>
|
||||
<td><code>reference/java.md</code></td>
|
||||
<td>~410</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>⚡ FastAPI</td>
|
||||
<td><code>reference/fastapi.md</code></td>
|
||||
<td>~590</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PHP 8.x</td>
|
||||
<td><code>reference/php.md</code></td>
|
||||
<td>~700</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>📦 NestJS</td>
|
||||
<td><code>reference/nestjs.md</code></td>
|
||||
<td>~590</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🐍 Django / DRF</td>
|
||||
<td><code>reference/django.md</code></td>
|
||||
<td>~1,030</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🐹 Go</td>
|
||||
<td><code>reference/go.md</code></td>
|
||||
<td>~990</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🦀 Rust</td>
|
||||
<td><code>reference/rust.md</code></td>
|
||||
<td>~840</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>💻 C# / .NET 8</td>
|
||||
<td><code>reference/csharp.md</code></td>
|
||||
<td>~520</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🐍 Python</td>
|
||||
<td><code>reference/python.md</code></td>
|
||||
<td>~1,070</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="5"><strong>Mobile / Systems</strong></td>
|
||||
<td>📱 Kotlin / Android</td>
|
||||
<td><code>reference/kotlin.md</code></td>
|
||||
<td>~1,020</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🍎 Swift / SwiftUI</td>
|
||||
<td><code>reference/swift.md</code></td>
|
||||
<td>~930</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>⚙️ C</td>
|
||||
<td><code>reference/c.md</code></td>
|
||||
<td>~290</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🔩 C++</td>
|
||||
<td><code>reference/cpp.md</code></td>
|
||||
<td>~390</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🖥️ Qt Framework</td>
|
||||
<td><code>reference/qt.md</code></td>
|
||||
<td>~190</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="3"><strong>Cross-Cutting</strong></td>
|
||||
<td>🏛️ Architecture Design Review</td>
|
||||
<td><code>reference/architecture-review-guide.md</code></td>
|
||||
<td>~470</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>⚡ Performance Review</td>
|
||||
<td><code>reference/performance-review-guide.md</code></td>
|
||||
<td>~820</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🔍 Universal Quality Anti-Patterns</td>
|
||||
<td><code>reference/code-quality-universal.md</code></td>
|
||||
<td>~490</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
---
|
||||
|
||||
### 🔄 The Four-Phase Review Process
|
||||
|
||||
```
|
||||
Phase 1 - Context Gathering
|
||||
Understand PR scope, linked issues, and intent
|
||||
|
|
||||
v
|
||||
Phase 2 - High-Level Review
|
||||
Architecture - Performance impact - Test strategy
|
||||
|
|
||||
v
|
||||
Phase 3 - Line-by-Line Analysis
|
||||
Logic - Security - Maintainability - Edge cases
|
||||
|
|
||||
v
|
||||
Phase 4 - Summary & Decision
|
||||
Structured feedback - Approval status - Action items
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🏷️ Severity Labels
|
||||
|
||||
| Label | Meaning |
|
||||
|-------|---------|
|
||||
| 🔴 `blocking` | Must be fixed before merge |
|
||||
| 🟠 `important` | Should be fixed; may block depending on context |
|
||||
| 🟡 `nit` | Minor style or preference issue |
|
||||
| 🔵 `suggestion` | Optional improvement worth considering |
|
||||
| 📚 `learning` | Educational note for the author |
|
||||
| 🌟 `praise` | Explicitly highlight great work |
|
||||
|
||||
---
|
||||
|
||||
### 📁 Repository Structure
|
||||
|
||||
```
|
||||
code-review-skill/
|
||||
|
|
||||
+-- SKILL.md # Core skill - loaded on activation (~190 lines)
|
||||
+-- README.md
|
||||
+-- LICENSE
|
||||
+-- CONTRIBUTING.md
|
||||
|
|
||||
+-- reference/ # On-demand language guides
|
||||
| +-- react.md # React 19 / Next.js / TanStack Query v5
|
||||
| +-- vue.md # Vue 3.5 Composition API
|
||||
| +-- angular.md # Angular 17+ / Signals / Zoneless
|
||||
| +-- svelte.md # Svelte 5 / SvelteKit
|
||||
| +-- rust.md # Rust ownership, async/await, unsafe
|
||||
| +-- typescript.md # TypeScript strict mode, generics, ESLint
|
||||
| +-- nestjs.md # NestJS DI, Guards, Interceptors, DTOs
|
||||
| +-- java.md # Java 17/21 & Spring Boot 3
|
||||
| +-- php.md # PHP 8.x types, PDO, security, Composer
|
||||
| +-- python.md # Python async, typing, pytest
|
||||
| +-- django.md # Django / DRF security, serializers, async
|
||||
| +-- fastapi.md # FastAPI Depends, Pydantic v2, async, test-driven verification
|
||||
| +-- go.md # Go goroutines, channels, context, interfaces
|
||||
| +-- kotlin.md # Kotlin / Android coroutines, Compose, Flow
|
||||
| +-- swift.md # Swift 5.9+/6, SwiftUI, concurrency, optionals
|
||||
| +-- csharp.md # C# 12 / .NET 8, EF Core, ASP.NET Core
|
||||
| +-- c.md # C memory safety, UB, error handling
|
||||
| +-- cpp.md # C++ RAII, move semantics, exception safety
|
||||
| +-- qt.md # Qt object model, signals/slots, GUI perf
|
||||
| +-- css-less-sass.md # CSS/Less/Sass variables, responsive design
|
||||
| +-- architecture-review-guide.md # SOLID, anti-patterns, coupling/cohesion
|
||||
| +-- code-quality-universal.md # Reuse audit, parameter sprawl, TOCTOU, no-op updates
|
||||
| +-- performance-review-guide.md # Core Web Vitals, N+1, memory leaks
|
||||
| +-- security-review-guide.md # Security checklist (all languages)
|
||||
| +-- common-bugs-checklist.md # Language-specific bug patterns
|
||||
| +-- code-review-best-practices.md # Communication & process guidelines
|
||||
|
|
||||
+-- assets/
|
||||
| +-- review-checklist.md # Quick reference checklist
|
||||
| +-- pr-review-template.md # PR review comment template
|
||||
|
|
||||
+-- scripts/
|
||||
+-- pr-analyzer.py # PR complexity analyzer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🚀 Installation
|
||||
|
||||
**Clone to your Claude Code skills directory:**
|
||||
|
||||
```bash
|
||||
# macOS / Linux
|
||||
git clone https://github.com/awesome-skills/code-review-skill.git \
|
||||
~/.claude/skills/code-review-skill
|
||||
|
||||
# Windows (PowerShell)
|
||||
git clone https://github.com/awesome-skills/code-review-skill.git `
|
||||
"$env:USERPROFILE\.claude\skills\code-review-skill"
|
||||
```
|
||||
|
||||
**Or add to an existing plugin:**
|
||||
|
||||
```bash
|
||||
cp -r code-review-skill ~/.claude/plugins/your-plugin/skills/code-review/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 💡 Usage
|
||||
|
||||
Once installed, activate the skill in your Claude Code session:
|
||||
|
||||
```
|
||||
Use code-review-skill to review this PR
|
||||
```
|
||||
|
||||
Or create a custom slash command in `.claude/commands/`:
|
||||
|
||||
```markdown
|
||||
<!-- .claude/commands/review.md -->
|
||||
Use code-review-skill to perform a thorough review of the changes in this PR.
|
||||
Focus on: security, performance, and maintainability.
|
||||
```
|
||||
|
||||
**Example prompts:**
|
||||
|
||||
| Prompt | What happens |
|
||||
|--------|-------------|
|
||||
| `Review this React component` | Loads `react.md` - checks hooks, Server Components, Suspense patterns |
|
||||
| `Review this Java PR` | Loads `java.md` - checks virtual threads, JPA, Spring Boot 3 patterns |
|
||||
| `Security review of this Go service` | Loads `go.md` + `security-review-guide.md` |
|
||||
| `Architecture review` | Loads `architecture-review-guide.md` - SOLID, anti-patterns, coupling |
|
||||
| `Performance review` | Loads `performance-review-guide.md` - Web Vitals, N+1, complexity |
|
||||
|
||||
---
|
||||
|
||||
### 🔬 Highlights by Language
|
||||
|
||||
<details>
|
||||
<summary><strong>⚛️ React 19</strong></summary>
|
||||
|
||||
- `useActionState` - Unified form state management
|
||||
- `useFormStatus` - Access parent form status without prop drilling
|
||||
- `useOptimistic` - Optimistic UI updates with automatic rollback
|
||||
- Server Components & Server Actions patterns (Next.js 15+)
|
||||
- Suspense boundary design, Error Boundary integration, streaming SSR
|
||||
- `use()` Hook for consuming Promises
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>☕ Java & Spring Boot 3</strong></summary>
|
||||
|
||||
- **Java 17/21**: Records, Pattern Matching for Switch, Text Blocks, Sealed Classes
|
||||
- **Virtual Threads** (Project Loom): High-throughput I/O patterns
|
||||
- **Spring Boot 3**: Constructor injection, `@ConfigurationProperties`, `ProblemDetail`
|
||||
- **JPA Performance**: Solving N+1, correct `equals`/`hashCode` on Entities
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>🦀 Rust</strong></summary>
|
||||
|
||||
- Ownership patterns and common pitfalls
|
||||
- `unsafe` code review requirements (mandatory `SAFETY` comments)
|
||||
- Async/await - avoiding blocking in async context, cancellation safety
|
||||
- Error handling: `thiserror` for libraries, `anyhow` for applications
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>🐹 Go</strong></summary>
|
||||
|
||||
- Goroutine lifecycle management and leak prevention
|
||||
- Channel patterns, select usage
|
||||
- `context.Context` propagation
|
||||
- Interface design (accept interfaces, return structs)
|
||||
- Error wrapping with `%w`
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>⚙️ C / C++</strong></summary>
|
||||
|
||||
- **C**: Pointer/buffer safety, undefined behavior, resource cleanup, integer overflow
|
||||
- **C++**: RAII ownership, Rule of 0/3/5, move semantics, exception safety, `noexcept`
|
||||
- **Qt**: Object parent/child memory model, thread-safe signal/slot connections, GUI performance
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 🤝 Contributing
|
||||
|
||||
Contributions are welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
|
||||
|
||||
**Ideas:**
|
||||
- New language guides (Ruby, Elixir, Scala...)
|
||||
- Framework-specific guides (Laravel, Spring WebFlux...)
|
||||
- Additional checklists and templates
|
||||
- Translations of core documentation
|
||||
|
||||
---
|
||||
|
||||
### 📄 License
|
||||
|
||||
MIT © [awesome-skills](https://github.com/awesome-skills)
|
||||
|
||||
---
|
||||
|
||||
<a name="chinese"></a>
|
||||
|
||||
## 中文
|
||||
|
||||
### 这是什么?
|
||||
|
||||
**Code Review Skill** 是专为 [Claude Code](https://claude.ai/code) 打造的生产级代码审查技能,将 AI 辅助的代码审查从模糊建议转变为**结构化、一致且专业级**的流程。
|
||||
|
||||
覆盖 **20+ 种语言和框架**,拥有超过 **16,000 行**精心整理的代码审查指南——按需加载,最大程度减少上下文占用。
|
||||
|
||||
---
|
||||
|
||||
### ✨ 核心特性
|
||||
|
||||
- **渐进式加载** — 核心技能仅 ~190 行,各语言指南(每份 200–1,000 行)仅在需要时才加载。
|
||||
- **四阶段审查流程** — 从理解 PR 范围到输出清晰反馈,每一步都有规可循。
|
||||
- **严重性标记** — 每条发现均分级:`blocking` · `important` · `nit` · `suggestion` · `learning` · `praise`
|
||||
- **安全优先** — 每个语言生态均配备专属安全检查清单。
|
||||
- **协作式语气** — 以提问替代命令,以建议替代指令。
|
||||
- **自动化感知** — 明确区分人工审查应关注的内容与 linter 自动处理的内容。
|
||||
|
||||
---
|
||||
|
||||
### 🌐 支持的语言与框架
|
||||
|
||||
| 分类 | 技术栈 | 指南文件 | 行数 |
|
||||
|------|--------|----------|------|
|
||||
| **前端** | ⚛️ React 19 / Next.js / TanStack Query v5 | `reference/react.md` | ~870 |
|
||||
| | 💚 Vue 3.5 Composition API | `reference/vue.md` | ~920 |
|
||||
| | 🔮 Angular 17+ / Signals / Zoneless | `reference/angular.md` | ~420 |
|
||||
| | 🔥 Svelte 5 / SvelteKit | `reference/svelte.md` | ~1,060 |
|
||||
| | 🎨 CSS / Less / Sass | `reference/css-less-sass.md` | ~660 |
|
||||
| | 🔷 TypeScript | `reference/typescript.md` | ~540 |
|
||||
| **后端** | ☕ Java 17/21 + Spring Boot 3 | `reference/java.md` | ~410 |
|
||||
| | ⚡ FastAPI | `reference/fastapi.md` | ~590 |
|
||||
| | PHP 8.x | `reference/php.md` | ~700 |
|
||||
| | 📦 NestJS | `reference/nestjs.md` | ~590 |
|
||||
| | 🐍 Django / DRF | `reference/django.md` | ~1,030 |
|
||||
| | 🐍 Python | `reference/python.md` | ~1,070 |
|
||||
| | 🐹 Go | `reference/go.md` | ~990 |
|
||||
| | 🦀 Rust | `reference/rust.md` | ~840 |
|
||||
| | 💻 C# / .NET 8 | `reference/csharp.md` | ~520 |
|
||||
| **移动 / 系统** | 📱 Kotlin / Android | `reference/kotlin.md` | ~1,020 |
|
||||
| | 🍎 Swift / SwiftUI | `reference/swift.md` | ~930 |
|
||||
| | ⚙️ C | `reference/c.md` | ~290 |
|
||||
| | 🔩 C++ | `reference/cpp.md` | ~390 |
|
||||
| | 🖥️ Qt 框架 | `reference/qt.md` | ~190 |
|
||||
| **架构** | 🏛️ 架构设计审查 | `reference/architecture-review-guide.md` | ~470 |
|
||||
| | ⚡ 性能审查 | `reference/performance-review-guide.md` | ~820 |
|
||||
| | 🔍 通用质量反模式 | `reference/code-quality-universal.md` | ~490 |
|
||||
|
||||
---
|
||||
|
||||
### 🔄 四阶段审查流程
|
||||
|
||||
```
|
||||
阶段一 - 上下文收集
|
||||
理解 PR 范围、关联 Issue 和实现意图
|
||||
|
|
||||
v
|
||||
阶段二 - 高层级审查
|
||||
架构设计 - 性能影响 - 测试策略
|
||||
|
|
||||
v
|
||||
阶段三 - 逐行深度分析
|
||||
逻辑正确性 - 安全漏洞 - 可维护性 - 边界情况
|
||||
|
|
||||
v
|
||||
阶段四 - 总结与决策
|
||||
结构化反馈 - 审批状态 - 后续行动项
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🏷️ 严重性标记说明
|
||||
|
||||
| 标记 | 含义 |
|
||||
|------|------|
|
||||
| 🔴 `blocking` | 合并前必须修复 |
|
||||
| 🟠 `important` | 应当修复,视情况可能阻塞合并 |
|
||||
| 🟡 `nit` | 风格或偏好上的小问题 |
|
||||
| 🔵 `suggestion` | 值得考虑的可选优化 |
|
||||
| 📚 `learning` | 给作者的教育性说明 |
|
||||
| 🌟 `praise` | 明确表扬优秀代码 |
|
||||
|
||||
---
|
||||
|
||||
### 📁 仓库结构
|
||||
|
||||
```
|
||||
code-review-skill/
|
||||
|
|
||||
+-- SKILL.md # 核心技能,激活时加载(~190 行)
|
||||
+-- README.md
|
||||
+-- LICENSE
|
||||
+-- CONTRIBUTING.md
|
||||
|
|
||||
+-- reference/ # 按需加载的语言指南
|
||||
| +-- react.md # React 19 / Next.js / TanStack Query v5
|
||||
| +-- vue.md # Vue 3.5 组合式 API
|
||||
| +-- angular.md # Angular 17+ / Signals / Zoneless
|
||||
| +-- svelte.md # Svelte 5 / SvelteKit
|
||||
| +-- rust.md # Rust 所有权、async/await、unsafe
|
||||
| +-- typescript.md # TypeScript strict 模式、泛型、ESLint
|
||||
| +-- nestjs.md # NestJS 依赖注入、Guard、Interceptor、DTO
|
||||
| +-- java.md # Java 17/21 & Spring Boot 3
|
||||
| +-- php.md # PHP 8.x 类型、PDO、安全、Composer
|
||||
| +-- python.md # Python async、类型注解、pytest
|
||||
| +-- django.md # Django / DRF 安全、Serializer、异步视图
|
||||
| +-- fastapi.md # FastAPI Depends、Pydantic v2、异步、测试驱动验证
|
||||
| +-- go.md # Go goroutine、channel、context、接口
|
||||
| +-- kotlin.md # Kotlin / Android 协程、Compose、Flow
|
||||
| +-- swift.md # Swift 5.9+/6、SwiftUI、并发、可选值
|
||||
| +-- csharp.md # C# 12 / .NET 8、EF Core、ASP.NET Core
|
||||
| +-- c.md # C 内存安全、UB、错误处理
|
||||
| +-- cpp.md # C++ RAII、移动语义、异常安全
|
||||
| +-- qt.md # Qt 对象模型、信号/槽、GUI 性能
|
||||
| +-- css-less-sass.md # CSS/Less/Sass 变量、响应式设计
|
||||
| +-- architecture-review-guide.md # SOLID、反模式、耦合度分析
|
||||
| +-- code-quality-universal.md # 复用审查、参数膨胀、抽象泄漏、TOCTOU
|
||||
| +-- performance-review-guide.md # Core Web Vitals、N+1、内存泄漏
|
||||
| +-- security-review-guide.md # 安全审查清单(全语言通用)
|
||||
| +-- common-bugs-checklist.md # 各语言常见 Bug 模式
|
||||
| +-- code-review-best-practices.md # 沟通与流程最佳实践
|
||||
|
|
||||
+-- assets/
|
||||
| +-- review-checklist.md # 快速参考清单
|
||||
| +-- pr-review-template.md # PR 审查评论模板
|
||||
|
|
||||
+-- scripts/
|
||||
+-- pr-analyzer.py # PR 复杂度分析工具
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🚀 安装方法
|
||||
|
||||
**克隆到 Claude Code skills 目录:**
|
||||
|
||||
```bash
|
||||
# macOS / Linux
|
||||
git clone https://github.com/awesome-skills/code-review-skill.git \
|
||||
~/.claude/skills/code-review-skill
|
||||
|
||||
# Windows(PowerShell)
|
||||
git clone https://github.com/awesome-skills/code-review-skill.git `
|
||||
"$env:USERPROFILE\.claude\skills\code-review-skill"
|
||||
```
|
||||
|
||||
**或添加到现有插件:**
|
||||
|
||||
```bash
|
||||
cp -r code-review-skill ~/.claude/plugins/your-plugin/skills/code-review/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 💡 使用方式
|
||||
|
||||
安装后,在 Claude Code 会话中激活技能:
|
||||
|
||||
```
|
||||
Use code-review-skill to review this PR
|
||||
```
|
||||
|
||||
或在 `.claude/commands/` 中创建自定义斜杠命令:
|
||||
|
||||
```markdown
|
||||
<!-- .claude/commands/review.md -->
|
||||
使用 code-review-skill 对这次 PR 的变更进行全面审查。
|
||||
重点关注:安全性、性能和可维护性。
|
||||
```
|
||||
|
||||
**示例提示词:**
|
||||
|
||||
| 提示词 | 效果 |
|
||||
|--------|------|
|
||||
| `审查这个 React 组件` | 加载 `react.md`,检查 Hooks、Server Components、Suspense |
|
||||
| `审查这个 Java PR` | 加载 `java.md`,检查虚拟线程、JPA、Spring Boot 3 |
|
||||
| `对这个 Go 服务进行安全审查` | 加载 `go.md` + `security-review-guide.md` |
|
||||
| `架构审查` | 加载 `architecture-review-guide.md`,检查 SOLID 与反模式 |
|
||||
| `性能审查` | 加载 `performance-review-guide.md`,分析 Web Vitals、N+1 等 |
|
||||
|
||||
---
|
||||
|
||||
### 🔬 各语言核心内容
|
||||
|
||||
<details>
|
||||
<summary><strong>⚛️ React 19</strong></summary>
|
||||
|
||||
- `useActionState` — 统一的表单状态管理
|
||||
- `useFormStatus` — 无需 props 透传即可访问父表单状态
|
||||
- `useOptimistic` — 带自动回滚的乐观 UI 更新
|
||||
- Server Components & Server Actions(Next.js 15+)
|
||||
- Suspense 边界设计、Error Boundary 集成、流式 SSR
|
||||
- `use()` Hook 消费 Promise
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>☕ Java & Spring Boot 3</strong></summary>
|
||||
|
||||
- **Java 17/21**:Records、Switch 模式匹配、文本块、Sealed Classes
|
||||
- **虚拟线程**(Project Loom):高吞吐量 I/O 模式
|
||||
- **Spring Boot 3**:构造器注入、`@ConfigurationProperties`、`ProblemDetail`
|
||||
- **JPA 性能**:解决 N+1、Entity 正确的 `equals`/`hashCode` 实现
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>🦀 Rust</strong></summary>
|
||||
|
||||
- 所有权模式与常见陷阱
|
||||
- `unsafe` 代码审查要求(必须有 `SAFETY` 注释)
|
||||
- Async/await — 避免在异步上下文中阻塞,取消安全性
|
||||
- 错误处理:库用 `thiserror`,应用用 `anyhow`
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>🐹 Go</strong></summary>
|
||||
|
||||
- Goroutine 生命周期管理与泄漏预防
|
||||
- Channel 模式、select 用法
|
||||
- `context.Context` 传播规范
|
||||
- 接口设计原则(接受接口,返回结构体)
|
||||
- 错误包装:使用 `%w`
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>⚙️ C / C++</strong></summary>
|
||||
|
||||
- **C**:指针/缓冲区安全、未定义行为、资源清理、整数溢出
|
||||
- **C++**:RAII 所有权、Rule of 0/3/5、移动语义、异常安全、`noexcept`
|
||||
- **Qt**:父子内存模型、线程安全的信号/槽连接、GUI 性能优化
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 🤝 参与贡献
|
||||
|
||||
欢迎贡献!请查阅 [CONTRIBUTING.md](./CONTRIBUTING.md) 了解规范。
|
||||
|
||||
**可贡献方向:**
|
||||
- 新增语言指南(Ruby、Elixir、Scala...)
|
||||
- 框架专属指南(Laravel、Spring WebFlux...)
|
||||
- 补充检查清单和审查模板
|
||||
- 核心文档的多语言翻译
|
||||
|
||||
---
|
||||
|
||||
### 📄 开源协议
|
||||
|
||||
MIT © [awesome-skills](https://github.com/awesome-skills)
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
Made with ❤️ for developers who care about code quality
|
||||
</div>
|
||||
@@ -0,0 +1,220 @@
|
||||
---
|
||||
name: code-review-skill
|
||||
description: |
|
||||
Provides comprehensive code review guidance for React 19, Vue 3, Angular 17+, Svelte 5, Rust, TypeScript, Java, PHP, Python, Django, Go, C#/.NET, Kotlin, Swift, NestJS, C/C++, and more.
|
||||
Helps catch bugs, improve code quality, and give constructive feedback.
|
||||
Use when: reviewing pull requests, conducting PR reviews, code review, reviewing code changes,
|
||||
establishing review standards, mentoring developers, architecture reviews, security audits,
|
||||
checking code quality, finding bugs, giving feedback on code.
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Grep
|
||||
- Glob
|
||||
- Bash # 运行 lint/test/build 命令验证代码质量
|
||||
- WebFetch # 查阅最新文档和最佳实践
|
||||
---
|
||||
|
||||
# Code Review Skill
|
||||
|
||||
Transform code reviews from gatekeeping to knowledge sharing through constructive feedback, systematic analysis, and collaborative improvement.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Reviewing pull requests and code changes
|
||||
- Establishing code review standards for teams
|
||||
- Mentoring junior developers through reviews
|
||||
- Conducting architecture reviews
|
||||
- Creating review checklists and guidelines
|
||||
- Improving team collaboration
|
||||
- Reducing code review cycle time
|
||||
- Maintaining code quality standards
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. The Review Mindset
|
||||
|
||||
**Goals of Code Review:**
|
||||
- Catch bugs and edge cases
|
||||
- Ensure code maintainability
|
||||
- Share knowledge across team
|
||||
- Enforce coding standards
|
||||
- Improve design and architecture
|
||||
- Build team culture
|
||||
|
||||
**Not the Goals:**
|
||||
- Show off knowledge
|
||||
- Nitpick formatting (use linters)
|
||||
- Block progress unnecessarily
|
||||
- Rewrite to your preference
|
||||
|
||||
### 2. Effective Feedback
|
||||
|
||||
**Good Feedback is:**
|
||||
- Specific and actionable
|
||||
- Educational, not judgmental
|
||||
- Focused on the code, not the person
|
||||
- Balanced (praise good work too)
|
||||
- Prioritized (critical vs nice-to-have)
|
||||
|
||||
```markdown
|
||||
❌ Bad: "This is wrong."
|
||||
✅ Good: "This could cause a race condition when multiple users
|
||||
access simultaneously. Consider using a mutex here."
|
||||
|
||||
❌ Bad: "Why didn't you use X pattern?"
|
||||
✅ Good: "Have you considered the Repository pattern? It would
|
||||
make this easier to test. Here's an example: [link]"
|
||||
|
||||
❌ Bad: "Rename this variable."
|
||||
✅ Good: "[nit] Consider `userCount` instead of `uc` for
|
||||
clarity. Not blocking if you prefer to keep it."
|
||||
```
|
||||
|
||||
### 3. Review Scope
|
||||
|
||||
**What to Review:**
|
||||
- Logic correctness and edge cases
|
||||
- Security vulnerabilities
|
||||
- Performance implications
|
||||
- Test coverage and quality
|
||||
- Error handling
|
||||
- Documentation and comments
|
||||
- API design and naming
|
||||
- Architectural fit
|
||||
|
||||
**What Not to Review Manually:**
|
||||
- Code formatting (use Prettier, Black, etc.)
|
||||
- Import organization
|
||||
- Linting violations
|
||||
- Simple typos
|
||||
|
||||
## Review Process
|
||||
|
||||
### Phase 1: Context Gathering (2-3 minutes)
|
||||
|
||||
Before diving into code, understand:
|
||||
1. Read PR description and linked issue
|
||||
2. Check PR size (>400 lines? Ask to split)
|
||||
3. Review CI/CD status (tests passing?)
|
||||
4. Understand the business requirement
|
||||
5. Note any relevant architectural decisions
|
||||
|
||||
> For large diffs, pipe the diff through [`scripts/pr-analyzer.py`](scripts/pr-analyzer.py) (`git diff main...HEAD | python scripts/pr-analyzer.py`) to triage complexity and get a suggested review approach before reading.
|
||||
|
||||
### Phase 2: High-Level Review (5-10 minutes)
|
||||
|
||||
1. **Architecture & Design** - Does the solution fit the problem?
|
||||
- For significant changes, consult [Architecture Review Guide](reference/architecture-review-guide.md)
|
||||
- Check: SOLID principles, coupling/cohesion, anti-patterns
|
||||
2. **Performance Assessment** - Are there performance concerns?
|
||||
- For performance-critical code, consult [Performance Review Guide](reference/performance-review-guide.md)
|
||||
- Check: Algorithm complexity, N+1 queries, memory usage
|
||||
3. **File Organization** - Are new files in the right places?
|
||||
4. **Testing Strategy** - Are there tests covering edge cases?
|
||||
|
||||
### Phase 3: Line-by-Line Review (10-20 minutes)
|
||||
|
||||
For each file, check:
|
||||
- **Logic & Correctness** - Edge cases, off-by-one, null checks, race conditions
|
||||
- **Security** - Input validation, injection risks, XSS, sensitive data
|
||||
- **Performance** - N+1 queries, unnecessary loops, memory leaks
|
||||
- **Maintainability** - Clear names, single responsibility, comments
|
||||
- **Reuse** - Before accepting new code, search for existing utilities/helpers that could replace it. Check adjacent files and shared modules for similar patterns. See [Universal Quality Guide](reference/code-quality-universal.md) for anti-patterns like parameter sprawl, leaky abstractions, nested conditionals, stringly-typed code, TOCTOU, and no-op updates.
|
||||
|
||||
### Phase 4: Summary & Decision (2-3 minutes)
|
||||
|
||||
1. Summarize key concerns
|
||||
2. Highlight what you liked
|
||||
3. Make clear decision:
|
||||
- ✅ Approve
|
||||
- 💬 Comment (minor suggestions)
|
||||
- 🔄 Request Changes (must address)
|
||||
4. Offer to pair if complex
|
||||
|
||||
## Review Techniques
|
||||
|
||||
### Technique 1: The Checklist Method
|
||||
|
||||
Use checklists for consistent reviews. See [Security Review Guide](reference/security-review-guide.md) for comprehensive security checklist.
|
||||
|
||||
### Technique 2: The Question Approach
|
||||
|
||||
Instead of stating problems, ask questions:
|
||||
|
||||
```markdown
|
||||
❌ "This will fail if the list is empty."
|
||||
✅ "What happens if `items` is an empty array?"
|
||||
|
||||
❌ "You need error handling here."
|
||||
✅ "How should this behave if the API call fails?"
|
||||
```
|
||||
|
||||
### Technique 3: Suggest, Don't Command
|
||||
|
||||
Use collaborative language:
|
||||
|
||||
```markdown
|
||||
❌ "You must change this to use async/await"
|
||||
✅ "Suggestion: async/await might make this more readable. What do you think?"
|
||||
|
||||
❌ "Extract this into a function"
|
||||
✅ "This logic appears in 3 places. Would it make sense to extract it?"
|
||||
```
|
||||
|
||||
### Technique 4: Differentiate Severity
|
||||
|
||||
Use labels to indicate priority:
|
||||
|
||||
- 🔴 `[blocking]` - Must fix before merge
|
||||
- 🟡 `[important]` - Should fix, discuss if disagree
|
||||
- 🟢 `[nit]` - Nice to have, not blocking
|
||||
- 💡 `[suggestion]` - Alternative approach to consider
|
||||
- 📚 `[learning]` - Educational comment, no action needed
|
||||
- 🎉 `[praise]` - Good work, keep it up!
|
||||
|
||||
**Severity levels:** 🔴 / 🟡 / 🟢 are the three severity tiers used as the standard across all guides in this skill — 🔴 blocks the merge, 🟡 should be addressed, 🟢 is optional. The remaining markers (💡 / 📚 / 🎉) are non-blocking annotations.
|
||||
|
||||
## Language-Specific Guides
|
||||
|
||||
根据审查的代码语言,查阅对应的详细指南:
|
||||
|
||||
| Language/Framework | Reference File | Key Topics |
|
||||
|-------------------|----------------|------------|
|
||||
| **React** | [React Guide](reference/react.md) | Hooks, useEffect, React 19 Actions, RSC, Suspense, TanStack Query v5 |
|
||||
| **Vue 3** | [Vue Guide](reference/vue.md) | Composition API, 响应性系统, Props/Emits, Watchers, Composables |
|
||||
| **Angular 17+** | [Angular Guide](reference/angular.md) | Signals, Standalone 组件, RxJS, Zoneless 变更检测, 模板优化 |
|
||||
| **Rust** | [Rust Guide](reference/rust.md) | 所有权/借用, Unsafe 审查, 异步代码, 取消安全性, 错误处理 |
|
||||
| **TypeScript** | [TypeScript Guide](reference/typescript.md) | 类型安全, async/await, 不可变性 |
|
||||
| **Python** | [Python Guide](reference/python.md) | 可变默认参数, 异常处理, 类属性 |
|
||||
| **Django / DRF** | [Django Guide](reference/django.md) | 安全审查, N+1 查询, Serializer 反模式, ViewSet, 异步视图 |
|
||||
| **FastAPI** | [FastAPI Guide](reference/fastapi.md) | Depends, Pydantic v2 validation, async correctness, sessions/N+1, auth vs authorization, test-driven verification |
|
||||
| **Java** | [Java Guide](reference/java.md) | Java 17/21 新特性, Spring Boot 3, 虚拟线程, Stream/Optional |
|
||||
| **PHP** | [PHP Guide](reference/php.md) | PHP 8.x type system, PDO, security review, Composer, PHPUnit/PHPStan |
|
||||
| **C# / .NET** | [C# Guide](reference/csharp.md) | C# 12 特性, 异步编程, EF Core 性能, ASP.NET Core, LINQ |
|
||||
| **Go** | [Go Guide](reference/go.md) | 错误处理, goroutine/channel, context, 接口设计 |
|
||||
| **Kotlin / Android** | [Kotlin Guide](reference/kotlin.md) | 协程, Flow, Jetpack Compose, 空安全, 内存泄漏, 架构模式 |
|
||||
| **Swift / SwiftUI** | [Swift Guide](reference/swift.md) | Optionals, Swift Concurrency, Sendable/actors, SwiftUI property wrappers, value vs reference types, API design |
|
||||
| **NestJS** | [NestJS Guide](reference/nestjs.md) | 依赖注入, 分层架构, DTO 验证, Guard/Interceptor, 循环依赖 |
|
||||
| **Svelte / SvelteKit** | [Svelte Guide](reference/svelte.md) | Runes, Load 函数, Form Actions, Store 迁移, SSR/CSR 边界 |
|
||||
| **C** | [C Guide](reference/c.md) | 指针/缓冲区, 内存安全, UB, 错误处理 |
|
||||
| **C++** | [C++ Guide](reference/cpp.md) | RAII, 生命周期, Rule of 0/3/5, 异常安全 |
|
||||
| **CSS/Less/Sass** | [CSS Guide](reference/css-less-sass.md) | 变量规范, !important, 性能优化, 响应式, 兼容性 |
|
||||
| **Qt** | [Qt Guide](reference/qt.md) | 对象模型, 信号/槽, 内存管理, 线程安全, 性能 |
|
||||
|
||||
## Cross-Cutting Guides
|
||||
|
||||
Language-agnostic patterns applicable to all code reviews:
|
||||
|
||||
| Topic | Reference File | Key Topics |
|
||||
|-------|----------------|------------|
|
||||
| **Universal Quality** | [Universal Quality Guide](reference/code-quality-universal.md) | Reuse audit, parameter sprawl, leaky abstractions, nested conditionals, stringly-typed code, TOCTOU, no-op updates, redundant state |
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Architecture Review Guide](reference/architecture-review-guide.md) - 架构设计审查指南(SOLID、反模式、耦合度)
|
||||
- [Performance Review Guide](reference/performance-review-guide.md) - 性能审查指南(Web Vitals、N+1、复杂度)
|
||||
- [Common Bugs Checklist](reference/common-bugs-checklist.md) - 按语言分类的常见错误清单
|
||||
- [Security Review Guide](reference/security-review-guide.md) - 安全审查指南
|
||||
- [Code Review Best Practices](reference/code-review-best-practices.md) - 代码审查最佳实践
|
||||
- [PR Review Template](assets/pr-review-template.md) - PR 审查评论模板
|
||||
- [Review Checklist](assets/review-checklist.md) - 快速参考清单
|
||||
@@ -0,0 +1,114 @@
|
||||
# PR Review Template
|
||||
|
||||
Copy and use this template for your code reviews.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
[Brief overview of what was reviewed - 1-2 sentences]
|
||||
|
||||
**PR Size:** [Small/Medium/Large] (~X lines)
|
||||
**Review Time:** [X minutes]
|
||||
|
||||
## Strengths
|
||||
|
||||
- [What was done well]
|
||||
- [Good patterns or approaches used]
|
||||
- [Improvements from previous code]
|
||||
|
||||
## Required Changes
|
||||
|
||||
🔴 **[blocking]** [Issue description]
|
||||
> [Code location or example]
|
||||
> [Suggested fix or explanation]
|
||||
|
||||
🔴 **[blocking]** [Issue description]
|
||||
> [Details]
|
||||
|
||||
## Important Suggestions
|
||||
|
||||
🟡 **[important]** [Issue description]
|
||||
> [Why this matters]
|
||||
> [Suggested approach]
|
||||
|
||||
## Minor Suggestions
|
||||
|
||||
🟢 **[nit]** [Minor improvement suggestion]
|
||||
|
||||
💡 **[suggestion]** [Alternative approach to consider]
|
||||
|
||||
## Learning Notes
|
||||
|
||||
📚 [Educational context worth sharing about X]
|
||||
|
||||
📚 [Background behind design decision Y]
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- [ ] No hardcoded secrets
|
||||
- [ ] Input validation present
|
||||
- [ ] Authorization checks in place
|
||||
- [ ] No SQL/XSS injection risks
|
||||
|
||||
## Test Coverage
|
||||
|
||||
- [ ] Unit tests added/updated
|
||||
- [ ] Edge cases covered
|
||||
- [ ] Error cases tested
|
||||
|
||||
## Verdict
|
||||
|
||||
**[ ] ✅ Approve** - Ready to merge
|
||||
**[ ] 💬 Comment** - Minor suggestions, can merge
|
||||
**[ ] 🔄 Request Changes** - Must address blocking issues
|
||||
|
||||
---
|
||||
|
||||
## Quick Copy Templates
|
||||
|
||||
### Blocking Issue
|
||||
```
|
||||
🔴 **[blocking]** [Title]
|
||||
|
||||
[Description of the issue]
|
||||
|
||||
**Location:** `file.ts:123`
|
||||
|
||||
**Suggested fix:**
|
||||
\`\`\`typescript
|
||||
// Your suggested code
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
### Important Suggestion
|
||||
```
|
||||
🟡 **[important]** [Title]
|
||||
|
||||
[Why this is important]
|
||||
|
||||
**Consider:**
|
||||
- Option A: [description]
|
||||
- Option B: [description]
|
||||
```
|
||||
|
||||
### Minor Suggestion
|
||||
```
|
||||
🟢 **[nit]** [Suggestion]
|
||||
|
||||
Not blocking, but consider [improvement].
|
||||
```
|
||||
|
||||
### Praise
|
||||
```
|
||||
🎉 **[praise]** Great work on [specific thing]!
|
||||
|
||||
[Why this is good]
|
||||
```
|
||||
|
||||
### Learning
|
||||
```
|
||||
📚 **[learning]** [Educational note]
|
||||
|
||||
For context, [X] works this way because [Y]. No action needed — just sharing.
|
||||
```
|
||||
@@ -0,0 +1,121 @@
|
||||
# Code Review Quick Checklist
|
||||
|
||||
Quick reference checklist for code reviews.
|
||||
|
||||
## Pre-Review (2 min)
|
||||
|
||||
- [ ] Read PR description and linked issue
|
||||
- [ ] Check PR size (<400 lines ideal)
|
||||
- [ ] Verify CI/CD status (tests passing?)
|
||||
- [ ] Understand the business requirement
|
||||
|
||||
## Architecture & Design (5 min)
|
||||
|
||||
- [ ] Solution fits the problem
|
||||
- [ ] Consistent with existing patterns
|
||||
- [ ] No simpler approach exists
|
||||
- [ ] Will it scale?
|
||||
- [ ] Changes in right location
|
||||
|
||||
## Logic & Correctness (10 min)
|
||||
|
||||
- [ ] Edge cases handled
|
||||
- [ ] Null/undefined checks present
|
||||
- [ ] Off-by-one errors checked
|
||||
- [ ] Race conditions considered
|
||||
- [ ] Error handling complete
|
||||
- [ ] Correct data types used
|
||||
|
||||
## Security (5 min)
|
||||
|
||||
- [ ] No hardcoded secrets
|
||||
- [ ] Input validated/sanitized
|
||||
- [ ] SQL injection prevented
|
||||
- [ ] XSS prevented
|
||||
- [ ] Authorization checks present
|
||||
- [ ] Sensitive data protected
|
||||
|
||||
## Performance (3 min)
|
||||
|
||||
- [ ] No N+1 queries
|
||||
- [ ] Expensive operations optimized
|
||||
- [ ] Large lists paginated
|
||||
- [ ] No memory leaks
|
||||
- [ ] Caching considered where appropriate
|
||||
|
||||
## Testing (5 min)
|
||||
|
||||
- [ ] Tests exist for new code
|
||||
- [ ] Edge cases tested
|
||||
- [ ] Error cases tested
|
||||
- [ ] Tests are readable
|
||||
- [ ] Tests are deterministic
|
||||
|
||||
## Code Quality (3 min)
|
||||
|
||||
- [ ] Clear variable/function names
|
||||
- [ ] No code duplication
|
||||
- [ ] Functions do one thing
|
||||
- [ ] Complex code commented
|
||||
- [ ] No magic numbers
|
||||
|
||||
## Documentation (2 min)
|
||||
|
||||
- [ ] Public APIs documented
|
||||
- [ ] README updated if needed
|
||||
- [ ] Breaking changes noted
|
||||
- [ ] Complex logic explained
|
||||
|
||||
---
|
||||
|
||||
## Severity Labels
|
||||
|
||||
| Label | Meaning | Action |
|
||||
|-------|---------|--------|
|
||||
| 🔴 `[blocking]` | Must fix | Block merge |
|
||||
| 🟡 `[important]` | Should fix | Discuss if disagree |
|
||||
| 🟢 `[nit]` | Nice to have | Non-blocking |
|
||||
| 💡 `[suggestion]` | Alternative | Consider |
|
||||
| 📚 `[learning]` | Educational comment | No action needed |
|
||||
| 🎉 `[praise]` | Good work | Celebrate! |
|
||||
|
||||
---
|
||||
|
||||
## Decision Matrix
|
||||
|
||||
| Situation | Decision |
|
||||
|-----------|----------|
|
||||
| Critical security issue | 🔴 Block, fix immediately |
|
||||
| Breaking change without migration | 🔴 Block |
|
||||
| Missing error handling | 🟡 Should fix |
|
||||
| No tests for new code | 🟡 Should fix |
|
||||
| Style preference | 🟢 Non-blocking |
|
||||
| Minor naming improvement | 🟢 Non-blocking |
|
||||
| Clever but working code | 💡 Suggest simpler |
|
||||
|
||||
---
|
||||
|
||||
## Time Budget
|
||||
|
||||
| PR Size | Target Time |
|
||||
|---------|-------------|
|
||||
| < 100 lines | 10-15 min |
|
||||
| 100-400 lines | 20-40 min |
|
||||
| > 400 lines | Ask to split |
|
||||
|
||||
---
|
||||
|
||||
## Red Flags
|
||||
|
||||
Watch for these patterns:
|
||||
|
||||
- `// TODO` in production code
|
||||
- `console.log` left in code
|
||||
- Commented out code
|
||||
- `any` type in TypeScript
|
||||
- Empty catch blocks
|
||||
- `unwrap()` in Rust production code
|
||||
- Magic numbers/strings
|
||||
- Copy-pasted code blocks
|
||||
- Missing null checks
|
||||
- Hardcoded URLs/credentials
|
||||
@@ -0,0 +1,702 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>code-review-skill(1) — User Commands (en_US)</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #14110d;
|
||||
--bg-alt: #1a1611;
|
||||
--fg: #c4b596;
|
||||
--fg-bright:#e8d5a8;
|
||||
--fg-dim: #7a6f56;
|
||||
--fg-faint: #4a4334;
|
||||
--amber: #d8964a;
|
||||
--amber-2: #e8a455;
|
||||
--red: #d56350;
|
||||
--green: #8fae5a;
|
||||
--blue: #6b94c4;
|
||||
--rule: #2a2520;
|
||||
}
|
||||
|
||||
html { background: var(--bg); }
|
||||
|
||||
body {
|
||||
font-family: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
color: var(--fg);
|
||||
background: var(--bg);
|
||||
min-height: 100vh;
|
||||
padding: 0 0 4rem;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* faint scanline-free phosphor texture — very subtle */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 50% 0%, rgba(216,150,74,0.04) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
/* ─── HEADER / FOOTER BAND ─── */
|
||||
.band {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--rule);
|
||||
z-index: 10;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.band-inner {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
padding: 0.625rem 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
|
||||
.band-l, .band-r {
|
||||
color: var(--fg-bright);
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.band-c { color: var(--fg-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
.band a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted var(--fg-faint);
|
||||
}
|
||||
.band a:hover { color: var(--amber); border-bottom-color: var(--amber); }
|
||||
|
||||
/* ─── PAGE ─── */
|
||||
main {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
padding: 3rem 2rem 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
pre, .pre {
|
||||
font-family: inherit;
|
||||
white-space: pre;
|
||||
color: inherit;
|
||||
background: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ─── SECTIONS ─── */
|
||||
h2.sec {
|
||||
color: var(--fg-bright);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.04em;
|
||||
margin: 2.75rem 0 0.875rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h2.sec::before { content: ''; }
|
||||
|
||||
section.body {
|
||||
padding-left: 7ch;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
section.body p {
|
||||
margin-bottom: 0.875rem;
|
||||
max-width: 70ch;
|
||||
}
|
||||
section.body p:last-child { margin-bottom: 0; }
|
||||
|
||||
.em { color: var(--fg-bright); }
|
||||
.dim { color: var(--fg-dim); }
|
||||
.faint { color: var(--fg-faint); }
|
||||
.amber { color: var(--amber); }
|
||||
.red { color: var(--red); }
|
||||
.green { color: var(--green); }
|
||||
.blue { color: var(--blue); }
|
||||
|
||||
a.link {
|
||||
color: var(--amber);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted var(--amber);
|
||||
}
|
||||
a.link:hover {
|
||||
color: var(--bg);
|
||||
background: var(--amber);
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
/* ─── TITLE BLOCK ─── */
|
||||
.title-block {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.ascii-title {
|
||||
color: var(--amber);
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
margin: 1.5rem 0 2.25rem;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
text-shadow: 0 0 12px rgba(216,150,74,0.25);
|
||||
}
|
||||
|
||||
.one-liner {
|
||||
color: var(--fg-bright);
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.lang-toggle {
|
||||
font-size: 12px;
|
||||
color: var(--fg-dim);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.lang-toggle a {
|
||||
color: var(--fg-dim);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted var(--fg-faint);
|
||||
padding-bottom: 1px;
|
||||
margin: 0 0.25em;
|
||||
}
|
||||
.lang-toggle a.on {
|
||||
color: var(--amber);
|
||||
border-bottom-color: var(--amber);
|
||||
}
|
||||
.lang-toggle a:hover { color: var(--amber); border-bottom-color: var(--amber); }
|
||||
.lang-toggle .sep { color: var(--fg-faint); }
|
||||
|
||||
.one-liner-sub {
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
|
||||
/* ─── TABLES ─── */
|
||||
.lang-row {
|
||||
display: grid;
|
||||
grid-template-columns: 26ch 1fr 7ch;
|
||||
gap: 1ch;
|
||||
padding: 0.125rem 0;
|
||||
align-items: baseline;
|
||||
transition: background 0.1s;
|
||||
border-bottom: 1px dotted var(--rule);
|
||||
}
|
||||
|
||||
.lang-row:hover { background: var(--bg-alt); }
|
||||
|
||||
.lang-row .file { color: var(--amber); }
|
||||
.lang-row .desc { color: var(--fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.lang-row .desc .topics { color: var(--fg-dim); }
|
||||
.lang-row .lines { text-align: right; color: var(--fg-dim); font-variant-numeric: tabular-nums; }
|
||||
|
||||
.dotleader {
|
||||
color: var(--fg-faint);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cat-head {
|
||||
color: var(--fg-bright);
|
||||
margin: 1.25rem 0 0.5rem;
|
||||
padding-bottom: 0.25rem;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.cat-head:first-child { margin-top: 0; }
|
||||
|
||||
/* ─── PHASE DIAGRAM ─── */
|
||||
.phase-flow {
|
||||
margin: 1rem 0 1.5rem;
|
||||
color: var(--fg-dim);
|
||||
line-height: 1.4;
|
||||
font-size: 13px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.phase-flow .box { color: var(--amber); }
|
||||
.phase-flow .arrow { color: var(--fg-bright); }
|
||||
|
||||
.phase-list dt {
|
||||
color: var(--fg-bright);
|
||||
margin-top: 0.875rem;
|
||||
}
|
||||
.phase-list dt:first-child { margin-top: 0; }
|
||||
.phase-list dd {
|
||||
color: var(--fg);
|
||||
max-width: 70ch;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
.phase-list dd.t {
|
||||
color: var(--fg-dim);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ─── SEVERITY LIST ─── */
|
||||
.sev-list {
|
||||
list-style: none;
|
||||
}
|
||||
.sev-list li {
|
||||
display: grid;
|
||||
grid-template-columns: 16ch 1fr;
|
||||
gap: 1ch;
|
||||
padding: 0.25rem 0;
|
||||
border-bottom: 1px dotted var(--rule);
|
||||
align-items: baseline;
|
||||
}
|
||||
.sev-list li:last-child { border-bottom: none; }
|
||||
.sev-list li .label { color: var(--fg-bright); }
|
||||
.sev-list li .desc { color: var(--fg); }
|
||||
.sev-list li .desc .aside { color: var(--fg-dim); }
|
||||
|
||||
/* ─── CODE BLOCKS ─── */
|
||||
.codeblock {
|
||||
background: var(--bg-alt);
|
||||
border-left: 2px solid var(--amber);
|
||||
padding: 0.875rem 1.25rem;
|
||||
margin: 0.875rem 0;
|
||||
color: var(--fg);
|
||||
overflow-x: auto;
|
||||
max-width: 70ch;
|
||||
}
|
||||
|
||||
.codeblock .prompt { color: var(--green); }
|
||||
.codeblock .cmt { color: var(--fg-dim); }
|
||||
.codeblock .cmd { color: var(--amber); }
|
||||
.codeblock .arg { color: var(--fg-bright); }
|
||||
|
||||
.examples {
|
||||
list-style: none;
|
||||
max-width: 70ch;
|
||||
}
|
||||
.examples li {
|
||||
padding: 0.375rem 0;
|
||||
color: var(--fg);
|
||||
}
|
||||
.examples li::before {
|
||||
content: '$ ';
|
||||
color: var(--green);
|
||||
}
|
||||
.examples li .q { color: var(--fg-bright); }
|
||||
.examples li .note {
|
||||
display: block;
|
||||
margin-top: 0.125rem;
|
||||
padding-left: 2ch;
|
||||
color: var(--fg-dim);
|
||||
font-size: 13px;
|
||||
}
|
||||
.examples li .note::before { content: '↳ '; color: var(--fg-faint); }
|
||||
|
||||
/* ─── FILES TREE ─── */
|
||||
.tree {
|
||||
color: var(--fg);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.tree .dir { color: var(--amber); }
|
||||
.tree .file { color: var(--fg); }
|
||||
.tree .cmt { color: var(--fg-dim); }
|
||||
.tree .branch { color: var(--fg-faint); }
|
||||
|
||||
/* ─── STATUS BAR / VIM-LIKE ─── */
|
||||
.statusbar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--amber);
|
||||
color: var(--bg);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.02em;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.statusbar-inner {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
padding: 0.25rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.statusbar-l, .statusbar-r { display: flex; gap: 1.25rem; align-items: center; }
|
||||
.statusbar-l > span:last-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 22ch;
|
||||
}
|
||||
.statusbar kbd {
|
||||
background: var(--bg);
|
||||
color: var(--amber);
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ─── CURSOR ─── */
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
width: 0.55em;
|
||||
height: 1em;
|
||||
background: var(--amber);
|
||||
vertical-align: -2px;
|
||||
animation: blink 1.1s steps(1) infinite;
|
||||
margin-left: 1px;
|
||||
}
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
|
||||
/* ─── SEPARATOR ─── */
|
||||
.hr {
|
||||
color: var(--rule);
|
||||
margin: 2rem 0 0;
|
||||
max-width: 70ch;
|
||||
padding-left: 7ch;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* ─── BIB ─── */
|
||||
.bib {
|
||||
max-width: 70ch;
|
||||
}
|
||||
.bib dt {
|
||||
color: var(--fg-bright);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.bib dt:first-child { margin-top: 0; }
|
||||
.bib dd { color: var(--fg-dim); }
|
||||
|
||||
/* ─── RESPONSIVE ─── */
|
||||
@media (max-width: 720px) {
|
||||
body { font-size: 13px; }
|
||||
main { padding: 2rem 1rem 0; }
|
||||
.band-inner, .statusbar-inner { padding: 0.5rem 1rem; font-size: 11px; }
|
||||
section.body { padding-left: 4ch; }
|
||||
.lang-row { grid-template-columns: 1fr 6ch; gap: 0.5ch; }
|
||||
.lang-row .desc { display: none; }
|
||||
.ascii-title { font-size: 9px; }
|
||||
.sev-list li { grid-template-columns: 14ch 1fr; }
|
||||
.phase-flow { font-size: 10px; }
|
||||
.band-c { display: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ═══ TOP BAND (man page header line) ═══ -->
|
||||
<div class="band">
|
||||
<div class="band-inner">
|
||||
<span class="band-l">CODE-REVIEW-SKILL(1)</span>
|
||||
<span class="band-c">User Commands · Edition 2026.01</span>
|
||||
<span class="band-r">CODE-REVIEW-SKILL(1)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
|
||||
<!-- ═══ TITLE BLOCK ═══ -->
|
||||
<div class="title-block">
|
||||
<pre class="ascii-title"> ___ ___ ___ ___ ___ _____ _____ _____ __ ___ _ _____ _ _
|
||||
/ __/ _ \| \| __| ___ | _ \ __\ \ / /_ _| __\ \ /\ / / __/ __| |/ /_ _| | | |
|
||||
| (_| (_) | |) | _| |___|| / _| \ V / | || _| \ V V /__\__ \ ' < | || |__| |__
|
||||
\___\___/|___/|___| |_|_\___| \_/ |___|___| \_/\_/ |___/_|\_\___|____|____|</pre>
|
||||
|
||||
<div class="one-liner">
|
||||
<span>
|
||||
<span class="dim">$ </span><span class="em">man code-review-skill</span><span class="cursor"></span>
|
||||
</span>
|
||||
<span class="lang-toggle">
|
||||
<span class="dim">LANG=</span><a href="index.html">zh_CN</a><span class="sep"> | </span><a href="index.en.html" class="on">en_US</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="one-liner-sub">
|
||||
v1.0 · awesome-skills · MIT · 20 languages · 16,000+ lines
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ NAME ═══ -->
|
||||
<h2 class="sec">NAME</h2>
|
||||
<section class="body">
|
||||
<p>
|
||||
<span class="em">code-review-skill</span> — A comprehensive, modular code review skill for Claude Code
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ═══ SYNOPSIS ═══ -->
|
||||
<h2 class="sec">SYNOPSIS</h2>
|
||||
<section class="body">
|
||||
<pre class="pre">
|
||||
<span class="amber">Use code-review-skill to</span> review this PR
|
||||
<span class="amber">Use code-review-skill to</span> review this <<span class="dim">component</span>>
|
||||
<span class="amber">Use code-review-skill for</span> <span class="dim">[</span>security <span class="dim">|</span> performance <span class="dim">|</span> architecture<span class="dim">]</span> review</pre>
|
||||
</section>
|
||||
|
||||
<!-- ═══ DESCRIPTION ═══ -->
|
||||
<h2 class="sec">DESCRIPTION</h2>
|
||||
<section class="body">
|
||||
<p>A production-grade code review skill. It transforms AI-assisted code review from vague suggestions into a structured, consistent, expert-level collaborative process.</p>
|
||||
<p>Core is only <span class="em">~190 lines</span>; the full <span class="em">16,000+ lines</span> of language guides load on demand. Covers <span class="em">20+</span> mainstream languages and frameworks — progressive loading, zero overhead.</p>
|
||||
<p>Every finding carries an explicit severity label. Every review proceeds through four phases: PR context · high-level assessment · line-by-line analysis · summary & decision.</p>
|
||||
</section>
|
||||
|
||||
<!-- ═══ LANGUAGES ═══ -->
|
||||
<h2 class="sec">LANGUAGES</h2>
|
||||
<section class="body">
|
||||
|
||||
<div class="cat-head">┌── frontend ──┘</div>
|
||||
<div class="lang-row"><span class="file">react.md</span><span class="desc">React 19, Hooks, Server Components, TanStack v5 <span class="dotleader">.................</span></span><span class="lines">870</span></div>
|
||||
<div class="lang-row"><span class="file">vue.md</span><span class="desc">Vue 3.5, Composition API, Composables, Watchers <span class="dotleader">.................</span></span><span class="lines">920</span></div>
|
||||
<div class="lang-row"><span class="file">angular.md</span><span class="desc">Angular 17+, Signals, Standalone, Zoneless <span class="dotleader">..........................</span></span><span class="lines">420</span></div>
|
||||
<div class="lang-row"><span class="file">svelte.md</span><span class="desc">Svelte 5, Runes, SvelteKit, SSR/CSR boundaries <span class="dotleader">..................</span></span><span class="lines">1,060</span></div>
|
||||
<div class="lang-row"><span class="file">typescript.md</span><span class="desc">TypeScript strict mode, generics, immutability <span class="dotleader">..................</span></span><span class="lines">540</span></div>
|
||||
<div class="lang-row"><span class="file">css-less-sass.md</span><span class="desc">CSS/Less/Sass variables, responsive, compatibility <span class="dotleader">..............</span></span><span class="lines">660</span></div>
|
||||
|
||||
<div class="cat-head">┌── backend ──┘</div>
|
||||
<div class="lang-row"><span class="file">python.md</span><span class="desc">Python async, typing, pytest, mutable defaults <span class="dotleader">.................</span></span><span class="lines">1,070</span></div>
|
||||
<div class="lang-row"><span class="file">django.md</span><span class="desc">Django/DRF security, N+1, serializers, async views <span class="dotleader">..............</span></span><span class="lines">1,030</span></div>
|
||||
<div class="lang-row"><span class="file">java.md</span><span class="desc">Java 17/21, Spring Boot 3, virtual threads, JPA <span class="dotleader">................</span></span><span class="lines">800</span></div>
|
||||
<div class="lang-row"><span class="file">php.md</span><span class="desc">PHP 8.x, types, PDO, security, Composer <span class="dotleader">...........................</span></span><span class="lines">700</span></div>
|
||||
<div class="lang-row"><span class="file">go.md</span><span class="desc">Goroutines, channels, context, interface design <span class="dotleader">.................</span></span><span class="lines">990</span></div>
|
||||
<div class="lang-row"><span class="file">rust.md</span><span class="desc">Ownership, async/await, unsafe, cancellation safety <span class="dotleader">.............</span></span><span class="lines">840</span></div>
|
||||
<div class="lang-row"><span class="file">csharp.md</span><span class="desc">C# 12 / .NET 8, EF Core, ASP.NET Core, LINQ <span class="dotleader">.....................</span></span><span class="lines">520</span></div>
|
||||
<div class="lang-row"><span class="file">nestjs.md</span><span class="desc">NestJS DI, guards, interceptors, DTO validation <span class="dotleader">.................</span></span><span class="lines">590</span></div>
|
||||
|
||||
<div class="cat-head">┌── mobile / systems ──┘</div>
|
||||
<div class="lang-row"><span class="file">kotlin.md</span><span class="desc">Kotlin/Android coroutines, Compose, Flow, null safety <span class="dotleader">...........</span></span><span class="lines">1,020</span></div>
|
||||
<div class="lang-row"><span class="file">swift.md</span><span class="desc">Swift 5.9+/6, SwiftUI, concurrency, Sendable, optionals <span class="dotleader">..........</span></span><span class="lines">930</span></div>
|
||||
<div class="lang-row"><span class="file">c.md</span><span class="desc">C pointer safety, undefined behavior, resources <span class="dotleader">.................</span></span><span class="lines">210</span></div>
|
||||
<div class="lang-row"><span class="file">cpp.md</span><span class="desc">C++ RAII, Rule of 0/3/5, move semantics, noexcept <span class="dotleader">...............</span></span><span class="lines">300</span></div>
|
||||
<div class="lang-row"><span class="file">qt.md</span><span class="desc">Qt object model, signals/slots, GUI performance <span class="dotleader">.................</span></span><span class="lines">190</span></div>
|
||||
|
||||
<div class="cat-head">┌── cross-cutting ──┘</div>
|
||||
<div class="lang-row"><span class="file">architecture-review-guide.md</span><span class="desc">SOLID, anti-patterns, coupling <span class="dotleader">...</span></span><span class="lines">470</span></div>
|
||||
<div class="lang-row"><span class="file">performance-review-guide.md</span><span class="desc">Web Vitals, N+1, complexity <span class="dotleader">.......</span></span><span class="lines">850</span></div>
|
||||
<div class="lang-row"><span class="file">code-quality-universal.md</span><span class="desc">TOCTOU, leaky abstractions, sprawl <span class="dotleader">....</span></span><span class="lines">320</span></div>
|
||||
<div class="lang-row"><span class="file">security-review-guide.md</span><span class="desc">Injection, XSS, secrets, all langs <span class="dotleader">.....</span></span><span class="lines">—</span></div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ PHASES ═══ -->
|
||||
<h2 class="sec">PHASES</h2>
|
||||
<section class="body">
|
||||
|
||||
<pre class="phase-flow"> <span class="box">┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐</span>
|
||||
<span class="box">│ context │</span> <span class="arrow">─▶</span> <span class="box">│ high level │</span> <span class="arrow">─▶</span> <span class="box">│ line by line│</span> <span class="arrow">─▶</span> <span class="box">│ decide │</span>
|
||||
<span class="box">│ 2-3m │</span> <span class="box">│ 5-10m │</span> <span class="box">│ 10-20m │</span> <span class="box">│ 2-3m │</span>
|
||||
<span class="box">└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘</span></pre>
|
||||
|
||||
<dl class="phase-list" style="margin-top:1.5rem;">
|
||||
<dt>1. context gathering <span class="dim">— 2-3 min</span></dt>
|
||||
<dd>Read the PR description and linked issues, assess scope, check CI status, understand the business intent.</dd>
|
||||
<dt>2. high-level review <span class="dim">— 5-10 min</span></dt>
|
||||
<dd>Evaluate architectural fit, performance impact, file organization, test strategy. See the whole first.</dd>
|
||||
<dt>3. line-by-line analysis <span class="dim">— 10-20 min</span></dt>
|
||||
<dd>Logic correctness · security · performance · maintainability · edge cases. One by one.</dd>
|
||||
<dt>4. summary & decision <span class="dim">— 2-3 min</span></dt>
|
||||
<dd>Summarize findings, name what was done well, deliver approve / comment / request-changes.</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<!-- ═══ SEVERITY ═══ -->
|
||||
<h2 class="sec">SEVERITY</h2>
|
||||
<section class="body">
|
||||
<ul class="sev-list">
|
||||
<li>
|
||||
<span class="label"><span class="red">●</span> [blocking]</span>
|
||||
<span class="desc">must fix <span class="aside">— resolve before merge; security / correctness / serious logic</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span style="color:#d68a3d;">●</span> [important]</span>
|
||||
<span class="desc">should fix <span class="aside">— strongly recommended; discuss if you disagree</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span style="color:#c7a648;">●</span> [nit]</span>
|
||||
<span class="desc">nice to have <span class="aside">— style or preference; non-blocking</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span class="blue">●</span> [suggestion]</span>
|
||||
<span class="desc">alternative <span class="aside">— worth considering; author decides</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span style="color:#9078b8;">●</span> [learning]</span>
|
||||
<span class="desc">educational <span class="aside">— no action needed; share knowledge</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span class="green">●</span> [praise]</span>
|
||||
<span class="desc">good work <span class="aside">— say it out loud when you see it</span></span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- ═══ INSTALLATION ═══ -->
|
||||
<h2 class="sec">INSTALLATION</h2>
|
||||
<section class="body">
|
||||
|
||||
<p>Clone into the Claude Code skills directory. Two commands.</p>
|
||||
|
||||
<pre class="codeblock"><span class="cmt"># macOS / Linux</span>
|
||||
<span class="prompt">$</span> <span class="cmd">git clone</span> <span class="arg">https://github.com/awesome-skills/code-review-skill.git</span> \
|
||||
~/.claude/skills/code-review-skill
|
||||
|
||||
<span class="cmt"># Windows PowerShell</span>
|
||||
<span class="prompt">PS></span> <span class="cmd">git clone</span> <span class="arg">https://github.com/awesome-skills/code-review-skill.git</span> `
|
||||
"$env:USERPROFILE\.claude\skills\code-review-skill"</pre>
|
||||
</section>
|
||||
|
||||
<!-- ═══ EXAMPLES ═══ -->
|
||||
<h2 class="sec">EXAMPLES</h2>
|
||||
<section class="body">
|
||||
<ul class="examples">
|
||||
<li>
|
||||
<span class="q">Use code-review-skill to review this PR</span>
|
||||
<span class="note">runs the full four-phase review</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="q">Review this React component</span>
|
||||
<span class="note">loads react.md · checks Hooks · Server Components</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="q">Security review of this Go service</span>
|
||||
<span class="note">loads go.md + security-review-guide.md together</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="q">Architecture review</span>
|
||||
<span class="note">loads the architecture guide · SOLID · anti-patterns · coupling</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- ═══ FILES ═══ -->
|
||||
<h2 class="sec">FILES</h2>
|
||||
<section class="body">
|
||||
<pre class="tree">
|
||||
<span class="dir">~/.claude/skills/code-review-skill/</span>
|
||||
<span class="branch">├──</span> <span class="file">SKILL.md</span> <span class="cmt"># core, loaded on activation (~190 lines)</span>
|
||||
<span class="branch">├──</span> <span class="file">README.md</span>
|
||||
<span class="branch">├──</span> <span class="file">LICENSE</span> <span class="cmt"># MIT</span>
|
||||
<span class="branch">├──</span> <span class="dir">reference/</span> <span class="cmt"># on-demand language guides</span>
|
||||
<span class="branch">│ ├──</span> <span class="file">react.md</span> <span class="file">vue.md</span> <span class="file">angular.md</span> ...
|
||||
<span class="branch">│ └──</span> <span class="file">architecture-review-guide.md</span> ...
|
||||
<span class="branch">├──</span> <span class="dir">assets/</span>
|
||||
<span class="branch">│ ├──</span> <span class="file">review-checklist.md</span> <span class="cmt"># quick reference</span>
|
||||
<span class="branch">│ └──</span> <span class="file">pr-review-template.md</span> <span class="cmt"># PR comment template</span>
|
||||
<span class="branch">└──</span> <span class="dir">scripts/</span>
|
||||
<span class="branch">└──</span> <span class="file">pr-analyzer.py</span> <span class="cmt"># PR complexity analyzer</span></pre>
|
||||
</section>
|
||||
|
||||
<!-- ═══ SEE ALSO ═══ -->
|
||||
<h2 class="sec">SEE ALSO</h2>
|
||||
<section class="body">
|
||||
<p>
|
||||
<a class="link" href="https://claude.ai/code" target="_blank">claude-code(1)</a>,
|
||||
<a class="link" href="https://github.com/awesome-skills/code-review-skill" target="_blank">github / awesome-skills</a>,
|
||||
<a class="link" href="https://github.com/awesome-skills/code-review-skill/blob/main/CONTRIBUTING.md" target="_blank">CONTRIBUTING(7)</a>,
|
||||
<a class="link" href="https://github.com/awesome-skills/code-review-skill/blob/main/assets/review-checklist.md" target="_blank">review-checklist(7)</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ═══ AUTHORS ═══ -->
|
||||
<h2 class="sec">AUTHORS</h2>
|
||||
<section class="body">
|
||||
<dl class="bib">
|
||||
<dt>awesome-skills</dt>
|
||||
<dd>maintainer, primary author</dd>
|
||||
<dt>contributors</dt>
|
||||
<dd>see <a class="link" href="https://github.com/awesome-skills/code-review-skill/graphs/contributors" target="_blank">graphs/contributors</a></dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<!-- ═══ COPYRIGHT ═══ -->
|
||||
<h2 class="sec">COPYRIGHT</h2>
|
||||
<section class="body">
|
||||
<p>
|
||||
<span class="dim">Copyright (c) 2025 awesome-skills.</span><br>
|
||||
Released under the MIT License.<br>
|
||||
<span class="dim">This is free software: you are free to change and redistribute it.</span><br>
|
||||
<span class="dim">There is NO WARRANTY, to the extent permitted by law.</span>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div style="height: 4rem;"></div>
|
||||
|
||||
<!-- ═══ END-OF-PAGE BAND ═══ -->
|
||||
<div style="border-top:1px solid var(--rule); margin-top:2rem; padding:0.625rem 0;">
|
||||
<div style="display:flex; justify-content:space-between; color:var(--fg-dim); font-size:12px; white-space:nowrap; gap:1rem;">
|
||||
<span class="band-l" style="color:var(--fg-bright);">CODE-REVIEW-SKILL(1)</span>
|
||||
<span style="color:var(--fg-dim);">awesome-skills</span>
|
||||
<span class="band-r" style="color:var(--fg-bright);">CODE-REVIEW-SKILL(1)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- ═══ VIM-LIKE STATUS BAR ═══ -->
|
||||
<div class="statusbar">
|
||||
<div class="statusbar-inner">
|
||||
<div class="statusbar-l">
|
||||
<span>-- NORMAL --</span>
|
||||
<span>code-review-skill.1</span>
|
||||
</div>
|
||||
<div class="statusbar-r">
|
||||
<span><kbd>g</kbd> top</span>
|
||||
<span><kbd>G</kbd> end</span>
|
||||
<span><kbd>q</kbd> quit</span>
|
||||
<span id="pos">1,1</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Vim-like keyboard nav for the man-page vibe
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||
|
||||
if (e.key === 'g') {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else if (e.key === 'G') {
|
||||
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
||||
} else if (e.key === 'j') {
|
||||
window.scrollBy({ top: 60, behavior: 'smooth' });
|
||||
} else if (e.key === 'k') {
|
||||
window.scrollBy({ top: -60, behavior: 'smooth' });
|
||||
} else if (e.key === 'q') {
|
||||
const ok = confirm('Quit man page?');
|
||||
if (ok) window.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Update line/col-like indicator from scroll position
|
||||
const posEl = document.getElementById('pos');
|
||||
function updatePos() {
|
||||
const pct = Math.round((window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100) || 0;
|
||||
const line = Math.max(1, Math.round((window.scrollY / 20)));
|
||||
posEl.textContent = line + ',1 ' + (pct >= 99 ? 'Bot' : pct <= 1 ? 'Top' : pct + '%');
|
||||
}
|
||||
updatePos();
|
||||
window.addEventListener('scroll', updatePos, { passive: true });
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,702 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>code-review-skill(1) — User Commands</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #14110d;
|
||||
--bg-alt: #1a1611;
|
||||
--fg: #c4b596;
|
||||
--fg-bright:#e8d5a8;
|
||||
--fg-dim: #7a6f56;
|
||||
--fg-faint: #4a4334;
|
||||
--amber: #d8964a;
|
||||
--amber-2: #e8a455;
|
||||
--red: #d56350;
|
||||
--green: #8fae5a;
|
||||
--blue: #6b94c4;
|
||||
--rule: #2a2520;
|
||||
}
|
||||
|
||||
html { background: var(--bg); }
|
||||
|
||||
body {
|
||||
font-family: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
color: var(--fg);
|
||||
background: var(--bg);
|
||||
min-height: 100vh;
|
||||
padding: 0 0 4rem;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* faint scanline-free phosphor texture — very subtle */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 50% 0%, rgba(216,150,74,0.04) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
/* ─── HEADER / FOOTER BAND ─── */
|
||||
.band {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--rule);
|
||||
z-index: 10;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.band-inner {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
padding: 0.625rem 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
|
||||
.band-l, .band-r {
|
||||
color: var(--fg-bright);
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.band-c { color: var(--fg-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
.band a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted var(--fg-faint);
|
||||
}
|
||||
.band a:hover { color: var(--amber); border-bottom-color: var(--amber); }
|
||||
|
||||
/* ─── PAGE ─── */
|
||||
main {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
padding: 3rem 2rem 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
pre, .pre {
|
||||
font-family: inherit;
|
||||
white-space: pre;
|
||||
color: inherit;
|
||||
background: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ─── SECTIONS ─── */
|
||||
h2.sec {
|
||||
color: var(--fg-bright);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.04em;
|
||||
margin: 2.75rem 0 0.875rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h2.sec::before { content: ''; }
|
||||
|
||||
section.body {
|
||||
padding-left: 7ch;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
section.body p {
|
||||
margin-bottom: 0.875rem;
|
||||
max-width: 70ch;
|
||||
}
|
||||
section.body p:last-child { margin-bottom: 0; }
|
||||
|
||||
.em { color: var(--fg-bright); }
|
||||
.dim { color: var(--fg-dim); }
|
||||
.faint { color: var(--fg-faint); }
|
||||
.amber { color: var(--amber); }
|
||||
.red { color: var(--red); }
|
||||
.green { color: var(--green); }
|
||||
.blue { color: var(--blue); }
|
||||
|
||||
a.link {
|
||||
color: var(--amber);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted var(--amber);
|
||||
}
|
||||
a.link:hover {
|
||||
color: var(--bg);
|
||||
background: var(--amber);
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
/* ─── TITLE BLOCK ─── */
|
||||
.title-block {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.ascii-title {
|
||||
color: var(--amber);
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
margin: 1.5rem 0 2.25rem;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
text-shadow: 0 0 12px rgba(216,150,74,0.25);
|
||||
}
|
||||
|
||||
.one-liner {
|
||||
color: var(--fg-bright);
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.lang-toggle {
|
||||
font-size: 12px;
|
||||
color: var(--fg-dim);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.lang-toggle a {
|
||||
color: var(--fg-dim);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted var(--fg-faint);
|
||||
padding-bottom: 1px;
|
||||
margin: 0 0.25em;
|
||||
}
|
||||
.lang-toggle a.on {
|
||||
color: var(--amber);
|
||||
border-bottom-color: var(--amber);
|
||||
}
|
||||
.lang-toggle a:hover { color: var(--amber); border-bottom-color: var(--amber); }
|
||||
.lang-toggle .sep { color: var(--fg-faint); }
|
||||
|
||||
.one-liner-sub {
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
|
||||
/* ─── TABLES ─── */
|
||||
.lang-row {
|
||||
display: grid;
|
||||
grid-template-columns: 26ch 1fr 7ch;
|
||||
gap: 1ch;
|
||||
padding: 0.125rem 0;
|
||||
align-items: baseline;
|
||||
transition: background 0.1s;
|
||||
border-bottom: 1px dotted var(--rule);
|
||||
}
|
||||
|
||||
.lang-row:hover { background: var(--bg-alt); }
|
||||
|
||||
.lang-row .file { color: var(--amber); }
|
||||
.lang-row .desc { color: var(--fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.lang-row .desc .topics { color: var(--fg-dim); }
|
||||
.lang-row .lines { text-align: right; color: var(--fg-dim); font-variant-numeric: tabular-nums; }
|
||||
|
||||
.dotleader {
|
||||
color: var(--fg-faint);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cat-head {
|
||||
color: var(--fg-bright);
|
||||
margin: 1.25rem 0 0.5rem;
|
||||
padding-bottom: 0.25rem;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.cat-head:first-child { margin-top: 0; }
|
||||
|
||||
/* ─── PHASE DIAGRAM ─── */
|
||||
.phase-flow {
|
||||
margin: 1rem 0 1.5rem;
|
||||
color: var(--fg-dim);
|
||||
line-height: 1.4;
|
||||
font-size: 13px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.phase-flow .box { color: var(--amber); }
|
||||
.phase-flow .arrow { color: var(--fg-bright); }
|
||||
|
||||
.phase-list dt {
|
||||
color: var(--fg-bright);
|
||||
margin-top: 0.875rem;
|
||||
}
|
||||
.phase-list dt:first-child { margin-top: 0; }
|
||||
.phase-list dd {
|
||||
color: var(--fg);
|
||||
max-width: 70ch;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
.phase-list dd.t {
|
||||
color: var(--fg-dim);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ─── SEVERITY LIST ─── */
|
||||
.sev-list {
|
||||
list-style: none;
|
||||
}
|
||||
.sev-list li {
|
||||
display: grid;
|
||||
grid-template-columns: 16ch 1fr;
|
||||
gap: 1ch;
|
||||
padding: 0.25rem 0;
|
||||
border-bottom: 1px dotted var(--rule);
|
||||
align-items: baseline;
|
||||
}
|
||||
.sev-list li:last-child { border-bottom: none; }
|
||||
.sev-list li .label { color: var(--fg-bright); }
|
||||
.sev-list li .desc { color: var(--fg); }
|
||||
.sev-list li .desc .aside { color: var(--fg-dim); }
|
||||
|
||||
/* ─── CODE BLOCKS ─── */
|
||||
.codeblock {
|
||||
background: var(--bg-alt);
|
||||
border-left: 2px solid var(--amber);
|
||||
padding: 0.875rem 1.25rem;
|
||||
margin: 0.875rem 0;
|
||||
color: var(--fg);
|
||||
overflow-x: auto;
|
||||
max-width: 70ch;
|
||||
}
|
||||
|
||||
.codeblock .prompt { color: var(--green); }
|
||||
.codeblock .cmt { color: var(--fg-dim); }
|
||||
.codeblock .cmd { color: var(--amber); }
|
||||
.codeblock .arg { color: var(--fg-bright); }
|
||||
|
||||
.examples {
|
||||
list-style: none;
|
||||
max-width: 70ch;
|
||||
}
|
||||
.examples li {
|
||||
padding: 0.375rem 0;
|
||||
color: var(--fg);
|
||||
}
|
||||
.examples li::before {
|
||||
content: '$ ';
|
||||
color: var(--green);
|
||||
}
|
||||
.examples li .q { color: var(--fg-bright); }
|
||||
.examples li .note {
|
||||
display: block;
|
||||
margin-top: 0.125rem;
|
||||
padding-left: 2ch;
|
||||
color: var(--fg-dim);
|
||||
font-size: 13px;
|
||||
}
|
||||
.examples li .note::before { content: '↳ '; color: var(--fg-faint); }
|
||||
|
||||
/* ─── FILES TREE ─── */
|
||||
.tree {
|
||||
color: var(--fg);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.tree .dir { color: var(--amber); }
|
||||
.tree .file { color: var(--fg); }
|
||||
.tree .cmt { color: var(--fg-dim); }
|
||||
.tree .branch { color: var(--fg-faint); }
|
||||
|
||||
/* ─── STATUS BAR / VIM-LIKE ─── */
|
||||
.statusbar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--amber);
|
||||
color: var(--bg);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.02em;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.statusbar-inner {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
padding: 0.25rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.statusbar-l, .statusbar-r { display: flex; gap: 1.25rem; align-items: center; }
|
||||
.statusbar-l > span:last-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 22ch;
|
||||
}
|
||||
.statusbar kbd {
|
||||
background: var(--bg);
|
||||
color: var(--amber);
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ─── CURSOR ─── */
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
width: 0.55em;
|
||||
height: 1em;
|
||||
background: var(--amber);
|
||||
vertical-align: -2px;
|
||||
animation: blink 1.1s steps(1) infinite;
|
||||
margin-left: 1px;
|
||||
}
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
|
||||
/* ─── SEPARATOR ─── */
|
||||
.hr {
|
||||
color: var(--rule);
|
||||
margin: 2rem 0 0;
|
||||
max-width: 70ch;
|
||||
padding-left: 7ch;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* ─── BIB ─── */
|
||||
.bib {
|
||||
max-width: 70ch;
|
||||
}
|
||||
.bib dt {
|
||||
color: var(--fg-bright);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.bib dt:first-child { margin-top: 0; }
|
||||
.bib dd { color: var(--fg-dim); }
|
||||
|
||||
/* ─── RESPONSIVE ─── */
|
||||
@media (max-width: 720px) {
|
||||
body { font-size: 13px; }
|
||||
main { padding: 2rem 1rem 0; }
|
||||
.band-inner, .statusbar-inner { padding: 0.5rem 1rem; font-size: 11px; }
|
||||
section.body { padding-left: 4ch; }
|
||||
.lang-row { grid-template-columns: 1fr 6ch; gap: 0.5ch; }
|
||||
.lang-row .desc { display: none; }
|
||||
.ascii-title { font-size: 9px; }
|
||||
.sev-list li { grid-template-columns: 14ch 1fr; }
|
||||
.phase-flow { font-size: 10px; }
|
||||
.band-c { display: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ═══ TOP BAND (man page header line) ═══ -->
|
||||
<div class="band">
|
||||
<div class="band-inner">
|
||||
<span class="band-l">CODE-REVIEW-SKILL(1)</span>
|
||||
<span class="band-c">User Commands · Edition 2026.01</span>
|
||||
<span class="band-r">CODE-REVIEW-SKILL(1)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
|
||||
<!-- ═══ TITLE BLOCK ═══ -->
|
||||
<div class="title-block">
|
||||
<pre class="ascii-title"> ___ ___ ___ ___ ___ _____ _____ _____ __ ___ _ _____ _ _
|
||||
/ __/ _ \| \| __| ___ | _ \ __\ \ / /_ _| __\ \ /\ / / __/ __| |/ /_ _| | | |
|
||||
| (_| (_) | |) | _| |___|| / _| \ V / | || _| \ V V /__\__ \ ' < | || |__| |__
|
||||
\___\___/|___/|___| |_|_\___| \_/ |___|___| \_/\_/ |___/_|\_\___|____|____|</pre>
|
||||
|
||||
<div class="one-liner">
|
||||
<span>
|
||||
<span class="dim">$ </span><span class="em">man code-review-skill</span><span class="cursor"></span>
|
||||
</span>
|
||||
<span class="lang-toggle">
|
||||
<span class="dim">LANG=</span><a href="index.html" class="on">zh_CN</a><span class="sep"> | </span><a href="index.en.html">en_US</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="one-liner-sub">
|
||||
v1.0 · awesome-skills · MIT · 20 languages · 16,000+ lines
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ NAME ═══ -->
|
||||
<h2 class="sec">NAME</h2>
|
||||
<section class="body">
|
||||
<p>
|
||||
<span class="em">code-review-skill</span> — 面向 Claude Code 的全面、模块化代码审查技能
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ═══ SYNOPSIS ═══ -->
|
||||
<h2 class="sec">SYNOPSIS</h2>
|
||||
<section class="body">
|
||||
<pre class="pre">
|
||||
<span class="amber">Use code-review-skill to</span> review this PR
|
||||
<span class="amber">Use code-review-skill to</span> review this <<span class="dim">component</span>>
|
||||
<span class="amber">Use code-review-skill for</span> <span class="dim">[</span>security <span class="dim">|</span> performance <span class="dim">|</span> architecture<span class="dim">]</span> review</pre>
|
||||
</section>
|
||||
|
||||
<!-- ═══ DESCRIPTION ═══ -->
|
||||
<h2 class="sec">DESCRIPTION</h2>
|
||||
<section class="body">
|
||||
<p>一份生产级的代码审查技能。它把 AI 辅助的代码审查从模糊建议提升为结构化、一致、专业级的协作流程。</p>
|
||||
<p>核心仅约 <span class="em">190 行</span>,按需调阅共计 <span class="em">16,000+ 行</span> 的语言指南。覆盖 <span class="em">20+ 种</span> 主流语言与框架——按需加载,零冗余。</p>
|
||||
<p>每一条审查意见都带有明确的严重性标记。每一次审查都按四个阶段推进:从 PR 上下文 · 高层级评估 · 逐行分析 · 总结决策。</p>
|
||||
</section>
|
||||
|
||||
<!-- ═══ LANGUAGES ═══ -->
|
||||
<h2 class="sec">LANGUAGES</h2>
|
||||
<section class="body">
|
||||
|
||||
<div class="cat-head">┌── frontend ──┘</div>
|
||||
<div class="lang-row"><span class="file">react.md</span><span class="desc">React 19, Hooks, Server Components, TanStack v5 <span class="dotleader">.................</span></span><span class="lines">870</span></div>
|
||||
<div class="lang-row"><span class="file">vue.md</span><span class="desc">Vue 3.5, Composition API, Composables, Watchers <span class="dotleader">.................</span></span><span class="lines">920</span></div>
|
||||
<div class="lang-row"><span class="file">angular.md</span><span class="desc">Angular 17+, Signals, Standalone, Zoneless <span class="dotleader">..........................</span></span><span class="lines">420</span></div>
|
||||
<div class="lang-row"><span class="file">svelte.md</span><span class="desc">Svelte 5, Runes, SvelteKit, SSR/CSR boundaries <span class="dotleader">..................</span></span><span class="lines">1,060</span></div>
|
||||
<div class="lang-row"><span class="file">typescript.md</span><span class="desc">TypeScript strict mode, generics, immutability <span class="dotleader">..................</span></span><span class="lines">540</span></div>
|
||||
<div class="lang-row"><span class="file">css-less-sass.md</span><span class="desc">CSS/Less/Sass variables, responsive, compatibility <span class="dotleader">..............</span></span><span class="lines">660</span></div>
|
||||
|
||||
<div class="cat-head">┌── backend ──┘</div>
|
||||
<div class="lang-row"><span class="file">python.md</span><span class="desc">Python async, typing, pytest, mutable defaults <span class="dotleader">.................</span></span><span class="lines">1,070</span></div>
|
||||
<div class="lang-row"><span class="file">django.md</span><span class="desc">Django/DRF security, N+1, serializers, async views <span class="dotleader">..............</span></span><span class="lines">1,030</span></div>
|
||||
<div class="lang-row"><span class="file">java.md</span><span class="desc">Java 17/21, Spring Boot 3, virtual threads, JPA <span class="dotleader">................</span></span><span class="lines">800</span></div>
|
||||
<div class="lang-row"><span class="file">php.md</span><span class="desc">PHP 8.x, types, PDO, security, Composer <span class="dotleader">...........................</span></span><span class="lines">700</span></div>
|
||||
<div class="lang-row"><span class="file">go.md</span><span class="desc">Goroutines, channels, context, interface design <span class="dotleader">.................</span></span><span class="lines">990</span></div>
|
||||
<div class="lang-row"><span class="file">rust.md</span><span class="desc">Ownership, async/await, unsafe, cancellation safety <span class="dotleader">.............</span></span><span class="lines">840</span></div>
|
||||
<div class="lang-row"><span class="file">csharp.md</span><span class="desc">C# 12 / .NET 8, EF Core, ASP.NET Core, LINQ <span class="dotleader">.....................</span></span><span class="lines">520</span></div>
|
||||
<div class="lang-row"><span class="file">nestjs.md</span><span class="desc">NestJS DI, guards, interceptors, DTO validation <span class="dotleader">.................</span></span><span class="lines">590</span></div>
|
||||
|
||||
<div class="cat-head">┌── mobile / systems ──┘</div>
|
||||
<div class="lang-row"><span class="file">kotlin.md</span><span class="desc">Kotlin/Android coroutines, Compose, Flow, null safety <span class="dotleader">...........</span></span><span class="lines">1,020</span></div>
|
||||
<div class="lang-row"><span class="file">swift.md</span><span class="desc">Swift 5.9+/6, SwiftUI, concurrency, Sendable, optionals <span class="dotleader">..........</span></span><span class="lines">930</span></div>
|
||||
<div class="lang-row"><span class="file">c.md</span><span class="desc">C pointer safety, undefined behavior, resources <span class="dotleader">.................</span></span><span class="lines">210</span></div>
|
||||
<div class="lang-row"><span class="file">cpp.md</span><span class="desc">C++ RAII, Rule of 0/3/5, move semantics, noexcept <span class="dotleader">...............</span></span><span class="lines">300</span></div>
|
||||
<div class="lang-row"><span class="file">qt.md</span><span class="desc">Qt object model, signals/slots, GUI performance <span class="dotleader">.................</span></span><span class="lines">190</span></div>
|
||||
|
||||
<div class="cat-head">┌── cross-cutting ──┘</div>
|
||||
<div class="lang-row"><span class="file">architecture-review-guide.md</span><span class="desc">SOLID, anti-patterns, coupling <span class="dotleader">...</span></span><span class="lines">470</span></div>
|
||||
<div class="lang-row"><span class="file">performance-review-guide.md</span><span class="desc">Web Vitals, N+1, complexity <span class="dotleader">.......</span></span><span class="lines">850</span></div>
|
||||
<div class="lang-row"><span class="file">code-quality-universal.md</span><span class="desc">TOCTOU, leaky abstractions, sprawl <span class="dotleader">....</span></span><span class="lines">320</span></div>
|
||||
<div class="lang-row"><span class="file">security-review-guide.md</span><span class="desc">Injection, XSS, secrets, all langs <span class="dotleader">.....</span></span><span class="lines">—</span></div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ PHASES ═══ -->
|
||||
<h2 class="sec">PHASES</h2>
|
||||
<section class="body">
|
||||
|
||||
<pre class="phase-flow"> <span class="box">┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐</span>
|
||||
<span class="box">│ context │</span> <span class="arrow">─▶</span> <span class="box">│ high level │</span> <span class="arrow">─▶</span> <span class="box">│ line by line│</span> <span class="arrow">─▶</span> <span class="box">│ decide │</span>
|
||||
<span class="box">│ 2-3m │</span> <span class="box">│ 5-10m │</span> <span class="box">│ 10-20m │</span> <span class="box">│ 2-3m │</span>
|
||||
<span class="box">└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘</span></pre>
|
||||
|
||||
<dl class="phase-list" style="margin-top:1.5rem;">
|
||||
<dt>1. context gathering <span class="dim">— 2-3 min</span></dt>
|
||||
<dd>读 PR 描述与关联 issue,评估规模,检查 CI 状态,理解业务需求。</dd>
|
||||
<dt>2. high-level review <span class="dim">— 5-10 min</span></dt>
|
||||
<dd>评估架构合理性、性能影响面、文件组织、测试策略。先看全局。</dd>
|
||||
<dt>3. line-by-line analysis <span class="dim">— 10-20 min</span></dt>
|
||||
<dd>逻辑正确性 · 安全 · 性能 · 可维护性 · 边界情况。一一过目。</dd>
|
||||
<dt>4. summary & decision <span class="dim">— 2-3 min</span></dt>
|
||||
<dd>汇总问题,表扬亮点,给出 approve / comment / request-changes。</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<!-- ═══ SEVERITY ═══ -->
|
||||
<h2 class="sec">SEVERITY</h2>
|
||||
<section class="body">
|
||||
<ul class="sev-list">
|
||||
<li>
|
||||
<span class="label"><span class="red">●</span> [blocking]</span>
|
||||
<span class="desc">必须修复 <span class="aside">— 合并前解决;安全漏洞 / 数据正确性 / 严重逻辑</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span style="color:#d68a3d;">●</span> [important]</span>
|
||||
<span class="desc">应当修复 <span class="aside">— 强烈建议;有分歧应讨论</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span style="color:#c7a648;">●</span> [nit]</span>
|
||||
<span class="desc">细节建议 <span class="aside">— 风格或偏好,不阻塞合并</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span class="blue">●</span> [suggestion]</span>
|
||||
<span class="desc">可选优化 <span class="aside">— 替代方案,由作者决定</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span style="color:#9078b8;">●</span> [learning]</span>
|
||||
<span class="desc">知识分享 <span class="aside">— 教育性说明,无需采取行动</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span class="green">●</span> [praise]</span>
|
||||
<span class="desc">表扬肯定 <span class="aside">— 看到好代码就说出来</span></span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- ═══ INSTALLATION ═══ -->
|
||||
<h2 class="sec">INSTALLATION</h2>
|
||||
<section class="body">
|
||||
|
||||
<p>克隆到 Claude Code skills 目录。两条命令即可。</p>
|
||||
|
||||
<pre class="codeblock"><span class="cmt"># macOS / Linux</span>
|
||||
<span class="prompt">$</span> <span class="cmd">git clone</span> <span class="arg">https://github.com/awesome-skills/code-review-skill.git</span> \
|
||||
~/.claude/skills/code-review-skill
|
||||
|
||||
<span class="cmt"># Windows PowerShell</span>
|
||||
<span class="prompt">PS></span> <span class="cmd">git clone</span> <span class="arg">https://github.com/awesome-skills/code-review-skill.git</span> `
|
||||
"$env:USERPROFILE\.claude\skills\code-review-skill"</pre>
|
||||
</section>
|
||||
|
||||
<!-- ═══ EXAMPLES ═══ -->
|
||||
<h2 class="sec">EXAMPLES</h2>
|
||||
<section class="body">
|
||||
<ul class="examples">
|
||||
<li>
|
||||
<span class="q">Use code-review-skill to review this PR</span>
|
||||
<span class="note">激活完整四阶段流程</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="q">Review this React component</span>
|
||||
<span class="note">加载 react.md · 检查 Hooks · Server Components</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="q">Security review of this Go service</span>
|
||||
<span class="note">同时加载 go.md + security-review-guide.md</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="q">Architecture review</span>
|
||||
<span class="note">加载架构指南 · SOLID · 反模式 · 耦合度</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- ═══ FILES ═══ -->
|
||||
<h2 class="sec">FILES</h2>
|
||||
<section class="body">
|
||||
<pre class="tree">
|
||||
<span class="dir">~/.claude/skills/code-review-skill/</span>
|
||||
<span class="branch">├──</span> <span class="file">SKILL.md</span> <span class="cmt"># 核心,激活时加载 (~190 行)</span>
|
||||
<span class="branch">├──</span> <span class="file">README.md</span>
|
||||
<span class="branch">├──</span> <span class="file">LICENSE</span> <span class="cmt"># MIT</span>
|
||||
<span class="branch">├──</span> <span class="dir">reference/</span> <span class="cmt"># 按需加载的语言指南</span>
|
||||
<span class="branch">│ ├──</span> <span class="file">react.md</span> <span class="file">vue.md</span> <span class="file">angular.md</span> ...
|
||||
<span class="branch">│ └──</span> <span class="file">architecture-review-guide.md</span> ...
|
||||
<span class="branch">├──</span> <span class="dir">assets/</span>
|
||||
<span class="branch">│ ├──</span> <span class="file">review-checklist.md</span> <span class="cmt"># 快速参考</span>
|
||||
<span class="branch">│ └──</span> <span class="file">pr-review-template.md</span> <span class="cmt"># 评论模板</span>
|
||||
<span class="branch">└──</span> <span class="dir">scripts/</span>
|
||||
<span class="branch">└──</span> <span class="file">pr-analyzer.py</span> <span class="cmt"># PR 复杂度分析</span></pre>
|
||||
</section>
|
||||
|
||||
<!-- ═══ SEE ALSO ═══ -->
|
||||
<h2 class="sec">SEE ALSO</h2>
|
||||
<section class="body">
|
||||
<p>
|
||||
<a class="link" href="https://claude.ai/code" target="_blank">claude-code(1)</a>,
|
||||
<a class="link" href="https://github.com/awesome-skills/code-review-skill" target="_blank">github / awesome-skills</a>,
|
||||
<a class="link" href="https://github.com/awesome-skills/code-review-skill/blob/main/CONTRIBUTING.md" target="_blank">CONTRIBUTING(7)</a>,
|
||||
<a class="link" href="https://github.com/awesome-skills/code-review-skill/blob/main/assets/review-checklist.md" target="_blank">review-checklist(7)</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ═══ AUTHORS ═══ -->
|
||||
<h2 class="sec">AUTHORS</h2>
|
||||
<section class="body">
|
||||
<dl class="bib">
|
||||
<dt>awesome-skills</dt>
|
||||
<dd>maintainer, primary author</dd>
|
||||
<dt>contributors</dt>
|
||||
<dd>see <a class="link" href="https://github.com/awesome-skills/code-review-skill/graphs/contributors" target="_blank">graphs/contributors</a></dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<!-- ═══ COPYRIGHT ═══ -->
|
||||
<h2 class="sec">COPYRIGHT</h2>
|
||||
<section class="body">
|
||||
<p>
|
||||
<span class="dim">Copyright (c) 2025 awesome-skills.</span><br>
|
||||
Released under the MIT License.<br>
|
||||
<span class="dim">This is free software: you are free to change and redistribute it.</span><br>
|
||||
<span class="dim">There is NO WARRANTY, to the extent permitted by law.</span>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div style="height: 4rem;"></div>
|
||||
|
||||
<!-- ═══ END-OF-PAGE BAND ═══ -->
|
||||
<div style="border-top:1px solid var(--rule); margin-top:2rem; padding:0.625rem 0;">
|
||||
<div style="display:flex; justify-content:space-between; color:var(--fg-dim); font-size:12px; white-space:nowrap; gap:1rem;">
|
||||
<span class="band-l" style="color:var(--fg-bright);">CODE-REVIEW-SKILL(1)</span>
|
||||
<span style="color:var(--fg-dim);">awesome-skills</span>
|
||||
<span class="band-r" style="color:var(--fg-bright);">CODE-REVIEW-SKILL(1)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- ═══ VIM-LIKE STATUS BAR ═══ -->
|
||||
<div class="statusbar">
|
||||
<div class="statusbar-inner">
|
||||
<div class="statusbar-l">
|
||||
<span>-- NORMAL --</span>
|
||||
<span>code-review-skill.1</span>
|
||||
</div>
|
||||
<div class="statusbar-r">
|
||||
<span><kbd>g</kbd> top</span>
|
||||
<span><kbd>G</kbd> end</span>
|
||||
<span><kbd>q</kbd> quit</span>
|
||||
<span id="pos">1,1</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Vim-like keyboard nav for the man-page vibe
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||
|
||||
if (e.key === 'g') {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else if (e.key === 'G') {
|
||||
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
||||
} else if (e.key === 'j') {
|
||||
window.scrollBy({ top: 60, behavior: 'smooth' });
|
||||
} else if (e.key === 'k') {
|
||||
window.scrollBy({ top: -60, behavior: 'smooth' });
|
||||
} else if (e.key === 'q') {
|
||||
const ok = confirm('Quit man page?');
|
||||
if (ok) window.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Update line/col-like indicator from scroll position
|
||||
const posEl = document.getElementById('pos');
|
||||
function updatePos() {
|
||||
const pct = Math.round((window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100) || 0;
|
||||
const line = Math.max(1, Math.round((window.scrollY / 20)));
|
||||
posEl.textContent = line + ',1 ' + (pct >= 99 ? 'Bot' : pct <= 1 ? 'Top' : pct + '%');
|
||||
}
|
||||
updatePos();
|
||||
window.addEventListener('scroll', updatePos, { passive: true });
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,419 @@
|
||||
# Angular Code Review Guide
|
||||
|
||||
> Angular 17+ 代码审查指南,覆盖 Signals、Standalone 组件、RxJS 反模式、Zoneless 变更检测、模板最佳实践及性能优化等核心主题。
|
||||
|
||||
## 目录
|
||||
|
||||
- [Signals 与变更检测](#signals-与变更检测)
|
||||
- [Standalone 组件迁移](#standalone-组件迁移)
|
||||
- [RxJS 反模式](#rxjs-反模式)
|
||||
- [Zoneless 变更检测](#zoneless-变更检测)
|
||||
- [模板最佳实践](#模板最佳实践)
|
||||
- [性能优化](#性能优化)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Signals 与变更检测
|
||||
|
||||
### Signal + OnPush 自动触发变更检测
|
||||
|
||||
```typescript
|
||||
// ❌ 可变状态 + OnPush = 界面不更新
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `<p>{{ data.name }}</p>`,
|
||||
})
|
||||
export class UserProfile {
|
||||
data = { name: 'Alice' };
|
||||
changeName() { this.data.name = 'Bob'; } // UI 不会更新!
|
||||
}
|
||||
|
||||
// ✅ Signal + OnPush = 自动变更检测
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `<p>{{ name() }}</p>`,
|
||||
})
|
||||
export class UserProfile {
|
||||
name = signal('Alice');
|
||||
changeName() { this.name.set('Bob'); } // 自动触发 CD
|
||||
}
|
||||
```
|
||||
|
||||
### @Input() 对象变异不会被 OnPush 检测
|
||||
|
||||
```typescript
|
||||
// ❌ 变异 Input 对象——引用不变,OnPush 不检测
|
||||
@Input() config!: Config;
|
||||
updateConfig() { this.config.theme = 'dark'; }
|
||||
|
||||
// ✅ 创建新引用
|
||||
updateConfig() { this.config = { ...this.config, theme: 'dark' }; }
|
||||
```
|
||||
|
||||
### computed() 用于派生状态
|
||||
|
||||
```typescript
|
||||
// ❌ effect 用于同步状态——反模式,可能触发额外 CD 周期
|
||||
export class CartComponent {
|
||||
total = signal(0);
|
||||
discounted = signal(0);
|
||||
|
||||
constructor() {
|
||||
effect(() => this.discounted.set(this.total() * 0.9));
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ computed 用于派生状态——惰性计算,无副作用
|
||||
export class CartComponent {
|
||||
total = signal(0);
|
||||
discounted = computed(() => this.total() * 0.9);
|
||||
}
|
||||
```
|
||||
|
||||
### effect() 中 Signal 读取在 await 后不会被追踪
|
||||
|
||||
```typescript
|
||||
// ❌ await 之后读取 Signal——依赖未被追踪
|
||||
effect(async () => {
|
||||
const data = await fetchUserData();
|
||||
console.log(`Theme: ${theme()}`); // theme() 未被追踪!
|
||||
});
|
||||
|
||||
// ✅ 在 await 之前同步读取
|
||||
effect(async () => {
|
||||
const currentTheme = theme(); // 同步读取,被追踪
|
||||
const data = await fetchUserData();
|
||||
console.log(`Theme: ${currentTheme}`);
|
||||
});
|
||||
```
|
||||
|
||||
### effect 只在特定场景使用
|
||||
|
||||
```typescript
|
||||
// ❌ 用 effect 同步两个 Signal——永远用 computed
|
||||
effect(() => { this.filtered.set(this.items().filter(i => i.active)); });
|
||||
|
||||
// ✅ effect 的合理场景:DOM 操作、分析日志、订阅外部源
|
||||
effect(() => {
|
||||
const canvas = this.canvasRef.nativeElement;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = this.color();
|
||||
ctx.fillRect(0, 0, this.size(), this.size());
|
||||
});
|
||||
|
||||
// 💡 "There are no situations where effect is good,
|
||||
// only situations where it is appropriate."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Standalone 组件迁移
|
||||
|
||||
### Angular 19+ standalone 是默认值
|
||||
|
||||
```typescript
|
||||
// ❌ Legacy NgModule 组件
|
||||
@Component({
|
||||
selector: 'old-component',
|
||||
standalone: false,
|
||||
})
|
||||
export class OldComponent {}
|
||||
|
||||
// ✅ 现代 Standalone 组件(Angular 19+ standalone 是默认值)
|
||||
@Component({
|
||||
selector: 'user-profile',
|
||||
imports: [ProfilePhoto, RouterLink],
|
||||
template: `<profile-photo /><a routerLink="/edit">Edit</a>`,
|
||||
})
|
||||
export class UserProfile {}
|
||||
```
|
||||
|
||||
### 审查标记
|
||||
|
||||
```typescript
|
||||
// ⚠️ 需要迁移的信号:
|
||||
// 1. standalone: false
|
||||
// 2. @NgModule declarations
|
||||
// 3. 组件通过 NgModule 而非直接 import
|
||||
|
||||
// ✅ 迁移路径:
|
||||
// 1. 删除 standalone: false
|
||||
// 2. 将依赖添加到组件的 imports 数组
|
||||
// 3. 如果不再有 declarations,删除 NgModule
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RxJS 反模式
|
||||
|
||||
### subscribe() 必须配 takeUntilDestroyed
|
||||
|
||||
```typescript
|
||||
// ❌ 裸 subscribe——内存泄漏!组件销毁后仍继续接收数据
|
||||
@Component({ /* ... */ })
|
||||
export class UserProfile implements OnInit {
|
||||
ngOnInit() {
|
||||
this.data$.subscribe(data => this.processData(data));
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ takeUntilDestroyed——自动在组件销毁时取消(需在构造函数或注入上下文中调用)
|
||||
@Component({ /* ... */ })
|
||||
export class UserProfile {
|
||||
constructor() {
|
||||
this.data$.pipe(takeUntilDestroyed()).subscribe(data => {
|
||||
this.processData(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 在构造函数外使用——传入 DestroyRef
|
||||
@Component({ /* ... */ })
|
||||
export class UserProfile {
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
startListening() {
|
||||
this.data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(/* ... */);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### toSignal 优于 AsyncPipe
|
||||
|
||||
```typescript
|
||||
// ❌ AsyncPipe——需要导入,模板中有 | async
|
||||
@Component({
|
||||
imports: [AsyncPipe],
|
||||
template: `{{ data$ | async }}`,
|
||||
})
|
||||
|
||||
// ✅ toSignal——自动取消订阅,可在任何地方使用
|
||||
export class UserProfile {
|
||||
data = toSignal(this.data$, { initialValue: null });
|
||||
// 模板直接用 data()
|
||||
}
|
||||
```
|
||||
|
||||
### 避免重复 toSignal 调用
|
||||
|
||||
```typescript
|
||||
// ❌ toSignal 每次调用都创建新订阅
|
||||
getData() {
|
||||
return toSignal(this.http.get('/api/data'));
|
||||
}
|
||||
|
||||
// ✅ 存储结果
|
||||
data = toSignal(this.http.get('/api/data'), { initialValue: null });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Zoneless 变更检测
|
||||
|
||||
### 普通属性变异不会被检测(Angular 21+)
|
||||
|
||||
```typescript
|
||||
// ❌ Zoneless 下普通属性赋值不触发 CD
|
||||
export class UserService {
|
||||
user: User | null = null;
|
||||
loadUser() { this.user = fetchResult; } // 不触发!
|
||||
}
|
||||
|
||||
// ✅ Signal 自动触发 CD
|
||||
export class UserService {
|
||||
private _user = signal<User | null>(null);
|
||||
readonly user = this._user.asReadonly();
|
||||
loadUser() { this._user.set(fetchResult); }
|
||||
}
|
||||
```
|
||||
|
||||
### NgZone API 在 Zoneless 中失效
|
||||
|
||||
```typescript
|
||||
// ❌ NgZone.onStable 在 zoneless 中永远不会触发
|
||||
ngZone.onStable.subscribe(() => { /* 永远不触发 */ });
|
||||
|
||||
// ✅ 使用 afterNextRender
|
||||
afterNextRender({ write: () => { /* CD 之后执行 */ } });
|
||||
```
|
||||
|
||||
### Reactive Forms 变异需要 markForCheck
|
||||
|
||||
```typescript
|
||||
// ❌ Reactive Forms 的 setValue/patchValue 在 zoneless 中不自动调度 CD
|
||||
this.form.patchValue({ name: 'Alice' }); // UI 可能不更新
|
||||
|
||||
// ✅ 手动标记或通过 Signal 反映
|
||||
this.form.patchValue({ name: 'Alice' });
|
||||
this.cdr.markForCheck();
|
||||
```
|
||||
|
||||
### Zoneless 下有效的 CD 触发器
|
||||
|
||||
| 触发器 | 说明 |
|
||||
|--------|------|
|
||||
| `signal.set()` / `.update()` | Signal 更新自动触发 |
|
||||
| `ChangeDetectorRef.markForCheck()` | 手动标记 |
|
||||
| `ComponentRef.setInput()` | 输入绑定 |
|
||||
| 模板事件监听器回调 | 用户交互 |
|
||||
|
||||
---
|
||||
|
||||
## 模板最佳实践
|
||||
|
||||
### 复杂逻辑提取为 computed Signal
|
||||
|
||||
```typescript
|
||||
// ❌ 模板中复杂表达式
|
||||
template: `<div *ngIf="items.filter(i => i.active).length > 0 && user.role === 'admin'">`
|
||||
|
||||
// ✅ 提取为 computed
|
||||
filteredItems = computed(() => this.items().filter(i => i.active));
|
||||
shouldShow = computed(() => this.filteredItems().length > 0 && this.user().role === 'admin');
|
||||
template: `@if (shouldShow()) { <div>...</div> }`
|
||||
```
|
||||
|
||||
### 原生绑定优于 NgClass / NgStyle
|
||||
|
||||
```typescript
|
||||
// ❌ NgClass/NgStyle——额外指令开销
|
||||
template: `<div [ngClass]="{active: isActive}" [ngStyle]="{'color': textColor}">`
|
||||
|
||||
// ✅ 原生 class/style 绑定——性能更好
|
||||
template: `<div [class.active]="isActive" [style.color]="textColor">`
|
||||
```
|
||||
|
||||
### 模板专用成员标记 protected
|
||||
|
||||
```typescript
|
||||
// ❂ 模板专用方法暴露为 public
|
||||
export class UserProfile {
|
||||
formatName(name: string) { return name.trim(); }
|
||||
}
|
||||
|
||||
// ✅ 模板专用成员用 protected
|
||||
export class UserProfile {
|
||||
protected formatName(name: string) { return name.trim(); }
|
||||
}
|
||||
```
|
||||
|
||||
### Angular 管理的属性标记 readonly
|
||||
|
||||
```typescript
|
||||
// ❌ input/output/model 可被意外覆盖
|
||||
userId = input<string>();
|
||||
userSaved = output<void>();
|
||||
|
||||
// ✅ readonly 防止意外赋值
|
||||
readonly userId = input<string>();
|
||||
readonly userSaved = output<void>();
|
||||
readonly userName = model<string>();
|
||||
```
|
||||
|
||||
### 命名规范:操作名而非事件名
|
||||
|
||||
```typescript
|
||||
// ❌ 以事件命名
|
||||
template: `<button (click)="handleClick()">Save</button>`
|
||||
|
||||
// ✅ 以操作命名
|
||||
template: `<button (click)="saveUserData()">Save</button>`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能优化
|
||||
|
||||
### effect 是最后手段——优先 computed
|
||||
|
||||
```typescript
|
||||
// ❌ effect 用于状态同步——触发额外 CD,可能无限循环
|
||||
effect(() => {
|
||||
this.filteredItems.set(this.items().filter(i => i.active));
|
||||
});
|
||||
|
||||
// ✅ computed——惰性计算,无副作用,无额外 CD
|
||||
filteredItems = computed(() => this.items().filter(i => i.active));
|
||||
```
|
||||
|
||||
### afterRenderEffect 分离读写阶段
|
||||
|
||||
```typescript
|
||||
// ❌ 无阶段指定 = mixedReadWrite = 额外 DOM 回流
|
||||
afterRenderEffect(() => {
|
||||
const height = el.offsetHeight; // 读
|
||||
el.style.height = height + 10 + 'px'; // 写
|
||||
});
|
||||
|
||||
// ✅ 分离阶段减少回流
|
||||
afterRenderEffect({
|
||||
earlyRead: () => el.offsetHeight,
|
||||
write: (height) => { el.style.height = height() + 10 + 'px'; },
|
||||
read: () => verifyLayout(),
|
||||
});
|
||||
```
|
||||
|
||||
### inject() 优于构造函数注入
|
||||
|
||||
```typescript
|
||||
// ❌ 构造函数注入——多依赖时难以阅读
|
||||
export class UserService {
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private router: Router,
|
||||
private auth: AuthService,
|
||||
) {}
|
||||
}
|
||||
|
||||
// ✅ inject()——更好的类型推断和可读性
|
||||
export class UserService {
|
||||
private http = inject(HttpClient);
|
||||
private router = inject(Router);
|
||||
private auth = inject(AuthService);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Signals 与变更检测
|
||||
|
||||
- [ ] Signal + OnPush 用于模板状态(非可变对象)
|
||||
- [ ] `@Input()` 对象通过新引用更新(非变异)
|
||||
- [ ] 派生状态用 `computed()`,不用 `effect()`
|
||||
- [ ] `effect()` 中 Signal 读取在 `await` 之前
|
||||
- [ ] `effect()` 只用于 DOM 操作、日志、外部源订阅
|
||||
|
||||
### Standalone 组件
|
||||
|
||||
- [ ] 无 `standalone: false`(Angular 19+)
|
||||
- [ ] 组件通过 `imports` 数组导入依赖
|
||||
- [ ] 无不必要的 `@NgModule`
|
||||
|
||||
### RxJS
|
||||
|
||||
- [ ] `.subscribe()` 配 `takeUntilDestroyed` 或 `async` pipe
|
||||
- [ ] 优先 `toSignal` 而非 `AsyncPipe`
|
||||
- [ ] 无重复 `toSignal` 调用
|
||||
|
||||
### Zoneless
|
||||
|
||||
- [ ] 模板状态通过 Signal 管理(非普通属性)
|
||||
- [ ] 无 `NgZone.onStable` / `NgZone.onMicrotaskEmpty`
|
||||
- [ ] Reactive Forms 变异后有 `markForCheck()`
|
||||
|
||||
### 模板
|
||||
|
||||
- [ ] 复杂逻辑提取为 `computed` Signal
|
||||
- [ ] 使用原生 `[class]`/`[style]` 而非 `NgClass`/`NgStyle`
|
||||
- [ ] 模板专用成员标记 `protected`
|
||||
- [ ] `input`/`output`/`model` 属性标记 `readonly`
|
||||
- [ ] 事件处理器以操作命名(`saveData` 而非 `handleClick`)
|
||||
|
||||
### 性能
|
||||
|
||||
- [ ] `effect()` 不用于状态同步
|
||||
- [ ] `afterRenderEffect` 分离读写阶段
|
||||
- [ ] `inject()` 用于依赖注入
|
||||
@@ -0,0 +1,472 @@
|
||||
# Architecture Review Guide
|
||||
|
||||
架构设计审查指南,帮助评估代码的架构是否合理、设计是否恰当。
|
||||
|
||||
## SOLID 原则检查清单
|
||||
|
||||
### S - 单一职责原则 (SRP)
|
||||
|
||||
**检查要点:**
|
||||
- 这个类/模块是否只有一个改变的理由?
|
||||
- 类中的方法是否都服务于同一个目的?
|
||||
- 如果要向非技术人员描述这个类,能否用一句话说清楚?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
```
|
||||
⚠️ 类名包含 "And"、"Manager"、"Handler"、"Processor" 等泛化词汇
|
||||
⚠️ 一个类超过 200-300 行代码
|
||||
⚠️ 类有超过 5-7 个公共方法
|
||||
⚠️ 不同的方法操作完全不同的数据
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
- "这个类负责哪些事情?能否拆分?"
|
||||
- "如果 X 需求变化,哪些方法需要改?如果 Y 需求变化呢?"
|
||||
|
||||
### O - 开闭原则 (OCP)
|
||||
|
||||
**检查要点:**
|
||||
- 添加新功能时,是否需要修改现有代码?
|
||||
- 是否可以通过扩展(继承、组合)来添加新行为?
|
||||
- 是否存在大量的 if/else 或 switch 语句来处理不同类型?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
```
|
||||
⚠️ switch/if-else 链处理不同类型
|
||||
⚠️ 添加新功能需要修改核心类
|
||||
⚠️ 类型检查 (instanceof, typeof) 散布在代码中
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
- "如果要添加新的 X 类型,需要修改哪些文件?"
|
||||
- "这个 switch 语句会随着新类型增加而增长吗?"
|
||||
|
||||
### L - 里氏替换原则 (LSP)
|
||||
|
||||
**检查要点:**
|
||||
- 子类是否可以完全替代父类使用?
|
||||
- 子类是否改变了父类方法的预期行为?
|
||||
- 是否存在子类抛出父类未声明的异常?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
```
|
||||
⚠️ 显式类型转换 (casting)
|
||||
⚠️ 子类方法抛出 NotImplementedException
|
||||
⚠️ 子类方法为空实现或只有 return
|
||||
⚠️ 使用基类的地方需要检查具体类型
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
- "如果用子类替换父类,调用方代码是否需要修改?"
|
||||
- "这个方法在子类中的行为是否符合父类的契约?"
|
||||
|
||||
### I - 接口隔离原则 (ISP)
|
||||
|
||||
**检查要点:**
|
||||
- 接口是否足够小且专注?
|
||||
- 实现类是否被迫实现不需要的方法?
|
||||
- 客户端是否依赖了它不使用的方法?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
```
|
||||
⚠️ 接口超过 5-7 个方法
|
||||
⚠️ 实现类有空方法或抛出 NotImplementedException
|
||||
⚠️ 接口名称过于宽泛 (IManager, IService)
|
||||
⚠️ 不同的客户端只使用接口的部分方法
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
- "这个接口的所有方法是否都被每个实现类使用?"
|
||||
- "能否将这个大接口拆分为更小的专用接口?"
|
||||
|
||||
### D - 依赖倒置原则 (DIP)
|
||||
|
||||
**检查要点:**
|
||||
- 高层模块是否依赖于抽象而非具体实现?
|
||||
- 是否使用依赖注入而非直接 new 对象?
|
||||
- 抽象是否由高层模块定义而非低层模块?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
```
|
||||
⚠️ 高层模块直接 new 低层模块的具体类
|
||||
⚠️ 导入具体实现类而非接口/抽象类
|
||||
⚠️ 配置和连接字符串硬编码在业务逻辑中
|
||||
⚠️ 难以为某个类编写单元测试
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
- "这个类的依赖能否在测试时被 mock 替换?"
|
||||
- "如果要更换数据库/API 实现,需要修改多少地方?"
|
||||
|
||||
---
|
||||
|
||||
## 架构反模式识别
|
||||
|
||||
### 致命反模式
|
||||
|
||||
| 反模式 | 识别信号 | 影响 |
|
||||
|--------|----------|------|
|
||||
| **大泥球 (Big Ball of Mud)** | 没有清晰的模块边界,任何代码都可能调用任何其他代码 | 难以理解、修改和测试 |
|
||||
| **上帝类 (God Object)** | 单个类承担过多职责,知道太多、做太多 | 高耦合,难以重用和测试 |
|
||||
| **意大利面条代码** | 控制流程混乱,goto 或深层嵌套,难以追踪执行路径 | 难以理解和维护 |
|
||||
| **熔岩流 (Lava Flow)** | 没人敢动的古老代码,缺乏文档和测试 | 技术债务累积 |
|
||||
|
||||
### 设计反模式
|
||||
|
||||
| 反模式 | 识别信号 | 建议 |
|
||||
|--------|----------|------|
|
||||
| **金锤子 (Golden Hammer)** | 对所有问题使用同一种技术/模式 | 根据问题选择合适的解决方案 |
|
||||
| **过度工程 (Gas Factory)** | 简单问题用复杂方案解决,滥用设计模式 | YAGNI 原则,先简单后复杂 |
|
||||
| **船锚 (Boat Anchor)** | 为"将来可能需要"而写的未使用代码 | 删除未使用代码,需要时再写 |
|
||||
| **复制粘贴编程** | 相同逻辑出现在多处 | 提取公共方法或模块 |
|
||||
|
||||
### 审查问题
|
||||
|
||||
```markdown
|
||||
🔴 [blocking] "这个类有 2000 行代码,建议拆分为多个专注的类"
|
||||
🟡 [important] "这段逻辑在 3 个地方重复,考虑提取为公共方法?"
|
||||
💡 [suggestion] "这个 switch 语句可以用策略模式替代,更易扩展"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 耦合度与内聚性评估
|
||||
|
||||
### 耦合类型(从好到差)
|
||||
|
||||
| 类型 | 描述 | 示例 |
|
||||
|------|------|------|
|
||||
| **消息耦合** ✅ | 通过参数传递数据 | `calculate(price, quantity)` |
|
||||
| **数据耦合** ✅ | 共享简单数据结构 | `processOrder(orderDTO)` |
|
||||
| **印记耦合** ⚠️ | 共享复杂数据结构但只用部分 | 传入整个 User 对象但只用 name |
|
||||
| **控制耦合** ⚠️ | 传递控制标志影响行为 | `process(data, isAdmin=true)` |
|
||||
| **公共耦合** ❌ | 共享全局变量 | 多个模块读写同一个全局状态 |
|
||||
| **内容耦合** ❌ | 直接访问另一模块的内部 | 直接操作另一个类的私有属性 |
|
||||
|
||||
### 内聚类型(从好到差)
|
||||
|
||||
| 类型 | 描述 | 质量 |
|
||||
|------|------|------|
|
||||
| **功能内聚** | 所有元素完成单一任务 | ✅ 最佳 |
|
||||
| **顺序内聚** | 输出作为下一步输入 | ✅ 良好 |
|
||||
| **通信内聚** | 操作相同数据 | ⚠️ 可接受 |
|
||||
| **时间内聚** | 同时执行的任务 | ⚠️ 较差 |
|
||||
| **逻辑内聚** | 逻辑相关但功能不同 | ❌ 差 |
|
||||
| **偶然内聚** | 没有明显关系 | ❌ 最差 |
|
||||
|
||||
### 度量指标参考
|
||||
|
||||
```yaml
|
||||
耦合指标:
|
||||
CBO (类间耦合):
|
||||
好: < 5
|
||||
警告: 5-10
|
||||
危险: > 10
|
||||
|
||||
Ce (传出耦合):
|
||||
描述: 依赖多少外部类
|
||||
好: < 7
|
||||
|
||||
Ca (传入耦合):
|
||||
描述: 被多少类依赖
|
||||
高值意味着: 修改影响大,需要稳定
|
||||
|
||||
内聚指标:
|
||||
LCOM4 (方法缺乏内聚):
|
||||
1: 单一职责 ✅
|
||||
2-3: 可能需要拆分 ⚠️
|
||||
>3: 应该拆分 ❌
|
||||
```
|
||||
|
||||
### 审查问题
|
||||
|
||||
- "这个模块依赖了多少其他模块?能否减少?"
|
||||
- "修改这个类会影响多少其他地方?"
|
||||
- "这个类的方法是否都操作相同的数据?"
|
||||
|
||||
---
|
||||
|
||||
## 分层架构审查
|
||||
|
||||
### Clean Architecture 层次检查
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Frameworks & Drivers │ ← 最外层:Web、DB、UI
|
||||
├─────────────────────────────────────┤
|
||||
│ Interface Adapters │ ← Controllers、Gateways、Presenters
|
||||
├─────────────────────────────────────┤
|
||||
│ Application Layer │ ← Use Cases、Application Services
|
||||
├─────────────────────────────────────┤
|
||||
│ Domain Layer │ ← Entities、Domain Services
|
||||
└─────────────────────────────────────┘
|
||||
↑ 依赖方向只能向内 ↑
|
||||
```
|
||||
|
||||
### 依赖规则检查
|
||||
|
||||
**核心规则:源代码依赖只能指向内层**
|
||||
|
||||
```typescript
|
||||
// ❌ 违反依赖规则:Domain 层依赖 Infrastructure
|
||||
// domain/User.ts
|
||||
import { MySQLConnection } from '../infrastructure/database';
|
||||
|
||||
// ✅ 正确:Domain 层定义接口,Infrastructure 实现
|
||||
// domain/UserRepository.ts (接口)
|
||||
interface UserRepository {
|
||||
findById(id: string): Promise<User>;
|
||||
}
|
||||
|
||||
// infrastructure/MySQLUserRepository.ts (实现)
|
||||
class MySQLUserRepository implements UserRepository {
|
||||
findById(id: string): Promise<User> { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### 审查清单
|
||||
|
||||
**层次边界检查:**
|
||||
- [ ] Domain 层是否有外部依赖(数据库、HTTP、文件系统)?
|
||||
- [ ] Application 层是否直接操作数据库或调用外部 API?
|
||||
- [ ] Controller 是否包含业务逻辑?
|
||||
- [ ] 是否存在跨层调用(UI 直接调用 Repository)?
|
||||
|
||||
**关注点分离检查:**
|
||||
- [ ] 业务逻辑是否与展示逻辑分离?
|
||||
- [ ] 数据访问是否封装在专门的层?
|
||||
- [ ] 配置和环境相关代码是否集中管理?
|
||||
|
||||
### 审查问题
|
||||
|
||||
```markdown
|
||||
🔴 [blocking] "Domain 实体直接导入了数据库连接,违反依赖规则"
|
||||
🟡 [important] "Controller 包含业务计算逻辑,建议移到 Service 层"
|
||||
💡 [suggestion] "考虑使用依赖注入来解耦这些组件"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计模式使用评估
|
||||
|
||||
### 何时使用设计模式
|
||||
|
||||
| 模式 | 适用场景 | 不适用场景 |
|
||||
|------|----------|------------|
|
||||
| **Factory** | 需要创建不同类型对象,类型在运行时确定 | 只有一种类型,或类型固定不变 |
|
||||
| **Strategy** | 算法需要在运行时切换,有多种可互换的行为 | 只有一种算法,或算法不会变化 |
|
||||
| **Observer** | 一对多依赖,状态变化需要通知多个对象 | 简单的直接调用即可满足需求 |
|
||||
| **Singleton** | 确实需要全局唯一实例,如配置管理 | 可以通过依赖注入传递的对象 |
|
||||
| **Decorator** | 需要动态添加职责,避免继承爆炸 | 职责固定,不需要动态组合 |
|
||||
|
||||
### 过度设计警告信号
|
||||
|
||||
```
|
||||
⚠️ Patternitis(模式炎)识别信号:
|
||||
|
||||
1. 简单的 if/else 被替换为策略模式 + 工厂 + 注册表
|
||||
2. 只有一个实现的接口
|
||||
3. 为了"将来可能需要"而添加的抽象层
|
||||
4. 代码行数因模式应用而大幅增加
|
||||
5. 新人需要很长时间才能理解代码结构
|
||||
```
|
||||
|
||||
### 审查原则
|
||||
|
||||
```markdown
|
||||
✅ 正确使用模式:
|
||||
- 解决了实际的可扩展性问题
|
||||
- 代码更容易理解和测试
|
||||
- 添加新功能变得更简单
|
||||
|
||||
❌ 过度使用模式:
|
||||
- 为了使用模式而使用
|
||||
- 增加了不必要的复杂度
|
||||
- 违反了 YAGNI 原则
|
||||
```
|
||||
|
||||
### 审查问题
|
||||
|
||||
- "使用这个模式解决了什么具体问题?"
|
||||
- "如果不用这个模式,代码会有什么问题?"
|
||||
- "这个抽象层带来的价值是否大于它的复杂度?"
|
||||
|
||||
---
|
||||
|
||||
## 可扩展性评估
|
||||
|
||||
### 扩展性检查清单
|
||||
|
||||
**功能扩展性:**
|
||||
- [ ] 添加新功能是否需要修改核心代码?
|
||||
- [ ] 是否提供了扩展点(hooks、plugins、events)?
|
||||
- [ ] 配置是否外部化(配置文件、环境变量)?
|
||||
|
||||
**数据扩展性:**
|
||||
- [ ] 数据模型是否支持新增字段?
|
||||
- [ ] 是否考虑了数据量增长的场景?
|
||||
- [ ] 查询是否有合适的索引?
|
||||
|
||||
**负载扩展性:**
|
||||
- [ ] 是否可以水平扩展(添加更多实例)?
|
||||
- [ ] 是否有状态依赖(session、本地缓存)?
|
||||
- [ ] 数据库连接是否使用连接池?
|
||||
|
||||
### 扩展点设计检查
|
||||
|
||||
```typescript
|
||||
// ✅ 好的扩展设计:使用事件/钩子
|
||||
class OrderService {
|
||||
private hooks: OrderHooks;
|
||||
|
||||
async createOrder(order: Order) {
|
||||
await this.hooks.beforeCreate?.(order);
|
||||
const result = await this.save(order);
|
||||
await this.hooks.afterCreate?.(result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 差的扩展设计:硬编码所有行为
|
||||
class OrderService {
|
||||
async createOrder(order: Order) {
|
||||
await this.sendEmail(order); // 硬编码
|
||||
await this.updateInventory(order); // 硬编码
|
||||
await this.notifyWarehouse(order); // 硬编码
|
||||
return await this.save(order);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 审查问题
|
||||
|
||||
```markdown
|
||||
💡 [suggestion] "如果将来需要支持新的支付方式,这个设计是否容易扩展?"
|
||||
🟡 [important] "这里的逻辑是硬编码的,考虑使用配置或策略模式?"
|
||||
📚 [learning] "事件驱动架构可以让这个功能更容易扩展"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代码结构最佳实践
|
||||
|
||||
### 目录组织
|
||||
|
||||
**按功能/领域组织(推荐):**
|
||||
```
|
||||
src/
|
||||
├── user/
|
||||
│ ├── User.ts (实体)
|
||||
│ ├── UserService.ts (服务)
|
||||
│ ├── UserRepository.ts (数据访问)
|
||||
│ └── UserController.ts (API)
|
||||
├── order/
|
||||
│ ├── Order.ts
|
||||
│ ├── OrderService.ts
|
||||
│ └── ...
|
||||
└── shared/
|
||||
├── utils/
|
||||
└── types/
|
||||
```
|
||||
|
||||
**按技术层组织(不推荐):**
|
||||
```
|
||||
src/
|
||||
├── controllers/ ← 不同领域混在一起
|
||||
│ ├── UserController.ts
|
||||
│ └── OrderController.ts
|
||||
├── services/
|
||||
├── repositories/
|
||||
└── models/
|
||||
```
|
||||
|
||||
### 命名约定检查
|
||||
|
||||
| 类型 | 约定 | 示例 |
|
||||
|------|------|------|
|
||||
| 类名 | PascalCase,名词 | `UserService`, `OrderRepository` |
|
||||
| 方法名 | camelCase,动词 | `createUser`, `findOrderById` |
|
||||
| 接口名 | I 前缀或无前缀 | `IUserService` 或 `UserService` |
|
||||
| 常量 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
|
||||
| 私有属性 | 下划线前缀或无 | `_cache` 或 `#cache` |
|
||||
|
||||
### 文件大小指南
|
||||
|
||||
```yaml
|
||||
建议限制:
|
||||
单个文件: < 300 行
|
||||
单个函数: < 50 行
|
||||
单个类: < 200 行
|
||||
函数参数: < 4 个
|
||||
嵌套深度: < 4 层
|
||||
|
||||
超出限制时:
|
||||
- 考虑拆分为更小的单元
|
||||
- 使用组合而非继承
|
||||
- 提取辅助函数或类
|
||||
```
|
||||
|
||||
### 审查问题
|
||||
|
||||
```markdown
|
||||
🟢 [nit] "这个 500 行的文件可以考虑按职责拆分"
|
||||
🟡 [important] "建议按功能领域而非技术层组织目录结构"
|
||||
💡 [suggestion] "函数名 `process` 不够明确,考虑改为 `calculateOrderTotal`?"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速参考清单
|
||||
|
||||
### 架构审查 5 分钟速查
|
||||
|
||||
```markdown
|
||||
□ 依赖方向是否正确?(外层依赖内层)
|
||||
□ 是否存在循环依赖?
|
||||
□ 核心业务逻辑是否与框架/UI/数据库解耦?
|
||||
□ 是否遵循 SOLID 原则?
|
||||
□ 是否存在明显的反模式?
|
||||
```
|
||||
|
||||
### 红旗信号(必须处理)
|
||||
|
||||
```markdown
|
||||
🔴 God Object - 单个类超过 1000 行
|
||||
🔴 循环依赖 - A → B → C → A
|
||||
🔴 Domain 层包含框架依赖
|
||||
🔴 硬编码的配置和密钥
|
||||
🔴 没有接口的外部服务调用
|
||||
```
|
||||
|
||||
### 黄旗信号(建议处理)
|
||||
|
||||
```markdown
|
||||
🟡 类间耦合度 (CBO) > 10
|
||||
🟡 方法参数超过 5 个
|
||||
🟡 嵌套深度超过 4 层
|
||||
🟡 重复代码块 > 10 行
|
||||
🟡 只有一个实现的接口
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工具推荐
|
||||
|
||||
| 工具 | 用途 | 语言支持 |
|
||||
|------|------|----------|
|
||||
| **SonarQube** | 代码质量、耦合度分析 | 多语言 |
|
||||
| **NDepend** | 依赖分析、架构规则 | .NET |
|
||||
| **JDepend** | 包依赖分析 | Java |
|
||||
| **Madge** | 模块依赖图 | JavaScript/TypeScript |
|
||||
| **ESLint** | 代码规范、复杂度检查 | JavaScript/TypeScript |
|
||||
| **CodeScene** | 技术债务、热点分析 | 多语言 |
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Clean Architecture - Uncle Bob](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||
- [SOLID Principles in Code Review - JetBrains](https://blog.jetbrains.com/upsource/2015/08/31/what-to-look-for-in-a-code-review-solid-principles-2/)
|
||||
- [Software Architecture Anti-Patterns](https://medium.com/@christophnissle/anti-patterns-in-software-architecture-3c8970c9c4f5)
|
||||
- [Coupling and Cohesion in System Design](https://www.geeksforgeeks.org/system-design/coupling-and-cohesion-in-system-design/)
|
||||
- [Design Patterns - Refactoring Guru](https://refactoring.guru/design-patterns)
|
||||
@@ -0,0 +1,285 @@
|
||||
# C Code Review Guide
|
||||
|
||||
> C code review guide focused on memory safety, undefined behavior, and portability. Examples assume C11.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Pointer and Buffer Safety](#pointer-and-buffer-safety)
|
||||
- [Ownership and Resource Management](#ownership-and-resource-management)
|
||||
- [Undefined Behavior Pitfalls](#undefined-behavior-pitfalls)
|
||||
- [Integer Types and Overflow](#integer-types-and-overflow)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Concurrency](#concurrency)
|
||||
- [Macros and Preprocessor](#macros-and-preprocessor)
|
||||
- [API Design and Const](#api-design-and-const)
|
||||
- [Tooling and Build Checks](#tooling-and-build-checks)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Pointer and Buffer Safety
|
||||
|
||||
### Always carry size with buffers
|
||||
|
||||
```c
|
||||
// ❌ Bad: ignores destination size
|
||||
bool copy_name(char *dst, size_t dst_size, const char *src) {
|
||||
strcpy(dst, src);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ✅ Good: validate size and terminate
|
||||
bool copy_name(char *dst, size_t dst_size, const char *src) {
|
||||
size_t len = strlen(src);
|
||||
if (len + 1 > dst_size) {
|
||||
return false;
|
||||
}
|
||||
memcpy(dst, src, len + 1);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid dangerous APIs
|
||||
|
||||
Prefer `snprintf`, `fgets`, and explicit bounds over `gets`, `strcpy`, or `sprintf`.
|
||||
|
||||
```c
|
||||
// ❌ Bad: unbounded write
|
||||
sprintf(buf, "%s", input);
|
||||
|
||||
// ✅ Good: bounded write
|
||||
snprintf(buf, buf_size, "%s", input);
|
||||
```
|
||||
|
||||
### Use the right copy primitive
|
||||
|
||||
```c
|
||||
// ❌ Bad: memcpy with overlapping regions
|
||||
memcpy(dst, src, len);
|
||||
|
||||
// ✅ Good: memmove handles overlap
|
||||
memmove(dst, src, len);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ownership and Resource Management
|
||||
|
||||
### One allocation, one free
|
||||
|
||||
Track ownership and clean up on every error path.
|
||||
|
||||
```c
|
||||
// ✅ Good: cleanup label avoids leaks
|
||||
int load_file(const char *path) {
|
||||
int rc = -1;
|
||||
FILE *f = NULL;
|
||||
char *buf = NULL;
|
||||
|
||||
f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
goto cleanup;
|
||||
}
|
||||
buf = malloc(4096);
|
||||
if (!buf) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (fread(buf, 1, 4096, f) == 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rc = 0;
|
||||
|
||||
cleanup:
|
||||
free(buf);
|
||||
if (f) {
|
||||
fclose(f);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Undefined Behavior Pitfalls
|
||||
|
||||
### Common UB patterns
|
||||
|
||||
```c
|
||||
// ❌ Bad: use after free
|
||||
char *p = malloc(10);
|
||||
free(p);
|
||||
p[0] = 'a';
|
||||
|
||||
// ❌ Bad: uninitialized read
|
||||
int x;
|
||||
if (x > 0) { /* UB */ }
|
||||
|
||||
// ❌ Bad: signed overflow
|
||||
int sum = a + b;
|
||||
```
|
||||
|
||||
### Avoid pointer arithmetic past the object
|
||||
|
||||
```c
|
||||
// ❌ Bad: pointer past the end then dereference
|
||||
int arr[4];
|
||||
int *p = arr + 4;
|
||||
int v = *p; // UB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integer Types and Overflow
|
||||
|
||||
### Avoid signed/unsigned surprises
|
||||
|
||||
```c
|
||||
// ❌ Bad: negative converted to large size_t
|
||||
int len = -1;
|
||||
size_t n = len;
|
||||
|
||||
// ✅ Good: validate before converting
|
||||
if (len < 0) {
|
||||
return -1;
|
||||
}
|
||||
size_t n = (size_t)len;
|
||||
```
|
||||
|
||||
### Check for overflow in size calculations
|
||||
|
||||
```c
|
||||
// ❌ Bad: potential overflow in multiplication
|
||||
size_t bytes = count * sizeof(Item);
|
||||
|
||||
// ✅ Good: check before multiplying
|
||||
if (count > SIZE_MAX / sizeof(Item)) {
|
||||
return NULL;
|
||||
}
|
||||
size_t bytes = count * sizeof(Item);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Always check return values
|
||||
|
||||
```c
|
||||
// ❌ Bad: ignore errors
|
||||
fread(buf, 1, size, f);
|
||||
|
||||
// ✅ Good: handle errors
|
||||
size_t read = fread(buf, 1, size, f);
|
||||
if (read != size && ferror(f)) {
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
### Consistent error contracts
|
||||
|
||||
- Use a clear convention: 0 for success, negative for failure.
|
||||
- Document ownership rules on success and failure.
|
||||
- If using `errno`, set it only for actual failures.
|
||||
|
||||
---
|
||||
|
||||
## Concurrency
|
||||
|
||||
### volatile is not synchronization
|
||||
|
||||
```c
|
||||
// ❌ Bad: data race
|
||||
volatile int stop = 0;
|
||||
void worker(void) {
|
||||
while (!stop) { /* ... */ }
|
||||
}
|
||||
|
||||
// ✅ Good: C11 atomics
|
||||
_Atomic int stop = 0;
|
||||
void worker(void) {
|
||||
while (!atomic_load(&stop)) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Use mutexes for shared state
|
||||
|
||||
Protect shared data with `pthread_mutex_t` or equivalent. Avoid holding locks while doing I/O.
|
||||
|
||||
---
|
||||
|
||||
## Macros and Preprocessor
|
||||
|
||||
### Parenthesize arguments
|
||||
|
||||
```c
|
||||
// ❌ Bad: macro with side effects
|
||||
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
||||
int x = MIN(i++, j++);
|
||||
|
||||
// ✅ Good: static inline function
|
||||
static inline int min_int(int a, int b) {
|
||||
return a < b ? a : b;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Design and Const
|
||||
|
||||
### Const-correctness and sizes
|
||||
|
||||
```c
|
||||
// ✅ Good: explicit size and const input
|
||||
int hash_bytes(const uint8_t *data, size_t len, uint8_t *out);
|
||||
```
|
||||
|
||||
### Document nullability
|
||||
|
||||
Clearly document whether pointers may be NULL. Prefer returning error codes instead of NULL when possible.
|
||||
|
||||
---
|
||||
|
||||
## Tooling and Build Checks
|
||||
|
||||
```bash
|
||||
# Warnings
|
||||
clang -Wall -Wextra -Werror -Wconversion -Wshadow -std=c11 ...
|
||||
|
||||
# Sanitizers (debug builds)
|
||||
clang -fsanitize=address,undefined -fno-omit-frame-pointer -g ...
|
||||
clang -fsanitize=thread -fno-omit-frame-pointer -g ...
|
||||
|
||||
# Static analysis
|
||||
clang-tidy src/*.c -- -std=c11
|
||||
cppcheck --enable=warning,performance,portability src/
|
||||
|
||||
# Formatting
|
||||
clang-format -i src/*.c include/*.h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Memory and UB
|
||||
- [ ] All buffers have explicit size parameters
|
||||
- [ ] No out-of-bounds access or pointer arithmetic past objects
|
||||
- [ ] No use after free or uninitialized reads
|
||||
- [ ] Signed overflow and shift rules are respected
|
||||
|
||||
### API and Design
|
||||
- [ ] Ownership rules are documented and consistent
|
||||
- [ ] const-correctness is applied for inputs
|
||||
- [ ] Error contracts are clear and consistent
|
||||
|
||||
### Concurrency
|
||||
- [ ] No data races on shared state
|
||||
- [ ] volatile is not used for synchronization
|
||||
- [ ] Locks are held for minimal time
|
||||
|
||||
### Tooling and Tests
|
||||
- [ ] Builds clean with warnings enabled
|
||||
- [ ] Sanitizers run on critical code paths
|
||||
- [ ] Static analysis results are addressed
|
||||
@@ -0,0 +1,488 @@
|
||||
# Universal Code Quality Anti-Patterns
|
||||
|
||||
> 语言无关的代码质量反模式指南,覆盖代码复用、抽象泄漏、参数膨胀、嵌套条件、字符串类型化、TOCTOU、空操作更新等核心主题。适用于所有语言的 PR 审查。
|
||||
|
||||
## 目录
|
||||
|
||||
- [代码复用审查](#代码复用审查)
|
||||
- [参数膨胀](#参数膨胀)
|
||||
- [抽象泄漏](#抽象泄漏)
|
||||
- [字符串类型化](#字符串类型化)
|
||||
- [嵌套条件表达式](#嵌套条件表达式)
|
||||
- [复制粘贴变种](#复制粘贴变种)
|
||||
- [空操作更新](#空操作更新)
|
||||
- [TOCTOU 竞争条件](#toctou-竞争条件)
|
||||
- [过度宽泛操作](#过度宽泛操作)
|
||||
- [冗余状态](#冗余状态)
|
||||
- [通用质量审查清单](#通用质量审查清单)
|
||||
|
||||
---
|
||||
|
||||
## 代码复用审查
|
||||
|
||||
Before accepting new code, search the existing codebase for reusable utilities.
|
||||
|
||||
### 搜索现有工具函数
|
||||
|
||||
```python
|
||||
# ❌ 新写的路径拼接逻辑——项目中已有 PathBuilder
|
||||
def get_config_path(name):
|
||||
base = os.environ.get("APP_ROOT", ".")
|
||||
return os.path.join(base, "config", name + ".json")
|
||||
|
||||
# ✅ 使用已有的 PathBuilder
|
||||
def get_config_path(name):
|
||||
return PathBuilder.config(f"{name}.json")
|
||||
```
|
||||
|
||||
```javascript
|
||||
// ❌ 手写 debounce——项目已有 lodash 或 utils/debounce.ts
|
||||
function debounce(fn, ms) {
|
||||
let timer;
|
||||
return (...args) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn(...args), ms);
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ 使用已有的工具函数
|
||||
import { debounce } from "@/utils/debounce";
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 新增函数是否与已有 utility 重名或功能重叠?
|
||||
- inline 逻辑是否可以提取为已有模块的调用?
|
||||
- 检查相邻文件和 shared/utils 目录
|
||||
|
||||
---
|
||||
|
||||
## 参数膨胀
|
||||
|
||||
### 函数参数不断增长
|
||||
|
||||
```python
|
||||
# ❌ 每次新需求加一个参数
|
||||
def create_user(name, email, role, team, active, avatar_url, timezone):
|
||||
...
|
||||
|
||||
# ✅ 使用配置对象 / dataclass
|
||||
@dataclass
|
||||
class CreateUserParams:
|
||||
name: str
|
||||
email: str
|
||||
role: Role = Role.MEMBER
|
||||
team: str | None = None
|
||||
active: bool = True
|
||||
avatar_url: str | None = None
|
||||
timezone: str = "UTC"
|
||||
|
||||
def create_user(params: CreateUserParams) -> User:
|
||||
...
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ 6+ 个 positional 参数
|
||||
function renderWidget(
|
||||
title: string, width: number, height: number,
|
||||
theme: string, collapsible: boolean, icon: string
|
||||
) { ... }
|
||||
|
||||
// ✅ Options object pattern
|
||||
interface WidgetOptions {
|
||||
title: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
theme?: "light" | "dark";
|
||||
collapsible?: boolean;
|
||||
icon?: string;
|
||||
}
|
||||
function renderWidget(options: WidgetOptions) { ... }
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 函数参数是否 ≥ 4 个?考虑 options object / dataclass
|
||||
- 新参数是否只是布尔标志?考虑 enum 或 strategy pattern
|
||||
- 是否有 `enable_x`, `disable_y` 这类互斥参数?
|
||||
|
||||
---
|
||||
|
||||
## 抽象泄漏
|
||||
|
||||
### 暴露内部实现细节
|
||||
|
||||
```python
|
||||
# ❌ 返回内部 ORM 对象——调用者被迫了解 SQLAlchemy
|
||||
def get_users():
|
||||
return session.query(User).filter(User.active == True).all()
|
||||
|
||||
# ✅ 返回 domain 对象,隐藏持久化层
|
||||
def get_active_users() -> list[UserDTO]:
|
||||
rows = user_repo.find_active()
|
||||
return [UserDTO.from_row(r) for r in rows]
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ 组件接收 API response 原始结构
|
||||
<UserCard user={apiResponse.data.results[0]} />
|
||||
|
||||
// ✅ 组件接收 domain 类型,adapter 处理映射
|
||||
interface UserSummary {
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
<UserCard user={adaptUser(apiResponse)} />
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 函数返回类型是否泄露底层实现(ORM, HTTP client, file format)?
|
||||
- 组件/函数是否依赖外部系统的数据结构?
|
||||
- 是否破坏了已有的抽象边界?
|
||||
|
||||
---
|
||||
|
||||
## 字符串类型化
|
||||
|
||||
### 用原始字符串代替常量/枚举
|
||||
|
||||
```python
|
||||
# ❌ Magic strings 散落各处
|
||||
if status == "active":
|
||||
...
|
||||
if role == "admin":
|
||||
...
|
||||
|
||||
# ✅ 使用 enum
|
||||
class Status(StrEnum):
|
||||
ACTIVE = "active"
|
||||
SUSPENDED = "suspended"
|
||||
ARCHIVED = "archived"
|
||||
|
||||
if user.status == Status.ACTIVE:
|
||||
...
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ Raw string event names——拼写错误不会报错
|
||||
emitter.emit("userCreated", data);
|
||||
emitter.on("usercreated", handler); // bug: typo
|
||||
|
||||
// ✅ 常量或 branded type
|
||||
const Events = {
|
||||
USER_CREATED: "userCreated",
|
||||
USER_SUSPENDED: "userSuspended",
|
||||
} as const;
|
||||
emitter.emit(Events.USER_CREATED, data);
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 是否用字符串代替了已有的 enum/union type?
|
||||
- 事件名、action type、status 值是否散落在多个文件?
|
||||
- 字符串比较是否 case-sensitive 但未验证?
|
||||
|
||||
---
|
||||
|
||||
## 嵌套条件表达式
|
||||
|
||||
### 三元链和嵌套 if/else
|
||||
|
||||
```python
|
||||
# ❌ 三元链难以阅读
|
||||
label = (
|
||||
"Admin" if role == "admin" else
|
||||
"Manager" if role == "manager" else
|
||||
"Viewer" if role == "viewer" else
|
||||
"Unknown"
|
||||
)
|
||||
|
||||
# ✅ 查找表或 match
|
||||
ROLE_LABELS = {
|
||||
"admin": "Admin",
|
||||
"manager": "Manager",
|
||||
"viewer": "Viewer",
|
||||
}
|
||||
label = ROLE_LABELS.get(role, "Unknown")
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ 嵌套三元
|
||||
const bg = isHovered
|
||||
? isSelected ? "blue" : "gray"
|
||||
: isSelected ? "navy" : "white";
|
||||
|
||||
// ✅ 查找表(lookup map)
|
||||
const bgMap: Record<string, string> = {
|
||||
"true-true": "blue",
|
||||
"true-false": "gray",
|
||||
"false-true": "navy",
|
||||
"false-false": "white",
|
||||
};
|
||||
const bg = bgMap[`${isHovered}-${isSelected}`];
|
||||
```
|
||||
|
||||
```python
|
||||
# ❌ 嵌套 if 3+ 层
|
||||
def process(order):
|
||||
if order is not None:
|
||||
if order.items:
|
||||
for item in order.items:
|
||||
if item.price > 0:
|
||||
...
|
||||
|
||||
# ✅ Early return + guard clauses
|
||||
def process(order):
|
||||
if not order or not order.items:
|
||||
return
|
||||
for item in order.items:
|
||||
if item.price <= 0:
|
||||
continue
|
||||
...
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 三元表达式是否嵌套 ≥ 2 层?
|
||||
- if/else 嵌套是否 ≥ 3 层?
|
||||
- 能否用 lookup table、early return 或 match 替换?
|
||||
|
||||
---
|
||||
|
||||
## 复制粘贴变种
|
||||
|
||||
### 近乎重复的代码块
|
||||
|
||||
```python
|
||||
# ❌ 两个函数几乎一样,只有字段名不同
|
||||
def format_user(user):
|
||||
return f"{user.first_name} {user.last_name} ({user.email})"
|
||||
|
||||
def format_employee(emp):
|
||||
return f"{emp.first_name} {emp.last_name} ({emp.work_email})"
|
||||
|
||||
# ✅ 统一抽象
|
||||
def format_person(first: str, last: str, email: str) -> str:
|
||||
return f"{first} {last} ({email})"
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ Copy-paste handler 只改了 URL
|
||||
async function deletePost(id: string) {
|
||||
await fetch(`/api/posts/${id}`, { method: "DELETE" });
|
||||
router.push("/posts");
|
||||
}
|
||||
async function deleteComment(id: string) {
|
||||
await fetch(`/api/comments/${id}`, { method: "DELETE" });
|
||||
router.push("/comments");
|
||||
}
|
||||
|
||||
// ✅ 参数化
|
||||
async function deleteResource(resource: string, id: string) {
|
||||
await fetch(`/api/${resource}/${id}`, { method: "DELETE" });
|
||||
router.push(`/${resource}`);
|
||||
}
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 是否有 ≥ 2 段代码仅变量名/URL/字符串不同?
|
||||
- 能否提取参数化的共享函数?
|
||||
- 是否可以用 template method 或 strategy 消除变种?
|
||||
|
||||
---
|
||||
|
||||
## 空操作更新
|
||||
|
||||
### 无条件触发状态更新
|
||||
|
||||
```typescript
|
||||
// ❌ 每次 poll 都触发 update——即使数据未变
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
fetch("/api/status").then(r => r.json()).then(setStatus);
|
||||
}, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// ✅ 仅在值变化时更新
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
fetch("/api/status")
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
setStatus(prev => isEqual(prev, data) ? prev : data);
|
||||
});
|
||||
}, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
```
|
||||
|
||||
```python
|
||||
# ❌ 每次 loop 都写 DB——即使值未变
|
||||
for item in items:
|
||||
item.status = compute_status(item)
|
||||
session.commit()
|
||||
|
||||
# ✅ 仅在变化时写入
|
||||
for item in items:
|
||||
new_status = compute_status(item)
|
||||
if item.status != new_status:
|
||||
item.status = new_status
|
||||
session.commit()
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- polling / interval / event handler 是否无条件更新?
|
||||
- wrapper function 是否尊重 same-reference return?
|
||||
- DB 写入是否检查了实际变化?
|
||||
|
||||
---
|
||||
|
||||
## TOCTOU 竞争条件
|
||||
|
||||
### Time-of-Check-to-Time-of-Use
|
||||
|
||||
```python
|
||||
# ❌ 先检查后操作——中间文件可能被删除/创建
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
data = f.read()
|
||||
|
||||
# ✅ 直接操作 + 处理异常
|
||||
try:
|
||||
with open(path) as f:
|
||||
data = f.read()
|
||||
except FileNotFoundError:
|
||||
data = None
|
||||
```
|
||||
|
||||
```python
|
||||
# ❌ 检查余额 → 扣款 两步操作不是原子的
|
||||
if account.balance >= amount:
|
||||
account.balance -= amount
|
||||
|
||||
# ✅ 原子操作或锁
|
||||
with account.lock:
|
||||
if account.balance < amount:
|
||||
raise InsufficientFundsError()
|
||||
account.balance -= amount
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ Check-then-act 在 async 环境中不安全
|
||||
if (!fileExists(path)) {
|
||||
await writeFile(path, content);
|
||||
}
|
||||
|
||||
// ✅ 直接操作 + catch
|
||||
try {
|
||||
await writeFile(path, content, { flag: "wx" });
|
||||
} catch (e) {
|
||||
if (e.code === "EEXIST") { /* handle */ }
|
||||
else throw e;
|
||||
}
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- `if exists → operate` 模式是否可替换为 `try operate → catch`?
|
||||
- 多步状态变更是否在事务/锁内?
|
||||
- async 操作中 check 和 act 之间是否有 await?
|
||||
|
||||
---
|
||||
|
||||
## 过度宽泛操作
|
||||
|
||||
### 读取过多数据
|
||||
|
||||
```python
|
||||
# ❌ 读取整个文件再取第一行
|
||||
content = Path("log.txt").read_text()
|
||||
first_line = content.split("\n")[0]
|
||||
|
||||
# ✅ 只读第一行,不加载整个文件
|
||||
with open("log.txt") as f:
|
||||
first_line = f.readline()
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ 加载所有 items 再过滤
|
||||
const allItems = await db.query("SELECT * FROM orders");
|
||||
const pending = allItems.filter(o => o.status === "pending");
|
||||
|
||||
// ✅ 数据库层过滤
|
||||
const pending = await db.query(
|
||||
"SELECT * FROM orders WHERE status = ?", ["pending"]
|
||||
);
|
||||
```
|
||||
|
||||
```python
|
||||
# ❌ 读取整个列表找一条记录
|
||||
users = list(User.objects.all())
|
||||
user = next(u for u in users if u.id == user_id)
|
||||
|
||||
# ✅ 精确查询
|
||||
user = User.objects.get(id=user_id)
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 是否读取了整个集合/文件再只用一小部分?
|
||||
- 能否将过滤推到数据库/存储层?
|
||||
- API 调用是否支持 pagination/limit 参数?
|
||||
|
||||
---
|
||||
|
||||
## 冗余状态
|
||||
|
||||
### 状态可以被推导
|
||||
|
||||
```typescript
|
||||
// ❌ 同时存储 fullName 和 firstName + lastName
|
||||
interface User {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
fullName: string; // redundant
|
||||
}
|
||||
|
||||
// ✅ fullName 是推导值
|
||||
interface User {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}
|
||||
const fullName = `${user.firstName} ${user.lastName}`;
|
||||
```
|
||||
|
||||
```python
|
||||
# ❌ 缓存值在源数据变化时可能过时
|
||||
class Order:
|
||||
total: float
|
||||
item_count: int # redundant if len(items) gives the same
|
||||
items: list[Item]
|
||||
|
||||
# ✅ 推导或 property
|
||||
class Order:
|
||||
items: list[Item]
|
||||
|
||||
@property
|
||||
def total(self) -> float:
|
||||
return sum(item.price for item in self.items)
|
||||
|
||||
@property
|
||||
def item_count(self) -> int:
|
||||
return len(self.items)
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 是否有字段可以从其他字段推导?
|
||||
- 缓存值是否有 invalidation 机制?
|
||||
- observer/effect 是否可以替换为直接调用?
|
||||
|
||||
---
|
||||
|
||||
## 通用质量审查清单
|
||||
|
||||
- [ ] **复用审查**: 搜索了现有 utility/helper,没有重复造轮子?
|
||||
- [ ] **参数数量**: 函数参数 ≤ 3 个?超过则用 options object / dataclass?
|
||||
- [ ] **抽象边界**: 返回类型没有暴露内部实现细节(ORM、HTTP client、file format)?
|
||||
- [ ] **类型安全**: 没有 magic strings 代替已有的 enum/constant/union type?
|
||||
- [ ] **条件深度**: 三元嵌套 ≤ 1 层?if/else 嵌套 ≤ 2 层?
|
||||
- [ ] **DRY**: 没有 copy-paste-with-variation(≥ 2 段近似代码)?
|
||||
- [ ] **空操作防护**: polling / interval / event handler 有 change-detection guard?
|
||||
- [ ] **TOCTOU**: `if exists → operate` 替换为 `try operate → catch`?
|
||||
- [ ] **数据精度**: 没有读取整个集合/文件只为了取子集?
|
||||
- [ ] **冗余状态**: 没有可以从其他字段推导的存储字段?
|
||||
@@ -0,0 +1,136 @@
|
||||
# Code Review Best Practices
|
||||
|
||||
Comprehensive guidelines for conducting effective code reviews.
|
||||
|
||||
## Review Philosophy
|
||||
|
||||
### Goals of Code Review
|
||||
|
||||
**Primary Goals:**
|
||||
- Catch bugs and edge cases before production
|
||||
- Ensure code maintainability and readability
|
||||
- Share knowledge across the team
|
||||
- Enforce coding standards consistently
|
||||
- Improve design and architecture decisions
|
||||
|
||||
**Secondary Goals:**
|
||||
- Mentor junior developers
|
||||
- Build team culture and trust
|
||||
- Document design decisions through discussions
|
||||
|
||||
### What Code Review is NOT
|
||||
|
||||
- A gatekeeping mechanism to block progress
|
||||
- An opportunity to show off knowledge
|
||||
- A place to nitpick formatting (use linters)
|
||||
- A way to rewrite code to personal preference
|
||||
|
||||
## Review Timing
|
||||
|
||||
### When to Review
|
||||
|
||||
| Trigger | Action |
|
||||
|---------|--------|
|
||||
| PR opened | Review within 24 hours, ideally same day |
|
||||
| Changes requested | Re-review within 4 hours |
|
||||
| Blocking issue found | Communicate immediately |
|
||||
|
||||
### Time Allocation
|
||||
|
||||
- **Small PR (<100 lines)**: 10-15 minutes
|
||||
- **Medium PR (100-400 lines)**: 20-40 minutes
|
||||
- **Large PR (>400 lines)**: Request to split, or 60+ minutes
|
||||
|
||||
## Review Depth Levels
|
||||
|
||||
### Level 1: Skim Review (5 minutes)
|
||||
- Check PR description and linked issues
|
||||
- Verify CI/CD status
|
||||
- Look at file changes overview
|
||||
- Identify if deeper review needed
|
||||
|
||||
### Level 2: Standard Review (20-30 minutes)
|
||||
- Full code walkthrough
|
||||
- Logic verification
|
||||
- Test coverage check
|
||||
- Security scan
|
||||
|
||||
### Level 3: Deep Review (60+ minutes)
|
||||
- Architecture evaluation
|
||||
- Performance analysis
|
||||
- Security audit
|
||||
- Edge case exploration
|
||||
|
||||
## Communication Guidelines
|
||||
|
||||
### Tone and Language
|
||||
|
||||
**Use collaborative language:**
|
||||
- "What do you think about..." instead of "You should..."
|
||||
- "Could we consider..." instead of "This is wrong"
|
||||
- "I'm curious about..." instead of "Why didn't you..."
|
||||
|
||||
**Be specific and actionable:**
|
||||
- Include code examples when suggesting changes
|
||||
- Link to documentation or past discussions
|
||||
- Explain the "why" behind suggestions
|
||||
|
||||
### Handling Disagreements
|
||||
|
||||
1. **Seek to understand**: Ask clarifying questions
|
||||
2. **Acknowledge valid points**: Show you've considered their perspective
|
||||
3. **Provide data**: Use benchmarks, docs, or examples
|
||||
4. **Escalate if needed**: Involve senior dev or architect
|
||||
5. **Know when to let go**: Not every hill is worth dying on
|
||||
|
||||
## Review Prioritization
|
||||
|
||||
### Must Fix (Blocking)
|
||||
- Security vulnerabilities
|
||||
- Data corruption risks
|
||||
- Breaking changes without migration
|
||||
- Critical performance issues
|
||||
- Missing error handling for user-facing features
|
||||
|
||||
### Should Fix (Important)
|
||||
- Test coverage gaps
|
||||
- Moderate performance concerns
|
||||
- Code duplication
|
||||
- Unclear naming or structure
|
||||
- Missing documentation for complex logic
|
||||
|
||||
### Nice to Have (Non-blocking)
|
||||
- Style preferences beyond linting
|
||||
- Minor optimizations
|
||||
- Additional test cases
|
||||
- Documentation improvements
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### Reviewer Anti-Patterns
|
||||
- **Rubber stamping**: Approving without actually reviewing
|
||||
- **Bike shedding**: Debating trivial details extensively
|
||||
- **Scope creep**: "While you're at it, can you also..."
|
||||
- **Ghosting**: Requesting changes then disappearing
|
||||
- **Perfectionism**: Blocking for minor style preferences
|
||||
|
||||
### Author Anti-Patterns
|
||||
- **Mega PRs**: Submitting 1000+ line changes
|
||||
- **No context**: Missing PR description or linked issues
|
||||
- **Defensive responses**: Arguing every suggestion
|
||||
- **Silent updates**: Making changes without responding to comments
|
||||
|
||||
## Metrics and Improvement
|
||||
|
||||
### Track These Metrics
|
||||
- Time to first review
|
||||
- Review cycle time
|
||||
- Number of review rounds
|
||||
- Defect escape rate
|
||||
- Review coverage percentage
|
||||
|
||||
### Continuous Improvement
|
||||
- Hold retrospectives on review process
|
||||
- Share learnings from escaped bugs
|
||||
- Update checklists based on common issues
|
||||
- Celebrate good reviews and catches
|
||||
@@ -0,0 +1,248 @@
|
||||
# Common Bugs Checklist
|
||||
|
||||
Quick-reference bug patterns organized by category. For detailed code examples, explanations, and comprehensive review checklists, see the dedicated language guides linked below.
|
||||
|
||||
## Universal Issues
|
||||
|
||||
### Logic Errors
|
||||
- [ ] Off-by-one errors in loops and array access
|
||||
- [ ] Incorrect boolean logic (De Morgan's law violations)
|
||||
- [ ] Missing null/undefined checks
|
||||
- [ ] Race conditions in concurrent code
|
||||
- [ ] Incorrect comparison operators (`==` vs `===`, `=` vs `==`)
|
||||
- [ ] Integer overflow/underflow
|
||||
- [ ] Floating point comparison issues
|
||||
|
||||
### Resource Management
|
||||
- [ ] Memory leaks (unclosed connections, listeners)
|
||||
- [ ] File handles not closed
|
||||
- [ ] Database connections not released
|
||||
- [ ] Event listeners not removed
|
||||
- [ ] Timers/intervals not cleared
|
||||
|
||||
### Error Handling
|
||||
- [ ] Swallowed exceptions (empty catch blocks)
|
||||
- [ ] Generic exception handling hiding specific errors
|
||||
- [ ] Missing error propagation
|
||||
- [ ] Incorrect error types thrown
|
||||
- [ ] Missing finally/cleanup blocks
|
||||
|
||||
## TypeScript/JavaScript
|
||||
|
||||
- [ ] `==` instead of `===`
|
||||
- [ ] Using `any` — prefer proper types or `unknown` with type guards
|
||||
- [ ] Missing `await` on async calls
|
||||
- [ ] Unhandled promise rejections (no try-catch around await)
|
||||
- [ ] `this` context lost in callbacks
|
||||
- [ ] Missing `key` prop in lists
|
||||
- [ ] Closure capturing stale loop variable
|
||||
- [ ] `parseInt` without radix parameter
|
||||
- [ ] Modifying array/object during iteration
|
||||
|
||||
**Full guide:** [TypeScript Review Guide](typescript.md)
|
||||
|
||||
## React / React 19
|
||||
|
||||
- [ ] Hooks called conditionally or in loops (violates Rules of Hooks)
|
||||
- [ ] `useEffect` dependency array incomplete or incorrect
|
||||
- [ ] `useEffect` missing cleanup function (subscriptions, timers, fetches)
|
||||
- [ ] `useEffect` used for derived state (use `useMemo` instead)
|
||||
- [ ] `useMemo`/`useCallback` over-used or used without `React.memo`
|
||||
- [ ] Component defined inside another component (re-mounts every render)
|
||||
- [ ] Unstable props (inline objects/functions passed to memo components)
|
||||
- [ ] Direct mutation of props
|
||||
- [ ] List missing `key` or using array index as key (reorderable lists)
|
||||
- [ ] Server Component using client APIs (`useState`, `useEffect`, `onClick`)
|
||||
- [ ] `'use client'` on parent making entire subtree client-side
|
||||
- [ ] `useActionState` calling `setState` instead of returning new state
|
||||
- [ ] `useFormStatus` called in same component as `<form>` (must be in child)
|
||||
- [ ] `useOptimistic` used for critical operations (payments, deletions)
|
||||
- [ ] Single Suspense boundary for entire page (slow blocks fast)
|
||||
- [ ] Missing Error Boundary wrapping Suspense
|
||||
- [ ] `use()` Hook receiving a new Promise each render
|
||||
|
||||
**TanStack Query v5:**
|
||||
- [ ] `queryKey` missing parameters that affect data
|
||||
- [ ] Default `staleTime: 0` causing excessive refetches
|
||||
- [ ] `useSuspenseQuery` with `enabled` option (not supported)
|
||||
- [ ] Mutation not invalidating related queries on success
|
||||
- [ ] Optimistic update missing rollback in `onError`
|
||||
- [ ] Using v4 array syntax (`useQuery(['key'], fn)`) instead of v5 object syntax
|
||||
|
||||
**Testing:**
|
||||
- [ ] Using `container.querySelector` instead of `screen.getByRole`
|
||||
- [ ] Using `fireEvent` instead of `userEvent`
|
||||
- [ ] Testing implementation details instead of user-visible behavior
|
||||
- [ ] Using `getBy*` for async content (use `findBy*`)
|
||||
|
||||
**Full guide:** [React Review Guide](react.md)
|
||||
|
||||
## Vue 3
|
||||
|
||||
- [ ] Destructuring `reactive()` object loses reactivity (use `toRefs`)
|
||||
- [ ] Passing `props.x` to composable instead of `() => props.x` or `toRef(props, 'x')`
|
||||
- [ ] `watch` with async callback missing `onCleanup` (race condition)
|
||||
- [ ] `computed` with side effects (mutations, API calls)
|
||||
- [ ] `v-for` using index as `:key` when list can reorder
|
||||
- [ ] `v-if` and `v-for` on the same element
|
||||
- [ ] `defineProps` without TypeScript type declaration
|
||||
- [ ] `withDefaults` object default values not using factory functions
|
||||
- [ ] Directly mutating props instead of emitting events
|
||||
- [ ] `watchEffect` with unclear dependencies causing over-triggering
|
||||
|
||||
**Full guide:** [Vue 3 Review Guide](vue.md)
|
||||
|
||||
## Python
|
||||
|
||||
- [ ] Mutable default arguments (`def f(x=[])`)
|
||||
- [ ] Bare `except:` catching `KeyboardInterrupt` and `SystemExit`
|
||||
- [ ] Shared mutable class attributes (`class C: items = []`)
|
||||
- [ ] Using `is` instead of `==` for value comparison
|
||||
- [ ] Forgetting `self` parameter in methods
|
||||
- [ ] Modifying list while iterating
|
||||
- [ ] String concatenation in loops (use `"".join()`)
|
||||
- [ ] Not closing files (use `with` statement)
|
||||
- [ ] Missing type annotations on public functions
|
||||
|
||||
**Full guide:** [Python Review Guide](python.md)
|
||||
|
||||
## Rust
|
||||
|
||||
**Ownership & Borrowing:**
|
||||
- [ ] Unnecessary `clone()` to work around borrow checker
|
||||
- [ ] `Arc<Mutex<T>>` when single-owner would suffice
|
||||
- [ ] Storing borrows in structs when owned data is simpler
|
||||
- [ ] Unnecessary `RefCell` (runtime checks vs compile-time)
|
||||
|
||||
**Unsafe Code:**
|
||||
- [ ] `unsafe` block without `SAFETY:` comment explaining invariants
|
||||
- [ ] `unsafe fn` without `# Safety` doc section
|
||||
- [ ] Unsafe invariants split across modules
|
||||
|
||||
**Async & Concurrency:**
|
||||
- [ ] Blocking in async context (`std::fs`, `std::thread::sleep`)
|
||||
- [ ] Holding `std::sync::Mutex` across `.await`
|
||||
- [ ] Spawned task missing `'static` lifetime bound
|
||||
- [ ] Dropping a Future without awaiting (forgotten work)
|
||||
|
||||
**Error Handling:**
|
||||
- [ ] `unwrap()`/`expect()` in production code
|
||||
- [ ] Library using `anyhow` instead of `thiserror` (callers can't match)
|
||||
- [ ] Swallowing error context (`map_err(|_| ...)`)
|
||||
- [ ] Ignoring `must_use` return values
|
||||
|
||||
**Performance:**
|
||||
- [ ] Unnecessary `.collect()` — prefer lazy iterators
|
||||
- [ ] String concatenation in loops without `with_capacity`
|
||||
- [ ] `Box<dyn Trait>` when `impl Trait` would work
|
||||
|
||||
**Full guide:** [Rust Review Guide](rust.md)
|
||||
|
||||
## Go
|
||||
|
||||
- [ ] Ignoring errors (`result, _ := SomeFunction()`)
|
||||
- [ ] Goroutine with no exit mechanism (leak)
|
||||
- [ ] Missing or incorrect `context.Context` propagation
|
||||
- [ ] Loop variable capture issue (Go < 1.22)
|
||||
- [ ] `defer` in loops (deferred until function, not loop iteration)
|
||||
- [ ] Variable shadowing
|
||||
- [ ] Map used before initialization
|
||||
- [ ] Error wrapping with `%v` instead of `%w` (breaks `errors.Is`/`errors.As`)
|
||||
|
||||
**Full guide:** [Go Review Guide](go.md)
|
||||
|
||||
## Java / Spring Boot
|
||||
|
||||
- [ ] POJO/DTO with manual boilerplate instead of `record`
|
||||
- [ ] Traditional switch missing `break` (use switch expressions)
|
||||
- [ ] Field injection instead of constructor injection
|
||||
- [ ] JPA N+1 query (missing `fetch join` or `@EntityGraph`)
|
||||
- [ ] Incorrect `equals`/`hashCode` on JPA entities (use business key, not ID)
|
||||
- [ ] `Optional.get()` without `isPresent()` check
|
||||
- [ ] Stream operations with side effects
|
||||
|
||||
**Full guide:** [Java Review Guide](java.md)
|
||||
|
||||
## PHP
|
||||
|
||||
- [ ] Missing `declare(strict_types=1);` in new files
|
||||
- [ ] Weak comparison (`==`, `!=`) in auth, token, payment, or state logic
|
||||
- [ ] `in_array()` / `array_search()` used without strict mode
|
||||
- [ ] SQL built with string concatenation instead of prepared statements
|
||||
- [ ] User input echoed without context-aware escaping
|
||||
- [ ] Passwords stored with `md5()` / `sha1()` instead of `password_hash()`
|
||||
- [ ] Untrusted data passed to `unserialize()`
|
||||
- [ ] PHP 8.2+ dynamic properties used instead of declared properties
|
||||
- [ ] Errors hidden with `@` or swallowed in empty `catch` blocks
|
||||
- [ ] File uploads using client-provided names or missing MIME/size validation
|
||||
|
||||
**Full guide:** [PHP Review Guide](php.md)
|
||||
|
||||
## Swift
|
||||
|
||||
- [ ] Force-unwrap (`!`) or `try!` where safe unwrapping is possible
|
||||
- [ ] Closure capturing `self` strongly without `[weak self]` (retain cycle)
|
||||
- [ ] Reference type (`class`) used where a value type (`struct`) is intended
|
||||
- [ ] Errors swallowed instead of propagated via `throws` / `Result`
|
||||
- [ ] Data race across concurrency boundaries (missing `Sendable`, `@MainActor`, actor isolation)
|
||||
- [ ] Fire-and-forget `Task {}` that is never cancelled or leaks
|
||||
- [ ] `@ObservedObject` used where `@StateObject` is required for ownership
|
||||
- [ ] Implicitly unwrapped optional (`var x: T!`) outside IBOutlets
|
||||
- [ ] Over-broad access control (`public` / `open` where `internal` suffices)
|
||||
|
||||
**Full guide:** [Swift Review Guide](swift.md)
|
||||
|
||||
## C
|
||||
|
||||
- [ ] Pointer/buffer overflow or underflow
|
||||
- [ ] Undefined behavior (use-after-free, double-free, null deref)
|
||||
- [ ] Missing error handling after allocation (`malloc` can return `NULL`)
|
||||
- [ ] Integer overflow in size calculations
|
||||
- [ ] Resource leaks (missing `free`, `fclose`, etc.)
|
||||
- [ ] Missing `static` on file-local functions/variables
|
||||
|
||||
**Full guide:** [C Review Guide](c.md)
|
||||
|
||||
## C++
|
||||
|
||||
- [ ] Missing RAII wrapper for resources
|
||||
- [ ] Violating Rule of 0/3/5 (destructor, copy, move)
|
||||
- [ ] Exception safety issues (no `noexcept` where applicable)
|
||||
- [ ] Dangling references from returned iterators or references
|
||||
- [ ] Unnecessary copies (missing `std::move` or pass-by-reference)
|
||||
|
||||
**Full guide:** [C++ Review Guide](cpp.md)
|
||||
|
||||
## SQL
|
||||
|
||||
- [ ] String concatenation for queries (SQL injection risk) — use parameterized queries
|
||||
- [ ] Missing indexes on filtered/joined columns
|
||||
- [ ] `SELECT *` instead of specific columns
|
||||
- [ ] N+1 query patterns
|
||||
- [ ] Missing `LIMIT` on large tables
|
||||
- [ ] Not handling `NULL` comparisons correctly (`IS NULL` vs `= NULL`)
|
||||
- [ ] Missing transactions for related operations
|
||||
- [ ] Incorrect JOIN types
|
||||
- [ ] Collation / case sensitivity surprises across databases (MySQL vs Postgres defaults)
|
||||
- [ ] Date and timezone handling errors (naive timestamps, server-local `NOW()`, DST)
|
||||
|
||||
**See also:** [Security Review Guide](security-review-guide.md) for SQL injection prevention
|
||||
|
||||
## API Design
|
||||
|
||||
- [ ] Inconsistent resource naming
|
||||
- [ ] Wrong HTTP methods (POST for idempotent operations)
|
||||
- [ ] Missing pagination for list endpoints
|
||||
- [ ] Incorrect status codes
|
||||
- [ ] Missing rate limiting
|
||||
- [ ] Missing input validation and sanitization
|
||||
- [ ] Trusting client-side validation only
|
||||
|
||||
## Testing
|
||||
|
||||
- [ ] Testing implementation details instead of behavior
|
||||
- [ ] Missing edge case tests
|
||||
- [ ] Flaky tests (non-deterministic)
|
||||
- [ ] Tests with external dependencies (no mocks)
|
||||
- [ ] Missing negative tests (error cases)
|
||||
- [ ] Overly complex test setup
|
||||
@@ -0,0 +1,385 @@
|
||||
# C++ Code Review Guide
|
||||
|
||||
> C++ code review guide focused on memory safety, lifetime, API design, and performance. Examples assume C++17/20.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Ownership and RAII](#ownership-and-raii)
|
||||
- [Lifetime and References](#lifetime-and-references)
|
||||
- [Copy and Move Semantics](#copy-and-move-semantics)
|
||||
- [Const-Correctness and API Design](#const-correctness-and-api-design)
|
||||
- [Error Handling and Exception Safety](#error-handling-and-exception-safety)
|
||||
- [Concurrency](#concurrency)
|
||||
- [Performance and Allocation](#performance-and-allocation)
|
||||
- [Templates and Type Safety](#templates-and-type-safety)
|
||||
- [Tooling and Build Checks](#tooling-and-build-checks)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Ownership and RAII
|
||||
|
||||
### Prefer RAII and smart pointers
|
||||
|
||||
Use RAII to express ownership. Default to `std::unique_ptr`, use `std::shared_ptr` only for shared lifetime.
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: manual new/delete with early returns
|
||||
Foo* make_foo() {
|
||||
Foo* foo = new Foo();
|
||||
if (!foo->Init()) {
|
||||
delete foo;
|
||||
return nullptr;
|
||||
}
|
||||
return foo;
|
||||
}
|
||||
|
||||
// ✅ Good: RAII with unique_ptr
|
||||
std::unique_ptr<Foo> make_foo() {
|
||||
auto foo = std::make_unique<Foo>();
|
||||
if (!foo->Init()) {
|
||||
return {};
|
||||
}
|
||||
return foo;
|
||||
}
|
||||
```
|
||||
|
||||
### Wrap C resources
|
||||
|
||||
```cpp
|
||||
// ✅ Good: wrap FILE* with unique_ptr
|
||||
using FilePtr = std::unique_ptr<FILE, decltype(&fclose)>;
|
||||
|
||||
FilePtr open_file(const char* path) {
|
||||
return FilePtr(fopen(path, "rb"), &fclose);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lifetime and References
|
||||
|
||||
### Avoid dangling references and views
|
||||
|
||||
`std::string_view` and `std::span` do not own data. Make sure the owner outlives the view.
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: returning string_view to a temporary
|
||||
std::string_view bad_view() {
|
||||
std::string s = make_name();
|
||||
return s; // dangling
|
||||
}
|
||||
|
||||
// ✅ Good: return owning string
|
||||
std::string good_name() {
|
||||
return make_name();
|
||||
}
|
||||
|
||||
// ✅ Good: view tied to caller-owned data
|
||||
std::string_view good_view(const std::string& s) {
|
||||
return s;
|
||||
}
|
||||
```
|
||||
|
||||
### Lambda captures
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: capture reference that escapes
|
||||
std::function<void()> make_task() {
|
||||
int value = 42;
|
||||
return [&]() { use(value); }; // dangling
|
||||
}
|
||||
|
||||
// ✅ Good: capture by value
|
||||
std::function<void()> make_task() {
|
||||
int value = 42;
|
||||
return [value]() { use(value); };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Copy and Move Semantics
|
||||
|
||||
### Rule of 0/3/5
|
||||
|
||||
Prefer the Rule of 0 by using RAII types. If you own a resource, define or delete copy and move operations.
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: raw ownership with default copy
|
||||
struct Buffer {
|
||||
int* data;
|
||||
size_t size;
|
||||
explicit Buffer(size_t n) : data(new int[n]), size(n) {}
|
||||
~Buffer() { delete[] data; }
|
||||
// copy ctor/assign are implicitly generated -> double delete
|
||||
};
|
||||
|
||||
// ✅ Good: Rule of 0 with std::vector
|
||||
struct Buffer {
|
||||
std::vector<int> data;
|
||||
explicit Buffer(size_t n) : data(n) {}
|
||||
};
|
||||
```
|
||||
|
||||
### Delete unwanted copies
|
||||
|
||||
```cpp
|
||||
struct Socket {
|
||||
Socket() = default;
|
||||
~Socket() { close(); }
|
||||
|
||||
Socket(const Socket&) = delete;
|
||||
Socket& operator=(const Socket&) = delete;
|
||||
Socket(Socket&&) noexcept = default;
|
||||
Socket& operator=(Socket&&) noexcept = default;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Const-Correctness and API Design
|
||||
|
||||
### Use const and explicit
|
||||
|
||||
```cpp
|
||||
class User {
|
||||
public:
|
||||
const std::string& name() const { return name_; }
|
||||
void set_name(std::string name) { name_ = std::move(name); }
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
};
|
||||
|
||||
struct Millis {
|
||||
explicit Millis(int v) : value(v) {}
|
||||
int value;
|
||||
};
|
||||
```
|
||||
|
||||
### Avoid object slicing
|
||||
|
||||
```cpp
|
||||
struct Shape { virtual ~Shape() = default; };
|
||||
struct Circle : Shape { void draw() const; };
|
||||
|
||||
// ❌ Bad: slices Circle into Shape
|
||||
void draw(Shape shape);
|
||||
|
||||
// ✅ Good: pass by reference
|
||||
void draw(const Shape& shape);
|
||||
```
|
||||
|
||||
### Use override and final
|
||||
|
||||
```cpp
|
||||
struct Base {
|
||||
virtual void run() = 0;
|
||||
};
|
||||
|
||||
struct Worker final : Base {
|
||||
void run() override {}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling and Exception Safety
|
||||
|
||||
### Prefer RAII for cleanup
|
||||
|
||||
```cpp
|
||||
// ✅ Good: RAII handles cleanup on exceptions
|
||||
void process() {
|
||||
std::vector<int> data = load_data(); // safe cleanup
|
||||
do_work(data);
|
||||
}
|
||||
```
|
||||
|
||||
### Do not throw from destructors
|
||||
|
||||
```cpp
|
||||
struct File {
|
||||
~File() noexcept { close(); }
|
||||
void close();
|
||||
};
|
||||
```
|
||||
|
||||
### Use expected results for normal failures
|
||||
|
||||
```cpp
|
||||
// ✅ Expected error: use optional or expected
|
||||
std::optional<int> parse_int(const std::string& s) {
|
||||
try {
|
||||
return std::stoi(s);
|
||||
} catch (...) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Concurrency
|
||||
|
||||
### Protect shared data
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: data race
|
||||
int counter = 0;
|
||||
void inc() { counter++; }
|
||||
|
||||
// ✅ Good: atomic
|
||||
std::atomic<int> counter{0};
|
||||
void inc() { counter.fetch_add(1, std::memory_order_relaxed); }
|
||||
```
|
||||
|
||||
### Use RAII locks
|
||||
|
||||
```cpp
|
||||
std::mutex mu;
|
||||
std::vector<int> data;
|
||||
|
||||
void add(int v) {
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
data.push_back(v);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance and Allocation
|
||||
|
||||
### Avoid repeated allocations
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: repeated reallocation
|
||||
std::vector<int> build(int n) {
|
||||
std::vector<int> out;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
out.push_back(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ✅ Good: reserve upfront
|
||||
std::vector<int> build(int n) {
|
||||
std::vector<int> out;
|
||||
out.reserve(static_cast<size_t>(n));
|
||||
for (int i = 0; i < n; ++i) {
|
||||
out.push_back(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
```
|
||||
|
||||
### String concatenation
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: repeated allocation
|
||||
std::string join(const std::vector<std::string>& parts) {
|
||||
std::string out;
|
||||
for (const auto& p : parts) {
|
||||
out += p;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ✅ Good: reserve total size
|
||||
std::string join(const std::vector<std::string>& parts) {
|
||||
size_t total = 0;
|
||||
for (const auto& p : parts) {
|
||||
total += p.size();
|
||||
}
|
||||
std::string out;
|
||||
out.reserve(total);
|
||||
for (const auto& p : parts) {
|
||||
out += p;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Templates and Type Safety
|
||||
|
||||
### Prefer constrained templates (C++20)
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: overly generic
|
||||
template <typename T>
|
||||
T add(T a, T b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// ✅ Good: constrained
|
||||
template <typename T>
|
||||
requires std::is_integral_v<T>
|
||||
T add(T a, T b) {
|
||||
return a + b;
|
||||
}
|
||||
```
|
||||
|
||||
### Use static_assert for invariants
|
||||
|
||||
```cpp
|
||||
template <typename T>
|
||||
struct Packet {
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"Packet payload must be trivially copyable");
|
||||
T payload;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tooling and Build Checks
|
||||
|
||||
```bash
|
||||
# Warnings
|
||||
clang++ -Wall -Wextra -Werror -Wconversion -Wshadow -std=c++20 ...
|
||||
|
||||
# Sanitizers (debug builds)
|
||||
clang++ -fsanitize=address,undefined -fno-omit-frame-pointer -g ...
|
||||
clang++ -fsanitize=thread -fno-omit-frame-pointer -g ...
|
||||
|
||||
# Static analysis
|
||||
clang-tidy src/*.cpp -- -std=c++20
|
||||
|
||||
# Formatting
|
||||
clang-format -i src/*.cpp include/*.h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Safety and Lifetime
|
||||
- [ ] Ownership is explicit (RAII, unique_ptr by default)
|
||||
- [ ] No dangling references or views
|
||||
- [ ] Rule of 0/3/5 followed for resource-owning types
|
||||
- [ ] No raw new/delete in business logic
|
||||
- [ ] Destructors are noexcept and do not throw
|
||||
|
||||
### API and Design
|
||||
- [ ] const-correctness is applied consistently
|
||||
- [ ] Constructors are explicit where needed
|
||||
- [ ] Override/final used for virtual functions
|
||||
- [ ] No object slicing (pass by ref or pointer)
|
||||
|
||||
### Concurrency
|
||||
- [ ] Shared data is protected (mutex or atomics)
|
||||
- [ ] Locking order is consistent
|
||||
- [ ] No blocking while holding locks
|
||||
|
||||
### Performance
|
||||
- [ ] Unnecessary allocations avoided (reserve, move)
|
||||
- [ ] Copies avoided in hot paths
|
||||
- [ ] Algorithmic complexity is reasonable
|
||||
|
||||
### Tooling and Tests
|
||||
- [ ] Builds clean with warnings enabled
|
||||
- [ ] Sanitizers run on critical code paths
|
||||
- [ ] Static analysis (clang-tidy) results are addressed
|
||||
@@ -0,0 +1,521 @@
|
||||
# C# / .NET Code Review Guide
|
||||
|
||||
> C# / .NET 8 代码审查指南,覆盖 C# 12 新特性、异步编程、EF Core 性能、ASP.NET Core 最佳实践、依赖注入、LINQ 等核心主题。
|
||||
|
||||
## 目录
|
||||
|
||||
- [C# 12 新特性](#c-12-新特性)
|
||||
- [异步编程](#异步编程)
|
||||
- [EF Core 性能](#ef-core-性能)
|
||||
- [ASP.NET Core 最佳实践](#aspnet-core-最佳实践)
|
||||
- [依赖注入](#依赖注入)
|
||||
- [LINQ 最佳实践](#linq-最佳实践)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## C# 12 新特性
|
||||
|
||||
### Primary Constructors(非 record 类型)
|
||||
|
||||
```csharp
|
||||
// ❌ 样板代码过多的传统构造函数
|
||||
public class ProductService
|
||||
{
|
||||
private readonly ProductDbContext _db;
|
||||
private readonly ILogger<ProductService> _logger;
|
||||
|
||||
public ProductService(ProductDbContext db, ILogger<ProductService> logger)
|
||||
{
|
||||
_db = db;
|
||||
_logger = logger;
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Primary Constructor——简洁的依赖注入
|
||||
public class ProductService(ProductDbContext db, ILogger<ProductService> logger)
|
||||
{
|
||||
public async Task<Product?> GetAsync(int id)
|
||||
=> await db.Products.FindAsync(id);
|
||||
}
|
||||
|
||||
// ⚠️ 注意:primary constructor 参数不是属性,不能被重新赋值
|
||||
// ⚠️ 如果需要长期存储,显式声明字段
|
||||
public class OrderService(OrderDbContext db)
|
||||
{
|
||||
private readonly OrderDbContext _db = db; // 显式捕获
|
||||
}
|
||||
```
|
||||
|
||||
### Collection Expressions
|
||||
|
||||
```csharp
|
||||
// ❌ 传统集合初始化
|
||||
int[] nums = new int[] { 1, 2, 3 };
|
||||
List<string> names = new List<string> { "alice", "bob" };
|
||||
|
||||
// ✅ 集合表达式
|
||||
int[] nums = [1, 2, 3];
|
||||
List<string> names = ["alice", "bob"];
|
||||
Span<char> span = ['a', 'b'];
|
||||
|
||||
// ✅ 展开运算符
|
||||
int[] merged = [..nums, 4, 5];
|
||||
```
|
||||
|
||||
### Default Lambda Parameters
|
||||
|
||||
```csharp
|
||||
// ❌ 重载 lambda
|
||||
var add = (int a, int b) => a + b;
|
||||
var addDefault = (int a) => a + 1;
|
||||
|
||||
// ✅ 默认参数
|
||||
var add = (int a, int b = 1) => a + b;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 异步编程
|
||||
|
||||
### Task.Wait() / .Result / async void 是严重反模式
|
||||
|
||||
```csharp
|
||||
// ❌ Task.Wait() —— 死锁风险(同步阻塞异步操作)
|
||||
public ActionResult<Data> Get(int id)
|
||||
{
|
||||
var data = _service.GetDataAsync(id).Result; // 死锁!
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
// ❌ async void —— 异常无法捕获,会崩溃进程
|
||||
public async void HandleEvent()
|
||||
{
|
||||
await _service.ProcessAsync(); // 异常直接崩溃
|
||||
}
|
||||
|
||||
// ✅ async Task —— 全链路异步
|
||||
public async Task<ActionResult<Data>> Get(int id)
|
||||
{
|
||||
var data = await _service.GetDataAsync(id);
|
||||
return Ok(data);
|
||||
}
|
||||
```
|
||||
|
||||
### ConfigureAwait(false) 用于库代码
|
||||
|
||||
```csharp
|
||||
// ❌ 库代码不必要地捕获 SynchronizationContext
|
||||
public class LibraryService
|
||||
{
|
||||
public async Task<string> GetDataAsync()
|
||||
{
|
||||
var response = await _httpClient.GetAsync("/api/data");
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 库代码使用 ConfigureAwait(false) 避免死锁
|
||||
public class LibraryService
|
||||
{
|
||||
public async Task<string> GetDataAsync()
|
||||
{
|
||||
var response = await _httpClient.GetAsync("/api/data").ConfigureAwait(false);
|
||||
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CancellationToken 传播
|
||||
|
||||
```csharp
|
||||
// ❌ 丢弃 CancellationToken
|
||||
public async Task<List<User>> SearchAsync(string query)
|
||||
{
|
||||
return await _db.Users.Where(u => u.Name.Contains(query)).ToListAsync();
|
||||
}
|
||||
|
||||
// ✅ 全链路传递 CancellationToken
|
||||
public async Task<List<User>> SearchAsync(string query, CancellationToken ct = default)
|
||||
{
|
||||
return await _db.Users
|
||||
.Where(u => u.Name.Contains(query))
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
```
|
||||
|
||||
### Async Disposal
|
||||
|
||||
```csharp
|
||||
// ❌ 同步 dispose 异步资源
|
||||
public class DataClient : IDisposable
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient.Dispose(); // 可能丢弃正在进行的请求
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ IAsyncDisposable
|
||||
public class DataClient : IAsyncDisposable
|
||||
{
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _stream.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 调用方使用 await using
|
||||
await using var client = new DataClient();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## EF Core 性能
|
||||
|
||||
### N+1 查询问题
|
||||
|
||||
```csharp
|
||||
// ❌ 经典 N+1——每个 Blog 触发一次查询获取 Posts
|
||||
foreach (var blog in await context.Blogs.ToListAsync())
|
||||
{
|
||||
foreach (var post in blog.Posts) // 每次循环都查询数据库!
|
||||
{
|
||||
Console.WriteLine(post.Title);
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Eager Loading + 投影
|
||||
await foreach (var blog in context.Blogs
|
||||
.Select(b => new { b.Url, b.Posts })
|
||||
.AsAsyncEnumerable())
|
||||
{
|
||||
foreach (var post in blog.Posts)
|
||||
Console.WriteLine(post.Title);
|
||||
}
|
||||
```
|
||||
|
||||
### 过度获取(不投影)
|
||||
|
||||
```csharp
|
||||
// ❌ 加载所有列——只需要 Url 时加载了全部字段
|
||||
var urls = await context.Blogs.ToListAsync();
|
||||
|
||||
// ✅ 只投影需要的字段
|
||||
var urls = await context.Blogs
|
||||
.Select(b => b.Url)
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### 缺少分页
|
||||
|
||||
```csharp
|
||||
// ❌ 无界结果集
|
||||
var posts = await context.Posts
|
||||
.Where(p => p.Title.StartsWith("A"))
|
||||
.ToListAsync(); // 可能有百万条记录!
|
||||
|
||||
// ✅ 限制结果数量
|
||||
var posts = await context.Posts
|
||||
.Where(p => p.Title.StartsWith("A"))
|
||||
.OrderBy(p => p.Id)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### Cartesian Explosion(JOIN 笛卡尔爆炸)
|
||||
|
||||
```csharp
|
||||
// ❌ 多个 Include 创建大量重复数据
|
||||
var blogs = await context.Blogs
|
||||
.Include(b => b.Posts)
|
||||
.Include(b => b.Tags)
|
||||
.ToListAsync(); // 每行重复 Blog 数据
|
||||
|
||||
// ✅ 使用 AsSplitQuery 拆分查询
|
||||
var blogs = await context.Blogs
|
||||
.Include(b => b.Posts)
|
||||
.Include(b => b.Tags)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### 只读场景缺少 AsNoTracking
|
||||
|
||||
```csharp
|
||||
// ❌ 默认跟踪——只读查询也付出跟踪开销
|
||||
var products = await context.Products.ToListAsync();
|
||||
|
||||
// ✅ AsNoTracking——跳过变更跟踪,更快且更省内存
|
||||
var products = await context.Products
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### 列上函数阻止索引使用
|
||||
|
||||
```csharp
|
||||
// ✅ 可以使用索引——sargable
|
||||
var posts1 = await context.Posts
|
||||
.Where(p => p.Title.StartsWith("A"))
|
||||
.ToListAsync();
|
||||
|
||||
// ❌ 无法使用索引——全表扫描
|
||||
var posts2 = await context.Posts
|
||||
.Where(p => p.Title.EndsWith("A"))
|
||||
.ToListAsync();
|
||||
|
||||
// ❌ 列上套函数——全表扫描
|
||||
var posts3 = await context.Posts
|
||||
.Where(p => p.Title.ToLower() == "foo")
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### 同步 vs 异步数据库访问
|
||||
|
||||
```csharp
|
||||
// ❌ 同步数据库调用——阻塞线程
|
||||
var products = context.Products.ToList();
|
||||
context.SaveChanges();
|
||||
|
||||
// ✅ 异步数据库调用
|
||||
var products = await context.Products.ToListAsync();
|
||||
await context.SaveChangesAsync();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ASP.NET Core 最佳实践
|
||||
|
||||
### HttpClient 误用
|
||||
|
||||
```csharp
|
||||
// ❌ 每次请求创建新的 HttpClient——socket 耗尽
|
||||
using var client = new HttpClient();
|
||||
var response = await client.GetAsync("https://api.example.com/data");
|
||||
|
||||
// ✅ IHttpClientFactory 注入
|
||||
public class MyService
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
public MyService(HttpClient client) => _client = client; // 从工厂注入
|
||||
}
|
||||
```
|
||||
|
||||
### HttpContext 在后台线程中使用
|
||||
|
||||
```csharp
|
||||
// ❌ 在后台任务中捕获 scoped 服务——请求结束后已释放
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await context.SaveChangesAsync(); // ObjectDisposedException!
|
||||
});
|
||||
|
||||
// ✅ 创建新的 scope
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await using var scope = serviceScopeFactory.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
```
|
||||
|
||||
### Request.Form 同步访问
|
||||
|
||||
```csharp
|
||||
// ❌ 同步读取 Form——sync over async
|
||||
var form = HttpContext.Request.Form;
|
||||
|
||||
// ✅ 异步读取
|
||||
var form = await HttpContext.Request.ReadFormAsync();
|
||||
```
|
||||
|
||||
### 异常用于控制流
|
||||
|
||||
```csharp
|
||||
// ❌ 用异常判断是否存在——异常开销大,比直接检查慢得多
|
||||
try
|
||||
{
|
||||
var user = await _db.Users.FirstAsync(u => u.Id == id);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
// ✅ 使用检查而非异常
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == id);
|
||||
if (user is null) return NotFound();
|
||||
```
|
||||
|
||||
### 响应头在 Body 之后设置
|
||||
|
||||
```csharp
|
||||
// ❌ body 已发送后再设置 header——抛异常
|
||||
await next(context);
|
||||
context.Response.Headers["X-Custom"] = "value"; // 可能抛异常!
|
||||
|
||||
// ✅ 使用 OnStarting 回调
|
||||
context.Response.OnStarting(() =>
|
||||
{
|
||||
context.Response.Headers["X-Custom"] = "value";
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
await next(context);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 依赖注入
|
||||
|
||||
### Scoped 服务注入 Singleton
|
||||
|
||||
```csharp
|
||||
// ❌ Scoped 服务注入 Singleton——生命周期不匹配
|
||||
services.AddSingleton<BackgroundWorker>();
|
||||
services.AddScoped<IUserRepository, UserRepository>();
|
||||
|
||||
// BackgroundWorker 是 Singleton,UserRepository 是 Scoped
|
||||
// → UserRepository 在多个请求间共享或已释放
|
||||
|
||||
// ✅ 在 Singleton 中通过 IServiceProvider 创建 scope
|
||||
public class BackgroundWorker : BackgroundService
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
|
||||
public BackgroundWorker(IServiceScopeFactory scopeFactory)
|
||||
=> _scopeFactory = scopeFactory;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken ct)
|
||||
{
|
||||
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<IUserRepository>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LINQ 最佳实践
|
||||
|
||||
### ToList 之后再 LINQ
|
||||
|
||||
```csharp
|
||||
// ❌ 先 ToList 再过滤——全表加载到内存
|
||||
var results = context.Posts
|
||||
.Where(p => p.Title.StartsWith("A"))
|
||||
.ToList()
|
||||
.Where(p => SomeClientFilter(p)); // 客户端过滤,已加载全部行
|
||||
|
||||
// ✅ 尽可能让数据库执行过滤
|
||||
var results = await context.Posts
|
||||
.Where(p => p.Title.StartsWith("A") && SomeDbFilter(p))
|
||||
.AsAsyncEnumerable()
|
||||
.Where(p => SomeClientFilter(p)) // 只过滤数据库返回的行
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### Count() vs Any()
|
||||
|
||||
```csharp
|
||||
// ❌ Count() 执行完整查询
|
||||
if (context.Users.Count() > 0) { /* ... */ }
|
||||
|
||||
// ✅ Any() 更高效——遇到第一条记录就返回
|
||||
if (await context.Users.AnyAsync()) { /* ... */ }
|
||||
```
|
||||
|
||||
### 多次枚举 IEnumerable
|
||||
|
||||
```csharp
|
||||
// ❌ IEnumerable 被枚举两次
|
||||
public void Process(IEnumerable<int> numbers)
|
||||
{
|
||||
if (numbers.Any()) // 第一次枚举
|
||||
{
|
||||
foreach (var n in numbers) // 第二次枚举(可能是重新查询)
|
||||
{
|
||||
Console.WriteLine(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 如果需要多次使用,先物化
|
||||
public void Process(IEnumerable<int> numbers)
|
||||
{
|
||||
var list = numbers.ToList(); // 只枚举一次
|
||||
if (list.Any())
|
||||
{
|
||||
foreach (var n in list)
|
||||
{
|
||||
Console.WriteLine(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Select 中的副作用
|
||||
|
||||
```csharp
|
||||
// ❌ Select 中执行副作用——不可预测的执行时机
|
||||
var results = users.Select(u =>
|
||||
{
|
||||
_logger.LogInformation($"Processing {u.Name}"); // 副作用!
|
||||
return u.Email;
|
||||
}).ToList();
|
||||
|
||||
// ✅ 副作用放在 foreach 中
|
||||
foreach (var user in users)
|
||||
{
|
||||
_logger.LogInformation("Processing {Name}", user.Name);
|
||||
}
|
||||
var results = users.Select(u => u.Email).ToList();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### C# 12 新特性
|
||||
|
||||
- [ ] Primary constructor 参数不被重新赋值
|
||||
- [ ] 集合表达式语法一致(不混用新旧风格)
|
||||
|
||||
### 异步编程
|
||||
|
||||
- [ ] 无 `Task.Wait()`、`.Result`、`async void`
|
||||
- [ ] 库代码使用 `ConfigureAwait(false)`
|
||||
- [ ] `CancellationToken` 全链路传递
|
||||
- [ ] 异步资源使用 `IAsyncDisposable` / `await using`
|
||||
- [ ] 不混用同步和异步数据访问
|
||||
|
||||
### EF Core
|
||||
|
||||
- [ ] 无 N+1 查询(导航属性在循环中访问)
|
||||
- [ ] 投影 `Select()` 避免过度获取
|
||||
- [ ] 分页:`ToListAsync()` 前有 `Take()`/`Skip()`
|
||||
- [ ] 多个 `Include()` 使用 `AsSplitQuery()`
|
||||
- [ ] 只读查询使用 `AsNoTracking()`
|
||||
- [ ] 列上无函数调用阻止索引使用
|
||||
- [ ] 数据库调用全部异步
|
||||
|
||||
### ASP.NET Core
|
||||
|
||||
- [ ] HttpClient 通过 `IHttpClientFactory` 获取
|
||||
- [ ] 后台任务中不直接使用 scoped 服务
|
||||
- [ ] 使用 `ReadFormAsync` 代替 `Request.Form`
|
||||
- [ ] 异常不用于控制流
|
||||
- [ ] 响应头通过 `OnStarting` 设置
|
||||
|
||||
### 依赖注入
|
||||
|
||||
- [ ] Scoped 服务不注入 Singleton
|
||||
- [ ] 后台任务创建新 scope
|
||||
|
||||
### LINQ
|
||||
|
||||
- [ ] 无不必要的 `ToList()` 后再 LINQ
|
||||
- [ ] `Any()` 代替 `Count() > 0`
|
||||
- [ ] IEnumerable 不被多次枚举(或先物化)
|
||||
- [ ] Select 中无副作用
|
||||
@@ -0,0 +1,661 @@
|
||||
# CSS / Less / Sass Review Guide
|
||||
|
||||
CSS 及预处理器代码审查指南,覆盖性能、可维护性、响应式设计和浏览器兼容性。
|
||||
|
||||
## CSS 变量 vs 硬编码
|
||||
|
||||
### 应该使用变量的场景
|
||||
|
||||
```css
|
||||
/* ❌ 硬编码 - 难以维护 */
|
||||
.button {
|
||||
background: #3b82f6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.card {
|
||||
border: 1px solid #3b82f6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* ✅ 使用 CSS 变量 */
|
||||
:root {
|
||||
--color-primary: #3b82f6;
|
||||
--radius-md: 8px;
|
||||
}
|
||||
.button {
|
||||
background: var(--color-primary);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.card {
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
```
|
||||
|
||||
### 变量命名规范
|
||||
|
||||
```css
|
||||
/* 推荐的变量分类 */
|
||||
:root {
|
||||
/* 颜色 */
|
||||
--color-primary: #3b82f6;
|
||||
--color-primary-hover: #2563eb;
|
||||
--color-text: #1f2937;
|
||||
--color-text-muted: #6b7280;
|
||||
--color-bg: #ffffff;
|
||||
--color-border: #e5e7eb;
|
||||
|
||||
/* 间距 */
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
--spacing-xl: 32px;
|
||||
|
||||
/* 字体 */
|
||||
--font-size-sm: 14px;
|
||||
--font-size-base: 16px;
|
||||
--font-size-lg: 18px;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
/* 圆角 */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* 阴影 */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* 过渡 */
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-normal: 300ms ease;
|
||||
}
|
||||
```
|
||||
|
||||
### 变量作用域建议
|
||||
|
||||
```css
|
||||
/* ✅ 组件级变量 - 减少全局污染 */
|
||||
.card {
|
||||
--card-padding: var(--spacing-md);
|
||||
--card-radius: var(--radius-md);
|
||||
|
||||
padding: var(--card-padding);
|
||||
border-radius: var(--card-radius);
|
||||
}
|
||||
|
||||
/* ⚠️ 避免频繁用 JS 动态修改变量 - 影响性能 */
|
||||
```
|
||||
|
||||
### 审查清单
|
||||
|
||||
- [ ] 颜色值是否使用变量?
|
||||
- [ ] 间距是否来自设计系统?
|
||||
- [ ] 重复值是否提取为变量?
|
||||
- [ ] 变量命名是否语义化?
|
||||
|
||||
---
|
||||
|
||||
## !important 使用规范
|
||||
|
||||
### 何时可以使用
|
||||
|
||||
```css
|
||||
/* ✅ 工具类 - 明确需要覆盖 */
|
||||
.hidden { display: none !important; }
|
||||
.sr-only { position: absolute !important; }
|
||||
|
||||
/* ✅ 覆盖第三方库样式(无法修改源码时) */
|
||||
.third-party-modal {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
/* ✅ 打印样式 */
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
}
|
||||
```
|
||||
|
||||
### 何时禁止使用
|
||||
|
||||
```css
|
||||
/* ❌ 解决特异性问题 - 应该重构选择器 */
|
||||
.button {
|
||||
background: blue !important; /* 为什么需要 !important? */
|
||||
}
|
||||
|
||||
/* ❌ 覆盖自己写的样式 */
|
||||
.card { padding: 20px; }
|
||||
.card { padding: 30px !important; } /* 直接修改原规则 */
|
||||
|
||||
/* ❌ 在组件样式中 */
|
||||
.my-component .title {
|
||||
font-size: 24px !important; /* 破坏组件封装 */
|
||||
}
|
||||
```
|
||||
|
||||
### 替代方案
|
||||
|
||||
```css
|
||||
/* 问题:需要覆盖 .btn 的样式 */
|
||||
|
||||
/* ❌ 使用 !important */
|
||||
.my-btn {
|
||||
background: red !important;
|
||||
}
|
||||
|
||||
/* ✅ 提高特异性 */
|
||||
button.my-btn {
|
||||
background: red;
|
||||
}
|
||||
|
||||
/* ✅ 使用更具体的选择器 */
|
||||
.container .my-btn {
|
||||
background: red;
|
||||
}
|
||||
|
||||
/* ✅ 使用 :where() 降低被覆盖样式的特异性 */
|
||||
:where(.btn) {
|
||||
background: blue; /* 特异性为 0 */
|
||||
}
|
||||
.my-btn {
|
||||
background: red; /* 可以正常覆盖 */
|
||||
}
|
||||
```
|
||||
|
||||
### 审查问题
|
||||
|
||||
```markdown
|
||||
🔴 [blocking] "发现 15 处 !important,请说明每处的必要性"
|
||||
🟡 [important] "这个 !important 可以通过调整选择器特异性来解决"
|
||||
💡 [suggestion] "考虑使用 CSS Layers (@layer) 来管理样式优先级"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能考虑
|
||||
|
||||
### 🔴 高危性能问题
|
||||
|
||||
#### 1. `transition: all` 问题
|
||||
|
||||
```css
|
||||
/* ❌ 性能杀手 - 浏览器检查所有可动画属性 */
|
||||
.button {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* ✅ 明确指定属性 */
|
||||
.button {
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* ✅ 多属性时使用变量 */
|
||||
.button {
|
||||
--transition-duration: 0.3s;
|
||||
transition:
|
||||
background-color var(--transition-duration) ease,
|
||||
box-shadow var(--transition-duration) ease,
|
||||
transform var(--transition-duration) ease;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. box-shadow 动画
|
||||
|
||||
```css
|
||||
/* ❌ 每帧触发重绘 - 严重影响性能 */
|
||||
.card {
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
.card:hover {
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* ✅ 使用伪元素 + opacity */
|
||||
.card {
|
||||
position: relative;
|
||||
}
|
||||
.card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
}
|
||||
.card:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 触发布局(Reflow)的属性
|
||||
|
||||
```css
|
||||
/* ❌ 动画这些属性会触发布局重计算 */
|
||||
.bad-animation {
|
||||
transition: width 0.3s, height 0.3s, top 0.3s, left 0.3s, margin 0.3s;
|
||||
}
|
||||
|
||||
/* ✅ 只动画 transform 和 opacity(仅触发合成) */
|
||||
.good-animation {
|
||||
transition: transform 0.3s, opacity 0.3s;
|
||||
}
|
||||
|
||||
/* 位移用 translate 代替 top/left */
|
||||
.move {
|
||||
transform: translateX(100px); /* ✅ */
|
||||
/* left: 100px; */ /* ❌ */
|
||||
}
|
||||
|
||||
/* 缩放用 scale 代替 width/height */
|
||||
.grow {
|
||||
transform: scale(1.1); /* ✅ */
|
||||
/* width: 110%; */ /* ❌ */
|
||||
}
|
||||
```
|
||||
|
||||
### 🟡 中等性能问题
|
||||
|
||||
#### 复杂选择器
|
||||
|
||||
```css
|
||||
/* ❌ 过深的嵌套 - 选择器匹配慢 */
|
||||
.page .container .content .article .section .paragraph span {
|
||||
color: red;
|
||||
}
|
||||
|
||||
/* ✅ 扁平化 */
|
||||
.article-text {
|
||||
color: red;
|
||||
}
|
||||
|
||||
/* ❌ 通配符选择器 */
|
||||
* { box-sizing: border-box; } /* 影响所有元素 */
|
||||
[class*="icon-"] { display: inline; } /* 属性选择器较慢 */
|
||||
|
||||
/* ✅ 限制范围 */
|
||||
.icon-box * { box-sizing: border-box; }
|
||||
```
|
||||
|
||||
#### 大量阴影和滤镜
|
||||
|
||||
```css
|
||||
/* ⚠️ 复杂阴影影响渲染性能 */
|
||||
.heavy-shadow {
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0,0,0,0.1),
|
||||
0 2px 4px rgba(0,0,0,0.1),
|
||||
0 4px 8px rgba(0,0,0,0.1),
|
||||
0 8px 16px rgba(0,0,0,0.1),
|
||||
0 16px 32px rgba(0,0,0,0.1); /* 5 层阴影 */
|
||||
}
|
||||
|
||||
/* ⚠️ 滤镜消耗 GPU */
|
||||
.blur-heavy {
|
||||
filter: blur(20px) brightness(1.2) contrast(1.1);
|
||||
backdrop-filter: blur(10px); /* 更消耗性能 */
|
||||
}
|
||||
```
|
||||
|
||||
### 性能优化建议
|
||||
|
||||
```css
|
||||
/* 使用 will-change 提示浏览器(谨慎使用) */
|
||||
.animated-element {
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
/* 动画完成后移除 will-change */
|
||||
.animated-element.idle {
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
/* 使用 contain 限制重绘范围 */
|
||||
.card {
|
||||
contain: layout paint; /* 告诉浏览器内部变化不影响外部 */
|
||||
}
|
||||
```
|
||||
|
||||
### 审查清单
|
||||
|
||||
- [ ] 是否使用 `transition: all`?
|
||||
- [ ] 是否动画 width/height/top/left?
|
||||
- [ ] box-shadow 是否被动画?
|
||||
- [ ] 选择器嵌套是否超过 3 层?
|
||||
- [ ] 是否有不必要的 `will-change`?
|
||||
|
||||
---
|
||||
|
||||
## 响应式设计检查点
|
||||
|
||||
### Mobile First 原则
|
||||
|
||||
```css
|
||||
/* ✅ Mobile First - 基础样式针对移动端 */
|
||||
.container {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 逐步增强 */
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
padding: 24px;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
padding: 32px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ❌ Desktop First - 需要覆盖更多样式 */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
padding: 32px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.container {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.container {
|
||||
padding: 16px;
|
||||
flex-direction: column;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 断点建议
|
||||
|
||||
```css
|
||||
/* 推荐断点(基于内容而非设备) */
|
||||
:root {
|
||||
--breakpoint-sm: 640px; /* 大手机 */
|
||||
--breakpoint-md: 768px; /* 平板竖屏 */
|
||||
--breakpoint-lg: 1024px; /* 平板横屏/小笔记本 */
|
||||
--breakpoint-xl: 1280px; /* 桌面 */
|
||||
--breakpoint-2xl: 1536px; /* 大桌面 */
|
||||
}
|
||||
|
||||
/* 使用示例 */
|
||||
@media (min-width: 768px) { /* md */ }
|
||||
@media (min-width: 1024px) { /* lg */ }
|
||||
```
|
||||
|
||||
### 响应式审查清单
|
||||
|
||||
- [ ] 是否采用 Mobile First?
|
||||
- [ ] 断点是否基于内容断裂点而非设备?
|
||||
- [ ] 是否避免断点重叠?
|
||||
- [ ] 文字是否使用相对单位(rem/em)?
|
||||
- [ ] 触摸目标是否足够大(≥44px)?
|
||||
- [ ] 是否测试了横竖屏切换?
|
||||
|
||||
### 常见问题
|
||||
|
||||
```css
|
||||
/* ❌ 固定宽度 */
|
||||
.container {
|
||||
width: 1200px;
|
||||
}
|
||||
|
||||
/* ✅ 最大宽度 + 弹性 */
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
/* ❌ 固定高度的文本容器 */
|
||||
.text-box {
|
||||
height: 100px; /* 文字可能溢出 */
|
||||
}
|
||||
|
||||
/* ✅ 最小高度 */
|
||||
.text-box {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
/* ❌ 小触摸目标 */
|
||||
.small-button {
|
||||
padding: 4px 8px; /* 太小,难以点击 */
|
||||
}
|
||||
|
||||
/* ✅ 足够的触摸区域 */
|
||||
.touch-button {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 浏览器兼容性
|
||||
|
||||
### 需要检查的特性
|
||||
|
||||
| 特性 | 兼容性 | 建议 |
|
||||
|------|--------|------|
|
||||
| CSS Grid | 现代浏览器 ✅ | IE 需要 Autoprefixer + 测试 |
|
||||
| Flexbox | 广泛支持 ✅ | 旧版需要前缀 |
|
||||
| CSS Variables | 现代浏览器 ✅ | IE 不支持,需要回退 |
|
||||
| `gap` (flexbox) | 较新 ⚠️ | Safari 14.1+ |
|
||||
| `:has()` | 较新 ⚠️ | Firefox 121+ |
|
||||
| `container queries` | 较新 ⚠️ | 2023 年后的浏览器 |
|
||||
| `@layer` | 较新 ⚠️ | 检查目标浏览器 |
|
||||
|
||||
### 回退策略
|
||||
|
||||
```css
|
||||
/* CSS 变量回退 */
|
||||
.button {
|
||||
background: #3b82f6; /* 回退值 */
|
||||
background: var(--color-primary); /* 现代浏览器 */
|
||||
}
|
||||
|
||||
/* Flexbox gap 回退 */
|
||||
.flex-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
/* 旧浏览器回退 */
|
||||
.flex-container > * + * {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
/* Grid 回退 */
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@supports (display: grid) {
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Autoprefixer 配置
|
||||
|
||||
```javascript
|
||||
// postcss.config.js
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('autoprefixer')({
|
||||
// 根据 browserslist 配置
|
||||
grid: 'autoplace', // 启用 Grid 前缀(IE 支持)
|
||||
flexbox: 'no-2009', // 只用现代 flexbox 语法
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
// package.json
|
||||
{
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"not ie 11" // 根据项目需求
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 审查清单
|
||||
|
||||
- [ ] 是否检查了 [Can I Use](https://caniuse.com)?
|
||||
- [ ] 新特性是否有回退方案?
|
||||
- [ ] 是否配置了 Autoprefixer?
|
||||
- [ ] browserslist 是否符合项目要求?
|
||||
- [ ] 是否在目标浏览器中测试?
|
||||
|
||||
---
|
||||
|
||||
## Less / Sass 特定问题
|
||||
|
||||
### 嵌套深度
|
||||
|
||||
```scss
|
||||
/* ❌ 过深嵌套 - 编译后选择器过长 */
|
||||
.page {
|
||||
.container {
|
||||
.content {
|
||||
.article {
|
||||
.title {
|
||||
color: red; // 编译为 .page .container .content .article .title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ✅ 最多 3 层 */
|
||||
.article {
|
||||
&__title {
|
||||
color: red;
|
||||
}
|
||||
|
||||
&__content {
|
||||
p { margin-bottom: 1em; }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mixin vs Extend vs 变量
|
||||
|
||||
```scss
|
||||
@use 'sass:color';
|
||||
|
||||
/* 变量 - 用于单个值 */
|
||||
$primary-color: #3b82f6;
|
||||
|
||||
/* Mixin - 用于可配置的代码块 */
|
||||
@mixin button-variant($bg, $text) {
|
||||
background: $bg;
|
||||
color: $text;
|
||||
&:hover {
|
||||
// Dart Sass 已弃用全局 darken()/lighten(),改用 color 模块
|
||||
background: color.adjust($bg, $lightness: -10%);
|
||||
// color.scale($bg, $lightness: -10%) 按比例调整,深浅过渡更自然
|
||||
}
|
||||
}
|
||||
|
||||
/* Extend - 用于共享相同样式(谨慎使用) */
|
||||
%visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%); /* clip: rect() 已弃用,改用 clip-path */
|
||||
white-space: nowrap; /* 避免内容被挤成一列后撑开布局 */
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
@extend %visually-hidden;
|
||||
}
|
||||
|
||||
/* ⚠️ @extend 的问题 */
|
||||
// 可能产生意外的选择器组合
|
||||
// 不能在 @media 中使用
|
||||
// 优先使用 mixin
|
||||
```
|
||||
|
||||
### 审查清单
|
||||
|
||||
- [ ] 嵌套是否超过 3 层?
|
||||
- [ ] 是否滥用 @extend?
|
||||
- [ ] Mixin 是否过于复杂?
|
||||
- [ ] 编译后的 CSS 大小是否合理?
|
||||
|
||||
---
|
||||
|
||||
## 快速审查清单
|
||||
|
||||
### 🔴 必须修复
|
||||
|
||||
```markdown
|
||||
□ transition: all
|
||||
□ 动画 width/height/top/left/margin
|
||||
□ 大量 !important
|
||||
□ 硬编码的颜色/间距重复 >3 次
|
||||
□ 选择器嵌套 >4 层
|
||||
```
|
||||
|
||||
### 🟡 建议修复
|
||||
|
||||
```markdown
|
||||
□ 缺少响应式处理
|
||||
□ 使用 Desktop First
|
||||
□ 复杂 box-shadow 被动画
|
||||
□ 缺少浏览器兼容回退
|
||||
□ CSS 变量作用域过大
|
||||
```
|
||||
|
||||
### 🟢 优化建议
|
||||
|
||||
```markdown
|
||||
□ 可以使用 CSS Grid 简化布局
|
||||
□ 可以使用 CSS 变量提取重复值
|
||||
□ 可以使用 @layer 管理优先级
|
||||
□ 可以添加 contain 优化性能
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工具推荐
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| [Stylelint](https://stylelint.io/) | CSS 代码检查 |
|
||||
| [PurgeCSS](https://purgecss.com/) | 移除未使用 CSS |
|
||||
| [Autoprefixer](https://autoprefixer.github.io/) | 自动添加前缀 |
|
||||
| [CSS Stats](https://cssstats.com/) | 分析 CSS 统计 |
|
||||
| [Can I Use](https://caniuse.com/) | 浏览器兼容性查询 |
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [CSS Performance Optimization - MDN](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Performance/CSS)
|
||||
- [What a CSS Code Review Might Look Like - CSS-Tricks](https://css-tricks.com/what-a-css-code-review-might-look-like/)
|
||||
- [How to Animate Box-Shadow - Tobias Ahlin](https://tobiasahlin.com/blog/how-to-animate-box-shadow/)
|
||||
- [Media Query Fundamentals - MDN](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/CSS_layout/Media_queries)
|
||||
- [Autoprefixer - GitHub](https://github.com/postcss/autoprefixer)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,584 @@
|
||||
# FastAPI Code Review Guide
|
||||
|
||||
> FastAPI code review guide covering dependency injection (`Depends`), Pydantic v2 validation boundaries, async correctness, database session lifecycle and N+1, security, and a test-driven verification workflow that turns the reviewer's in-process test client into a tool for *proving* bugs rather than guessing at them.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Dependency Injection (`Depends`)](#dependency-injection-depends)
|
||||
- [Pydantic v2 Models & Validation](#pydantic-v2-models--validation)
|
||||
- [Async Correctness](#async-correctness)
|
||||
- [Database Sessions & N+1](#database-sessions--n1)
|
||||
- [Security](#security)
|
||||
- [Test-Driven Verification](#test-driven-verification)
|
||||
- [Review Checklist](#review-checklist)
|
||||
- [References](#references)
|
||||
|
||||
---
|
||||
|
||||
## Dependency Injection (`Depends`)
|
||||
|
||||
FastAPI's `Depends` is the seam that keeps routes thin and testable. Most review problems here come from doing real work in the route function instead of behind a dependency.
|
||||
|
||||
### Business logic belongs behind a dependency or service, not in the route
|
||||
|
||||
```python
|
||||
# ❌ Bad — DB access, auth, and business rules all inline in the route
|
||||
@app.get("/orders/{order_id}")
|
||||
async def get_order(order_id: int):
|
||||
conn = await asyncpg.connect(DATABASE_URL) # connection created per request
|
||||
row = await conn.fetchrow("SELECT * FROM orders WHERE id = $1", order_id)
|
||||
await conn.close()
|
||||
if row is None:
|
||||
raise HTTPException(404)
|
||||
return dict(row)
|
||||
|
||||
# ✅ Good — the route declares what it needs; the session is injected and pooled
|
||||
async def get_session() -> AsyncIterator[AsyncSession]:
|
||||
async with SessionLocal() as session:
|
||||
yield session
|
||||
|
||||
@app.get("/orders/{order_id}", response_model=OrderOut)
|
||||
async def get_order(order_id: int, session: AsyncSession = Depends(get_session)):
|
||||
order = await session.get(Order, order_id)
|
||||
if order is None:
|
||||
raise HTTPException(status_code=404, detail="Order not found")
|
||||
return order
|
||||
```
|
||||
|
||||
The injected version is also the version you can override in tests (see [Test-Driven Verification](#test-driven-verification)).
|
||||
|
||||
### `yield` dependencies must clean up, and cleanup runs even on error
|
||||
|
||||
```python
|
||||
# ❌ Bad — no cleanup; the session leaks if the route raises
|
||||
async def get_session() -> AsyncSession:
|
||||
return SessionLocal()
|
||||
|
||||
# ✅ Good — the context manager closes the session on success AND on exception
|
||||
async def get_session() -> AsyncIterator[AsyncSession]:
|
||||
async with SessionLocal() as session:
|
||||
yield session
|
||||
```
|
||||
|
||||
Review point: confirm any `yield` dependency holding a resource (DB session, file handle, lock) releases it through a context manager or `try/finally`, so an exception in the route does not leak it.
|
||||
|
||||
### Don't re-create singletons per request
|
||||
|
||||
```python
|
||||
# ❌ Bad — a new HTTP client (and connection pool) per request
|
||||
@app.get("/proxy")
|
||||
async def proxy(client: httpx.AsyncClient = Depends(lambda: httpx.AsyncClient())):
|
||||
...
|
||||
|
||||
# ✅ Good — one client for the app lifetime, injected by reference
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
app.state.http = httpx.AsyncClient()
|
||||
yield
|
||||
await app.state.http.aclose()
|
||||
|
||||
def get_http(request: Request) -> httpx.AsyncClient:
|
||||
return request.app.state.http
|
||||
```
|
||||
|
||||
### Prefer the `Annotated` form and async dependencies
|
||||
|
||||
Since FastAPI 0.95 the idiomatic way to declare a dependency is `Annotated[T, Depends(...)]`, not the default-value form. It is reusable across routes and plays well with type checkers. Also prefer `async def` dependencies: a sync (`def`) dependency runs in the threadpool, which is wasted overhead for a small non-I/O check.
|
||||
|
||||
```python
|
||||
# ⚠️ Older form — still works, but not the current idiom
|
||||
@app.get("/items")
|
||||
async def list_items(session: AsyncSession = Depends(get_session)): ...
|
||||
|
||||
# ✅ Good — Annotated form; define once, reuse everywhere
|
||||
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
||||
|
||||
@app.get("/items")
|
||||
async def list_items(session: SessionDep): ...
|
||||
```
|
||||
|
||||
### Use dependencies to validate existence and permissions — they're cached per request
|
||||
|
||||
A dependency is the natural place to answer "does this resource exist and may this caller touch it?" Pydantic validates *shape*; a dependency validates against the database. FastAPI caches each dependency's result within a single request, so chaining small dependencies costs nothing extra and removes duplicated lookups.
|
||||
|
||||
```python
|
||||
# ✅ Good — small dependencies chain; valid_post is resolved once per request
|
||||
async def valid_post(post_id: int, session: SessionDep) -> Post:
|
||||
post = await session.get(Post, post_id)
|
||||
if post is None:
|
||||
raise HTTPException(status_code=404, detail="Post not found")
|
||||
return post
|
||||
|
||||
async def owned_post(post: Annotated[Post, Depends(valid_post)], user: CurrentUser) -> Post:
|
||||
if post.owner_id != user.id:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
return post
|
||||
|
||||
@app.delete("/posts/{post_id}", status_code=204)
|
||||
async def delete_post(post: Annotated[Post, Depends(owned_post)], session: SessionDep):
|
||||
await session.delete(post) # existence + ownership already enforced
|
||||
await session.commit()
|
||||
```
|
||||
|
||||
This is also the cleanest place to fix the auth-vs-authorization bug from the [Security](#security) section: the ownership check moves into a reusable `owned_post` dependency.
|
||||
|
||||
---
|
||||
|
||||
## Pydantic v2 Models & Validation
|
||||
|
||||
### Separate input and output models; never echo the ORM object directly
|
||||
|
||||
```python
|
||||
# ❌ Bad — response_model is the DB model, so hashed_password leaks to the client
|
||||
@app.post("/users", response_model=UserTable)
|
||||
async def create_user(user: UserTable): # also accepts client-set id, is_admin...
|
||||
...
|
||||
|
||||
# ✅ Good — distinct schemas draw the trust boundary
|
||||
class UserCreate(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
class UserOut(BaseModel):
|
||||
id: int
|
||||
email: EmailStr
|
||||
model_config = ConfigDict(from_attributes=True) # read from ORM safely
|
||||
|
||||
@app.post("/users", response_model=UserOut, status_code=201)
|
||||
async def create_user(payload: UserCreate, session: AsyncSession = Depends(get_session)):
|
||||
...
|
||||
```
|
||||
|
||||
`response_model` is a filter, not just documentation — fields absent from the output model are stripped from the response. Reusing the DB model as the response is the most common way sensitive fields leak.
|
||||
|
||||
### Use distinct Create and Update schemas
|
||||
|
||||
```python
|
||||
# ❌ Bad — one schema for create and update means every field is required on PATCH
|
||||
class ItemSchema(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
# ✅ Good — update is a partial; create requires the full payload
|
||||
class ItemCreate(BaseModel):
|
||||
name: str
|
||||
price: float = Field(gt=0)
|
||||
|
||||
class ItemUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
price: float | None = Field(default=None, gt=0)
|
||||
```
|
||||
|
||||
### Validate at the boundary, not after the DB write
|
||||
|
||||
```python
|
||||
# ❌ Bad — negative quantity reaches the database before anything checks it
|
||||
@app.post("/cart")
|
||||
async def add_to_cart(item_id: int, quantity: int):
|
||||
await save(item_id, quantity) # quantity = -5 silently accepted
|
||||
|
||||
# ✅ Good — the type system rejects it before the handler body runs
|
||||
class CartLine(BaseModel):
|
||||
item_id: int
|
||||
quantity: int = Field(gt=0)
|
||||
|
||||
@app.post("/cart")
|
||||
async def add_to_cart(line: CartLine):
|
||||
await save(line.item_id, line.quantity)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Async Correctness
|
||||
|
||||
This is the axis on which FastAPI differs most from Django and Flask, and the one most worth a reviewer's attention. FastAPI's throughput comes from a single event loop interleaving many concurrent requests. That model only holds if the loop is **never blocked**: one synchronous call on the loop stalls *every* in-flight request, not just its own. Get this wrong across the codebase and FastAPI does not just lose its edge — it performs *worse* than a sync framework like Flask, because Flask's worker-per-request model has no shared loop to choke. The reviewer's job is to keep work on the loop genuinely non-blocking and to treat every escape hatch as a cost, not a fix.
|
||||
|
||||
### Never call blocking code inside an `async def` route
|
||||
|
||||
```python
|
||||
# ❌ Bad — blocking I/O on the loop freezes ALL concurrent requests, not just this one
|
||||
@app.get("/report")
|
||||
async def report():
|
||||
data = requests.get("https://slow-api.example.com").json() # blocking socket
|
||||
time.sleep(2) # blocks the loop
|
||||
return data
|
||||
|
||||
# ✅ Good — await a native-async client; the loop serves other requests meanwhile
|
||||
@app.get("/report")
|
||||
async def report(client: httpx.AsyncClient = Depends(get_http)):
|
||||
resp = await client.get("https://slow-api.example.com")
|
||||
return resp.json()
|
||||
```
|
||||
|
||||
### Prefer native-async SDKs over sync libraries
|
||||
|
||||
The right fix for blocking I/O is almost always a library that speaks `async` natively — not wrapping a sync one. Reach for the async client first; the threadpool is the last resort, not the default.
|
||||
|
||||
| Sync (blocks the loop) | Native-async replacement |
|
||||
|------------------------|--------------------------|
|
||||
| `requests` | `httpx.AsyncClient`, `aiohttp` |
|
||||
| `psycopg2` (sync) | `asyncpg`, SQLAlchemy async engine |
|
||||
| `redis-py` (sync) | `redis.asyncio` |
|
||||
| `pymongo` | `motor` |
|
||||
| `boto3` | `aioboto3` |
|
||||
|
||||
If you find `asyncio.run(...)`, a new event loop, or a manually started thread *inside* a route, that is a red flag — it's an attempt to bolt sync code onto the loop. `asyncio.run()` inside a running loop raises `RuntimeError` outright; the rest quietly burns the performance you adopted FastAPI for.
|
||||
|
||||
```python
|
||||
# ❌ Bad — spinning up a loop/thread to call an async SDK from a sync context
|
||||
@app.get("/users/{uid}")
|
||||
def get_user(uid: int):
|
||||
return asyncio.run(repo.fetch(uid)) # RuntimeError under the running loop
|
||||
|
||||
# ✅ Good — let the route be async and await the native client directly
|
||||
@app.get("/users/{uid}")
|
||||
async def get_user(uid: int):
|
||||
return await repo.fetch(uid)
|
||||
```
|
||||
|
||||
### The threadpool is a bounded escape hatch, not a default
|
||||
|
||||
A plain `def` route — and `run_in_threadpool(...)` — does not run on the loop; FastAPI runs it in a **bounded** worker threadpool (AnyIO's default cap is 40 threads). For an occasional, genuinely-unavoidable blocking call this is the correct tool:
|
||||
|
||||
```python
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
|
||||
@app.get("/legacy")
|
||||
async def legacy():
|
||||
return await run_in_threadpool(blocking_library_call) # only if no async SDK exists
|
||||
```
|
||||
|
||||
But it does not scale the way the loop does. Route every hot path through the threadpool and, under load, all workers block at once; further requests queue behind the cap and throughput collapses. Spawning your own threads or processes to "add concurrency" makes it worse: once live threads exceed the machine's core count, context-switch and GIL contention degrade performance sharply rather than improving it. The escape hatch is for the rare blocking dependency you cannot replace — not a substitute for choosing async SDKs.
|
||||
|
||||
Review heuristic: a `def` route is acceptable for a low-traffic endpoint with no async equivalent. A high-traffic endpoint doing blocking work in a `def` route (or via `run_in_threadpool`) is a scaling bug — flag it and ask for an async SDK.
|
||||
|
||||
### CPU-bound work belongs in a worker process, not the loop or the threadpool
|
||||
|
||||
Neither the event loop nor the threadpool helps CPU-bound work: under the GIL only one thread runs Python bytecode at a time, so a heavy computation blocks just as badly from a threadpool as from the loop. Offload it to a separate process (Celery, Arq, RQ, or `multiprocessing`).
|
||||
|
||||
```python
|
||||
# ❌ Bad — a CPU-heavy job pins a worker; throughput drops for everyone
|
||||
@app.post("/render")
|
||||
async def render(doc: Doc):
|
||||
return heavy_pdf_render(doc) # seconds of pure CPU on the loop
|
||||
|
||||
# ✅ Good — enqueue to a worker process; return a job handle
|
||||
@app.post("/render", status_code=202)
|
||||
async def render(doc: Doc):
|
||||
job = await queue.enqueue(heavy_pdf_render, doc)
|
||||
return {"job_id": job.id}
|
||||
```
|
||||
|
||||
### Don't fire-and-forget unawaited coroutines
|
||||
|
||||
```python
|
||||
# ❌ Bad — coroutine never awaited; the email is never sent (and no error surfaces)
|
||||
@app.post("/signup")
|
||||
async def signup(user: UserCreate):
|
||||
send_welcome_email(user.email) # returns a coroutine, silently dropped
|
||||
|
||||
# ✅ Good — defer post-response work with BackgroundTasks
|
||||
@app.post("/signup")
|
||||
async def signup(user: UserCreate, tasks: BackgroundTasks):
|
||||
tasks.add_task(send_welcome_email, user.email)
|
||||
```
|
||||
|
||||
`BackgroundTasks` runs in-process and offers no retries or persistence — use it only for short, fire-and-forget work (send an email, log an event). Anything long-running or retry-critical (data processing, payments) belongs in a real task queue (Celery/Arq/RQ).
|
||||
|
||||
---
|
||||
|
||||
## Database Sessions & N+1
|
||||
|
||||
### One session per request, injected — not a global
|
||||
|
||||
```python
|
||||
# ❌ Bad — a module-level session is shared across concurrent requests (not safe)
|
||||
session = SessionLocal()
|
||||
|
||||
# ✅ Good — request-scoped session via dependency (see get_session above)
|
||||
@app.get("/items")
|
||||
async def list_items(session: AsyncSession = Depends(get_session)):
|
||||
...
|
||||
```
|
||||
|
||||
### Eager-load relationships to avoid N+1
|
||||
|
||||
```python
|
||||
# ❌ Bad — one query for orders, then one query per order for its customer
|
||||
orders = (await session.execute(select(Order))).scalars().all()
|
||||
return [{"id": o.id, "customer": o.customer.name} for o in orders] # N+1
|
||||
|
||||
# ✅ Good — a single query with the relationship eager-loaded
|
||||
stmt = select(Order).options(selectinload(Order.customer))
|
||||
orders = (await session.execute(stmt)).scalars().all()
|
||||
return [{"id": o.id, "customer": o.customer.name} for o in orders]
|
||||
```
|
||||
|
||||
With async SQLAlchemy, lazy attribute access outside the session often raises instead of silently querying — but the design issue is the same. Look for relationship access inside a loop without an `options(...)` eager load.
|
||||
|
||||
### Paginate list endpoints
|
||||
|
||||
```python
|
||||
# ❌ Bad — returns every row; degrades as the table grows
|
||||
@app.get("/users")
|
||||
async def list_users(session: AsyncSession = Depends(get_session)):
|
||||
return (await session.execute(select(User))).scalars().all()
|
||||
|
||||
# ✅ Good — bounded page with a sane cap
|
||||
@app.get("/users", response_model=list[UserOut])
|
||||
async def list_users(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
limit: int = Query(default=50, le=100),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
):
|
||||
stmt = select(User).limit(limit).offset(offset)
|
||||
return (await session.execute(stmt)).scalars().all()
|
||||
```
|
||||
|
||||
### Aggregate and join in SQL, not in Python
|
||||
|
||||
If a handler pulls rows into memory and then loops to group, count, or join them, the database is being used as dumb storage. Push the work down — the database does set operations far faster, and you transfer less data.
|
||||
|
||||
```python
|
||||
# ❌ Bad — fetch every order, then tally per customer in Python
|
||||
orders = (await session.execute(select(Order))).scalars().all()
|
||||
totals: dict[int, float] = {}
|
||||
for o in orders:
|
||||
totals[o.customer_id] = totals.get(o.customer_id, 0) + o.amount
|
||||
|
||||
# ✅ Good — let the database group and sum
|
||||
stmt = select(Order.customer_id, func.sum(Order.amount)).group_by(Order.customer_id)
|
||||
totals = dict((await session.execute(stmt)).all())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
### A declared auth dependency is not an enforced authorization check
|
||||
|
||||
This is the highest-value thing to look for. `Depends(get_current_user)` proves *who* the caller is — it does **not** prove they may touch *this* resource.
|
||||
|
||||
```python
|
||||
# ❌ Bad — any authenticated user can delete any other user's document
|
||||
@app.delete("/documents/{doc_id}")
|
||||
async def delete_document(
|
||||
doc_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
doc = await session.get(Document, doc_id)
|
||||
await session.delete(doc) # never checks doc.owner_id == user.id
|
||||
await session.commit()
|
||||
|
||||
# ✅ Good — ownership is verified before the mutation
|
||||
@app.delete("/documents/{doc_id}", status_code=204)
|
||||
async def delete_document(
|
||||
doc_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
doc = await session.get(Document, doc_id)
|
||||
if doc is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
if doc.owner_id != user.id:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
await session.delete(doc)
|
||||
await session.commit()
|
||||
```
|
||||
|
||||
The [Test-Driven Verification](#test-driven-verification) section reproduces exactly this bug with a failing test.
|
||||
|
||||
### Parameterize SQL; never f-string user input
|
||||
|
||||
```python
|
||||
# ❌ Bad — SQL injection
|
||||
await session.execute(text(f"SELECT * FROM users WHERE email = '{email}'"))
|
||||
|
||||
# ✅ Good — bound parameter
|
||||
await session.execute(text("SELECT * FROM users WHERE email = :email"), {"email": email})
|
||||
```
|
||||
|
||||
### Don't widen CORS to credentials + wildcard
|
||||
|
||||
```python
|
||||
# ❌ Bad — wildcard origin together with credentials is rejected by browsers and unsafe
|
||||
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True)
|
||||
|
||||
# ✅ Good — enumerate trusted origins when credentials are allowed
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["https://app.example.com"],
|
||||
allow_credentials=True,
|
||||
)
|
||||
```
|
||||
|
||||
Also check: secrets read from config/env (not hard-coded), `HTTPException` details that don't leak internals (stack traces, SQL), and rate limiting on auth endpoints.
|
||||
|
||||
---
|
||||
|
||||
## Test-Driven Verification
|
||||
|
||||
> Inspired by the test-driven development discipline: *if you didn't watch the test fail, you don't know it tests the right thing.* This matters even more for a coding agent than for a human reviewer. An agent's reading and reasoning are fallible — it can misread control flow, hallucinate a guarantee that isn't there, or rationalize a comfortable conclusion — so a prose verdict like "this looks safe" carries little weight on its own. An executable test is the one piece of **objective ground truth** the agent fully controls: it either passes or it doesn't, regardless of how confident the reasoning felt. That is what makes tests the agent's anchor of confidence. Reviewing the same way the discipline writes code — reproduce, don't assert — turns a hunch into proof.
|
||||
|
||||
A natural-language review comment ("this might let users delete each other's data") is exactly that kind of fallible hypothesis. FastAPI makes the ground truth cheap to obtain: an in-process client (`httpx.AsyncClient` over `ASGITransport`) runs the whole app, and `app.dependency_overrides` swaps out auth and the database without patching internals. So instead of trusting its own read of the code, the agent settles the question by reproduction.
|
||||
|
||||
### Reproduce a suspected bug with a failing test (Verify RED)
|
||||
|
||||
Suppose the reviewer suspects the `DELETE /documents/{doc_id}` route above never checks ownership. Write the test that asserts the *secure* behavior, then run it and **watch it fail** — the failure is the proof.
|
||||
|
||||
```python
|
||||
# test_document_authorization.py
|
||||
import pytest
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
from fastapi import Header
|
||||
from app.main import app
|
||||
from app.deps import get_current_user, get_session
|
||||
|
||||
# Two users; the override picks one based on a test header.
|
||||
USERS = {"alice": User(id=1, email="alice@example.com"),
|
||||
"bob": User(id=2, email="bob@example.com")}
|
||||
|
||||
def fake_current_user(x_test_user: str = Header(default="alice")) -> User:
|
||||
return USERS[x_test_user]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_cannot_delete_another_users_document(session): # async fixture
|
||||
# Arrange: a document owned by Alice (id=1)
|
||||
session.add(Document(id=10, owner_id=1, title="Alice's doc"))
|
||||
await session.commit()
|
||||
|
||||
app.dependency_overrides[get_current_user] = fake_current_user
|
||||
app.dependency_overrides[get_session] = lambda: session
|
||||
|
||||
# Act: Bob tries to delete Alice's document
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
resp = await client.delete("/documents/10", headers={"X-Test-User": "bob"})
|
||||
|
||||
# Assert the SECURE behavior we expect
|
||||
assert resp.status_code == 403
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
```
|
||||
|
||||
Run it against the unfixed code and confirm the failure is the bug, not a typo:
|
||||
|
||||
```bash
|
||||
$ pytest test_document_authorization.py
|
||||
FAILED assert 204 == 403
|
||||
# ^ the endpoint deleted Alice's document for Bob — vulnerability confirmed
|
||||
```
|
||||
|
||||
A failure of `204 == 403` (not an import error, not a 404) is what makes the finding credible: the route returned success for an action that should have been forbidden. Now the fix from the [Security](#security) section turns it green:
|
||||
|
||||
```bash
|
||||
$ pytest test_document_authorization.py
|
||||
PASSED
|
||||
```
|
||||
|
||||
Attach this test to the review. It documents the vulnerability, proves the fix, and guards against regression — far stronger than "consider checking ownership here."
|
||||
|
||||
### Prefer `dependency_overrides` over `patch`/`mock`
|
||||
|
||||
FastAPI's DI is the seam the TDD discipline asks for: when something is hard to test without mocking everything, that usually signals coupling — and `Depends` already gives you the injection point, so you rarely need `unittest.mock.patch`.
|
||||
|
||||
```python
|
||||
# ❌ Bad — patching internals: brittle, couples the test to import paths
|
||||
@patch("app.routes.orders.asyncpg.connect")
|
||||
def test_get_order(mock_connect): ...
|
||||
|
||||
# ✅ Good — override the dependency with a real in-memory fake
|
||||
app.dependency_overrides[get_session] = lambda: in_memory_session
|
||||
app.dependency_overrides[get_current_user] = lambda: test_user
|
||||
```
|
||||
|
||||
Always reset overrides between tests (`app.dependency_overrides.clear()` in a fixture teardown) so state doesn't leak across tests.
|
||||
|
||||
The reproduction above uses `httpx.AsyncClient` over `ASGITransport` with `@pytest.mark.asyncio` — the community convention for an async app, so the suite shares the app's event loop and you avoid loop-mismatch errors later. The synchronous `TestClient` is simpler and fine for a fully sync app, but standardizing on the async client from the start saves a painful migration once any route or fixture becomes async.
|
||||
|
||||
### Critique the PR's own tests, not just its source
|
||||
|
||||
A PR that ships tests is not automatically safe. Apply these checks to the *tests* in the diff:
|
||||
|
||||
```python
|
||||
# ❌ Bad — happy-path only. Proves the route works when everything is correct,
|
||||
# says nothing about the validation and authorization paths.
|
||||
def test_create_item():
|
||||
resp = client.post("/items", json={"name": "x", "price": 5})
|
||||
assert resp.status_code == 201
|
||||
|
||||
# ✅ Good — the boundary and failure paths are where bugs live
|
||||
def test_create_item_rejects_negative_price():
|
||||
resp = client.post("/items", json={"name": "x", "price": -5})
|
||||
assert resp.status_code == 422
|
||||
|
||||
def test_create_item_requires_authentication():
|
||||
resp = client_without_auth.post("/items", json={"name": "x", "price": 5})
|
||||
assert resp.status_code == 401
|
||||
```
|
||||
|
||||
Review questions for the test suite:
|
||||
|
||||
- **Does it test behavior, or the mock?** An assertion that only confirms a mock was called proves the test's own setup, not the endpoint.
|
||||
- **Are the failure paths covered?** 401/403/404/422 — not just 200/201. Bugs cluster at the boundaries.
|
||||
- **Is the mock complete?** A partial mock of an external API response that omits fields the handler reads passes in the test and fails in production.
|
||||
- **Were the tests written after the fact?** Tests added alongside an implementation and passing on the first run never demonstrated that they can fail — and so prove little. A test that reproduces the bug (fails first, then passes) is worth more than one that was green from birth.
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
- [ ] Routes stay thin — DB access and business rules live behind `Depends`/services
|
||||
- [ ] `yield` dependencies release resources via context manager or `try/finally`
|
||||
- [ ] Singletons (HTTP clients, pools) created once in `lifespan`, not per request
|
||||
- [ ] `Annotated[T, Depends(...)]` form used; dependencies are `async def` unless they do blocking I/O
|
||||
- [ ] Existence/permission checks live in (cached) dependencies, not copy-pasted into routes
|
||||
- [ ] Dependencies are overridable in tests (no resources created inline in the route)
|
||||
|
||||
### Validation
|
||||
|
||||
- [ ] Input and output use distinct Pydantic models; ORM objects are not the `response_model`
|
||||
- [ ] `response_model` set so sensitive fields can't leak
|
||||
- [ ] Separate Create vs Update schemas (update is partial)
|
||||
- [ ] Constraints (`gt`, `le`, `EmailStr`, ...) enforced at the boundary, before the DB write
|
||||
|
||||
### Async
|
||||
|
||||
- [ ] No blocking calls (`requests`, `time.sleep`, blocking DB drivers) inside `async def`
|
||||
- [ ] Native-async SDKs preferred (`httpx`, `asyncpg`, `redis.asyncio`, ...) over sync ones
|
||||
- [ ] No `asyncio.run`/manual event loops/manual threads inside routes
|
||||
- [ ] `run_in_threadpool`/`def` routes used only as a last resort, not on hot paths
|
||||
- [ ] CPU-bound work offloaded to a worker process (Celery/Arq/RQ), not the loop or threadpool
|
||||
- [ ] No unawaited coroutines; `BackgroundTasks` only for short fire-and-forget work
|
||||
|
||||
### Database
|
||||
|
||||
- [ ] One request-scoped session via dependency; no module-level shared session
|
||||
- [ ] Relationships eager-loaded (`selectinload`/`joinedload`) where accessed in a loop
|
||||
- [ ] Joins/aggregations done in SQL, not by looping in Python
|
||||
- [ ] List endpoints are paginated with a capped `limit`
|
||||
|
||||
### Security
|
||||
|
||||
- [ ] Authentication dependency is backed by an explicit **authorization** check (ownership/role)
|
||||
- [ ] All SQL parameterized; no f-string interpolation of user input
|
||||
- [ ] CORS does not combine `allow_origins=["*"]` with `allow_credentials=True`
|
||||
- [ ] Secrets come from config/env; error responses don't leak internals
|
||||
|
||||
### Tests
|
||||
|
||||
- [ ] Suspected bugs reproduced with a failing test (`TestClient`/`AsyncClient`) before being claimed
|
||||
- [ ] `dependency_overrides` used instead of patching internals; overrides reset between tests
|
||||
- [ ] Failure paths covered (401/403/404/422), not just the happy path
|
||||
- [ ] Mocks of external responses are complete, not partial
|
||||
- [ ] New tests demonstrate they can fail (reproduce-then-fix), not green from birth
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [FastAPI official documentation](https://fastapi.tiangolo.com/) — async, dependencies, testing
|
||||
- [zhanymkanov/fastapi-best-practices](https://github.com/zhanymkanov/fastapi-best-practices) — production conventions (async routes, dependency caching, project structure)
|
||||
@@ -0,0 +1,989 @@
|
||||
# Go 代码审查指南
|
||||
|
||||
基于 Go 官方指南、Effective Go 和社区最佳实践的代码审查清单。
|
||||
|
||||
## 快速审查清单
|
||||
|
||||
### 必查项
|
||||
- [ ] 错误是否正确处理(不忽略、有上下文)
|
||||
- [ ] goroutine 是否有退出机制(避免泄漏)
|
||||
- [ ] context 是否正确传递和取消
|
||||
- [ ] 接收器类型选择是否合理(值/指针)
|
||||
- [ ] 是否使用 `gofmt` 格式化代码
|
||||
|
||||
### 高频问题
|
||||
- [ ] 循环变量捕获问题(Go < 1.22)
|
||||
- [ ] nil 检查是否完整
|
||||
- [ ] map 是否初始化后使用
|
||||
- [ ] defer 在循环中的使用
|
||||
- [ ] 变量遮蔽(shadowing)
|
||||
|
||||
---
|
||||
|
||||
## 1. 错误处理
|
||||
|
||||
### 1.1 永远不要忽略错误
|
||||
|
||||
```go
|
||||
// ❌ 错误:忽略错误
|
||||
result, _ := SomeFunction()
|
||||
|
||||
// ✅ 正确:处理错误
|
||||
result, err := SomeFunction()
|
||||
if err != nil {
|
||||
return fmt.Errorf("some function failed: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 错误包装与上下文
|
||||
|
||||
```go
|
||||
// ❌ 错误:丢失上下文
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ❌ 错误:使用 %v 丢失错误链
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed: %v", err)
|
||||
}
|
||||
|
||||
// ✅ 正确:使用 %w 保留错误链
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to process user %d: %w", userID, err)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 使用 errors.Is 和 errors.As
|
||||
|
||||
```go
|
||||
// ❌ 错误:直接比较(无法处理包装错误)
|
||||
if err == sql.ErrNoRows {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ 正确:使用 errors.Is(支持错误链)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
// ✅ 正确:使用 errors.As 提取特定类型
|
||||
var pathErr *os.PathError
|
||||
if errors.As(err, &pathErr) {
|
||||
log.Printf("path error: %s", pathErr.Path)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 自定义错误类型
|
||||
|
||||
```go
|
||||
// ✅ 推荐:定义 sentinel 错误
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
)
|
||||
|
||||
// ✅ 推荐:带上下文的自定义错误
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *ValidationError) Error() string {
|
||||
return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.5 错误处理只做一次
|
||||
|
||||
```go
|
||||
// ❌ 错误:既记录又返回(重复处理)
|
||||
if err != nil {
|
||||
log.Printf("error: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// ✅ 正确:只返回,让调用者决定
|
||||
if err != nil {
|
||||
return fmt.Errorf("operation failed: %w", err)
|
||||
}
|
||||
|
||||
// ✅ 或者:只记录并处理(不返回)
|
||||
if err != nil {
|
||||
log.Printf("non-critical error: %v", err)
|
||||
// 继续执行备用逻辑
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 并发与 Goroutine
|
||||
|
||||
### 2.1 避免 Goroutine 泄漏
|
||||
|
||||
```go
|
||||
// ❌ 错误:goroutine 永远无法退出
|
||||
func bad() {
|
||||
ch := make(chan int)
|
||||
go func() {
|
||||
val := <-ch // 永远阻塞,无人发送
|
||||
fmt.Println(val)
|
||||
}()
|
||||
// 函数返回,goroutine 泄漏
|
||||
}
|
||||
|
||||
// ✅ 正确:使用 context 或 done channel
|
||||
func good(ctx context.Context) {
|
||||
ch := make(chan int)
|
||||
go func() {
|
||||
select {
|
||||
case val := <-ch:
|
||||
fmt.Println(val)
|
||||
case <-ctx.Done():
|
||||
return // 优雅退出
|
||||
}
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Channel 使用规范
|
||||
|
||||
```go
|
||||
// ❌ 错误:向 nil channel 发送(永久阻塞)
|
||||
var ch chan int
|
||||
ch <- 1 // 永久阻塞
|
||||
|
||||
// ❌ 错误:向已关闭的 channel 发送(panic)
|
||||
close(ch)
|
||||
ch <- 1 // panic!
|
||||
|
||||
// ✅ 正确:发送方关闭 channel
|
||||
func producer(ch chan<- int) {
|
||||
defer close(ch) // 发送方负责关闭
|
||||
for i := 0; i < 10; i++ {
|
||||
ch <- i
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 正确:接收方检测关闭
|
||||
for val := range ch {
|
||||
process(val)
|
||||
}
|
||||
// 或者
|
||||
val, ok := <-ch
|
||||
if !ok {
|
||||
// channel 已关闭
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 使用 sync.WaitGroup
|
||||
|
||||
```go
|
||||
// ❌ 错误:Add 在 goroutine 内部
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 10; i++ {
|
||||
go func() {
|
||||
wg.Add(1) // 竞态条件!
|
||||
defer wg.Done()
|
||||
work()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// ✅ 正确:Add 在 goroutine 启动前
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
work()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
```
|
||||
|
||||
### 2.4 避免在循环中捕获变量(Go < 1.22)
|
||||
|
||||
```go
|
||||
// ❌ 错误(Go < 1.22):捕获循环变量
|
||||
for _, item := range items {
|
||||
go func() {
|
||||
process(item) // 所有 goroutine 可能使用同一个 item
|
||||
}()
|
||||
}
|
||||
|
||||
// ✅ 正确:传递参数
|
||||
for _, item := range items {
|
||||
go func(it Item) {
|
||||
process(it)
|
||||
}(item)
|
||||
}
|
||||
|
||||
// ✅ Go 1.22+:默认行为已修复,每次迭代创建新变量
|
||||
```
|
||||
|
||||
### 2.5 Worker Pool 模式
|
||||
|
||||
```go
|
||||
// ✅ 推荐:限制并发数量
|
||||
func processWithWorkerPool(ctx context.Context, items []Item, workers int) error {
|
||||
jobs := make(chan Item, len(items))
|
||||
results := make(chan error, len(items))
|
||||
|
||||
// 启动 worker
|
||||
for w := 0; w < workers; w++ {
|
||||
go func() {
|
||||
for item := range jobs {
|
||||
results <- process(item)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送任务
|
||||
for _, item := range items {
|
||||
jobs <- item
|
||||
}
|
||||
close(jobs)
|
||||
|
||||
// 收集结果
|
||||
for range items {
|
||||
if err := <-results; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Context 使用
|
||||
|
||||
### 3.1 Context 作为第一个参数
|
||||
|
||||
```go
|
||||
// ❌ 错误:context 不是第一个参数
|
||||
func Process(data []byte, ctx context.Context) error
|
||||
|
||||
// ❌ 错误:context 存储在 struct 中
|
||||
type Service struct {
|
||||
ctx context.Context // 不要这样做!
|
||||
}
|
||||
|
||||
// ✅ 正确:context 作为第一个参数,命名为 ctx
|
||||
func Process(ctx context.Context, data []byte) error
|
||||
```
|
||||
|
||||
### 3.2 传播而非创建新的根 Context
|
||||
|
||||
```go
|
||||
// ❌ 错误:在调用链中创建新的根 context
|
||||
func middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background() // 丢失了请求的 context!
|
||||
process(ctx)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// ✅ 正确:从请求中获取并传播
|
||||
func middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, key, value)
|
||||
process(ctx)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 始终调用 cancel 函数
|
||||
|
||||
```go
|
||||
// ❌ 错误:未调用 cancel
|
||||
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
|
||||
// 缺少 cancel() 调用,可能资源泄漏
|
||||
|
||||
// ✅ 正确:使用 defer 确保调用
|
||||
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
|
||||
defer cancel() // 即使超时也要调用
|
||||
```
|
||||
|
||||
### 3.4 响应 Context 取消
|
||||
|
||||
```go
|
||||
// ✅ 推荐:在长时间操作中检查 context
|
||||
func LongRunningTask(ctx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err() // 返回 context.Canceled 或 context.DeadlineExceeded
|
||||
default:
|
||||
// 执行一小部分工作
|
||||
if err := doChunk(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 区分取消原因
|
||||
|
||||
```go
|
||||
// ✅ 根据 ctx.Err() 区分取消原因
|
||||
if err := ctx.Err(); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, context.Canceled):
|
||||
log.Println("operation was canceled")
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
log.Println("operation timed out")
|
||||
}
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 接口设计
|
||||
|
||||
### 4.1 接受接口,返回结构体
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:接受具体类型
|
||||
func SaveUser(db *sql.DB, user User) error
|
||||
|
||||
// ✅ 推荐:接受接口(解耦、易测试)
|
||||
type UserStore interface {
|
||||
Save(ctx context.Context, user User) error
|
||||
}
|
||||
|
||||
func SaveUser(store UserStore, user User) error
|
||||
|
||||
// ❌ 不推荐:返回接口
|
||||
func NewUserService() UserServiceInterface
|
||||
|
||||
// ✅ 推荐:返回具体类型
|
||||
func NewUserService(store UserStore) *UserService
|
||||
```
|
||||
|
||||
### 4.2 在消费者处定义接口
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:在实现包中定义接口
|
||||
// package database
|
||||
type Database interface {
|
||||
Query(ctx context.Context, query string) ([]Row, error)
|
||||
// ... 20 个方法
|
||||
}
|
||||
|
||||
// ✅ 推荐:在消费者包中定义所需的最小接口
|
||||
// package userservice
|
||||
type UserQuerier interface {
|
||||
QueryUsers(ctx context.Context, filter Filter) ([]User, error)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 保持接口小而专注
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:大而全的接口
|
||||
type Repository interface {
|
||||
GetUser(id int) (*User, error)
|
||||
CreateUser(u *User) error
|
||||
UpdateUser(u *User) error
|
||||
DeleteUser(id int) error
|
||||
GetOrder(id int) (*Order, error)
|
||||
CreateOrder(o *Order) error
|
||||
// ... 更多方法
|
||||
}
|
||||
|
||||
// ✅ 推荐:小而专注的接口
|
||||
type UserReader interface {
|
||||
GetUser(ctx context.Context, id int) (*User, error)
|
||||
}
|
||||
|
||||
type UserWriter interface {
|
||||
CreateUser(ctx context.Context, u *User) error
|
||||
UpdateUser(ctx context.Context, u *User) error
|
||||
}
|
||||
|
||||
// 组合接口
|
||||
type UserRepository interface {
|
||||
UserReader
|
||||
UserWriter
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 避免空接口滥用
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:过度使用 interface{}
|
||||
func Process(data interface{}) interface{}
|
||||
|
||||
// ✅ 推荐:使用泛型(Go 1.18+)
|
||||
func Process[T any](data T) T
|
||||
|
||||
// ✅ 推荐:定义具体接口
|
||||
type Processor interface {
|
||||
Process() Result
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 接收器类型选择
|
||||
|
||||
### 5.1 使用指针接收器的情况
|
||||
|
||||
```go
|
||||
// ✅ 需要修改接收器时
|
||||
func (u *User) SetName(name string) {
|
||||
u.Name = name
|
||||
}
|
||||
|
||||
// ✅ 接收器包含 sync.Mutex 等同步原语
|
||||
type SafeCounter struct {
|
||||
mu sync.Mutex
|
||||
count int
|
||||
}
|
||||
|
||||
func (c *SafeCounter) Inc() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.count++
|
||||
}
|
||||
|
||||
// ✅ 接收器是大型结构体(避免复制开销)
|
||||
type LargeStruct struct {
|
||||
Data [1024]byte
|
||||
// ...
|
||||
}
|
||||
|
||||
func (l *LargeStruct) Process() { /* ... */ }
|
||||
```
|
||||
|
||||
### 5.2 使用值接收器的情况
|
||||
|
||||
```go
|
||||
// ✅ 接收器是小型不可变结构体
|
||||
type Point struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
func (p Point) Distance(other Point) float64 {
|
||||
return math.Sqrt(math.Pow(p.X-other.X, 2) + math.Pow(p.Y-other.Y, 2))
|
||||
}
|
||||
|
||||
// ✅ 接收器是基本类型的别名
|
||||
type Counter int
|
||||
|
||||
func (c Counter) String() string {
|
||||
return fmt.Sprintf("%d", c)
|
||||
}
|
||||
|
||||
// ✅ 接收器是 map、func、chan(本身是引用类型)
|
||||
type StringSet map[string]struct{}
|
||||
|
||||
func (s StringSet) Contains(key string) bool {
|
||||
_, ok := s[key]
|
||||
return ok
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 一致性原则
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:混合使用接收器类型
|
||||
func (u User) GetName() string // 值接收器
|
||||
func (u *User) SetName(n string) // 指针接收器
|
||||
|
||||
// ✅ 推荐:如果有任何方法需要指针接收器,全部使用指针
|
||||
func (u *User) GetName() string { return u.Name }
|
||||
func (u *User) SetName(n string) { u.Name = n }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 性能优化
|
||||
|
||||
### 6.1 预分配 Slice
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:动态增长
|
||||
var result []int
|
||||
for i := 0; i < 10000; i++ {
|
||||
result = append(result, i) // 多次分配和复制
|
||||
}
|
||||
|
||||
// ✅ 推荐:预分配已知大小
|
||||
result := make([]int, 0, 10000)
|
||||
for i := 0; i < 10000; i++ {
|
||||
result = append(result, i)
|
||||
}
|
||||
|
||||
// ✅ 或者直接初始化
|
||||
result := make([]int, 10000)
|
||||
for i := 0; i < 10000; i++ {
|
||||
result[i] = i
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 避免不必要的堆分配
|
||||
|
||||
```go
|
||||
// ❌ 可能逃逸到堆
|
||||
func NewUser() *User {
|
||||
return &User{} // 逃逸到堆
|
||||
}
|
||||
|
||||
// ✅ 考虑返回值(如果适用)
|
||||
func NewUser() User {
|
||||
return User{} // 可能在栈上分配
|
||||
}
|
||||
|
||||
// 检查逃逸分析
|
||||
// go build -gcflags '-m -m' ./...
|
||||
```
|
||||
|
||||
### 6.3 使用 sync.Pool 复用对象
|
||||
|
||||
```go
|
||||
// ✅ 推荐:高频创建/销毁的对象使用 sync.Pool
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
func ProcessData(data []byte) string {
|
||||
buf := bufferPool.Get().(*bytes.Buffer)
|
||||
defer func() {
|
||||
buf.Reset()
|
||||
bufferPool.Put(buf)
|
||||
}()
|
||||
|
||||
buf.Write(data)
|
||||
return buf.String()
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 字符串拼接优化
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:循环中使用 + 拼接
|
||||
var result string
|
||||
for _, s := range strings {
|
||||
result += s // 每次创建新字符串
|
||||
}
|
||||
|
||||
// ✅ 推荐:使用 strings.Builder
|
||||
var builder strings.Builder
|
||||
for _, s := range strings {
|
||||
builder.WriteString(s)
|
||||
}
|
||||
result := builder.String()
|
||||
|
||||
// ✅ 或者使用 strings.Join
|
||||
result := strings.Join(strings, "")
|
||||
```
|
||||
|
||||
### 6.5 避免 interface{} 转换开销
|
||||
|
||||
```go
|
||||
// ❌ 热路径中使用 interface{}
|
||||
func process(data interface{}) {
|
||||
switch v := data.(type) { // 类型断言有开销
|
||||
case int:
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 热路径中使用泛型或具体类型
|
||||
func process[T int | int64 | float64](data T) {
|
||||
// 编译时确定类型,无运行时开销
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试
|
||||
|
||||
### 7.1 表驱动测试
|
||||
|
||||
```go
|
||||
// ✅ 推荐:表驱动测试
|
||||
func TestAdd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b int
|
||||
expected int
|
||||
}{
|
||||
{"positive numbers", 1, 2, 3},
|
||||
{"with zero", 0, 5, 5},
|
||||
{"negative numbers", -1, -2, -3},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := Add(tt.a, tt.b)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Add(%d, %d) = %d; want %d",
|
||||
tt.a, tt.b, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 并行测试
|
||||
|
||||
```go
|
||||
// ✅ 推荐:独立测试用例并行执行
|
||||
func TestParallel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{"test1", "input1"},
|
||||
{"test2", "input2"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt // Go < 1.22 需要复制
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel() // 标记为可并行
|
||||
result := Process(tt.input)
|
||||
// assertions...
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 使用接口进行 Mock
|
||||
|
||||
```go
|
||||
// ✅ 定义接口以便测试
|
||||
type EmailSender interface {
|
||||
Send(to, subject, body string) error
|
||||
}
|
||||
|
||||
// 生产实现
|
||||
type SMTPSender struct { /* ... */ }
|
||||
|
||||
// 测试 Mock
|
||||
type MockEmailSender struct {
|
||||
SendFunc func(to, subject, body string) error
|
||||
}
|
||||
|
||||
func (m *MockEmailSender) Send(to, subject, body string) error {
|
||||
return m.SendFunc(to, subject, body)
|
||||
}
|
||||
|
||||
func TestUserRegistration(t *testing.T) {
|
||||
mock := &MockEmailSender{
|
||||
SendFunc: func(to, subject, body string) error {
|
||||
if to != "test@example.com" {
|
||||
t.Errorf("unexpected recipient: %s", to)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
service := NewUserService(mock)
|
||||
// test...
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 测试辅助函数
|
||||
|
||||
```go
|
||||
// ✅ 使用 t.Helper() 标记辅助函数
|
||||
func assertEqual(t *testing.T, got, want interface{}) {
|
||||
t.Helper() // 错误报告时显示调用者位置
|
||||
if got != want {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 使用 t.Cleanup() 清理资源
|
||||
func TestWithTempFile(t *testing.T) {
|
||||
f, err := os.CreateTemp("", "test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
os.Remove(f.Name())
|
||||
})
|
||||
// test...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 常见陷阱
|
||||
|
||||
### 8.1 Nil Slice vs Empty Slice
|
||||
|
||||
```go
|
||||
var nilSlice []int // nil, len=0, cap=0
|
||||
emptySlice := []int{} // not nil, len=0, cap=0
|
||||
made := make([]int, 0) // not nil, len=0, cap=0
|
||||
|
||||
// ✅ JSON 编码差异
|
||||
json.Marshal(nilSlice) // null
|
||||
json.Marshal(emptySlice) // []
|
||||
|
||||
// ✅ 推荐:需要空数组 JSON 时显式初始化
|
||||
if slice == nil {
|
||||
slice = []int{}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 Map 初始化
|
||||
|
||||
```go
|
||||
// ❌ 错误:未初始化的 map
|
||||
var m map[string]int
|
||||
m["key"] = 1 // panic: assignment to entry in nil map
|
||||
|
||||
// ✅ 正确:使用 make 初始化
|
||||
m := make(map[string]int)
|
||||
m["key"] = 1
|
||||
|
||||
// ✅ 或者使用字面量
|
||||
m := map[string]int{}
|
||||
```
|
||||
|
||||
### 8.3 Defer 在循环中
|
||||
|
||||
```go
|
||||
// ❌ 潜在问题:defer 在函数结束时才执行
|
||||
func processFiles(files []string) error {
|
||||
for _, file := range files {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close() // 所有文件在函数结束时才关闭!
|
||||
// process...
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ✅ 正确:使用闭包或提取函数
|
||||
func processFiles(files []string) error {
|
||||
for _, file := range files {
|
||||
if err := processFile(file); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func processFile(file string) error {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
// process...
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 8.4 Slice 底层数组共享
|
||||
|
||||
```go
|
||||
// ❌ 潜在问题:切片共享底层数组
|
||||
original := []int{1, 2, 3, 4, 5}
|
||||
slice := original[1:3] // [2, 3]
|
||||
slice[0] = 100 // 修改了 original!
|
||||
// original 变成 [1, 100, 3, 4, 5]
|
||||
|
||||
// ✅ 正确:需要独立副本时显式复制
|
||||
slice := make([]int, 2)
|
||||
copy(slice, original[1:3])
|
||||
slice[0] = 100 // 不影响 original
|
||||
```
|
||||
|
||||
### 8.5 字符串子串内存泄漏
|
||||
|
||||
```go
|
||||
// ❌ 潜在问题:子串持有整个底层数组
|
||||
func getPrefix(s string) string {
|
||||
return s[:10] // 仍引用整个 s 的底层数组
|
||||
}
|
||||
|
||||
// ✅ 正确:创建独立副本(Go 1.18+)
|
||||
func getPrefix(s string) string {
|
||||
return strings.Clone(s[:10])
|
||||
}
|
||||
|
||||
// ✅ Go 1.18 之前
|
||||
func getPrefix(s string) string {
|
||||
return string([]byte(s[:10]))
|
||||
}
|
||||
```
|
||||
|
||||
### 8.6 Interface Nil 陷阱
|
||||
|
||||
```go
|
||||
// ❌ 陷阱:interface 的 nil 判断
|
||||
type MyError struct{}
|
||||
func (e *MyError) Error() string { return "error" }
|
||||
|
||||
func returnsError() error {
|
||||
var e *MyError = nil
|
||||
return e // 返回的 error 不是 nil!
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := returnsError()
|
||||
if err != nil { // true! interface{type: *MyError, value: nil}
|
||||
fmt.Println("error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 正确:显式返回 nil
|
||||
func returnsError() error {
|
||||
var e *MyError = nil
|
||||
if e == nil {
|
||||
return nil // 显式返回 nil
|
||||
}
|
||||
return e
|
||||
}
|
||||
```
|
||||
|
||||
### 8.7 Time 比较
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:直接使用 == 比较 time.Time
|
||||
if t1 == t2 { // 可能因为单调时钟差异而失败
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ 推荐:使用 Equal 方法
|
||||
if t1.Equal(t2) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ 比较时间范围
|
||||
if t1.Before(t2) || t1.After(t2) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 代码组织
|
||||
|
||||
### 9.1 包命名
|
||||
|
||||
```go
|
||||
// ❌ 不推荐
|
||||
package common // 过于宽泛
|
||||
package utils // 过于宽泛
|
||||
package helpers // 过于宽泛
|
||||
package models // 按类型分组
|
||||
|
||||
// ✅ 推荐:按功能命名
|
||||
package user // 用户相关功能
|
||||
package order // 订单相关功能
|
||||
package postgres // PostgreSQL 实现
|
||||
```
|
||||
|
||||
### 9.2 避免循环依赖
|
||||
|
||||
```go
|
||||
// ❌ 循环依赖
|
||||
// package a imports package b
|
||||
// package b imports package a
|
||||
|
||||
// ✅ 解决方案1:提取共享类型到独立包
|
||||
// package types (共享类型)
|
||||
// package a imports types
|
||||
// package b imports types
|
||||
|
||||
// ✅ 解决方案2:使用接口解耦
|
||||
// package a 定义接口
|
||||
// package b 实现接口
|
||||
```
|
||||
|
||||
### 9.3 导出标识符规范
|
||||
|
||||
```go
|
||||
// ✅ 只导出必要的标识符
|
||||
type UserService struct {
|
||||
db *sql.DB // 私有
|
||||
}
|
||||
|
||||
func (s *UserService) GetUser(id int) (*User, error) // 公开
|
||||
func (s *UserService) validate(u *User) error // 私有
|
||||
|
||||
// ✅ 内部包限制访问
|
||||
// internal/database/... 只能被同项目代码导入
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 工具与检查
|
||||
|
||||
### 10.1 必须使用的工具
|
||||
|
||||
```bash
|
||||
# 格式化(必须)
|
||||
gofmt -w .
|
||||
goimports -w .
|
||||
|
||||
# 静态分析
|
||||
go vet ./...
|
||||
|
||||
# 竞态检测
|
||||
go test -race ./...
|
||||
|
||||
# 逃逸分析
|
||||
go build -gcflags '-m -m' ./...
|
||||
```
|
||||
|
||||
### 10.2 推荐的 Linter
|
||||
|
||||
```bash
|
||||
# golangci-lint(集成多个 linter)
|
||||
golangci-lint run
|
||||
|
||||
# 常用检查项
|
||||
# - errcheck: 检查未处理的错误
|
||||
# - gosec: 安全检查
|
||||
# - ineffassign: 无效赋值
|
||||
# - staticcheck: 静态分析
|
||||
# - unused: 未使用的代码
|
||||
```
|
||||
|
||||
### 10.3 Benchmark 测试
|
||||
|
||||
```go
|
||||
// ✅ 性能基准测试
|
||||
func BenchmarkProcess(b *testing.B) {
|
||||
data := prepareData()
|
||||
b.ResetTimer() // 重置计时器
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
Process(data)
|
||||
}
|
||||
}
|
||||
|
||||
// 运行 benchmark
|
||||
// go test -bench=. -benchmem ./...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Effective Go](https://go.dev/doc/effective_go)
|
||||
- [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments)
|
||||
- [Go Common Mistakes](https://go.dev/wiki/CommonMistakes)
|
||||
- [100 Go Mistakes](https://100go.co/)
|
||||
- [Go Proverbs](https://go-proverbs.github.io/)
|
||||
- [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md)
|
||||
@@ -0,0 +1,405 @@
|
||||
# Java Code Review Guide
|
||||
|
||||
Java 审查重点:Java 17/21 新特性、Spring Boot 3 最佳实践、并发编程(虚拟线程)、JPA 性能优化以及代码可维护性。
|
||||
|
||||
## 目录
|
||||
|
||||
- [现代 Java 特性 (17/21+)](#现代-java-特性-1721)
|
||||
- [Stream API & Optional](#stream-api--optional)
|
||||
- [Spring Boot 最佳实践](#spring-boot-最佳实践)
|
||||
- [JPA 与 数据库性能](#jpa-与-数据库性能)
|
||||
- [并发与虚拟线程](#并发与虚拟线程)
|
||||
- [Lombok 使用规范](#lombok-使用规范)
|
||||
- [异常处理](#异常处理)
|
||||
- [测试规范](#测试规范)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 现代 Java 特性 (17/21+)
|
||||
|
||||
### Record (记录类)
|
||||
|
||||
```java
|
||||
// ❌ 传统的 POJO/DTO:样板代码多
|
||||
public class UserDto {
|
||||
private final String name;
|
||||
private final int age;
|
||||
|
||||
public UserDto(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
// getters, equals, hashCode, toString...
|
||||
}
|
||||
|
||||
// ✅ 使用 Record:简洁、不可变、语义清晰
|
||||
public record UserDto(String name, int age) {
|
||||
// 紧凑构造函数进行验证
|
||||
public UserDto {
|
||||
if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Switch 表达式与模式匹配
|
||||
|
||||
```java
|
||||
// ❌ 传统的 Switch:容易漏掉 break,不仅冗长且易错
|
||||
String type = "";
|
||||
switch (obj) {
|
||||
case Integer i: // Java 16+
|
||||
type = String.format("int %d", i);
|
||||
break;
|
||||
case String s:
|
||||
type = String.format("string %s", s);
|
||||
break;
|
||||
default:
|
||||
type = "unknown";
|
||||
}
|
||||
|
||||
// ✅ Switch 表达式:无穿透风险,强制返回值
|
||||
String type = switch (obj) {
|
||||
case Integer i -> "int %d".formatted(i);
|
||||
case String s -> "string %s".formatted(s);
|
||||
case null -> "null value"; // Java 21 处理 null
|
||||
default -> "unknown";
|
||||
};
|
||||
```
|
||||
|
||||
### 文本块 (Text Blocks)
|
||||
|
||||
```java
|
||||
// ❌ 拼接 SQL/JSON 字符串
|
||||
String json = "{\n" +
|
||||
" \"name\": \"Alice\",\n" +
|
||||
" \"age\": 20\n" +
|
||||
"}";
|
||||
|
||||
// ✅ 使用文本块:所见即所得
|
||||
String json = """
|
||||
{
|
||||
"name": "Alice",
|
||||
"age": 20
|
||||
}
|
||||
""";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stream API & Optional
|
||||
|
||||
### 避免滥用 Stream
|
||||
|
||||
```java
|
||||
// ❌ 简单的循环不需要 Stream(性能开销 + 可读性差)
|
||||
items.stream().forEach(item -> {
|
||||
process(item);
|
||||
});
|
||||
|
||||
// ✅ 简单场景直接用 for-each
|
||||
for (var item : items) {
|
||||
process(item);
|
||||
}
|
||||
|
||||
// ❌ 极其复杂的 Stream 链
|
||||
List<Dto> result = list.stream()
|
||||
.filter(...)
|
||||
.map(...)
|
||||
.peek(...)
|
||||
.sorted(...)
|
||||
.collect(...); // 难以调试
|
||||
|
||||
// ✅ 拆分为有意义的步骤
|
||||
var filtered = list.stream().filter(...).toList();
|
||||
// ...
|
||||
```
|
||||
|
||||
### Optional 正确用法
|
||||
|
||||
```java
|
||||
// ❌ 将 Optional 用作参数或字段(序列化问题,增加调用复杂度)
|
||||
public void process(Optional<String> name) { ... }
|
||||
public class User {
|
||||
private Optional<String> email; // 不推荐
|
||||
}
|
||||
|
||||
// ✅ Optional 仅用于返回值
|
||||
public Optional<User> findUser(String id) { ... }
|
||||
|
||||
// ❌ 既然用了 Optional 还在用 isPresent() + get()
|
||||
Optional<User> userOpt = findUser(id);
|
||||
if (userOpt.isPresent()) {
|
||||
return userOpt.get().getName();
|
||||
} else {
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
// ✅ 使用函数式 API
|
||||
return findUser(id)
|
||||
.map(User::getName)
|
||||
.orElse("Unknown");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spring Boot 最佳实践
|
||||
|
||||
### 依赖注入 (DI)
|
||||
|
||||
```java
|
||||
// ❌ 字段注入 (@Autowired)
|
||||
// 缺点:难以测试(需要反射注入),掩盖了依赖过多的问题,且不可变性差
|
||||
@Service
|
||||
public class UserService {
|
||||
@Autowired
|
||||
private UserRepository userRepo;
|
||||
}
|
||||
|
||||
// ✅ 构造器注入 (Constructor Injection)
|
||||
// 优点:依赖明确,易于单元测试 (Mock),字段可为 final
|
||||
@Service
|
||||
public class UserService {
|
||||
private final UserRepository userRepo;
|
||||
|
||||
public UserService(UserRepository userRepo) {
|
||||
this.userRepo = userRepo;
|
||||
}
|
||||
}
|
||||
// 💡 提示:结合 Lombok @RequiredArgsConstructor 可简化代码,但要小心循环依赖
|
||||
```
|
||||
|
||||
### 配置管理
|
||||
|
||||
```java
|
||||
// ❌ 硬编码配置值
|
||||
@Service
|
||||
public class PaymentService {
|
||||
private String apiKey = "sk_live_12345";
|
||||
}
|
||||
|
||||
// ❌ 直接使用 @Value 散落在代码中
|
||||
@Value("${app.payment.api-key}")
|
||||
private String apiKey;
|
||||
|
||||
// ✅ 使用 @ConfigurationProperties 类型安全配置
|
||||
@ConfigurationProperties(prefix = "app.payment")
|
||||
public record PaymentProperties(String apiKey, int timeout, String url) {}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JPA 与 数据库性能
|
||||
|
||||
### N+1 查询问题
|
||||
|
||||
```java
|
||||
// ❌ FetchType.EAGER 或 循环中触发懒加载
|
||||
// Entity 定义
|
||||
@Entity
|
||||
public class User {
|
||||
@OneToMany(fetch = FetchType.EAGER) // 危险!
|
||||
private List<Order> orders;
|
||||
}
|
||||
|
||||
// 业务代码
|
||||
List<User> users = userRepo.findAll(); // 1 条 SQL
|
||||
for (User user : users) {
|
||||
// 如果是 Lazy,这里会触发 N 条 SQL
|
||||
System.out.println(user.getOrders().size());
|
||||
}
|
||||
|
||||
// ✅ 使用 @EntityGraph 或 JOIN FETCH
|
||||
@Query("SELECT u FROM User u JOIN FETCH u.orders")
|
||||
List<User> findAllWithOrders();
|
||||
```
|
||||
|
||||
### 事务管理
|
||||
|
||||
```java
|
||||
// ❌ 在 Controller 层开启事务(数据库连接占用时间过长)
|
||||
// ❌ 在 private 方法上加 @Transactional(AOP 不生效)
|
||||
@Transactional
|
||||
private void saveInternal() { ... }
|
||||
|
||||
// ✅ 在 Service 层公共方法加 @Transactional
|
||||
// ✅ 读操作显式标记 readOnly = true (性能优化)
|
||||
@Service
|
||||
public class UserService {
|
||||
@Transactional(readOnly = true)
|
||||
public User getUser(Long id) { ... }
|
||||
|
||||
@Transactional
|
||||
public void createUser(UserDto dto) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Entity 设计
|
||||
|
||||
```java
|
||||
// ❌ 在 Entity 中使用 Lombok @Data
|
||||
// @Data 生成的 equals/hashCode 包含所有字段,可能触发懒加载导致性能问题或异常
|
||||
@Entity
|
||||
@Data
|
||||
public class User { ... }
|
||||
|
||||
// ✅ 仅使用 @Getter, @Setter
|
||||
// ✅ 自定义 equals/hashCode (通常基于 ID)
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
public class User {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof User)) return false;
|
||||
return id != null && id.equals(((User) o).id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 并发与虚拟线程
|
||||
|
||||
### 虚拟线程 (Java 21+)
|
||||
|
||||
```java
|
||||
// ❌ 传统线程池处理大量 I/O 阻塞任务(资源耗尽)
|
||||
ExecutorService executor = Executors.newFixedThreadPool(100);
|
||||
|
||||
// ✅ 使用虚拟线程处理 I/O 密集型任务(高吞吐量)
|
||||
// Spring Boot 3.2+ 开启:spring.threads.virtual.enabled=true
|
||||
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||
|
||||
// 在虚拟线程中,阻塞操作(如 DB 查询、HTTP 请求)几乎不消耗 OS 线程资源
|
||||
```
|
||||
|
||||
### 线程安全
|
||||
|
||||
```java
|
||||
// ❌ SimpleDateFormat 是线程不安全的
|
||||
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
|
||||
// ✅ 使用 DateTimeFormatter (Java 8+)
|
||||
private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
// ❌ HashMap 在多线程环境会数据丢失(Java 7 及之前 resize 还可能死循环,Java 8 修复了死循环但仍非线程安全)
|
||||
// ✅ 使用 ConcurrentHashMap
|
||||
Map<String, String> cache = new ConcurrentHashMap<>();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lombok 使用规范
|
||||
|
||||
```java
|
||||
// ❌ 滥用 @Builder 导致无法强制校验必填字段
|
||||
@Builder
|
||||
public class Order {
|
||||
private String id; // 必填
|
||||
private String note; // 选填
|
||||
}
|
||||
// 调用者可能漏掉 id: Order.builder().note("hi").build();
|
||||
|
||||
// ✅ 关键业务对象建议手动编写 Builder 或构造函数以确保不变量
|
||||
// 或者在 build() 方法中添加校验逻辑 (Lombok @Builder.Default 等)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 异常处理
|
||||
|
||||
### 全局异常处理
|
||||
|
||||
```java
|
||||
// ❌ 到处 try-catch 吞掉异常或只打印日志
|
||||
try {
|
||||
userService.create(user);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(); // 不应该在生产环境使用
|
||||
// return null; // 吞掉异常,上层不知道发生了什么
|
||||
}
|
||||
|
||||
// ✅ 自定义异常 + @ControllerAdvice (Spring Boot 3 ProblemDetail)
|
||||
public class UserNotFoundException extends RuntimeException { ... }
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(UserNotFoundException.class)
|
||||
public ProblemDetail handleNotFound(UserNotFoundException e) {
|
||||
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试规范
|
||||
|
||||
### 单元测试 vs 集成测试
|
||||
|
||||
```java
|
||||
// ❌ 单元测试依赖真实数据库或外部服务
|
||||
@SpringBootTest // 启动整个 Context,慢
|
||||
public class UserServiceTest { ... }
|
||||
|
||||
// ✅ 单元测试使用 Mockito
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserServiceTest {
|
||||
@Mock UserRepository repo;
|
||||
@InjectMocks UserService service;
|
||||
|
||||
@Test
|
||||
void shouldCreateUser() { ... }
|
||||
}
|
||||
|
||||
// ✅ 集成测试使用 Testcontainers
|
||||
@Testcontainers
|
||||
@SpringBootTest
|
||||
class UserRepositoryTest {
|
||||
@Container
|
||||
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### 基础与规范
|
||||
- [ ] 遵循 Java 17/21 新特性(Switch 表达式, Records, 文本块)
|
||||
- [ ] 避免使用已过时的类(Date, Calendar, SimpleDateFormat)
|
||||
- [ ] 集合操作是否优先使用了 Stream API 或 Collections 方法?
|
||||
- [ ] Optional 仅用于返回值,未用于字段或参数
|
||||
|
||||
### Spring Boot
|
||||
- [ ] 使用构造器注入而非 @Autowired 字段注入
|
||||
- [ ] 配置属性使用了 @ConfigurationProperties
|
||||
- [ ] Controller 职责单一,业务逻辑下沉到 Service
|
||||
- [ ] 全局异常处理使用了 @ControllerAdvice / ProblemDetail
|
||||
|
||||
### 数据库 & 事务
|
||||
- [ ] 读操作事务标记了 `@Transactional(readOnly = true)`
|
||||
- [ ] 检查是否存在 N+1 查询(EAGER fetch 或循环调用)
|
||||
- [ ] Entity 类未使用 @Data,正确实现了 equals/hashCode
|
||||
- [ ] 数据库索引是否覆盖了查询条件
|
||||
|
||||
### 并发与性能
|
||||
- [ ] I/O 密集型任务是否考虑了虚拟线程?
|
||||
- [ ] 线程安全类是否使用正确(ConcurrentHashMap vs HashMap)
|
||||
- [ ] 锁的粒度是否合理?避免在锁内进行 I/O 操作
|
||||
|
||||
### 可维护性
|
||||
- [ ] 关键业务逻辑有充分的单元测试
|
||||
- [ ] 日志记录恰当(使用 Slf4j,避免 System.out)
|
||||
- [ ] 魔法值提取为常量或枚举
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,593 @@
|
||||
# NestJS Code Review Guide
|
||||
|
||||
> NestJS 代码审查指南,覆盖依赖注入与分层架构、模块组织、Guard/Interceptor/Pipe、DTO 验证、错误处理、循环依赖及测试模式等核心主题。
|
||||
|
||||
## 目录
|
||||
|
||||
- [依赖注入与分层架构](#依赖注入与分层架构)
|
||||
- [模块组织](#模块组织)
|
||||
- [Guard / Interceptor / Pipe](#guard--interceptor--pipe)
|
||||
- [验证模式 (DTO)](#验证模式-dto)
|
||||
- [错误处理](#错误处理)
|
||||
- [循环依赖](#循环依赖)
|
||||
- [测试模式](#测试模式)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 依赖注入与分层架构
|
||||
|
||||
### 三层架构:Controller → Service → Repository
|
||||
|
||||
```typescript
|
||||
// ❌ ORM 直接注入 Controller,跳过 Service 层
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.prisma.user.findMany();
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Controller → Service → Repository
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.usersService.findAll();
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(private readonly usersRepo: UsersRepository) {}
|
||||
|
||||
findAll() {
|
||||
return this.usersRepo.findAll();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Repository 之间不应互相注入
|
||||
|
||||
```typescript
|
||||
// ❌ Repository 导入另一个 Repository——编排逻辑属于 Service
|
||||
@Injectable()
|
||||
export class OrdersRepository {
|
||||
constructor(private readonly usersRepository: UsersRepository) {}
|
||||
}
|
||||
|
||||
// ✅ 跨 Repository 编排在 Service 中完成
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
constructor(
|
||||
private readonly ordersRepo: OrdersRepository,
|
||||
private readonly usersRepo: UsersRepository,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### God Service:依赖超过 8 个时拆分
|
||||
|
||||
```typescript
|
||||
// ❌ 9 个依赖的巨型 Service
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
constructor(
|
||||
private readonly ordersRepo: OrdersRepository,
|
||||
private readonly usersRepo: UsersRepository,
|
||||
private readonly productsRepo: ProductsRepository,
|
||||
private readonly paymentsService: PaymentsService,
|
||||
private readonly mailerService: MailerService,
|
||||
private readonly inventoryService: InventoryService,
|
||||
private readonly discountService: DiscountService,
|
||||
private readonly taxService: TaxService,
|
||||
private readonly auditService: AuditService,
|
||||
) {}
|
||||
}
|
||||
|
||||
// ✅ 拆分为 Use-Case Service(一个文件一个操作)
|
||||
@Injectable()
|
||||
export class CreateOrderService {
|
||||
constructor(
|
||||
private readonly ordersRepo: OrdersRepository,
|
||||
private readonly paymentsService: PaymentsService,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateOrderDto) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Symbol Token 实现依赖反转
|
||||
|
||||
```typescript
|
||||
// ❌ 直接依赖具体实现——测试时无法替换
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(private readonly repo: TypeOrmUserRepository) {}
|
||||
}
|
||||
|
||||
// ✅ 接口 + Symbol Token——可替换为内存实现
|
||||
export const USER_REPOSITORY = Symbol('USER_REPOSITORY');
|
||||
|
||||
export interface UserRepository {
|
||||
findAll(): Promise<User[]>;
|
||||
findById(id: string): Promise<User | null>;
|
||||
}
|
||||
|
||||
// module:
|
||||
{
|
||||
provide: USER_REPOSITORY,
|
||||
useClass: TypeOrmUserRepository,
|
||||
}
|
||||
|
||||
// service:
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(@Inject(USER_REPOSITORY) private readonly repo: UserRepository) {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 模块组织
|
||||
|
||||
### 推荐四层结构
|
||||
|
||||
```
|
||||
src/
|
||||
common/ ← 全局技术基础设施(Guards、Filters、Interceptors、Decorators)
|
||||
core/ ← 内部基础设施(Config、Database、Queue 配置)
|
||||
integrations/ ← 外部服务封装(Mailer、Storage、Stripe、SMS)
|
||||
modules/ ← 按领域组织的业务逻辑
|
||||
[feature]/
|
||||
dtos/
|
||||
repositories/
|
||||
services/
|
||||
internal/ ← 模块内共享 Service
|
||||
use-cases/ ← 一个文件 = 一个操作
|
||||
types/
|
||||
[feature].controller.ts
|
||||
[feature].module.ts
|
||||
```
|
||||
|
||||
### Domain 必须框架无关
|
||||
|
||||
```typescript
|
||||
// ❌ Domain Entity 依赖 NestJS——不可独立测试
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class User {
|
||||
constructor(private readonly email: string) {}
|
||||
}
|
||||
|
||||
// ✅ Domain 是纯类,无框架装饰器
|
||||
export class User {
|
||||
private constructor(private readonly email: string) {}
|
||||
|
||||
static create(email: string): User {
|
||||
return new User(email);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 关键规则
|
||||
|
||||
- `common/` 必须 **不涉及业务**——如果需要知道"订单",它不属于这里
|
||||
- `integrations/` 封装每个外部服务;换 SendGrid → AWS SES 只改一个目录
|
||||
- 使用 **Use-Case Service**(一个文件一个操作)而非 15 个方法的巨型 `XxxService`
|
||||
|
||||
---
|
||||
|
||||
## Guard / Interceptor / Pipe
|
||||
|
||||
### 业务逻辑不应放在 Guard 中
|
||||
|
||||
```typescript
|
||||
// ❌ Guard 中查询数据库 + 业务判断
|
||||
@Injectable()
|
||||
export class OrderOwnershipGuard implements CanActivate {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const order = await this.prisma.order.findUnique({
|
||||
where: { id: req.params.id },
|
||||
});
|
||||
if (order.userId !== req.user.id) {
|
||||
return false; // 数据获取 + 业务规则判断都在 Guard 里
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Guard 只做授权检查(角色/权限)
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(private readonly reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (!requiredRoles) return true;
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
return requiredRoles.some((role) => user.roles?.includes(role));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Interceptor 只用于横切关注点
|
||||
|
||||
```typescript
|
||||
// ❌ Interceptor 中执行业务逻辑
|
||||
@Injectable()
|
||||
export class PricingInterceptor implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, next: CallHandler) {
|
||||
// 计算折扣——这不是横切关注点!
|
||||
return next.handle().pipe(map(data => applyDiscount(data)));
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Interceptor 用于日志、缓存、响应转换、计时
|
||||
@Injectable()
|
||||
export class LoggingInterceptor implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, next: CallHandler) {
|
||||
const now = Date.now();
|
||||
const req = context.switchToHttp().getRequest();
|
||||
return next.handle().pipe(
|
||||
tap(() => console.log(`${req.method} ${req.url} - ${Date.now() - now}ms`)),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 全局 ValidationPipe 必须配置 whitelist
|
||||
|
||||
```typescript
|
||||
// ❌ 没有 whitelist——请求体中的额外属性直接传入
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
await app.listen(3000);
|
||||
}
|
||||
|
||||
// ✅ 全局 ValidationPipe + whitelist 过滤未知属性
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
}),
|
||||
);
|
||||
await app.listen(3000);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验证模式 (DTO)
|
||||
|
||||
### @ValidateNested() 必须搭配 @Type()
|
||||
|
||||
```typescript
|
||||
// ❌ 只有 @ValidateNested——嵌套对象验证被静默跳过!
|
||||
export class CreateOrderDto {
|
||||
@ValidateNested()
|
||||
shipping: AddressDto;
|
||||
}
|
||||
|
||||
// ✅ @ValidateNested + @Type 配对使用
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateOrderDto {
|
||||
@ValidateNested()
|
||||
@Type(() => AddressDto)
|
||||
shipping: AddressDto;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => OrderItemDto)
|
||||
items: OrderItemDto[];
|
||||
}
|
||||
```
|
||||
|
||||
### 禁止裸 any Body
|
||||
|
||||
```typescript
|
||||
// ❌ 没有 DTO——无验证、无类型安全、无 Swagger 文档
|
||||
@Post()
|
||||
create(@Body() body: any) {
|
||||
return this.service.create(body);
|
||||
}
|
||||
|
||||
// ✅ 为每个操作创建 DTO
|
||||
export class CreateUserDto {
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(100)
|
||||
name: string;
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreateUserDto) {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
```
|
||||
|
||||
### Create 和 Update 应使用不同 DTO
|
||||
|
||||
```typescript
|
||||
// ❌ PATCH 也要求所有字段——不合理的 API 设计
|
||||
@Patch(':id')
|
||||
update(@Body() dto: CreateUserDto) { /* all fields required */ }
|
||||
|
||||
// ✅ Update 使用 PartialType
|
||||
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Body() dto: UpdateUserDto) { /* all fields optional */ }
|
||||
```
|
||||
|
||||
### 可选嵌套对象
|
||||
|
||||
```typescript
|
||||
// ❌ 可选嵌套对象缺少 @IsOptional
|
||||
export class UpdateOrderDto {
|
||||
@ValidateNested()
|
||||
@Type(() => AddressDto)
|
||||
shipping?: AddressDto; // undefined 时仍尝试验证
|
||||
}
|
||||
|
||||
// ✅ @IsOptional + @ValidateNested + @Type
|
||||
export class UpdateOrderDto {
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => AddressDto)
|
||||
shipping?: AddressDto;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 禁止吞掉错误
|
||||
|
||||
```typescript
|
||||
// ❌ catch { return null }——隐藏了问题,调用者无法区分"不存在"和"出错了"
|
||||
async findOne(id: string) {
|
||||
try {
|
||||
return await this.repo.findById(id);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 抛出有意义的异常
|
||||
async findOne(id: string): Promise<User> {
|
||||
const user = await this.repo.findById(id);
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User ${id} not found`);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
```
|
||||
|
||||
### 使用内置异常类
|
||||
|
||||
```typescript
|
||||
// ❌ 手动构造 HTTP 响应
|
||||
throw new HttpException('Bad request', 400);
|
||||
|
||||
// ✅ 使用语义化的内置异常
|
||||
throw new BadRequestException('Invalid email format');
|
||||
throw new NotFoundException('User not found');
|
||||
throw new ConflictException('Email already taken');
|
||||
throw new ForbiddenException('Insufficient permissions');
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
```
|
||||
|
||||
### 自定义异常过滤器
|
||||
|
||||
```typescript
|
||||
// ✅ 全局异常过滤器——统一响应格式
|
||||
@Catch()
|
||||
export class AllExceptionsFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(AllExceptionsFilter.name);
|
||||
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse();
|
||||
const request = ctx.getRequest();
|
||||
|
||||
const status =
|
||||
exception instanceof HttpException
|
||||
? exception.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
this.logger.error(`${request.method} ${request.url} - ${status}`, exception instanceof Error ? exception.stack : '');
|
||||
|
||||
response.status(status).json({
|
||||
statusCode: status,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 循环依赖
|
||||
|
||||
### 模块间循环引用
|
||||
|
||||
```typescript
|
||||
// ❌ Module A ↔ Module B
|
||||
@Module({ imports: [UsersModule] })
|
||||
export class OrdersModule {}
|
||||
|
||||
@Module({ imports: [OrdersModule] })
|
||||
export class UsersModule {}
|
||||
|
||||
// ✅ 提取共享逻辑到第三个模块
|
||||
@Module({
|
||||
providers: [SharedService],
|
||||
exports: [SharedService],
|
||||
})
|
||||
export class SharedModule {}
|
||||
|
||||
@Module({ imports: [SharedModule] })
|
||||
export class OrdersModule {}
|
||||
|
||||
@Module({ imports: [SharedModule] })
|
||||
export class UsersModule {}
|
||||
```
|
||||
|
||||
### forwardRef 是最后手段
|
||||
|
||||
```typescript
|
||||
// ⚠️ forwardRef 表示设计有问题——优先重新设计
|
||||
@Module({
|
||||
imports: [forwardRef(() => UsersModule)],
|
||||
})
|
||||
export class OrdersModule {}
|
||||
|
||||
// ✅ 重新设计消除循环:
|
||||
// 1. 提取共享模块
|
||||
// 2. 使用事件驱动(EventEmitter)代替直接调用
|
||||
// 3. 将共享逻辑提升到上层 Service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试模式
|
||||
|
||||
### Use-Case 可脱离 NestJS 测试
|
||||
|
||||
```typescript
|
||||
// ✅ 无需 NestFactory——直接 new
|
||||
describe('CreateUserHandler', () => {
|
||||
let handler: CreateUserHandler;
|
||||
let repo: InMemoryUserRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
repo = new InMemoryUserRepository();
|
||||
handler = new CreateUserHandler(repo);
|
||||
});
|
||||
|
||||
it('creates a user', async () => {
|
||||
const id = await handler.execute(
|
||||
new CreateUserCommand('user@example.com', 'Alice'),
|
||||
);
|
||||
expect(id).toBeDefined();
|
||||
});
|
||||
|
||||
it('rejects duplicate email', async () => {
|
||||
await handler.execute(new CreateUserCommand('user@example.com', 'Alice'));
|
||||
await expect(
|
||||
handler.execute(new CreateUserCommand('user@example.com', 'Bob')),
|
||||
).rejects.toThrow('already exists');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### E2E 测试应配置与生产一致的 Pipes
|
||||
|
||||
```typescript
|
||||
describe('UsersController (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleFixture = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
// 必须与 main.ts 中相同的全局配置
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
}),
|
||||
);
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/POST users - valid', () => {
|
||||
return request(app.getHttpServer())
|
||||
.post('/users')
|
||||
.send({ email: 'test@test.com', name: 'Test' })
|
||||
.expect(201);
|
||||
});
|
||||
|
||||
it('/POST users - extra fields rejected', () => {
|
||||
return request(app.getHttpServer())
|
||||
.post('/users')
|
||||
.send({ email: 'test@test.com', name: 'Test', role: 'admin' })
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### 分层架构
|
||||
|
||||
- [ ] ORM/Prisma 未直接注入 Controller
|
||||
- [ ] 业务逻辑不在 Controller 中
|
||||
- [ ] Repository 之间无互相注入
|
||||
- [ ] Service 依赖数 ≤ 8(超出则拆分为 Use-Case)
|
||||
|
||||
### 依赖注入
|
||||
|
||||
- [ ] 接口 + Symbol Token 用于可替换的依赖
|
||||
- [ ] 无 `forwardRef()`(如有,需设计文档说明原因)
|
||||
- [ ] Scoped 服务未注入到 Singleton 中
|
||||
|
||||
### 验证
|
||||
|
||||
- [ ] 每个 `@ValidateNested()` 都有对应的 `@Type()`
|
||||
- [ ] 全局 `ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })` 已配置
|
||||
- [ ] 无 `@Body() body: any`——必须使用 DTO
|
||||
- [ ] Create 和 Update 使用不同 DTO(`PartialType`)
|
||||
- [ ] 数组验证使用 `{ each: true }`
|
||||
- [ ] 可选嵌套对象使用 `@IsOptional()` + `@ValidateNested()` + `@Type()`
|
||||
|
||||
### Guard / Interceptor / Pipe
|
||||
|
||||
- [ ] Guard 只做授权检查,不查询数据库
|
||||
- [ ] Interceptor 只用于横切关注点(日志、缓存、响应转换)
|
||||
- [ ] 业务规则在 Service 中
|
||||
|
||||
### 错误处理
|
||||
|
||||
- [ ] 无 `catch { return null }`——抛出有意义的异常
|
||||
- [ ] 使用 NestJS 内置异常类
|
||||
- [ ] 自定义异常过滤器在 `common/filters/` 中
|
||||
|
||||
### 模块
|
||||
|
||||
- [ ] 无循环模块引用
|
||||
- [ ] Domain Entity 无框架装饰器(`@Injectable` 等)
|
||||
- [ ] 外部服务调用在 `integrations/` 中
|
||||
|
||||
### 测试
|
||||
|
||||
- [ ] Use-Case Service 可脱离 NestJS 测试
|
||||
- [ ] E2E 测试配置与生产一致的全局 Pipes/Guards
|
||||
- [ ] Domain Entity 零框架依赖
|
||||
@@ -0,0 +1,816 @@
|
||||
# Performance Review Guide
|
||||
|
||||
性能审查指南,覆盖前端、后端、数据库、算法复杂度和 API 性能。
|
||||
|
||||
## 目录
|
||||
|
||||
- [前端性能 (Core Web Vitals)](#前端性能-core-web-vitals)
|
||||
- [JavaScript 性能](#javascript-性能)
|
||||
- [内存管理](#内存管理)
|
||||
- [数据库性能](#数据库性能)
|
||||
- [API 性能](#api-性能)
|
||||
- [算法复杂度](#算法复杂度)
|
||||
- [性能审查清单](#性能审查清单)
|
||||
|
||||
---
|
||||
|
||||
## 前端性能 (Core Web Vitals)
|
||||
|
||||
### 2024 核心指标
|
||||
|
||||
| 指标 | 全称 | 目标值 | 含义 |
|
||||
|------|------|--------|------|
|
||||
| **LCP** | Largest Contentful Paint | ≤ 2.5s | 最大内容绘制时间 |
|
||||
| **INP** | Interaction to Next Paint | ≤ 200ms | 交互响应时间(2024 年替代 FID)|
|
||||
| **CLS** | Cumulative Layout Shift | ≤ 0.1 | 累积布局偏移 |
|
||||
| **FCP** | First Contentful Paint | ≤ 1.8s | 首次内容绘制 |
|
||||
| **TBT** | Total Blocking Time | ≤ 200ms | 主线程阻塞时间 |
|
||||
|
||||
### LCP 优化检查
|
||||
|
||||
```javascript
|
||||
// ❌ LCP 图片懒加载 - 延迟关键内容
|
||||
<img src="hero.jpg" loading="lazy" />
|
||||
|
||||
// ✅ LCP 图片立即加载
|
||||
<img src="hero.jpg" fetchpriority="high" />
|
||||
|
||||
// ❌ 未优化的图片格式
|
||||
<img src="hero.png" /> // PNG 文件过大
|
||||
|
||||
// ✅ 现代图片格式 + 响应式
|
||||
<picture>
|
||||
<source srcset="hero.avif" type="image/avif" />
|
||||
<source srcset="hero.webp" type="image/webp" />
|
||||
<img src="hero.jpg" alt="Hero" />
|
||||
</picture>
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- [ ] LCP 元素是否设置 `fetchpriority="high"`?
|
||||
- [ ] 是否使用 WebP/AVIF 格式?
|
||||
- [ ] 是否有服务端渲染或静态生成?
|
||||
- [ ] CDN 是否配置正确?
|
||||
|
||||
### FCP 优化检查
|
||||
|
||||
```html
|
||||
<!-- ❌ 阻塞渲染的 CSS -->
|
||||
<link rel="stylesheet" href="all-styles.css" />
|
||||
|
||||
<!-- ✅ 关键 CSS 内联 + 异步加载其余 -->
|
||||
<style>/* 首屏关键样式 */</style>
|
||||
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'" />
|
||||
|
||||
<!-- ❌ 阻塞渲染的字体 -->
|
||||
@font-face {
|
||||
font-family: 'CustomFont';
|
||||
src: url('font.woff2');
|
||||
}
|
||||
|
||||
<!-- ✅ 字体显示优化 -->
|
||||
@font-face {
|
||||
font-family: 'CustomFont';
|
||||
src: url('font.woff2');
|
||||
font-display: swap; /* 先用系统字体,加载后切换 */
|
||||
}
|
||||
```
|
||||
|
||||
### INP 优化检查
|
||||
|
||||
```javascript
|
||||
// ❌ 长任务阻塞主线程
|
||||
button.addEventListener('click', () => {
|
||||
// 耗时 500ms 的同步操作
|
||||
processLargeData(data);
|
||||
updateUI();
|
||||
});
|
||||
|
||||
// ✅ 拆分长任务
|
||||
button.addEventListener('click', async () => {
|
||||
// 让出主线程
|
||||
await scheduler.yield?.() ?? new Promise(r => setTimeout(r, 0));
|
||||
|
||||
// 分批处理
|
||||
for (const chunk of chunks) {
|
||||
processChunk(chunk);
|
||||
await scheduler.yield?.();
|
||||
}
|
||||
updateUI();
|
||||
});
|
||||
|
||||
// ✅ 使用 Web Worker 处理复杂计算
|
||||
const worker = new Worker('heavy-computation.js');
|
||||
worker.postMessage(data);
|
||||
worker.onmessage = (e) => updateUI(e.data);
|
||||
```
|
||||
|
||||
### CLS 优化检查
|
||||
|
||||
```css
|
||||
/* ❌ 未指定尺寸的媒体 */
|
||||
img { width: 100%; }
|
||||
|
||||
/* ✅ 预留空间 */
|
||||
img {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
/* ❌ 动态插入内容导致布局偏移 */
|
||||
.ad-container { }
|
||||
|
||||
/* ✅ 预留固定高度 */
|
||||
.ad-container {
|
||||
min-height: 250px;
|
||||
}
|
||||
```
|
||||
|
||||
**CLS 审查清单:**
|
||||
- [ ] 图片/视频是否有 width/height 或 aspect-ratio?
|
||||
- [ ] 字体加载是否使用 `font-display: swap`?
|
||||
- [ ] 动态内容是否预留空间?
|
||||
- [ ] 是否避免在现有内容上方插入内容?
|
||||
|
||||
---
|
||||
|
||||
## JavaScript 性能
|
||||
|
||||
### 代码分割与懒加载
|
||||
|
||||
```javascript
|
||||
// ❌ 一次性加载所有代码
|
||||
import { HeavyChart } from './charts';
|
||||
import { PDFExporter } from './pdf';
|
||||
import { AdminPanel } from './admin';
|
||||
|
||||
// ✅ 按需加载
|
||||
const HeavyChart = lazy(() => import('./charts'));
|
||||
const PDFExporter = lazy(() => import('./pdf'));
|
||||
|
||||
// ✅ 路由级代码分割
|
||||
const routes = [
|
||||
{
|
||||
path: '/dashboard',
|
||||
component: lazy(() => import('./pages/Dashboard')),
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
component: lazy(() => import('./pages/Admin')),
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### Bundle 体积优化
|
||||
|
||||
```javascript
|
||||
// ❌ 导入整个库
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
// ✅ 按需导入
|
||||
import debounce from 'lodash/debounce';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
// ❌ 未使用 Tree Shaking
|
||||
export default {
|
||||
fn1() {},
|
||||
fn2() {}, // 未使用但被打包
|
||||
};
|
||||
|
||||
// ✅ 命名导出支持 Tree Shaking
|
||||
export function fn1() {}
|
||||
export function fn2() {}
|
||||
```
|
||||
|
||||
**Bundle 审查清单:**
|
||||
- [ ] 是否使用动态 import() 进行代码分割?
|
||||
- [ ] 大型库是否按需导入?
|
||||
- [ ] 是否分析过 bundle 大小?(webpack-bundle-analyzer)
|
||||
- [ ] 是否有未使用的依赖?
|
||||
|
||||
### 列表渲染优化
|
||||
|
||||
```javascript
|
||||
// ❌ 渲染大列表
|
||||
function List({ items }) {
|
||||
return (
|
||||
<ul>
|
||||
{items.map(item => <li key={item.id}>{item.name}</li>)}
|
||||
</ul>
|
||||
); // 10000 条数据 = 10000 个 DOM 节点
|
||||
}
|
||||
|
||||
// ✅ 虚拟列表 - 只渲染可见项
|
||||
import { FixedSizeList } from 'react-window';
|
||||
|
||||
function VirtualList({ items }) {
|
||||
return (
|
||||
<FixedSizeList
|
||||
height={400}
|
||||
itemCount={items.length}
|
||||
itemSize={35}
|
||||
>
|
||||
{({ index, style }) => (
|
||||
<div style={style}>{items[index].name}</div>
|
||||
)}
|
||||
</FixedSizeList>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**大数据审查要点:**
|
||||
- [ ] 列表超过 100 项是否使用虚拟滚动?
|
||||
- [ ] 表格是否支持分页或虚拟化?
|
||||
- [ ] 是否有不必要的全量渲染?
|
||||
|
||||
---
|
||||
|
||||
## 内存管理
|
||||
|
||||
### 常见内存泄漏
|
||||
|
||||
#### 1. 未清理的事件监听
|
||||
|
||||
```javascript
|
||||
// ❌ 组件卸载后事件仍在监听
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
// ✅ 清理事件监听
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
```
|
||||
|
||||
#### 2. 未清理的定时器
|
||||
|
||||
```javascript
|
||||
// ❌ 定时器未清理
|
||||
useEffect(() => {
|
||||
setInterval(fetchData, 5000);
|
||||
}, []);
|
||||
|
||||
// ✅ 清理定时器
|
||||
useEffect(() => {
|
||||
const timer = setInterval(fetchData, 5000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
```
|
||||
|
||||
#### 3. 闭包引用
|
||||
|
||||
```javascript
|
||||
// ❌ 闭包持有大对象引用
|
||||
function createHandler() {
|
||||
const largeData = new Array(1000000).fill('x');
|
||||
|
||||
return function handler() {
|
||||
// largeData 被闭包引用,无法被回收
|
||||
console.log(largeData.length);
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ 只保留必要数据
|
||||
function createHandler() {
|
||||
const largeData = new Array(1000000).fill('x');
|
||||
const length = largeData.length; // 只保留需要的值
|
||||
|
||||
return function handler() {
|
||||
console.log(length);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. 未清理的订阅
|
||||
|
||||
```javascript
|
||||
// ❌ WebSocket/EventSource 未关闭
|
||||
useEffect(() => {
|
||||
const ws = new WebSocket('wss://...');
|
||||
ws.onmessage = handleMessage;
|
||||
}, []);
|
||||
|
||||
// ✅ 清理连接
|
||||
useEffect(() => {
|
||||
const ws = new WebSocket('wss://...');
|
||||
ws.onmessage = handleMessage;
|
||||
return () => ws.close();
|
||||
}, []);
|
||||
```
|
||||
|
||||
### 内存审查清单
|
||||
|
||||
```markdown
|
||||
- [ ] useEffect 是否都有清理函数?
|
||||
- [ ] 事件监听是否在组件卸载时移除?
|
||||
- [ ] 定时器是否被清理?
|
||||
- [ ] WebSocket/SSE 连接是否关闭?
|
||||
- [ ] 大对象是否及时释放?
|
||||
- [ ] 是否有全局变量累积数据?
|
||||
```
|
||||
|
||||
### 检测工具
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| Chrome DevTools Memory | 堆快照分析 |
|
||||
| MemLab (Meta) | 自动化内存泄漏检测 |
|
||||
| Performance Monitor | 实时内存监控 |
|
||||
|
||||
---
|
||||
|
||||
## 数据库性能
|
||||
|
||||
### N+1 查询问题
|
||||
|
||||
```python
|
||||
# ❌ N+1 问题 - 1 + N 次查询
|
||||
users = User.objects.all() # 1 次查询
|
||||
for user in users:
|
||||
print(user.profile.bio) # N 次查询(每个用户一次)
|
||||
|
||||
# ✅ Eager Loading - 2 次查询
|
||||
users = User.objects.select_related('profile').all()
|
||||
for user in users:
|
||||
print(user.profile.bio) # 无额外查询
|
||||
|
||||
# ✅ 多对多关系用 prefetch_related
|
||||
posts = Post.objects.prefetch_related('tags').all()
|
||||
```
|
||||
|
||||
```javascript
|
||||
// TypeORM 示例
|
||||
// ❌ N+1 问题
|
||||
const users = await userRepository.find();
|
||||
for (const user of users) {
|
||||
const posts = await user.posts; // 每次循环都查询
|
||||
}
|
||||
|
||||
// ✅ Eager Loading
|
||||
const users = await userRepository.find({
|
||||
relations: ['posts'],
|
||||
});
|
||||
```
|
||||
|
||||
### 索引优化
|
||||
|
||||
```sql
|
||||
-- ❌ 全表扫描
|
||||
SELECT * FROM orders WHERE status = 'pending';
|
||||
|
||||
-- ✅ 添加索引
|
||||
CREATE INDEX idx_orders_status ON orders(status);
|
||||
|
||||
-- ❌ 索引失效:函数操作
|
||||
SELECT * FROM users WHERE YEAR(created_at) = 2024;
|
||||
|
||||
-- ✅ 范围查询可用索引
|
||||
SELECT * FROM users
|
||||
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
|
||||
|
||||
-- ❌ 索引失效:LIKE 前缀通配符
|
||||
SELECT * FROM products WHERE name LIKE '%phone%';
|
||||
|
||||
-- ✅ 前缀匹配可用索引
|
||||
SELECT * FROM products WHERE name LIKE 'phone%';
|
||||
```
|
||||
|
||||
### 查询优化
|
||||
|
||||
```sql
|
||||
-- ❌ SELECT * 获取不需要的列
|
||||
SELECT * FROM users WHERE id = 1;
|
||||
|
||||
-- ✅ 只查询需要的列
|
||||
SELECT id, name, email FROM users WHERE id = 1;
|
||||
|
||||
-- ❌ 大表无 LIMIT
|
||||
SELECT * FROM logs WHERE type = 'error';
|
||||
|
||||
-- ✅ 分页查询
|
||||
SELECT * FROM logs WHERE type = 'error' LIMIT 100 OFFSET 0;
|
||||
|
||||
-- ❌ 在循环中执行查询
|
||||
for id in user_ids:
|
||||
cursor.execute("SELECT * FROM users WHERE id = %s", (id,))
|
||||
|
||||
-- ✅ 批量查询
|
||||
cursor.execute("SELECT * FROM users WHERE id IN %s", (tuple(user_ids),))
|
||||
```
|
||||
|
||||
### 数据库审查清单
|
||||
|
||||
```markdown
|
||||
🔴 必须检查:
|
||||
- [ ] 是否存在 N+1 查询?
|
||||
- [ ] WHERE 子句列是否有索引?
|
||||
- [ ] 是否避免了 SELECT *?
|
||||
- [ ] 大表查询是否有 LIMIT?
|
||||
|
||||
🟡 建议检查:
|
||||
- [ ] 是否使用了 EXPLAIN 分析查询计划?
|
||||
- [ ] 复合索引列顺序是否正确?
|
||||
- [ ] 是否有未使用的索引?
|
||||
- [ ] 是否有慢查询日志监控?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 性能
|
||||
|
||||
### 分页实现
|
||||
|
||||
```javascript
|
||||
// ❌ 返回全部数据
|
||||
app.get('/users', async (req, res) => {
|
||||
const users = await User.findAll(); // 可能返回 100000 条
|
||||
res.json(users);
|
||||
});
|
||||
|
||||
// ✅ 分页 + 限制最大数量
|
||||
app.get('/users', async (req, res) => {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = Math.min(parseInt(req.query.limit) || 20, 100); // 最大 100
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { rows, count } = await User.findAndCountAll({
|
||||
limit,
|
||||
offset,
|
||||
order: [['id', 'ASC']],
|
||||
});
|
||||
|
||||
res.json({
|
||||
data: rows,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: count,
|
||||
totalPages: Math.ceil(count / limit),
|
||||
},
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 缓存策略
|
||||
|
||||
```javascript
|
||||
// ✅ Redis 缓存示例
|
||||
async function getUser(id) {
|
||||
const cacheKey = `user:${id}`;
|
||||
|
||||
// 1. 检查缓存
|
||||
const cached = await redis.get(cacheKey);
|
||||
if (cached) {
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
|
||||
// 2. 查询数据库
|
||||
const user = await db.users.findById(id);
|
||||
|
||||
// 3. 写入缓存(设置过期时间)
|
||||
await redis.setex(cacheKey, 3600, JSON.stringify(user));
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
// ✅ HTTP 缓存头
|
||||
app.get('/static-data', (req, res) => {
|
||||
res.set({
|
||||
'Cache-Control': 'public, max-age=86400', // 24 小时
|
||||
'ETag': 'abc123',
|
||||
});
|
||||
res.json(data);
|
||||
});
|
||||
```
|
||||
|
||||
### 响应压缩
|
||||
|
||||
```javascript
|
||||
// ✅ 启用 Gzip/Brotli 压缩
|
||||
const compression = require('compression');
|
||||
app.use(compression());
|
||||
|
||||
// ✅ 只返回必要字段
|
||||
// 请求: GET /users?fields=id,name,email
|
||||
app.get('/users', async (req, res) => {
|
||||
const fields = req.query.fields?.split(',') || ['id', 'name'];
|
||||
const users = await User.findAll({
|
||||
attributes: fields,
|
||||
});
|
||||
res.json(users);
|
||||
});
|
||||
```
|
||||
|
||||
### 限流保护
|
||||
|
||||
```javascript
|
||||
// ✅ 速率限制
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 60 * 1000, // 1 分钟
|
||||
max: 100, // 最多 100 次请求
|
||||
message: { error: 'Too many requests, please try again later.' },
|
||||
});
|
||||
|
||||
app.use('/api/', limiter);
|
||||
```
|
||||
|
||||
### API 审查清单
|
||||
|
||||
```markdown
|
||||
- [ ] 列表接口是否有分页?
|
||||
- [ ] 是否限制了每页最大数量?
|
||||
- [ ] 热点数据是否有缓存?
|
||||
- [ ] 是否启用了响应压缩?
|
||||
- [ ] 是否有速率限制?
|
||||
- [ ] 是否只返回必要字段?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 算法复杂度
|
||||
|
||||
### 常见复杂度对比
|
||||
|
||||
| 复杂度 | 名称 | 10 条 | 1000 条 | 100 万条 | 示例 |
|
||||
|--------|------|-------|---------|----------|------|
|
||||
| O(1) | 常数 | 1 | 1 | 1 | 哈希查找 |
|
||||
| O(log n) | 对数 | 3 | 10 | 20 | 二分查找 |
|
||||
| O(n) | 线性 | 10 | 1000 | 100 万 | 遍历数组 |
|
||||
| O(n log n) | 线性对数 | 33 | 10000 | 2000 万 | 快速排序 |
|
||||
| O(n²) | 平方 | 100 | 100 万 | 1 万亿 | 嵌套循环 |
|
||||
| O(2ⁿ) | 指数 | 1024 | ∞ | ∞ | 递归斐波那契 |
|
||||
|
||||
### 代码审查中的识别
|
||||
|
||||
```javascript
|
||||
// ❌ O(n²) - 嵌套循环
|
||||
function findDuplicates(arr) {
|
||||
const duplicates = [];
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
for (let j = i + 1; j < arr.length; j++) {
|
||||
if (arr[i] === arr[j]) {
|
||||
duplicates.push(arr[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
// ✅ O(n) - 使用 Set
|
||||
function findDuplicates(arr) {
|
||||
const seen = new Set();
|
||||
const duplicates = new Set();
|
||||
for (const item of arr) {
|
||||
if (seen.has(item)) {
|
||||
duplicates.add(item);
|
||||
}
|
||||
seen.add(item);
|
||||
}
|
||||
return [...duplicates];
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// ❌ O(n²) - 每次循环都调用 includes
|
||||
function removeDuplicates(arr) {
|
||||
const result = [];
|
||||
for (const item of arr) {
|
||||
if (!result.includes(item)) { // includes 是 O(n)
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ✅ O(n) - 使用 Set
|
||||
function removeDuplicates(arr) {
|
||||
return [...new Set(arr)];
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// ❌ O(n) 查找 - 每次都遍历
|
||||
const users = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }, ...];
|
||||
|
||||
function getUser(id) {
|
||||
return users.find(u => u.id === id); // O(n)
|
||||
}
|
||||
|
||||
// ✅ O(1) 查找 - 使用 Map
|
||||
const userMap = new Map(users.map(u => [u.id, u]));
|
||||
|
||||
function getUser(id) {
|
||||
return userMap.get(id); // O(1)
|
||||
}
|
||||
```
|
||||
|
||||
### 空间复杂度考虑
|
||||
|
||||
```javascript
|
||||
// ⚠️ O(n) 空间 - 创建新数组
|
||||
const doubled = arr.map(x => x * 2);
|
||||
|
||||
// ✅ O(1) 空间 - 原地修改(如果允许)
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
arr[i] *= 2;
|
||||
}
|
||||
|
||||
// ⚠️ 递归深度过大可能栈溢出
|
||||
function factorial(n) {
|
||||
if (n <= 1) return 1;
|
||||
return n * factorial(n - 1); // O(n) 栈空间
|
||||
}
|
||||
|
||||
// ✅ 迭代版本 O(1) 空间
|
||||
function factorial(n) {
|
||||
let result = 1;
|
||||
for (let i = 2; i <= n; i++) {
|
||||
result *= i;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### 复杂度审查问题
|
||||
|
||||
```markdown
|
||||
💡 "这个嵌套循环的复杂度是 O(n²),数据量大时会有性能问题"
|
||||
🔴 "这里用 Array.includes() 在循环中,整体是 O(n²),建议用 Set"
|
||||
🟡 "这个递归深度可能导致栈溢出,建议改为迭代或尾递归"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能审查清单
|
||||
|
||||
### 🔴 必须检查(阻塞级)
|
||||
|
||||
**前端:**
|
||||
- [ ] LCP 图片是否懒加载?(不应该)
|
||||
- [ ] 是否有 `transition: all`?
|
||||
- [ ] 是否动画 width/height/top/left?
|
||||
- [ ] 列表 >100 项是否虚拟化?
|
||||
|
||||
**后端:**
|
||||
- [ ] 是否存在 N+1 查询?
|
||||
- [ ] 列表接口是否有分页?
|
||||
- [ ] 是否有 SELECT * 查大表?
|
||||
|
||||
**通用:**
|
||||
- [ ] 是否有 O(n²) 或更差的嵌套循环?
|
||||
- [ ] useEffect/事件监听是否有清理?
|
||||
|
||||
### 🟡 建议检查(重要级)
|
||||
|
||||
**前端:**
|
||||
- [ ] 是否使用代码分割?
|
||||
- [ ] 大型库是否按需导入?
|
||||
- [ ] 图片是否使用 WebP/AVIF?
|
||||
- [ ] 是否有未使用的依赖?
|
||||
|
||||
**后端:**
|
||||
- [ ] 热点数据是否有缓存?
|
||||
- [ ] WHERE 列是否有索引?
|
||||
- [ ] 是否有慢查询监控?
|
||||
|
||||
**API:**
|
||||
- [ ] 是否启用响应压缩?
|
||||
- [ ] 是否有速率限制?
|
||||
- [ ] 是否只返回必要字段?
|
||||
|
||||
### 🟢 优化建议(建议级)
|
||||
|
||||
- [ ] 是否分析过 bundle 大小?
|
||||
- [ ] 是否使用 CDN?
|
||||
- [ ] 是否有性能监控?
|
||||
- [ ] 是否做过性能基准测试?
|
||||
|
||||
---
|
||||
|
||||
## 性能度量阈值
|
||||
|
||||
### 前端指标
|
||||
|
||||
| 指标 | 好 | 需改进 | 差 |
|
||||
|------|-----|--------|-----|
|
||||
| LCP | ≤ 2.5s | 2.5-4s | > 4s |
|
||||
| INP | ≤ 200ms | 200-500ms | > 500ms |
|
||||
| CLS | ≤ 0.1 | 0.1-0.25 | > 0.25 |
|
||||
| FCP | ≤ 1.8s | 1.8-3s | > 3s |
|
||||
| Bundle Size (JS) | < 200KB | 200-500KB | > 500KB |
|
||||
|
||||
### 后端指标
|
||||
|
||||
| 指标 | 好 | 需改进 | 差 |
|
||||
|------|-----|--------|-----|
|
||||
| API 响应时间 | < 100ms | 100-500ms | > 500ms |
|
||||
| 数据库查询 | < 50ms | 50-200ms | > 200ms |
|
||||
| 页面加载 | < 3s | 3-5s | > 5s |
|
||||
|
||||
---
|
||||
|
||||
## 工具推荐
|
||||
|
||||
### 前端性能
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| [Lighthouse](https://developer.chrome.com/docs/lighthouse/) | Core Web Vitals 测试 |
|
||||
| [WebPageTest](https://www.webpagetest.org/) | 详细性能分析 |
|
||||
| [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) | Bundle 分析 |
|
||||
| [Chrome DevTools Performance](https://developer.chrome.com/docs/devtools/performance/) | 运行时性能分析 |
|
||||
|
||||
### 内存检测
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| [MemLab](https://github.com/facebookincubator/memlab) | 自动化内存泄漏检测 |
|
||||
| Chrome Memory Tab | 堆快照分析 |
|
||||
|
||||
### 后端性能
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| EXPLAIN | 数据库查询计划分析 |
|
||||
| [pganalyze](https://pganalyze.com/) | PostgreSQL 性能监控 |
|
||||
| [New Relic](https://newrelic.com/) / [Datadog](https://www.datadoghq.com/) | APM 监控 |
|
||||
|
||||
---
|
||||
|
||||
## 低级别效率反模式
|
||||
|
||||
代码层面的效率失误,独立于架构层面的性能问题。补充 [common-bugs-checklist.md](common-bugs-checklist.md) 中已涵盖的资源管理与并发缺陷。
|
||||
|
||||
### 不必要的重复工作
|
||||
|
||||
- [ ] 同一函数 / 查询是否在同一 request/render 中被重复调用?
|
||||
- [ ] 文件 / 配置是否在循环内重复读取(loop-invariant)?
|
||||
- [ ] 计算结果是否可以被缓存或向下游传递?
|
||||
|
||||
```typescript
|
||||
// ❌ loop-invariant 在循环内反复执行
|
||||
for (const path of paths) {
|
||||
const config = JSON.parse(fs.readFileSync("config.json", "utf-8"));
|
||||
processFile(path, config);
|
||||
}
|
||||
|
||||
// ✅ 提到循环外
|
||||
const config = JSON.parse(fs.readFileSync("config.json", "utf-8"));
|
||||
for (const path of paths) processFile(path, config);
|
||||
```
|
||||
|
||||
### 错失的并发机会
|
||||
|
||||
- [ ] 独立的 async 操作是否顺序 `await`?
|
||||
- [ ] 是否可以用 `Promise.all` / `asyncio.gather` / `tokio::join!` 并发?
|
||||
|
||||
```typescript
|
||||
// ❌ 顺序 await
|
||||
const a = await fetchA();
|
||||
const b = await fetchB();
|
||||
|
||||
// ✅ 并发
|
||||
const [a, b] = await Promise.all([fetchA(), fetchB()]);
|
||||
```
|
||||
|
||||
### 热路径膨胀
|
||||
|
||||
- [ ] 模块级 / import 时代码是否执行重操作(文件 I/O、网络、大对象构造)?
|
||||
- [ ] per-request 路径是否有可延迟的初始化?
|
||||
- [ ] 启动时代码是否阻塞首次请求?
|
||||
|
||||
### 无界数据结构
|
||||
|
||||
> 资源生命周期相关缺陷(未关闭的连接、未移除的监听器、未清除的定时器)见 [common-bugs-checklist.md → Resource Management](common-bugs-checklist.md#resource-management)。本节聚焦 *容量边界*。
|
||||
|
||||
- [ ] 全局 dict / list / 缓存是否有 `max-size` 或 TTL?
|
||||
- [ ] 累积型数据结构(队列、日志、metrics buffer)是否有上限?
|
||||
- [ ] 每请求分配的对象是否会被持久引用而无法 GC?
|
||||
|
||||
```python
|
||||
# ❌ 无界缓存
|
||||
_cache: dict[str, Any] = {}
|
||||
|
||||
# ✅ 有界 LRU
|
||||
from functools import lru_cache
|
||||
|
||||
@lru_cache(maxsize=256)
|
||||
def get_cached(key: str) -> Any:
|
||||
return expensive_computation(key)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Core Web Vitals - web.dev](https://web.dev/articles/vitals)
|
||||
- [Optimizing Core Web Vitals - Vercel](https://vercel.com/guides/optimizing-core-web-vitals-in-2024)
|
||||
- [MemLab - Meta Engineering](https://engineering.fb.com/2022/09/12/open-source/memlab/)
|
||||
- [Big O Cheat Sheet](https://www.bigocheatsheet.com/)
|
||||
- [N+1 Query Problem - Stack Overflow](https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping)
|
||||
- [API Performance Optimization](https://algorithmsin60days.com/blog/optimizing-api-performance/)
|
||||
@@ -0,0 +1,704 @@
|
||||
# PHP Code Review Guide
|
||||
|
||||
> PHP 8.x code review guide covering the type system, modern language features, OOP modeling, PDO data access, security, error handling, Composer dependencies, performance, and testing.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Review Checklist](#quick-review-checklist)
|
||||
- [Type System & Modern PHP](#type-system--modern-php)
|
||||
- [Object Modeling](#object-modeling)
|
||||
- [Input, Output & Security](#input-output--security)
|
||||
- [Database Access](#database-access)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Composer & Dependencies](#composer--dependencies)
|
||||
- [Performance & Resource Management](#performance--resource-management)
|
||||
- [Testing & Static Analysis](#testing--static-analysis)
|
||||
- [Review Checklist](#review-checklist)
|
||||
- [References](#references)
|
||||
|
||||
---
|
||||
|
||||
## Quick Review Checklist
|
||||
|
||||
### Must-check
|
||||
|
||||
- [ ] New files enable `declare(strict_types=1);`
|
||||
- [ ] Public APIs have parameter, return, and property types
|
||||
- [ ] User input is validated; output is escaped per context
|
||||
- [ ] SQL uses parameterized queries or ORM binding
|
||||
- [ ] Passwords use `password_hash()` / `password_verify()`
|
||||
- [ ] File uploads validate MIME, size, extension, and storage path
|
||||
- [ ] `composer.lock` is committed; dependency ranges are reasonable
|
||||
- [ ] PHPUnit/Pest tests and PHPStan/Psalm static analysis are present
|
||||
|
||||
### Common issues
|
||||
|
||||
- [ ] Loose comparison `==` / `!=` causing type-juggling vulnerabilities
|
||||
- [ ] `md5()` / `sha1()` used to store passwords
|
||||
- [ ] Concatenating SQL, HTML, shell commands, or file paths
|
||||
- [ ] Using `@` to suppress errors
|
||||
- [ ] `unserialize()` on untrusted data
|
||||
- [ ] `$_GET` / `$_POST` / `$_FILES` flowing straight into business logic
|
||||
- [ ] PHP 8.2+ dynamic properties trigger a deprecation; PHP 9 may turn it into an error
|
||||
|
||||
---
|
||||
|
||||
## Type System & Modern PHP
|
||||
|
||||
### strict_types and explicit types
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ weak boundary: passing "42" gets silently coerced
|
||||
function findUser($id) {
|
||||
return User::find($id);
|
||||
}
|
||||
|
||||
// ✅ enable strict_types at the top of the file; type the public API
|
||||
declare(strict_types=1);
|
||||
|
||||
function findUser(int $id): ?User
|
||||
{
|
||||
return User::find($id);
|
||||
}
|
||||
```
|
||||
|
||||
Don't leave type checking entirely to runtime input validation. Type declarations express an internal contract; input validation expresses how much to trust the boundary. You need both.
|
||||
|
||||
### Avoid loose comparisons
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ strings like "0e12345" can be treated as 0 under loose comparison
|
||||
if ($providedHash == $storedHash) {
|
||||
grantAccess();
|
||||
}
|
||||
|
||||
// ✅ strict comparison; use hash_equals() for secrets or tokens
|
||||
if (hash_equals($storedHash, $providedHash)) {
|
||||
grantAccess();
|
||||
}
|
||||
|
||||
// ✅ match uses identity checks, so fewer type-juggling surprises than switch
|
||||
$status = match ($code) {
|
||||
200 => 'ok',
|
||||
404 => 'not_found',
|
||||
default => 'unknown',
|
||||
};
|
||||
```
|
||||
|
||||
Pay attention to `==`, `!=`, and `in_array($x, $list)` (loose by default) in auth, payment, state machine, and permission logic. Use `===`, `!==`, and `in_array($x, $list, true)` where it matters.
|
||||
|
||||
### Union / intersection / nullable types
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ mixed or untyped makes callers guess the return shape
|
||||
function loadConfig($source) {
|
||||
return parseConfig($source);
|
||||
}
|
||||
|
||||
// ✅ express the real contract with types
|
||||
function loadConfig(string|PathInfo $source): Config
|
||||
{
|
||||
return parseConfig($source);
|
||||
}
|
||||
|
||||
// ✅ make null explicit when it's a real business state
|
||||
function currentUser(): ?User
|
||||
{
|
||||
return Auth::user();
|
||||
}
|
||||
```
|
||||
|
||||
`mixed` can show up at the boundary or while migrating legacy code, but in core business services it usually signals missing modeling.
|
||||
|
||||
### The nullsafe operator shouldn't hide missing state
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ chained nullsafe blurs the reason for failure
|
||||
$country = $order?->customer?->profile?->country;
|
||||
|
||||
// ✅ branch explicitly on critical business state
|
||||
if ($order === null) {
|
||||
throw new OrderNotFound();
|
||||
}
|
||||
|
||||
$customer = $order->customer();
|
||||
if ($customer === null) {
|
||||
throw new MissingCustomer($order->id);
|
||||
}
|
||||
|
||||
$country = $customer->profile()?->country;
|
||||
```
|
||||
|
||||
Distinguish "optional display field" from "business invariant that must exist." The former is a good fit for `?->`; the latter should fail loudly.
|
||||
|
||||
---
|
||||
|
||||
## Object Modeling
|
||||
|
||||
### Use readonly properties and value objects
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ public mutable fields let callers change state at will
|
||||
class Money
|
||||
{
|
||||
public $amount;
|
||||
public $currency;
|
||||
}
|
||||
|
||||
// ✅ express an immutable value object with types and readonly
|
||||
final readonly class Money
|
||||
{
|
||||
public function __construct(
|
||||
public int $amount,
|
||||
public string $currency,
|
||||
) {
|
||||
if ($amount < 0) {
|
||||
throw new InvalidArgumentException('Amount must be non-negative');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For DTOs, config, and domain value objects, check first whether a `readonly class` or readonly properties can remove hidden side effects.
|
||||
|
||||
### Enums instead of string states
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ string states are easy to typo and can't enumerate the legal set
|
||||
if ($order->status === 'paied') {
|
||||
ship($order);
|
||||
}
|
||||
|
||||
// ✅ an enum surfaces illegal states earlier
|
||||
enum OrderStatus: string
|
||||
{
|
||||
case Pending = 'pending';
|
||||
case Paid = 'paid';
|
||||
case Cancelled = 'cancelled';
|
||||
}
|
||||
|
||||
if ($order->status === OrderStatus::Paid) {
|
||||
ship($order);
|
||||
}
|
||||
```
|
||||
|
||||
When reviewing state machines, permissions, or type fields, look for "magic string values." If the value set is stable, suggest an enum; if it comes from an external system, convert it to an internal enum before it enters the business layer.
|
||||
|
||||
### Don't rely on dynamic properties
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ PHP 8.2+ triggers a deprecation when creating a dynamic property
|
||||
$user = new User();
|
||||
$user->emali = 'a@example.com'; // a typo also silently creates a property
|
||||
|
||||
// ✅ declare properties or use a dedicated data structure
|
||||
final class User
|
||||
{
|
||||
public function __construct(
|
||||
public string $email,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
`#[AllowDynamicProperties]` should be an exception for legacy compatibility, not the default for new code. Watch for serialization, ORM hydration, and test doubles that secretly rely on dynamic properties.
|
||||
|
||||
### Don't do heavy I/O in constructors
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ quietly connecting to the DB on construction makes testing and error handling hard
|
||||
final class ReportService
|
||||
{
|
||||
private PDO $pdo;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->pdo = new PDO($_ENV['DSN']);
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ inject dependencies from the outside
|
||||
final class ReportService
|
||||
{
|
||||
public function __construct(
|
||||
private PDO $pdo,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
A constructor should establish the object's invariants — not send HTTP requests, open connections, read large files, or run complex queries.
|
||||
|
||||
---
|
||||
|
||||
## Input, Output & Security
|
||||
|
||||
### Validate input at the boundary
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ superglobals flow straight into business logic
|
||||
$user = $service->create($_POST['email'], $_POST['age']);
|
||||
|
||||
// ✅ validate and coerce types at the boundary first
|
||||
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
|
||||
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, [
|
||||
'options' => ['min_range' => 0, 'max_range' => 130],
|
||||
]);
|
||||
|
||||
if ($email === false || $email === null || $age === false || $age === null) {
|
||||
throw new InvalidInput();
|
||||
}
|
||||
|
||||
$user = $service->create($email, $age);
|
||||
```
|
||||
|
||||
`filter_input()` only handles a slice of basic validation. Complex rules, cross-field constraints, and business constraints still need a dedicated validator or request DTO.
|
||||
|
||||
### Escape output per context
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ user input goes straight into HTML
|
||||
echo "<h1>Hello {$_GET['name']}</h1>";
|
||||
|
||||
// ✅ use htmlspecialchars in an HTML text context
|
||||
$name = (string) ($_GET['name'] ?? '');
|
||||
echo '<h1>Hello ' . htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</h1>';
|
||||
```
|
||||
|
||||
Different contexts need different escaping: HTML text, HTML attributes, URLs, JavaScript strings, and CSS are all different. When a template engine's default escaping is turned off, treat it as a security risk.
|
||||
|
||||
### Passwords and randomness
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ md5/sha1 must not be used for password storage
|
||||
$hash = md5($password);
|
||||
|
||||
// ✅ use PHP's built-in password API
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
if (!password_verify($password, $hash)) {
|
||||
throw new InvalidCredentials();
|
||||
}
|
||||
|
||||
// ✅ use a CSPRNG for tokens
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$code = random_int(100000, 999999);
|
||||
```
|
||||
|
||||
Don't hand-roll salts, round migration, or password comparison. Use `password_needs_rehash()` when you need to upgrade the cost factor.
|
||||
|
||||
### Deserialization and object injection
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ untrusted input into unserialize can trigger object injection
|
||||
$payload = unserialize($_COOKIE['state']);
|
||||
|
||||
// ✅ prefer JSON for external data, and validate its schema/shape
|
||||
$payload = json_decode($_COOKIE['state'] ?? '{}', true, flags: JSON_THROW_ON_ERROR);
|
||||
```
|
||||
|
||||
If you must process historical serialized data, at least restrict `allowed_classes` and make sure the relevant classes' magic methods can't produce dangerous side effects.
|
||||
|
||||
### File uploads and paths
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ building the path from the raw filename
|
||||
$target = __DIR__ . '/uploads/' . $_FILES['avatar']['name'];
|
||||
move_uploaded_file($_FILES['avatar']['tmp_name'], $target);
|
||||
|
||||
// ✅ generate a server-side filename, check the upload error and MIME
|
||||
$file = $_FILES['avatar'];
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
throw new UploadFailed();
|
||||
}
|
||||
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mime = $finfo->file($file['tmp_name']);
|
||||
if (!in_array($mime, ['image/png', 'image/jpeg'], true)) {
|
||||
throw new InvalidFileType();
|
||||
}
|
||||
|
||||
$target = __DIR__ . '/uploads/' . bin2hex(random_bytes(16)) . '.jpg';
|
||||
move_uploaded_file($file['tmp_name'], $target);
|
||||
```
|
||||
|
||||
When reviewing upload features, check size limits, MIME detection, extensions, a non-executable storage directory, path traversal, overwrite protection, and any virus-scan or async-processing requirements.
|
||||
|
||||
---
|
||||
|
||||
## Database Access
|
||||
|
||||
### Use parameterized queries
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ concatenated SQL is an injection risk
|
||||
$sql = "SELECT * FROM users WHERE email = '" . $_GET['email'] . "'";
|
||||
$user = $pdo->query($sql)->fetch();
|
||||
|
||||
// ✅ PDO prepared statement + bound value
|
||||
$stmt = $pdo->prepare('SELECT id, email FROM users WHERE email = :email');
|
||||
$stmt->execute(['email' => $email]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
```
|
||||
|
||||
Parameters can only bind values — not table names, column names, or sort direction. Dynamic identifiers must go through a whitelist mapping.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ✅ whitelist the dynamic sort column
|
||||
$columns = [
|
||||
'created' => 'created_at',
|
||||
'email' => 'email',
|
||||
];
|
||||
|
||||
$column = $columns[$_GET['sort'] ?? 'created'] ?? $columns['created'];
|
||||
$stmt = $pdo->query("SELECT id, email FROM users ORDER BY {$column} DESC");
|
||||
```
|
||||
|
||||
### Wrap multi-step writes in transactions
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ multi-step writes with no transaction leave half-finished state on failure
|
||||
$orderId = $orders->create($cart);
|
||||
$inventory->reserve($cart);
|
||||
$payments->charge($orderId);
|
||||
|
||||
// ✅ explicit transaction boundary
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$orderId = $orders->create($cart);
|
||||
$inventory->reserve($cart);
|
||||
$payments->recordIntent($orderId);
|
||||
$pdo->commit();
|
||||
} catch (Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
```
|
||||
|
||||
Don't casually put external, non-rollbackable side effects (an actual charge, an email, a message dispatch) inside a database transaction. Common patterns are an outbox, an idempotency key, or triggering after the transaction commits.
|
||||
|
||||
### Avoid N+1 queries
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ querying inside a loop
|
||||
foreach ($orders as $order) {
|
||||
$customer = $customerRepo->find($order->customerId);
|
||||
render($order, $customer);
|
||||
}
|
||||
|
||||
// ✅ batch-load, then map
|
||||
$customerIds = array_unique(array_map(fn ($o) => $o->customerId, $orders));
|
||||
$customers = $customerRepo->findByIds($customerIds);
|
||||
|
||||
foreach ($orders as $order) {
|
||||
render($order, $customers[$order->customerId] ?? null);
|
||||
}
|
||||
```
|
||||
|
||||
In ORMs like Laravel/Doctrine, check eager loading, join fetch, selected columns, pagination, and indexes.
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Catch specific exceptions, keep context
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ swallowing the exception leaves callers unable to know it failed
|
||||
try {
|
||||
$mailer->send($message);
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
|
||||
// ✅ catch a specific exception, keep context, and rethrow
|
||||
try {
|
||||
$mailer->send($message);
|
||||
} catch (TransportException $e) {
|
||||
throw new NotificationFailed($userId, previous: $e);
|
||||
}
|
||||
```
|
||||
|
||||
Empty `catch` blocks, `error_log()`-and-continue without surfacing the error, and turning every exception into `RuntimeException('failed')` in production code all deserve a question.
|
||||
|
||||
### Don't suppress errors with @
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ hides the real error and makes debugging hard
|
||||
$content = @file_get_contents($path);
|
||||
|
||||
// ✅ handle failure explicitly
|
||||
$content = file_get_contents($path);
|
||||
if ($content === false) {
|
||||
throw new RuntimeException("Unable to read file: {$path}");
|
||||
}
|
||||
```
|
||||
|
||||
`@` is common around file, network, array access, and legacy library calls. Push for an explicit branch, or convert third-party errors into project exceptions.
|
||||
|
||||
### Don't leak sensitive data in logs
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ writing tokens, passwords, or the full request body to the log
|
||||
$logger->error('Login failed', ['request' => $_POST]);
|
||||
|
||||
// ✅ log non-sensitive context that still helps locate the problem
|
||||
$logger->warning('Login failed', [
|
||||
'email_hash' => hash('sha256', strtolower($email)),
|
||||
'ip' => $requestIp,
|
||||
]);
|
||||
```
|
||||
|
||||
Check logs, exception messages, the debug toolbar, error pages, and failed-queue records. Sensitive data includes passwords, tokens, sessions, PII, payment data, and full cookies.
|
||||
|
||||
---
|
||||
|
||||
## Composer & Dependencies
|
||||
|
||||
### Lock reproducible dependencies
|
||||
|
||||
```json
|
||||
{
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"monolog/monolog": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.0",
|
||||
"phpstan/phpstan": "^1.10"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When reviewing `composer.json` / `composer.lock`, watch for:
|
||||
|
||||
- Application repos commit `composer.lock`; library repos usually don't
|
||||
- `require-dev` shouldn't make it into the production image
|
||||
- The PHP platform version matches the CI version
|
||||
- Autoload rules aren't too broad (don't load test or script directories)
|
||||
- `scripts` commands don't depend on a developer's local secret config
|
||||
|
||||
### Dependency security and maintenance
|
||||
|
||||
```bash
|
||||
composer audit
|
||||
composer outdated --direct
|
||||
composer validate --strict
|
||||
```
|
||||
|
||||
When adding a package, look at its maintenance status — download count isn't the only signal. What matters is its security history, release cadence, minimal dependency footprint, and whether it duplicates the standard library or a framework built-in.
|
||||
|
||||
---
|
||||
|
||||
## Performance & Resource Management
|
||||
|
||||
### Stream large datasets with generators or pagination
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ loading every record at once
|
||||
$rows = $repo->all();
|
||||
foreach ($rows as $row) {
|
||||
exportRow($row);
|
||||
}
|
||||
|
||||
// ✅ paginate or use a generator to avoid a memory spike
|
||||
foreach ($repo->cursor() as $row) {
|
||||
exportRow($row);
|
||||
}
|
||||
```
|
||||
|
||||
A PHP request lifecycle is short, but CLI jobs, queue workers, and export tasks run for a long time. For that kind of code, watch memory growth, unclosed resources, and global-state pollution especially closely.
|
||||
|
||||
### Avoid expensive work inside loops
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ re-parsing config or opening a connection on every iteration
|
||||
foreach ($items as $item) {
|
||||
$client = new ApiClient($_ENV['API_KEY']);
|
||||
$client->send($item);
|
||||
}
|
||||
|
||||
// ✅ create reusable dependencies outside the loop
|
||||
$client = new ApiClient($_ENV['API_KEY']);
|
||||
foreach ($items as $item) {
|
||||
$client->send($item);
|
||||
}
|
||||
```
|
||||
|
||||
Watch for database queries, HTTP requests, regex compilation, large array copies, accumulating `array_merge()` appends, and repeatedly reading env vars or config files inside loops.
|
||||
|
||||
### Release or scope resources
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ✅ close file handles after use
|
||||
$handle = fopen($path, 'rb');
|
||||
if ($handle === false) {
|
||||
throw new RuntimeException('Unable to open file');
|
||||
}
|
||||
|
||||
try {
|
||||
while (($line = fgets($handle)) !== false) {
|
||||
process($line);
|
||||
}
|
||||
} finally {
|
||||
fclose($handle);
|
||||
}
|
||||
```
|
||||
|
||||
PDO connections are usually managed by the container, but file handles, curl handles, temp files, locks, and cached objects in queue workers still need an explicit lifecycle.
|
||||
|
||||
---
|
||||
|
||||
## Testing & Static Analysis
|
||||
|
||||
### Test behavior, not implementation details
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ asserting an internal method call makes refactoring expensive
|
||||
$mailer->expects($this->once())->method('buildTemplate');
|
||||
|
||||
// ✅ assert observable results
|
||||
$service->sendWelcomeEmail($user);
|
||||
|
||||
$this->assertTrue($mailbox->hasMessageFor($user->email));
|
||||
```
|
||||
|
||||
For business services, controllers, and queue jobs, prefer covering observable behavior: inputs/outputs, database state, published events, and dispatched messages.
|
||||
|
||||
### Static analysis and formatting
|
||||
|
||||
```bash
|
||||
vendor/bin/phpunit
|
||||
vendor/bin/phpstan analyse
|
||||
vendor/bin/psalm
|
||||
vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||
vendor/bin/rector process --dry-run
|
||||
```
|
||||
|
||||
When reviewing a PR, check whether the new code lowers the PHPStan/Psalm level, leans heavily on baseline ignores, or uses `@phpstan-ignore-next-line` to paper over a real type problem.
|
||||
|
||||
### Isolate test data
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ the test depends on real time and external services
|
||||
$service->expireOldSessions();
|
||||
|
||||
// ✅ inject a clock and a fake gateway
|
||||
$clock->setNow(new DateTimeImmutable('2026-01-01T00:00:00Z'));
|
||||
$service->expireOldSessions();
|
||||
```
|
||||
|
||||
Watch for database transaction rollback, fixture cleanup, randomness, time, queues, caches, and external APIs. Slow PHP tests are usually not a language problem — it's that the boundaries aren't isolated.
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Types & modeling
|
||||
|
||||
- [ ] `declare(strict_types=1);` at the top of the file
|
||||
- [ ] Parameters, return values, and properties have explicit types
|
||||
- [ ] `===` / `!==` used; collection lookups use strict mode
|
||||
- [ ] Stable state sets use an enum, not magic strings
|
||||
- [ ] New code doesn't rely on dynamic properties
|
||||
- [ ] Value objects are readonly or otherwise immutable
|
||||
|
||||
### Security
|
||||
|
||||
- [ ] Input is validated and type-coerced at the boundary
|
||||
- [ ] Output is escaped per HTML/URL/JS/CSS context
|
||||
- [ ] SQL uses prepared statements or ORM binding
|
||||
- [ ] Dynamic table/column/sort names go through a whitelist
|
||||
- [ ] Passwords use `password_hash()` / `password_verify()`
|
||||
- [ ] Tokens, codes, and filenames use `random_bytes()` / `random_int()`
|
||||
- [ ] Untrusted input never reaches `unserialize()`
|
||||
- [ ] File uploads check the error code, size, MIME, extension, and storage directory
|
||||
- [ ] No injection or leakage risk in shell commands, path building, or log output
|
||||
|
||||
### Data & transactions
|
||||
|
||||
- [ ] Multi-step writes have a transaction or compensation mechanism
|
||||
- [ ] External side effects are designed to be idempotent
|
||||
- [ ] N+1 queries avoided
|
||||
- [ ] Pagination, indexes, and selected columns are reasonable
|
||||
- [ ] Database errors aren't swallowed
|
||||
|
||||
### Maintainability
|
||||
|
||||
- [ ] Constructors don't do heavy I/O
|
||||
- [ ] Dependency injection is clear; no hidden global state
|
||||
- [ ] No `@` error suppression
|
||||
- [ ] Exceptions preserve context and `previous`
|
||||
- [ ] Composer dependency ranges, autoload, and scripts are reasonable
|
||||
- [ ] Application repos commit `composer.lock`
|
||||
|
||||
### Testing & tooling
|
||||
|
||||
- [ ] PHPUnit/Pest cover the critical and failure paths
|
||||
- [ ] PHPStan/Psalm config doesn't lower strictness
|
||||
- [ ] New ignores/baselines are explained
|
||||
- [ ] Formatting tools and CI commands are reproducible
|
||||
- [ ] Tests isolate time, randomness, the database, queues, and external APIs
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [PHP Manual: Type declarations](https://www.php.net/manual/en/language.types.declarations.php)
|
||||
- [PHP Manual: match](https://www.php.net/match)
|
||||
- [PHP Manual: Enumerations](https://www.php.net/manual/en/language.enumerations.overview.php)
|
||||
- [PHP Manual: Properties](https://www.php.net/manual/en/language.oop5.properties.php)
|
||||
- [PHP Manual: PDO](https://www.php.net/manual/en/class.pdo.php)
|
||||
- [PHP Manual: password_hash](https://www.php.net/manual/en/function.password-hash.php)
|
||||
- [PHP Manual: random_bytes](https://www.php.net/manual/en/function.random-bytes.php)
|
||||
- [Composer documentation](https://getcomposer.org/doc/)
|
||||
- [PHPUnit documentation](https://docs.phpunit.de/)
|
||||
- [PHPStan documentation](https://phpstan.org/user-guide/getting-started)
|
||||
- [Psalm documentation](https://psalm.dev/docs/)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,186 @@
|
||||
# Qt Code Review Guide
|
||||
|
||||
> Code review guidelines focusing on object model, signals/slots, event loop, and GUI performance. Examples based on Qt 5.15 / Qt 6.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Object Model & Memory Management](#object-model--memory-management)
|
||||
- [Signals & Slots](#signals--slots)
|
||||
- [Containers & Strings](#containers--strings)
|
||||
- [Threads & Concurrency](#threads--concurrency)
|
||||
- [GUI & Widgets](#gui--widgets)
|
||||
- [Meta-Object System](#meta-object-system)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Object Model & Memory Management
|
||||
|
||||
### Use Parent-Child Ownership Mechanism
|
||||
Qt's `QObject` hierarchy automatically manages memory. For `QObject`, prefer setting a parent object over manual `delete` or smart pointers.
|
||||
|
||||
```cpp
|
||||
// ❌ Manual management prone to memory leaks
|
||||
QWidget* w = new QWidget();
|
||||
QLabel* l = new QLabel();
|
||||
l->setParent(w);
|
||||
// ... If w is deleted, l is automatically deleted. But if w leaks, l also leaks.
|
||||
|
||||
// ✅ Specify parent in constructor
|
||||
QWidget* w = new QWidget(this); // Owned by 'this'
|
||||
QLabel* l = new QLabel(w); // Owned by 'w'
|
||||
```
|
||||
|
||||
### Use Smart Pointers with QObject
|
||||
If a `QObject` has no parent, use `QScopedPointer` or `std::unique_ptr` with a custom deleter (use `deleteLater` if cross-thread). Avoid `std::shared_ptr` for `QObject` unless necessary, as it confuses the parent-child ownership system.
|
||||
|
||||
```cpp
|
||||
// ✅ Scoped pointer for local/member QObject without parent
|
||||
QScopedPointer<MyObject> obj(new MyObject());
|
||||
|
||||
// ✅ Safe pointer to prevent dangling pointers
|
||||
QPointer<MyObject> safePtr = obj.data();
|
||||
if (safePtr) {
|
||||
safePtr->doSomething();
|
||||
}
|
||||
```
|
||||
|
||||
### Use `deleteLater()`
|
||||
For asynchronous deletion, especially in slots or event handlers, use `deleteLater()` instead of `delete` to ensure pending events in the event loop are processed.
|
||||
|
||||
---
|
||||
|
||||
## Signals & Slots
|
||||
|
||||
### Prefer Function Pointer Syntax
|
||||
Use compile-time checked syntax (Qt 5+).
|
||||
|
||||
```cpp
|
||||
// ❌ String-based (runtime check only, slower)
|
||||
connect(sender, SIGNAL(valueChanged(int)), receiver, SLOT(updateValue(int)));
|
||||
|
||||
// ✅ Compile-time check
|
||||
connect(sender, &Sender::valueChanged, receiver, &Receiver::updateValue);
|
||||
```
|
||||
|
||||
### Connection Types
|
||||
Be explicit or aware of connection types when crossing threads.
|
||||
- `Qt::AutoConnection` (Default): Direct if same thread, Queued if different thread.
|
||||
- `Qt::QueuedConnection`: Always posts event (thread-safe across threads).
|
||||
- `Qt::DirectConnection`: Immediate call (dangerous if accessing non-thread-safe data across threads).
|
||||
|
||||
### Avoid Loops
|
||||
Check logic that might cause infinite signal loops (e.g., `valueChanged` -> `setValue` -> `valueChanged`). Block signals or check for equality before setting values.
|
||||
|
||||
```cpp
|
||||
void MyClass::setValue(int v) {
|
||||
if (m_value == v) return; // ✅ Good: Break loop
|
||||
m_value = v;
|
||||
emit valueChanged(v);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Containers & Strings
|
||||
|
||||
### QString Efficiency
|
||||
- Use `QStringLiteral("...")` for compile-time string creation to avoid runtime allocation.
|
||||
- Use `QLatin1String` for comparison with ASCII literals (in Qt 5).
|
||||
- Prefer `arg()` for formatting (or `QStringBuilder`'s `%` operator).
|
||||
|
||||
```cpp
|
||||
// ❌ Runtime conversion
|
||||
if (str == "test") ...
|
||||
|
||||
// ✅ Prefer QLatin1String for comparison with ASCII literals (in Qt 5)
|
||||
if (str == QLatin1String("test")) ... // Qt 5
|
||||
if (str == u"test"_s) ... // Qt 6
|
||||
```
|
||||
|
||||
### Container Selection
|
||||
- **Qt 6**: `QList` is now the default choice (unified with `QVector`).
|
||||
- **Qt 5**: Prefer `QVector` over `QList` for contiguous memory and cache performance, unless stable references are needed.
|
||||
- Be aware of Implicit Sharing (Copy-on-Write). Passing containers by value is cheap *until* modified. Use `const &` for read-only access.
|
||||
|
||||
```cpp
|
||||
// ❌ Forces deep copy if function modifies 'list'
|
||||
void process(QVector<int> list) {
|
||||
list[0] = 1;
|
||||
}
|
||||
|
||||
// ✅ Read-only reference
|
||||
void process(const QVector<int>& list) { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Threads & Concurrency
|
||||
|
||||
### Subclassing QThread vs Worker Object
|
||||
Prefer the "Worker Object" pattern over subclassing `QThread` implementation details.
|
||||
|
||||
```cpp
|
||||
// ❌ Business logic inside QThread::run()
|
||||
class MyThread : public QThread {
|
||||
void run() override { ... }
|
||||
};
|
||||
|
||||
// ✅ Worker object moved to thread
|
||||
QThread* thread = new QThread;
|
||||
Worker* worker = new Worker;
|
||||
worker->moveToThread(thread);
|
||||
connect(thread, &QThread::started, worker, &Worker::process);
|
||||
thread->start();
|
||||
```
|
||||
|
||||
### GUI Thread Safety
|
||||
**NEVER** access UI widgets (`QWidget` and subclasses) from a background thread. Use signals/slots to communicate updates to the main thread.
|
||||
|
||||
---
|
||||
|
||||
## GUI & Widgets
|
||||
|
||||
### Logic Separation
|
||||
Keep business logic out of UI classes (`MainWindow`, `Dialog`). UI classes should only handle display and user input forwarding.
|
||||
|
||||
### Layouts
|
||||
Avoid fixed sizes (`setGeometry`, `resize`). Use layouts (`QVBoxLayout`, `QGridLayout`) to handle different DPIs and window resizing gracefully.
|
||||
|
||||
### Blocking Event Loop
|
||||
Never execute long-running operations on the main thread (freezes GUI).
|
||||
- **Bad**: `Sleep()`, `while(busy)`, synchronous network calls.
|
||||
- **Good**: `QProcess`, `QThread`, `QtConcurrent`, or asynchronous APIs (`QNetworkAccessManager`).
|
||||
|
||||
---
|
||||
|
||||
## Meta-Object System
|
||||
|
||||
### Properties & Enums
|
||||
Use `Q_PROPERTY` for values exposed to QML or needing introspection.
|
||||
Use `Q_ENUM` to enable string conversion for enums.
|
||||
|
||||
```cpp
|
||||
class MyObject : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)
|
||||
public:
|
||||
enum State { Idle, Running };
|
||||
Q_ENUM(State)
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
### qobject_cast
|
||||
Use `qobject_cast<T*>` for QObjects instead of `dynamic_cast`. It is faster and doesn't require RTTI.
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
- [ ] **Memory**: Is parent-child relationship correct? Are dangling pointers avoided (using `QPointer`)?
|
||||
- [ ] **Signals**: Are connections checked? Do lambdas use safe captures (context object)?
|
||||
- [ ] **Threads**: Is UI accessed only from main thread? Are long tasks offloaded?
|
||||
- [ ] **Strings**: Are `QStringLiteral` or `tr()` used appropriately?
|
||||
- [ ] **Style**: Naming conventions (camelCase for methods, PascalCase for classes).
|
||||
- [ ] **Resources**: Are resources (images, styles) loaded from `.qrc`?
|
||||
@@ -0,0 +1,871 @@
|
||||
# React Code Review Guide
|
||||
|
||||
React 审查重点:Hooks 规则、性能优化的适度性、组件设计、以及现代 React 19/RSC 模式。
|
||||
|
||||
## 目录
|
||||
|
||||
- [基础 Hooks 规则](#基础-hooks-规则)
|
||||
- [useEffect 模式](#useeffect-模式)
|
||||
- [useMemo / useCallback](#usememo--usecallback)
|
||||
- [组件设计](#组件设计)
|
||||
- [Error Boundaries & Suspense](#error-boundaries--suspense)
|
||||
- [Server Components (RSC)](#server-components-rsc)
|
||||
- [React 19 Actions & Forms](#react-19-actions--forms)
|
||||
- [Suspense & Streaming SSR](#suspense--streaming-ssr)
|
||||
- [TanStack Query v5](#tanstack-query-v5)
|
||||
- [Review Checklists](#review-checklists)
|
||||
|
||||
---
|
||||
|
||||
## 基础 Hooks 规则
|
||||
|
||||
```tsx
|
||||
// ❌ 条件调用 Hooks — 违反 Hooks 规则
|
||||
function BadComponent({ isLoggedIn }) {
|
||||
if (isLoggedIn) {
|
||||
const [user, setUser] = useState(null); // Error!
|
||||
}
|
||||
return <div>...</div>;
|
||||
}
|
||||
|
||||
// ✅ Hooks 必须在组件顶层调用
|
||||
function GoodComponent({ isLoggedIn }) {
|
||||
const [user, setUser] = useState(null);
|
||||
if (!isLoggedIn) return <LoginPrompt />;
|
||||
return <div>{user?.name}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## useEffect 模式
|
||||
|
||||
```tsx
|
||||
// ❌ 依赖数组缺失或不完整
|
||||
function BadEffect({ userId }) {
|
||||
const [user, setUser] = useState(null);
|
||||
useEffect(() => {
|
||||
fetchUser(userId).then(setUser);
|
||||
}, []); // 缺少 userId 依赖!
|
||||
}
|
||||
|
||||
// ✅ 完整的依赖数组
|
||||
function GoodEffect({ userId }) {
|
||||
const [user, setUser] = useState(null);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetchUser(userId).then(data => {
|
||||
if (!cancelled) setUser(data);
|
||||
});
|
||||
return () => { cancelled = true; }; // 清理函数
|
||||
}, [userId]);
|
||||
}
|
||||
|
||||
// ❌ useEffect 用于派生状态(反模式)
|
||||
function BadDerived({ items }) {
|
||||
const [filteredItems, setFilteredItems] = useState([]);
|
||||
useEffect(() => {
|
||||
setFilteredItems(items.filter(i => i.active));
|
||||
}, [items]); // 不必要的 effect + 额外渲染
|
||||
return <List items={filteredItems} />;
|
||||
}
|
||||
|
||||
// ✅ 直接在渲染时计算,或用 useMemo
|
||||
function GoodDerived({ items }) {
|
||||
const filteredItems = useMemo(
|
||||
() => items.filter(i => i.active),
|
||||
[items]
|
||||
);
|
||||
return <List items={filteredItems} />;
|
||||
}
|
||||
|
||||
// ❌ useEffect 用于事件响应
|
||||
function BadEventEffect() {
|
||||
const [query, setQuery] = useState('');
|
||||
useEffect(() => {
|
||||
if (query) {
|
||||
analytics.track('search', { query }); // 应该在事件处理器中
|
||||
}
|
||||
}, [query]);
|
||||
}
|
||||
|
||||
// ✅ 在事件处理器中执行副作用
|
||||
function GoodEvent() {
|
||||
const [query, setQuery] = useState('');
|
||||
const handleSearch = (q: string) => {
|
||||
setQuery(q);
|
||||
analytics.track('search', { query: q });
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## useMemo / useCallback
|
||||
|
||||
```tsx
|
||||
// ❌ 过度优化 — 常量不需要 useMemo
|
||||
function OverOptimized() {
|
||||
const config = useMemo(() => ({ timeout: 5000 }), []); // 无意义
|
||||
const handleClick = useCallback(() => {
|
||||
console.log('clicked');
|
||||
}, []); // 如果不传给 memo 组件,无意义
|
||||
}
|
||||
|
||||
// ✅ 只在需要时优化
|
||||
function ProperlyOptimized() {
|
||||
const config = { timeout: 5000 }; // 简单对象直接定义
|
||||
const handleClick = () => console.log('clicked');
|
||||
}
|
||||
|
||||
// ❌ useCallback 依赖总是变化
|
||||
function BadCallback({ data }) {
|
||||
// data 每次渲染都是新对象,useCallback 无效
|
||||
const process = useCallback(() => {
|
||||
return data.map(transform);
|
||||
}, [data]);
|
||||
}
|
||||
|
||||
// ✅ useMemo + useCallback 配合 React.memo 使用
|
||||
const MemoizedChild = React.memo(function Child({ onClick, items }) {
|
||||
return <div onClick={onClick}>{items.length}</div>;
|
||||
});
|
||||
|
||||
function Parent({ rawItems }) {
|
||||
const items = useMemo(() => processItems(rawItems), [rawItems]);
|
||||
const handleClick = useCallback(() => {
|
||||
console.log(items.length);
|
||||
}, [items]);
|
||||
return <MemoizedChild onClick={handleClick} items={items} />;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 组件设计
|
||||
|
||||
```tsx
|
||||
// ❌ 在组件内定义组件 — 每次渲染都创建新组件
|
||||
function BadParent() {
|
||||
function ChildComponent() { // 每次渲染都是新函数!
|
||||
return <div>child</div>;
|
||||
}
|
||||
return <ChildComponent />;
|
||||
}
|
||||
|
||||
// ✅ 组件定义在外部
|
||||
function ChildComponent() {
|
||||
return <div>child</div>;
|
||||
}
|
||||
function GoodParent() {
|
||||
return <ChildComponent />;
|
||||
}
|
||||
|
||||
// ❌ Props 总是新对象引用
|
||||
function BadProps() {
|
||||
return (
|
||||
<MemoizedComponent
|
||||
style={{ color: 'red' }} // 每次渲染新对象
|
||||
onClick={() => {}} // 每次渲染新函数
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ 稳定的引用
|
||||
const style = { color: 'red' };
|
||||
function GoodProps() {
|
||||
const handleClick = useCallback(() => {}, []);
|
||||
return <MemoizedComponent style={style} onClick={handleClick} />;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Boundaries & Suspense
|
||||
|
||||
```tsx
|
||||
// ❌ 没有错误边界
|
||||
function BadApp() {
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<DataComponent /> {/* 错误会导致整个应用崩溃 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Error Boundary 包裹 Suspense
|
||||
function GoodApp() {
|
||||
return (
|
||||
<ErrorBoundary fallback={<ErrorUI />}>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<DataComponent />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Server Components (RSC)
|
||||
|
||||
```tsx
|
||||
// ❌ 在 Server Component 中使用客户端特性
|
||||
// app/page.tsx (Server Component by default)
|
||||
function BadServerComponent() {
|
||||
const [count, setCount] = useState(0); // Error! No hooks in RSC
|
||||
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
||||
}
|
||||
|
||||
// ✅ 交互逻辑提取到 Client Component
|
||||
// app/counter.tsx
|
||||
'use client';
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
||||
}
|
||||
|
||||
// app/page.tsx (Server Component)
|
||||
async function GoodServerComponent() {
|
||||
const data = await fetchData(); // 可以直接 await
|
||||
return (
|
||||
<div>
|
||||
<h1>{data.title}</h1>
|
||||
<Counter /> {/* 客户端组件 */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ❌ 'use client' 放置不当 — 整个树都变成客户端
|
||||
// layout.tsx
|
||||
'use client'; // 这会让所有子组件都成为客户端组件
|
||||
export default function Layout({ children }) { ... }
|
||||
|
||||
// ✅ 只在需要交互的组件使用 'use client'
|
||||
// 将客户端逻辑隔离到叶子组件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## React 19 Actions & Forms
|
||||
|
||||
React 19 引入了 Actions 系统和新的表单处理 Hooks,简化异步操作和乐观更新。
|
||||
|
||||
### useActionState
|
||||
|
||||
```tsx
|
||||
// ❌ 传统方式:多个状态变量
|
||||
function OldForm() {
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
const handleSubmit = async (formData: FormData) => {
|
||||
setIsPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await submitForm(formData);
|
||||
setData(result);
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setIsPending(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ React 19: useActionState 统一管理
|
||||
import { useActionState } from 'react';
|
||||
|
||||
function NewForm() {
|
||||
const [state, formAction, isPending] = useActionState(
|
||||
async (prevState, formData: FormData) => {
|
||||
try {
|
||||
const result = await submitForm(formData);
|
||||
return { success: true, data: result };
|
||||
} catch (e) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
},
|
||||
{ success: false, data: null, error: null }
|
||||
);
|
||||
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<input name="email" />
|
||||
<button disabled={isPending}>
|
||||
{isPending ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
{state.error && <p className="error">{state.error}</p>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### useFormStatus
|
||||
|
||||
```tsx
|
||||
// ❌ Props 透传表单状态
|
||||
function BadSubmitButton({ isSubmitting }) {
|
||||
return <button disabled={isSubmitting}>Submit</button>;
|
||||
}
|
||||
|
||||
// ✅ useFormStatus 访问父 <form> 状态(无需 props)
|
||||
import { useFormStatus } from 'react-dom';
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending, data, method, action } = useFormStatus();
|
||||
// 注意:必须在 <form> 内部的子组件中使用
|
||||
return (
|
||||
<button disabled={pending}>
|
||||
{pending ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ❌ useFormStatus 在 form 同级组件中调用——不工作
|
||||
function BadForm() {
|
||||
const { pending } = useFormStatus(); // 这里无法获取状态!
|
||||
return (
|
||||
<form action={action}>
|
||||
<button disabled={pending}>Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ useFormStatus 必须在 form 的子组件中
|
||||
function GoodForm() {
|
||||
return (
|
||||
<form action={action}>
|
||||
<SubmitButton /> {/* useFormStatus 在这里面调用 */}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### useOptimistic
|
||||
|
||||
```tsx
|
||||
// ❌ 等待服务器响应再更新 UI
|
||||
function SlowLike({ postId, likes }) {
|
||||
const [likeCount, setLikeCount] = useState(likes);
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
const handleLike = async () => {
|
||||
setIsPending(true);
|
||||
const newCount = await likePost(postId); // 等待...
|
||||
setLikeCount(newCount);
|
||||
setIsPending(false);
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ useOptimistic 即时反馈,失败自动回滚
|
||||
import { useOptimistic } from 'react';
|
||||
|
||||
function FastLike({ postId, likes }) {
|
||||
const [optimisticLikes, addOptimisticLike] = useOptimistic(
|
||||
likes,
|
||||
(currentLikes, increment: number) => currentLikes + increment
|
||||
);
|
||||
|
||||
const handleLike = async () => {
|
||||
addOptimisticLike(1); // 立即更新 UI
|
||||
try {
|
||||
await likePost(postId); // 后台同步
|
||||
} catch {
|
||||
// React 自动回滚到 likes 原值
|
||||
}
|
||||
};
|
||||
|
||||
return <button onClick={handleLike}>{optimisticLikes} likes</button>;
|
||||
}
|
||||
```
|
||||
|
||||
### Server Actions (Next.js 15+)
|
||||
|
||||
```tsx
|
||||
// ❌ 客户端调用 API
|
||||
'use client';
|
||||
function ClientForm() {
|
||||
const handleSubmit = async (formData: FormData) => {
|
||||
const res = await fetch('/api/submit', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
// ...
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ Server Action + useActionState
|
||||
// actions.ts
|
||||
'use server';
|
||||
export async function createPost(prevState: any, formData: FormData) {
|
||||
const title = formData.get('title');
|
||||
await db.posts.create({ title });
|
||||
revalidatePath('/posts');
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// form.tsx
|
||||
'use client';
|
||||
import { createPost } from './actions';
|
||||
|
||||
function PostForm() {
|
||||
const [state, formAction, isPending] = useActionState(createPost, null);
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<input name="title" />
|
||||
<SubmitButton />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Suspense & Streaming SSR
|
||||
|
||||
Suspense 和 Streaming 是 React 18+ 的核心特性,在 2025 年的 Next.js 15 等框架中广泛使用。
|
||||
|
||||
### 基础 Suspense
|
||||
|
||||
```tsx
|
||||
// ❌ 传统加载状态管理
|
||||
function OldComponent() {
|
||||
const [data, setData] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData().then(setData).finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) return <Spinner />;
|
||||
return <DataView data={data} />;
|
||||
}
|
||||
|
||||
// ✅ Suspense 声明式加载状态
|
||||
function NewComponent() {
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<DataView /> {/* 内部使用 use() 或支持 Suspense 的数据获取 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 多个独立 Suspense 边界
|
||||
|
||||
```tsx
|
||||
// ❌ 单一边界——所有内容一起加载
|
||||
function BadLayout() {
|
||||
return (
|
||||
<Suspense fallback={<FullPageSpinner />}>
|
||||
<Header />
|
||||
<MainContent /> {/* 慢 */}
|
||||
<Sidebar /> {/* 快 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ 独立边界——各部分独立流式传输
|
||||
function GoodLayout() {
|
||||
return (
|
||||
<>
|
||||
<Header /> {/* 立即显示 */}
|
||||
<div className="flex">
|
||||
<Suspense fallback={<ContentSkeleton />}>
|
||||
<MainContent /> {/* 独立加载 */}
|
||||
</Suspense>
|
||||
<Suspense fallback={<SidebarSkeleton />}>
|
||||
<Sidebar /> {/* 独立加载 */}
|
||||
</Suspense>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Next.js 15 Streaming
|
||||
|
||||
```tsx
|
||||
// app/page.tsx - 自动 Streaming
|
||||
export default async function Page() {
|
||||
// 这个 await 不会阻塞整个页面
|
||||
const data = await fetchSlowData();
|
||||
return <div>{data}</div>;
|
||||
}
|
||||
|
||||
// app/loading.tsx - 自动 Suspense 边界
|
||||
export default function Loading() {
|
||||
return <Skeleton />;
|
||||
}
|
||||
```
|
||||
|
||||
### use() Hook (React 19)
|
||||
|
||||
```tsx
|
||||
// ✅ 在组件中读取 Promise
|
||||
import { use } from 'react';
|
||||
|
||||
function Comments({ commentsPromise }) {
|
||||
const comments = use(commentsPromise); // 自动触发 Suspense
|
||||
return (
|
||||
<ul>
|
||||
{comments.map(c => <li key={c.id}>{c.text}</li>)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
// 父组件创建 Promise,子组件消费
|
||||
function Post({ postId }) {
|
||||
const commentsPromise = fetchComments(postId); // 不 await
|
||||
return (
|
||||
<article>
|
||||
<PostContent id={postId} />
|
||||
<Suspense fallback={<CommentsSkeleton />}>
|
||||
<Comments commentsPromise={commentsPromise} />
|
||||
</Suspense>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TanStack Query v5
|
||||
|
||||
TanStack Query 是 React 生态中最流行的数据获取库,v5 是当前稳定版本。
|
||||
|
||||
### 基础配置
|
||||
|
||||
```tsx
|
||||
// ❌ 不正确的默认配置
|
||||
const queryClient = new QueryClient(); // 默认配置可能不适合
|
||||
|
||||
// ✅ 生产环境推荐配置
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // 5 分钟内数据视为新鲜
|
||||
gcTime: 1000 * 60 * 30, // 30 分钟后垃圾回收(v5 重命名)
|
||||
retry: 3,
|
||||
refetchOnWindowFocus: false, // 根据需求决定
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### queryOptions (v5 新增)
|
||||
|
||||
```tsx
|
||||
// ❌ 重复定义 queryKey 和 queryFn
|
||||
function Component1() {
|
||||
const { data } = useQuery({
|
||||
queryKey: ['users', userId],
|
||||
queryFn: () => fetchUser(userId),
|
||||
});
|
||||
}
|
||||
|
||||
function prefetchUser(queryClient, userId) {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['users', userId], // 重复!
|
||||
queryFn: () => fetchUser(userId), // 重复!
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ queryOptions 统一定义,类型安全
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
|
||||
const userQueryOptions = (userId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['users', userId],
|
||||
queryFn: () => fetchUser(userId),
|
||||
});
|
||||
|
||||
function Component1({ userId }) {
|
||||
const { data } = useQuery(userQueryOptions(userId));
|
||||
}
|
||||
|
||||
function prefetchUser(queryClient, userId) {
|
||||
queryClient.prefetchQuery(userQueryOptions(userId));
|
||||
}
|
||||
|
||||
// getQueryData 也是类型安全的
|
||||
const user = queryClient.getQueryData(userQueryOptions(userId).queryKey);
|
||||
```
|
||||
|
||||
### 常见陷阱
|
||||
|
||||
```tsx
|
||||
// ❌ staleTime 为 0 导致过度请求
|
||||
useQuery({
|
||||
queryKey: ['data'],
|
||||
queryFn: fetchData,
|
||||
// staleTime 默认为 0,每次组件挂载都会 refetch
|
||||
});
|
||||
|
||||
// ✅ 设置合理的 staleTime
|
||||
useQuery({
|
||||
queryKey: ['data'],
|
||||
queryFn: fetchData,
|
||||
staleTime: 1000 * 60, // 1 分钟内不会重新请求
|
||||
});
|
||||
|
||||
// ❌ 在 queryFn 中使用不稳定的引用
|
||||
function BadQuery({ filters }) {
|
||||
useQuery({
|
||||
queryKey: ['items'], // queryKey 没有包含 filters!
|
||||
queryFn: () => fetchItems(filters), // filters 变化不会触发重新请求
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ queryKey 包含所有影响数据的参数
|
||||
function GoodQuery({ filters }) {
|
||||
useQuery({
|
||||
queryKey: ['items', filters], // filters 是 queryKey 的一部分
|
||||
queryFn: () => fetchItems(filters),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### useSuspenseQuery
|
||||
|
||||
> **重要限制**:useSuspenseQuery 与 useQuery 有显著差异,选择前需了解其限制。
|
||||
|
||||
#### useSuspenseQuery 的限制
|
||||
|
||||
| 特性 | useQuery | useSuspenseQuery |
|
||||
|------|----------|------------------|
|
||||
| `enabled` 选项 | ✅ 支持 | ❌ 不支持 |
|
||||
| `placeholderData` | ✅ 支持 | ❌ 不支持 |
|
||||
| `data` 类型 | `T \| undefined` | `T`(保证有值)|
|
||||
| 错误处理 | `error` 属性 | 抛出到 Error Boundary |
|
||||
| 加载状态 | `isLoading` 属性 | 挂起到 Suspense |
|
||||
|
||||
#### 不支持 enabled 的替代方案
|
||||
|
||||
```tsx
|
||||
// ❌ 使用 useQuery + enabled 实现条件查询
|
||||
function BadSuspenseQuery({ userId }) {
|
||||
const { data } = useSuspenseQuery({
|
||||
queryKey: ['user', userId],
|
||||
queryFn: () => fetchUser(userId),
|
||||
enabled: !!userId, // useSuspenseQuery 不支持 enabled!
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 组件组合实现条件渲染
|
||||
function GoodSuspenseQuery({ userId }) {
|
||||
// useSuspenseQuery 保证 data 是 T 不是 T | undefined
|
||||
const { data } = useSuspenseQuery({
|
||||
queryKey: ['user', userId],
|
||||
queryFn: () => fetchUser(userId),
|
||||
});
|
||||
return <UserProfile user={data} />;
|
||||
}
|
||||
|
||||
function Parent({ userId }) {
|
||||
if (!userId) return <NoUserSelected />;
|
||||
return (
|
||||
<Suspense fallback={<UserSkeleton />}>
|
||||
<GoodSuspenseQuery userId={userId} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误处理差异
|
||||
|
||||
```tsx
|
||||
// ❌ useSuspenseQuery 没有 error 属性
|
||||
function BadErrorHandling() {
|
||||
const { data, error } = useSuspenseQuery({...});
|
||||
if (error) return <Error />; // error 总是 null!
|
||||
}
|
||||
|
||||
// ✅ 使用 Error Boundary 处理错误
|
||||
function GoodErrorHandling() {
|
||||
return (
|
||||
<ErrorBoundary fallback={<ErrorMessage />}>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<DataComponent />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function DataComponent() {
|
||||
// 错误会抛出到 Error Boundary
|
||||
const { data } = useSuspenseQuery({
|
||||
queryKey: ['data'],
|
||||
queryFn: fetchData,
|
||||
});
|
||||
return <Display data={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
#### 何时选择 useSuspenseQuery
|
||||
|
||||
```tsx
|
||||
// ✅ 适合场景:
|
||||
// 1. 数据总是需要的(无条件查询)
|
||||
// 2. 组件必须有数据才能渲染
|
||||
// 3. 使用 React 19 的 Suspense 模式
|
||||
// 4. 服务端组件 + 客户端 hydration
|
||||
|
||||
// ❌ 不适合场景:
|
||||
// 1. 条件查询(根据用户操作触发)
|
||||
// 2. 需要 placeholderData 或初始数据
|
||||
// 3. 需要在组件内处理 loading/error 状态
|
||||
// 4. 多个查询有依赖关系
|
||||
|
||||
// ✅ 多个独立查询用 useSuspenseQueries
|
||||
function MultipleQueries({ userId }) {
|
||||
const [userQuery, postsQuery] = useSuspenseQueries({
|
||||
queries: [
|
||||
{ queryKey: ['user', userId], queryFn: () => fetchUser(userId) },
|
||||
{ queryKey: ['posts', userId], queryFn: () => fetchPosts(userId) },
|
||||
],
|
||||
});
|
||||
// 两个查询并行执行,都完成后组件渲染
|
||||
return <Profile user={userQuery.data} posts={postsQuery.data} />;
|
||||
}
|
||||
```
|
||||
|
||||
### 乐观更新 (v5 简化)
|
||||
|
||||
```tsx
|
||||
// ❌ 手动管理缓存的乐观更新(复杂)
|
||||
const mutation = useMutation({
|
||||
mutationFn: updateTodo,
|
||||
onMutate: async (newTodo) => {
|
||||
await queryClient.cancelQueries({ queryKey: ['todos'] });
|
||||
const previousTodos = queryClient.getQueryData(['todos']);
|
||||
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
|
||||
return { previousTodos };
|
||||
},
|
||||
onError: (err, newTodo, context) => {
|
||||
queryClient.setQueryData(['todos'], context.previousTodos);
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
||||
},
|
||||
});
|
||||
|
||||
// ✅ v5 简化:使用 variables 进行乐观 UI
|
||||
function TodoList() {
|
||||
const { data: todos } = useQuery(todosQueryOptions);
|
||||
const { mutate, variables, isPending } = useMutation({
|
||||
mutationFn: addTodo,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{todos?.map(todo => <TodoItem key={todo.id} todo={todo} />)}
|
||||
{/* 乐观显示正在添加的 todo */}
|
||||
{isPending && <TodoItem todo={variables} isOptimistic />}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### v5 状态字段变化
|
||||
|
||||
```tsx
|
||||
// v4: isLoading 表示首次加载或后续获取
|
||||
// v5: isPending 表示没有数据,isLoading = isPending && isFetching
|
||||
|
||||
const { data, isPending, isFetching, isLoading } = useQuery({...});
|
||||
|
||||
// isPending: 缓存中没有数据(首次加载)
|
||||
// isFetching: 正在请求中(包括后台刷新)
|
||||
// isLoading: isPending && isFetching(首次加载中)
|
||||
|
||||
// ❌ v4 代码直接迁移
|
||||
if (isLoading) return <Spinner />; // v5 中行为可能不同
|
||||
|
||||
// ✅ 明确意图
|
||||
if (isPending) return <Spinner />; // 没有数据时显示加载
|
||||
// 或
|
||||
if (isLoading) return <Spinner />; // 首次加载中
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklists
|
||||
|
||||
### Hooks 规则
|
||||
|
||||
- [ ] Hooks 在组件/自定义 Hook 顶层调用
|
||||
- [ ] 没有条件/循环中调用 Hooks
|
||||
- [ ] useEffect 依赖数组完整
|
||||
- [ ] useEffect 有清理函数(订阅/定时器/请求)
|
||||
- [ ] 没有用 useEffect 计算派生状态
|
||||
|
||||
### 性能优化(适度原则)
|
||||
|
||||
- [ ] useMemo/useCallback 只用于真正需要的场景
|
||||
- [ ] React.memo 配合稳定的 props 引用
|
||||
- [ ] 没有在组件内定义子组件
|
||||
- [ ] 没有在 JSX 中创建新对象/函数(除非传给非 memo 组件)
|
||||
- [ ] 长列表使用虚拟化(react-window/react-virtual)
|
||||
|
||||
### 组件设计
|
||||
|
||||
- [ ] 组件职责单一,不超过 200 行
|
||||
- [ ] 逻辑与展示分离(Custom Hooks)
|
||||
- [ ] Props 接口清晰,使用 TypeScript
|
||||
- [ ] 避免 Props Drilling(考虑 Context 或组合)
|
||||
|
||||
### 状态管理
|
||||
|
||||
- [ ] 状态就近原则(最小必要范围)
|
||||
- [ ] 复杂状态用 useReducer
|
||||
- [ ] 全局状态用 Context 或状态库
|
||||
- [ ] 避免不必要的状态(派生 > 存储)
|
||||
|
||||
### 错误处理
|
||||
|
||||
- [ ] 关键区域有 Error Boundary
|
||||
- [ ] Suspense 配合 Error Boundary 使用
|
||||
- [ ] 异步操作有错误处理
|
||||
|
||||
### Server Components (RSC)
|
||||
|
||||
- [ ] 'use client' 只用于需要交互的组件
|
||||
- [ ] Server Component 不使用 Hooks/事件处理
|
||||
- [ ] 客户端组件尽量放在叶子节点
|
||||
- [ ] 数据获取在 Server Component 中进行
|
||||
|
||||
### React 19 Forms
|
||||
|
||||
- [ ] 使用 useActionState 替代多个 useState
|
||||
- [ ] useFormStatus 在 form 子组件中调用
|
||||
- [ ] useOptimistic 不用于关键业务(支付等)
|
||||
- [ ] Server Action 正确标记 'use server'
|
||||
|
||||
### Suspense & Streaming
|
||||
|
||||
- [ ] 按用户体验需求划分 Suspense 边界
|
||||
- [ ] 每个 Suspense 有对应的 Error Boundary
|
||||
- [ ] 提供有意义的 fallback(骨架屏 > Spinner)
|
||||
- [ ] 避免在 layout 层级 await 慢数据
|
||||
|
||||
### TanStack Query
|
||||
|
||||
- [ ] queryKey 包含所有影响数据的参数
|
||||
- [ ] 设置合理的 staleTime(不是默认 0)
|
||||
- [ ] useSuspenseQuery 不使用 enabled
|
||||
- [ ] Mutation 成功后 invalidate 相关查询
|
||||
- [ ] 理解 isPending vs isLoading 区别
|
||||
|
||||
### 测试
|
||||
|
||||
- [ ] 使用 @testing-library/react
|
||||
- [ ] 用 screen 查询元素
|
||||
- [ ] 用 userEvent 代替 fireEvent
|
||||
- [ ] 优先使用 *ByRole 查询
|
||||
- [ ] 测试行为而非实现细节
|
||||
@@ -0,0 +1,842 @@
|
||||
# Rust Code Review Guide
|
||||
|
||||
> Rust 代码审查指南。编译器能捕获内存安全问题,但审查者需要关注编译器无法检测的问题——业务逻辑、API 设计、性能、取消安全性和可维护性。
|
||||
|
||||
## 目录
|
||||
|
||||
- [所有权与借用](#所有权与借用)
|
||||
- [Unsafe 代码审查](#unsafe-代码审查最关键)
|
||||
- [异步代码](#异步代码)
|
||||
- [取消安全性](#取消安全性)
|
||||
- [spawn vs await](#spawn-vs-await)
|
||||
- [错误处理](#错误处理)
|
||||
- [性能](#性能)
|
||||
- [Trait 设计](#trait-设计)
|
||||
- [Review Checklist](#rust-review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 所有权与借用
|
||||
|
||||
### 避免不必要的 clone()
|
||||
|
||||
```rust
|
||||
// ❌ clone() 是"Rust 的胶带"——用于绕过借用检查器
|
||||
fn bad_process(data: &Data) -> Result<()> {
|
||||
let owned = data.clone(); // 为什么需要 clone?
|
||||
expensive_operation(owned)
|
||||
}
|
||||
|
||||
// ✅ 审查时问:clone 是否必要?能否用借用?
|
||||
fn good_process(data: &Data) -> Result<()> {
|
||||
expensive_operation(data) // 传递引用
|
||||
}
|
||||
|
||||
// ✅ 如果确实需要 clone,添加注释说明原因
|
||||
fn justified_clone(data: &Data) -> Result<()> {
|
||||
// Clone needed: data will be moved to spawned task
|
||||
let owned = data.clone();
|
||||
tokio::spawn(async move {
|
||||
process(owned).await
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Arc<Mutex<T>> 的使用
|
||||
|
||||
```rust
|
||||
// ❌ Arc<Mutex<T>> 可能隐藏不必要的共享状态
|
||||
struct BadService {
|
||||
cache: Arc<Mutex<HashMap<String, Data>>>, // 真的需要共享?
|
||||
}
|
||||
|
||||
// ✅ 考虑是否需要共享,或者设计可以避免
|
||||
struct GoodService {
|
||||
cache: HashMap<String, Data>, // 单一所有者
|
||||
}
|
||||
|
||||
// ✅ 如果确实需要并发访问,考虑更好的数据结构
|
||||
use dashmap::DashMap;
|
||||
|
||||
struct ConcurrentService {
|
||||
cache: DashMap<String, Data>, // 更细粒度的锁
|
||||
}
|
||||
```
|
||||
|
||||
### Cow (Copy-on-Write) 模式
|
||||
|
||||
```rust
|
||||
use std::borrow::Cow;
|
||||
|
||||
// ❌ 总是分配新字符串
|
||||
fn bad_process_name(name: &str) -> String {
|
||||
if name.is_empty() {
|
||||
"Unknown".to_string() // 分配
|
||||
} else {
|
||||
name.to_string() // 不必要的分配
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 使用 Cow 避免不必要的分配
|
||||
fn good_process_name(name: &str) -> Cow<'_, str> {
|
||||
if name.is_empty() {
|
||||
Cow::Borrowed("Unknown") // 静态字符串,无分配
|
||||
} else {
|
||||
Cow::Borrowed(name) // 借用原始数据
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 只在需要修改时才分配
|
||||
fn normalize_name(name: &str) -> Cow<'_, str> {
|
||||
if name.chars().any(|c| c.is_uppercase()) {
|
||||
Cow::Owned(name.to_lowercase()) // 需要修改,分配
|
||||
} else {
|
||||
Cow::Borrowed(name) // 无需修改,借用
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Unsafe 代码审查(最关键!)
|
||||
|
||||
### 基本要求
|
||||
|
||||
```rust
|
||||
// ❌ unsafe 没有安全文档——这是红旗
|
||||
unsafe fn bad_transmute<T, U>(t: T) -> U {
|
||||
std::mem::transmute(t)
|
||||
}
|
||||
|
||||
// ✅ 每个 unsafe 必须解释:为什么安全?什么不变量?
|
||||
/// Transmutes `T` to `U`.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - `T` and `U` must have the same size and alignment
|
||||
/// - `T` must be a valid bit pattern for `U`
|
||||
/// - The caller ensures no references to `t` exist after this call
|
||||
unsafe fn documented_transmute<T, U>(t: T) -> U {
|
||||
// SAFETY: Caller guarantees size/alignment match and bit validity
|
||||
std::mem::transmute(t)
|
||||
}
|
||||
```
|
||||
|
||||
### Unsafe 块注释
|
||||
|
||||
```rust
|
||||
// ❌ 没有解释的 unsafe 块
|
||||
fn bad_get_unchecked(slice: &[u8], index: usize) -> u8 {
|
||||
unsafe { *slice.get_unchecked(index) }
|
||||
}
|
||||
|
||||
// ✅ 每个 unsafe 块必须有 SAFETY 注释
|
||||
fn good_get_unchecked(slice: &[u8], index: usize) -> u8 {
|
||||
debug_assert!(index < slice.len(), "index out of bounds");
|
||||
// SAFETY: We verified index < slice.len() via debug_assert.
|
||||
// In release builds, callers must ensure valid index.
|
||||
unsafe { *slice.get_unchecked(index) }
|
||||
}
|
||||
|
||||
// ✅ 封装 unsafe 提供安全 API
|
||||
pub fn checked_get(slice: &[u8], index: usize) -> Option<u8> {
|
||||
if index < slice.len() {
|
||||
// SAFETY: bounds check performed above
|
||||
Some(unsafe { *slice.get_unchecked(index) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 常见 unsafe 模式
|
||||
|
||||
```rust
|
||||
// ✅ FFI 边界
|
||||
extern "C" {
|
||||
fn external_function(ptr: *const u8, len: usize) -> i32;
|
||||
}
|
||||
|
||||
pub fn safe_wrapper(data: &[u8]) -> Result<i32, Error> {
|
||||
// SAFETY: data.as_ptr() is valid for data.len() bytes,
|
||||
// and external_function only reads from the buffer.
|
||||
let result = unsafe {
|
||||
external_function(data.as_ptr(), data.len())
|
||||
};
|
||||
if result < 0 {
|
||||
Err(Error::from_code(result))
|
||||
} else {
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 性能关键路径的 unsafe
|
||||
pub fn fast_copy(src: &[u8], dst: &mut [u8]) {
|
||||
assert_eq!(src.len(), dst.len(), "slices must be equal length");
|
||||
// SAFETY: src and dst are valid slices of equal length,
|
||||
// and dst is mutable so no aliasing.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
src.as_ptr(),
|
||||
dst.as_mut_ptr(),
|
||||
src.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 异步代码
|
||||
|
||||
### 避免阻塞操作
|
||||
|
||||
```rust
|
||||
// ❌ 在 async 上下文中阻塞——会饿死其他任务
|
||||
async fn bad_async() {
|
||||
let data = std::fs::read_to_string("file.txt").unwrap(); // 阻塞!
|
||||
std::thread::sleep(Duration::from_secs(1)); // 阻塞!
|
||||
}
|
||||
|
||||
// ✅ 使用异步 API
|
||||
async fn good_async() -> Result<String> {
|
||||
let data = tokio::fs::read_to_string("file.txt").await?;
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
// ✅ 如果必须使用阻塞操作,用 spawn_blocking
|
||||
async fn with_blocking() -> Result<Data> {
|
||||
let result = tokio::task::spawn_blocking(|| {
|
||||
// 这里可以安全地进行阻塞操作
|
||||
expensive_cpu_computation()
|
||||
}).await?;
|
||||
Ok(result)
|
||||
}
|
||||
```
|
||||
|
||||
### Mutex 和 .await
|
||||
|
||||
```rust
|
||||
// ❌ 跨 .await 持有 std::sync::Mutex——可能死锁
|
||||
async fn bad_lock(mutex: &std::sync::Mutex<Data>) {
|
||||
let guard = mutex.lock().unwrap();
|
||||
async_operation().await; // 持锁等待!
|
||||
process(&guard);
|
||||
}
|
||||
|
||||
// ✅ 方案1:最小化锁范围
|
||||
async fn good_lock_scoped(mutex: &std::sync::Mutex<Data>) {
|
||||
let data = {
|
||||
let guard = mutex.lock().unwrap();
|
||||
guard.clone() // 立即释放锁
|
||||
};
|
||||
async_operation().await;
|
||||
process(&data);
|
||||
}
|
||||
|
||||
// ✅ 方案2:使用 tokio::sync::Mutex(可跨 await)
|
||||
async fn good_lock_tokio(mutex: &tokio::sync::Mutex<Data>) {
|
||||
let guard = mutex.lock().await;
|
||||
async_operation().await; // OK: tokio Mutex 设计为可跨 await
|
||||
process(&guard);
|
||||
}
|
||||
|
||||
// 💡 选择指南:
|
||||
// - std::sync::Mutex:低竞争、短临界区、不跨 await
|
||||
// - tokio::sync::Mutex:需要跨 await、高竞争场景
|
||||
```
|
||||
|
||||
### 异步 trait 方法
|
||||
|
||||
```rust
|
||||
// ❌ async trait 方法的陷阱(旧版本)
|
||||
#[async_trait]
|
||||
trait BadRepository {
|
||||
async fn find(&self, id: i64) -> Option<Entity>; // 隐式 Box
|
||||
}
|
||||
|
||||
// ✅ Rust 1.75+:原生 async trait 方法
|
||||
trait Repository {
|
||||
async fn find(&self, id: i64) -> Option<Entity>;
|
||||
|
||||
// 返回具体 Future 类型以避免 allocation
|
||||
fn find_many(&self, ids: &[i64]) -> impl Future<Output = Vec<Entity>> + Send;
|
||||
}
|
||||
|
||||
// ✅ 对于需要 dyn 的场景
|
||||
trait DynRepository: Send + Sync {
|
||||
fn find(&self, id: i64) -> Pin<Box<dyn Future<Output = Option<Entity>> + Send + '_>>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 取消安全性
|
||||
|
||||
### 什么是取消安全
|
||||
|
||||
```rust
|
||||
// 当一个 Future 在 .await 点被 drop 时,它处于什么状态?
|
||||
// 取消安全的 Future:可以在任何 await 点安全取消
|
||||
// 取消不安全的 Future:取消可能导致数据丢失或不一致状态
|
||||
|
||||
// ❌ 取消不安全的例子
|
||||
async fn cancel_unsafe(conn: &mut Connection) -> Result<()> {
|
||||
let data = receive_data().await; // 如果这里被取消...
|
||||
conn.send_ack().await; // ...确认永远不会发送,数据可能丢失
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ✅ 取消安全的版本
|
||||
async fn cancel_safe(conn: &mut Connection) -> Result<()> {
|
||||
// 使用事务或原子操作确保一致性
|
||||
let transaction = conn.begin_transaction().await?;
|
||||
let data = receive_data().await;
|
||||
transaction.commit_with_ack(data).await?; // 原子操作
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### select! 中的取消安全
|
||||
|
||||
```rust
|
||||
use tokio::select;
|
||||
|
||||
// ❌ 在 select! 中使用取消不安全的 Future
|
||||
async fn bad_select(stream: &mut TcpStream) {
|
||||
let mut buffer = vec![0u8; 1024];
|
||||
loop {
|
||||
select! {
|
||||
// read_exact 不是取消安全的:timeout 先完成时,
|
||||
// 已经读进 buffer 的部分字节会随 Future 一起丢弃
|
||||
result = stream.read_exact(&mut buffer) => {
|
||||
result?;
|
||||
handle_data(&buffer);
|
||||
}
|
||||
_ = tokio::time::sleep(Duration::from_secs(5)) => {
|
||||
println!("Timeout");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 使用取消安全的 API
|
||||
async fn good_select(stream: &mut TcpStream) {
|
||||
let mut buffer = vec![0u8; 1024];
|
||||
loop {
|
||||
select! {
|
||||
// read 是取消安全的:被取消时未读取的数据仍留在流中
|
||||
// 真的需要按定长读取时,把 read_exact 丢到单独的 task 里,
|
||||
// 这里 select! 它的 JoinHandle,取消就不会丢字节
|
||||
result = stream.read(&mut buffer) => {
|
||||
match result {
|
||||
Ok(0) => break, // EOF
|
||||
Ok(n) => handle_data(&buffer[..n]),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
_ = tokio::time::sleep(Duration::from_secs(5)) => {
|
||||
println!("Timeout, retrying...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 使用 tokio::pin! 确保 Future 可以安全重用
|
||||
async fn pinned_select() {
|
||||
let sleep = tokio::time::sleep(Duration::from_secs(10));
|
||||
tokio::pin!(sleep);
|
||||
|
||||
loop {
|
||||
select! {
|
||||
_ = &mut sleep => {
|
||||
println!("Timer elapsed");
|
||||
break;
|
||||
}
|
||||
data = receive_data() => {
|
||||
process(data).await;
|
||||
// sleep 继续倒计时,不会重置
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 文档化取消安全性
|
||||
|
||||
```rust
|
||||
/// Reads a complete message from the stream.
|
||||
///
|
||||
/// # Cancel Safety
|
||||
///
|
||||
/// This method is **not** cancel safe. If cancelled while reading,
|
||||
/// partial data may be lost and the stream state becomes undefined.
|
||||
/// Use `read_message_cancel_safe` if cancellation is expected.
|
||||
async fn read_message(stream: &mut TcpStream) -> Result<Message> {
|
||||
let len = stream.read_u32().await?;
|
||||
let mut buffer = vec![0u8; len as usize];
|
||||
stream.read_exact(&mut buffer).await?;
|
||||
Ok(Message::from_bytes(&buffer))
|
||||
}
|
||||
|
||||
/// Reads a message with cancel safety.
|
||||
///
|
||||
/// # Cancel Safety
|
||||
///
|
||||
/// This method is cancel safe. If cancelled, any partial data
|
||||
/// is preserved in the internal buffer for the next call.
|
||||
async fn read_message_cancel_safe(reader: &mut BufferedReader) -> Result<Message> {
|
||||
reader.read_message_buffered().await
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## spawn vs await
|
||||
|
||||
### 何时使用 spawn
|
||||
|
||||
```rust
|
||||
// ❌ 不必要的 spawn——增加开销,失去结构化并发
|
||||
async fn bad_unnecessary_spawn() {
|
||||
let handle = tokio::spawn(async {
|
||||
simple_operation().await
|
||||
});
|
||||
handle.await.unwrap(); // 为什么不直接 await?
|
||||
}
|
||||
|
||||
// ✅ 直接 await 简单操作
|
||||
async fn good_direct_await() {
|
||||
simple_operation().await;
|
||||
}
|
||||
|
||||
// ✅ spawn 用于真正的并行执行
|
||||
async fn good_parallel_spawn() {
|
||||
let task1 = tokio::spawn(fetch_from_service_a());
|
||||
let task2 = tokio::spawn(fetch_from_service_b());
|
||||
|
||||
// 两个请求并行执行
|
||||
let (result1, result2) = tokio::try_join!(task1, task2)?;
|
||||
}
|
||||
|
||||
// ✅ spawn 用于后台任务(fire-and-forget)
|
||||
async fn good_background_spawn() {
|
||||
// 启动后台任务,不等待完成
|
||||
tokio::spawn(async {
|
||||
cleanup_old_sessions().await;
|
||||
log_metrics().await;
|
||||
});
|
||||
|
||||
// 继续执行其他工作
|
||||
handle_request().await;
|
||||
}
|
||||
```
|
||||
|
||||
### spawn 的 'static 要求
|
||||
|
||||
```rust
|
||||
// ❌ spawn 的 Future 必须是 'static
|
||||
async fn bad_spawn_borrow(data: &Data) {
|
||||
tokio::spawn(async {
|
||||
process(data).await; // Error: `data` 不是 'static
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 方案1:克隆数据
|
||||
async fn good_spawn_clone(data: &Data) {
|
||||
let owned = data.clone();
|
||||
tokio::spawn(async move {
|
||||
process(&owned).await;
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 方案2:使用 Arc 共享
|
||||
async fn good_spawn_arc(data: Arc<Data>) {
|
||||
let data = Arc::clone(&data);
|
||||
tokio::spawn(async move {
|
||||
process(&data).await;
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 方案3:使用作用域任务(tokio-scoped 或 async-scoped)
|
||||
async fn good_scoped_spawn(data: &Data) {
|
||||
// 假设使用 async-scoped crate
|
||||
async_scoped::scope(|s| async {
|
||||
s.spawn(async {
|
||||
process(data).await; // 可以借用
|
||||
});
|
||||
}).await;
|
||||
}
|
||||
```
|
||||
|
||||
### JoinHandle 错误处理
|
||||
|
||||
```rust
|
||||
// ❌ 忽略 spawn 的错误
|
||||
async fn bad_ignore_spawn_error() {
|
||||
let handle = tokio::spawn(async {
|
||||
risky_operation().await
|
||||
});
|
||||
let _ = handle.await; // 忽略了 panic 和错误
|
||||
}
|
||||
|
||||
// ✅ 正确处理 JoinHandle 结果
|
||||
async fn good_handle_spawn_error() -> Result<()> {
|
||||
let handle = tokio::spawn(async {
|
||||
risky_operation().await
|
||||
});
|
||||
|
||||
match handle.await {
|
||||
Ok(Ok(result)) => {
|
||||
// 任务成功完成
|
||||
process_result(result);
|
||||
Ok(())
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
// 任务内部错误
|
||||
Err(e.into())
|
||||
}
|
||||
Err(join_err) => {
|
||||
// 任务 panic 或被取消
|
||||
if join_err.is_panic() {
|
||||
error!("Task panicked: {:?}", join_err);
|
||||
}
|
||||
Err(anyhow!("Task failed: {}", join_err))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 结构化并发 vs spawn
|
||||
|
||||
```rust
|
||||
// ✅ 优先使用 join!(结构化并发)
|
||||
async fn structured_concurrency() -> Result<(A, B, C)> {
|
||||
// 所有任务在同一个作用域内
|
||||
// 如果任何一个失败,其他的会被取消
|
||||
tokio::try_join!(
|
||||
fetch_a(),
|
||||
fetch_b(),
|
||||
fetch_c()
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ 使用 spawn 时考虑任务生命周期
|
||||
struct TaskManager {
|
||||
handles: Vec<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl TaskManager {
|
||||
async fn shutdown(self) {
|
||||
// 优雅关闭:等待所有任务完成
|
||||
for handle in self.handles {
|
||||
if let Err(e) = handle.await {
|
||||
error!("Task failed during shutdown: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn abort_all(self) {
|
||||
// 强制关闭:取消所有任务
|
||||
for handle in self.handles {
|
||||
handle.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 库 vs 应用的错误类型
|
||||
|
||||
```rust
|
||||
// ❌ 库代码用 anyhow——调用者无法 match 错误
|
||||
pub fn parse_config(s: &str) -> anyhow::Result<Config> { ... }
|
||||
|
||||
// ✅ 库用 thiserror,应用用 anyhow
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConfigError {
|
||||
#[error("invalid syntax at line {line}: {message}")]
|
||||
Syntax { line: usize, message: String },
|
||||
#[error("missing required field: {0}")]
|
||||
MissingField(String),
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
pub fn parse_config(s: &str) -> Result<Config, ConfigError> { ... }
|
||||
```
|
||||
|
||||
### 保留错误上下文
|
||||
|
||||
```rust
|
||||
// ❌ 吞掉错误上下文
|
||||
fn bad_error() -> Result<()> {
|
||||
operation().map_err(|_| anyhow!("failed"))?; // 原始错误丢失
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ✅ 使用 context 保留错误链
|
||||
fn good_error() -> Result<()> {
|
||||
operation().context("failed to perform operation")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ✅ 使用 with_context 进行懒计算
|
||||
fn good_error_lazy() -> Result<()> {
|
||||
operation()
|
||||
.with_context(|| format!("failed to process file: {}", filename))?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 错误类型设计
|
||||
|
||||
```rust
|
||||
// ✅ 使用 #[source] 保留错误链
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ServiceError {
|
||||
#[error("database error")]
|
||||
Database(#[source] sqlx::Error),
|
||||
|
||||
#[error("network error: {message}")]
|
||||
Network {
|
||||
message: String,
|
||||
#[source]
|
||||
source: reqwest::Error,
|
||||
},
|
||||
|
||||
#[error("validation failed: {0}")]
|
||||
Validation(String),
|
||||
}
|
||||
|
||||
// ✅ 为常见转换实现 From
|
||||
impl From<sqlx::Error> for ServiceError {
|
||||
fn from(err: sqlx::Error) -> Self {
|
||||
ServiceError::Database(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能
|
||||
|
||||
### 避免不必要的 collect()
|
||||
|
||||
```rust
|
||||
// ❌ 不必要的 collect——中间分配
|
||||
fn bad_sum(items: &[i32]) -> i32 {
|
||||
items.iter()
|
||||
.filter(|x| **x > 0)
|
||||
.collect::<Vec<_>>() // 不必要!
|
||||
.iter()
|
||||
.sum()
|
||||
}
|
||||
|
||||
// ✅ 惰性迭代
|
||||
fn good_sum(items: &[i32]) -> i32 {
|
||||
items.iter().filter(|x| **x > 0).copied().sum()
|
||||
}
|
||||
```
|
||||
|
||||
### 字符串拼接
|
||||
|
||||
```rust
|
||||
// ❌ 字符串拼接在循环中重复分配
|
||||
fn bad_concat(items: &[&str]) -> String {
|
||||
let mut s = String::new();
|
||||
for item in items {
|
||||
s = s + item; // 每次都重新分配!
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
// ✅ 预分配或用 join
|
||||
fn good_concat(items: &[&str]) -> String {
|
||||
items.join("")
|
||||
}
|
||||
|
||||
// ✅ 使用 with_capacity 预分配
|
||||
fn good_concat_capacity(items: &[&str]) -> String {
|
||||
let total_len: usize = items.iter().map(|s| s.len()).sum();
|
||||
let mut result = String::with_capacity(total_len);
|
||||
for item in items {
|
||||
result.push_str(item);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// ✅ 使用 write! 宏
|
||||
use std::fmt::Write;
|
||||
|
||||
fn good_concat_write(items: &[&str]) -> String {
|
||||
let mut result = String::new();
|
||||
for item in items {
|
||||
write!(result, "{}", item).unwrap();
|
||||
}
|
||||
result
|
||||
}
|
||||
```
|
||||
|
||||
### 避免不必要的分配
|
||||
|
||||
```rust
|
||||
// ❌ 不必要的 Vec 分配
|
||||
fn bad_check_any(items: &[Item]) -> bool {
|
||||
let filtered: Vec<_> = items.iter()
|
||||
.filter(|i| i.is_valid())
|
||||
.collect();
|
||||
!filtered.is_empty()
|
||||
}
|
||||
|
||||
// ✅ 使用迭代器方法
|
||||
fn good_check_any(items: &[Item]) -> bool {
|
||||
items.iter().any(|i| i.is_valid())
|
||||
}
|
||||
|
||||
// ❌ String::from 用于静态字符串
|
||||
fn bad_static() -> String {
|
||||
String::from("error message") // 运行时分配
|
||||
}
|
||||
|
||||
// ✅ 返回 &'static str
|
||||
fn good_static() -> &'static str {
|
||||
"error message" // 无分配
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trait 设计
|
||||
|
||||
### 避免过度抽象
|
||||
|
||||
```rust
|
||||
// ❌ 过度抽象——不是 Java,不需要 Interface 一切
|
||||
trait Processor { fn process(&self); }
|
||||
trait Handler { fn handle(&self); }
|
||||
trait Manager { fn manage(&self); } // Trait 过多
|
||||
|
||||
// ✅ 只在需要多态时创建 trait
|
||||
// 具体类型通常更简单、更快
|
||||
struct DataProcessor {
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl DataProcessor {
|
||||
fn process(&self, data: &Data) -> Result<Output> {
|
||||
// 直接实现
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Trait 对象 vs 泛型
|
||||
|
||||
```rust
|
||||
// ❌ 不必要的 trait 对象(动态分发)
|
||||
fn bad_process(handler: &dyn Handler) {
|
||||
handler.handle(); // 虚表调用
|
||||
}
|
||||
|
||||
// ✅ 使用泛型(静态分发,可内联)
|
||||
fn good_process<H: Handler>(handler: &H) {
|
||||
handler.handle(); // 可能被内联
|
||||
}
|
||||
|
||||
// ✅ trait 对象适用场景:异构集合
|
||||
fn store_handlers(handlers: Vec<Box<dyn Handler>>) {
|
||||
// 需要存储不同类型的 handlers
|
||||
}
|
||||
|
||||
// ✅ 使用 impl Trait 返回类型
|
||||
fn create_handler() -> impl Handler {
|
||||
ConcreteHandler::new()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rust Review Checklist
|
||||
|
||||
### 编译器不能捕获的问题
|
||||
|
||||
**业务逻辑正确性**
|
||||
- [ ] 边界条件处理正确
|
||||
- [ ] 状态机转换完整
|
||||
- [ ] 并发场景下的竞态条件
|
||||
|
||||
**API 设计**
|
||||
- [ ] 公共 API 难以误用
|
||||
- [ ] 类型签名清晰表达意图
|
||||
- [ ] 错误类型粒度合适
|
||||
|
||||
### 所有权与借用
|
||||
|
||||
- [ ] clone() 是有意为之,文档说明了原因
|
||||
- [ ] Arc<Mutex<T>> 真的需要共享状态吗?
|
||||
- [ ] RefCell 的使用有正当理由
|
||||
- [ ] 生命周期不过度复杂
|
||||
- [ ] 考虑使用 Cow 避免不必要的分配
|
||||
|
||||
### Unsafe 代码(最重要)
|
||||
|
||||
- [ ] 每个 unsafe 块有 SAFETY 注释
|
||||
- [ ] unsafe fn 有 # Safety 文档节
|
||||
- [ ] 解释了为什么是安全的,不只是做什么
|
||||
- [ ] 列出了必须维护的不变量
|
||||
- [ ] unsafe 边界尽可能小
|
||||
- [ ] 考虑过是否有 safe 替代方案
|
||||
|
||||
### 异步/并发
|
||||
|
||||
- [ ] 没有在 async 中阻塞(std::fs、thread::sleep)
|
||||
- [ ] 没有跨 .await 持有 std::sync 锁
|
||||
- [ ] spawn 的任务满足 'static
|
||||
- [ ] 锁的获取顺序一致
|
||||
- [ ] Channel 缓冲区大小合理
|
||||
|
||||
### 取消安全性
|
||||
|
||||
- [ ] select! 中的 Future 是取消安全的
|
||||
- [ ] 文档化了 async 函数的取消安全性
|
||||
- [ ] 取消不会导致数据丢失或不一致状态
|
||||
- [ ] 使用 tokio::pin! 正确处理需要重用的 Future
|
||||
|
||||
### spawn vs await
|
||||
|
||||
- [ ] spawn 只用于真正需要并行的场景
|
||||
- [ ] 简单操作直接 await,不要 spawn
|
||||
- [ ] spawn 的 JoinHandle 结果被正确处理
|
||||
- [ ] 考虑任务的生命周期和关闭策略
|
||||
- [ ] 优先使用 join!/try_join! 进行结构化并发
|
||||
|
||||
### 错误处理
|
||||
|
||||
- [ ] 库:thiserror 定义结构化错误
|
||||
- [ ] 应用:anyhow + context
|
||||
- [ ] 没有生产代码 unwrap/expect
|
||||
- [ ] 错误消息对调试有帮助
|
||||
- [ ] must_use 返回值被处理
|
||||
- [ ] 使用 #[source] 保留错误链
|
||||
|
||||
### 性能
|
||||
|
||||
- [ ] 避免不必要的 collect()
|
||||
- [ ] 大数据传引用
|
||||
- [ ] 字符串用 with_capacity 或 write!
|
||||
- [ ] impl Trait vs Box<dyn Trait> 选择合理
|
||||
- [ ] 热路径避免分配
|
||||
- [ ] 考虑使用 Cow 减少克隆
|
||||
|
||||
### 代码质量
|
||||
|
||||
- [ ] cargo clippy 零警告
|
||||
- [ ] cargo fmt 格式化
|
||||
- [ ] 文档注释完整
|
||||
- [ ] 测试覆盖边界条件
|
||||
- [ ] 公共 API 有文档示例
|
||||
@@ -0,0 +1,266 @@
|
||||
# Security Review Guide
|
||||
|
||||
Security-focused code review checklist based on OWASP Top 10 and best practices.
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
### Authentication
|
||||
- [ ] Passwords hashed with strong algorithm (bcrypt, argon2)
|
||||
- [ ] Password complexity requirements enforced
|
||||
- [ ] Account lockout after failed attempts
|
||||
- [ ] Secure password reset flow
|
||||
- [ ] Multi-factor authentication for sensitive operations
|
||||
- [ ] Session tokens are cryptographically random
|
||||
- [ ] Session timeout implemented
|
||||
|
||||
### Authorization
|
||||
- [ ] Authorization checks on every request
|
||||
- [ ] Principle of least privilege applied
|
||||
- [ ] Role-based access control (RBAC) properly implemented
|
||||
- [ ] No privilege escalation paths
|
||||
- [ ] Direct object reference checks (IDOR prevention)
|
||||
- [ ] API endpoints protected appropriately
|
||||
|
||||
### JWT Security
|
||||
```typescript
|
||||
// ❌ Insecure JWT configuration
|
||||
jwt.sign(payload, 'weak-secret');
|
||||
|
||||
// ✅ Secure JWT configuration
|
||||
jwt.sign(payload, process.env.JWT_SECRET, {
|
||||
algorithm: 'RS256',
|
||||
expiresIn: '15m',
|
||||
issuer: 'your-app',
|
||||
audience: 'your-api'
|
||||
});
|
||||
|
||||
// ❌ Not verifying JWT properly
|
||||
const decoded = jwt.decode(token); // No signature verification!
|
||||
|
||||
// ✅ Verify signature and claims
|
||||
const decoded = jwt.verify(token, publicKey, {
|
||||
algorithms: ['RS256'],
|
||||
issuer: 'your-app',
|
||||
audience: 'your-api'
|
||||
});
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
|
||||
### SQL Injection Prevention
|
||||
```python
|
||||
# ❌ Vulnerable to SQL injection
|
||||
query = f"SELECT * FROM users WHERE id = {user_id}"
|
||||
|
||||
# ✅ Use parameterized queries
|
||||
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
||||
|
||||
# ✅ Use ORM with proper escaping
|
||||
User.objects.filter(id=user_id)
|
||||
```
|
||||
|
||||
### XSS Prevention
|
||||
```typescript
|
||||
// ❌ Vulnerable to XSS
|
||||
element.innerHTML = userInput;
|
||||
|
||||
// ✅ Use textContent for plain text
|
||||
element.textContent = userInput;
|
||||
|
||||
// ✅ Use DOMPurify for HTML
|
||||
element.innerHTML = DOMPurify.sanitize(userInput);
|
||||
|
||||
// ✅ React automatically escapes (but watch dangerouslySetInnerHTML)
|
||||
return <div>{userInput}</div>; // Safe
|
||||
return <div dangerouslySetInnerHTML={{__html: userInput}} />; // Dangerous!
|
||||
```
|
||||
|
||||
### Command Injection Prevention
|
||||
```python
|
||||
# ❌ Vulnerable to command injection
|
||||
os.system(f"convert {filename} output.png")
|
||||
|
||||
# ✅ Use subprocess with list arguments
|
||||
subprocess.run(['convert', filename, 'output.png'], check=True)
|
||||
|
||||
# ✅ Validate and sanitize input
|
||||
import shlex
|
||||
safe_filename = shlex.quote(filename)
|
||||
```
|
||||
|
||||
### Path Traversal Prevention
|
||||
```typescript
|
||||
// ❌ Vulnerable to path traversal
|
||||
const filePath = `./uploads/${req.params.filename}`;
|
||||
|
||||
// ✅ Validate and sanitize path
|
||||
const path = require('path');
|
||||
const safeName = path.basename(req.params.filename);
|
||||
const uploadsDir = path.resolve('./uploads');
|
||||
const filePath = path.resolve(uploadsDir, safeName);
|
||||
|
||||
// Verify it's still within uploads directory (both sides absolute)
|
||||
if (!filePath.startsWith(uploadsDir + path.sep)) {
|
||||
throw new Error('Invalid path');
|
||||
}
|
||||
```
|
||||
|
||||
## Data Protection
|
||||
|
||||
### Sensitive Data Handling
|
||||
- [ ] No secrets in source code
|
||||
- [ ] Secrets stored in environment variables or secret manager
|
||||
- [ ] Sensitive data encrypted at rest
|
||||
- [ ] Sensitive data encrypted in transit (HTTPS)
|
||||
- [ ] PII handled according to regulations (GDPR, etc.)
|
||||
- [ ] Sensitive data not logged
|
||||
- [ ] Secure data deletion when required
|
||||
|
||||
### Configuration Security
|
||||
```yaml
|
||||
# ❌ Secrets in config files
|
||||
database:
|
||||
password: "super-secret-password"
|
||||
|
||||
# ✅ Reference environment variables
|
||||
database:
|
||||
password: ${DATABASE_PASSWORD}
|
||||
```
|
||||
|
||||
### Error Messages
|
||||
```typescript
|
||||
// ❌ Leaking sensitive information
|
||||
catch (error) {
|
||||
return res.status(500).json({
|
||||
error: error.stack, // Exposes internal details
|
||||
query: sqlQuery // Exposes database structure
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Generic error messages
|
||||
catch (error) {
|
||||
logger.error('Database error', { error, userId }); // Log internally
|
||||
return res.status(500).json({
|
||||
error: 'An unexpected error occurred'
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Security
|
||||
|
||||
### Rate Limiting
|
||||
- [ ] Rate limiting on all public endpoints
|
||||
- [ ] Stricter limits on authentication endpoints
|
||||
- [ ] Per-user and per-IP limits
|
||||
- [ ] Graceful handling when limits exceeded
|
||||
|
||||
### CORS Configuration
|
||||
```typescript
|
||||
// ❌ Overly permissive CORS
|
||||
app.use(cors({ origin: '*' }));
|
||||
|
||||
// ✅ Restrictive CORS
|
||||
app.use(cors({
|
||||
origin: ['https://your-app.com'],
|
||||
methods: ['GET', 'POST'],
|
||||
credentials: true
|
||||
}));
|
||||
```
|
||||
|
||||
### HTTP Headers
|
||||
```typescript
|
||||
// Security headers to set
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
}
|
||||
},
|
||||
hsts: { maxAge: 31536000, includeSubDomains: true },
|
||||
noSniff: true,
|
||||
xssFilter: true,
|
||||
frameguard: { action: 'deny' }
|
||||
}));
|
||||
```
|
||||
|
||||
## Cryptography
|
||||
|
||||
### Secure Practices
|
||||
- [ ] Using well-established algorithms (AES-256, RSA-2048+)
|
||||
- [ ] Not implementing custom cryptography
|
||||
- [ ] Using cryptographically secure random number generation
|
||||
- [ ] Proper key management and rotation
|
||||
- [ ] Secure key storage (HSM, KMS)
|
||||
|
||||
### Common Mistakes
|
||||
```typescript
|
||||
// ❌ Weak random generation
|
||||
const token = Math.random().toString(36);
|
||||
|
||||
// ✅ Cryptographically secure random
|
||||
const crypto = require('crypto');
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
// ❌ MD5/SHA1 for passwords
|
||||
const hash = crypto.createHash('md5').update(password).digest('hex');
|
||||
|
||||
// ✅ Use bcrypt or argon2
|
||||
const bcrypt = require('bcrypt');
|
||||
const hash = await bcrypt.hash(password, 12);
|
||||
```
|
||||
|
||||
## Dependency Security
|
||||
|
||||
### Checklist
|
||||
- [ ] Dependencies from trusted sources only
|
||||
- [ ] No known vulnerabilities (npm audit, cargo audit)
|
||||
- [ ] Dependencies kept up to date
|
||||
- [ ] Lock files committed (package-lock.json, Cargo.lock)
|
||||
- [ ] Minimal dependency usage
|
||||
- [ ] License compliance verified
|
||||
|
||||
### Audit Commands
|
||||
```bash
|
||||
# Node.js
|
||||
npm audit
|
||||
npm audit fix
|
||||
|
||||
# Python
|
||||
pip-audit
|
||||
safety check
|
||||
|
||||
# Rust
|
||||
cargo audit
|
||||
|
||||
# General
|
||||
snyk test
|
||||
```
|
||||
|
||||
## Logging & Monitoring
|
||||
|
||||
### Secure Logging
|
||||
- [ ] No sensitive data in logs (passwords, tokens, PII)
|
||||
- [ ] Logs protected from tampering
|
||||
- [ ] Appropriate log retention
|
||||
- [ ] Security events logged (login attempts, permission changes)
|
||||
- [ ] Log injection prevented
|
||||
|
||||
```typescript
|
||||
// ❌ Logging sensitive data
|
||||
logger.info(`User login: ${email}, password: ${password}`);
|
||||
|
||||
// ✅ Safe logging
|
||||
logger.info('User login attempt', { email, success: true });
|
||||
```
|
||||
|
||||
## Security Review Severity Levels
|
||||
|
||||
| Severity | Description | Action |
|
||||
|----------|-------------|--------|
|
||||
| **Critical** | Immediate exploitation possible, data breach risk | Block merge, fix immediately |
|
||||
| **High** | Significant vulnerability, requires specific conditions | Block merge, fix before release |
|
||||
| **Medium** | Moderate risk, defense in depth concern | Should fix, can merge with tracking |
|
||||
| **Low** | Minor issue, best practice violation | Nice to fix, non-blocking |
|
||||
| **Info** | Suggestion for improvement | Optional enhancement |
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,932 @@
|
||||
# Swift Code Review Guide
|
||||
|
||||
A code review checklist for modern Swift (5.9+/6), covering SwiftUI, Swift Concurrency, and the Swift API Design Guidelines.
|
||||
|
||||
## Quick Review Checklist
|
||||
|
||||
### Must-Check Items
|
||||
- [ ] Are force-unwraps (`!`) and `try!` avoided in favor of safe unwrapping
|
||||
- [ ] Do closures that capture `self` use `[weak self]` to avoid retain cycles
|
||||
- [ ] Is the value vs reference type choice intentional (struct vs class)
|
||||
- [ ] Are errors propagated with `throws`/`Result` instead of being swallowed
|
||||
- [ ] Are concurrency boundaries data-race-safe (`Sendable`, `@MainActor`, actors)
|
||||
|
||||
### Common Issues
|
||||
- [ ] Fire-and-forget `Task {}` that leaks or is never cancelled
|
||||
- [ ] Wrong SwiftUI property wrapper (`@ObservedObject` where `@StateObject` is needed)
|
||||
- [ ] O(n^2) lookups in loops that could use a `Set` or `Dictionary`
|
||||
- [ ] Implicitly unwrapped optionals (`var x: T!`) outside of IBOutlets
|
||||
- [ ] Over-broad access control (`public`/`open` where `internal` suffices)
|
||||
- [ ] Naming that ignores the Swift API Design Guidelines
|
||||
|
||||
---
|
||||
|
||||
## 1. Optionals and Unwrapping
|
||||
|
||||
### 1.1 Avoid Force-Unwrapping
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: crashes at runtime if nil
|
||||
let name = user.name!
|
||||
let url = URL(string: urlString)!
|
||||
|
||||
// ✅ Correct: bind with guard let / if let
|
||||
guard let name = user.name else {
|
||||
return
|
||||
}
|
||||
|
||||
if let url = URL(string: urlString) {
|
||||
load(url)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 Use Nil-Coalescing for Defaults
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: verbose and crash-prone
|
||||
let count: Int
|
||||
if let c = dictionary["count"] {
|
||||
count = c
|
||||
} else {
|
||||
count = 0
|
||||
}
|
||||
|
||||
// ✅ Correct: nil-coalescing
|
||||
let count = dictionary["count"] ?? 0
|
||||
```
|
||||
|
||||
### 1.3 Prefer guard let for Early Exit
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: deep nesting (pyramid of doom)
|
||||
func process(_ input: String?) {
|
||||
if let input = input {
|
||||
if let value = Int(input) {
|
||||
if value > 0 {
|
||||
handle(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct: guard keeps the happy path unindented
|
||||
func process(_ input: String?) {
|
||||
guard let input,
|
||||
let value = Int(input),
|
||||
value > 0 else {
|
||||
return
|
||||
}
|
||||
handle(value)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 Avoid Implicitly Unwrapped Optionals
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: T! is a hidden force-unwrap on every access
|
||||
class ViewModel {
|
||||
var service: NetworkService!
|
||||
}
|
||||
|
||||
// ✅ Correct: inject a non-optional dependency
|
||||
class ViewModel {
|
||||
private let service: NetworkService
|
||||
|
||||
init(service: NetworkService) {
|
||||
self.service = service
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.5 Use Optional Chaining and map/flatMap
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: manual unwrapping just to transform
|
||||
var initial: String?
|
||||
if let name = user.name {
|
||||
initial = String(name.prefix(1))
|
||||
}
|
||||
|
||||
// ✅ Correct: optional chaining + map
|
||||
let initial = user.name.map { String($0.prefix(1)) }
|
||||
|
||||
// ✅ Correct: flatMap to avoid double optionals
|
||||
let port: Int? = components.port.flatMap { Int(exactly: $0) }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Memory Management and Retain Cycles
|
||||
|
||||
### 2.1 Use [weak self] in Escaping Closures
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: closure strongly captures self, creating a retain cycle
|
||||
class ImageLoader {
|
||||
var onComplete: (() -> Void)?
|
||||
|
||||
func load() {
|
||||
service.fetch { data in
|
||||
self.cache = data // self is retained by the closure
|
||||
self.onComplete?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct: capture self weakly and guard
|
||||
class ImageLoader {
|
||||
var onComplete: (() -> Void)?
|
||||
|
||||
func load() {
|
||||
service.fetch { [weak self] data in
|
||||
guard let self else { return }
|
||||
self.cache = data
|
||||
self.onComplete?()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 weak vs unowned
|
||||
|
||||
```swift
|
||||
// ✅ Use weak when the reference can legitimately become nil
|
||||
class Controller {
|
||||
weak var delegate: ControllerDelegate?
|
||||
}
|
||||
|
||||
// ✅ Use unowned only when the captured object is guaranteed to
|
||||
// outlive the closure (e.g. self owns the closure tightly).
|
||||
// unowned crashes if accessed after deallocation.
|
||||
class Owner {
|
||||
lazy var describe: () -> String = { [unowned self] in
|
||||
self.name
|
||||
}
|
||||
let name = "owner"
|
||||
}
|
||||
|
||||
// ❌ Wrong: unowned on something that can outlive self -> crash
|
||||
networkClient.onResponse = { [unowned self] in self.update() }
|
||||
// Prefer [weak self] here, since onResponse may fire after self is gone.
|
||||
```
|
||||
|
||||
### 2.3 Break Delegate Retain Cycles
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: strong delegate keeps both objects alive forever
|
||||
protocol DataSourceDelegate: AnyObject {}
|
||||
|
||||
class DataSource {
|
||||
var delegate: DataSourceDelegate? // strong by default
|
||||
}
|
||||
|
||||
// ✅ Correct: delegates should be weak (and protocol AnyObject-bound)
|
||||
class DataSource {
|
||||
weak var delegate: DataSourceDelegate?
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 Closures Stored as Properties
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: stored closure captures self strongly -> permanent cycle
|
||||
class Timer {
|
||||
var tick: (() -> Void)!
|
||||
func configure() {
|
||||
tick = { self.count += 1 }
|
||||
}
|
||||
var count = 0
|
||||
}
|
||||
|
||||
// ✅ Correct: weak capture for stored closures referencing self
|
||||
class Timer {
|
||||
var tick: (() -> Void)?
|
||||
func configure() {
|
||||
tick = { [weak self] in self?.count += 1 }
|
||||
}
|
||||
var count = 0
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Value vs Reference Types
|
||||
|
||||
### 3.1 Prefer Structs by Default
|
||||
|
||||
```swift
|
||||
// ✅ Use a struct for data/models with value semantics
|
||||
struct Coordinate {
|
||||
var latitude: Double
|
||||
var longitude: Double
|
||||
}
|
||||
|
||||
// Copies are independent; no shared mutable state, thread-friendly.
|
||||
var a = Coordinate(latitude: 1, longitude: 2)
|
||||
var b = a
|
||||
b.latitude = 99 // a is unchanged
|
||||
```
|
||||
|
||||
### 3.2 Use a Class for Identity or Shared State
|
||||
|
||||
```swift
|
||||
// ✅ Use a class when instances have identity or must be shared/mutated
|
||||
// by reference, or when you need inheritance / Objective-C interop.
|
||||
final class DatabaseConnection {
|
||||
private(set) var isOpen = false
|
||||
func open() { isOpen = true }
|
||||
}
|
||||
|
||||
// Two references point to the same connection.
|
||||
let conn1 = DatabaseConnection()
|
||||
let conn2 = conn1
|
||||
conn1.open()
|
||||
// conn2.isOpen == true
|
||||
```
|
||||
|
||||
### 3.3 Mark Classes final When Not Subclassed
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: open to subclassing unintentionally (slower dispatch, fragile API)
|
||||
class UserViewModel {}
|
||||
|
||||
// ✅ Correct: final enables static dispatch and signals intent
|
||||
final class UserViewModel {}
|
||||
```
|
||||
|
||||
### 3.4 Beware Reference Types Inside Structs
|
||||
|
||||
```swift
|
||||
// ❌ Surprising: struct copy still shares the inner class instance
|
||||
final class Box { var value = 0 }
|
||||
struct Container { var box = Box() }
|
||||
|
||||
var x = Container()
|
||||
var y = x
|
||||
y.box.value = 42 // x.box.value is also 42 (shared reference!)
|
||||
|
||||
// ✅ Correct: use value semantics throughout, or copy on write deliberately
|
||||
struct Container {
|
||||
var value = 0 // plain value type, copies are independent
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Error Handling
|
||||
|
||||
### 4.1 Avoid try! and try?
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: try! crashes on any thrown error
|
||||
let data = try! Data(contentsOf: url)
|
||||
|
||||
// ❌ Often wrong: try? silently discards the error and the cause
|
||||
let data = try? Data(contentsOf: url) // data is nil, you lose "why"
|
||||
|
||||
// ✅ Correct: propagate or handle with do-catch
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
process(data)
|
||||
} catch {
|
||||
log.error("failed to read \(url): \(error)")
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Define Meaningful Error Types
|
||||
|
||||
```swift
|
||||
// ✅ Recommended: an Error enum communicates failure modes precisely
|
||||
enum NetworkError: Error {
|
||||
case invalidURL
|
||||
case unauthorized
|
||||
case server(statusCode: Int)
|
||||
case decoding(underlying: Error)
|
||||
}
|
||||
|
||||
func fetch(_ path: String) throws -> Data {
|
||||
guard let url = URL(string: path) else {
|
||||
throw NetworkError.invalidURL
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Use Result for Stored or Deferred Outcomes
|
||||
|
||||
```swift
|
||||
// ✅ Result is useful at callback boundaries or when storing an outcome
|
||||
func load(completion: @escaping (Result<User, NetworkError>) -> Void) {
|
||||
// completion(.success(user)) or completion(.failure(.unauthorized))
|
||||
}
|
||||
|
||||
// ✅ Convert between Result and throws as needed
|
||||
let user = try result.get()
|
||||
```
|
||||
|
||||
### 4.4 Typed Throws (Swift 6)
|
||||
|
||||
```swift
|
||||
// ✅ Typed throws constrains the error type when it is fully known.
|
||||
// Use it for closed, exhaustive error domains; prefer untyped
|
||||
// `throws` for library APIs that may grow new error cases.
|
||||
func parse(_ raw: String) throws(ParsingError) -> Token {
|
||||
guard let token = Token(raw) else {
|
||||
throw ParsingError.malformed
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
do {
|
||||
let token = try parse(input)
|
||||
} catch {
|
||||
// `error` is statically known to be ParsingError
|
||||
handle(error)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 Don't Catch and Rethrow Without Value
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: catch that adds nothing but obscures the trace
|
||||
do {
|
||||
try work()
|
||||
} catch {
|
||||
throw error // pointless
|
||||
}
|
||||
|
||||
// ✅ Correct: only catch to add context or recover
|
||||
do {
|
||||
try work()
|
||||
} catch {
|
||||
throw AppError.workFailed(underlying: error)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Swift Concurrency
|
||||
|
||||
### 5.1 Prefer async/await Over Nested Callbacks
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: callback pyramid, error handling scattered
|
||||
func loadProfile(completion: @escaping (Result<Profile, Error>) -> Void) {
|
||||
fetchUser { userResult in
|
||||
switch userResult {
|
||||
case .success(let user):
|
||||
fetchAvatar(user) { avatarResult in /* ... */ }
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct: linear async/await
|
||||
func loadProfile() async throws -> Profile {
|
||||
let user = try await fetchUser()
|
||||
let avatar = try await fetchAvatar(user)
|
||||
return Profile(user: user, avatar: avatar)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Use @MainActor for UI State
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: mutating UI state from a background context (data race / crash)
|
||||
func refresh() async {
|
||||
let items = try? await api.load()
|
||||
self.items = items ?? [] // may run off the main thread
|
||||
}
|
||||
|
||||
// ✅ Correct: isolate UI-facing types to the main actor
|
||||
@MainActor
|
||||
final class FeedViewModel: ObservableObject {
|
||||
@Published var items: [Item] = []
|
||||
|
||||
func refresh() async {
|
||||
let loaded = (try? await api.load()) ?? []
|
||||
items = loaded // guaranteed on the main actor
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Protect Mutable State with Actors
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: shared mutable state without synchronization (data race)
|
||||
final class Counter {
|
||||
var value = 0
|
||||
func increment() { value += 1 }
|
||||
}
|
||||
|
||||
// ✅ Correct: an actor serializes access to its mutable state
|
||||
actor Counter {
|
||||
private(set) var value = 0
|
||||
func increment() { value += 1 }
|
||||
}
|
||||
|
||||
let counter = Counter()
|
||||
await counter.increment() // access is awaited and serialized
|
||||
```
|
||||
|
||||
### 5.4 Conform Shared Types to Sendable
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: passing a non-Sendable class across actors (Swift 6 error)
|
||||
final class Config { // mutable, not Sendable
|
||||
var retries = 3
|
||||
}
|
||||
|
||||
// ✅ Correct: make shared types Sendable (immutable value type is ideal)
|
||||
struct Config: Sendable {
|
||||
let retries: Int
|
||||
}
|
||||
|
||||
// ✅ For reference types, use final + immutable stored properties,
|
||||
// or @unchecked Sendable only with manual synchronization.
|
||||
final class Cache: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var storage: [String: Data] = [:]
|
||||
// all access guarded by lock
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5 Handle Task Cancellation
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: ignores cancellation, keeps working after the view is gone
|
||||
func search(_ query: String) async -> [Result] {
|
||||
var results: [Result] = []
|
||||
for page in 0..<100 {
|
||||
results += await fetchPage(query, page) // never stops
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// ✅ Correct: check for cancellation cooperatively
|
||||
func search(_ query: String) async throws -> [Result] {
|
||||
var results: [Result] = []
|
||||
for page in 0..<100 {
|
||||
try Task.checkCancellation()
|
||||
results += try await fetchPage(query, page)
|
||||
}
|
||||
return results
|
||||
}
|
||||
```
|
||||
|
||||
### 5.6 Don't Leak Fire-and-Forget Tasks
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: unstructured Task with no handle, never cancelled
|
||||
final class ViewModel {
|
||||
func onAppear() {
|
||||
Task {
|
||||
await self.stream() // runs forever even after dismissal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct: retain the handle and cancel it (or use .task in SwiftUI)
|
||||
final class ViewModel {
|
||||
private var streamTask: Task<Void, Never>?
|
||||
|
||||
func onAppear() {
|
||||
streamTask = Task { [weak self] in
|
||||
await self?.stream()
|
||||
}
|
||||
}
|
||||
|
||||
func onDisappear() {
|
||||
streamTask?.cancel()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.7 Use Structured Concurrency for Parallelism
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: sequential awaits where work could run concurrently
|
||||
let a = await loadA()
|
||||
let b = await loadB() // waits for A to finish first
|
||||
|
||||
// ✅ Correct: async let runs them concurrently
|
||||
async let a = loadA()
|
||||
async let b = loadB()
|
||||
let (resultA, resultB) = await (a, b)
|
||||
|
||||
// ✅ For a dynamic number of children, use a task group
|
||||
try await withThrowingTaskGroup(of: Item.self) { group in
|
||||
for id in ids {
|
||||
group.addTask { try await fetch(id) }
|
||||
}
|
||||
for try await item in group {
|
||||
store(item)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. SwiftUI
|
||||
|
||||
### 6.1 Choose the Right State Wrapper
|
||||
|
||||
```swift
|
||||
// ✅ @State: simple value-type state owned by this view
|
||||
struct Toggle: View {
|
||||
@State private var isOn = false
|
||||
var body: some View { /* ... */ }
|
||||
}
|
||||
|
||||
// ✅ @StateObject: the view CREATES and OWNS a reference-type model
|
||||
struct ProfileScreen: View {
|
||||
@StateObject private var model = ProfileViewModel()
|
||||
var body: some View { /* ... */ }
|
||||
}
|
||||
|
||||
// ✅ @ObservedObject: the model is OWNED elsewhere and passed in
|
||||
struct ProfileHeader: View {
|
||||
@ObservedObject var model: ProfileViewModel
|
||||
var body: some View { /* ... */ }
|
||||
}
|
||||
|
||||
// ✅ @Binding: a two-way reference to state owned by a parent
|
||||
struct SearchField: View {
|
||||
@Binding var text: String
|
||||
var body: some View { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 @StateObject vs @ObservedObject
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: @ObservedObject for an object the view itself creates.
|
||||
// SwiftUI may recreate the view, re-instantiating the model and
|
||||
// losing its state on every re-render.
|
||||
struct CounterView: View {
|
||||
@ObservedObject var model = CounterModel() // recreated unexpectedly
|
||||
}
|
||||
|
||||
// ✅ Correct: @StateObject ties the model's lifetime to the view
|
||||
struct CounterView: View {
|
||||
@StateObject private var model = CounterModel()
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 Preserve View Identity
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: index-based id reuses identity when the array reorders,
|
||||
// causing wrong animations and stale state.
|
||||
ForEach(0..<items.count, id: \.self) { i in
|
||||
ItemRow(item: items[i])
|
||||
}
|
||||
|
||||
// ✅ Correct: use a stable, unique identifier
|
||||
ForEach(items) { item in // Item: Identifiable
|
||||
ItemRow(item: item)
|
||||
}
|
||||
|
||||
// ✅ Use .id(...) to deliberately reset a view's state
|
||||
ProfileView(user: user)
|
||||
.id(user.id) // new identity per user -> fresh state
|
||||
```
|
||||
|
||||
### 6.4 Avoid Over-Rendering
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: a single huge body re-renders everything on any change
|
||||
struct Dashboard: View {
|
||||
@ObservedObject var model: DashboardModel
|
||||
var body: some View {
|
||||
VStack {
|
||||
// header + heavy chart + list all recompute together
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct: extract subviews so only the affected part re-renders.
|
||||
// Each child observes only the state it needs.
|
||||
struct Dashboard: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
HeaderView()
|
||||
ChartView()
|
||||
ItemList()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 Do Async Work with .task
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: kicking off work in onAppear without cancellation
|
||||
.onAppear {
|
||||
Task { await model.load() } // not cancelled when view disappears
|
||||
}
|
||||
|
||||
// ✅ Correct: .task is tied to the view's lifetime and auto-cancels
|
||||
.task {
|
||||
await model.load()
|
||||
}
|
||||
|
||||
// ✅ Re-run when an input changes
|
||||
.task(id: query) {
|
||||
await model.search(query)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Protocols and Generics
|
||||
|
||||
### 7.1 Protocol-Oriented Design
|
||||
|
||||
```swift
|
||||
// ✅ Compose behavior with protocols and default implementations
|
||||
protocol Identifiable2 {
|
||||
var id: String { get }
|
||||
}
|
||||
|
||||
protocol Describable {
|
||||
var description: String { get }
|
||||
}
|
||||
|
||||
extension Describable {
|
||||
var description: String { "no description" } // default
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 Prefer some Over any
|
||||
|
||||
```swift
|
||||
// ❌ Slower: `any` is an existential box with dynamic dispatch
|
||||
func makeShape() -> any Shape { Circle() }
|
||||
|
||||
// ✅ Faster: `some` is an opaque type resolved at compile time,
|
||||
// preserving the concrete type and enabling static dispatch.
|
||||
func makeShape() -> some Shape { Circle() }
|
||||
|
||||
// Use `any` only when you genuinely need heterogeneous values:
|
||||
let shapes: [any Shape] = [Circle(), Square()]
|
||||
```
|
||||
|
||||
### 7.3 Generic Constraints Over Existentials
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: existential parameter loses the concrete type and is slower
|
||||
func logTotal(_ items: [any Numeric]) {
|
||||
// awkward: the concrete numeric type is erased, so arithmetic needs casts
|
||||
}
|
||||
|
||||
// ✅ Correct: a generic constraint keeps full type information
|
||||
func total<T: Numeric>(_ items: [T]) -> T {
|
||||
items.reduce(.zero, +)
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 Associated Types with Primary Associated Types
|
||||
|
||||
```swift
|
||||
// ✅ Primary associated types (Swift 5.7+) allow lightweight constraints
|
||||
protocol Container<Item> {
|
||||
associatedtype Item
|
||||
var count: Int { get }
|
||||
subscript(_ index: Int) -> Item { get }
|
||||
}
|
||||
|
||||
// Constrain the element type without a where-clause:
|
||||
func first(in container: some Container<Int>) -> Int {
|
||||
container[0]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Access Control and API Design
|
||||
|
||||
### 8.1 Use the Narrowest Access Level
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: everything public exposes internal details as API surface
|
||||
public class Service {
|
||||
public var cache: [String: Data] = [:]
|
||||
public func reset() {}
|
||||
}
|
||||
|
||||
// ✅ Correct: expose only the intended API; hide the rest
|
||||
public final class Service {
|
||||
private var cache: [String: Data] = [:]
|
||||
public func reset() { cache.removeAll() }
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 private vs fileprivate vs internal vs public/open
|
||||
|
||||
```swift
|
||||
// private: visible only within the enclosing declaration (and its extensions in the same file)
|
||||
// fileprivate: visible within the same source file
|
||||
// internal: visible within the module (the default)
|
||||
// public: visible outside the module, but not subclassable/overridable
|
||||
// open: visible outside the module AND subclassable/overridable
|
||||
|
||||
// ✅ Use private(set) to expose read-only state
|
||||
public final class Account {
|
||||
public private(set) var balance: Decimal = 0
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 Follow the Swift API Design Guidelines
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: redundant words, unclear argument roles
|
||||
func insertObject(_ object: Element, atIndex index: Int)
|
||||
list.removeElement(at: 0)
|
||||
|
||||
// ✅ Correct: read at the call site like a phrase; omit needless words
|
||||
func insert(_ element: Element, at index: Int)
|
||||
list.insert(item, at: 0) // reads as "insert item at 0"
|
||||
list.remove(at: 0)
|
||||
|
||||
// ✅ Boolean properties read as assertions
|
||||
var isEmpty: Bool
|
||||
var hasChanges: Bool
|
||||
```
|
||||
|
||||
### 8.4 Name Methods by Side Effects
|
||||
|
||||
```swift
|
||||
// ✅ Mutating verb vs non-mutating noun pairs (the "ed/ing" rule)
|
||||
var sorted = array.sorted() // returns a new value (non-mutating)
|
||||
array.sort() // mutates in place (imperative verb)
|
||||
|
||||
let reversed = text.reversed()
|
||||
text.reverse()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Collections and Functional Style
|
||||
|
||||
### 9.1 Prefer map/filter/compactMap
|
||||
|
||||
```swift
|
||||
// ❌ Verbose: manual loop with mutable accumulator
|
||||
var names: [String] = []
|
||||
for user in users {
|
||||
if user.isActive {
|
||||
names.append(user.name)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct: declarative transform
|
||||
let names = users.filter(\.isActive).map(\.name)
|
||||
```
|
||||
|
||||
### 9.2 compactMap to Drop nils
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: map leaves an [Int?] you then have to unwrap
|
||||
let numbers = strings.map { Int($0) } // [Int?]
|
||||
|
||||
// ✅ Correct: compactMap removes nils and unwraps
|
||||
let numbers = strings.compactMap { Int($0) } // [Int]
|
||||
```
|
||||
|
||||
### 9.3 Avoid O(n^2) Membership Checks
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: contains on an Array is O(n); the loop is O(n*m)
|
||||
let result = candidates.filter { blocked.contains($0) } // blocked: [ID]
|
||||
|
||||
// ✅ Correct: a Set makes membership O(1)
|
||||
let blockedSet = Set(blocked)
|
||||
let result = candidates.filter { blockedSet.contains($0) }
|
||||
```
|
||||
|
||||
### 9.4 reduce and Dictionary Grouping
|
||||
|
||||
```swift
|
||||
// ✅ Group with Dictionary(grouping:)
|
||||
let byFirstLetter = Dictionary(grouping: words) { $0.first }
|
||||
|
||||
// ❌ Wrong: reduce(into:) is preferred over reduce that copies each step
|
||||
let total = numbers.reduce(0) { $0 + $1 } // fine for scalars
|
||||
|
||||
// ✅ Use reduce(into:) when accumulating into a collection (avoids copies)
|
||||
let counts = words.reduce(into: [:]) { acc, word in
|
||||
acc[word, default: 0] += 1
|
||||
}
|
||||
```
|
||||
|
||||
### 9.5 Use lazy for Chained Transforms on Large Sequences
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: each step allocates an intermediate array
|
||||
let firstMatch = bigArray.map(expensive).filter(isValid).first
|
||||
|
||||
// ✅ Correct: lazy avoids intermediate arrays and stops early
|
||||
let firstMatch = bigArray.lazy.map(expensive).filter(isValid).first
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Testing
|
||||
|
||||
### 10.1 Arrange-Act-Assert with XCTest
|
||||
|
||||
```swift
|
||||
import XCTest
|
||||
@testable import MyApp
|
||||
|
||||
final class PriceCalculatorTests: XCTestCase {
|
||||
func testDiscountApplied() {
|
||||
// Arrange
|
||||
let calculator = PriceCalculator(discount: 0.1)
|
||||
// Act
|
||||
let total = calculator.total(for: 100)
|
||||
// Assert
|
||||
XCTAssertEqual(total, 90, accuracy: 0.001)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 Testing async Code
|
||||
|
||||
```swift
|
||||
// ✅ Mark the test method async and await directly
|
||||
func testFetchUser() async throws {
|
||||
let service = UserService(client: MockClient())
|
||||
let user = try await service.fetchUser(id: "42")
|
||||
XCTAssertEqual(user.id, "42")
|
||||
}
|
||||
|
||||
// ✅ Assert that an async call throws the expected error
|
||||
func testFetchUserUnauthorized() async {
|
||||
let service = UserService(client: UnauthorizedClient())
|
||||
do {
|
||||
_ = try await service.fetchUser(id: "42")
|
||||
XCTFail("expected to throw")
|
||||
} catch NetworkError.unauthorized {
|
||||
// expected
|
||||
} catch {
|
||||
XCTFail("unexpected error: \(error)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.3 Inject Dependencies via Protocols
|
||||
|
||||
```swift
|
||||
// ✅ Depend on a protocol so tests can substitute a mock
|
||||
protocol HTTPClient {
|
||||
func get(_ url: URL) async throws -> Data
|
||||
}
|
||||
|
||||
struct MockClient: HTTPClient {
|
||||
var result: Result<Data, Error>
|
||||
func get(_ url: URL) async throws -> Data {
|
||||
try result.get()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.4 Avoid Sleeps; Await Expectations or Values
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: arbitrary sleep makes tests slow and flaky
|
||||
func testCallback() {
|
||||
var done = false
|
||||
object.run { done = true }
|
||||
Thread.sleep(forTimeInterval: 1)
|
||||
XCTAssertTrue(done)
|
||||
}
|
||||
|
||||
// ✅ Correct: use XCTestExpectation for callback APIs
|
||||
func testCallback() {
|
||||
let expectation = expectation(description: "callback fired")
|
||||
object.run { expectation.fulfill() }
|
||||
wait(for: [expectation], timeout: 1.0)
|
||||
}
|
||||
|
||||
// ✅ Better: refactor to async and await the value directly
|
||||
func testCallback() async {
|
||||
let value = await object.run()
|
||||
XCTAssertEqual(value, expected)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/)
|
||||
- [The Swift Programming Language](https://docs.swift.org/swift-book/)
|
||||
- [Swift Concurrency (TSPL)](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/)
|
||||
- [Migrating to Swift 6](https://www.swift.org/migration/documentation/migrationguide/)
|
||||
- [Apple: Managing Model Data in Your App (SwiftUI)](https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app)
|
||||
- [Apple: Automatic Reference Counting](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting/)
|
||||
- [WWDC: Protocol-Oriented Programming in Swift](https://developer.apple.com/videos/play/wwdc2015/408/)
|
||||
- [Swift Evolution](https://github.com/apple/swift-evolution)
|
||||
@@ -0,0 +1,553 @@
|
||||
# TypeScript/JavaScript Code Review Guide
|
||||
|
||||
> TypeScript 代码审查指南,覆盖类型系统、泛型、条件类型、strict 模式、async/await 模式等核心主题。
|
||||
|
||||
## 目录
|
||||
|
||||
- [类型安全基础](#类型安全基础)
|
||||
- [泛型模式](#泛型模式)
|
||||
- [高级类型](#高级类型)
|
||||
- [Strict 模式配置](#strict-模式配置)
|
||||
- [异步处理](#异步处理)
|
||||
- [不可变性](#不可变性)
|
||||
- [ESLint 规则](#eslint-规则)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 类型安全基础
|
||||
|
||||
### 避免使用 any
|
||||
|
||||
```typescript
|
||||
// ❌ Using any defeats type safety
|
||||
function processData(data: any) {
|
||||
return data.value; // 无类型检查,运行时可能崩溃
|
||||
}
|
||||
|
||||
// ✅ Use proper types
|
||||
interface DataPayload {
|
||||
value: string;
|
||||
}
|
||||
function processData(data: DataPayload) {
|
||||
return data.value;
|
||||
}
|
||||
|
||||
// ✅ 未知类型用 unknown + 类型守卫
|
||||
function processUnknown(data: unknown) {
|
||||
if (typeof data === 'object' && data !== null && 'value' in data) {
|
||||
return (data as { value: string }).value;
|
||||
}
|
||||
throw new Error('Invalid data');
|
||||
}
|
||||
```
|
||||
|
||||
### 类型收窄
|
||||
|
||||
```typescript
|
||||
// ❌ 不安全的类型断言
|
||||
function getLength(value: string | string[]) {
|
||||
return (value as string[]).length; // 如果是 string 会出错
|
||||
}
|
||||
|
||||
// ✅ 使用类型守卫
|
||||
function getLength(value: string | string[]): number {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length;
|
||||
}
|
||||
return value.length;
|
||||
}
|
||||
|
||||
// ✅ 使用 in 操作符
|
||||
interface Dog { bark(): void }
|
||||
interface Cat { meow(): void }
|
||||
|
||||
function speak(animal: Dog | Cat) {
|
||||
if ('bark' in animal) {
|
||||
animal.bark();
|
||||
} else {
|
||||
animal.meow();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 字面量类型与 as const
|
||||
|
||||
```typescript
|
||||
// ❌ 类型过于宽泛
|
||||
const config = {
|
||||
endpoint: '/api',
|
||||
method: 'GET' // 类型是 string
|
||||
};
|
||||
|
||||
// ✅ 使用 as const 获得字面量类型
|
||||
const config = {
|
||||
endpoint: '/api',
|
||||
method: 'GET'
|
||||
} as const; // method 类型是 'GET'
|
||||
|
||||
// ✅ 用于函数参数
|
||||
function request(method: 'GET' | 'POST', url: string) { ... }
|
||||
request(config.method, config.endpoint); // 正确!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 泛型模式
|
||||
|
||||
### 基础泛型
|
||||
|
||||
```typescript
|
||||
// ❌ 重复代码
|
||||
function getFirstString(arr: string[]): string | undefined {
|
||||
return arr[0];
|
||||
}
|
||||
function getFirstNumber(arr: number[]): number | undefined {
|
||||
return arr[0];
|
||||
}
|
||||
|
||||
// ✅ 使用泛型
|
||||
function getFirst<T>(arr: T[]): T | undefined {
|
||||
return arr[0];
|
||||
}
|
||||
```
|
||||
|
||||
### 泛型约束
|
||||
|
||||
```typescript
|
||||
// ❌ 泛型没有约束,无法访问属性
|
||||
function getProperty<T>(obj: T, key: string) {
|
||||
return obj[key]; // Error: 无法索引
|
||||
}
|
||||
|
||||
// ✅ 使用 keyof 约束
|
||||
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
|
||||
return obj[key];
|
||||
}
|
||||
|
||||
const user = { name: 'Alice', age: 30 };
|
||||
getProperty(user, 'name'); // 返回类型是 string
|
||||
getProperty(user, 'age'); // 返回类型是 number
|
||||
getProperty(user, 'foo'); // Error: 'foo' 不在 keyof User
|
||||
```
|
||||
|
||||
### 泛型默认值
|
||||
|
||||
```typescript
|
||||
// ✅ 提供合理的默认类型
|
||||
interface ApiResponse<T = unknown> {
|
||||
data: T;
|
||||
status: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 可以不指定泛型参数
|
||||
const response: ApiResponse = { data: null, status: 200, message: 'OK' };
|
||||
// 也可以指定
|
||||
const userResponse: ApiResponse<User> = { ... };
|
||||
```
|
||||
|
||||
### 常见泛型工具类型
|
||||
|
||||
```typescript
|
||||
// ✅ 善用内置工具类型
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
type PartialUser = Partial<User>; // 所有属性可选
|
||||
type RequiredUser = Required<User>; // 所有属性必需
|
||||
type ReadonlyUser = Readonly<User>; // 所有属性只读
|
||||
type UserKeys = keyof User; // 'id' | 'name' | 'email'
|
||||
type NameOnly = Pick<User, 'name'>; // { name: string }
|
||||
type WithoutId = Omit<User, 'id'>; // { name: string; email: string }
|
||||
type UserRecord = Record<string, User>; // { [key: string]: User }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 高级类型
|
||||
|
||||
### 条件类型
|
||||
|
||||
```typescript
|
||||
// ✅ 根据输入类型返回不同类型
|
||||
type IsString<T> = T extends string ? true : false;
|
||||
|
||||
type A = IsString<string>; // true
|
||||
type B = IsString<number>; // false
|
||||
|
||||
// ✅ 提取数组元素类型
|
||||
type ElementType<T> = T extends (infer U)[] ? U : never;
|
||||
|
||||
type Elem = ElementType<string[]>; // string
|
||||
|
||||
// ✅ 提取函数返回类型(内置 ReturnType)
|
||||
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
|
||||
```
|
||||
|
||||
### 映射类型
|
||||
|
||||
```typescript
|
||||
// ✅ 转换对象类型的所有属性
|
||||
type Nullable<T> = {
|
||||
[K in keyof T]: T[K] | null;
|
||||
};
|
||||
|
||||
interface User {
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
type NullableUser = Nullable<User>;
|
||||
// { name: string | null; age: number | null }
|
||||
|
||||
// ✅ 添加前缀
|
||||
type Getters<T> = {
|
||||
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
|
||||
};
|
||||
|
||||
type UserGetters = Getters<User>;
|
||||
// { getName: () => string; getAge: () => number }
|
||||
```
|
||||
|
||||
### 模板字面量类型
|
||||
|
||||
```typescript
|
||||
// ✅ 类型安全的事件名称
|
||||
type EventName = 'click' | 'focus' | 'blur';
|
||||
type HandlerName = `on${Capitalize<EventName>}`;
|
||||
// 'onClick' | 'onFocus' | 'onBlur'
|
||||
|
||||
// ✅ API 路由类型
|
||||
type ApiRoute = `/api/${string}`;
|
||||
const route: ApiRoute = '/api/users'; // OK
|
||||
const badRoute: ApiRoute = '/users'; // Error
|
||||
```
|
||||
|
||||
### Discriminated Unions
|
||||
|
||||
```typescript
|
||||
// ✅ 使用判别属性实现类型安全
|
||||
type Result<T, E> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: E };
|
||||
|
||||
function handleResult(result: Result<User, Error>) {
|
||||
if (result.success) {
|
||||
console.log(result.data.name); // TypeScript 知道 data 存在
|
||||
} else {
|
||||
console.log(result.error.message); // TypeScript 知道 error 存在
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Redux Action 模式
|
||||
type Action =
|
||||
| { type: 'INCREMENT'; payload: number }
|
||||
| { type: 'DECREMENT'; payload: number }
|
||||
| { type: 'RESET' };
|
||||
|
||||
function reducer(state: number, action: Action): number {
|
||||
switch (action.type) {
|
||||
case 'INCREMENT':
|
||||
return state + action.payload; // payload 类型已知
|
||||
case 'DECREMENT':
|
||||
return state - action.payload;
|
||||
case 'RESET':
|
||||
return 0; // 这里没有 payload
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Strict 模式配置
|
||||
|
||||
### 推荐的 tsconfig.json
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
// ✅ 必须开启的 strict 选项
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"useUnknownInCatchVariables": true,
|
||||
|
||||
// ✅ 额外推荐选项
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noPropertyAccessFromIndexSignature": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### noUncheckedIndexedAccess 的影响
|
||||
|
||||
```typescript
|
||||
// tsconfig: "noUncheckedIndexedAccess": true
|
||||
|
||||
const arr = [1, 2, 3];
|
||||
const first = arr[0]; // 类型是 number | undefined
|
||||
|
||||
// ❌ 直接使用可能出错
|
||||
console.log(first.toFixed(2)); // Error: 可能是 undefined
|
||||
|
||||
// ✅ 先检查
|
||||
if (first !== undefined) {
|
||||
console.log(first.toFixed(2));
|
||||
}
|
||||
|
||||
// ✅ 或使用非空断言(确定时)
|
||||
console.log(arr[0]!.toFixed(2));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 异步处理
|
||||
|
||||
### Promise 错误处理
|
||||
|
||||
```typescript
|
||||
// ❌ Not handling async errors
|
||||
async function fetchUser(id: string) {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
return response.json(); // 网络错误未处理
|
||||
}
|
||||
|
||||
// ✅ Handle errors properly
|
||||
async function fetchUser(id: string): Promise<User> {
|
||||
try {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to fetch user: ${error.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Promise.all vs Promise.allSettled
|
||||
|
||||
```typescript
|
||||
// ❌ Promise.all 一个失败全部失败
|
||||
async function fetchAllUsers(ids: string[]) {
|
||||
const users = await Promise.all(ids.map(fetchUser));
|
||||
return users; // 一个失败就全部失败
|
||||
}
|
||||
|
||||
// ✅ Promise.allSettled 获取所有结果
|
||||
async function fetchAllUsers(ids: string[]) {
|
||||
const results = await Promise.allSettled(ids.map(fetchUser));
|
||||
|
||||
const users: User[] = [];
|
||||
const errors: Error[] = [];
|
||||
|
||||
for (const result of results) {
|
||||
if (result.status === 'fulfilled') {
|
||||
users.push(result.value);
|
||||
} else {
|
||||
errors.push(result.reason);
|
||||
}
|
||||
}
|
||||
|
||||
return { users, errors };
|
||||
}
|
||||
```
|
||||
|
||||
### 竞态条件处理
|
||||
|
||||
```typescript
|
||||
// ❌ 竞态条件:旧请求可能覆盖新请求
|
||||
function useSearch() {
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/search?q=${query}`)
|
||||
.then(r => r.json())
|
||||
.then(setResults); // 旧请求可能后返回!
|
||||
}, [query]);
|
||||
}
|
||||
|
||||
// ✅ 使用 AbortController
|
||||
function useSearch() {
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
fetch(`/api/search?q=${query}`, { signal: controller.signal })
|
||||
.then(r => r.json())
|
||||
.then(setResults)
|
||||
.catch(e => {
|
||||
if (e.name !== 'AbortError') throw e;
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [query]);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 不可变性
|
||||
|
||||
### Readonly 与 ReadonlyArray
|
||||
|
||||
```typescript
|
||||
// ❌ 可变参数可能被意外修改
|
||||
function processUsers(users: User[]) {
|
||||
users.sort((a, b) => a.name.localeCompare(b.name)); // 修改了原数组!
|
||||
return users;
|
||||
}
|
||||
|
||||
// ✅ 使用 readonly 防止修改
|
||||
function processUsers(users: readonly User[]): User[] {
|
||||
return [...users].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
// ✅ 深度只读
|
||||
type DeepReadonly<T> = {
|
||||
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
|
||||
};
|
||||
```
|
||||
|
||||
### 不变式函数参数
|
||||
|
||||
```typescript
|
||||
// ✅ 使用 as const 和 readonly 保护数据
|
||||
function createConfig<T extends readonly string[]>(routes: T) {
|
||||
return routes;
|
||||
}
|
||||
|
||||
const routes = createConfig(['home', 'about', 'contact'] as const);
|
||||
// 类型是 readonly ['home', 'about', 'contact']
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ESLint 规则
|
||||
|
||||
### 推荐的 @typescript-eslint 规则
|
||||
|
||||
```javascript
|
||||
// eslint.config.js(flat config,typescript-eslint v8)
|
||||
import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
// 需要类型信息的规则集,对应旧的 recommended-requiring-type-checking
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
tseslint.configs.strictTypeChecked,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
// 让带类型的规则自动找到对应 tsconfig
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// ✅ 类型安全
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'error',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'error',
|
||||
'@typescript-eslint/no-unsafe-call': 'error',
|
||||
'@typescript-eslint/no-unsafe-return': 'error',
|
||||
|
||||
// ✅ 最佳实践
|
||||
'@typescript-eslint/explicit-function-return-type': 'warn',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/await-thenable': 'error',
|
||||
'@typescript-eslint/no-misused-promises': 'error',
|
||||
|
||||
// ✅ 代码风格
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'error',
|
||||
'@typescript-eslint/prefer-optional-chain': 'error',
|
||||
},
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### 常见 ESLint 错误修复
|
||||
|
||||
```typescript
|
||||
// ❌ no-floating-promises: Promise 必须被处理
|
||||
async function save() { ... }
|
||||
save(); // Error: 未处理的 Promise
|
||||
|
||||
// ✅ 显式处理
|
||||
await save();
|
||||
// 或
|
||||
save().catch(console.error);
|
||||
// 或明确忽略
|
||||
void save();
|
||||
|
||||
// ❌ no-misused-promises: 不能在非 async 位置使用 Promise
|
||||
const items = [1, 2, 3];
|
||||
items.forEach(async (item) => { // Error!
|
||||
await processItem(item);
|
||||
});
|
||||
|
||||
// ✅ 使用 for...of
|
||||
for (const item of items) {
|
||||
await processItem(item);
|
||||
}
|
||||
// 或 Promise.all
|
||||
await Promise.all(items.map(processItem));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### 类型系统
|
||||
- [ ] 没有使用 `any`(使用 `unknown` + 类型守卫代替)
|
||||
- [ ] 接口和类型定义完整且有意义的命名
|
||||
- [ ] 使用泛型提高代码复用性
|
||||
- [ ] 联合类型有正确的类型收窄
|
||||
- [ ] 善用工具类型(Partial、Pick、Omit 等)
|
||||
|
||||
### 泛型
|
||||
- [ ] 泛型有适当的约束(extends)
|
||||
- [ ] 泛型参数有合理的默认值
|
||||
- [ ] 避免过度泛型化(KISS 原则)
|
||||
|
||||
### Strict 模式
|
||||
- [ ] tsconfig.json 启用了 strict: true
|
||||
- [ ] 启用了 noUncheckedIndexedAccess
|
||||
- [ ] 没有使用 @ts-ignore(改用 @ts-expect-error)
|
||||
|
||||
### 异步代码
|
||||
- [ ] async 函数有错误处理
|
||||
- [ ] Promise rejection 被正确处理
|
||||
- [ ] 没有 floating promises(未处理的 Promise)
|
||||
- [ ] 并发请求使用 Promise.all 或 Promise.allSettled
|
||||
- [ ] 竞态条件使用 AbortController 处理
|
||||
|
||||
### 不可变性
|
||||
- [ ] 不直接修改函数参数
|
||||
- [ ] 使用 spread 操作符创建新对象/数组
|
||||
- [ ] 考虑使用 readonly 修饰符
|
||||
|
||||
### ESLint
|
||||
- [ ] 使用 @typescript-eslint/recommended
|
||||
- [ ] 没有 ESLint 警告或错误
|
||||
- [ ] 使用 consistent-type-imports
|
||||
@@ -0,0 +1,924 @@
|
||||
# Vue 3 Code Review Guide
|
||||
|
||||
> Vue 3 Composition API 代码审查指南,覆盖响应性系统、Props/Emits、Watchers、Composables、Vue 3.5 新特性等核心主题。
|
||||
|
||||
## 目录
|
||||
|
||||
- [响应性系统](#响应性系统)
|
||||
- [Props & Emits](#props--emits)
|
||||
- [Vue 3.5 新特性](#vue-35-新特性)
|
||||
- [Watchers](#watchers)
|
||||
- [模板最佳实践](#模板最佳实践)
|
||||
- [Composables](#composables)
|
||||
- [性能优化](#性能优化)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 响应性系统
|
||||
|
||||
### ref vs reactive 选择
|
||||
|
||||
```vue
|
||||
<!-- ✅ 基本类型用 ref -->
|
||||
<script setup lang="ts">
|
||||
const count = ref(0)
|
||||
const name = ref('Vue')
|
||||
|
||||
// ref 需要 .value 访问
|
||||
count.value++
|
||||
</script>
|
||||
|
||||
<!-- ✅ 对象/数组用 reactive(可选)-->
|
||||
<script setup lang="ts">
|
||||
const state = reactive({
|
||||
user: null,
|
||||
loading: false,
|
||||
error: null
|
||||
})
|
||||
|
||||
// reactive 直接访问
|
||||
state.loading = true
|
||||
</script>
|
||||
|
||||
<!-- 💡 现代最佳实践:全部使用 ref,保持一致性 -->
|
||||
<script setup lang="ts">
|
||||
const user = ref<User | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<Error | null>(null)
|
||||
</script>
|
||||
```
|
||||
|
||||
### 解构 reactive 对象
|
||||
|
||||
```vue
|
||||
<!-- ❌ 解构 reactive 会丢失响应性 -->
|
||||
<script setup lang="ts">
|
||||
const state = reactive({ count: 0, name: 'Vue' })
|
||||
const { count, name } = state // 丢失响应性!
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 toRefs 保持响应性 -->
|
||||
<script setup lang="ts">
|
||||
const state = reactive({ count: 0, name: 'Vue' })
|
||||
const { count, name } = toRefs(state) // 保持响应性
|
||||
// 或者直接使用 ref
|
||||
const count = ref(0)
|
||||
const name = ref('Vue')
|
||||
</script>
|
||||
```
|
||||
|
||||
### computed 副作用
|
||||
|
||||
```vue
|
||||
<!-- ❌ computed 中产生副作用 -->
|
||||
<script setup lang="ts">
|
||||
const fullName = computed(() => {
|
||||
console.log('Computing...') // 副作用!
|
||||
otherRef.value = 'changed' // 修改其他状态!
|
||||
return `${firstName.value} ${lastName.value}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ computed 只用于派生状态 -->
|
||||
<script setup lang="ts">
|
||||
const fullName = computed(() => {
|
||||
return `${firstName.value} ${lastName.value}`
|
||||
})
|
||||
// 副作用放在 watch 或事件处理中
|
||||
watch(fullName, (name) => {
|
||||
console.log('Name changed:', name)
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### shallowRef 优化
|
||||
|
||||
```vue
|
||||
<!-- ❌ 大型对象使用 ref 会深度转换 -->
|
||||
<script setup lang="ts">
|
||||
const largeData = ref(hugeNestedObject) // 深度响应式,性能开销大
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 shallowRef 避免深度转换 -->
|
||||
<script setup lang="ts">
|
||||
const largeData = shallowRef(hugeNestedObject)
|
||||
|
||||
// 整体替换才会触发更新
|
||||
function updateData(newData) {
|
||||
largeData.value = newData // ✅ 触发更新
|
||||
}
|
||||
|
||||
// ❌ 修改嵌套属性不会触发更新
|
||||
// largeData.value.nested.prop = 'new'
|
||||
|
||||
// 需要手动触发时使用 triggerRef
|
||||
import { triggerRef } from 'vue'
|
||||
largeData.value.nested.prop = 'new'
|
||||
triggerRef(largeData)
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Props & Emits
|
||||
|
||||
### 直接修改 props
|
||||
|
||||
```vue
|
||||
<!-- ❌ 直接修改 props -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ user: User }>()
|
||||
props.user.name = 'New Name' // 永远不要直接修改 props!
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 emit 通知父组件更新 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ user: User }>()
|
||||
const emit = defineEmits<{
|
||||
update: [name: string]
|
||||
}>()
|
||||
const updateName = (name: string) => emit('update', name)
|
||||
</script>
|
||||
```
|
||||
|
||||
### defineProps 类型声明
|
||||
|
||||
```vue
|
||||
<!-- ❌ defineProps 缺少类型声明 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps(['title', 'count']) // 无类型检查
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用类型声明 + withDefaults -->
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title: string
|
||||
count?: number
|
||||
items?: string[]
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
count: 0,
|
||||
items: () => [] // 对象/数组默认值需要工厂函数
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### defineEmits 类型安全
|
||||
|
||||
```vue
|
||||
<!-- ❌ defineEmits 缺少类型 -->
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits(['update', 'delete']) // 无类型检查
|
||||
emit('update', someValue) // 参数类型不安全
|
||||
</script>
|
||||
|
||||
<!-- ✅ 完整的类型定义 -->
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits<{
|
||||
update: [id: number, value: string]
|
||||
delete: [id: number]
|
||||
'custom-event': [payload: CustomPayload]
|
||||
}>()
|
||||
|
||||
// 现在有完整的类型检查
|
||||
emit('update', 1, 'new value') // ✅
|
||||
emit('update', 'wrong') // ❌ TypeScript 报错
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Vue 3.5 新特性
|
||||
|
||||
### Reactive Props Destructure (3.5+)
|
||||
|
||||
```vue
|
||||
<!-- Vue 3.5 之前:解构会丢失响应性 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ count: number }>()
|
||||
// 需要使用 props.count 或 toRefs
|
||||
</script>
|
||||
|
||||
<!-- ✅ Vue 3.5+:解构保持响应性 -->
|
||||
<script setup lang="ts">
|
||||
const { count, name = 'default' } = defineProps<{
|
||||
count: number
|
||||
name?: string
|
||||
}>()
|
||||
|
||||
// count 和 name 自动保持响应性!
|
||||
// 可以直接在模板和 watch 中使用
|
||||
watch(() => count, (newCount) => {
|
||||
console.log('Count changed:', newCount)
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ 配合默认值使用 -->
|
||||
<script setup lang="ts">
|
||||
const {
|
||||
title,
|
||||
count = 0,
|
||||
items = () => [] // 函数作为默认值(对象/数组)
|
||||
} = defineProps<{
|
||||
title: string
|
||||
count?: number
|
||||
items?: () => string[]
|
||||
}>()
|
||||
</script>
|
||||
```
|
||||
|
||||
### defineModel (3.4+)
|
||||
|
||||
```vue
|
||||
<!-- ❌ 传统 v-model 实现:冗长 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ modelValue: string }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
|
||||
|
||||
// 需要 computed 来双向绑定
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ defineModel:简洁的 v-model 实现 -->
|
||||
<script setup lang="ts">
|
||||
// 自动处理 props 和 emit
|
||||
const model = defineModel<string>()
|
||||
|
||||
// 直接使用
|
||||
model.value = 'new value' // 自动 emit
|
||||
</script>
|
||||
<template>
|
||||
<input v-model="model" />
|
||||
</template>
|
||||
|
||||
<!-- ✅ 命名 v-model -->
|
||||
<script setup lang="ts">
|
||||
// v-model:title 的实现
|
||||
const title = defineModel<string>('title')
|
||||
|
||||
// 带默认值和选项
|
||||
const count = defineModel<number>('count', {
|
||||
default: 0,
|
||||
required: false
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ 多个 v-model -->
|
||||
<script setup lang="ts">
|
||||
const firstName = defineModel<string>('firstName')
|
||||
const lastName = defineModel<string>('lastName')
|
||||
</script>
|
||||
<template>
|
||||
<!-- 父组件使用:<MyInput v-model:first-name="first" v-model:last-name="last" /> -->
|
||||
</template>
|
||||
|
||||
<!-- ✅ v-model 修饰符 -->
|
||||
<script setup lang="ts">
|
||||
const [model, modifiers] = defineModel<string>()
|
||||
|
||||
// 检查修饰符
|
||||
if (modifiers.capitalize) {
|
||||
// 处理 .capitalize 修饰符
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### useTemplateRef (3.5+)
|
||||
|
||||
```vue
|
||||
<!-- 传统方式:ref 属性与变量同名 -->
|
||||
<script setup lang="ts">
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
</script>
|
||||
<template>
|
||||
<input ref="inputRef" />
|
||||
</template>
|
||||
|
||||
<!-- ✅ useTemplateRef:更清晰的模板引用 -->
|
||||
<script setup lang="ts">
|
||||
import { useTemplateRef } from 'vue'
|
||||
|
||||
const input = useTemplateRef<HTMLInputElement>('my-input')
|
||||
|
||||
onMounted(() => {
|
||||
input.value?.focus()
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<input ref="my-input" />
|
||||
</template>
|
||||
|
||||
<!-- ✅ 动态 ref -->
|
||||
<script setup lang="ts">
|
||||
const refKey = ref('input-a')
|
||||
const dynamicInput = useTemplateRef<HTMLInputElement>(refKey)
|
||||
</script>
|
||||
```
|
||||
|
||||
### useId (3.5+)
|
||||
|
||||
```vue
|
||||
<!-- ❌ 手动生成 ID 可能冲突 -->
|
||||
<script setup lang="ts">
|
||||
const id = `input-${Math.random()}` // SSR 不一致!
|
||||
</script>
|
||||
|
||||
<!-- ✅ useId:SSR 安全的唯一 ID -->
|
||||
<script setup lang="ts">
|
||||
import { useId } from 'vue'
|
||||
|
||||
const id = useId() // 例如:'v-0'
|
||||
</script>
|
||||
<template>
|
||||
<label :for="id">Name</label>
|
||||
<input :id="id" />
|
||||
</template>
|
||||
|
||||
<!-- ✅ 表单组件中使用 -->
|
||||
<script setup lang="ts">
|
||||
const inputId = useId()
|
||||
const errorId = useId()
|
||||
</script>
|
||||
<template>
|
||||
<label :for="inputId">Email</label>
|
||||
<input
|
||||
:id="inputId"
|
||||
:aria-describedby="errorId"
|
||||
/>
|
||||
<span :id="errorId" class="error">{{ error }}</span>
|
||||
</template>
|
||||
```
|
||||
|
||||
### onWatcherCleanup (3.5+)
|
||||
|
||||
```vue
|
||||
<!-- 传统方式:watch 第三个参数 -->
|
||||
<script setup lang="ts">
|
||||
watch(source, async (value, oldValue, onCleanup) => {
|
||||
const controller = new AbortController()
|
||||
onCleanup(() => controller.abort())
|
||||
// ...
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ onWatcherCleanup:更灵活的清理 -->
|
||||
<script setup lang="ts">
|
||||
import { onWatcherCleanup } from 'vue'
|
||||
|
||||
watch(source, async (value) => {
|
||||
const controller = new AbortController()
|
||||
onWatcherCleanup(() => controller.abort())
|
||||
|
||||
// 可以在任意位置调用,不限于回调开头
|
||||
if (someCondition) {
|
||||
const anotherResource = createResource()
|
||||
onWatcherCleanup(() => anotherResource.dispose())
|
||||
}
|
||||
|
||||
await fetchData(value, controller.signal)
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### Deferred Teleport (3.5+)
|
||||
|
||||
```vue
|
||||
<!-- ❌ Teleport 目标必须在挂载时存在 -->
|
||||
<template>
|
||||
<Teleport to="#modal-container">
|
||||
<!-- 如果 #modal-container 不存在会报错 -->
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<!-- ✅ defer 属性延迟挂载 -->
|
||||
<template>
|
||||
<Teleport to="#modal-container" defer>
|
||||
<!-- 等待目标元素存在后再挂载 -->
|
||||
<Modal />
|
||||
</Teleport>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Watchers
|
||||
|
||||
### watch vs watchEffect
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// ✅ watch:明确指定依赖,惰性执行
|
||||
watch(
|
||||
() => props.userId,
|
||||
async (userId) => {
|
||||
user.value = await fetchUser(userId)
|
||||
}
|
||||
)
|
||||
|
||||
// ✅ watchEffect:自动收集依赖,立即执行
|
||||
watchEffect(async () => {
|
||||
// 自动追踪 props.userId
|
||||
user.value = await fetchUser(props.userId)
|
||||
})
|
||||
|
||||
// 💡 选择指南:
|
||||
// - 需要旧值?用 watch
|
||||
// - 需要惰性执行?用 watch
|
||||
// - 依赖复杂?用 watchEffect
|
||||
</script>
|
||||
```
|
||||
|
||||
### watch 清理函数
|
||||
|
||||
```vue
|
||||
<!-- ❌ watch 缺少清理函数,可能内存泄漏 -->
|
||||
<script setup lang="ts">
|
||||
watch(searchQuery, async (query) => {
|
||||
const controller = new AbortController()
|
||||
const data = await fetch(`/api/search?q=${query}`, {
|
||||
signal: controller.signal
|
||||
})
|
||||
results.value = await data.json()
|
||||
// 如果 query 快速变化,旧请求不会被取消!
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 onCleanup 清理副作用 -->
|
||||
<script setup lang="ts">
|
||||
watch(searchQuery, async (query, _, onCleanup) => {
|
||||
const controller = new AbortController()
|
||||
onCleanup(() => controller.abort()) // 取消旧请求
|
||||
|
||||
try {
|
||||
const data = await fetch(`/api/search?q=${query}`, {
|
||||
signal: controller.signal
|
||||
})
|
||||
results.value = await data.json()
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') throw e
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### watch 选项
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// ✅ immediate:立即执行一次
|
||||
watch(
|
||||
userId,
|
||||
async (id) => {
|
||||
user.value = await fetchUser(id)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// ✅ deep:深度监听(性能开销大,谨慎使用)
|
||||
watch(
|
||||
state,
|
||||
(newState) => {
|
||||
console.log('State changed deeply')
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// ✅ flush: 'post':DOM 更新后执行
|
||||
watch(
|
||||
source,
|
||||
() => {
|
||||
// 可以安全访问更新后的 DOM
|
||||
// nextTick 不再需要
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
|
||||
// ✅ once: true (Vue 3.4+):只执行一次
|
||||
watch(
|
||||
source,
|
||||
(value) => {
|
||||
console.log('只会执行一次:', value)
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
</script>
|
||||
```
|
||||
|
||||
### 监听多个源
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// ✅ 监听多个 ref
|
||||
watch(
|
||||
[firstName, lastName],
|
||||
([newFirst, newLast], [oldFirst, oldLast]) => {
|
||||
console.log(`Name changed from ${oldFirst} ${oldLast} to ${newFirst} ${newLast}`)
|
||||
}
|
||||
)
|
||||
|
||||
// ✅ 监听 reactive 对象的特定属性
|
||||
watch(
|
||||
() => [state.count, state.name],
|
||||
([count, name]) => {
|
||||
console.log(`count: ${count}, name: ${name}`)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 模板最佳实践
|
||||
|
||||
### v-for 的 key
|
||||
|
||||
```vue
|
||||
<!-- ❌ v-for 中使用 index 作为 key -->
|
||||
<template>
|
||||
<li v-for="(item, index) in items" :key="index">
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 使用唯一标识作为 key -->
|
||||
<template>
|
||||
<li v-for="item in items" :key="item.id">
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 复合 key(当没有唯一 ID 时)-->
|
||||
<template>
|
||||
<li v-for="(item, index) in items" :key="`${item.name}-${item.type}-${index}`">
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</template>
|
||||
```
|
||||
|
||||
### v-if 和 v-for 优先级
|
||||
|
||||
```vue
|
||||
<!-- ❌ v-if 和 v-for 同时使用 -->
|
||||
<template>
|
||||
<li v-for="user in users" v-if="user.active" :key="user.id">
|
||||
{{ user.name }}
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 使用 computed 过滤 -->
|
||||
<script setup lang="ts">
|
||||
const activeUsers = computed(() =>
|
||||
users.value.filter(user => user.active)
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<li v-for="user in activeUsers" :key="user.id">
|
||||
{{ user.name }}
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 或用 template 包裹 -->
|
||||
<template>
|
||||
<template v-for="user in users" :key="user.id">
|
||||
<li v-if="user.active">
|
||||
{{ user.name }}
|
||||
</li>
|
||||
</template>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 事件处理
|
||||
|
||||
```vue
|
||||
<!-- ❌ 内联复杂逻辑 -->
|
||||
<template>
|
||||
<button @click="items = items.filter(i => i.id !== item.id); count--">
|
||||
Delete
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 使用方法 -->
|
||||
<script setup lang="ts">
|
||||
const deleteItem = (id: number) => {
|
||||
items.value = items.value.filter(i => i.id !== id)
|
||||
count.value--
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<button @click="deleteItem(item.id)">Delete</button>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 事件修饰符 -->
|
||||
<template>
|
||||
<!-- 阻止默认行为 -->
|
||||
<form @submit.prevent="handleSubmit">...</form>
|
||||
|
||||
<!-- 阻止冒泡 -->
|
||||
<button @click.stop="handleClick">...</button>
|
||||
|
||||
<!-- 只执行一次 -->
|
||||
<button @click.once="handleOnce">...</button>
|
||||
|
||||
<!-- 键盘修饰符 -->
|
||||
<input @keyup.enter="submit" @keyup.esc="cancel" />
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Composables
|
||||
|
||||
### Composable 设计原则
|
||||
|
||||
```typescript
|
||||
// ✅ 好的 composable 设计
|
||||
export function useCounter(initialValue = 0) {
|
||||
const count = ref(initialValue)
|
||||
|
||||
const increment = () => count.value++
|
||||
const decrement = () => count.value--
|
||||
const reset = () => count.value = initialValue
|
||||
|
||||
// 返回响应式引用和方法
|
||||
return {
|
||||
count: readonly(count), // 只读防止外部修改
|
||||
increment,
|
||||
decrement,
|
||||
reset
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 不要返回 .value
|
||||
export function useBadCounter() {
|
||||
const count = ref(0)
|
||||
return {
|
||||
count: count.value // ❌ 丢失响应性!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Props 传递给 composable
|
||||
|
||||
```vue
|
||||
<!-- ❌ 传递 props 到 composable 丢失响应性 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ userId: string }>()
|
||||
const { user } = useUser(props.userId) // 丢失响应性!
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 toRef 或 computed 保持响应性 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ userId: string }>()
|
||||
const userIdRef = toRef(props, 'userId')
|
||||
const { user } = useUser(userIdRef) // 保持响应性
|
||||
// 或使用 computed
|
||||
const { user } = useUser(computed(() => props.userId))
|
||||
|
||||
// ✅ Vue 3.5+:直接解构使用
|
||||
const { userId } = defineProps<{ userId: string }>()
|
||||
const { user } = useUser(() => userId) // getter 函数
|
||||
</script>
|
||||
```
|
||||
|
||||
### 异步 Composable
|
||||
|
||||
```typescript
|
||||
// ✅ 异步 composable 模式
|
||||
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
|
||||
const data = ref<T | null>(null)
|
||||
const error = ref<Error | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const execute = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await fetch(toValue(url))
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
data.value = await response.json()
|
||||
} catch (e) {
|
||||
error.value = e as Error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式 URL 时自动重新获取
|
||||
watchEffect(() => {
|
||||
toValue(url) // 追踪依赖
|
||||
execute()
|
||||
})
|
||||
|
||||
return {
|
||||
data: readonly(data),
|
||||
error: readonly(error),
|
||||
loading: readonly(loading),
|
||||
refetch: execute
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
const { data, loading, error, refetch } = useFetch<User[]>('/api/users')
|
||||
```
|
||||
|
||||
### 生命周期与清理
|
||||
|
||||
```typescript
|
||||
// ✅ Composable 中正确处理生命周期
|
||||
export function useEventListener(
|
||||
target: MaybeRefOrGetter<EventTarget>,
|
||||
event: string,
|
||||
handler: EventListener
|
||||
) {
|
||||
// 组件挂载后添加
|
||||
onMounted(() => {
|
||||
toValue(target).addEventListener(event, handler)
|
||||
})
|
||||
|
||||
// 组件卸载时移除
|
||||
onUnmounted(() => {
|
||||
toValue(target).removeEventListener(event, handler)
|
||||
})
|
||||
}
|
||||
|
||||
// ✅ 使用 effectScope 管理副作用
|
||||
export function useFeature() {
|
||||
const scope = effectScope()
|
||||
|
||||
scope.run(() => {
|
||||
// 所有响应式效果都在这个 scope 内
|
||||
const state = ref(0)
|
||||
watch(state, () => { /* ... */ })
|
||||
watchEffect(() => { /* ... */ })
|
||||
})
|
||||
|
||||
// 清理所有效果
|
||||
onUnmounted(() => scope.stop())
|
||||
|
||||
return { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能优化
|
||||
|
||||
### v-memo
|
||||
|
||||
```vue
|
||||
<!-- ✅ v-memo:缓存子树,避免重复渲染 -->
|
||||
<template>
|
||||
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
|
||||
<!-- 只有当 item.id === selected 变化时才重新渲染 -->
|
||||
<ExpensiveComponent :item="item" :selected="item.id === selected" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 配合 v-for 使用 -->
|
||||
<template>
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
v-memo="[item.name, item.status]"
|
||||
>
|
||||
<!-- 只有 name 或 status 变化时重新渲染 -->
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### defineAsyncComponent
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
// ✅ 懒加载组件
|
||||
const HeavyChart = defineAsyncComponent(() =>
|
||||
import('./components/HeavyChart.vue')
|
||||
)
|
||||
|
||||
// ✅ 带加载和错误状态
|
||||
const AsyncModal = defineAsyncComponent({
|
||||
loader: () => import('./components/Modal.vue'),
|
||||
loadingComponent: LoadingSpinner,
|
||||
errorComponent: ErrorDisplay,
|
||||
delay: 200, // 延迟显示 loading(避免闪烁)
|
||||
timeout: 3000 // 超时时间
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### KeepAlive
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- ✅ 缓存动态组件 -->
|
||||
<KeepAlive>
|
||||
<component :is="currentTab" />
|
||||
</KeepAlive>
|
||||
|
||||
<!-- ✅ 指定缓存的组件 -->
|
||||
<KeepAlive include="TabA,TabB">
|
||||
<component :is="currentTab" />
|
||||
</KeepAlive>
|
||||
|
||||
<!-- ✅ 限制缓存数量 -->
|
||||
<KeepAlive :max="10">
|
||||
<component :is="currentTab" />
|
||||
</KeepAlive>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// KeepAlive 组件的生命周期钩子
|
||||
onActivated(() => {
|
||||
// 组件被激活时(从缓存恢复)
|
||||
refreshData()
|
||||
})
|
||||
|
||||
onDeactivated(() => {
|
||||
// 组件被停用时(进入缓存)
|
||||
pauseTimers()
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### 虚拟列表
|
||||
|
||||
```vue
|
||||
<!-- ✅ 大型列表使用虚拟滚动 -->
|
||||
<script setup lang="ts">
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
|
||||
const { list, containerProps, wrapperProps } = useVirtualList(
|
||||
items,
|
||||
{ itemHeight: 50 }
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<div v-bind="containerProps" style="height: 400px; overflow: auto">
|
||||
<div v-bind="wrapperProps">
|
||||
<div v-for="item in list" :key="item.data.id" style="height: 50px">
|
||||
{{ item.data.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### 响应性系统
|
||||
- [ ] ref 用于基本类型,reactive 用于对象(或统一用 ref)
|
||||
- [ ] 没有解构 reactive 对象(或使用了 toRefs)
|
||||
- [ ] props 传递给 composable 时保持了响应性
|
||||
- [ ] shallowRef/shallowReactive 用于大型对象优化
|
||||
- [ ] computed 中没有副作用
|
||||
|
||||
### Props & Emits
|
||||
- [ ] defineProps 使用 TypeScript 类型声明
|
||||
- [ ] 复杂默认值使用 withDefaults + 工厂函数
|
||||
- [ ] defineEmits 有完整的类型定义
|
||||
- [ ] 没有直接修改 props
|
||||
- [ ] 考虑使用 defineModel 简化 v-model(Vue 3.4+)
|
||||
|
||||
### Vue 3.5 新特性(如适用)
|
||||
- [ ] 使用 Reactive Props Destructure 简化 props 访问
|
||||
- [ ] 使用 useTemplateRef 替代 ref 属性
|
||||
- [ ] 表单使用 useId 生成 SSR 安全的 ID
|
||||
- [ ] 使用 onWatcherCleanup 处理复杂清理逻辑
|
||||
|
||||
### Watchers
|
||||
- [ ] watch/watchEffect 有适当的清理函数
|
||||
- [ ] 异步 watch 处理了竞态条件
|
||||
- [ ] flush: 'post' 用于 DOM 操作的 watcher
|
||||
- [ ] 避免过度使用 watcher(优先用 computed)
|
||||
- [ ] 考虑 once: true 用于一次性监听
|
||||
|
||||
### 模板
|
||||
- [ ] v-for 使用唯一且稳定的 key
|
||||
- [ ] v-if 和 v-for 没有在同一元素上
|
||||
- [ ] 事件处理使用方法而非内联复杂逻辑
|
||||
- [ ] 大型列表使用虚拟滚动
|
||||
|
||||
### Composables
|
||||
- [ ] 相关逻辑提取到 composables
|
||||
- [ ] composables 返回响应式引用(不是 .value)
|
||||
- [ ] 纯函数不要包装成 composable
|
||||
- [ ] 副作用在组件卸载时清理
|
||||
- [ ] 使用 effectScope 管理复杂副作用
|
||||
|
||||
### 性能
|
||||
- [ ] 大型组件拆分为小组件
|
||||
- [ ] 使用 defineAsyncComponent 懒加载
|
||||
- [ ] 避免不必要的响应式转换
|
||||
- [ ] v-memo 用于昂贵的列表渲染
|
||||
- [ ] KeepAlive 用于缓存动态组件
|
||||
@@ -0,0 +1,388 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PR Analyzer - Analyze PR complexity and suggest review approach.
|
||||
|
||||
Usage:
|
||||
python pr-analyzer.py [--diff-file FILE] [--stats]
|
||||
|
||||
Or pipe diff directly:
|
||||
git diff main...HEAD | python pr-analyzer.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
RISK_NO_TESTS = "NO_TEST_CHANGES"
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileStats:
|
||||
"""Statistics for a single file."""
|
||||
filename: str
|
||||
additions: int = 0
|
||||
deletions: int = 0
|
||||
is_test: bool = False
|
||||
is_config: bool = False
|
||||
language: str = "unknown"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PRAnalysis:
|
||||
"""Complete PR analysis results."""
|
||||
total_files: int
|
||||
total_additions: int
|
||||
total_deletions: int
|
||||
files: List[FileStats]
|
||||
complexity_score: float
|
||||
size_category: str
|
||||
estimated_review_time: int
|
||||
risk_factors: List[str]
|
||||
suggestions: List[str]
|
||||
|
||||
|
||||
def detect_language(filename: str) -> str:
|
||||
"""Detect programming language from filename."""
|
||||
_, ext = os.path.splitext(filename)
|
||||
extensions = {
|
||||
'.py': 'Python',
|
||||
'.js': 'JavaScript',
|
||||
'.ts': 'TypeScript',
|
||||
'.tsx': 'TypeScript/React',
|
||||
'.jsx': 'JavaScript/React',
|
||||
'.rs': 'Rust',
|
||||
'.go': 'Go',
|
||||
'.c': 'C',
|
||||
'.h': 'C/C++',
|
||||
'.cpp': 'C++',
|
||||
'.hpp': 'C++',
|
||||
'.cc': 'C++',
|
||||
'.cxx': 'C++',
|
||||
'.hh': 'C++',
|
||||
'.hxx': 'C++',
|
||||
'.java': 'Java',
|
||||
'.kt': 'Kotlin',
|
||||
'.swift': 'Swift',
|
||||
'.rb': 'Ruby',
|
||||
'.php': 'PHP',
|
||||
'.cs': 'C#',
|
||||
'.vue': 'Vue',
|
||||
'.svelte': 'Svelte',
|
||||
'.sql': 'SQL',
|
||||
'.md': 'Markdown',
|
||||
'.json': 'JSON',
|
||||
'.yaml': 'YAML',
|
||||
'.yml': 'YAML',
|
||||
'.toml': 'TOML',
|
||||
'.css': 'CSS',
|
||||
'.scss': 'SCSS',
|
||||
'.less': 'Less',
|
||||
'.html': 'HTML',
|
||||
'.zig': 'Zig',
|
||||
'.ex': 'Elixir',
|
||||
'.exs': 'Elixir',
|
||||
'.erl': 'Erlang',
|
||||
'.scala': 'Scala',
|
||||
'.lua': 'Lua',
|
||||
}
|
||||
return extensions.get(ext.lower(), 'unknown')
|
||||
|
||||
|
||||
def is_test_file(filename: str) -> bool:
|
||||
"""Check if file is a test file."""
|
||||
test_patterns = [
|
||||
r'test_.*\.py$',
|
||||
r'.*_test\.py$',
|
||||
r'.*\.test\.(js|ts|tsx)$',
|
||||
r'.*\.spec\.(js|ts|tsx)$',
|
||||
r'tests?/',
|
||||
r'__tests__/',
|
||||
]
|
||||
return any(re.search(p, filename) for p in test_patterns)
|
||||
|
||||
|
||||
def is_config_file(filename: str) -> bool:
|
||||
"""Check if file is a configuration file."""
|
||||
config_patterns = [
|
||||
r'\.env',
|
||||
r'config\.',
|
||||
r'\.json$',
|
||||
r'\.yaml$',
|
||||
r'\.yml$',
|
||||
r'\.toml$',
|
||||
r'Cargo\.toml$',
|
||||
r'package\.json$',
|
||||
r'tsconfig\.json$',
|
||||
]
|
||||
return any(re.search(p, filename) for p in config_patterns)
|
||||
|
||||
|
||||
def parse_diff(diff_content: str) -> List[FileStats]:
|
||||
"""Parse git diff output and extract file statistics."""
|
||||
files = []
|
||||
current_file = None
|
||||
|
||||
for line in diff_content.split('\n'):
|
||||
# New file header
|
||||
if line.startswith('diff --git'):
|
||||
if current_file:
|
||||
files.append(current_file)
|
||||
# "diff --git a/<path> b/<path>" — match the b/ side via a
|
||||
# backreference so a literal "b/" inside paths like lib/, web/ or
|
||||
# db/ can't be mistaken for the prefix. Renames have differing
|
||||
# paths, so fall back to the b/ side after the separating space.
|
||||
match = re.match(r'diff --git a/(.+?) b/\1', line)
|
||||
if not match:
|
||||
match = re.search(r' b/(.+)$', line)
|
||||
if match:
|
||||
filename = match.group(1)
|
||||
current_file = FileStats(
|
||||
filename=filename,
|
||||
language=detect_language(filename),
|
||||
is_test=is_test_file(filename),
|
||||
is_config=is_config_file(filename),
|
||||
)
|
||||
else:
|
||||
current_file = None
|
||||
elif current_file:
|
||||
if line.startswith('+') and not line.startswith('+++'):
|
||||
current_file.additions += 1
|
||||
elif line.startswith('-') and not line.startswith('---'):
|
||||
current_file.deletions += 1
|
||||
|
||||
if current_file:
|
||||
files.append(current_file)
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def calculate_complexity(files: List[FileStats]) -> float:
|
||||
"""Calculate complexity score (0-1 scale)."""
|
||||
if not files:
|
||||
return 0.0
|
||||
|
||||
total_changes = sum(f.additions + f.deletions for f in files)
|
||||
|
||||
# Base complexity from size
|
||||
size_factor = min(total_changes / 1000, 1.0)
|
||||
|
||||
# Factor for number of files
|
||||
file_factor = min(len(files) / 20, 1.0)
|
||||
|
||||
# Factor for non-test code ratio
|
||||
test_lines = sum(f.additions + f.deletions for f in files if f.is_test)
|
||||
non_test_ratio = 1 - (test_lines / max(total_changes, 1))
|
||||
|
||||
# Factor for language diversity
|
||||
languages = set(f.language for f in files if f.language != 'unknown')
|
||||
lang_factor = min(len(languages) / 5, 1.0)
|
||||
|
||||
complexity = (
|
||||
size_factor * 0.4 +
|
||||
file_factor * 0.2 +
|
||||
non_test_ratio * 0.2 +
|
||||
lang_factor * 0.2
|
||||
)
|
||||
|
||||
return round(complexity, 2)
|
||||
|
||||
|
||||
def categorize_size(total_changes: int) -> str:
|
||||
"""Categorize PR size."""
|
||||
if total_changes < 50:
|
||||
return "XS (Extra Small)"
|
||||
elif total_changes < 200:
|
||||
return "S (Small)"
|
||||
elif total_changes < 400:
|
||||
return "M (Medium)"
|
||||
elif total_changes < 800:
|
||||
return "L (Large)"
|
||||
else:
|
||||
return "XL (Extra Large) - Consider splitting"
|
||||
|
||||
|
||||
def estimate_review_time(files: List[FileStats], complexity: float) -> int:
|
||||
"""Estimate review time in minutes."""
|
||||
total_changes = sum(f.additions + f.deletions for f in files)
|
||||
|
||||
# Base time: ~1 minute per 20 lines
|
||||
base_time = total_changes / 20
|
||||
|
||||
# Adjust for complexity
|
||||
adjusted_time = base_time * (1 + complexity)
|
||||
|
||||
# Minimum 5 minutes, maximum 120 minutes
|
||||
return max(5, min(120, int(adjusted_time)))
|
||||
|
||||
|
||||
def identify_risk_factors(files: List[FileStats]) -> List[str]:
|
||||
"""Identify potential risk factors in the PR."""
|
||||
risks = []
|
||||
|
||||
total_changes = sum(f.additions + f.deletions for f in files)
|
||||
test_changes = sum(f.additions + f.deletions for f in files if f.is_test)
|
||||
|
||||
if total_changes > 400:
|
||||
risks.append("Large PR (>400 lines) - harder to review thoroughly")
|
||||
|
||||
if test_changes == 0 and total_changes > 50:
|
||||
risks.append(f"{RISK_NO_TESTS}: No test changes - verify test coverage")
|
||||
|
||||
if total_changes > 100 and test_changes / max(total_changes, 1) < 0.2:
|
||||
risks.append("Low test ratio (<20%) - consider adding more tests")
|
||||
|
||||
# Security-sensitive files
|
||||
security_patterns = ['.env', 'auth', 'security', 'password', 'token', 'secret']
|
||||
for f in files:
|
||||
if any(p in f.filename.lower() for p in security_patterns):
|
||||
risks.append(f"Security-sensitive file: {f.filename}")
|
||||
break
|
||||
|
||||
# Database changes
|
||||
for f in files:
|
||||
if 'migration' in f.filename.lower() or f.language == 'SQL':
|
||||
risks.append("Database changes detected - review carefully")
|
||||
break
|
||||
|
||||
# Config changes
|
||||
config_files = [f for f in files if f.is_config]
|
||||
if config_files:
|
||||
risks.append(f"Configuration changes in {len(config_files)} file(s)")
|
||||
|
||||
return risks
|
||||
|
||||
|
||||
def generate_suggestions(files: List[FileStats], complexity: float, risks: List[str]) -> List[str]:
|
||||
"""Generate review suggestions."""
|
||||
suggestions = []
|
||||
|
||||
total_changes = sum(f.additions + f.deletions for f in files)
|
||||
|
||||
if total_changes > 800:
|
||||
suggestions.append("Consider splitting this PR into smaller, focused changes")
|
||||
|
||||
if complexity > 0.7:
|
||||
suggestions.append("High complexity - allocate extra review time")
|
||||
suggestions.append("Consider pair reviewing for critical sections")
|
||||
|
||||
if any(RISK_NO_TESTS in r for r in risks):
|
||||
suggestions.append("Request test additions before approval")
|
||||
|
||||
# Language-specific suggestions
|
||||
languages = set(f.language for f in files)
|
||||
if 'TypeScript' in languages or 'TypeScript/React' in languages:
|
||||
suggestions.append("Check for proper type usage (avoid 'any')")
|
||||
if 'Rust' in languages:
|
||||
suggestions.append("Check for unwrap() usage and error handling")
|
||||
if 'C' in languages or 'C++' in languages or 'C/C++' in languages:
|
||||
suggestions.append("Check for memory safety, bounds checks, and UB risks")
|
||||
if 'SQL' in languages:
|
||||
suggestions.append("Review for SQL injection and query performance")
|
||||
|
||||
if not suggestions:
|
||||
suggestions.append("Standard review process should suffice")
|
||||
|
||||
return suggestions
|
||||
|
||||
|
||||
def analyze_pr(diff_content: str) -> PRAnalysis:
|
||||
"""Perform complete PR analysis."""
|
||||
files = parse_diff(diff_content)
|
||||
|
||||
total_additions = sum(f.additions for f in files)
|
||||
total_deletions = sum(f.deletions for f in files)
|
||||
total_changes = total_additions + total_deletions
|
||||
|
||||
complexity = calculate_complexity(files)
|
||||
risks = identify_risk_factors(files)
|
||||
suggestions = generate_suggestions(files, complexity, risks)
|
||||
|
||||
return PRAnalysis(
|
||||
total_files=len(files),
|
||||
total_additions=total_additions,
|
||||
total_deletions=total_deletions,
|
||||
files=files,
|
||||
complexity_score=complexity,
|
||||
size_category=categorize_size(total_changes),
|
||||
estimated_review_time=estimate_review_time(files, complexity),
|
||||
risk_factors=risks,
|
||||
suggestions=suggestions,
|
||||
)
|
||||
|
||||
|
||||
def print_analysis(analysis: PRAnalysis, show_files: bool = False):
|
||||
"""Print analysis results."""
|
||||
print("\n" + "=" * 60)
|
||||
print("PR ANALYSIS REPORT")
|
||||
print("=" * 60)
|
||||
|
||||
print(f"\n📊 SUMMARY")
|
||||
print(f" Files changed: {analysis.total_files}")
|
||||
print(f" Additions: +{analysis.total_additions}")
|
||||
print(f" Deletions: -{analysis.total_deletions}")
|
||||
print(f" Total changes: {analysis.total_additions + analysis.total_deletions}")
|
||||
|
||||
print(f"\n📏 SIZE: {analysis.size_category}")
|
||||
print(f" Complexity score: {analysis.complexity_score}/1.0")
|
||||
print(f" Estimated review time: ~{analysis.estimated_review_time} minutes")
|
||||
|
||||
if analysis.risk_factors:
|
||||
print(f"\n⚠️ RISK FACTORS:")
|
||||
for risk in analysis.risk_factors:
|
||||
print(f" • {risk}")
|
||||
|
||||
print(f"\n💡 SUGGESTIONS:")
|
||||
for suggestion in analysis.suggestions:
|
||||
print(f" • {suggestion}")
|
||||
|
||||
if show_files:
|
||||
print(f"\n📁 FILES:")
|
||||
# Group by language
|
||||
by_lang: Dict[str, List[FileStats]] = defaultdict(list)
|
||||
for f in analysis.files:
|
||||
by_lang[f.language].append(f)
|
||||
|
||||
for lang, lang_files in sorted(by_lang.items()):
|
||||
print(f"\n [{lang}]")
|
||||
for f in lang_files:
|
||||
prefix = "🧪" if f.is_test else "⚙️" if f.is_config else "📄"
|
||||
print(f" {prefix} {f.filename} (+{f.additions}/-{f.deletions})")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Analyze PR complexity')
|
||||
parser.add_argument('--diff-file', '-f', help='Path to diff file')
|
||||
parser.add_argument('--stats', '-s', action='store_true', help='Show file details')
|
||||
args = parser.parse_args()
|
||||
|
||||
# Read diff from file or stdin
|
||||
try:
|
||||
if args.diff_file:
|
||||
with open(args.diff_file, 'r', encoding='utf-8', errors='replace') as f:
|
||||
diff_content = f.read()
|
||||
elif not sys.stdin.isatty():
|
||||
diff_content = sys.stdin.buffer.read().decode('utf-8', errors='replace')
|
||||
else:
|
||||
print("Usage: git diff main...HEAD | python pr-analyzer.py")
|
||||
print(" python pr-analyzer.py -f diff.txt")
|
||||
sys.exit(1)
|
||||
except OSError as e:
|
||||
print(f"Error reading diff input: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not diff_content.strip():
|
||||
print("No diff content provided")
|
||||
sys.exit(1)
|
||||
|
||||
analysis = analyze_pr(diff_content)
|
||||
print_analysis(analysis, show_files=args.stats)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for pr-analyzer.py diff parsing (stdlib unittest, no extra deps)."""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import unittest
|
||||
|
||||
# The script has a hyphen in its name, so load it by path.
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
'pr_analyzer', os.path.join(_HERE, 'pr-analyzer.py')
|
||||
)
|
||||
pr_analyzer = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(pr_analyzer)
|
||||
|
||||
|
||||
class ParseDiffFilenameTest(unittest.TestCase):
|
||||
def test_lib_prefixed_path(self):
|
||||
# "lib/" embeds a literal "b/" that the old regex swallowed.
|
||||
diff = (
|
||||
"diff --git a/lib/foo.py b/lib/foo.py\n"
|
||||
"index 1234567..89abcde 100644\n"
|
||||
"--- a/lib/foo.py\n"
|
||||
"+++ b/lib/foo.py\n"
|
||||
"@@ -1,2 +1,3 @@\n"
|
||||
" unchanged\n"
|
||||
"+added line\n"
|
||||
"-removed line\n"
|
||||
)
|
||||
files = pr_analyzer.parse_diff(diff)
|
||||
self.assertEqual(len(files), 1)
|
||||
self.assertEqual(files[0].filename, 'lib/foo.py')
|
||||
self.assertEqual(files[0].additions, 1)
|
||||
self.assertEqual(files[0].deletions, 1)
|
||||
|
||||
def test_normal_path(self):
|
||||
diff = (
|
||||
"diff --git a/src/main.py b/src/main.py\n"
|
||||
"index 1111111..2222222 100644\n"
|
||||
"--- a/src/main.py\n"
|
||||
"+++ b/src/main.py\n"
|
||||
"@@ -0,0 +1 @@\n"
|
||||
"+print('hi')\n"
|
||||
)
|
||||
files = pr_analyzer.parse_diff(diff)
|
||||
self.assertEqual(len(files), 1)
|
||||
self.assertEqual(files[0].filename, 'src/main.py')
|
||||
|
||||
def test_other_embedded_b_slash_prefixes(self):
|
||||
# web/ and db/ also contain a literal "b/".
|
||||
diff = (
|
||||
"diff --git a/web/x.js b/web/x.js\n"
|
||||
"+++ b/web/x.js\n"
|
||||
"+console.log(1)\n"
|
||||
"diff --git a/db/y.sql b/db/y.sql\n"
|
||||
"+++ b/db/y.sql\n"
|
||||
"+SELECT 1;\n"
|
||||
)
|
||||
files = pr_analyzer.parse_diff(diff)
|
||||
self.assertEqual([f.filename for f in files], ['web/x.js', 'db/y.sql'])
|
||||
|
||||
def test_rename_falls_back_to_b_side(self):
|
||||
diff = (
|
||||
"diff --git a/old/name.py b/new/name.py\n"
|
||||
"similarity index 100%\n"
|
||||
"rename from old/name.py\n"
|
||||
"rename to new/name.py\n"
|
||||
)
|
||||
files = pr_analyzer.parse_diff(diff)
|
||||
self.assertEqual(len(files), 1)
|
||||
self.assertEqual(files[0].filename, 'new/name.py')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user