fix: 将 code-review-skill 从子模块转为普通目录

This commit is contained in:
baozaotumao
2026-06-06 09:44:07 -07:00
parent 44c5d0237e
commit 8b89322e18
39 changed files with 19829 additions and 1 deletions
@@ -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 ExplosionJOIN 笛卡尔爆炸)
```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 是 SingletonUserRepository 是 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 方法上加 @TransactionalAOP 不生效)
@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.jsflat configtypescript-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>
<!-- useIdSSR 安全的唯一 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-modelVue 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 用于缓存动态组件