@pilllesss/yorn 1.0.182 → 1.0.183
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/skills/code-review/LICENSE +21 -0
- package/dist/skills/code-review/SKILL.md +233 -0
- package/dist/skills/code-review/assets/pr-review-template.md +137 -0
- package/dist/skills/code-review/assets/review-checklist.md +123 -0
- package/dist/skills/code-review/reference/angular.md +768 -0
- package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
- package/dist/skills/code-review/reference/c.md +890 -0
- package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
- package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
- package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
- package/dist/skills/code-review/reference/cpp.md +893 -0
- package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
- package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
- package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
- package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
- package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
- package/dist/skills/code-review/reference/csharp.md +525 -0
- package/dist/skills/code-review/reference/css-less-sass.md +661 -0
- package/dist/skills/code-review/reference/dart.md +670 -0
- package/dist/skills/code-review/reference/django.md +985 -0
- package/dist/skills/code-review/reference/fastapi.md +580 -0
- package/dist/skills/code-review/reference/go.md +993 -0
- package/dist/skills/code-review/reference/java.md +409 -0
- package/dist/skills/code-review/reference/java8.md +586 -0
- package/dist/skills/code-review/reference/kotlin.md +1018 -0
- package/dist/skills/code-review/reference/nestjs.md +593 -0
- package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
- package/dist/skills/code-review/reference/php.md +684 -0
- package/dist/skills/code-review/reference/python.md +1073 -0
- package/dist/skills/code-review/reference/qt.md +757 -0
- package/dist/skills/code-review/reference/react.md +871 -0
- package/dist/skills/code-review/reference/ruby.md +964 -0
- package/dist/skills/code-review/reference/rust.md +846 -0
- package/dist/skills/code-review/reference/security-review-guide.md +494 -0
- package/dist/skills/code-review/reference/svelte.md +1064 -0
- package/dist/skills/code-review/reference/swift.md +936 -0
- package/dist/skills/code-review/reference/typescript.md +1016 -0
- package/dist/skills/code-review/reference/vue.md +924 -0
- package/dist/skills/code-review/reference/zig.md +440 -0
- package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
- package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
- package/dist/yorn.cjs +628 -628
- package/package.json +2 -2
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
# Universal Code Quality Anti-Patterns
|
|
2
|
+
|
|
3
|
+
> 语言无关的代码质量反模式指南,覆盖代码复用、抽象泄漏、参数膨胀、嵌套条件、字符串类型化、TOCTOU、空操作更新等核心主题。适用于所有语言的 PR 审查。
|
|
4
|
+
|
|
5
|
+
## 目录
|
|
6
|
+
|
|
7
|
+
- [代码复用审查](#代码复用审查)
|
|
8
|
+
- [参数膨胀](#参数膨胀)
|
|
9
|
+
- [抽象泄漏](#抽象泄漏)
|
|
10
|
+
- [字符串类型化](#字符串类型化)
|
|
11
|
+
- [嵌套条件表达式](#嵌套条件表达式)
|
|
12
|
+
- [复制粘贴变种](#复制粘贴变种)
|
|
13
|
+
- [空操作更新](#空操作更新)
|
|
14
|
+
- [TOCTOU 竞争条件](#toctou-竞争条件)
|
|
15
|
+
- [过度宽泛操作](#过度宽泛操作)
|
|
16
|
+
- [冗余状态](#冗余状态)
|
|
17
|
+
- [通用质量审查清单](#通用质量审查清单)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 代码复用审查
|
|
22
|
+
|
|
23
|
+
Before accepting new code, search the existing codebase for reusable utilities.
|
|
24
|
+
|
|
25
|
+
### 搜索现有工具函数
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
# ❌ 新写的路径拼接逻辑——项目中已有 PathBuilder
|
|
29
|
+
def get_config_path(name):
|
|
30
|
+
base = os.environ.get("APP_ROOT", ".")
|
|
31
|
+
return os.path.join(base, "config", name + ".json")
|
|
32
|
+
|
|
33
|
+
# ✅ 使用已有的 PathBuilder
|
|
34
|
+
def get_config_path(name):
|
|
35
|
+
return PathBuilder.config(f"{name}.json")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
// ❌ 手写 debounce——项目已有 lodash 或 utils/debounce.ts
|
|
40
|
+
function debounce(fn, ms) {
|
|
41
|
+
let timer;
|
|
42
|
+
return (...args) => {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
timer = setTimeout(() => fn(...args), ms);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ✅ 使用已有的工具函数
|
|
49
|
+
import { debounce } from "@/utils/debounce";
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**审查要点:**
|
|
53
|
+
- 新增函数是否与已有 utility 重名或功能重叠?
|
|
54
|
+
- inline 逻辑是否可以提取为已有模块的调用?
|
|
55
|
+
- 检查相邻文件和 shared/utils 目录
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 参数膨胀
|
|
60
|
+
|
|
61
|
+
### 函数参数不断增长
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# ❌ 每次新需求加一个参数
|
|
65
|
+
def create_user(name, email, role, team, active, avatar_url, timezone):
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
# ✅ 使用配置对象 / dataclass
|
|
69
|
+
@dataclass
|
|
70
|
+
class CreateUserParams:
|
|
71
|
+
name: str
|
|
72
|
+
email: str
|
|
73
|
+
role: Role = Role.MEMBER
|
|
74
|
+
team: str | None = None
|
|
75
|
+
active: bool = True
|
|
76
|
+
avatar_url: str | None = None
|
|
77
|
+
timezone: str = "UTC"
|
|
78
|
+
|
|
79
|
+
def create_user(params: CreateUserParams) -> User:
|
|
80
|
+
...
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// ❌ 6+ 个 positional 参数
|
|
85
|
+
function renderWidget(
|
|
86
|
+
title: string, width: number, height: number,
|
|
87
|
+
theme: string, collapsible: boolean, icon: string
|
|
88
|
+
) { ... }
|
|
89
|
+
|
|
90
|
+
// ✅ Options object pattern
|
|
91
|
+
interface WidgetOptions {
|
|
92
|
+
title: string;
|
|
93
|
+
width?: number;
|
|
94
|
+
height?: number;
|
|
95
|
+
theme?: "light" | "dark";
|
|
96
|
+
collapsible?: boolean;
|
|
97
|
+
icon?: string;
|
|
98
|
+
}
|
|
99
|
+
function renderWidget(options: WidgetOptions) { ... }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**审查要点:**
|
|
103
|
+
- 函数参数是否 ≥ 4 个?考虑 options object / dataclass
|
|
104
|
+
- 新参数是否只是布尔标志?考虑 enum 或 strategy pattern
|
|
105
|
+
- 是否有 `enable_x`, `disable_y` 这类互斥参数?
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 抽象泄漏
|
|
110
|
+
|
|
111
|
+
### 暴露内部实现细节
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
# ❌ 返回内部 ORM 对象——调用者被迫了解 SQLAlchemy
|
|
115
|
+
def get_users():
|
|
116
|
+
return session.query(User).filter(User.active == True).all()
|
|
117
|
+
|
|
118
|
+
# ✅ 返回 domain 对象,隐藏持久化层
|
|
119
|
+
def get_active_users() -> list[UserDTO]:
|
|
120
|
+
rows = user_repo.find_active()
|
|
121
|
+
return [UserDTO.from_row(r) for r in rows]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// ❌ 组件接收 API response 原始结构
|
|
126
|
+
<UserCard user={apiResponse.data.results[0]} />
|
|
127
|
+
|
|
128
|
+
// ✅ 组件接收 domain 类型,adapter 处理映射
|
|
129
|
+
interface UserSummary {
|
|
130
|
+
displayName: string;
|
|
131
|
+
avatarUrl: string;
|
|
132
|
+
}
|
|
133
|
+
<UserCard user={adaptUser(apiResponse)} />
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**审查要点:**
|
|
137
|
+
- 函数返回类型是否泄露底层实现(ORM, HTTP client, file format)?
|
|
138
|
+
- 组件/函数是否依赖外部系统的数据结构?
|
|
139
|
+
- 是否破坏了已有的抽象边界?
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 字符串类型化
|
|
144
|
+
|
|
145
|
+
### 用原始字符串代替常量/枚举
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
# ❌ Magic strings 散落各处
|
|
149
|
+
if status == "active":
|
|
150
|
+
...
|
|
151
|
+
if role == "admin":
|
|
152
|
+
...
|
|
153
|
+
|
|
154
|
+
# ✅ 使用 enum
|
|
155
|
+
class Status(StrEnum):
|
|
156
|
+
ACTIVE = "active"
|
|
157
|
+
SUSPENDED = "suspended"
|
|
158
|
+
ARCHIVED = "archived"
|
|
159
|
+
|
|
160
|
+
if user.status == Status.ACTIVE:
|
|
161
|
+
...
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// ❌ Raw string event names——拼写错误不会报错
|
|
166
|
+
emitter.emit("userCreated", data);
|
|
167
|
+
emitter.on("usercreated", handler); // bug: typo
|
|
168
|
+
|
|
169
|
+
// ✅ 常量或 branded type
|
|
170
|
+
const Events = {
|
|
171
|
+
USER_CREATED: "userCreated",
|
|
172
|
+
USER_SUSPENDED: "userSuspended",
|
|
173
|
+
} as const;
|
|
174
|
+
emitter.emit(Events.USER_CREATED, data);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**审查要点:**
|
|
178
|
+
- 是否用字符串代替了已有的 enum/union type?
|
|
179
|
+
- 事件名、action type、status 值是否散落在多个文件?
|
|
180
|
+
- 字符串比较是否 case-sensitive 但未验证?
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 嵌套条件表达式
|
|
185
|
+
|
|
186
|
+
### 三元链和嵌套 if/else
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
# ❌ 三元链难以阅读
|
|
190
|
+
label = (
|
|
191
|
+
"Admin" if role == "admin" else
|
|
192
|
+
"Manager" if role == "manager" else
|
|
193
|
+
"Viewer" if role == "viewer" else
|
|
194
|
+
"Unknown"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# ✅ 查找表或 match
|
|
198
|
+
ROLE_LABELS = {
|
|
199
|
+
"admin": "Admin",
|
|
200
|
+
"manager": "Manager",
|
|
201
|
+
"viewer": "Viewer",
|
|
202
|
+
}
|
|
203
|
+
label = ROLE_LABELS.get(role, "Unknown")
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// ❌ 嵌套三元
|
|
208
|
+
const bg = isHovered
|
|
209
|
+
? isSelected ? "blue" : "gray"
|
|
210
|
+
: isSelected ? "navy" : "white";
|
|
211
|
+
|
|
212
|
+
// ✅ 查找表(lookup map)
|
|
213
|
+
const bgMap: Record<string, string> = {
|
|
214
|
+
"true-true": "blue",
|
|
215
|
+
"true-false": "gray",
|
|
216
|
+
"false-true": "navy",
|
|
217
|
+
"false-false": "white",
|
|
218
|
+
};
|
|
219
|
+
const bg = bgMap[`${isHovered}-${isSelected}`];
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
# ❌ 嵌套 if 3+ 层
|
|
224
|
+
def process(order):
|
|
225
|
+
if order is not None:
|
|
226
|
+
if order.items:
|
|
227
|
+
for item in order.items:
|
|
228
|
+
if item.price > 0:
|
|
229
|
+
...
|
|
230
|
+
|
|
231
|
+
# ✅ Early return + guard clauses
|
|
232
|
+
def process(order):
|
|
233
|
+
if not order or not order.items:
|
|
234
|
+
return
|
|
235
|
+
for item in order.items:
|
|
236
|
+
if item.price <= 0:
|
|
237
|
+
continue
|
|
238
|
+
...
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**审查要点:**
|
|
242
|
+
- 三元表达式是否嵌套 ≥ 2 层?
|
|
243
|
+
- if/else 嵌套是否 ≥ 3 层?
|
|
244
|
+
- 能否用 lookup table、early return 或 match 替换?
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 复制粘贴变种
|
|
249
|
+
|
|
250
|
+
### 近乎重复的代码块
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
# ❌ 两个函数几乎一样,只有字段名不同
|
|
254
|
+
def format_user(user):
|
|
255
|
+
return f"{user.first_name} {user.last_name} ({user.email})"
|
|
256
|
+
|
|
257
|
+
def format_employee(emp):
|
|
258
|
+
return f"{emp.first_name} {emp.last_name} ({emp.work_email})"
|
|
259
|
+
|
|
260
|
+
# ✅ 统一抽象
|
|
261
|
+
def format_person(first: str, last: str, email: str) -> str:
|
|
262
|
+
return f"{first} {last} ({email})"
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// ❌ Copy-paste handler 只改了 URL
|
|
267
|
+
async function deletePost(id: string) {
|
|
268
|
+
await fetch(`/api/posts/${id}`, { method: "DELETE" });
|
|
269
|
+
router.push("/posts");
|
|
270
|
+
}
|
|
271
|
+
async function deleteComment(id: string) {
|
|
272
|
+
await fetch(`/api/comments/${id}`, { method: "DELETE" });
|
|
273
|
+
router.push("/comments");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ✅ 参数化
|
|
277
|
+
async function deleteResource(resource: string, id: string) {
|
|
278
|
+
await fetch(`/api/${resource}/${id}`, { method: "DELETE" });
|
|
279
|
+
router.push(`/${resource}`);
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**审查要点:**
|
|
284
|
+
- 是否有 ≥ 2 段代码仅变量名/URL/字符串不同?
|
|
285
|
+
- 能否提取参数化的共享函数?
|
|
286
|
+
- 是否可以用 template method 或 strategy 消除变种?
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## 空操作更新
|
|
291
|
+
|
|
292
|
+
### 无条件触发状态更新
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
// ❌ 每次 poll 都触发 update——即使数据未变
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
const interval = setInterval(() => {
|
|
298
|
+
fetch("/api/status").then(r => r.json()).then(setStatus);
|
|
299
|
+
}, 5000);
|
|
300
|
+
return () => clearInterval(interval);
|
|
301
|
+
}, []);
|
|
302
|
+
|
|
303
|
+
// ✅ 仅在值变化时更新
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
const interval = setInterval(() => {
|
|
306
|
+
fetch("/api/status")
|
|
307
|
+
.then(r => r.json())
|
|
308
|
+
.then(data => {
|
|
309
|
+
setStatus(prev => isEqual(prev, data) ? prev : data);
|
|
310
|
+
});
|
|
311
|
+
}, 5000);
|
|
312
|
+
return () => clearInterval(interval);
|
|
313
|
+
}, []);
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
# ❌ 每次 loop 都写 DB——即使值未变
|
|
318
|
+
for item in items:
|
|
319
|
+
item.status = compute_status(item)
|
|
320
|
+
session.commit()
|
|
321
|
+
|
|
322
|
+
# ✅ 仅在变化时写入
|
|
323
|
+
for item in items:
|
|
324
|
+
new_status = compute_status(item)
|
|
325
|
+
if item.status != new_status:
|
|
326
|
+
item.status = new_status
|
|
327
|
+
session.commit()
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**审查要点:**
|
|
331
|
+
- polling / interval / event handler 是否无条件更新?
|
|
332
|
+
- wrapper function 是否尊重 same-reference return?
|
|
333
|
+
- DB 写入是否检查了实际变化?
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## TOCTOU 竞争条件
|
|
338
|
+
|
|
339
|
+
### Time-of-Check-to-Time-of-Use
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
# ❌ 先检查后操作——中间文件可能被删除/创建
|
|
343
|
+
if os.path.exists(path):
|
|
344
|
+
with open(path) as f:
|
|
345
|
+
data = f.read()
|
|
346
|
+
|
|
347
|
+
# ✅ 直接操作 + 处理异常
|
|
348
|
+
try:
|
|
349
|
+
with open(path) as f:
|
|
350
|
+
data = f.read()
|
|
351
|
+
except FileNotFoundError:
|
|
352
|
+
data = None
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
```python
|
|
356
|
+
# ❌ 检查余额 → 扣款 两步操作不是原子的
|
|
357
|
+
if account.balance >= amount:
|
|
358
|
+
account.balance -= amount
|
|
359
|
+
|
|
360
|
+
# ✅ 原子操作或锁
|
|
361
|
+
with account.lock:
|
|
362
|
+
if account.balance < amount:
|
|
363
|
+
raise InsufficientFundsError()
|
|
364
|
+
account.balance -= amount
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
// ❌ Check-then-act 在 async 环境中不安全
|
|
369
|
+
if (!fileExists(path)) {
|
|
370
|
+
await writeFile(path, content);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ✅ 直接操作 + catch
|
|
374
|
+
try {
|
|
375
|
+
await writeFile(path, content, { flag: "wx" });
|
|
376
|
+
} catch (e) {
|
|
377
|
+
if (e.code === "EEXIST") { /* handle */ }
|
|
378
|
+
else throw e;
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**审查要点:**
|
|
383
|
+
- `if exists → operate` 模式是否可替换为 `try operate → catch`?
|
|
384
|
+
- 多步状态变更是否在事务/锁内?
|
|
385
|
+
- async 操作中 check 和 act 之间是否有 await?
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## 过度宽泛操作
|
|
390
|
+
|
|
391
|
+
### 读取过多数据
|
|
392
|
+
|
|
393
|
+
```python
|
|
394
|
+
# ❌ 读取整个文件再取第一行
|
|
395
|
+
content = Path("log.txt").read_text()
|
|
396
|
+
first_line = content.split("\n")[0]
|
|
397
|
+
|
|
398
|
+
# ✅ 只读第一行,不加载整个文件
|
|
399
|
+
with open("log.txt") as f:
|
|
400
|
+
first_line = f.readline()
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
// ❌ 加载所有 items 再过滤
|
|
405
|
+
const allItems = await db.query("SELECT * FROM orders");
|
|
406
|
+
const pending = allItems.filter(o => o.status === "pending");
|
|
407
|
+
|
|
408
|
+
// ✅ 数据库层过滤
|
|
409
|
+
const pending = await db.query(
|
|
410
|
+
"SELECT * FROM orders WHERE status = ?", ["pending"]
|
|
411
|
+
);
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
```python
|
|
415
|
+
# ❌ 读取整个列表找一条记录
|
|
416
|
+
users = list(User.objects.all())
|
|
417
|
+
user = next(u for u in users if u.id == user_id)
|
|
418
|
+
|
|
419
|
+
# ✅ 精确查询
|
|
420
|
+
user = User.objects.get(id=user_id)
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**审查要点:**
|
|
424
|
+
- 是否读取了整个集合/文件再只用一小部分?
|
|
425
|
+
- 能否将过滤推到数据库/存储层?
|
|
426
|
+
- API 调用是否支持 pagination/limit 参数?
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## 冗余状态
|
|
431
|
+
|
|
432
|
+
### 状态可以被推导
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
// ❌ 同时存储 fullName 和 firstName + lastName
|
|
436
|
+
interface User {
|
|
437
|
+
firstName: string;
|
|
438
|
+
lastName: string;
|
|
439
|
+
fullName: string; // redundant
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ✅ fullName 是推导值
|
|
443
|
+
interface User {
|
|
444
|
+
firstName: string;
|
|
445
|
+
lastName: string;
|
|
446
|
+
}
|
|
447
|
+
const fullName = `${user.firstName} ${user.lastName}`;
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
```python
|
|
451
|
+
# ❌ 缓存值在源数据变化时可能过时
|
|
452
|
+
class Order:
|
|
453
|
+
total: float
|
|
454
|
+
item_count: int # redundant if len(items) gives the same
|
|
455
|
+
items: list[Item]
|
|
456
|
+
|
|
457
|
+
# ✅ 推导或 property
|
|
458
|
+
class Order:
|
|
459
|
+
items: list[Item]
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def total(self) -> float:
|
|
463
|
+
return sum(item.price for item in self.items)
|
|
464
|
+
|
|
465
|
+
@property
|
|
466
|
+
def item_count(self) -> int:
|
|
467
|
+
return len(self.items)
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
**审查要点:**
|
|
471
|
+
- 是否有字段可以从其他字段推导?
|
|
472
|
+
- 缓存值是否有 invalidation 机制?
|
|
473
|
+
- observer/effect 是否可以替换为直接调用?
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## 通用质量审查清单
|
|
478
|
+
|
|
479
|
+
- [ ] **复用审查**: 搜索了现有 utility/helper,没有重复造轮子?
|
|
480
|
+
- [ ] **参数数量**: 函数参数 ≤ 3 个?超过则用 options object / dataclass?
|
|
481
|
+
- [ ] **抽象边界**: 返回类型没有暴露内部实现细节(ORM、HTTP client、file format)?
|
|
482
|
+
- [ ] **类型安全**: 没有 magic strings 代替已有的 enum/constant/union type?
|
|
483
|
+
- [ ] **条件深度**: 三元嵌套 ≤ 1 层?if/else 嵌套 ≤ 2 层?
|
|
484
|
+
- [ ] **DRY**: 没有 copy-paste-with-variation(≥ 2 段近似代码)?
|
|
485
|
+
- [ ] **空操作防护**: polling / interval / event handler 有 change-detection guard?
|
|
486
|
+
- [ ] **TOCTOU**: `if exists → operate` 替换为 `try operate → catch`?
|
|
487
|
+
- [ ] **数据精度**: 没有读取整个集合/文件只为了取子集?
|
|
488
|
+
- [ ] **冗余状态**: 没有可以从其他字段推导的存储字段?
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Code Review Best Practices
|
|
2
|
+
|
|
3
|
+
Comprehensive guidelines for conducting effective code reviews.
|
|
4
|
+
|
|
5
|
+
## Review Philosophy
|
|
6
|
+
|
|
7
|
+
### Goals of Code Review
|
|
8
|
+
|
|
9
|
+
**Primary Goals:**
|
|
10
|
+
- Catch bugs and edge cases before production
|
|
11
|
+
- Ensure code maintainability and readability
|
|
12
|
+
- Share knowledge across the team
|
|
13
|
+
- Enforce coding standards consistently
|
|
14
|
+
- Improve design and architecture decisions
|
|
15
|
+
|
|
16
|
+
**Secondary Goals:**
|
|
17
|
+
- Mentor junior developers
|
|
18
|
+
- Build team culture and trust
|
|
19
|
+
- Document design decisions through discussions
|
|
20
|
+
|
|
21
|
+
### What Code Review is NOT
|
|
22
|
+
|
|
23
|
+
- A gatekeeping mechanism to block progress
|
|
24
|
+
- An opportunity to show off knowledge
|
|
25
|
+
- A place to nitpick formatting (use linters)
|
|
26
|
+
- A way to rewrite code to personal preference
|
|
27
|
+
|
|
28
|
+
## Review Timing
|
|
29
|
+
|
|
30
|
+
### When to Review
|
|
31
|
+
|
|
32
|
+
| Trigger | Action |
|
|
33
|
+
|---------|--------|
|
|
34
|
+
| PR opened | Review within 24 hours, ideally same day |
|
|
35
|
+
| Changes requested | Re-review within 4 hours |
|
|
36
|
+
| Blocking issue found | Communicate immediately |
|
|
37
|
+
|
|
38
|
+
### Time Allocation
|
|
39
|
+
|
|
40
|
+
- **Small PR (<100 lines)**: 10-15 minutes
|
|
41
|
+
- **Medium PR (100-400 lines)**: 20-40 minutes
|
|
42
|
+
- **Large PR (>400 lines)**: Request to split, or 60+ minutes
|
|
43
|
+
|
|
44
|
+
## Review Depth Levels
|
|
45
|
+
|
|
46
|
+
### Level 1: Skim Review (5 minutes)
|
|
47
|
+
- Check PR description and linked issues
|
|
48
|
+
- Verify CI/CD status
|
|
49
|
+
- Look at file changes overview
|
|
50
|
+
- Identify if deeper review needed
|
|
51
|
+
|
|
52
|
+
### Level 2: Standard Review (20-30 minutes)
|
|
53
|
+
- Full code walkthrough
|
|
54
|
+
- Logic verification
|
|
55
|
+
- Test coverage check
|
|
56
|
+
- Security scan
|
|
57
|
+
|
|
58
|
+
### Level 3: Deep Review (60+ minutes)
|
|
59
|
+
- Architecture evaluation
|
|
60
|
+
- Performance analysis
|
|
61
|
+
- Security audit
|
|
62
|
+
- Edge case exploration
|
|
63
|
+
|
|
64
|
+
## Communication Guidelines
|
|
65
|
+
|
|
66
|
+
### Tone and Language
|
|
67
|
+
|
|
68
|
+
**Use collaborative language:**
|
|
69
|
+
- "What do you think about..." instead of "You should..."
|
|
70
|
+
- "Could we consider..." instead of "This is wrong"
|
|
71
|
+
- "I'm curious about..." instead of "Why didn't you..."
|
|
72
|
+
|
|
73
|
+
**Be specific and actionable:**
|
|
74
|
+
- Include code examples when suggesting changes
|
|
75
|
+
- Link to documentation or past discussions
|
|
76
|
+
- Explain the "why" behind suggestions
|
|
77
|
+
|
|
78
|
+
### Handling Disagreements
|
|
79
|
+
|
|
80
|
+
1. **Seek to understand**: Ask clarifying questions
|
|
81
|
+
2. **Acknowledge valid points**: Show you've considered their perspective
|
|
82
|
+
3. **Provide data**: Use benchmarks, docs, or examples
|
|
83
|
+
4. **Escalate if needed**: Involve senior dev or architect
|
|
84
|
+
5. **Know when to let go**: Not every hill is worth dying on
|
|
85
|
+
|
|
86
|
+
## Review Prioritization
|
|
87
|
+
|
|
88
|
+
### Must Fix (Blocking)
|
|
89
|
+
- Security vulnerabilities
|
|
90
|
+
- Data corruption risks
|
|
91
|
+
- Breaking changes without migration
|
|
92
|
+
- Critical performance issues
|
|
93
|
+
- Missing error handling for user-facing features
|
|
94
|
+
|
|
95
|
+
### Should Fix (Important)
|
|
96
|
+
- Test coverage gaps
|
|
97
|
+
- Moderate performance concerns
|
|
98
|
+
- Code duplication
|
|
99
|
+
- Unclear naming or structure
|
|
100
|
+
- Missing documentation for complex logic
|
|
101
|
+
|
|
102
|
+
### Nice to Have (Non-blocking)
|
|
103
|
+
- Style preferences beyond linting
|
|
104
|
+
- Minor optimizations
|
|
105
|
+
- Additional test cases
|
|
106
|
+
- Documentation improvements
|
|
107
|
+
|
|
108
|
+
## Anti-Patterns to Avoid
|
|
109
|
+
|
|
110
|
+
### Reviewer Anti-Patterns
|
|
111
|
+
- **Rubber stamping**: Approving without actually reviewing
|
|
112
|
+
- **Bike shedding**: Debating trivial details extensively
|
|
113
|
+
- **Scope creep**: "While you're at it, can you also..."
|
|
114
|
+
- **Ghosting**: Requesting changes then disappearing
|
|
115
|
+
- **Perfectionism**: Blocking for minor style preferences
|
|
116
|
+
|
|
117
|
+
### Author Anti-Patterns
|
|
118
|
+
- **Mega PRs**: Submitting 1000+ line changes
|
|
119
|
+
- **No context**: Missing PR description or linked issues
|
|
120
|
+
- **Defensive responses**: Arguing every suggestion
|
|
121
|
+
- **Silent updates**: Making changes without responding to comments
|
|
122
|
+
|
|
123
|
+
## Metrics and Improvement
|
|
124
|
+
|
|
125
|
+
### Track These Metrics
|
|
126
|
+
- Time to first review
|
|
127
|
+
- Review cycle time
|
|
128
|
+
- Number of review rounds
|
|
129
|
+
- Defect escape rate
|
|
130
|
+
- Review coverage percentage
|
|
131
|
+
|
|
132
|
+
### Continuous Improvement
|
|
133
|
+
- Hold retrospectives on review process
|
|
134
|
+
- Share learnings from escaped bugs
|
|
135
|
+
- Update checklists based on common issues
|
|
136
|
+
- Celebrate good reviews and catches
|