Skills included: - pr-reviewer: Adapted for Gitea/GitHub via platform-aware scripts (dropped fetch_pr_data.py and add_inline_comment.py, kept generate_review_files.py) - code-review-excellence: Methodology and checklists (React, TS, Python, etc.) - vercel-react-best-practices: 57 rules for React/Next.js performance - tailwind-design-system: Tailwind CSS v4 patterns, CVA, design tokens New shell scripts added to ~/.claude/scripts/git/: - pr-diff.sh: Get PR diff (GitHub gh / Gitea API) - pr-metadata.sh: Get PR metadata as normalized JSON Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1228 lines
31 KiB
Markdown
1228 lines
31 KiB
Markdown
# Common Bugs Checklist
|
||
|
||
Language-specific bugs and issues to watch for during code review.
|
||
|
||
## 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
|
||
|
||
### Type Issues
|
||
```typescript
|
||
// ❌ Using any defeats type safety
|
||
function process(data: any) { return data.value; }
|
||
|
||
// ✅ Use proper types
|
||
interface Data { value: string; }
|
||
function process(data: Data) { return data.value; }
|
||
```
|
||
|
||
### Async/Await Pitfalls
|
||
```typescript
|
||
// ❌ Missing await
|
||
async function fetch() {
|
||
const data = fetchData(); // Missing await!
|
||
return data.json();
|
||
}
|
||
|
||
// ❌ Unhandled promise rejection
|
||
async function risky() {
|
||
const result = await fetchData(); // No try-catch
|
||
return result;
|
||
}
|
||
|
||
// ✅ Proper error handling
|
||
async function safe() {
|
||
try {
|
||
const result = await fetchData();
|
||
return result;
|
||
} catch (error) {
|
||
console.error('Fetch failed:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
```
|
||
|
||
### React Specific
|
||
|
||
#### Hooks 规则违反
|
||
```tsx
|
||
// ❌ 条件调用 Hooks — 违反 Hooks 规则
|
||
function BadComponent({ show }) {
|
||
if (show) {
|
||
const [value, setValue] = useState(0); // Error!
|
||
}
|
||
return <div>...</div>;
|
||
}
|
||
|
||
// ✅ Hooks 必须在顶层无条件调用
|
||
function GoodComponent({ show }) {
|
||
const [value, setValue] = useState(0);
|
||
if (!show) return null;
|
||
return <div>{value}</div>;
|
||
}
|
||
|
||
// ❌ 循环中调用 Hooks
|
||
function BadLoop({ items }) {
|
||
items.forEach(item => {
|
||
const [selected, setSelected] = useState(false); // Error!
|
||
});
|
||
}
|
||
|
||
// ✅ 将状态提升或使用不同的数据结构
|
||
function GoodLoop({ items }) {
|
||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||
return items.map(item => (
|
||
<Item key={item.id} selected={selectedIds.has(item.id)} />
|
||
));
|
||
}
|
||
```
|
||
|
||
#### useEffect 常见错误
|
||
```tsx
|
||
// ❌ 依赖数组不完整 — stale closure
|
||
function StaleClosureExample({ userId, onSuccess }) {
|
||
const [data, setData] = useState(null);
|
||
useEffect(() => {
|
||
fetchData(userId).then(result => {
|
||
setData(result);
|
||
onSuccess(result); // onSuccess 可能是 stale 的!
|
||
});
|
||
}, [userId]); // 缺少 onSuccess 依赖
|
||
}
|
||
|
||
// ✅ 完整的依赖数组
|
||
useEffect(() => {
|
||
fetchData(userId).then(result => {
|
||
setData(result);
|
||
onSuccess(result);
|
||
});
|
||
}, [userId, onSuccess]);
|
||
|
||
// ❌ 无限循环 — 在 effect 中更新依赖
|
||
function InfiniteLoop() {
|
||
const [count, setCount] = useState(0);
|
||
useEffect(() => {
|
||
setCount(count + 1); // 触发重渲染,又触发 effect
|
||
}, [count]); // 无限循环!
|
||
}
|
||
|
||
// ❌ 缺少清理函数 — 内存泄漏
|
||
function MemoryLeak({ userId }) {
|
||
const [user, setUser] = useState(null);
|
||
useEffect(() => {
|
||
fetchUser(userId).then(setUser); // 组件卸载后仍然调用 setUser
|
||
}, [userId]);
|
||
}
|
||
|
||
// ✅ 正确的清理
|
||
function NoLeak({ 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 [total, setTotal] = useState(0);
|
||
useEffect(() => {
|
||
setTotal(items.reduce((a, b) => a + b.price, 0));
|
||
}, [items]); // 不必要的 effect + 额外渲染
|
||
}
|
||
|
||
// ✅ 直接计算或用 useMemo
|
||
function GoodDerived({ items }) {
|
||
const total = useMemo(
|
||
() => items.reduce((a, b) => a + b.price, 0),
|
||
[items]
|
||
);
|
||
}
|
||
|
||
// ❌ useEffect 用于事件响应
|
||
function BadEvent() {
|
||
const [query, setQuery] = useState('');
|
||
useEffect(() => {
|
||
if (query) logSearch(query); // 应该在事件处理器中
|
||
}, [query]);
|
||
}
|
||
|
||
// ✅ 副作用在事件处理器中
|
||
function GoodEvent() {
|
||
const handleSearch = (q: string) => {
|
||
setQuery(q);
|
||
logSearch(q);
|
||
};
|
||
}
|
||
```
|
||
|
||
#### useMemo / useCallback 误用
|
||
```tsx
|
||
// ❌ 过度优化 — 常量不需要 memo
|
||
function OverOptimized() {
|
||
const config = useMemo(() => ({ api: '/v1' }), []); // 无意义
|
||
const noop = useCallback(() => {}, []); // 无意义
|
||
}
|
||
|
||
// ❌ 空依赖的 useMemo(可能隐藏 bug)
|
||
function EmptyDeps({ user }) {
|
||
const greeting = useMemo(() => `Hello ${user.name}`, []);
|
||
// user 变化时 greeting 不更新!
|
||
}
|
||
|
||
// ❌ useCallback 依赖总是变化
|
||
function UselessCallback({ data }) {
|
||
const process = useCallback(() => {
|
||
return data.map(transform);
|
||
}, [data]); // 如果 data 每次都是新引用,完全无效
|
||
}
|
||
|
||
// ❌ useMemo/useCallback 没有配合 React.memo
|
||
function Parent() {
|
||
const data = useMemo(() => compute(), []);
|
||
const handler = useCallback(() => {}, []);
|
||
return <Child data={data} onClick={handler} />;
|
||
// Child 没有用 React.memo,这些优化毫无意义
|
||
}
|
||
|
||
// ✅ 正确的优化组合
|
||
const MemoChild = React.memo(function Child({ data, onClick }) {
|
||
return <button onClick={onClick}>{data}</button>;
|
||
});
|
||
|
||
function Parent() {
|
||
const data = useMemo(() => expensiveCompute(), [dep]);
|
||
const handler = useCallback(() => {}, []);
|
||
return <MemoChild data={data} onClick={handler} />;
|
||
}
|
||
```
|
||
|
||
#### 组件设计问题
|
||
```tsx
|
||
// ❌ 在组件内定义组件
|
||
function Parent() {
|
||
// 每次渲染都创建新的 Child 函数,导致完全重新挂载
|
||
const Child = () => <div>child</div>;
|
||
return <Child />;
|
||
}
|
||
|
||
// ✅ 组件定义在外部
|
||
const Child = () => <div>child</div>;
|
||
function Parent() {
|
||
return <Child />;
|
||
}
|
||
|
||
// ❌ Props 总是新引用 — 破坏 memo
|
||
function BadProps() {
|
||
return (
|
||
<MemoComponent
|
||
style={{ color: 'red' }} // 每次渲染新对象
|
||
onClick={() => handle()} // 每次渲染新函数
|
||
items={data.filter(x => x)} // 每次渲染新数组
|
||
/>
|
||
);
|
||
}
|
||
|
||
// ❌ 直接修改 props
|
||
function MutateProps({ user }) {
|
||
user.name = 'Changed'; // 永远不要这样做!
|
||
return <div>{user.name}</div>;
|
||
}
|
||
```
|
||
|
||
#### Server Components 错误 (React 19+)
|
||
```tsx
|
||
// ❌ 在 Server Component 中使用客户端 API
|
||
// app/page.tsx (默认是 Server Component)
|
||
export default function Page() {
|
||
const [count, setCount] = useState(0); // Error!
|
||
useEffect(() => {}, []); // Error!
|
||
return <button onClick={() => {}}>Click</button>; // Error!
|
||
}
|
||
|
||
// ✅ 交互逻辑移到 Client Component
|
||
// app/counter.tsx
|
||
'use client';
|
||
export function Counter() {
|
||
const [count, setCount] = useState(0);
|
||
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
||
}
|
||
|
||
// app/page.tsx
|
||
import { Counter } from './counter';
|
||
export default async function Page() {
|
||
const data = await fetchData(); // Server Component 可以直接 await
|
||
return <Counter initialCount={data.count} />;
|
||
}
|
||
|
||
// ❌ 在父组件标记 'use client',整个子树变成客户端
|
||
// layout.tsx
|
||
'use client'; // 坏主意!所有子组件都变成客户端组件
|
||
export default function Layout({ children }) { ... }
|
||
```
|
||
|
||
#### 测试常见错误
|
||
```tsx
|
||
// ❌ 使用 container 查询
|
||
const { container } = render(<Component />);
|
||
const button = container.querySelector('button'); // 不推荐
|
||
|
||
// ✅ 使用 screen 和语义查询
|
||
render(<Component />);
|
||
const button = screen.getByRole('button', { name: /submit/i });
|
||
|
||
// ❌ 使用 fireEvent
|
||
fireEvent.click(button);
|
||
|
||
// ✅ 使用 userEvent
|
||
await userEvent.click(button);
|
||
|
||
// ❌ 测试实现细节
|
||
expect(component.state.isOpen).toBe(true);
|
||
|
||
// ✅ 测试行为
|
||
expect(screen.getByRole('dialog')).toBeVisible();
|
||
|
||
// ❌ 等待同步查询
|
||
await screen.getByText('Hello'); // getBy 是同步的
|
||
|
||
// ✅ 异步用 findBy
|
||
await screen.findByText('Hello'); // findBy 会等待
|
||
```
|
||
|
||
### React Common Mistakes Checklist
|
||
- [ ] Hooks 不在顶层调用(条件/循环中)
|
||
- [ ] useEffect 依赖数组不完整
|
||
- [ ] useEffect 缺少清理函数
|
||
- [ ] useEffect 用于派生状态计算
|
||
- [ ] useMemo/useCallback 过度使用
|
||
- [ ] useMemo/useCallback 没配合 React.memo
|
||
- [ ] 在组件内定义子组件
|
||
- [ ] Props 是新对象/函数引用(传给 memo 组件时)
|
||
- [ ] 直接修改 props
|
||
- [ ] 列表缺少 key 或用 index 作为 key
|
||
- [ ] Server Component 使用客户端 API
|
||
- [ ] 'use client' 放在父组件导致整个树客户端化
|
||
- [ ] 测试使用 container 查询而非 screen
|
||
- [ ] 测试实现细节而非行为
|
||
|
||
### React 19 Actions & Forms 错误
|
||
|
||
```tsx
|
||
// === useActionState 错误 ===
|
||
|
||
// ❌ 在 Action 中直接 setState 而不是返回状态
|
||
const [state, action] = useActionState(async (prev, formData) => {
|
||
setSomeState(newValue); // 错误!应该返回新状态
|
||
}, initialState);
|
||
|
||
// ✅ 返回新状态
|
||
const [state, action] = useActionState(async (prev, formData) => {
|
||
const result = await submitForm(formData);
|
||
return { ...prev, data: result }; // 返回新状态
|
||
}, initialState);
|
||
|
||
// ❌ 忘记处理 isPending
|
||
const [state, action] = useActionState(submitAction, null);
|
||
return <button>Submit</button>; // 用户可以重复点击
|
||
|
||
// ✅ 使用 isPending 禁用按钮
|
||
const [state, action, isPending] = useActionState(submitAction, null);
|
||
return <button disabled={isPending}>Submit</button>;
|
||
|
||
// === useFormStatus 错误 ===
|
||
|
||
// ❌ 在 form 同级调用 useFormStatus
|
||
function Form() {
|
||
const { pending } = useFormStatus(); // 永远是 undefined!
|
||
return <form><button disabled={pending}>Submit</button></form>;
|
||
}
|
||
|
||
// ✅ 在子组件中调用
|
||
function SubmitButton() {
|
||
const { pending } = useFormStatus();
|
||
return <button disabled={pending}>Submit</button>;
|
||
}
|
||
function Form() {
|
||
return <form><SubmitButton /></form>;
|
||
}
|
||
|
||
// === useOptimistic 错误 ===
|
||
|
||
// ❌ 用于关键业务操作
|
||
function PaymentButton() {
|
||
const [optimisticPaid, setPaid] = useOptimistic(false);
|
||
const handlePay = async () => {
|
||
setPaid(true); // 危险:显示已支付但可能失败
|
||
await processPayment();
|
||
};
|
||
}
|
||
|
||
// ❌ 没有处理回滚后的 UI 状态
|
||
const [optimisticLikes, addLike] = useOptimistic(likes);
|
||
// 失败后 UI 回滚,但用户可能困惑为什么点赞消失了
|
||
|
||
// ✅ 提供失败反馈
|
||
const handleLike = async () => {
|
||
addLike(1);
|
||
try {
|
||
await likePost();
|
||
} catch {
|
||
toast.error('点赞失败,请重试'); // 通知用户
|
||
}
|
||
};
|
||
```
|
||
|
||
### React 19 Forms Checklist
|
||
- [ ] useActionState 返回新状态而不是 setState
|
||
- [ ] useActionState 正确使用 isPending 禁用提交
|
||
- [ ] useFormStatus 在 form 子组件中调用
|
||
- [ ] useOptimistic 不用于关键业务(支付、删除等)
|
||
- [ ] useOptimistic 失败时有用户反馈
|
||
- [ ] Server Action 正确标记 'use server'
|
||
|
||
### Suspense & Streaming 错误
|
||
|
||
```tsx
|
||
// === Suspense 边界错误 ===
|
||
|
||
// ❌ 整个页面一个 Suspense——慢内容阻塞快内容
|
||
function BadPage() {
|
||
return (
|
||
<Suspense fallback={<FullPageLoader />}>
|
||
<FastHeader /> {/* 快 */}
|
||
<SlowMainContent /> {/* 慢——阻塞整个页面 */}
|
||
<FastFooter /> {/* 快 */}
|
||
</Suspense>
|
||
);
|
||
}
|
||
|
||
// ✅ 独立边界,互不阻塞
|
||
function GoodPage() {
|
||
return (
|
||
<>
|
||
<FastHeader />
|
||
<Suspense fallback={<ContentSkeleton />}>
|
||
<SlowMainContent />
|
||
</Suspense>
|
||
<FastFooter />
|
||
</>
|
||
);
|
||
}
|
||
|
||
// ❌ 没有 Error Boundary
|
||
function NoErrorHandling() {
|
||
return (
|
||
<Suspense fallback={<Loading />}>
|
||
<DataFetcher /> {/* 抛错导致白屏 */}
|
||
</Suspense>
|
||
);
|
||
}
|
||
|
||
// ✅ Error Boundary + Suspense
|
||
function WithErrorHandling() {
|
||
return (
|
||
<ErrorBoundary fallback={<ErrorFallback />}>
|
||
<Suspense fallback={<Loading />}>
|
||
<DataFetcher />
|
||
</Suspense>
|
||
</ErrorBoundary>
|
||
);
|
||
}
|
||
|
||
// === use() Hook 错误 ===
|
||
|
||
// ❌ 在组件外创建 Promise(每次渲染新 Promise)
|
||
function BadUse() {
|
||
const data = use(fetchData()); // 每次渲染都创建新 Promise!
|
||
return <div>{data}</div>;
|
||
}
|
||
|
||
// ✅ 在父组件创建,通过 props 传递
|
||
function Parent() {
|
||
const dataPromise = useMemo(() => fetchData(), []);
|
||
return <Child dataPromise={dataPromise} />;
|
||
}
|
||
function Child({ dataPromise }) {
|
||
const data = use(dataPromise);
|
||
return <div>{data}</div>;
|
||
}
|
||
|
||
// === Next.js Streaming 错误 ===
|
||
|
||
// ❌ 在 layout.tsx 中 await 慢数据——阻塞所有子页面
|
||
// app/layout.tsx
|
||
export default async function Layout({ children }) {
|
||
const config = await fetchSlowConfig(); // 阻塞整个应用!
|
||
return <ConfigProvider value={config}>{children}</ConfigProvider>;
|
||
}
|
||
|
||
// ✅ 将慢数据放在页面级别或使用 Suspense
|
||
// app/layout.tsx
|
||
export default function Layout({ children }) {
|
||
return (
|
||
<Suspense fallback={<ConfigSkeleton />}>
|
||
<ConfigProvider>{children}</ConfigProvider>
|
||
</Suspense>
|
||
);
|
||
}
|
||
```
|
||
|
||
### Suspense Checklist
|
||
- [ ] 慢内容有独立的 Suspense 边界
|
||
- [ ] 每个 Suspense 有对应的 Error Boundary
|
||
- [ ] fallback 是有意义的骨架屏(不是简单 spinner)
|
||
- [ ] use() 的 Promise 不在渲染时创建
|
||
- [ ] 没有在 layout 中 await 慢数据
|
||
- [ ] 嵌套层级不超过 3 层
|
||
|
||
### TanStack Query 错误
|
||
|
||
```tsx
|
||
// === 查询配置错误 ===
|
||
|
||
// ❌ queryKey 不包含查询参数
|
||
function BadQuery({ userId, filters }) {
|
||
const { data } = useQuery({
|
||
queryKey: ['users'], // 缺少 userId 和 filters!
|
||
queryFn: () => fetchUsers(userId, filters),
|
||
});
|
||
// userId 或 filters 变化时数据不会更新
|
||
}
|
||
|
||
// ✅ queryKey 包含所有影响数据的参数
|
||
function GoodQuery({ userId, filters }) {
|
||
const { data } = useQuery({
|
||
queryKey: ['users', userId, filters],
|
||
queryFn: () => fetchUsers(userId, filters),
|
||
});
|
||
}
|
||
|
||
// ❌ staleTime: 0 导致过度请求
|
||
const { data } = useQuery({
|
||
queryKey: ['data'],
|
||
queryFn: fetchData,
|
||
// 默认 staleTime: 0,每次组件挂载/窗口聚焦都会 refetch
|
||
});
|
||
|
||
// ✅ 设置合理的 staleTime
|
||
const { data } = useQuery({
|
||
queryKey: ['data'],
|
||
queryFn: fetchData,
|
||
staleTime: 5 * 60 * 1000, // 5 分钟内不会自动 refetch
|
||
});
|
||
|
||
// === useSuspenseQuery 错误 ===
|
||
|
||
// ❌ useSuspenseQuery + enabled(不支持)
|
||
const { data } = useSuspenseQuery({
|
||
queryKey: ['user', userId],
|
||
queryFn: () => fetchUser(userId),
|
||
enabled: !!userId, // 错误!useSuspenseQuery 不支持 enabled
|
||
});
|
||
|
||
// ✅ 条件渲染实现
|
||
function UserQuery({ userId }) {
|
||
const { data } = useSuspenseQuery({
|
||
queryKey: ['user', userId],
|
||
queryFn: () => fetchUser(userId),
|
||
});
|
||
return <UserProfile user={data} />;
|
||
}
|
||
|
||
function Parent({ userId }) {
|
||
if (!userId) return <SelectUser />;
|
||
return (
|
||
<Suspense fallback={<UserSkeleton />}>
|
||
<UserQuery userId={userId} />
|
||
</Suspense>
|
||
);
|
||
}
|
||
|
||
// === Mutation 错误 ===
|
||
|
||
// ❌ Mutation 成功后不 invalidate 查询
|
||
const mutation = useMutation({
|
||
mutationFn: updateUser,
|
||
// 忘记 invalidate,UI 显示旧数据
|
||
});
|
||
|
||
// ✅ 成功后 invalidate 相关查询
|
||
const mutation = useMutation({
|
||
mutationFn: updateUser,
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||
},
|
||
});
|
||
|
||
// ❌ 乐观更新不处理回滚
|
||
const mutation = useMutation({
|
||
mutationFn: updateTodo,
|
||
onMutate: async (newTodo) => {
|
||
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
|
||
// 没有保存旧数据,失败后无法回滚!
|
||
},
|
||
});
|
||
|
||
// ✅ 完整的乐观更新
|
||
const mutation = useMutation({
|
||
mutationFn: updateTodo,
|
||
onMutate: async (newTodo) => {
|
||
await queryClient.cancelQueries({ queryKey: ['todos'] });
|
||
const previous = queryClient.getQueryData(['todos']);
|
||
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
|
||
return { previous };
|
||
},
|
||
onError: (err, newTodo, context) => {
|
||
queryClient.setQueryData(['todos'], context.previous);
|
||
},
|
||
onSettled: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
||
},
|
||
});
|
||
|
||
// === v5 迁移错误 ===
|
||
|
||
// ❌ 使用废弃的 API
|
||
const { data, isLoading } = useQuery(['key'], fetchFn); // v4 语法
|
||
|
||
// ✅ v5 单一对象参数
|
||
const { data, isPending } = useQuery({
|
||
queryKey: ['key'],
|
||
queryFn: fetchFn,
|
||
});
|
||
|
||
// ❌ 混淆 isPending 和 isLoading
|
||
if (isLoading) return <Spinner />;
|
||
// v5 中 isLoading = isPending && isFetching
|
||
|
||
// ✅ 根据意图选择
|
||
if (isPending) return <Spinner />; // 没有缓存数据
|
||
// 或
|
||
if (isFetching) return <Refreshing />; // 正在后台刷新
|
||
```
|
||
|
||
### TanStack Query Checklist
|
||
- [ ] queryKey 包含所有影响数据的参数
|
||
- [ ] 设置了合理的 staleTime(不是默认 0)
|
||
- [ ] useSuspenseQuery 不使用 enabled
|
||
- [ ] Mutation 成功后 invalidate 相关查询
|
||
- [ ] 乐观更新有完整的回滚逻辑
|
||
- [ ] v5 使用单一对象参数语法
|
||
- [ ] 理解 isPending vs isLoading vs isFetching
|
||
|
||
### TypeScript/JavaScript Common Mistakes
|
||
- [ ] `==` instead of `===`
|
||
- [ ] Modifying array/object during iteration
|
||
- [ ] `this` context lost in callbacks
|
||
- [ ] Missing `key` prop in lists
|
||
- [ ] Closure capturing loop variable
|
||
- [ ] parseInt without radix parameter
|
||
|
||
## Vue 3
|
||
|
||
### 响应性丢失
|
||
```vue
|
||
<!-- ❌ 解构 reactive 丢失响应性 -->
|
||
<script setup>
|
||
const state = reactive({ count: 0 })
|
||
const { count } = state // count 不是响应式的!
|
||
</script>
|
||
|
||
<!-- ✅ 使用 toRefs -->
|
||
<script setup>
|
||
const state = reactive({ count: 0 })
|
||
const { count } = toRefs(state) // count.value 是响应式的
|
||
</script>
|
||
```
|
||
|
||
### Props 响应性传递
|
||
```vue
|
||
<!-- ❌ 传递 props 值到 composable 丢失响应性 -->
|
||
<script setup>
|
||
const props = defineProps<{ id: string }>()
|
||
const { data } = useFetch(props.id) // id 变化时不会重新获取!
|
||
</script>
|
||
|
||
<!-- ✅ 使用 toRef 或 getter -->
|
||
<script setup>
|
||
const props = defineProps<{ id: string }>()
|
||
const { data } = useFetch(() => props.id) // getter 保持响应性
|
||
// 或
|
||
const { data } = useFetch(toRef(props, 'id'))
|
||
</script>
|
||
```
|
||
|
||
### Watch 清理
|
||
```vue
|
||
<!-- ❌ 异步 watch 无清理,导致竞态 -->
|
||
<script setup>
|
||
watch(id, async (newId) => {
|
||
const data = await fetchData(newId)
|
||
result.value = data // 旧请求可能覆盖新结果!
|
||
})
|
||
</script>
|
||
|
||
<!-- ✅ 使用 onCleanup 取消旧请求 -->
|
||
<script setup>
|
||
watch(id, async (newId, _, onCleanup) => {
|
||
const controller = new AbortController()
|
||
onCleanup(() => controller.abort())
|
||
|
||
const data = await fetchData(newId, controller.signal)
|
||
result.value = data
|
||
})
|
||
</script>
|
||
```
|
||
|
||
### Computed 副作用
|
||
```vue
|
||
<!-- ❌ computed 中修改其他状态 -->
|
||
<script setup>
|
||
const total = computed(() => {
|
||
sideEffect.value++ // 副作用!每次访问都会执行
|
||
return items.value.reduce((a, b) => a + b, 0)
|
||
})
|
||
</script>
|
||
|
||
<!-- ✅ computed 只做纯计算 -->
|
||
<script setup>
|
||
const total = computed(() => {
|
||
return items.value.reduce((a, b) => a + b, 0)
|
||
})
|
||
// 副作用放 watch
|
||
watch(total, () => { sideEffect.value++ })
|
||
</script>
|
||
```
|
||
|
||
### 模板常见错误
|
||
```vue
|
||
<!-- ❌ v-if 和 v-for 同时使用(v-if 优先级更高) -->
|
||
<template>
|
||
<div v-for="item in items" v-if="item.visible" :key="item.id">
|
||
{{ item.name }}
|
||
</div>
|
||
</template>
|
||
|
||
<!-- ✅ 使用 computed 或 template 包裹 -->
|
||
<template>
|
||
<template v-for="item in items" :key="item.id">
|
||
<div v-if="item.visible">{{ item.name }}</div>
|
||
</template>
|
||
</template>
|
||
```
|
||
|
||
### Common Mistakes
|
||
- [ ] 解构 reactive 对象丢失响应性
|
||
- [ ] props 传递给 composable 时未保持响应性
|
||
- [ ] watch 异步回调无清理函数
|
||
- [ ] computed 中产生副作用
|
||
- [ ] v-for 使用 index 作为 key(列表会重排时)
|
||
- [ ] v-if 和 v-for 在同一元素上
|
||
- [ ] defineProps 未使用 TypeScript 类型声明
|
||
- [ ] withDefaults 对象默认值未使用工厂函数
|
||
- [ ] 直接修改 props(而不是 emit)
|
||
- [ ] watchEffect 依赖不明确导致过度触发
|
||
|
||
## Python
|
||
|
||
### Mutable Default Arguments
|
||
```python
|
||
# ❌ Bug: List shared across all calls
|
||
def add_item(item, items=[]):
|
||
items.append(item)
|
||
return items
|
||
|
||
# ✅ Correct
|
||
def add_item(item, items=None):
|
||
if items is None:
|
||
items = []
|
||
items.append(item)
|
||
return items
|
||
```
|
||
|
||
### Exception Handling
|
||
```python
|
||
# ❌ Catching everything, including KeyboardInterrupt
|
||
try:
|
||
risky_operation()
|
||
except:
|
||
pass
|
||
|
||
# ✅ Catch specific exceptions
|
||
try:
|
||
risky_operation()
|
||
except ValueError as e:
|
||
logger.error(f"Invalid value: {e}")
|
||
raise
|
||
```
|
||
|
||
### Class Attributes
|
||
```python
|
||
# ❌ Shared mutable class attribute
|
||
class User:
|
||
permissions = [] # Shared across all instances!
|
||
|
||
# ✅ Initialize in __init__
|
||
class User:
|
||
def __init__(self):
|
||
self.permissions = []
|
||
```
|
||
|
||
### Common Mistakes
|
||
- [ ] 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)
|
||
|
||
## Rust
|
||
|
||
### 所有权与借用
|
||
|
||
```rust
|
||
// ❌ Use after move
|
||
let s = String::from("hello");
|
||
let s2 = s;
|
||
println!("{}", s); // Error: s was moved
|
||
|
||
// ✅ Clone if needed (but consider if clone is necessary)
|
||
let s = String::from("hello");
|
||
let s2 = s.clone();
|
||
println!("{}", s); // OK
|
||
|
||
// ❌ 用 clone() 绕过借用检查器(反模式)
|
||
fn process(data: &Data) {
|
||
let owned = data.clone(); // 不必要的 clone
|
||
do_something(owned);
|
||
}
|
||
|
||
// ✅ 正确使用借用
|
||
fn process(data: &Data) {
|
||
do_something(data); // 传递引用
|
||
}
|
||
|
||
// ❌ 在结构体中存储借用(通常是坏主意)
|
||
struct Parser<'a> {
|
||
input: &'a str, // 生命周期复杂化
|
||
position: usize,
|
||
}
|
||
|
||
// ✅ 使用拥有的数据
|
||
struct Parser {
|
||
input: String, // 拥有数据,简化生命周期
|
||
position: usize,
|
||
}
|
||
|
||
// ❌ 迭代时修改集合
|
||
let mut vec = vec![1, 2, 3];
|
||
for item in &vec {
|
||
vec.push(*item); // Error: cannot borrow as mutable
|
||
}
|
||
|
||
// ✅ 收集到新集合
|
||
let vec = vec![1, 2, 3];
|
||
let new_vec: Vec<_> = vec.iter().map(|x| x * 2).collect();
|
||
```
|
||
|
||
### Unsafe 代码审查
|
||
|
||
```rust
|
||
// ❌ unsafe 没有安全注释
|
||
unsafe {
|
||
ptr::write(dest, value);
|
||
}
|
||
|
||
// ✅ 必须有 SAFETY 注释说明不变量
|
||
// SAFETY: dest 指针由 Vec::as_mut_ptr() 获得,保证:
|
||
// 1. 指针有效且已对齐
|
||
// 2. 目标内存未被其他引用借用
|
||
// 3. 写入不会超出分配的容量
|
||
unsafe {
|
||
ptr::write(dest, value);
|
||
}
|
||
|
||
// ❌ unsafe fn 没有 # Safety 文档
|
||
pub unsafe fn from_raw_parts(ptr: *mut T, len: usize) -> Self { ... }
|
||
|
||
// ✅ 必须文档化安全契约
|
||
/// Creates a new instance from raw parts.
|
||
///
|
||
/// # Safety
|
||
///
|
||
/// - `ptr` must have been allocated via `GlobalAlloc`
|
||
/// - `len` must be less than or equal to the allocated capacity
|
||
/// - The caller must ensure no other references to the memory exist
|
||
pub unsafe fn from_raw_parts(ptr: *mut T, len: usize) -> Self { ... }
|
||
|
||
// ❌ 跨模块 unsafe 不变量
|
||
mod a {
|
||
pub fn set_flag() { FLAG = true; } // 安全代码影响 unsafe
|
||
}
|
||
mod b {
|
||
pub unsafe fn do_thing() {
|
||
if FLAG { /* assumes FLAG means something */ }
|
||
}
|
||
}
|
||
|
||
// ✅ 将 unsafe 边界封装在单一模块
|
||
mod safe_wrapper {
|
||
// 所有 unsafe 逻辑在一个模块内
|
||
// 对外提供 safe API
|
||
}
|
||
```
|
||
|
||
### 异步/并发
|
||
|
||
```rust
|
||
// ❌ 在异步上下文中阻塞
|
||
async fn bad_fetch(url: &str) -> Result<String> {
|
||
let resp = reqwest::blocking::get(url)?; // 阻塞整个运行时!
|
||
Ok(resp.text()?)
|
||
}
|
||
|
||
// ✅ 使用异步版本
|
||
async fn good_fetch(url: &str) -> Result<String> {
|
||
let resp = reqwest::get(url).await?;
|
||
Ok(resp.text().await?)
|
||
}
|
||
|
||
// ❌ 跨 .await 持有 Mutex
|
||
async fn bad_lock(mutex: &Mutex<Data>) {
|
||
let guard = mutex.lock().unwrap();
|
||
some_async_op().await; // 持锁跨越 await!
|
||
drop(guard);
|
||
}
|
||
|
||
// ✅ 缩短锁持有时间
|
||
async fn good_lock(mutex: &Mutex<Data>) {
|
||
let data = {
|
||
let guard = mutex.lock().unwrap();
|
||
guard.clone() // 获取数据后立即释放锁
|
||
};
|
||
some_async_op().await;
|
||
// 处理 data
|
||
}
|
||
|
||
// ❌ 在异步函数中使用 std::sync::Mutex
|
||
async fn bad_async_mutex(mutex: &std::sync::Mutex<Data>) {
|
||
let _guard = mutex.lock().unwrap(); // 可能死锁
|
||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||
}
|
||
|
||
// ✅ 使用 tokio::sync::Mutex(如果必须跨 await)
|
||
async fn good_async_mutex(mutex: &tokio::sync::Mutex<Data>) {
|
||
let _guard = mutex.lock().await;
|
||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||
}
|
||
|
||
// ❌ 忘记 Future 是惰性的
|
||
fn bad_spawn() {
|
||
let future = async_operation(); // 没有执行!
|
||
// future 被丢弃,什么都没发生
|
||
}
|
||
|
||
// ✅ 必须 await 或 spawn
|
||
async fn good_spawn() {
|
||
async_operation().await; // 执行
|
||
// 或
|
||
tokio::spawn(async_operation()); // 后台执行
|
||
}
|
||
|
||
// ❌ spawn 任务缺少 'static
|
||
async fn bad_spawn_lifetime(data: &str) {
|
||
tokio::spawn(async {
|
||
println!("{}", data); // Error: data 不是 'static
|
||
});
|
||
}
|
||
|
||
// ✅ 使用 move 或 Arc
|
||
async fn good_spawn_lifetime(data: String) {
|
||
tokio::spawn(async move {
|
||
println!("{}", data); // OK: 拥有数据
|
||
});
|
||
}
|
||
```
|
||
|
||
### 错误处理
|
||
|
||
```rust
|
||
// ❌ 生产代码中使用 unwrap/expect
|
||
fn bad_parse(input: &str) -> i32 {
|
||
input.parse().unwrap() // panic!
|
||
}
|
||
|
||
// ✅ 正确传播错误
|
||
fn good_parse(input: &str) -> Result<i32, ParseIntError> {
|
||
input.parse()
|
||
}
|
||
|
||
// ❌ 吞掉错误信息
|
||
fn bad_error_handling() -> Result<()> {
|
||
match operation() {
|
||
Ok(v) => Ok(v),
|
||
Err(_) => Err(anyhow!("operation failed")) // 丢失原始错误
|
||
}
|
||
}
|
||
|
||
// ✅ 使用 context 添加上下文
|
||
fn good_error_handling() -> Result<()> {
|
||
operation().context("failed to perform operation")?;
|
||
Ok(())
|
||
}
|
||
|
||
// ❌ 库代码使用 anyhow(应该用 thiserror)
|
||
// lib.rs
|
||
pub fn parse_config(path: &str) -> anyhow::Result<Config> {
|
||
// 调用者无法区分错误类型
|
||
}
|
||
|
||
// ✅ 库代码用 thiserror 定义错误类型
|
||
#[derive(Debug, thiserror::Error)]
|
||
pub enum ConfigError {
|
||
#[error("failed to read config file: {0}")]
|
||
Io(#[from] std::io::Error),
|
||
#[error("invalid config format: {0}")]
|
||
Parse(#[from] serde_json::Error),
|
||
}
|
||
|
||
pub fn parse_config(path: &str) -> Result<Config, ConfigError> {
|
||
// 调用者可以 match 不同错误
|
||
}
|
||
|
||
// ❌ 忽略 must_use 返回值
|
||
fn bad_ignore_result() {
|
||
some_fallible_operation(); // 警告:unused Result
|
||
}
|
||
|
||
// ✅ 显式处理或标记忽略
|
||
fn good_handle_result() {
|
||
let _ = some_fallible_operation(); // 显式忽略
|
||
// 或
|
||
some_fallible_operation().ok(); // 转换为 Option
|
||
}
|
||
```
|
||
|
||
### 性能陷阱
|
||
|
||
```rust
|
||
// ❌ 不必要的 collect
|
||
fn bad_process(items: &[i32]) -> i32 {
|
||
items.iter()
|
||
.filter(|x| **x > 0)
|
||
.collect::<Vec<_>>() // 不必要的分配
|
||
.iter()
|
||
.sum()
|
||
}
|
||
|
||
// ✅ 惰性迭代
|
||
fn good_process(items: &[i32]) -> i32 {
|
||
items.iter()
|
||
.filter(|x| **x > 0)
|
||
.sum()
|
||
}
|
||
|
||
// ❌ 循环中重复分配
|
||
fn bad_loop() -> String {
|
||
let mut result = String::new();
|
||
for i in 0..1000 {
|
||
result = result + &i.to_string(); // 每次迭代都重新分配!
|
||
}
|
||
result
|
||
}
|
||
|
||
// ✅ 预分配或使用 push_str
|
||
fn good_loop() -> String {
|
||
let mut result = String::with_capacity(4000); // 预分配
|
||
for i in 0..1000 {
|
||
write!(result, "{}", i).unwrap(); // 原地追加
|
||
}
|
||
result
|
||
}
|
||
|
||
// ❌ 过度使用 clone
|
||
fn bad_clone(data: &HashMap<String, Vec<u8>>) -> Vec<u8> {
|
||
data.get("key").cloned().unwrap_or_default()
|
||
}
|
||
|
||
// ✅ 返回引用或使用 Cow
|
||
fn good_ref(data: &HashMap<String, Vec<u8>>) -> &[u8] {
|
||
data.get("key").map(|v| v.as_slice()).unwrap_or(&[])
|
||
}
|
||
|
||
// ❌ 大结构体按值传递
|
||
fn bad_pass(data: LargeStruct) { ... } // 拷贝整个结构体
|
||
|
||
// ✅ 传递引用
|
||
fn good_pass(data: &LargeStruct) { ... }
|
||
|
||
// ❌ Box<dyn Trait> 用于小型已知类型
|
||
fn bad_trait_object() -> Box<dyn Iterator<Item = i32>> {
|
||
Box::new(vec![1, 2, 3].into_iter())
|
||
}
|
||
|
||
// ✅ 使用 impl Trait
|
||
fn good_impl_trait() -> impl Iterator<Item = i32> {
|
||
vec![1, 2, 3].into_iter()
|
||
}
|
||
|
||
// ❌ retain 比 filter+collect 慢(某些场景)
|
||
vec.retain(|x| x.is_valid()); // O(n) 但常数因子大
|
||
|
||
// ✅ 如果不需要原地修改,考虑 filter
|
||
let vec: Vec<_> = vec.into_iter().filter(|x| x.is_valid()).collect();
|
||
```
|
||
|
||
### 生命周期与引用
|
||
|
||
```rust
|
||
// ❌ 返回局部变量的引用
|
||
fn bad_return_ref() -> &str {
|
||
let s = String::from("hello");
|
||
&s // Error: s will be dropped
|
||
}
|
||
|
||
// ✅ 返回拥有的数据或静态引用
|
||
fn good_return_owned() -> String {
|
||
String::from("hello")
|
||
}
|
||
|
||
// ❌ 生命周期过度泛化
|
||
fn bad_lifetime<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
|
||
x // 'b 没有被使用
|
||
}
|
||
|
||
// ✅ 简化生命周期
|
||
fn good_lifetime(x: &str, _y: &str) -> &str {
|
||
x // 编译器自动推断
|
||
}
|
||
|
||
// ❌ 结构体持有多个相关引用但生命周期独立
|
||
struct Bad<'a, 'b> {
|
||
name: &'a str,
|
||
data: &'b [u8], // 通常应该是同一个生命周期
|
||
}
|
||
|
||
// ✅ 相关数据使用相同生命周期
|
||
struct Good<'a> {
|
||
name: &'a str,
|
||
data: &'a [u8],
|
||
}
|
||
```
|
||
|
||
### Rust 审查清单
|
||
|
||
**所有权与借用**
|
||
- [ ] clone() 是有意为之,不是绕过借用检查器
|
||
- [ ] 避免在结构体中存储借用(除非必要)
|
||
- [ ] Rc/Arc 使用合理,没有隐藏不必要的共享状态
|
||
- [ ] 没有不必要的 RefCell(运行时检查 vs 编译时)
|
||
|
||
**Unsafe 代码**
|
||
- [ ] 每个 unsafe 块有 SAFETY 注释
|
||
- [ ] unsafe fn 有 # Safety 文档
|
||
- [ ] 安全不变量被清晰记录
|
||
- [ ] unsafe 边界尽可能小
|
||
|
||
**异步/并发**
|
||
- [ ] 没有在异步上下文中阻塞
|
||
- [ ] 没有跨 .await 持有 std::sync 锁
|
||
- [ ] spawn 的任务满足 'static 约束
|
||
- [ ] Future 被正确 await 或 spawn
|
||
- [ ] 锁的顺序一致(避免死锁)
|
||
|
||
**错误处理**
|
||
- [ ] 库代码使用 thiserror,应用代码使用 anyhow
|
||
- [ ] 错误有足够的上下文信息
|
||
- [ ] 没有在生产代码中 unwrap/expect
|
||
- [ ] must_use 返回值被正确处理
|
||
|
||
**性能**
|
||
- [ ] 避免不必要的 collect()
|
||
- [ ] 大数据结构传引用
|
||
- [ ] 字符串拼接使用 String::with_capacity 或 write!
|
||
- [ ] impl Trait 优于 Box<dyn Trait>(当可能时)
|
||
|
||
**类型系统**
|
||
- [ ] 善用 newtype 模式增加类型安全
|
||
- [ ] 枚举穷尽匹配(没有 _ 通配符隐藏新变体)
|
||
- [ ] 生命周期尽可能简化
|
||
|
||
## SQL
|
||
|
||
### Injection Vulnerabilities
|
||
```sql
|
||
-- ❌ String concatenation (SQL injection risk)
|
||
query = "SELECT * FROM users WHERE id = " + user_id
|
||
|
||
-- ✅ Parameterized queries
|
||
query = "SELECT * FROM users WHERE id = ?"
|
||
cursor.execute(query, (user_id,))
|
||
```
|
||
|
||
### Performance Issues
|
||
- [ ] Missing indexes on filtered/joined columns
|
||
- [ ] SELECT * instead of specific columns
|
||
- [ ] N+1 query patterns
|
||
- [ ] Missing LIMIT on large tables
|
||
- [ ] Inefficient subqueries vs JOINs
|
||
|
||
### Common Mistakes
|
||
- [ ] Not handling NULL comparisons correctly
|
||
- [ ] Missing transactions for related operations
|
||
- [ ] Incorrect JOIN types
|
||
- [ ] Case sensitivity issues
|
||
- [ ] Date/timezone handling errors
|
||
|
||
## API Design
|
||
|
||
### REST Issues
|
||
- [ ] Inconsistent resource naming
|
||
- [ ] Wrong HTTP methods (POST for idempotent operations)
|
||
- [ ] Missing pagination for list endpoints
|
||
- [ ] Incorrect status codes
|
||
- [ ] Missing rate limiting
|
||
|
||
### Data Validation
|
||
- [ ] Missing input validation
|
||
- [ ] Incorrect data type validation
|
||
- [ ] Missing length/range checks
|
||
- [ ] Not sanitizing user input
|
||
- [ ] Trusting client-side validation
|
||
|
||
## Testing
|
||
|
||
### Test Quality Issues
|
||
- [ ] Testing implementation details instead of behavior
|
||
- [ ] Missing edge case tests
|
||
- [ ] Flaky tests (non-deterministic)
|
||
- [ ] Tests with external dependencies
|
||
- [ ] Missing negative tests (error cases)
|
||
- [ ] Overly complex test setup
|