diff --git a/.claude/skills/code-review-skill b/.claude/skills/code-review-skill deleted file mode 160000 index 17267ff..0000000 --- a/.claude/skills/code-review-skill +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 17267ff26b95dcea28c3dae76f9931cdf9604520 diff --git a/.claude/skills/code-review-skill/.gitignore b/.claude/skills/code-review-skill/.gitignore new file mode 100644 index 0000000..3f7a9f0 --- /dev/null +++ b/.claude/skills/code-review-skill/.gitignore @@ -0,0 +1,25 @@ +# OS files +.DS_Store +Thumbs.db + +# Editor files +*.swp +*.swo +*~ +.idea/ +.vscode/ + +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.eggs/ +dist/ +build/ + +# Logs +*.log + +# Local config +.env +.env.local diff --git a/.claude/skills/code-review-skill/.nojekyll b/.claude/skills/code-review-skill/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/.claude/skills/code-review-skill/CONTRIBUTING.md b/.claude/skills/code-review-skill/CONTRIBUTING.md new file mode 100644 index 0000000..04acd97 --- /dev/null +++ b/.claude/skills/code-review-skill/CONTRIBUTING.md @@ -0,0 +1,348 @@ +# Contributing to AI Code Review Guide + +Thank you for your interest in contributing! This document provides guidelines for contributing to this Claude Code Skill project. + +## Claude Code Skill 开发规范 + +本项目是一个 Claude Code Skill,贡献者需要遵循以下规范。 + +### 目录结构 + +``` +code-review-skill/ +├── SKILL.md # Required: main file (always loaded) +├── README.md +├── CONTRIBUTING.md +├── LICENSE +├── reference/ # On-demand language/framework guides +│ ├── react.md # React 19 / Next.js / TanStack Query v5 +│ ├── vue.md # Vue 3.5 Composition API +│ ├── angular.md # Angular 17+, Signals, Standalone, RxJS +│ ├── svelte.md # Svelte 5 / SvelteKit, runes, SSR boundary +│ ├── rust.md # Ownership, async, unsafe, cancellation +│ ├── typescript.md # Type safety, generics, strict mode +│ ├── nestjs.md # NestJS DI, modules, Guards/Pipes, DTOs +│ ├── python.md # Type hints, async, testing +│ ├── django.md # Django / DRF, N+1, serializers, async views +│ ├── fastapi.md # FastAPI, Depends, Pydantic v2, async +│ ├── java.md # Java 17/21, Spring Boot 3, virtual threads +│ ├── kotlin.md # Kotlin / Android, coroutines, Flow, Compose +│ ├── go.md # Error handling, goroutines, context +│ ├── csharp.md # C# / .NET 8, async, EF Core, ASP.NET Core +│ ├── php.md # PHP 8.x, types, PDO, security, Composer +│ ├── c.md # Memory safety, UB, error handling +│ ├── cpp.md # RAII, move semantics, exception safety +│ ├── qt.md # Object model, signals/slots, GUI perf +│ ├── css-less-sass.md # Variables, responsive, performance +│ ├── architecture-review-guide.md # SOLID, anti-patterns, coupling +│ ├── performance-review-guide.md # Web Vitals, N+1, complexity +│ ├── security-review-guide.md # OWASP Top 10, JWT, validation +│ ├── common-bugs-checklist.md # Quick-reference bug patterns +│ ├── code-quality-universal.md # Language-agnostic quality anti-patterns +│ └── code-review-best-practices.md # Communication & process +├── assets/ # Templates and quick reference +│ ├── review-checklist.md +│ └── pr-review-template.md +└── scripts/ + └── pr-analyzer.py # PR complexity analyzer +``` + +### Frontmatter 规范 + +SKILL.md 必须包含 YAML frontmatter: + +```yaml +--- +name: skill-name +description: | + 功能描述。触发条件说明。 + Use when [具体使用场景]。 +allowed-tools: ["Read", "Grep", "Glob"] # 可选:限制工具访问 +--- +``` + +#### 必需字段 + +| 字段 | 说明 | 约束 | +|------|------|------| +| `name` | Skill 标识符 | 小写字母、数字、连字符;最多 64 字符 | +| `description` | 功能和激活条件 | 最多 1024 字符;必须包含 "Use when" | + +#### 可选字段 + +| 字段 | 说明 | 示例 | +|------|------|------| +| `allowed-tools` | 限制工具访问 | `["Read", "Grep", "Glob"]` | + +### 命名约定 + +**Skill 名称规则**: +- 仅使用小写字母、数字和连字符(kebab-case) +- 最多 64 个字符 +- 避免下划线或大写字母 + +``` +✅ 正确:code-review-skill, typescript-advanced-types +❌ 错误:CodeReview, code_review, TYPESCRIPT +``` + +**文件命名规则**: +- reference 文件使用小写:`react.md`, `vue.md` +- 多词文件使用连字符:`common-bugs-checklist.md` + +### Description 写法规范 + +Description 必须包含两部分: + +1. **功能陈述**:具体说明 Skill 能做什么 +2. **触发条件**:以 "Use when" 开头,说明何时激活 + +```yaml +# ✅ 正确示例 +description: | + Provides comprehensive code review guidance for React 19, Vue 3, Rust, + TypeScript, Java, Python, and C/C++. + Helps catch bugs, improve code quality, and give constructive feedback. + Use when reviewing pull requests, conducting PR reviews, establishing + review standards, or mentoring developers through code reviews. + +# ❌ 错误示例(太模糊,缺少触发条件) +description: | + Helps with code review. +``` + +### Progressive Disclosure(渐进式披露) + +Claude 只在需要时加载支持文件,不会一次性加载所有内容。 + +#### 文件职责划分 + +| 文件 | 加载时机 | 内容 | +|------|----------|------| +| `SKILL.md` | 始终加载 | 核心原则、快速索引、何时使用 | +| `reference/*.md` | 按需加载 | 语言/框架的详细指南 | +| `assets/*.md` | 明确需要时 | 模板、清单 | +| `scripts/*.py` | 明确指引时 | 工具脚本 | + +#### 内容组织原则 + +**SKILL.md**(~200 行以内): +- 简述:2-3 句话说明用途 +- 核心原则和方法论 +- 语言/框架索引表(链接到 reference/) +- 何时使用此 Skill + +**reference/*.md**(详细内容): +- 完整的代码示例 +- 所有最佳实践 +- Review Checklist +- 边界情况和陷阱 + +### 文件引用规范 + +在 SKILL.md 中引用其他文件时: + +```markdown +# ✅ 正确:使用 Markdown 链接格式 +| **React** | [React Guide](reference/react.md) | Hooks, React 19, RSC | +| **Vue 3** | [Vue Guide](reference/vue.md) | Composition API | + +详见 [React Guide](reference/react.md) 获取完整指南。 + +# ❌ 错误:使用代码块格式 +参考 `reference/react.md` 文件。 +``` + +**路径规则**: +- 使用相对路径(相对于 Skill 目录) +- 使用正斜杠 `/`,不使用反斜杠 +- 不需要 `./` 前缀 + +### 约定(Conventions) + +**严重级别(severity)**:审查意见统一使用 SKILL.md「Technique 4」的标记方案,三档由红到绿表示优先级: + +- 🔴 `[blocking]` - 合并前必须修复 +- 🟡 `[important]` - 应当修复,有异议可讨论 +- 🟢 `[nit]` - 可选优化,不阻塞合并 + +新增 reference 指南时请沿用这套标记,不要自创等价的名称(如 critical/warning/suggestion)。 + +**语言策略**:现有指南是中英混合的——部分通篇中文,部分(如 fastapi.md、php.md)以英文为主。新增内容时**跟随同一领域既有指南的语言**:改某个指南就用它的语言;新建指南可自行选择中文或英文,但单个文件内部保持一致。 + +--- + +## 贡献类型 + +### 添加新语言支持 + +1. 在 `reference/` 目录创建新文件(如 `go.md`) +2. 遵循以下结构: + +```markdown +# [Language] Code Review Guide + +> 简短描述,一句话说明覆盖内容。 + +## 目录 +- [主题1](#主题1) +- [主题2](#主题2) +- [Review Checklist](#review-checklist) + +--- + +## 主题1 + +### 子主题 + +```[language] +// ❌ Bad pattern - 说明为什么不好 +bad_code_example() + +// ✅ Good pattern - 说明为什么好 +good_code_example() +``` + +--- + +## Review Checklist + +### 类别1 +- [ ] 检查项 1 +- [ ] 检查项 2 +``` + +3. 在 `SKILL.md` 的索引表中添加链接 +4. 更新 `README.md` 的统计信息 + +### 添加框架模式 + +1. 确保引用官方文档 +2. 包含版本号(如 "React 19", "Vue 3.5+") +3. 提供可运行的代码示例 +4. 添加对应的 checklist 项 + +### 改进现有内容 + +- 修复拼写或语法错误 +- 更新过时的模式(注明版本变化) +- 添加边界情况示例 +- 改进代码示例的清晰度 + +--- + +## 代码示例规范 + +### 格式要求 + +```markdown +// ❌ 问题描述 - 解释为什么这样做不好 +problematic_code() + +// ✅ 推荐做法 - 解释为什么这样做更好 +recommended_code() +``` + +### 质量标准 + +- 示例应基于真实场景,避免人为构造 +- 同时展示问题和解决方案 +- 保持示例简洁聚焦 +- 包含必要的上下文(import 语句等) + +--- + +## 提交流程 + +### Issue 报告 + +- 使用 GitHub Issues 报告问题或建议 +- 提供清晰的描述和示例 +- 标注相关的语言/框架 + +### Pull Request 流程 + +1. Fork 仓库 +2. 创建功能分支:`git checkout -b feature/add-go-support` +3. 进行修改 +4. 提交(见下文 commit 格式) +5. 推送到 fork:`git push origin feature/add-go-support` +6. 创建 Pull Request + +### Commit 消息格式 + +``` +类型: 简短描述 + +详细说明(如需要) + +- 具体变更 1 +- 具体变更 2 +``` + +**类型**: +- `feat`: 新功能或新内容 +- `fix`: 修复错误 +- `docs`: 仅文档变更 +- `refactor`: 重构(不改变功能) +- `chore`: 维护性工作 + +**示例**: +``` +feat: 添加 Go 语言代码审查指南 + +- 新增 reference/go.md +- 覆盖错误处理、并发、接口设计 +- 更新 SKILL.md 索引表 +``` + +--- + +## Skill 设计原则 + +### 单一职责 + +每个 Skill 专注一个核心能力。本 Skill 专注于**代码审查**,不应扩展到: +- 代码生成 +- 项目初始化 +- 部署配置 + +### 版本管理 + +- 在 reference 文件中标注框架/语言版本 +- 更新时在 commit 中说明版本变化 +- 过时内容应更新而非删除(除非完全废弃) + +### 内容质量 + +- 所有建议应有依据(官方文档、最佳实践) +- 避免主观偏好(如代码风格),专注于客观问题 +- 优先覆盖常见陷阱和安全问题 + +--- + +## 常见问题 + +### Q: 如何测试我的更改? + +将修改后的 Skill 复制到 `~/.claude/skills/` 目录,然后在 Claude Code 中测试: +```bash +cp -r code-review-skill ~/.claude/skills/code-review-skill +``` + +### Q: 我应该更新 SKILL.md 还是 reference 文件? + +- **SKILL.md**:只修改索引表或核心原则 +- **reference/*.md**:添加/更新具体的语言或框架内容 + +### Q: 如何处理过时的内容? + +1. 标注版本变化(如 "React 18 → React 19") +2. 保留旧版本内容(如果仍有用户使用) +3. 在 checklist 中更新相关项 + +--- + +## 问题咨询 + +如有任何问题,欢迎在 GitHub Issues 中提问。 diff --git a/.claude/skills/code-review-skill/LICENSE b/.claude/skills/code-review-skill/LICENSE new file mode 100644 index 0000000..8217668 --- /dev/null +++ b/.claude/skills/code-review-skill/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 tt-a1i + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.claude/skills/code-review-skill/README.md b/.claude/skills/code-review-skill/README.md new file mode 100644 index 0000000..7ae043c --- /dev/null +++ b/.claude/skills/code-review-skill/README.md @@ -0,0 +1,658 @@ +
+ +

🔍 Code Review Skill

+ +

+ A comprehensive, modular code review skill for Claude Code
+ 面向 Claude Code 的全面模块化代码审查技能 +

+ +

+ + License: MIT + + Claude Code Skill + 16000+ lines + 20+ languages + PRs Welcome +

+ +

+ English + · + 中文 + · + Contributing +

+ +
+ +--- + + + +## English + +### What is this? + +**Code Review Skill** is a production-ready skill for [Claude Code](https://claude.ai/code) that transforms AI-assisted code review from vague suggestions into a **structured, consistent, and expert-level** process. + +It covers **20+ languages and frameworks** with over **16,000 lines** of carefully curated review guidelines — loaded progressively to minimize context window usage. + +--- + +### ✨ Key Features + +- **Progressive Disclosure** — Core skill is ~190 lines; language guides (~200–1,000 lines each) load only when needed. +- **Four-Phase Review Process** — Structured workflow from understanding scope to delivering clear feedback. +- **Severity Labeling** — Every finding is categorized: `blocking` · `important` · `nit` · `suggestion` · `learning` · `praise` +- **Security-First** — Dedicated security checklists per language ecosystem. +- **Collaborative Tone** — Questions over commands, suggestions over mandates. +- **Automation Awareness** — Clearly separates what human review should catch vs. what linters handle. + +--- + +### 🌐 Supported Languages & Frameworks + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryTechnologyGuideLines
Frontend⚛️ React 19 / Next.js / TanStack Query v5reference/react.md~870
💚 Vue 3.5 + Composition APIreference/vue.md~920
🔮 Angular 17+ / Signals / Zonelessreference/angular.md~420
🔥 Svelte 5 / SvelteKitreference/svelte.md~1,060
🎨 CSS / Less / Sassreference/css-less-sass.md~660
🔷 TypeScriptreference/typescript.md~540
Backend☕ Java 17/21 + Spring Boot 3reference/java.md~410
⚡ FastAPIreference/fastapi.md~590
PHP 8.xreference/php.md~700
📦 NestJSreference/nestjs.md~590
🐍 Django / DRFreference/django.md~1,030
🐹 Goreference/go.md~990
🦀 Rustreference/rust.md~840
💻 C# / .NET 8reference/csharp.md~520
🐍 Pythonreference/python.md~1,070
Mobile / Systems📱 Kotlin / Androidreference/kotlin.md~1,020
🍎 Swift / SwiftUIreference/swift.md~930
⚙️ Creference/c.md~290
🔩 C++reference/cpp.md~390
🖥️ Qt Frameworkreference/qt.md~190
Cross-Cutting🏛️ Architecture Design Reviewreference/architecture-review-guide.md~470
⚡ Performance Reviewreference/performance-review-guide.md~820
🔍 Universal Quality Anti-Patternsreference/code-quality-universal.md~490
+ +--- + +### 🔄 The Four-Phase Review Process + +``` +Phase 1 - Context Gathering + Understand PR scope, linked issues, and intent + | + v +Phase 2 - High-Level Review + Architecture - Performance impact - Test strategy + | + v +Phase 3 - Line-by-Line Analysis + Logic - Security - Maintainability - Edge cases + | + v +Phase 4 - Summary & Decision + Structured feedback - Approval status - Action items +``` + +--- + +### 🏷️ Severity Labels + +| Label | Meaning | +|-------|---------| +| 🔴 `blocking` | Must be fixed before merge | +| 🟠 `important` | Should be fixed; may block depending on context | +| 🟡 `nit` | Minor style or preference issue | +| 🔵 `suggestion` | Optional improvement worth considering | +| 📚 `learning` | Educational note for the author | +| 🌟 `praise` | Explicitly highlight great work | + +--- + +### 📁 Repository Structure + +``` +code-review-skill/ +| ++-- SKILL.md # Core skill - loaded on activation (~190 lines) ++-- README.md ++-- LICENSE ++-- CONTRIBUTING.md +| ++-- reference/ # On-demand language guides +| +-- react.md # React 19 / Next.js / TanStack Query v5 +| +-- vue.md # Vue 3.5 Composition API +| +-- angular.md # Angular 17+ / Signals / Zoneless +| +-- svelte.md # Svelte 5 / SvelteKit +| +-- rust.md # Rust ownership, async/await, unsafe +| +-- typescript.md # TypeScript strict mode, generics, ESLint +| +-- nestjs.md # NestJS DI, Guards, Interceptors, DTOs +| +-- java.md # Java 17/21 & Spring Boot 3 +| +-- php.md # PHP 8.x types, PDO, security, Composer +| +-- python.md # Python async, typing, pytest +| +-- django.md # Django / DRF security, serializers, async +| +-- fastapi.md # FastAPI Depends, Pydantic v2, async, test-driven verification +| +-- go.md # Go goroutines, channels, context, interfaces +| +-- kotlin.md # Kotlin / Android coroutines, Compose, Flow +| +-- swift.md # Swift 5.9+/6, SwiftUI, concurrency, optionals +| +-- csharp.md # C# 12 / .NET 8, EF Core, ASP.NET Core +| +-- c.md # C memory safety, UB, error handling +| +-- cpp.md # C++ RAII, move semantics, exception safety +| +-- qt.md # Qt object model, signals/slots, GUI perf +| +-- css-less-sass.md # CSS/Less/Sass variables, responsive design +| +-- architecture-review-guide.md # SOLID, anti-patterns, coupling/cohesion +| +-- code-quality-universal.md # Reuse audit, parameter sprawl, TOCTOU, no-op updates +| +-- performance-review-guide.md # Core Web Vitals, N+1, memory leaks +| +-- security-review-guide.md # Security checklist (all languages) +| +-- common-bugs-checklist.md # Language-specific bug patterns +| +-- code-review-best-practices.md # Communication & process guidelines +| ++-- assets/ +| +-- review-checklist.md # Quick reference checklist +| +-- pr-review-template.md # PR review comment template +| ++-- scripts/ + +-- pr-analyzer.py # PR complexity analyzer +``` + +--- + +### 🚀 Installation + +**Clone to your Claude Code skills directory:** + +```bash +# macOS / Linux +git clone https://github.com/awesome-skills/code-review-skill.git \ + ~/.claude/skills/code-review-skill + +# Windows (PowerShell) +git clone https://github.com/awesome-skills/code-review-skill.git ` + "$env:USERPROFILE\.claude\skills\code-review-skill" +``` + +**Or add to an existing plugin:** + +```bash +cp -r code-review-skill ~/.claude/plugins/your-plugin/skills/code-review/ +``` + +--- + +### 💡 Usage + +Once installed, activate the skill in your Claude Code session: + +``` +Use code-review-skill to review this PR +``` + +Or create a custom slash command in `.claude/commands/`: + +```markdown + +Use code-review-skill to perform a thorough review of the changes in this PR. +Focus on: security, performance, and maintainability. +``` + +**Example prompts:** + +| Prompt | What happens | +|--------|-------------| +| `Review this React component` | Loads `react.md` - checks hooks, Server Components, Suspense patterns | +| `Review this Java PR` | Loads `java.md` - checks virtual threads, JPA, Spring Boot 3 patterns | +| `Security review of this Go service` | Loads `go.md` + `security-review-guide.md` | +| `Architecture review` | Loads `architecture-review-guide.md` - SOLID, anti-patterns, coupling | +| `Performance review` | Loads `performance-review-guide.md` - Web Vitals, N+1, complexity | + +--- + +### 🔬 Highlights by Language + +
+⚛️ React 19 + +- `useActionState` - Unified form state management +- `useFormStatus` - Access parent form status without prop drilling +- `useOptimistic` - Optimistic UI updates with automatic rollback +- Server Components & Server Actions patterns (Next.js 15+) +- Suspense boundary design, Error Boundary integration, streaming SSR +- `use()` Hook for consuming Promises + +
+ +
+☕ Java & Spring Boot 3 + +- **Java 17/21**: Records, Pattern Matching for Switch, Text Blocks, Sealed Classes +- **Virtual Threads** (Project Loom): High-throughput I/O patterns +- **Spring Boot 3**: Constructor injection, `@ConfigurationProperties`, `ProblemDetail` +- **JPA Performance**: Solving N+1, correct `equals`/`hashCode` on Entities + +
+ +
+🦀 Rust + +- Ownership patterns and common pitfalls +- `unsafe` code review requirements (mandatory `SAFETY` comments) +- Async/await - avoiding blocking in async context, cancellation safety +- Error handling: `thiserror` for libraries, `anyhow` for applications + +
+ +
+🐹 Go + +- Goroutine lifecycle management and leak prevention +- Channel patterns, select usage +- `context.Context` propagation +- Interface design (accept interfaces, return structs) +- Error wrapping with `%w` + +
+ +
+⚙️ C / C++ + +- **C**: Pointer/buffer safety, undefined behavior, resource cleanup, integer overflow +- **C++**: RAII ownership, Rule of 0/3/5, move semantics, exception safety, `noexcept` +- **Qt**: Object parent/child memory model, thread-safe signal/slot connections, GUI performance + +
+ +--- + +### 🤝 Contributing + +Contributions are welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines. + +**Ideas:** +- New language guides (Ruby, Elixir, Scala...) +- Framework-specific guides (Laravel, Spring WebFlux...) +- Additional checklists and templates +- Translations of core documentation + +--- + +### 📄 License + +MIT © [awesome-skills](https://github.com/awesome-skills) + +--- + + + +## 中文 + +### 这是什么? + +**Code Review Skill** 是专为 [Claude Code](https://claude.ai/code) 打造的生产级代码审查技能,将 AI 辅助的代码审查从模糊建议转变为**结构化、一致且专业级**的流程。 + +覆盖 **20+ 种语言和框架**,拥有超过 **16,000 行**精心整理的代码审查指南——按需加载,最大程度减少上下文占用。 + +--- + +### ✨ 核心特性 + +- **渐进式加载** — 核心技能仅 ~190 行,各语言指南(每份 200–1,000 行)仅在需要时才加载。 +- **四阶段审查流程** — 从理解 PR 范围到输出清晰反馈,每一步都有规可循。 +- **严重性标记** — 每条发现均分级:`blocking` · `important` · `nit` · `suggestion` · `learning` · `praise` +- **安全优先** — 每个语言生态均配备专属安全检查清单。 +- **协作式语气** — 以提问替代命令,以建议替代指令。 +- **自动化感知** — 明确区分人工审查应关注的内容与 linter 自动处理的内容。 + +--- + +### 🌐 支持的语言与框架 + +| 分类 | 技术栈 | 指南文件 | 行数 | +|------|--------|----------|------| +| **前端** | ⚛️ React 19 / Next.js / TanStack Query v5 | `reference/react.md` | ~870 | +| | 💚 Vue 3.5 Composition API | `reference/vue.md` | ~920 | +| | 🔮 Angular 17+ / Signals / Zoneless | `reference/angular.md` | ~420 | +| | 🔥 Svelte 5 / SvelteKit | `reference/svelte.md` | ~1,060 | +| | 🎨 CSS / Less / Sass | `reference/css-less-sass.md` | ~660 | +| | 🔷 TypeScript | `reference/typescript.md` | ~540 | +| **后端** | ☕ Java 17/21 + Spring Boot 3 | `reference/java.md` | ~410 | +| | ⚡ FastAPI | `reference/fastapi.md` | ~590 | +| | PHP 8.x | `reference/php.md` | ~700 | +| | 📦 NestJS | `reference/nestjs.md` | ~590 | +| | 🐍 Django / DRF | `reference/django.md` | ~1,030 | +| | 🐍 Python | `reference/python.md` | ~1,070 | +| | 🐹 Go | `reference/go.md` | ~990 | +| | 🦀 Rust | `reference/rust.md` | ~840 | +| | 💻 C# / .NET 8 | `reference/csharp.md` | ~520 | +| **移动 / 系统** | 📱 Kotlin / Android | `reference/kotlin.md` | ~1,020 | +| | 🍎 Swift / SwiftUI | `reference/swift.md` | ~930 | +| | ⚙️ C | `reference/c.md` | ~290 | +| | 🔩 C++ | `reference/cpp.md` | ~390 | +| | 🖥️ Qt 框架 | `reference/qt.md` | ~190 | +| **架构** | 🏛️ 架构设计审查 | `reference/architecture-review-guide.md` | ~470 | +| | ⚡ 性能审查 | `reference/performance-review-guide.md` | ~820 | +| | 🔍 通用质量反模式 | `reference/code-quality-universal.md` | ~490 | + +--- + +### 🔄 四阶段审查流程 + +``` +阶段一 - 上下文收集 + 理解 PR 范围、关联 Issue 和实现意图 + | + v +阶段二 - 高层级审查 + 架构设计 - 性能影响 - 测试策略 + | + v +阶段三 - 逐行深度分析 + 逻辑正确性 - 安全漏洞 - 可维护性 - 边界情况 + | + v +阶段四 - 总结与决策 + 结构化反馈 - 审批状态 - 后续行动项 +``` + +--- + +### 🏷️ 严重性标记说明 + +| 标记 | 含义 | +|------|------| +| 🔴 `blocking` | 合并前必须修复 | +| 🟠 `important` | 应当修复,视情况可能阻塞合并 | +| 🟡 `nit` | 风格或偏好上的小问题 | +| 🔵 `suggestion` | 值得考虑的可选优化 | +| 📚 `learning` | 给作者的教育性说明 | +| 🌟 `praise` | 明确表扬优秀代码 | + +--- + +### 📁 仓库结构 + +``` +code-review-skill/ +| ++-- SKILL.md # 核心技能,激活时加载(~190 行) ++-- README.md ++-- LICENSE ++-- CONTRIBUTING.md +| ++-- reference/ # 按需加载的语言指南 +| +-- react.md # React 19 / Next.js / TanStack Query v5 +| +-- vue.md # Vue 3.5 组合式 API +| +-- angular.md # Angular 17+ / Signals / Zoneless +| +-- svelte.md # Svelte 5 / SvelteKit +| +-- rust.md # Rust 所有权、async/await、unsafe +| +-- typescript.md # TypeScript strict 模式、泛型、ESLint +| +-- nestjs.md # NestJS 依赖注入、Guard、Interceptor、DTO +| +-- java.md # Java 17/21 & Spring Boot 3 +| +-- php.md # PHP 8.x 类型、PDO、安全、Composer +| +-- python.md # Python async、类型注解、pytest +| +-- django.md # Django / DRF 安全、Serializer、异步视图 +| +-- fastapi.md # FastAPI Depends、Pydantic v2、异步、测试驱动验证 +| +-- go.md # Go goroutine、channel、context、接口 +| +-- kotlin.md # Kotlin / Android 协程、Compose、Flow +| +-- swift.md # Swift 5.9+/6、SwiftUI、并发、可选值 +| +-- csharp.md # C# 12 / .NET 8、EF Core、ASP.NET Core +| +-- c.md # C 内存安全、UB、错误处理 +| +-- cpp.md # C++ RAII、移动语义、异常安全 +| +-- qt.md # Qt 对象模型、信号/槽、GUI 性能 +| +-- css-less-sass.md # CSS/Less/Sass 变量、响应式设计 +| +-- architecture-review-guide.md # SOLID、反模式、耦合度分析 +| +-- code-quality-universal.md # 复用审查、参数膨胀、抽象泄漏、TOCTOU +| +-- performance-review-guide.md # Core Web Vitals、N+1、内存泄漏 +| +-- security-review-guide.md # 安全审查清单(全语言通用) +| +-- common-bugs-checklist.md # 各语言常见 Bug 模式 +| +-- code-review-best-practices.md # 沟通与流程最佳实践 +| ++-- assets/ +| +-- review-checklist.md # 快速参考清单 +| +-- pr-review-template.md # PR 审查评论模板 +| ++-- scripts/ + +-- pr-analyzer.py # PR 复杂度分析工具 +``` + +--- + +### 🚀 安装方法 + +**克隆到 Claude Code skills 目录:** + +```bash +# macOS / Linux +git clone https://github.com/awesome-skills/code-review-skill.git \ + ~/.claude/skills/code-review-skill + +# Windows(PowerShell) +git clone https://github.com/awesome-skills/code-review-skill.git ` + "$env:USERPROFILE\.claude\skills\code-review-skill" +``` + +**或添加到现有插件:** + +```bash +cp -r code-review-skill ~/.claude/plugins/your-plugin/skills/code-review/ +``` + +--- + +### 💡 使用方式 + +安装后,在 Claude Code 会话中激活技能: + +``` +Use code-review-skill to review this PR +``` + +或在 `.claude/commands/` 中创建自定义斜杠命令: + +```markdown + +使用 code-review-skill 对这次 PR 的变更进行全面审查。 +重点关注:安全性、性能和可维护性。 +``` + +**示例提示词:** + +| 提示词 | 效果 | +|--------|------| +| `审查这个 React 组件` | 加载 `react.md`,检查 Hooks、Server Components、Suspense | +| `审查这个 Java PR` | 加载 `java.md`,检查虚拟线程、JPA、Spring Boot 3 | +| `对这个 Go 服务进行安全审查` | 加载 `go.md` + `security-review-guide.md` | +| `架构审查` | 加载 `architecture-review-guide.md`,检查 SOLID 与反模式 | +| `性能审查` | 加载 `performance-review-guide.md`,分析 Web Vitals、N+1 等 | + +--- + +### 🔬 各语言核心内容 + +
+⚛️ React 19 + +- `useActionState` — 统一的表单状态管理 +- `useFormStatus` — 无需 props 透传即可访问父表单状态 +- `useOptimistic` — 带自动回滚的乐观 UI 更新 +- Server Components & Server Actions(Next.js 15+) +- Suspense 边界设计、Error Boundary 集成、流式 SSR +- `use()` Hook 消费 Promise + +
+ +
+☕ Java & Spring Boot 3 + +- **Java 17/21**:Records、Switch 模式匹配、文本块、Sealed Classes +- **虚拟线程**(Project Loom):高吞吐量 I/O 模式 +- **Spring Boot 3**:构造器注入、`@ConfigurationProperties`、`ProblemDetail` +- **JPA 性能**:解决 N+1、Entity 正确的 `equals`/`hashCode` 实现 + +
+ +
+🦀 Rust + +- 所有权模式与常见陷阱 +- `unsafe` 代码审查要求(必须有 `SAFETY` 注释) +- Async/await — 避免在异步上下文中阻塞,取消安全性 +- 错误处理:库用 `thiserror`,应用用 `anyhow` + +
+ +
+🐹 Go + +- Goroutine 生命周期管理与泄漏预防 +- Channel 模式、select 用法 +- `context.Context` 传播规范 +- 接口设计原则(接受接口,返回结构体) +- 错误包装:使用 `%w` + +
+ +
+⚙️ C / C++ + +- **C**:指针/缓冲区安全、未定义行为、资源清理、整数溢出 +- **C++**:RAII 所有权、Rule of 0/3/5、移动语义、异常安全、`noexcept` +- **Qt**:父子内存模型、线程安全的信号/槽连接、GUI 性能优化 + +
+ +--- + +### 🤝 参与贡献 + +欢迎贡献!请查阅 [CONTRIBUTING.md](./CONTRIBUTING.md) 了解规范。 + +**可贡献方向:** +- 新增语言指南(Ruby、Elixir、Scala...) +- 框架专属指南(Laravel、Spring WebFlux...) +- 补充检查清单和审查模板 +- 核心文档的多语言翻译 + +--- + +### 📄 开源协议 + +MIT © [awesome-skills](https://github.com/awesome-skills) + +--- + +
+ Made with ❤️ for developers who care about code quality +
diff --git a/.claude/skills/code-review-skill/SKILL.md b/.claude/skills/code-review-skill/SKILL.md new file mode 100644 index 0000000..edc8db6 --- /dev/null +++ b/.claude/skills/code-review-skill/SKILL.md @@ -0,0 +1,220 @@ +--- +name: code-review-skill +description: | + Provides comprehensive code review guidance for React 19, Vue 3, Angular 17+, Svelte 5, Rust, TypeScript, Java, PHP, Python, Django, Go, C#/.NET, Kotlin, Swift, NestJS, C/C++, and more. + Helps catch bugs, improve code quality, and give constructive feedback. + Use when: reviewing pull requests, conducting PR reviews, code review, reviewing code changes, + establishing review standards, mentoring developers, architecture reviews, security audits, + checking code quality, finding bugs, giving feedback on code. +allowed-tools: + - Read + - Grep + - Glob + - Bash # 运行 lint/test/build 命令验证代码质量 + - WebFetch # 查阅最新文档和最佳实践 +--- + +# Code Review Skill + +Transform code reviews from gatekeeping to knowledge sharing through constructive feedback, systematic analysis, and collaborative improvement. + +## When to Use This Skill + +- Reviewing pull requests and code changes +- Establishing code review standards for teams +- Mentoring junior developers through reviews +- Conducting architecture reviews +- Creating review checklists and guidelines +- Improving team collaboration +- Reducing code review cycle time +- Maintaining code quality standards + +## Core Principles + +### 1. The Review Mindset + +**Goals of Code Review:** +- Catch bugs and edge cases +- Ensure code maintainability +- Share knowledge across team +- Enforce coding standards +- Improve design and architecture +- Build team culture + +**Not the Goals:** +- Show off knowledge +- Nitpick formatting (use linters) +- Block progress unnecessarily +- Rewrite to your preference + +### 2. Effective Feedback + +**Good Feedback is:** +- Specific and actionable +- Educational, not judgmental +- Focused on the code, not the person +- Balanced (praise good work too) +- Prioritized (critical vs nice-to-have) + +```markdown +❌ Bad: "This is wrong." +✅ Good: "This could cause a race condition when multiple users + access simultaneously. Consider using a mutex here." + +❌ Bad: "Why didn't you use X pattern?" +✅ Good: "Have you considered the Repository pattern? It would + make this easier to test. Here's an example: [link]" + +❌ Bad: "Rename this variable." +✅ Good: "[nit] Consider `userCount` instead of `uc` for + clarity. Not blocking if you prefer to keep it." +``` + +### 3. Review Scope + +**What to Review:** +- Logic correctness and edge cases +- Security vulnerabilities +- Performance implications +- Test coverage and quality +- Error handling +- Documentation and comments +- API design and naming +- Architectural fit + +**What Not to Review Manually:** +- Code formatting (use Prettier, Black, etc.) +- Import organization +- Linting violations +- Simple typos + +## Review Process + +### Phase 1: Context Gathering (2-3 minutes) + +Before diving into code, understand: +1. Read PR description and linked issue +2. Check PR size (>400 lines? Ask to split) +3. Review CI/CD status (tests passing?) +4. Understand the business requirement +5. Note any relevant architectural decisions + +> For large diffs, pipe the diff through [`scripts/pr-analyzer.py`](scripts/pr-analyzer.py) (`git diff main...HEAD | python scripts/pr-analyzer.py`) to triage complexity and get a suggested review approach before reading. + +### Phase 2: High-Level Review (5-10 minutes) + +1. **Architecture & Design** - Does the solution fit the problem? + - For significant changes, consult [Architecture Review Guide](reference/architecture-review-guide.md) + - Check: SOLID principles, coupling/cohesion, anti-patterns +2. **Performance Assessment** - Are there performance concerns? + - For performance-critical code, consult [Performance Review Guide](reference/performance-review-guide.md) + - Check: Algorithm complexity, N+1 queries, memory usage +3. **File Organization** - Are new files in the right places? +4. **Testing Strategy** - Are there tests covering edge cases? + +### Phase 3: Line-by-Line Review (10-20 minutes) + +For each file, check: +- **Logic & Correctness** - Edge cases, off-by-one, null checks, race conditions +- **Security** - Input validation, injection risks, XSS, sensitive data +- **Performance** - N+1 queries, unnecessary loops, memory leaks +- **Maintainability** - Clear names, single responsibility, comments +- **Reuse** - Before accepting new code, search for existing utilities/helpers that could replace it. Check adjacent files and shared modules for similar patterns. See [Universal Quality Guide](reference/code-quality-universal.md) for anti-patterns like parameter sprawl, leaky abstractions, nested conditionals, stringly-typed code, TOCTOU, and no-op updates. + +### Phase 4: Summary & Decision (2-3 minutes) + +1. Summarize key concerns +2. Highlight what you liked +3. Make clear decision: + - ✅ Approve + - 💬 Comment (minor suggestions) + - 🔄 Request Changes (must address) +4. Offer to pair if complex + +## Review Techniques + +### Technique 1: The Checklist Method + +Use checklists for consistent reviews. See [Security Review Guide](reference/security-review-guide.md) for comprehensive security checklist. + +### Technique 2: The Question Approach + +Instead of stating problems, ask questions: + +```markdown +❌ "This will fail if the list is empty." +✅ "What happens if `items` is an empty array?" + +❌ "You need error handling here." +✅ "How should this behave if the API call fails?" +``` + +### Technique 3: Suggest, Don't Command + +Use collaborative language: + +```markdown +❌ "You must change this to use async/await" +✅ "Suggestion: async/await might make this more readable. What do you think?" + +❌ "Extract this into a function" +✅ "This logic appears in 3 places. Would it make sense to extract it?" +``` + +### Technique 4: Differentiate Severity + +Use labels to indicate priority: + +- 🔴 `[blocking]` - Must fix before merge +- 🟡 `[important]` - Should fix, discuss if disagree +- 🟢 `[nit]` - Nice to have, not blocking +- 💡 `[suggestion]` - Alternative approach to consider +- 📚 `[learning]` - Educational comment, no action needed +- 🎉 `[praise]` - Good work, keep it up! + +**Severity levels:** 🔴 / 🟡 / 🟢 are the three severity tiers used as the standard across all guides in this skill — 🔴 blocks the merge, 🟡 should be addressed, 🟢 is optional. The remaining markers (💡 / 📚 / 🎉) are non-blocking annotations. + +## Language-Specific Guides + +根据审查的代码语言,查阅对应的详细指南: + +| Language/Framework | Reference File | Key Topics | +|-------------------|----------------|------------| +| **React** | [React Guide](reference/react.md) | Hooks, useEffect, React 19 Actions, RSC, Suspense, TanStack Query v5 | +| **Vue 3** | [Vue Guide](reference/vue.md) | Composition API, 响应性系统, Props/Emits, Watchers, Composables | +| **Angular 17+** | [Angular Guide](reference/angular.md) | Signals, Standalone 组件, RxJS, Zoneless 变更检测, 模板优化 | +| **Rust** | [Rust Guide](reference/rust.md) | 所有权/借用, Unsafe 审查, 异步代码, 取消安全性, 错误处理 | +| **TypeScript** | [TypeScript Guide](reference/typescript.md) | 类型安全, async/await, 不可变性 | +| **Python** | [Python Guide](reference/python.md) | 可变默认参数, 异常处理, 类属性 | +| **Django / DRF** | [Django Guide](reference/django.md) | 安全审查, N+1 查询, Serializer 反模式, ViewSet, 异步视图 | +| **FastAPI** | [FastAPI Guide](reference/fastapi.md) | Depends, Pydantic v2 validation, async correctness, sessions/N+1, auth vs authorization, test-driven verification | +| **Java** | [Java Guide](reference/java.md) | Java 17/21 新特性, Spring Boot 3, 虚拟线程, Stream/Optional | +| **PHP** | [PHP Guide](reference/php.md) | PHP 8.x type system, PDO, security review, Composer, PHPUnit/PHPStan | +| **C# / .NET** | [C# Guide](reference/csharp.md) | C# 12 特性, 异步编程, EF Core 性能, ASP.NET Core, LINQ | +| **Go** | [Go Guide](reference/go.md) | 错误处理, goroutine/channel, context, 接口设计 | +| **Kotlin / Android** | [Kotlin Guide](reference/kotlin.md) | 协程, Flow, Jetpack Compose, 空安全, 内存泄漏, 架构模式 | +| **Swift / SwiftUI** | [Swift Guide](reference/swift.md) | Optionals, Swift Concurrency, Sendable/actors, SwiftUI property wrappers, value vs reference types, API design | +| **NestJS** | [NestJS Guide](reference/nestjs.md) | 依赖注入, 分层架构, DTO 验证, Guard/Interceptor, 循环依赖 | +| **Svelte / SvelteKit** | [Svelte Guide](reference/svelte.md) | Runes, Load 函数, Form Actions, Store 迁移, SSR/CSR 边界 | +| **C** | [C Guide](reference/c.md) | 指针/缓冲区, 内存安全, UB, 错误处理 | +| **C++** | [C++ Guide](reference/cpp.md) | RAII, 生命周期, Rule of 0/3/5, 异常安全 | +| **CSS/Less/Sass** | [CSS Guide](reference/css-less-sass.md) | 变量规范, !important, 性能优化, 响应式, 兼容性 | +| **Qt** | [Qt Guide](reference/qt.md) | 对象模型, 信号/槽, 内存管理, 线程安全, 性能 | + +## Cross-Cutting Guides + +Language-agnostic patterns applicable to all code reviews: + +| Topic | Reference File | Key Topics | +|-------|----------------|------------| +| **Universal Quality** | [Universal Quality Guide](reference/code-quality-universal.md) | Reuse audit, parameter sprawl, leaky abstractions, nested conditionals, stringly-typed code, TOCTOU, no-op updates, redundant state | + +## Additional Resources + +- [Architecture Review Guide](reference/architecture-review-guide.md) - 架构设计审查指南(SOLID、反模式、耦合度) +- [Performance Review Guide](reference/performance-review-guide.md) - 性能审查指南(Web Vitals、N+1、复杂度) +- [Common Bugs Checklist](reference/common-bugs-checklist.md) - 按语言分类的常见错误清单 +- [Security Review Guide](reference/security-review-guide.md) - 安全审查指南 +- [Code Review Best Practices](reference/code-review-best-practices.md) - 代码审查最佳实践 +- [PR Review Template](assets/pr-review-template.md) - PR 审查评论模板 +- [Review Checklist](assets/review-checklist.md) - 快速参考清单 diff --git a/.claude/skills/code-review-skill/assets/pr-review-template.md b/.claude/skills/code-review-skill/assets/pr-review-template.md new file mode 100644 index 0000000..5c1b9d9 --- /dev/null +++ b/.claude/skills/code-review-skill/assets/pr-review-template.md @@ -0,0 +1,114 @@ +# PR Review Template + +Copy and use this template for your code reviews. + +--- + +## Summary + +[Brief overview of what was reviewed - 1-2 sentences] + +**PR Size:** [Small/Medium/Large] (~X lines) +**Review Time:** [X minutes] + +## Strengths + +- [What was done well] +- [Good patterns or approaches used] +- [Improvements from previous code] + +## Required Changes + +🔴 **[blocking]** [Issue description] +> [Code location or example] +> [Suggested fix or explanation] + +🔴 **[blocking]** [Issue description] +> [Details] + +## Important Suggestions + +🟡 **[important]** [Issue description] +> [Why this matters] +> [Suggested approach] + +## Minor Suggestions + +🟢 **[nit]** [Minor improvement suggestion] + +💡 **[suggestion]** [Alternative approach to consider] + +## Learning Notes + +📚 [Educational context worth sharing about X] + +📚 [Background behind design decision Y] + +## Security Considerations + +- [ ] No hardcoded secrets +- [ ] Input validation present +- [ ] Authorization checks in place +- [ ] No SQL/XSS injection risks + +## Test Coverage + +- [ ] Unit tests added/updated +- [ ] Edge cases covered +- [ ] Error cases tested + +## Verdict + +**[ ] ✅ Approve** - Ready to merge +**[ ] 💬 Comment** - Minor suggestions, can merge +**[ ] 🔄 Request Changes** - Must address blocking issues + +--- + +## Quick Copy Templates + +### Blocking Issue +``` +🔴 **[blocking]** [Title] + +[Description of the issue] + +**Location:** `file.ts:123` + +**Suggested fix:** +\`\`\`typescript +// Your suggested code +\`\`\` +``` + +### Important Suggestion +``` +🟡 **[important]** [Title] + +[Why this is important] + +**Consider:** +- Option A: [description] +- Option B: [description] +``` + +### Minor Suggestion +``` +🟢 **[nit]** [Suggestion] + +Not blocking, but consider [improvement]. +``` + +### Praise +``` +🎉 **[praise]** Great work on [specific thing]! + +[Why this is good] +``` + +### Learning +``` +📚 **[learning]** [Educational note] + +For context, [X] works this way because [Y]. No action needed — just sharing. +``` diff --git a/.claude/skills/code-review-skill/assets/review-checklist.md b/.claude/skills/code-review-skill/assets/review-checklist.md new file mode 100644 index 0000000..60beda0 --- /dev/null +++ b/.claude/skills/code-review-skill/assets/review-checklist.md @@ -0,0 +1,121 @@ +# Code Review Quick Checklist + +Quick reference checklist for code reviews. + +## Pre-Review (2 min) + +- [ ] Read PR description and linked issue +- [ ] Check PR size (<400 lines ideal) +- [ ] Verify CI/CD status (tests passing?) +- [ ] Understand the business requirement + +## Architecture & Design (5 min) + +- [ ] Solution fits the problem +- [ ] Consistent with existing patterns +- [ ] No simpler approach exists +- [ ] Will it scale? +- [ ] Changes in right location + +## Logic & Correctness (10 min) + +- [ ] Edge cases handled +- [ ] Null/undefined checks present +- [ ] Off-by-one errors checked +- [ ] Race conditions considered +- [ ] Error handling complete +- [ ] Correct data types used + +## Security (5 min) + +- [ ] No hardcoded secrets +- [ ] Input validated/sanitized +- [ ] SQL injection prevented +- [ ] XSS prevented +- [ ] Authorization checks present +- [ ] Sensitive data protected + +## Performance (3 min) + +- [ ] No N+1 queries +- [ ] Expensive operations optimized +- [ ] Large lists paginated +- [ ] No memory leaks +- [ ] Caching considered where appropriate + +## Testing (5 min) + +- [ ] Tests exist for new code +- [ ] Edge cases tested +- [ ] Error cases tested +- [ ] Tests are readable +- [ ] Tests are deterministic + +## Code Quality (3 min) + +- [ ] Clear variable/function names +- [ ] No code duplication +- [ ] Functions do one thing +- [ ] Complex code commented +- [ ] No magic numbers + +## Documentation (2 min) + +- [ ] Public APIs documented +- [ ] README updated if needed +- [ ] Breaking changes noted +- [ ] Complex logic explained + +--- + +## Severity Labels + +| Label | Meaning | Action | +|-------|---------|--------| +| 🔴 `[blocking]` | Must fix | Block merge | +| 🟡 `[important]` | Should fix | Discuss if disagree | +| 🟢 `[nit]` | Nice to have | Non-blocking | +| 💡 `[suggestion]` | Alternative | Consider | +| 📚 `[learning]` | Educational comment | No action needed | +| 🎉 `[praise]` | Good work | Celebrate! | + +--- + +## Decision Matrix + +| Situation | Decision | +|-----------|----------| +| Critical security issue | 🔴 Block, fix immediately | +| Breaking change without migration | 🔴 Block | +| Missing error handling | 🟡 Should fix | +| No tests for new code | 🟡 Should fix | +| Style preference | 🟢 Non-blocking | +| Minor naming improvement | 🟢 Non-blocking | +| Clever but working code | 💡 Suggest simpler | + +--- + +## Time Budget + +| PR Size | Target Time | +|---------|-------------| +| < 100 lines | 10-15 min | +| 100-400 lines | 20-40 min | +| > 400 lines | Ask to split | + +--- + +## Red Flags + +Watch for these patterns: + +- `// TODO` in production code +- `console.log` left in code +- Commented out code +- `any` type in TypeScript +- Empty catch blocks +- `unwrap()` in Rust production code +- Magic numbers/strings +- Copy-pasted code blocks +- Missing null checks +- Hardcoded URLs/credentials diff --git a/.claude/skills/code-review-skill/index.en.html b/.claude/skills/code-review-skill/index.en.html new file mode 100644 index 0000000..9d27975 --- /dev/null +++ b/.claude/skills/code-review-skill/index.en.html @@ -0,0 +1,702 @@ + + + + + + code-review-skill(1) — User Commands (en_US) + + + + + + + + +
+
+ CODE-REVIEW-SKILL(1) + User Commands · Edition 2026.01 + CODE-REVIEW-SKILL(1) +
+
+ +
+ + +
+
  ___ ___  ___  ___        ___ _____   _____ _____      __    ___ _  _____ _    _    
+ / __/ _ \|   \| __|  ___ | _ \ __\ \ / /_ _| __\ \ /\ / / __/ __| |/ /_ _| |  | |   
+| (_| (_) | |) | _|  |___||   / _| \ V / | || _| \ V  V /__\__ \ ' < | || |__| |__ 
+ \___\___/|___/|___|      |_|_\___| \_/ |___|___| \_/\_/    |___/_|\_\___|____|____|
+ +
+ + man code-review-skill + + + LANG=zh_CN | en_US + +
+
+ v1.0 · awesome-skills · MIT · 20 languages · 16,000+ lines +
+
+ + +

NAME

+
+

+ code-review-skill — A comprehensive, modular code review skill for Claude Code +

+
+ + +

SYNOPSIS

+
+
+Use code-review-skill to review this PR
+Use code-review-skill to review this <component>
+Use code-review-skill for [security | performance | architecture] review
+
+ + +

DESCRIPTION

+
+

A production-grade code review skill. It transforms AI-assisted code review from vague suggestions into a structured, consistent, expert-level collaborative process.

+

Core is only ~190 lines; the full 16,000+ lines of language guides load on demand. Covers 20+ mainstream languages and frameworks — progressive loading, zero overhead.

+

Every finding carries an explicit severity label. Every review proceeds through four phases: PR context · high-level assessment · line-by-line analysis · summary & decision.

+
+ + +

LANGUAGES

+
+ +
┌── frontend ──┘
+
react.mdReact 19, Hooks, Server Components, TanStack v5 .................870
+
vue.mdVue 3.5, Composition API, Composables, Watchers .................920
+
angular.mdAngular 17+, Signals, Standalone, Zoneless ..........................420
+
svelte.mdSvelte 5, Runes, SvelteKit, SSR/CSR boundaries ..................1,060
+
typescript.mdTypeScript strict mode, generics, immutability ..................540
+
css-less-sass.mdCSS/Less/Sass variables, responsive, compatibility ..............660
+ +
┌── backend ──┘
+
python.mdPython async, typing, pytest, mutable defaults .................1,070
+
django.mdDjango/DRF security, N+1, serializers, async views ..............1,030
+
java.mdJava 17/21, Spring Boot 3, virtual threads, JPA ................800
+
php.mdPHP 8.x, types, PDO, security, Composer ...........................700
+
go.mdGoroutines, channels, context, interface design .................990
+
rust.mdOwnership, async/await, unsafe, cancellation safety .............840
+
csharp.mdC# 12 / .NET 8, EF Core, ASP.NET Core, LINQ .....................520
+
nestjs.mdNestJS DI, guards, interceptors, DTO validation .................590
+ +
┌── mobile / systems ──┘
+
kotlin.mdKotlin/Android coroutines, Compose, Flow, null safety ...........1,020
+
swift.mdSwift 5.9+/6, SwiftUI, concurrency, Sendable, optionals ..........930
+
c.mdC pointer safety, undefined behavior, resources .................210
+
cpp.mdC++ RAII, Rule of 0/3/5, move semantics, noexcept ...............300
+
qt.mdQt object model, signals/slots, GUI performance .................190
+ +
┌── cross-cutting ──┘
+
architecture-review-guide.mdSOLID, anti-patterns, coupling ...470
+
performance-review-guide.mdWeb Vitals, N+1, complexity .......850
+
code-quality-universal.mdTOCTOU, leaky abstractions, sprawl ....320
+
security-review-guide.mdInjection, XSS, secrets, all langs .....
+
+ + +

PHASES

+
+ +
  ┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
+  │   context   │ ─▶  │ high level  │ ─▶  │ line by line│ ─▶  │   decide    │
+  │    2-3m     │     │    5-10m    │     │   10-20m    │     │    2-3m     │
+  └─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
+ +
+
1. context gathering — 2-3 min
+
Read the PR description and linked issues, assess scope, check CI status, understand the business intent.
+
2. high-level review — 5-10 min
+
Evaluate architectural fit, performance impact, file organization, test strategy. See the whole first.
+
3. line-by-line analysis — 10-20 min
+
Logic correctness · security · performance · maintainability · edge cases. One by one.
+
4. summary & decision — 2-3 min
+
Summarize findings, name what was done well, deliver approve / comment / request-changes.
+
+
+ + +

SEVERITY

+
+ +
+ + +

INSTALLATION

+
+ +

Clone into the Claude Code skills directory. Two commands.

+ +
# macOS / Linux
+$ git clone https://github.com/awesome-skills/code-review-skill.git \
+    ~/.claude/skills/code-review-skill
+
+# Windows PowerShell
+PS> git clone https://github.com/awesome-skills/code-review-skill.git `
+       "$env:USERPROFILE\.claude\skills\code-review-skill"
+
+ + +

EXAMPLES

+
+ +
+ + +

FILES

+
+
+~/.claude/skills/code-review-skill/
+├── SKILL.md                              # core, loaded on activation (~190 lines)
+├── README.md
+├── LICENSE                               # MIT
+├── reference/                            # on-demand language guides
+│   ├── react.md vue.md angular.md ...
+│   └── architecture-review-guide.md ...
+├── assets/
+│   ├── review-checklist.md              # quick reference
+│   └── pr-review-template.md            # PR comment template
+└── scripts/
+    └── pr-analyzer.py                   # PR complexity analyzer
+
+ + +

SEE ALSO

+
+

+ claude-code(1), + github / awesome-skills, + CONTRIBUTING(7), + review-checklist(7) +

+
+ + +

AUTHORS

+
+
+
awesome-skills
+
maintainer, primary author
+
contributors
+
see graphs/contributors
+
+
+ + +

COPYRIGHT

+
+

+ Copyright (c) 2025 awesome-skills.
+ Released under the MIT License.
+ This is free software: you are free to change and redistribute it.
+ There is NO WARRANTY, to the extent permitted by law. +

+
+ +
+ + +
+
+ CODE-REVIEW-SKILL(1) + awesome-skills + CODE-REVIEW-SKILL(1) +
+
+ +
+ + +
+
+
+ -- NORMAL -- + code-review-skill.1 +
+
+ g top + G end + q quit + 1,1 +
+
+
+ + + + + diff --git a/.claude/skills/code-review-skill/index.html b/.claude/skills/code-review-skill/index.html new file mode 100644 index 0000000..b36f5c9 --- /dev/null +++ b/.claude/skills/code-review-skill/index.html @@ -0,0 +1,702 @@ + + + + + + code-review-skill(1) — User Commands + + + + + + + + +
+
+ CODE-REVIEW-SKILL(1) + User Commands · Edition 2026.01 + CODE-REVIEW-SKILL(1) +
+
+ +
+ + +
+
  ___ ___  ___  ___        ___ _____   _____ _____      __    ___ _  _____ _    _    
+ / __/ _ \|   \| __|  ___ | _ \ __\ \ / /_ _| __\ \ /\ / / __/ __| |/ /_ _| |  | |   
+| (_| (_) | |) | _|  |___||   / _| \ V / | || _| \ V  V /__\__ \ ' < | || |__| |__ 
+ \___\___/|___/|___|      |_|_\___| \_/ |___|___| \_/\_/    |___/_|\_\___|____|____|
+ +
+ + man code-review-skill + + + LANG=zh_CN | en_US + +
+
+ v1.0 · awesome-skills · MIT · 20 languages · 16,000+ lines +
+
+ + +

NAME

+
+

+ code-review-skill — 面向 Claude Code 的全面、模块化代码审查技能 +

+
+ + +

SYNOPSIS

+
+
+Use code-review-skill to review this PR
+Use code-review-skill to review this <component>
+Use code-review-skill for [security | performance | architecture] review
+
+ + +

DESCRIPTION

+
+

一份生产级的代码审查技能。它把 AI 辅助的代码审查从模糊建议提升为结构化、一致、专业级的协作流程。

+

核心仅约 190 行,按需调阅共计 16,000+ 行 的语言指南。覆盖 20+ 种 主流语言与框架——按需加载,零冗余。

+

每一条审查意见都带有明确的严重性标记。每一次审查都按四个阶段推进:从 PR 上下文 · 高层级评估 · 逐行分析 · 总结决策。

+
+ + +

LANGUAGES

+
+ +
┌── frontend ──┘
+
react.mdReact 19, Hooks, Server Components, TanStack v5 .................870
+
vue.mdVue 3.5, Composition API, Composables, Watchers .................920
+
angular.mdAngular 17+, Signals, Standalone, Zoneless ..........................420
+
svelte.mdSvelte 5, Runes, SvelteKit, SSR/CSR boundaries ..................1,060
+
typescript.mdTypeScript strict mode, generics, immutability ..................540
+
css-less-sass.mdCSS/Less/Sass variables, responsive, compatibility ..............660
+ +
┌── backend ──┘
+
python.mdPython async, typing, pytest, mutable defaults .................1,070
+
django.mdDjango/DRF security, N+1, serializers, async views ..............1,030
+
java.mdJava 17/21, Spring Boot 3, virtual threads, JPA ................800
+
php.mdPHP 8.x, types, PDO, security, Composer ...........................700
+
go.mdGoroutines, channels, context, interface design .................990
+
rust.mdOwnership, async/await, unsafe, cancellation safety .............840
+
csharp.mdC# 12 / .NET 8, EF Core, ASP.NET Core, LINQ .....................520
+
nestjs.mdNestJS DI, guards, interceptors, DTO validation .................590
+ +
┌── mobile / systems ──┘
+
kotlin.mdKotlin/Android coroutines, Compose, Flow, null safety ...........1,020
+
swift.mdSwift 5.9+/6, SwiftUI, concurrency, Sendable, optionals ..........930
+
c.mdC pointer safety, undefined behavior, resources .................210
+
cpp.mdC++ RAII, Rule of 0/3/5, move semantics, noexcept ...............300
+
qt.mdQt object model, signals/slots, GUI performance .................190
+ +
┌── cross-cutting ──┘
+
architecture-review-guide.mdSOLID, anti-patterns, coupling ...470
+
performance-review-guide.mdWeb Vitals, N+1, complexity .......850
+
code-quality-universal.mdTOCTOU, leaky abstractions, sprawl ....320
+
security-review-guide.mdInjection, XSS, secrets, all langs .....
+
+ + +

PHASES

+
+ +
  ┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
+  │   context   │ ─▶  │ high level  │ ─▶  │ line by line│ ─▶  │   decide    │
+  │    2-3m     │     │    5-10m    │     │   10-20m    │     │    2-3m     │
+  └─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
+ +
+
1. context gathering — 2-3 min
+
读 PR 描述与关联 issue,评估规模,检查 CI 状态,理解业务需求。
+
2. high-level review — 5-10 min
+
评估架构合理性、性能影响面、文件组织、测试策略。先看全局。
+
3. line-by-line analysis — 10-20 min
+
逻辑正确性 · 安全 · 性能 · 可维护性 · 边界情况。一一过目。
+
4. summary & decision — 2-3 min
+
汇总问题,表扬亮点,给出 approve / comment / request-changes。
+
+
+ + +

SEVERITY

+
+ +
+ + +

INSTALLATION

+
+ +

克隆到 Claude Code skills 目录。两条命令即可。

+ +
# macOS / Linux
+$ git clone https://github.com/awesome-skills/code-review-skill.git \
+    ~/.claude/skills/code-review-skill
+
+# Windows PowerShell
+PS> git clone https://github.com/awesome-skills/code-review-skill.git `
+       "$env:USERPROFILE\.claude\skills\code-review-skill"
+
+ + +

EXAMPLES

+
+ +
+ + +

FILES

+
+
+~/.claude/skills/code-review-skill/
+├── SKILL.md                              # 核心,激活时加载 (~190 行)
+├── README.md
+├── LICENSE                               # MIT
+├── reference/                            # 按需加载的语言指南
+│   ├── react.md vue.md angular.md ...
+│   └── architecture-review-guide.md ...
+├── assets/
+│   ├── review-checklist.md              # 快速参考
+│   └── pr-review-template.md            # 评论模板
+└── scripts/
+    └── pr-analyzer.py                   # PR 复杂度分析
+
+ + +

SEE ALSO

+
+

+ claude-code(1), + github / awesome-skills, + CONTRIBUTING(7), + review-checklist(7) +

+
+ + +

AUTHORS

+
+
+
awesome-skills
+
maintainer, primary author
+
contributors
+
see graphs/contributors
+
+
+ + +

COPYRIGHT

+
+

+ Copyright (c) 2025 awesome-skills.
+ Released under the MIT License.
+ This is free software: you are free to change and redistribute it.
+ There is NO WARRANTY, to the extent permitted by law. +

+
+ +
+ + +
+
+ CODE-REVIEW-SKILL(1) + awesome-skills + CODE-REVIEW-SKILL(1) +
+
+ +
+ + +
+
+
+ -- NORMAL -- + code-review-skill.1 +
+
+ g top + G end + q quit + 1,1 +
+
+
+ + + + + diff --git a/.claude/skills/code-review-skill/reference/angular.md b/.claude/skills/code-review-skill/reference/angular.md new file mode 100644 index 0000000..a26d4ab --- /dev/null +++ b/.claude/skills/code-review-skill/reference/angular.md @@ -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: `

{{ data.name }}

`, +}) +export class UserProfile { + data = { name: 'Alice' }; + changeName() { this.data.name = 'Bob'; } // UI 不会更新! +} + +// ✅ Signal + OnPush = 自动变更检测 +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + template: `

{{ name() }}

`, +}) +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: `Edit`, +}) +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(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: `
` + +// ✅ 提取为 computed +filteredItems = computed(() => this.items().filter(i => i.active)); +shouldShow = computed(() => this.filteredItems().length > 0 && this.user().role === 'admin'); +template: `@if (shouldShow()) {
...
}` +``` + +### 原生绑定优于 NgClass / NgStyle + +```typescript +// ❌ NgClass/NgStyle——额外指令开销 +template: `
` + +// ✅ 原生 class/style 绑定——性能更好 +template: `
` +``` + +### 模板专用成员标记 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(); +userSaved = output(); + +// ✅ readonly 防止意外赋值 +readonly userId = input(); +readonly userSaved = output(); +readonly userName = model(); +``` + +### 命名规范:操作名而非事件名 + +```typescript +// ❌ 以事件命名 +template: `` + +// ✅ 以操作命名 +template: `` +``` + +--- + +## 性能优化 + +### 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()` 用于依赖注入 diff --git a/.claude/skills/code-review-skill/reference/architecture-review-guide.md b/.claude/skills/code-review-skill/reference/architecture-review-guide.md new file mode 100644 index 0000000..abde68c --- /dev/null +++ b/.claude/skills/code-review-skill/reference/architecture-review-guide.md @@ -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; +} + +// infrastructure/MySQLUserRepository.ts (实现) +class MySQLUserRepository implements UserRepository { + findById(id: string): Promise { /* ... */ } +} +``` + +### 审查清单 + +**层次边界检查:** +- [ ] 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) diff --git a/.claude/skills/code-review-skill/reference/c.md b/.claude/skills/code-review-skill/reference/c.md new file mode 100644 index 0000000..83c3988 --- /dev/null +++ b/.claude/skills/code-review-skill/reference/c.md @@ -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 diff --git a/.claude/skills/code-review-skill/reference/code-quality-universal.md b/.claude/skills/code-review-skill/reference/code-quality-universal.md new file mode 100644 index 0000000..97b3d3c --- /dev/null +++ b/.claude/skills/code-review-skill/reference/code-quality-universal.md @@ -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 原始结构 + + +// ✅ 组件接收 domain 类型,adapter 处理映射 +interface UserSummary { + displayName: string; + avatarUrl: string; +} + +``` + +**审查要点:** +- 函数返回类型是否泄露底层实现(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 = { + "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`? +- [ ] **数据精度**: 没有读取整个集合/文件只为了取子集? +- [ ] **冗余状态**: 没有可以从其他字段推导的存储字段? diff --git a/.claude/skills/code-review-skill/reference/code-review-best-practices.md b/.claude/skills/code-review-skill/reference/code-review-best-practices.md new file mode 100644 index 0000000..8c6b9cd --- /dev/null +++ b/.claude/skills/code-review-skill/reference/code-review-best-practices.md @@ -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 diff --git a/.claude/skills/code-review-skill/reference/common-bugs-checklist.md b/.claude/skills/code-review-skill/reference/common-bugs-checklist.md new file mode 100644 index 0000000..d266f57 --- /dev/null +++ b/.claude/skills/code-review-skill/reference/common-bugs-checklist.md @@ -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 `
` (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>` 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` 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 diff --git a/.claude/skills/code-review-skill/reference/cpp.md b/.claude/skills/code-review-skill/reference/cpp.md new file mode 100644 index 0000000..98d764f --- /dev/null +++ b/.claude/skills/code-review-skill/reference/cpp.md @@ -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 make_foo() { + auto foo = std::make_unique(); + if (!foo->Init()) { + return {}; + } + return foo; +} +``` + +### Wrap C resources + +```cpp +// ✅ Good: wrap FILE* with unique_ptr +using FilePtr = std::unique_ptr; + +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 make_task() { + int value = 42; + return [&]() { use(value); }; // dangling +} + +// ✅ Good: capture by value +std::function 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 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 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 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 counter{0}; +void inc() { counter.fetch_add(1, std::memory_order_relaxed); } +``` + +### Use RAII locks + +```cpp +std::mutex mu; +std::vector data; + +void add(int v) { + std::lock_guard lock(mu); + data.push_back(v); +} +``` + +--- + +## Performance and Allocation + +### Avoid repeated allocations + +```cpp +// ❌ Bad: repeated reallocation +std::vector build(int n) { + std::vector out; + for (int i = 0; i < n; ++i) { + out.push_back(i); + } + return out; +} + +// ✅ Good: reserve upfront +std::vector build(int n) { + std::vector out; + out.reserve(static_cast(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& parts) { + std::string out; + for (const auto& p : parts) { + out += p; + } + return out; +} + +// ✅ Good: reserve total size +std::string join(const std::vector& 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 +T add(T a, T b) { + return a + b; +} + +// ✅ Good: constrained +template +requires std::is_integral_v +T add(T a, T b) { + return a + b; +} +``` + +### Use static_assert for invariants + +```cpp +template +struct Packet { + static_assert(std::is_trivially_copyable_v, + "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 diff --git a/.claude/skills/code-review-skill/reference/csharp.md b/.claude/skills/code-review-skill/reference/csharp.md new file mode 100644 index 0000000..b92a9be --- /dev/null +++ b/.claude/skills/code-review-skill/reference/csharp.md @@ -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 _logger; + + public ProductService(ProductDbContext db, ILogger logger) + { + _db = db; + _logger = logger; + } +} + +// ✅ Primary Constructor——简洁的依赖注入 +public class ProductService(ProductDbContext db, ILogger logger) +{ + public async Task 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 names = new List { "alice", "bob" }; + +// ✅ 集合表达式 +int[] nums = [1, 2, 3]; +List names = ["alice", "bob"]; +Span 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 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> Get(int id) +{ + var data = await _service.GetDataAsync(id); + return Ok(data); +} +``` + +### ConfigureAwait(false) 用于库代码 + +```csharp +// ❌ 库代码不必要地捕获 SynchronizationContext +public class LibraryService +{ + public async Task GetDataAsync() + { + var response = await _httpClient.GetAsync("/api/data"); + return await response.Content.ReadAsStringAsync(); + } +} + +// ✅ 库代码使用 ConfigureAwait(false) 避免死锁 +public class LibraryService +{ + public async Task GetDataAsync() + { + var response = await _httpClient.GetAsync("/api/data").ConfigureAwait(false); + return await response.Content.ReadAsStringAsync().ConfigureAwait(false); + } +} +``` + +### CancellationToken 传播 + +```csharp +// ❌ 丢弃 CancellationToken +public async Task> SearchAsync(string query) +{ + return await _db.Users.Where(u => u.Name.Contains(query)).ToListAsync(); +} + +// ✅ 全链路传递 CancellationToken +public async Task> SearchAsync(string query, CancellationToken ct = default) +{ + return await _db.Users + .Where(u => u.Name.Contains(query)) + .ToListAsync(ct); +} +``` + +### Async Disposal + +```csharp +// ❌ 同步 dispose 异步资源 +public class DataClient : IDisposable +{ + public void Dispose() + { + _httpClient.Dispose(); // 可能丢弃正在进行的请求 + } +} + +// ✅ IAsyncDisposable +public class DataClient : IAsyncDisposable +{ + public async ValueTask DisposeAsync() + { + await _stream.DisposeAsync(); + } +} + +// ✅ 调用方使用 await using +await using var client = new DataClient(); +``` + +--- + +## EF Core 性能 + +### N+1 查询问题 + +```csharp +// ❌ 经典 N+1——每个 Blog 触发一次查询获取 Posts +foreach (var blog in await context.Blogs.ToListAsync()) +{ + foreach (var post in blog.Posts) // 每次循环都查询数据库! + { + Console.WriteLine(post.Title); + } +} + +// ✅ Eager Loading + 投影 +await foreach (var blog in context.Blogs + .Select(b => new { b.Url, b.Posts }) + .AsAsyncEnumerable()) +{ + foreach (var post in blog.Posts) + Console.WriteLine(post.Title); +} +``` + +### 过度获取(不投影) + +```csharp +// ❌ 加载所有列——只需要 Url 时加载了全部字段 +var urls = await context.Blogs.ToListAsync(); + +// ✅ 只投影需要的字段 +var urls = await context.Blogs + .Select(b => b.Url) + .ToListAsync(); +``` + +### 缺少分页 + +```csharp +// ❌ 无界结果集 +var posts = await context.Posts + .Where(p => p.Title.StartsWith("A")) + .ToListAsync(); // 可能有百万条记录! + +// ✅ 限制结果数量 +var posts = await context.Posts + .Where(p => p.Title.StartsWith("A")) + .OrderBy(p => p.Id) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); +``` + +### Cartesian Explosion(JOIN 笛卡尔爆炸) + +```csharp +// ❌ 多个 Include 创建大量重复数据 +var blogs = await context.Blogs + .Include(b => b.Posts) + .Include(b => b.Tags) + .ToListAsync(); // 每行重复 Blog 数据 + +// ✅ 使用 AsSplitQuery 拆分查询 +var blogs = await context.Blogs + .Include(b => b.Posts) + .Include(b => b.Tags) + .AsSplitQuery() + .ToListAsync(); +``` + +### 只读场景缺少 AsNoTracking + +```csharp +// ❌ 默认跟踪——只读查询也付出跟踪开销 +var products = await context.Products.ToListAsync(); + +// ✅ AsNoTracking——跳过变更跟踪,更快且更省内存 +var products = await context.Products + .AsNoTracking() + .ToListAsync(); +``` + +### 列上函数阻止索引使用 + +```csharp +// ✅ 可以使用索引——sargable +var posts1 = await context.Posts + .Where(p => p.Title.StartsWith("A")) + .ToListAsync(); + +// ❌ 无法使用索引——全表扫描 +var posts2 = await context.Posts + .Where(p => p.Title.EndsWith("A")) + .ToListAsync(); + +// ❌ 列上套函数——全表扫描 +var posts3 = await context.Posts + .Where(p => p.Title.ToLower() == "foo") + .ToListAsync(); +``` + +### 同步 vs 异步数据库访问 + +```csharp +// ❌ 同步数据库调用——阻塞线程 +var products = context.Products.ToList(); +context.SaveChanges(); + +// ✅ 异步数据库调用 +var products = await context.Products.ToListAsync(); +await context.SaveChangesAsync(); +``` + +--- + +## ASP.NET Core 最佳实践 + +### HttpClient 误用 + +```csharp +// ❌ 每次请求创建新的 HttpClient——socket 耗尽 +using var client = new HttpClient(); +var response = await client.GetAsync("https://api.example.com/data"); + +// ✅ IHttpClientFactory 注入 +public class MyService +{ + private readonly HttpClient _client; + public MyService(HttpClient client) => _client = client; // 从工厂注入 +} +``` + +### HttpContext 在后台线程中使用 + +```csharp +// ❌ 在后台任务中捕获 scoped 服务——请求结束后已释放 +_ = Task.Run(async () => +{ + await context.SaveChangesAsync(); // ObjectDisposedException! +}); + +// ✅ 创建新的 scope +_ = Task.Run(async () => +{ + await using var scope = serviceScopeFactory.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + 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(); +services.AddScoped(); + +// BackgroundWorker 是 Singleton,UserRepository 是 Scoped +// → UserRepository 在多个请求间共享或已释放 + +// ✅ 在 Singleton 中通过 IServiceProvider 创建 scope +public class BackgroundWorker : BackgroundService +{ + private readonly IServiceScopeFactory _scopeFactory; + + public BackgroundWorker(IServiceScopeFactory scopeFactory) + => _scopeFactory = scopeFactory; + + protected override async Task ExecuteAsync(CancellationToken ct) + { + await using var scope = _scopeFactory.CreateAsyncScope(); + var repo = scope.ServiceProvider.GetRequiredService(); + } +} +``` + +--- + +## 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 numbers) +{ + if (numbers.Any()) // 第一次枚举 + { + foreach (var n in numbers) // 第二次枚举(可能是重新查询) + { + Console.WriteLine(n); + } + } +} + +// ✅ 如果需要多次使用,先物化 +public void Process(IEnumerable 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 中无副作用 diff --git a/.claude/skills/code-review-skill/reference/css-less-sass.md b/.claude/skills/code-review-skill/reference/css-less-sass.md new file mode 100644 index 0000000..782c5d9 --- /dev/null +++ b/.claude/skills/code-review-skill/reference/css-less-sass.md @@ -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) diff --git a/.claude/skills/code-review-skill/reference/django.md b/.claude/skills/code-review-skill/reference/django.md new file mode 100644 index 0000000..2f5995d --- /dev/null +++ b/.claude/skills/code-review-skill/reference/django.md @@ -0,0 +1,1030 @@ +# Django / DRF Code Review Guide + +> Django / DRF 代码审查指南,覆盖安全审查、N+1 查询优化、Serializer 反模式、ViewSet 最佳实践、异步视图及生产安全配置等核心主题。 + +## 目录 + +- [安全审查](#安全审查) +- [N+1 查询优化](#n1-查询优化) +- [Serializer 反模式](#serializer-反模式) +- [ViewSet 最佳实践](#viewset-最佳实践) +- [异步视图](#异步视图) +- [中间件与设置](#中间件与设置) +- [Review Checklist](#review-checklist) + +--- + +## 安全审查 + +### XSS 防护 + +```python +from django.utils.safestring import mark_safe +from django.template import engines + +# ❌ mark_safe 绕过自动转义,直接渲染用户输入 +def user_profile(request): + user_bio = request.user.bio # 用户可控 + return HttpResponse(mark_safe(f"

{user_bio}

")) + +# ❌ 在模板中手动关闭 autoescape +# {% autoescape off %}{{ user_bio }}{% endautoescape %} + +# ✅ 让 Django 模板引擎自动转义 +# template:

{{ user_bio }}

+ +# ✅ 必须使用 mark_safe 时,先手动转义 +from django.utils.html import escape + +def render_bio(bio: str) -> str: + return mark_safe(f"

{escape(bio)}

") +``` + +### CSRF 防护 + +```python +from django.views.decorators.csrf import csrf_exempt + +# ❌ 禁用 CSRF 保护 +@csrf_exempt +def process_payment(request): + # 任何恶意网站都可以提交表单 + amount = request.POST["amount"] + charge(amount) + +# ✅ 保留默认 CSRF 保护 +from django.middleware.csrf import CsrfViewMiddleware + +# settings.py — 确保 CSRF 中间件已启用 +MIDDLEWARE = [ + # ... + "django.middleware.csrf.CsrfViewMiddleware", + # ... +] + +# ✅ API 使用 token 认证代替 CSRF +# settings.py +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.SessionAuthentication", + "rest_framework.authentication.TokenAuthentication", + ], +} + +# ✅ 前端 AJAX 请求带上 CSRF token +# JavaScript: fetch("/api/endpoint/", { +# headers: {"X-CSRFToken": document.querySelector("[name=csrfmiddlewaretoken]").value} +# }) +``` + +### Cookie 安全设置 + +```python +# settings.py + +# ❌ 不安全的 cookie 配置 +SESSION_COOKIE_SECURE = False +CSRF_COOKIE_SECURE = False +SESSION_COOKIE_HTTPONLY = False + +# ✅ 生产环境 cookie 安全配置 +SESSION_COOKIE_SECURE = True # HTTPS only +SESSION_COOKIE_HTTPONLY = True # JavaScript 无法读取 +SESSION_COOKIE_SAMESITE = "Lax" # 防止 CSRF +CSRF_COOKIE_SECURE = True +CSRF_COOKIE_HTTPONLY = True +CSRF_COOKIE_SAMESITE = "Lax" +``` + +### SQL 注入防护 + +```python +from django.db import connection + +# ❌ 字符串拼接 SQL — SQL 注入风险 +def search_users(keyword): + query = f"SELECT * FROM auth_user WHERE username LIKE '%{keyword}%'" + with connection.cursor() as cursor: + cursor.execute(query) + +# ❌ extra() 方法不安全 +User.objects.extra( + where=[f"username = '{keyword}'"] +) + +# ✅ 使用 ORM 参数化查询 +def search_users(keyword): + return User.objects.filter(username__icontains=keyword) + +# ✅ 原始 SQL 使用参数化 +def search_users(keyword): + with connection.cursor() as cursor: + cursor.execute( + "SELECT * FROM auth_user WHERE username LIKE %s", + [f"%{keyword}%"], + ) + +# ✅ 使用 raw() 参数化 +User.objects.raw( + "SELECT * FROM auth_user WHERE username LIKE %s", + [f"%{keyword}%"], +) +``` + +### 文件上传安全 + +```python +# settings.py + +# ❌ 默认上传配置不安全 +FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 # 2.5 MB — 可以接受 +MEDIA_ROOT = "/var/www/uploads" # web 根目录下 +ALLOWED_UPLOAD_TYPES = None # 没有类型限制 + +# ✅ 限制上传大小和位置 +DATA_UPLOAD_MAX_MEMORY_SIZE = 10485760 # 10 MB +FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 # 2.5 MB in-memory +MEDIA_ROOT = "/srv/media/" # web 根目录之外 + +# ✅ 验证文件类型 +import mimetypes +from pathlib import Path + +ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".pdf"} + +def validate_upload(file): + ext = Path(file.name).suffix.lower() + if ext not in ALLOWED_EXTENSIONS: + raise ValidationError(f"File type {ext} is not allowed.") + mime, _ = mimetypes.guess_type(file.name) + if mime not in {"image/jpeg", "image/png", "application/pdf"}: + raise ValidationError("Invalid MIME type.") +``` + +--- + +## N+1 查询优化 + +### select_related(ForeignKey / OneToOne) + +```python +# ❌ N+1: 每本书查一次出版社 +books = Book.objects.all() +for book in books: + print(book.publisher.name) # 额外 N 条查询 + +# ✅ select_related 一次 JOIN 查询 +books = Book.objects.select_related("publisher") +for book in books: + print(book.publisher.name) # 无额外查询 + +# ✅ 多层关系 +books = Book.objects.select_related("publisher", "publisher__country") + +# ✅ 只查需要的字段(延迟加载优化) +books = Book.objects.select_related("publisher").only( + "title", "publisher__name" +) +``` + +### prefetch_related(M2M / 反向 ForeignKey) + +```python +# ❌ N+1: 每个作者查一次书 +authors = Author.objects.all() +for author in authors: + print(author.books.all()) # 额外 N 条查询 + +# ✅ prefetch_related 两条查询 + Python 合并 +authors = Author.objects.prefetch_related("books") +for author in authors: + print(list(author.books.all())) # 无额外查询 + +# ✅ 嵌套 prefetch +authors = Author.objects.prefetch_related( + "books", + "books__publisher", +) + +# ✅ Prefetch 对象控制预查行为 +from django.db.models import Prefetch + +authors = Author.objects.prefetch_related( + Prefetch( + "books", + queryset=Book.objects.filter(published=True).only("title", "author_id"), + to_attr="published_books", + ) +) +for author in authors: + print(author.published_books) # 已过滤,存在 to_attr 中 +``` + +### QuerySet 缓存误用 + +```python +# ❌ count() 后再迭代 —— 两次查询 +qs = Book.objects.all() +count = qs.count() # 查询 1: SELECT COUNT(*) — 不填充缓存 +titles = [b.title for b in qs] # 查询 2: SELECT * — 重新评估 + +# ✅ 既要对象又要数量时,用 len() 触发一次评估并复用缓存 +qs = Book.objects.all() +count = len(qs) # 查询 1: SELECT * — 全部加载并缓存 +titles = [b.title for b in qs] # 复用缓存,无新查询 + +# ✅ 如果需要多次迭代,先转 list +books = list(Book.objects.all()) # 一次查询 +count = len(books) +titles = [b.title for b in books] +``` + +### 切片/索引不填充缓存 + +```python +# ❌ 反复索引未评估的 QuerySet —— 每次都查库 +qs = Book.objects.all() +qs[0] # 查询 1: SELECT ... LIMIT 1 +qs[0] # 查询 2 — 切片/索引不会填充缓存 + +# ✅ 先整体评估,缓存保存所有行,之后索引走缓存 +qs = Book.objects.all() +list(qs) # SELECT * — 评估并缓存全部行 +qs[0] # 走缓存,无查询 +qs[5] # 走缓存,无查询 + +# ✅ 只需要前 N 条时,切一次并转 list +books = list(Book.objects.all()[:10]) # 一次查询:SELECT ... LIMIT 10 +first = books[0] +rest = books[1:] # 已是 Python list,无查询 +``` + +### len() vs count() + +```python +# ❌ len() 加载全部对象到内存 +total = len(Book.objects.all()) # SELECT * FROM book — 全表加载 + +# ✅ count() 在数据库端计数 +total = Book.objects.count() # SELECT COUNT(*) — 高效 + +# ✅ 如果已经需要 QuerySet 结果,再用 len +books = list(Book.objects.filter(published=True)) +total = len(books) # 已在内存中,不需要额外查询 +``` + +### if qs vs qs.exists() + +```python +# ❌ if qs 加载全部记录 +qs = Book.objects.filter(author_id=author_id) +if qs: # SELECT * FROM book WHERE ... — 全部加载 + return qs[0] + +# ✅ exists() 只检查是否有记录 +if Book.objects.filter(author_id=author_id).exists(): + return Book.objects.filter(author_id=author_id).first() + +# ✅ 或者直接 get/first 判空 +book = Book.objects.filter(author_id=author_id).first() +if book is not None: + return book +``` + +--- + +## Serializer 反模式 + +### 排除敏感字段 + +```python +from rest_framework import serializers + +# ❌ __all__ 暴露所有字段,包括敏感数据 +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = "__all__" # 密码 hash、is_superuser 等全部暴露 + +# ✅ 显式列出允许的字段 +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ["id", "username", "email", "first_name", "last_name"] + +# ✅ 使用 exclude 时也要注意 +class UserProfileSerializer(serializers.ModelSerializer): + class Meta: + model = UserProfile + exclude = ["internal_notes", "admin_flags"] + +# ✅ 密码字段用 write_only +class RegistrationSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True, min_length=8) + + class Meta: + model = User + fields = ["id", "username", "email", "password"] + + def create(self, validated_data): + user = User(**validated_data) + user.set_password(validated_data["password"]) + user.save() + return user +``` + +### 缺少验证 + +```python +from rest_framework import serializers + +# ❌ 没有验证,信任所有输入 +class OrderSerializer(serializers.ModelSerializer): + class Meta: + model = Order + fields = ["quantity", "price", "discount"] + +# ✅ 字段级验证 +class OrderSerializer(serializers.ModelSerializer): + quantity = serializers.IntegerField(min_value=1, max_value=100) + price = serializers.DecimalField(max_digits=10, decimal_places=2, min_value=0) + discount = serializers.DecimalField( + max_digits=5, decimal_places=2, min_value=0, max_value=1, required=False + ) + + class Meta: + model = Order + fields = ["quantity", "price", "discount"] + +# ✅ 对象级验证 +class OrderSerializer(serializers.ModelSerializer): + class Meta: + model = Order + fields = ["quantity", "price", "discount"] + + def validate(self, attrs): + if attrs.get("discount", 0) > 0.5 and attrs.get("quantity", 0) < 10: + raise serializers.ValidationError( + "Bulk discount requires minimum 10 items." + ) + return attrs + +# ✅ 自定义字段验证方法 +class BookingSerializer(serializers.ModelSerializer): + class Meta: + model = Booking + fields = ["start_date", "end_date", "room"] + + def validate_start_date(self, value): + if value < date.today(): + raise serializers.ValidationError("Start date cannot be in the past.") + return value + + def validate(self, attrs): + if attrs["end_date"] <= attrs["start_date"]: + raise serializers.ValidationError("End date must be after start date.") + return attrs +``` + +### 嵌套写入 + +```python +from rest_framework import serializers + +# ❌ 嵌套 Serializer 只读但没有实现 create/update +class TagSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + fields = ["id", "name"] + +class ArticleSerializer(serializers.ModelSerializer): + tags = TagSerializer(many=True) # 嵌套写入会失败 + + class Meta: + model = Article + fields = ["id", "title", "tags"] + +# ✅ 方案 1: 嵌套只读 + PrimaryKeyRelatedField 写入 +class ArticleSerializer(serializers.ModelSerializer): + tags = TagSerializer(many=True, read_only=True) + tag_ids = serializers.PrimaryKeyRelatedField( + queryset=Tag.objects.all(), + many=True, + write_only=True, + source="tags", + ) + + class Meta: + model = Article + fields = ["id", "title", "tags", "tag_ids"] + +# ✅ 方案 2: 实现 create() 处理嵌套 +class ArticleSerializer(serializers.ModelSerializer): + tags = TagSerializer(many=True) + + class Meta: + model = Article + fields = ["id", "title", "tags"] + + def create(self, validated_data): + tags_data = validated_data.pop("tags") + article = Article.objects.create(**validated_data) + for tag_data in tags_data: + tag, _ = Tag.objects.get_or_create(**tag_data) + article.tags.add(tag) + return article + + def update(self, instance, validated_data): + tags_data = validated_data.pop("tags", None) + instance = super().update(instance, validated_data) + if tags_data is not None: + instance.tags.clear() + for tag_data in tags_data: + tag, _ = Tag.objects.get_or_create(**tag_data) + instance.tags.add(tag) + return instance +``` + +### read_only_fields 遗漏 + +```python +from rest_framework import serializers + +# ❌ 计算字段和自动字段可被用户覆盖 +class CommentSerializer(serializers.ModelSerializer): + class Meta: + model = Comment + fields = ["id", "body", "author", "created_at", "updated_at"] + # created_at, updated_at, author 可被客户端篡改 + +# ✅ 标记只读字段 +class CommentSerializer(serializers.ModelSerializer): + class Meta: + model = Comment + fields = ["id", "body", "author", "created_at", "updated_at"] + read_only_fields = ["author", "created_at", "updated_at"] + +# ✅ 在视图中设置只读字段(如当前用户) +class CommentViewSet(viewsets.ModelViewSet): + serializer_class = CommentSerializer + + def get_serializer_context(self): + context = super().get_serializer_context() + context["request"] = self.request + return context + + def perform_create(self, serializer): + serializer.save(author=self.request.user) +``` + +--- + +## ViewSet 最佳实践 + +### 选择正确的基类 + +```python +from rest_framework import viewsets + +# ❌ ModelViewSet 提供完整 CRUD,但只需要读取 +class TagViewSet(viewsets.ModelViewSet): + queryset = Tag.objects.all() + serializer_class = TagSerializer + # 暴露了 destroy, update, create — 标签不应被随意修改 + +# ✅ 只读场景用 ReadOnlyModelViewSet +class TagViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Tag.objects.all() + serializer_class = TagSerializer + # 只提供 list 和 retrieve + +# ✅ 需要自定义操作时用 Mixin +from rest_framework import mixins + +class TagViewSet( + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.CreateModelMixin, + generics.GenericAPIView, +): + queryset = Tag.objects.all() + serializer_class = TagSerializer +``` + +### 用户级数据范围限定 + +```python +from rest_framework import viewsets + +# ❌ 任何用户可以看到所有数据 +class DocumentViewSet(viewsets.ModelViewSet): + queryset = Document.objects.all() + serializer_class = DocumentSerializer + +# ✅ get_queryset 限定当前用户数据 +class DocumentViewSet(viewsets.ModelViewSet): + serializer_class = DocumentSerializer + + def get_queryset(self): + return Document.objects.filter( + owner=self.request.user + ).select_related("owner") + +# ✅ 管理员看全部,普通用户看自己的 +class DocumentViewSet(viewsets.ModelViewSet): + serializer_class = DocumentSerializer + + def get_queryset(self): + qs = Document.objects.select_related("owner") + if self.request.user.is_staff: + return qs + return qs.filter(owner=self.request.user) + +# ✅ perform_create 自动关联当前用户 +class DocumentViewSet(viewsets.ModelViewSet): + serializer_class = DocumentSerializer + + def get_queryset(self): + return Document.objects.filter(owner=self.request.user) + + def perform_create(self, serializer): + serializer.save(owner=self.request.user) +``` + +### 权限控制 + +```python +from rest_framework import permissions, viewsets + +# ❌ 没有权限控制 +class ArticleViewSet(viewsets.ModelViewSet): + queryset = Article.objects.all() + serializer_class = ArticleSerializer + +# ✅ 类级别权限 +class ArticleViewSet(viewsets.ModelViewSet): + queryset = Article.objects.all() + serializer_class = ArticleSerializer + permission_classes = [permissions.IsAuthenticated] + +# ✅ 操作级别权限 +from rest_framework.decorators import action + +class ArticleViewSet(viewsets.ModelViewSet): + queryset = Article.objects.all() + serializer_class = ArticleSerializer + + def get_permissions(self): + if self.action in ("list", "retrieve"): + return [permissions.AllowAny()] + if self.action == "create": + return [permissions.IsAuthenticated()] + return [permissions.IsAdminUser()] + +# ✅ 自定义对象级权限 +class IsOwnerOrReadOnly(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + if request.method in permissions.SAFE_METHODS: + return True + return obj.owner == request.user +``` + +### 分页和节流 + +```python +# settings.py + +# ❌ 没有分页和节流配置 +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.SessionAuthentication", + ], +} + +# ✅ 全局分页和节流 +REST_FRAMEWORK = { + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 20, + "DEFAULT_THROTTLE_CLASSES": [ + "rest_framework.throttling.AnonRateThrottle", + "rest_framework.throttling.UserRateThrottle", + ], + "DEFAULT_THROTTLE_RATES": { + "anon": "100/hour", + "user": "1000/hour", + }, +} + +# ✅ 自定义分页器 +from rest_framework.pagination import PageNumberPagination + +class StandardPagination(PageNumberPagination): + page_size = 25 + page_size_query_param = "page_size" + max_page_size = 100 + +class ArticleViewSet(viewsets.ModelViewSet): + queryset = Article.objects.all() + serializer_class = ArticleSerializer + pagination_class = StandardPagination +``` + +--- + +## 异步视图 + +### 同步 ORM 在异步视图中的正确使用 + +```python +import asyncio +from asgiref.sync import sync_to_async +from django.http import JsonResponse + +# ❌ 在 async 视图中直接调用同步 ORM — 阻塞事件循环 +async def user_list(request): + users = User.objects.all() # Synchronous ORM call in async context! + data = [{"id": u.id, "name": u.username} for u in users] + return JsonResponse(data, safe=False) + +# ✅ 使用 async ORM(Django 4.1+) +async def user_list(request): + users = User.objects.all() + data = [] + async for user in users: # async iteration + data.append({"id": user.id, "name": user.username}) + return JsonResponse(data, safe=False) + +# ✅ 使用 aget / afilter / acreate +async def user_detail(request, pk): + user = await User.objects.aget(pk=pk) + return JsonResponse({"id": user.id, "name": user.username}) + +# ✅ 复杂查询用 sync_to_async +@sync_to_async +def get_user_with_profile(pk): + return User.objects.select_related("profile").get(pk=pk) + +async def user_profile(request, pk): + user = await get_user_with_profile(pk) + return JsonResponse({ + "id": user.id, + "name": user.username, + "bio": user.profile.bio, + }) +``` + +### 遗漏 await + +```python +from django.http import JsonResponse + +# ❌ 忘记 await — coroutine 不会执行,返回协程对象而非数据 +async def user_detail(request, pk): + user = User.objects.aget(pk=pk) # Missing await! + # user 是一个 coroutine 对象,不是 User 实例 + return JsonResponse({"name": user.username}) # RuntimeError + +# ✅ 始终 await 异步 ORM 调用 +async def user_detail(request, pk): + user = await User.objects.aget(pk=pk) + return JsonResponse({"name": user.username}) + +# ✅ 使用aget_or_404 的异步版本 +from django.shortcuts import aget_object_or_404 + +async def user_detail(request, pk): + user = await aget_object_or_404(User, pk=pk) + return JsonResponse({"name": user.username}) +``` + +### 异步视图中的事务 + +```python +from django.db import transaction +from asgiref.sync import sync_to_async + +# ❌ transaction.atomic() 是同步的,不能直接在 async 中用 +async def create_order(request): + async with transaction.atomic(): # Error! Not async-compatible + order = await Order.objects.acreate(total=100) + await OrderItem.objects.acreate(order=order, product_id=1) + return JsonResponse({"order_id": order.id}) + +# ✅ 用 sync_to_async 包装事务块 +@sync_to_async +def _create_order_with_items(): + with transaction.atomic(): + order = Order.objects.create(total=100) + OrderItem.objects.create(order=order, product_id=1) + return order.id + +async def create_order(request): + order_id = await _create_order_with_items() + return JsonResponse({"order_id": order_id}) + +# ✅ 多个操作打包到一个 sync_to_async 中 +@sync_to_async +def _bulk_create_products(items): + with transaction.atomic(): + products = Product.objects.bulk_create([Product(**i) for i in items]) + return [p.id for p in products] + +async def import_products(request): + ids = await _bulk_create_products(request.data) + return JsonResponse({"ids": ids}) +``` + +### 同步中间件拖慢异步性能 + +```python +# ❌ 同步中间件会把 async 视图降级为同步执行 +class TimingMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): # sync — blocks async views + start = time.time() + response = self.get_response(request) + elapsed = time.time() - start + response["X-Elapsed"] = str(elapsed) + return response + +# ✅ async-capable 中间件:async def __call__,并在 __init__ 里标记实例 +import time +from asgiref.sync import iscoroutinefunction, markcoroutinefunction + +class TimingMiddleware: + async_capable = True + sync_capable = False + + def __init__(self, get_response): + self.get_response = get_response + # get_response 是协程函数时标记自己,Django 才会 await 这个实例 + if iscoroutinefunction(self.get_response): + markcoroutinefunction(self) + + async def __call__(self, request): + start = time.time() + response = await self.get_response(request) + elapsed = time.time() - start + response["X-Elapsed"] = str(elapsed) + return response + +# ✅ 要同时兼容同步和异步,用工厂函数 + 内置装饰器 +from django.utils.decorators import sync_and_async_middleware +``` + +### async for 迭代模式 + +```python +from django.http import JsonResponse + +# ❌ 同步迭代大型 QuerySet 在 async 视图中阻塞 +async def export_users(request): + users = User.objects.all() + data = [] # 同步迭代阻塞事件循环 + for user in users: + data.append({"id": user.id, "name": user.username}) + return JsonResponse(data, safe=False) + +# ✅ 使用 async for 异步迭代 +async def export_users(request): + data = [] + async for user in User.objects.all(): + data.append({"id": user.id, "name": user.username}) + return JsonResponse(data, safe=False) + +# ✅ 大数据集使用 aiterator() + 分块处理 +async def export_large_dataset(request): + data = [] + async for user in User.objects.all().aiterator(chunk_size=500): + data.append({"id": user.id, "name": user.username}) + return JsonResponse(data, safe=False) + +# ✅ 使用 values() 减少内存 +async def lightweight_export(request): + data = [] + async for row in User.objects.values("id", "username"): + data.append(row) + return JsonResponse(data, safe=False) +``` + +--- + +## 中间件与设置 + +### 生产安全配置清单 + +```python +# settings.py — 生产环境必须的安全设置 + +# ❌ 开发默认值不应出现在生产环境 +DEBUG = True +SECRET_KEY = "django-insecure-..." +ALLOWED_HOSTS = ["*"] +SECURE_SSL_REDIRECT = False + +# ✅ 生产环境安全配置 + +# --- 基础安全 --- +DEBUG = False +SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # 从环境变量读取 +ALLOWED_HOSTS = ["example.com", "www.example.com"] + +# --- HTTPS --- +SECURE_SSL_REDIRECT = True # HTTP 重定向到 HTTPS +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True + +# --- 安全头 --- +SECURE_HSTS_SECONDS = 31536000 # 1 year HSTS +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_HSTS_PRELOAD = True +SECURE_CONTENT_TYPE_NOSNIFF = True # X-Content-Type-Options: nosniff +X_FRAME_OPTIONS = "DENY" # 防止 clickjacking +SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin" + +# --- 密码验证 --- +AUTH_PASSWORD_VALIDATORS = [ + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + "OPTIONS": {"min_length": 12}}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, + {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, +] + +# --- Session --- +SESSION_COOKIE_AGE = 3600 * 8 # 8 hours +SESSION_SAVE_EVERY_REQUEST = True +SESSION_EXPIRE_AT_BROWSER_CLOSE = True +``` + +### 数据库连接安全 + +```python +# settings.py + +# ❌ 明文密码在代码中 +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": "mydb", + "USER": "admin", + "PASSWORD": "hunter2", # 不要硬编码密码 + "HOST": "localhost", + "PORT": "5432", + } +} + +# ✅ 从环境变量读取 +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.environ.get("DB_NAME", "mydb"), + "USER": os.environ.get("DB_USER", "mydb_user"), + "PASSWORD": os.environ["DB_PASSWORD"], + "HOST": os.environ.get("DB_HOST", "localhost"), + "PORT": os.environ.get("DB_PORT", "5432"), + "OPTIONS": { + "sslmode": "require", # 强制 SSL 连接 + }, + "CONN_MAX_AGE": 60, # 持久连接 + } +} +``` + +### CORS 配置 + +```python +# settings.py (using django-cors-headers) + +# ❌ 允许所有来源 +CORS_ALLOW_ALL_ORIGINS = True + +# ✅ 限制允许的来源 +CORS_ALLOWED_ORIGINS = [ + "https://example.com", + "https://app.example.com", +] + +# ✅ 生产环境 CORS 设置 +CORS_ALLOW_ALL_ORIGINS = False +CORS_ALLOWED_ORIGINS = os.environ.get("CORS_ORIGINS", "").split(",") +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOW_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] +CORS_ALLOW_HEADERS = [ + "authorization", + "content-type", + "x-csrftoken", +] +``` + +### 日志配置 + +```python +# settings.py + +# ❌ 默认日志配置(或不配置) +LOGGING = {} + +# ✅ 生产环境日志配置 +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}", + "style": "{", + }, + }, + "handlers": { + "file": { + "level": "INFO", + "class": "logging.handlers.RotatingFileHandler", + "filename": "/var/log/django/app.log", + "maxBytes": 10 * 1024 * 1024, # 10 MB + "backupCount": 5, + "formatter": "verbose", + }, + }, + "loggers": { + "django": { + "handlers": ["file"], + "level": "INFO", + "propagate": False, + }, + "myapp": { + "handlers": ["file"], + "level": "DEBUG" if DEBUG else "INFO", + "propagate": False, + }, + }, +} +``` + +--- + +## Review Checklist + +### 安全审查 + +- [ ] 没有使用 `mark_safe` 渲染未转义的用户输入 +- [ ] CSRF 中间件已启用,没有 `@csrf_exempt` +- [ ] Session 和 CSRF cookie 设置 `Secure`, `HttpOnly`, `SameSite` +- [ ] SQL 查询使用参数化(ORM 或参数化 `raw()`),无字符串拼接 +- [ ] 文件上传有类型和大小限制 +- [ ] `SECRET_KEY` 从环境变量读取,不在代码仓库中 +- [ ] `DEBUG = False` 在生产环境 + +### HTTPS 与安全头 + +- [ ] `SECURE_SSL_REDIRECT = True` +- [ ] `SECURE_HSTS_SECONDS` 已设置(≥ 31536000) +- [ ] `SECURE_CONTENT_TYPE_NOSNIFF = True` +- [ ] `X_FRAME_OPTIONS` 设置为 `DENY` 或 `SAMEORIGIN` +- [ ] `ALLOWED_HOSTS` 不包含 `"*"` +- [ ] 数据库连接使用 SSL + +### N+1 查询 + +- [ ] ForeignKey 关系使用 `select_related` +- [ ] M2M / 反向关系使用 `prefetch_related` +- [ ] 没有在循环中访问关联对象 +- [ ] 使用 `count()` 代替 `len(queryset)` 做计数 +- [ ] 使用 `exists()` 代替 `if queryset` 做存在性检查 +- [ ] 大数据集使用 `only()` / `defer()` 或 `values()` 减少查询字段 +- [ ] 切片后的 QuerySet 不重复迭代 + +### Serializer + +- [ ] 不使用 `fields = "__all__"` 在敏感模型上 +- [ ] 密码字段标记 `write_only=True` +- [ ] 有字段级和对象级验证 +- [ ] 嵌套写入实现了 `create()` / `update()` 或使用 `read_only=True` +- [ ] 计算字段和自动字段在 `read_only_fields` 中 +- [ ] Serializer 不包含不应被修改的字段 + +### ViewSet + +- [ ] 只读场景使用 `ReadOnlyModelViewSet` +- [ ] `get_queryset()` 限定当前用户数据范围 +- [ ] 设置了 `permission_classes` +- [ ] 创建时用 `perform_create()` 自动设置 owner/author +- [ ] 配置了分页(全局或 ViewSet 级别) +- [ ] 配置了节流(throttling) + +### 异步视图 + +- [ ] async 视图中不直接调用同步 ORM(用 `aget`/`afilter`/`sync_to_async`) +- [ ] 所有异步调用都有 `await` +- [ ] `transaction.atomic()` 用 `sync_to_async` 包装 +- [ ] 中间件标记 `async_capable = True` 以避免降级 +- [ ] 大型 QuerySet 使用 `async for` + `aiterator()` + +### 生产配置 + +- [ ] `CORS_ALLOWED_ORIGINS` 不使用 `CORS_ALLOW_ALL_ORIGINS = True` +- [ ] 密码验证器已配置(最小长度、常见密码检查) +- [ ] Session 过期时间合理(`SESSION_COOKIE_AGE`) +- [ ] 日志配置使用 RotatingFileHandler,不在生产环境输出到 stdout +- [ ] 数据库连接使用 `CONN_MAX_AGE` 持久连接 diff --git a/.claude/skills/code-review-skill/reference/fastapi.md b/.claude/skills/code-review-skill/reference/fastapi.md new file mode 100644 index 0000000..d945f80 --- /dev/null +++ b/.claude/skills/code-review-skill/reference/fastapi.md @@ -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) diff --git a/.claude/skills/code-review-skill/reference/go.md b/.claude/skills/code-review-skill/reference/go.md new file mode 100644 index 0000000..2438fea --- /dev/null +++ b/.claude/skills/code-review-skill/reference/go.md @@ -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) diff --git a/.claude/skills/code-review-skill/reference/java.md b/.claude/skills/code-review-skill/reference/java.md new file mode 100644 index 0000000..9646d67 --- /dev/null +++ b/.claude/skills/code-review-skill/reference/java.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 result = list.stream() + .filter(...) + .map(...) + .peek(...) + .sorted(...) + .collect(...); // 难以调试 + +// ✅ 拆分为有意义的步骤 +var filtered = list.stream().filter(...).toList(); +// ... +``` + +### Optional 正确用法 + +```java +// ❌ 将 Optional 用作参数或字段(序列化问题,增加调用复杂度) +public void process(Optional name) { ... } +public class User { + private Optional email; // 不推荐 +} + +// ✅ Optional 仅用于返回值 +public Optional findUser(String id) { ... } + +// ❌ 既然用了 Optional 还在用 isPresent() + get() +Optional 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 orders; +} + +// 业务代码 +List 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 findAllWithOrders(); +``` + +### 事务管理 + +```java +// ❌ 在 Controller 层开启事务(数据库连接占用时间过长) +// ❌ 在 private 方法上加 @Transactional(AOP 不生效) +@Transactional +private void saveInternal() { ... } + +// ✅ 在 Service 层公共方法加 @Transactional +// ✅ 读操作显式标记 readOnly = true (性能优化) +@Service +public class UserService { + @Transactional(readOnly = true) + public User getUser(Long id) { ... } + + @Transactional + public void createUser(UserDto dto) { ... } +} +``` + +### Entity 设计 + +```java +// ❌ 在 Entity 中使用 Lombok @Data +// @Data 生成的 equals/hashCode 包含所有字段,可能触发懒加载导致性能问题或异常 +@Entity +@Data +public class User { ... } + +// ✅ 仅使用 @Getter, @Setter +// ✅ 自定义 equals/hashCode (通常基于 ID) +@Entity +@Getter +@Setter +public class User { + @Id + private Long id; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof User)) return false; + return id != null && id.equals(((User) o).id); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} +``` + +--- + +## 并发与虚拟线程 + +### 虚拟线程 (Java 21+) + +```java +// ❌ 传统线程池处理大量 I/O 阻塞任务(资源耗尽) +ExecutorService executor = Executors.newFixedThreadPool(100); + +// ✅ 使用虚拟线程处理 I/O 密集型任务(高吞吐量) +// Spring Boot 3.2+ 开启:spring.threads.virtual.enabled=true +ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); + +// 在虚拟线程中,阻塞操作(如 DB 查询、HTTP 请求)几乎不消耗 OS 线程资源 +``` + +### 线程安全 + +```java +// ❌ SimpleDateFormat 是线程不安全的 +private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + +// ✅ 使用 DateTimeFormatter (Java 8+) +private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + +// ❌ HashMap 在多线程环境会数据丢失(Java 7 及之前 resize 还可能死循环,Java 8 修复了死循环但仍非线程安全) +// ✅ 使用 ConcurrentHashMap +Map 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) +- [ ] 魔法值提取为常量或枚举 diff --git a/.claude/skills/code-review-skill/reference/kotlin.md b/.claude/skills/code-review-skill/reference/kotlin.md new file mode 100644 index 0000000..94a35b0 --- /dev/null +++ b/.claude/skills/code-review-skill/reference/kotlin.md @@ -0,0 +1,1016 @@ +# Kotlin / Android Code Review Guide + +> Kotlin/Android 代码审查指南,覆盖协程作用域与取消、Flow 陷阱、Compose 重组、空安全、内存泄漏、架构分层与密封类状态建模等核心主题。 + +## 目录 + +- [协程:作用域与取消](#协程作用域与取消) +- [Flow 陷阱](#flow-陷阱) +- [Jetpack Compose 重组](#jetpack-compose-重组) +- [空安全模式](#空安全模式) +- [内存泄漏](#内存泄漏) +- [架构:ViewModel 与 Repository](#架构viewmodel-与-repository) +- [密封类与状态管理](#密封类与状态管理) +- [Review Checklist](#review-checklist) + +--- + +## 协程:作用域与取消 + +### 避免 GlobalScope + +```kotlin +// ❌ GlobalScope 生命周期不受控,Activity/Fragment 销毁后协程仍在运行 +GlobalScope.launch { + val data = api.fetchData() + binding.textView.text = data.title // Crash: view destroyed +} + +// ✅ 使用 viewModelScope,ViewModel 清除时自动取消 +class MyViewModel(private val repo: Repository) : ViewModel() { + fun loadData() { + viewModelScope.launch { + val data = repo.fetchData() + _uiState.value = UiState.Success(data) + } + } +} + +// ✅ 在 Activity/Fragment 中使用 lifecycleScope +class MyActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycleScope.launch { + val data = repo.fetchData() + binding.textView.text = data.title + } + } +} +``` + +### CancellationException 不能吞掉 + +```kotlin +// ❌ 捕获所有异常导致取消信号丢失 +viewModelScope.launch { + try { + repo.fetchData() + } catch (e: Exception) { + // CancellationException 被吞掉,协程无法取消 + showError(e) + } +} + +// ✅ 重新抛出 CancellationException +viewModelScope.launch { + try { + repo.fetchData() + } catch (e: CancellationException) { + throw e // Must rethrow + } catch (e: Exception) { + showError(e) + } +} + +// ✅ 或使用 catch 配合 ensureActive +viewModelScope.launch { + try { + repo.fetchData() + } catch (e: Exception) { + ensureActive() // Rethrows if cancelled + showError(e) + } +} +``` + +### CPU-bound 任务需要检查取消 + +```kotlin +// ❌ CPU 密集计算不响应取消,即使协程已取消也会跑完 +viewModelScope.launch(Dispatchers.Default) { + for (item in largeList) { + heavyComputation(item) + } +} + +// ✅ 定期检查 isActive 或调用 ensureActive +viewModelScope.launch(Dispatchers.Default) { + for (item in largeList) { + ensureActive() // Throws CancellationException if cancelled + heavyComputation(item) + } +} + +// ✅ 或使用 yield 让出执行权 +viewModelScope.launch(Dispatchers.Default) { + for (item in largeList) { + yield() // Checks cancellation + yields to other coroutines + heavyComputation(item) + } +} +``` + +### 阻塞操作使用 runInterruptible + +```kotlin +// ❌ 在协程中直接调用阻塞 I/O,阻塞线程池线程 +viewModelScope.launch(Dispatchers.IO) { + val result = blockingLibraryCall() // Blocks IO thread +} + +// ✅ 使用 runInterruptible 包装阻塞调用,支持取消中断 +viewModelScope.launch(Dispatchers.IO) { + val result = runInterruptible { + blockingLibraryCall() // Interrupted on cancellation + } +} +``` + +### 正确选择调度器 + +```kotlin +// ❌ CPU 密集任务用了 IO 调度器(线程池过大,浪费资源) +viewModelScope.launch(Dispatchers.IO) { + val bitmap = decodeImage(byteArray) // CPU-bound on IO pool +} + +// ✅ CPU 密集用 Default,I/O 操作用 IO +viewModelScope.launch(Dispatchers.Default) { + val bitmap = decodeImage(byteArray) // CPU-bound on Default pool +} + +// ❌ IO 操作用了 Default 调度器(线程池太小,容易饥饿) +viewModelScope.launch(Dispatchers.Default) { + val response = okHttpClient.newCall(request).execute() // I/O on Default pool +} + +// ✅ I/O 操作用 IO 调度器 +viewModelScope.launch(Dispatchers.IO) { + val response = okHttpClient.newCall(request).execute() +} +``` + +### launch vs async + +```kotlin +// ❌ async 只用于"发个火",不需要返回值 +viewModelScope.launch { + async { analytics.trackEvent("click") } // Overkill +} + +// ✅ 不需要返回值用 launch +viewModelScope.launch { + launch { analytics.trackEvent("click") } +} + +// ✅ 需要返回值且可能并行时用 async +viewModelScope.launch { + val deferredA = async { api.fetchA() } + val deferredB = async { api.fetchB() } + val result = combine(deferredA.await(), deferredB.await()) +} +``` + +### 不要用 Job() 破坏父子关系 + +```kotlin +// ❌ Job() 切断了父协程的取消传播 +viewModelScope.launch { + launch(Job()) { // Detached from parent scope! + importantWork() // Will NOT be cancelled when viewModelScope cancels + } +} + +// ✅ 保持默认的父子关系 +viewModelScope.launch { + launch { // Child of viewModelScope + importantWork() // Cancelled when viewModelScope cancels + } +} + +// ✅ 如果确实需要独立生命周期,显式管理并说明原因 +class MyManager(private val scope: CoroutineScope) { + // Independent lifecycle managed by MyManager.shutdown() + private val managerJob = Job(scope.coroutineContext[Job]) + private val managerScope = scope + managerJob + Dispatchers.IO + + fun shutdown() { + managerJob.cancel() + } +} +``` + +### NonCancellable 的正确使用 + +```kotlin +// ❌ 整个协程都包在 withContext(NonCancellable) 中,无法取消 +viewModelScope.launch { + withContext(NonCancellable) { // Entire block is uncancellable! + val data = repo.fetchData() // Cannot be cancelled + db.saveData(data) // Cannot be cancelled + analytics.track("saved") + } +} + +// ✅ NonCancellable 只用于清理操作 +viewModelScope.launch { + try { + val data = repo.fetchData() + db.saveData(data) + } catch (e: CancellationException) { + throw e + } finally { + withContext(NonCancellable) { + db.cleanup() // Only cleanup is uncancellable + } + } +} +``` + +--- + +## Flow 陷阱 + +### 冷流与热流混淆 + +```kotlin +// ❌ 每次 collect 都重新执行 flow {} 块(冷流特性被误解) +val userFlow = flow { + emit(api.fetchUser()) // Called once per collector! +} + +// Two collectors = two network requests +lifecycleScope.launch { userFlow.collect { } } +lifecycleScope.launch { userFlow.collect { } } + +// ✅ 共享数据用 StateFlow/SharedFlow(热流) +class MyViewModel(private val repo: Repository) : ViewModel() { + private val _uiState = MutableStateFlow(UiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + viewModelScope.launch { + _uiState.value = UiState.Success(repo.fetchUser()) + } + } +} +// Multiple collectors share the same StateFlow +``` + +### 不要在 flow {} 中切换上下文 + +```kotlin +// ❌ 在 flow builder 中使用 withContext,违反约束 +val dataFlow = flow { + withContext(Dispatchers.IO) { // IllegalStateException! + emit(api.fetchData()) + } +} + +// ✅ 使用 flowOn 操作符切换上游上下文 +val dataFlow = flow { + emit(api.fetchData()) // Runs on IO via flowOn +}.flowOn(Dispatchers.IO) + +// ✅ 或使用 channelFlow / callbackFlow 需要切换时 +val dataFlow = channelFlow { + withContext(Dispatchers.IO) { + send(api.fetchData()) // send() is safe in channelFlow + } +} +``` + +### collect 需要生命周期感知 + +```kotlin +// ❌ 在 Activity/Fragment 中 collect 不感知生命周期 +lifecycleScope.launch { + viewModel.uiState.collect { state -> + binding.textView.text = state.title // Crash if view destroyed + } +} + +// ✅ 在 Fragment 中使用 viewLifecycleOwner.lifecycleScope + repeatOnLifecycle +viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect { state -> + binding.textView.text = state.title + } + } +} + +// ✅ 在 Compose 中使用 collectAsStateWithLifecycle +@Composable +fun MyScreen(viewModel: MyViewModel) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + // ... +} +``` + +### 异常透明性:使用 catch 操作符 + +```kotlin +// ❌ 在 collect 中 try-catch 处理上游异常 +viewModelScope.launch { + try { + dataFlow.collect { data -> + processData(data) + } + } catch (e: Exception) { + // This also catches exceptions from processData, not just upstream + showError(e) + } +} + +// ✅ 使用 catch 操作符保持异常透明性 +viewModelScope.launch { + dataFlow + .catch { e -> showError(e) } // Only catches upstream exceptions + .collect { data -> + processData(data) // Exceptions here propagate normally + } +} +``` + +### StateFlow vs SharedFlow 选择 + +```kotlin +// ❌ 用 SharedFlow 模拟 StateFlow,丢失最新值语义 +private val _state = MutableSharedFlow() +val state: SharedFlow = _state + +// ✅ UI 状态用 StateFlow:总是有值、新订阅者立即获得最新值 +class MyViewModel : ViewModel() { + private val _uiState = MutableStateFlow(UiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() +} + +// ✅ 事件(一次性通知)用 SharedFlow + replay(0) +class MyViewModel : ViewModel() { + private val _navigationEvent = MutableSharedFlow(extraBufferCapacity = 1) + val navigationEvent: SharedFlow = _navigationEvent.asSharedFlow() + + fun navigate(target: NavTarget) { + _navigationEvent.tryEmit(target) + } +} + +// ✅ Channel 用于一次性事件(替代方案) +private val _navigationEvent = Channel(Channel.BUFFERED) +val navigationEvent = _navigationEvent.receiveAsFlow() +``` + +--- + +## Jetpack Compose 重组 + +### 不稳定参数导致多余重组 + +```kotlin +// ❌ 使用不稳定的类作为参数,Compose 无法判断是否变化 +data class UserProfile( + val name: String, + val friends: List, // Unstable! List is not @Stable +) + +@Composable +fun ProfileCard(profile: UserProfile) { // Recomposes even if profile didn't change + Text(profile.name) +} + +// ✅ 使用 @Immutable 标注或使用稳定的集合类型 +@Immutable +data class UserProfile( + val name: String, + val friends: ImmutableList, // kotlinx.collections.immutable +) + +// ✅ 或将不稳定属性提取为单独的参数 +@Composable +fun ProfileCard( + name: String, // Stable: String is primitive + friendCount: Int, // Stable: Int is primitive +) { + Text(name) + Text("$friendCount friends") +} +``` + +### Lambda 不稳定与记忆化 + +```kotlin +// ❌ 每次重组都创建新的 Lambda,导致子组件不必要的重组 +@Composable +fun MyScreen(viewModel: MyViewModel) { + LazyColumn { + items(items, key = { it.id }) { item -> + ItemRow( + item = item, + onClick = { viewModel.handleClick(item.id) } // New lambda each recomposition! + ) + } + } +} + +// ✅ 使用 remember 包装 Lambda,或让 ViewModel 暴露稳定回调 +@Composable +fun MyScreen(viewModel: MyViewModel) { + LazyColumn { + items(items, key = { it.id }) { item -> + ItemRow( + item = item, + onClick = remember(item.id) { { viewModel.handleClick(item.id) } } + ) + } + } +} +``` + +### 使用 derivedStateOf 避免高频率重组 + +```kotlin +// ❌ 每次滚动都重组整个组件 +@Composable +fun ScrollToTopButton(lazyListState: LazyListState) { + val showButton = lazyListState.firstVisibleItemIndex > 0 // Recomposes on every scroll + if (showButton) { + Button(onClick = { /* scroll to top */ }) { + Text("Top") + } + } +} + +// ✅ 使用 derivedStateOf 只在结果变化时触发重组 +@Composable +fun ScrollToTopButton(lazyListState: LazyListState) { + val showButton by remember { + derivedStateOf { lazyListState.firstVisibleItemIndex > 0 } + } + if (showButton) { + Button(onClick = { /* scroll to top */ }) { + Text("Top") + } + } +} +``` + +### 不要在 Composable 函数体中执行副作用 + +```kotlin +// ❌ 在 Composable 函数体中直接触发副作用,每次重组都会执行 +@Composable +fun MyScreen(userId: String, viewModel: MyViewModel) { + viewModel.loadUser(userId) // Called on every recomposition! + val user by viewModel.user.collectAsStateWithLifecycle() + Text(user?.name ?: "Loading...") +} + +// ✅ 使用 LaunchedEffect 在 key 变化时执行副作用 +@Composable +fun MyScreen(userId: String, viewModel: MyViewModel) { + LaunchedEffect(userId) { + viewModel.loadUser(userId) // Only when userId changes + } + val user by viewModel.user.collectAsStateWithLifecycle() + Text(user?.name ?: "Loading...") +} + +// ✅ 一次性初始化用 remember { ... } +@Composable +fun MyScreen(viewModel: MyViewModel) { + val initialData = remember { viewModel.getInitialData() } +} +``` + +### 状态提升 + +```kotlin +// ❌ 状态和逻辑耦合在 Composable 内部,无法复用和测试 +@Composable +fun ToggleButton() { + var isChecked by remember { mutableStateOf(false) } + Switch( + checked = isChecked, + onCheckedChange = { isChecked = it } + ) +} + +// ✅ 状态提升:调用者控制状态 +@Composable +fun ToggleButton( + isChecked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + Switch( + checked = isChecked, + onCheckedChange = onCheckedChange, + modifier = modifier, + ) +} + +// ✅ 调用者持有状态 +@Composable +fun ParentScreen() { + var enabled by rememberSaveable { mutableStateOf(false) } + ToggleButton( + isChecked = enabled, + onCheckedChange = { enabled = it }, + ) +} +``` + +--- + +## 空安全模式 + +### 避免非空断言 !! + +```kotlin +// ❌ 非空断言:如果为 null 直接 NPE +val user = getUser()!! +val name = user.name!! + +// ✅ 安全调用 + 空合并 +val name = getUser()?.name ?: "Unknown" + +// ✅ requireNotNull 提供有意义的错误信息 +val user = requireNotNull(getUser()) { "User must not be null at this point" } + +// ✅ 提前返回 +fun process(user: User?) { + val nonNullUser = user ?: return + nonNullUser.doSomething() +} +``` + +### lateinit vs nullable vs lazy + +```kotlin +// ❌ lateinit 用于可能为 null 的值(语义不对) +lateinit var optionalConfig: Config // Might never be set + +// ✅ lateinit 用于一定会在使用前初始化的值 +class MyActivity : AppCompatActivity() { + lateinit var binding: ActivityMainBinding // Set in onCreate + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + } +} + +// ✅ nullable + lateinit 取决于初始化时机 +// lateinit: 生命周期保证在使用前初始化 +// nullable: 不确定是否初始化,需要 null 检查 +// lazy: 确定在首次访问时初始化 + +class MyViewModel(private val repo: Repository) : ViewModel() { + // lazy: 首次访问时初始化,线程安全 + val expensiveObject by lazy { ExpensiveObject(repo) } + + // nullable: 可能不会初始化 + var cachedData: Data? = null + private set +} +``` + +### Java 互操作:平台类型泄漏 + +```kotlin +// ❌ Java 返回平台类型(可能 null),Kotlin 当作非空使用 +// Java: +// public User getUser() { return null; } +val name: String = javaService.getUser().name // NPE! + +// ✅ 使用可空类型接收 Java 返回值 +val user: User? = javaService.getUser() +val name = user?.name ?: "Unknown" + +// ✅ 在 Kotlin 侧包装 Java API,提供安全的类型 +class SafeUserService(private val delegate: JavaUserService) { + fun getUser(): User? = delegate.getUser() // Explicitly nullable +} +``` + +--- + +## 内存泄漏 + +### 避免在长生命周期协程中捕获 Context/View + +```kotlin +// ❌ 协程捕获了 Activity Context,Activity 销毁后无法回收 +class MyActivity : AppCompatActivity() { + fun loadData() { + // Leaking Activity via coroutine + GlobalScope.launch { + val data = repo.fetchData() + // 'this' (Activity) is captured + binding.textView.text = data // Activity leaked! + } + } +} + +// ✅ 使用 viewModelScope + 生命周期感知 +class MyViewModel(private val repo: Repository) : ViewModel() { + private val _uiState = MutableStateFlow(UiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + fun loadData() { + viewModelScope.launch { + val data = repo.fetchData() + _uiState.value = UiState.Success(data) // No Activity reference + } + } +} +``` + +### 注销监听器 + +```kotlin +// ❌ 注册监听器但从不注销 +class MyFragment : Fragment() { + private val sensorListener = object : SensorEventListener { + override fun onSensorChanged(event: SensorEvent) { } + override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { } + } + + override fun onResume() { + super.onResume() + sensorManager.registerListener(sensorListener, sensor, SensorManager.SENSOR_DELAY_UI) + // Never unregistered! + } +} + +// ✅ 在 onPause/onDestroyView 中注销 +override fun onResume() { + super.onResume() + sensorManager.registerListener(sensorListener, sensor, SensorManager.SENSOR_DELAY_UI) +} + +override fun onPause() { + super.onPause() + sensorManager.unregisterListener(sensorListener) +} +``` + +### 取消自定义 CoroutineScope + +```kotlin +// ❌ 创建 CoroutineScope 但从不取消 +class MyManager(private val scope: CoroutineScope) { + private val job = SupervisorJob() + private val managerScope = scope + job + Dispatchers.IO + + fun start() { + managerScope.launch { + while (isActive) { + pollServer() + delay(5000) + } + } + } + // Never cancelled! job lives forever. +} + +// ✅ 提供关闭方法并取消 Job +class MyManager(private val scope: CoroutineScope) { + private val job = SupervisorJob() + private val managerScope = scope + job + Dispatchers.IO + + fun start() { + managerScope.launch { + while (isActive) { + pollServer() + delay(5000) + } + } + } + + fun shutdown() { + job.cancel() + } +} + +// ✅ ViewModel 里直接用内置的 viewModelScope,不用自己管生命周期 +class MyViewModel : ViewModel() { + private val scope = viewModelScope + Dispatchers.IO + // Automatically cancelled when ViewModel is cleared +} +``` + +--- + +## 架构:ViewModel 与 Repository + +### ViewModel 不暴露可变状态 + +```kotlin +// ❌ 直接暴露 MutableStateFlow,外部可以随意修改 +class MyViewModel : ViewModel() { + val uiState = MutableStateFlow(UiState.Loading) // Mutable! + + fun load() { + viewModelScope.launch { + uiState.value = UiState.Success(repo.fetchData()) + } + } +} + +// ✅ 暴露不可变接口,内部持有可变版本 +class MyViewModel(private val repo: Repository) : ViewModel() { + private val _uiState = MutableStateFlow(UiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + fun load() { + viewModelScope.launch { + _uiState.value = UiState.Success(repo.fetchData()) + } + } +} +``` + +### 业务逻辑下沉到 Repository + +```kotlin +// ❌ ViewModel 中包含数据处理和业务规则逻辑 +class UserViewModel(private val api: Api) : ViewModel() { + private val _users = MutableStateFlow>(emptyList()) + val users: StateFlow> = _users.asStateFlow() + + fun loadUsers() { + viewModelScope.launch { + val raw = api.getUsers() + val filtered = raw.filter { it.isActive } + val sorted = filtered.sortedBy { it.name.lowercase() } + val enriched = sorted.map { user -> + user.copy(displayName = "${user.firstName} ${user.lastName}") + } + _users.value = enriched + } + } +} + +// ✅ ViewModel 只做状态管理,逻辑下沉到 Repository +class UserRepository(private val api: Api) { + suspend fun getActiveUsersSorted(): List { + return api.getUsers() + .filter { it.isActive } + .sortedBy { it.name.lowercase() } + .map { it.copy(displayName = "${it.firstName} ${it.lastName}") } + } +} + +class UserViewModel(private val repo: UserRepository) : ViewModel() { + private val _users = MutableStateFlow>(emptyList()) + val users: StateFlow> = _users.asStateFlow() + + fun loadUsers() { + viewModelScope.launch { + _users.value = repo.getActiveUsersSorted() + } + } +} +``` + +### 单一数据源(Offline-First) + +```kotlin +// ❌ ViewModel 直接从网络获取,无缓存,离线不可用 +class MyViewModel(private val api: Api) : ViewModel() { + fun load() { + viewModelScope.launch { + _uiState.value = UiState.Success(api.fetchData()) + } + } +} + +// ✅ Repository 作为单一数据源,先展示本地缓存再更新网络数据 +class MyRepository( + private val api: Api, + private val dao: DataDao, +) { + val data: Flow> = dao.getAll() + .map { entities -> entities.map { it.toDomain() } } + + suspend fun refresh() { + val remote = api.fetchData() + dao.replaceAll(remote.map { it.toEntity() }) + } +} + +class MyViewModel(private val repo: MyRepository) : ViewModel() { + val uiState = repo.data.map { UiState.Success(it) } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), UiState.Loading) + + fun refresh() { + viewModelScope.launch { repo.refresh() } + } +} +``` + +### Use Case 用于复杂业务逻辑 + +```kotlin +// ❌ Repository 方法名变成动词短语,职责膨胀 +class OrderRepository { + suspend fun validateAndSubmitOrder(order: Order) { } + suspend fun calculateOrderTotalWithDiscounts(order: Order): Money { } + suspend fun checkInventoryAndReserve(items: List) { } +} + +// ✅ 使用 Use Case 封装复杂业务逻辑,Repository 只做数据访问 +class SubmitOrderUseCase( + private val orderRepo: OrderRepository, + private val inventoryRepo: InventoryRepository, + private val paymentRepo: PaymentRepository, +) { + suspend operator fun invoke(order: Order): Result { + val validated = order.validate() + inventoryRepo.reserve(validated.items) + val total = CalculateOrderTotalUseCase().invoke(validated) + return paymentRepo.charge(total).map { confirmation -> + orderRepo.save(validated.copy(status = OrderStatus.CONFIRMED)) + confirmation + } + } +} + +class OrderRepository { + suspend fun save(order: Order) { } + suspend fun getById(id: String): Order? { } + fun observeOrders(): Flow> { } +} +``` + +--- + +## 密封类与状态管理 + +### UI 状态建模:让不可能的状态无法表达 + +```kotlin +// ❌ 用 nullable 组合表示状态,可能产生无效组合 +data class UiState( + val isLoading: Boolean = false, + val data: List? = null, + val error: String? = null, +) +// Invalid: isLoading=true AND error != null +// Invalid: data != null AND error != null + +// ✅ 使用密封类建模,每种状态互斥 +sealed interface UiState { + data object Loading : UiState + data class Success(val data: List) : UiState + data class Error(val message: String, val cause: Throwable? = null) : UiState +} + +class MyViewModel(private val repo: Repository) : ViewModel() { + private val _uiState = MutableStateFlow(UiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() +} + +// ✅ Compose 中 exhaustive when +@Composable +fun MyScreen(viewModel: MyViewModel) { + val state by viewModel.uiState.collectAsStateWithLifecycle() + when (state) { + is UiState.Loading -> CircularProgressIndicator() + is UiState.Success -> DataList((state as UiState.Success).data) + is UiState.Error -> ErrorMessage((state as UiState.Error).message) + } +} +``` + +### 导航事件建模 + +```kotlin +// ❌ 用枚举或字符串表示导航事件,无法携带参数 +sealed class NavEvent { + object ToDetail : NavEvent() + object ToSettings : NavEvent() +} +// How to pass orderId to ToDetail? + +// ✅ 密封类携带类型安全参数 +sealed interface NavEvent { + data class ToDetail(val orderId: String) : NavEvent + data class ToSettings(val tab: SettingsTab) : NavEvent + data class ToProfile(val userId: String, val mode: ProfileMode) : NavEvent +} + +// ✅ 处理导航事件 +navController.handleNavEvent { event -> + when (event) { + is NavEvent.ToDetail -> navController.navigate(DetailRoute(event.orderId)) + is NavEvent.ToSettings -> navController.navigate(SettingsRoute(event.tab)) + is NavEvent.ToProfile -> navController.navigate(ProfileRoute(event.userId, event.mode)) + } +} +``` + +### 网络结果包装 + +```kotlin +// ❌ 用 Result? 或 nullable 表示网络结果,丢失错误信息 +suspend fun fetchUser(id: String): User? { + return try { + api.getUser(id) + } catch (e: Exception) { + null // What went wrong? + } +} + +// ✅ 使用密封类包装网络结果 +sealed interface NetworkResult { + data class Success(val data: T) : NetworkResult + data class Error(val code: Int, val message: String) : NetworkResult + data class Exception(val cause: Throwable) : NetworkResult +} + +suspend fun fetchUser(id: String): NetworkResult { + return try { + val response = api.getUser(id) + if (response.isSuccessful) { + NetworkResult.Success(response.body()!!) + } else { + NetworkResult.Error(response.code(), response.message()) + } + } catch (e: Exception) { + NetworkResult.Exception(e) + } +} + +// ✅ 在 ViewModel 中映射为 UI 状态 +fun loadUser(id: String) { + viewModelScope.launch { + when (val result = repo.fetchUser(id)) { + is NetworkResult.Success -> _uiState.value = UiState.Success(result.data) + is NetworkResult.Error -> _uiState.value = UiState.Error("Server error: ${result.code}") + is NetworkResult.Exception -> _uiState.value = UiState.Error(result.cause.message ?: "Unknown") + } + } +} +``` + +--- + +## Review Checklist + +### 协程 + +- [ ] 不使用 `GlobalScope`,使用 `viewModelScope` / `lifecycleScope` +- [ ] `CancellationException` 被正确重新抛出,未被吞掉 +- [ ] CPU 密集任务使用 `Dispatchers.Default`,I/O 操作使用 `Dispatchers.IO` +- [ ] 长时间运行的 CPU 任务定期调用 `ensureActive()` 或 `yield()` +- [ ] 阻塞调用使用 `runInterruptible` 包装 +- [ ] 不使用 `Job()` 破坏父子协程关系 +- [ ] `NonCancellable` 仅用于 `finally` 块中的清理操作 +- [ ] 不需要返回值用 `launch`,需要并行结果用 `async` + +### Flow + +- [ ] 理解冷流(`flow {}`)与热流(`StateFlow`/`SharedFlow`)的区别 +- [ ] 不在 `flow {}` builder 中使用 `withContext`,使用 `flowOn` 操作符 +- [ ] `collect` 配合 `repeatOnLifecycle` 或 `collectAsStateWithLifecycle` 使用 +- [ ] 异常处理使用 `.catch` 操作符而非 `try-catch` 包裹 `collect` +- [ ] UI 状态用 `StateFlow`,一次性事件用 `SharedFlow` 或 `Channel` + +### Compose + +- [ ] Composable 参数使用稳定类型,避免不必要重组 +- [ ] Lambda 参数使用 `remember` 包装,避免每次重组创建新实例 +- [ ] 派生状态使用 `derivedStateOf` 避免高频率重组 +- [ ] 副作用使用 `LaunchedEffect` / `SideEffect`,不在函数体中直接调用 +- [ ] 状态正确提升(state hoisting),Composable 无状态且可复用 + +### 空安全 + +- [ ] 不滥用非空断言 `!!`,使用安全调用 `?.` 或空合并 `?:` +- [ ] `lateinit` 仅用于生命周期保证初始化的属性 +- [ ] Java 互操作返回值使用可空类型接收 +- [ ] `lazy` 用于首次访问时初始化的昂贵对象 + +### 内存泄漏 + +- [ ] 协程不捕获 `Context` / `View` 等短生命周期对象 +- [ ] 监听器在 `onPause` / `onDestroyView` 中正确注销 +- [ ] 自定义 `CoroutineScope` 提供取消机制 +- [ ] 单例不持有 `Activity` / `Fragment` 引用 + +### 架构 + +- [ ] ViewModel 不暴露 `MutableStateFlow` / `MutableLiveData`,使用不可变接口 +- [ ] 业务逻辑下沉到 Repository / Use Case,ViewModel 只做状态管理 +- [ ] 实现 offline-first:Repository 作为单一数据源 +- [ ] 复杂业务逻辑封装为独立的 Use Case 类 + +### 密封类与状态 + +- [ ] UI 状态使用密封类建模,让不可能的状态无法表达 +- [ ] 导航事件使用密封类携带类型安全参数 +- [ ] 网络请求结果使用密封类包装,不丢失错误信息 +- [ ] `when` 表达式覆盖所有分支(exhaustive check) diff --git a/.claude/skills/code-review-skill/reference/nestjs.md b/.claude/skills/code-review-skill/reference/nestjs.md new file mode 100644 index 0000000..04ba6c0 --- /dev/null +++ b/.claude/skills/code-review-skill/reference/nestjs.md @@ -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; + findById(id: string): Promise; +} + +// 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 { + 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('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 { + 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 零框架依赖 diff --git a/.claude/skills/code-review-skill/reference/performance-review-guide.md b/.claude/skills/code-review-skill/reference/performance-review-guide.md new file mode 100644 index 0000000..58aebee --- /dev/null +++ b/.claude/skills/code-review-skill/reference/performance-review-guide.md @@ -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 图片懒加载 - 延迟关键内容 + + +// ✅ LCP 图片立即加载 + + +// ❌ 未优化的图片格式 + // PNG 文件过大 + +// ✅ 现代图片格式 + 响应式 + + + + Hero + +``` + +**审查要点:** +- [ ] LCP 元素是否设置 `fetchpriority="high"`? +- [ ] 是否使用 WebP/AVIF 格式? +- [ ] 是否有服务端渲染或静态生成? +- [ ] CDN 是否配置正确? + +### FCP 优化检查 + +```html + + + + + + + + +@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 ( +
    + {items.map(item =>
  • {item.name}
  • )} +
+ ); // 10000 条数据 = 10000 个 DOM 节点 +} + +// ✅ 虚拟列表 - 只渲染可见项 +import { FixedSizeList } from 'react-window'; + +function VirtualList({ items }) { + return ( + + {({ index, style }) => ( +
{items[index].name}
+ )} +
+ ); +} +``` + +**大数据审查要点:** +- [ ] 列表超过 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/) diff --git a/.claude/skills/code-review-skill/reference/php.md b/.claude/skills/code-review-skill/reference/php.md new file mode 100644 index 0000000..4eb8bb7 --- /dev/null +++ b/.claude/skills/code-review-skill/reference/php.md @@ -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 + '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 +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 +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 +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 +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 +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 +Hello {$_GET['name']}"; + +// ✅ use htmlspecialchars in an HTML text context +$name = (string) ($_GET['name'] ?? ''); +echo '

Hello ' . htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '

'; +``` + +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 +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 +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 + '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 +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 +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 +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 +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 +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 +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 +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 +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/) diff --git a/.claude/skills/code-review-skill/reference/python.md b/.claude/skills/code-review-skill/reference/python.md new file mode 100644 index 0000000..764db0e --- /dev/null +++ b/.claude/skills/code-review-skill/reference/python.md @@ -0,0 +1,1069 @@ +# Python Code Review Guide + +> Python 代码审查指南,覆盖类型注解、async/await、测试、异常处理、性能优化等核心主题。 + +## 目录 + +- [类型注解](#类型注解) +- [异步编程](#异步编程) +- [异常处理](#异常处理) +- [常见陷阱](#常见陷阱) +- [测试最佳实践](#测试最佳实践) +- [性能优化](#性能优化) +- [代码风格](#代码风格) +- [Review Checklist](#review-checklist) + +--- + +## 类型注解 + +### 基础类型注解 + +```python +# ❌ 没有类型注解,IDE 无法提供帮助 +def process_data(data, count): + return data[:count] + +# ✅ 使用类型注解 +def process_data(data: str, count: int) -> str: + return data[:count] + +# ✅ 复杂类型使用 typing 模块 +from typing import Optional, Union + +def find_user(user_id: int) -> Optional[User]: + """返回用户或 None""" + return db.get(user_id) + +def handle_input(value: Union[str, int]) -> str: + """接受字符串或整数""" + return str(value) +``` + +### 容器类型注解 + +```python +from typing import List, Dict, Set, Tuple, Sequence + +# ❌ 不精确的类型 +def get_names(users: list) -> list: + return [u.name for u in users] + +# ✅ 精确的容器类型(Python 3.9+ 可直接用 list[User]) +def get_names(users: List[User]) -> List[str]: + return [u.name for u in users] + +# ✅ 只读序列用 Sequence(更灵活) +def process_items(items: Sequence[str]) -> int: + return len(items) + +# ✅ 字典类型 +def count_words(text: str) -> Dict[str, int]: + words: Dict[str, int] = {} + for word in text.split(): + words[word] = words.get(word, 0) + 1 + return words + +# ✅ 元组(固定长度和类型) +def get_point() -> Tuple[float, float]: + return (1.0, 2.0) + +# ✅ 可变长度元组 +def get_scores() -> Tuple[int, ...]: + return (90, 85, 92, 88) +``` + +### 泛型与 TypeVar + +```python +from typing import TypeVar, Generic, List, Callable + +T = TypeVar('T') +K = TypeVar('K') +V = TypeVar('V') + +# ✅ 泛型函数 +def first(items: List[T]) -> T | None: + return items[0] if items else None + +# ✅ 有约束的 TypeVar +from typing import Hashable +H = TypeVar('H', bound=Hashable) + +def dedupe(items: List[H]) -> List[H]: + return list(set(items)) + +# ✅ 泛型类 +class Cache(Generic[K, V]): + def __init__(self) -> None: + self._data: Dict[K, V] = {} + + def get(self, key: K) -> V | None: + return self._data.get(key) + + def set(self, key: K, value: V) -> None: + self._data[key] = value +``` + +### Callable 与回调函数 + +```python +from typing import Callable, Awaitable + +# ✅ 函数类型注解 +Handler = Callable[[str, int], bool] + +def register_handler(name: str, handler: Handler) -> None: + handlers[name] = handler + +# ✅ 异步回调 +AsyncHandler = Callable[[str], Awaitable[dict]] + +async def fetch_with_handler( + url: str, + handler: AsyncHandler +) -> dict: + return await handler(url) + +# ✅ 返回函数的函数 +def create_multiplier(factor: int) -> Callable[[int], int]: + def multiplier(x: int) -> int: + return x * factor + return multiplier +``` + +### TypedDict 与结构化数据 + +```python +from typing import TypedDict, Required, NotRequired + +# ✅ 定义字典结构 +class UserDict(TypedDict): + id: int + name: str + email: str + age: NotRequired[int] # Python 3.11+ + +def create_user(data: UserDict) -> User: + return User(**data) + +# ✅ 部分必需字段 +class ConfigDict(TypedDict, total=False): + debug: bool + timeout: int + host: Required[str] # 这个必须有 +``` + +### Protocol 与结构化子类型 + +```python +from typing import Protocol, runtime_checkable + +# ✅ 定义协议(鸭子类型的类型检查) +class Readable(Protocol): + def read(self, size: int = -1) -> bytes: ... + +class Closeable(Protocol): + def close(self) -> None: ... + +# 组合协议 +class ReadableCloseable(Readable, Closeable, Protocol): + pass + +def process_stream(stream: Readable) -> bytes: + return stream.read() + +# ✅ 运行时可检查的协议 +@runtime_checkable +class Drawable(Protocol): + def draw(self) -> None: ... + +def render(obj: object) -> None: + if isinstance(obj, Drawable): # 运行时检查 + obj.draw() +``` + +--- + +## 异步编程 + +### async/await 基础 + +```python +import asyncio + +# ❌ 同步阻塞调用 +def fetch_all_sync(urls: list[str]) -> list[str]: + results = [] + for url in urls: + results.append(requests.get(url).text) # 串行执行 + return results + +# ✅ 异步并发调用 +async def fetch_url(url: str) -> str: + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + return await response.text() + +async def fetch_all(urls: list[str]) -> list[str]: + tasks = [fetch_url(url) for url in urls] + return await asyncio.gather(*tasks) # 并发执行 +``` + +### 异步上下文管理器 + +```python +from contextlib import asynccontextmanager +from typing import AsyncIterator + +# ✅ 异步上下文管理器类 +class AsyncDatabase: + async def __aenter__(self) -> 'AsyncDatabase': + await self.connect() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + await self.disconnect() + +# ✅ 使用装饰器 +@asynccontextmanager +async def get_connection() -> AsyncIterator[Connection]: + conn = await create_connection() + try: + yield conn + finally: + await conn.close() + +async def query_data(): + async with get_connection() as conn: + return await conn.fetch("SELECT * FROM users") +``` + +### 异步迭代器 + +```python +from typing import AsyncIterator + +# ✅ 异步生成器 +async def fetch_pages(url: str) -> AsyncIterator[dict]: + page = 1 + while True: + data = await fetch_page(url, page) + if not data['items']: + break + yield data + page += 1 + +# ✅ 使用异步迭代 +async def process_all_pages(): + async for page in fetch_pages("https://api.example.com"): + await process_page(page) +``` + +### 任务管理与取消 + +```python +import asyncio + +# ❌ 忘记处理取消 +async def bad_worker(): + while True: + await do_work() # 无法正常取消 + +# ✅ 正确处理取消 +async def good_worker(): + try: + while True: + await do_work() + except asyncio.CancelledError: + await cleanup() # 清理资源 + raise # 重新抛出,让调用者知道已取消 + +# ✅ 超时控制 +async def fetch_with_timeout(url: str) -> str: + try: + async with asyncio.timeout(10): # Python 3.11+ + return await fetch_url(url) + except asyncio.TimeoutError: + return "" + +# ✅ 任务组(Python 3.11+) +async def fetch_multiple(): + async with asyncio.TaskGroup() as tg: + task1 = tg.create_task(fetch_url("url1")) + task2 = tg.create_task(fetch_url("url2")) + # 所有任务完成后自动等待,异常会传播 + return task1.result(), task2.result() +``` + +### 同步与异步混合 + +```python +import asyncio +from concurrent.futures import ThreadPoolExecutor + +# ✅ 在异步代码中运行同步函数 +async def run_sync_in_async(): + loop = asyncio.get_event_loop() + # 使用线程池执行阻塞操作 + result = await loop.run_in_executor( + None, # 默认线程池 + blocking_io_function, + arg1, arg2 + ) + return result + +# ✅ 在同步代码中运行异步函数 +def run_async_in_sync(): + return asyncio.run(async_function()) + +# ❌ 不要在异步代码中使用 time.sleep +async def bad_delay(): + time.sleep(1) # 会阻塞整个事件循环! + +# ✅ 使用 asyncio.sleep +async def good_delay(): + await asyncio.sleep(1) +``` + +### 信号量与限流 + +```python +import asyncio + +# ✅ 使用信号量限制并发 +async def fetch_with_limit(urls: list[str], max_concurrent: int = 10): + semaphore = asyncio.Semaphore(max_concurrent) + + async def fetch_one(url: str) -> str: + async with semaphore: + return await fetch_url(url) + + return await asyncio.gather(*[fetch_one(url) for url in urls]) + +# ✅ 使用 asyncio.Queue 实现生产者-消费者 +async def producer_consumer(): + queue: asyncio.Queue[str] = asyncio.Queue(maxsize=100) + + async def producer(): + for item in items: + await queue.put(item) + await queue.put(None) # 结束信号 + + async def consumer(): + while True: + item = await queue.get() + if item is None: + break + await process(item) + queue.task_done() + + await asyncio.gather(producer(), consumer()) +``` + +--- + +## 异常处理 + +### 异常捕获最佳实践 + +```python +# ❌ Catching too broad +try: + result = risky_operation() +except: # Catches everything, even KeyboardInterrupt! + pass + +# ❌ 捕获 Exception 但不处理 +try: + result = risky_operation() +except Exception: + pass # 吞掉所有异常,难以调试 + +# ✅ Catch specific exceptions +try: + result = risky_operation() +except ValueError as e: + logger.error(f"Invalid value: {e}") + raise +except IOError as e: + logger.error(f"IO error: {e}") + return default_value + +# ✅ 多个异常类型 +try: + result = parse_and_process(data) +except (ValueError, TypeError, KeyError) as e: + logger.error(f"Data error: {e}") + raise DataProcessingError(str(e)) from e +``` + +### 异常链 + +```python +# ❌ 丢失原始异常信息 +try: + result = external_api.call() +except APIError as e: + raise RuntimeError("API failed") # 丢失了原因 + +# ✅ 使用 from 保留异常链 +try: + result = external_api.call() +except APIError as e: + raise RuntimeError("API failed") from e + +# ✅ 显式断开异常链(少见情况) +try: + result = external_api.call() +except APIError: + raise RuntimeError("API failed") from None +``` + +### 自定义异常 + +```python +# ✅ 定义业务异常层次结构 +class AppError(Exception): + """应用基础异常""" + pass + +class ValidationError(AppError): + """数据验证错误""" + def __init__(self, field: str, message: str): + self.field = field + self.message = message + super().__init__(f"{field}: {message}") + +class NotFoundError(AppError): + """资源未找到""" + def __init__(self, resource: str, id: str | int): + self.resource = resource + self.id = id + super().__init__(f"{resource} with id {id} not found") + +# 使用 +def get_user(user_id: int) -> User: + user = db.get(user_id) + if not user: + raise NotFoundError("User", user_id) + return user +``` + +### 上下文管理器中的异常 + +```python +from contextlib import contextmanager + +# ✅ 正确处理上下文管理器中的异常 +@contextmanager +def transaction(): + conn = get_connection() + try: + yield conn + conn.commit() + except Exception: + conn.rollback() + raise + finally: + conn.close() + +# ✅ 使用 ExceptionGroup(Python 3.11+) +def process_batch(items: list) -> None: + errors = [] + for item in items: + try: + process(item) + except Exception as e: + errors.append(e) + + if errors: + raise ExceptionGroup("Batch processing failed", errors) +``` + +--- + +## 常见陷阱 + +### 可变默认参数 + +```python +# ❌ Mutable default arguments +def add_item(item, items=[]): # Bug! Shared across calls + items.append(item) + return items + +# 问题演示 +add_item(1) # [1] +add_item(2) # [1, 2] 而不是 [2]! + +# ✅ Use None as default +def add_item(item, items=None): + if items is None: + items = [] + items.append(item) + return items + +# ✅ 或使用 dataclass 的 field +from dataclasses import dataclass, field + +@dataclass +class Container: + items: list = field(default_factory=list) +``` + +### 可变类属性 + +```python +# ❌ Using mutable class attributes +class User: + permissions = [] # Shared across all instances! + +# 问题演示 +u1 = User() +u2 = User() +u1.permissions.append("admin") +print(u2.permissions) # ["admin"] - 被意外共享! + +# ✅ Initialize in __init__ +class User: + def __init__(self): + self.permissions = [] + +# ✅ 使用 dataclass +@dataclass +class User: + permissions: list = field(default_factory=list) +``` + +### 循环中的闭包 + +```python +# ❌ 闭包捕获循环变量 +funcs = [] +for i in range(3): + funcs.append(lambda: i) + +print([f() for f in funcs]) # [2, 2, 2] 而不是 [0, 1, 2]! + +# ✅ 使用默认参数捕获值 +funcs = [] +for i in range(3): + funcs.append(lambda i=i: i) + +print([f() for f in funcs]) # [0, 1, 2] + +# ✅ 使用 functools.partial +from functools import partial + +funcs = [partial(lambda x: x, i) for i in range(3)] +``` + +### is vs == + +```python +# ❌ 用 is 比较值 +if x is 1000: # 可能不工作! + pass + +# Python 会缓存小整数 (-5 到 256) +a = 256 +b = 256 +a is b # True + +a = 257 +b = 257 +a is b # False! + +# ✅ 用 == 比较值 +if x == 1000: + pass + +# ✅ is 只用于 None 和单例 +if x is None: + pass + +if x is True: # 严格检查布尔值 + pass +``` + +### 字符串拼接性能 + +```python +# ❌ 循环中拼接字符串 +result = "" +for item in large_list: + result += str(item) # O(n²) 复杂度 + +# ✅ 使用 join +result = "".join(str(item) for item in large_list) # O(n) + +# ✅ 使用 StringIO 构建大字符串 +from io import StringIO + +buffer = StringIO() +for item in large_list: + buffer.write(str(item)) +result = buffer.getvalue() +``` + +--- + +## 测试最佳实践 + +### pytest 基础 + +```python +import pytest + +# ✅ 清晰的测试命名 +def test_user_creation_with_valid_email(): + user = User(email="test@example.com") + assert user.email == "test@example.com" + +def test_user_creation_with_invalid_email_raises_error(): + with pytest.raises(ValidationError): + User(email="invalid") + +# ✅ 使用参数化测试 +@pytest.mark.parametrize("input,expected", [ + ("hello", "HELLO"), + ("World", "WORLD"), + ("", ""), + ("123", "123"), +]) +def test_uppercase(input: str, expected: str): + assert input.upper() == expected + +# ✅ 测试异常 +def test_division_by_zero(): + with pytest.raises(ZeroDivisionError) as exc_info: + 1 / 0 + assert "division by zero" in str(exc_info.value) +``` + +### Fixtures + +```python +import pytest +from typing import Generator + +# ✅ 基础 fixture +@pytest.fixture +def user() -> User: + return User(name="Test User", email="test@example.com") + +def test_user_name(user: User): + assert user.name == "Test User" + +# ✅ 带清理的 fixture +@pytest.fixture +def database() -> Generator[Database, None, None]: + db = Database() + db.connect() + yield db + db.disconnect() # 测试后清理 + +# ✅ 异步 fixture +@pytest.fixture +async def async_client() -> AsyncGenerator[AsyncClient, None]: + async with AsyncClient() as client: + yield client + +# ✅ 共享 fixture(conftest.py) +# conftest.py +@pytest.fixture(scope="session") +def app(): + """整个测试会话共享的 app 实例""" + return create_app() + +@pytest.fixture(scope="module") +def db(app): + """每个测试模块共享的数据库连接""" + return app.db +``` + +### Mock 与 Patch + +```python +from unittest.mock import Mock, patch, AsyncMock + +# ✅ Mock 外部依赖 +def test_send_email(): + mock_client = Mock() + mock_client.send.return_value = True + + service = EmailService(client=mock_client) + result = service.send_welcome_email("user@example.com") + + assert result is True + mock_client.send.assert_called_once_with( + to="user@example.com", + subject="Welcome!", + body=ANY, + ) + +# ✅ Patch 模块级函数 +@patch("myapp.services.external_api.call") +def test_with_patched_api(mock_call): + mock_call.return_value = {"status": "ok"} + + result = process_data() + + assert result["status"] == "ok" + +# ✅ 异步 Mock +async def test_async_function(): + mock_fetch = AsyncMock(return_value={"data": "test"}) + + with patch("myapp.client.fetch", mock_fetch): + result = await get_data() + + assert result == {"data": "test"} +``` + +### 测试组织 + +```python +# ✅ 使用类组织相关测试 +class TestUserAuthentication: + """用户认证相关测试""" + + def test_login_with_valid_credentials(self, user): + assert authenticate(user.email, "password") is True + + def test_login_with_invalid_password(self, user): + assert authenticate(user.email, "wrong") is False + + def test_login_locks_after_failed_attempts(self, user): + for _ in range(5): + authenticate(user.email, "wrong") + assert user.is_locked is True + +# ✅ 使用 mark 标记测试 +@pytest.mark.slow +def test_large_data_processing(): + pass + +@pytest.mark.integration +def test_database_connection(): + pass + +# 运行特定标记的测试:pytest -m "not slow" +``` + +### 覆盖率与质量 + +```python +# pytest.ini 或 pyproject.toml +[tool.pytest.ini_options] +addopts = "--cov=myapp --cov-report=term-missing --cov-fail-under=80" +testpaths = ["tests"] + +# ✅ 测试边界情况 +def test_empty_input(): + assert process([]) == [] + +def test_none_input(): + with pytest.raises(TypeError): + process(None) + +def test_large_input(): + large_data = list(range(100000)) + result = process(large_data) + assert len(result) == 100000 +``` + +--- + +## 性能优化 + +### 数据结构选择 + +```python +# ❌ 列表查找 O(n) +if item in large_list: # 慢 + pass + +# ✅ 集合查找 O(1) +large_set = set(large_list) +if item in large_set: # 快 + pass + +# ✅ 使用 collections 模块 +from collections import Counter, defaultdict, deque + +# 计数 +word_counts = Counter(words) +most_common = word_counts.most_common(10) + +# 默认字典 +graph = defaultdict(list) +graph[node].append(neighbor) + +# 双端队列(两端操作 O(1)) +queue = deque() +queue.appendleft(item) # O(1) vs list.insert(0, item) O(n) +``` + +### 生成器与迭代器 + +```python +# ❌ 一次性加载所有数据 +def get_all_users(): + return [User(row) for row in db.fetch_all()] # 内存占用大 + +# ✅ 使用生成器 +def get_all_users(): + for row in db.fetch_all(): + yield User(row) # 懒加载 + +# ✅ 生成器表达式 +sum_of_squares = sum(x**2 for x in range(1000000)) # 不创建列表 + +# ✅ itertools 模块 +from itertools import islice, chain, groupby + +# 只取前 10 个 +first_10 = list(islice(infinite_generator(), 10)) + +# 链接多个迭代器 +all_items = chain(list1, list2, list3) + +# 分组 +for key, group in groupby(sorted(items, key=get_key), key=get_key): + process_group(key, list(group)) +``` + +### 缓存 + +```python +from functools import lru_cache, cache + +# ✅ LRU 缓存 +@lru_cache(maxsize=128) +def expensive_computation(n: int) -> int: + return sum(i**2 for i in range(n)) + +# ✅ 无限缓存(Python 3.9+) +@cache +def fibonacci(n: int) -> int: + if n < 2: + return n + return fibonacci(n - 1) + fibonacci(n - 2) + +# ✅ 手动缓存(需要更多控制时) +class DataService: + def __init__(self): + self._cache: dict[str, Any] = {} + self._cache_ttl: dict[str, float] = {} + + def get_data(self, key: str) -> Any: + if key in self._cache: + if time.time() < self._cache_ttl[key]: + return self._cache[key] + + data = self._fetch_data(key) + self._cache[key] = data + self._cache_ttl[key] = time.time() + 300 # 5 分钟 + return data +``` + +### 并行处理 + +```python +from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor + +# ✅ IO 密集型使用线程池 +def fetch_all_urls(urls: list[str]) -> list[str]: + with ThreadPoolExecutor(max_workers=10) as executor: + results = list(executor.map(fetch_url, urls)) + return results + +# ✅ CPU 密集型使用进程池 +def process_large_dataset(data: list) -> list: + with ProcessPoolExecutor() as executor: + results = list(executor.map(heavy_computation, data)) + return results + +# ✅ 使用 as_completed 获取最先完成的结果 +from concurrent.futures import as_completed + +with ThreadPoolExecutor() as executor: + futures = {executor.submit(fetch, url): url for url in urls} + for future in as_completed(futures): + url = futures[future] + try: + result = future.result() + except Exception as e: + print(f"{url} failed: {e}") +``` + +--- + +## 代码风格 + +### PEP 8 要点 + +```python +# ✅ 命名规范 +class MyClass: # 类名 PascalCase + MAX_SIZE = 100 # 常量 UPPER_SNAKE_CASE + + def method_name(self): # 方法 snake_case + local_var = 1 # 变量 snake_case + +# ✅ 导入顺序 +# 1. 标准库 +import os +import sys +from typing import Optional + +# 2. 第三方库 +import numpy as np +import pandas as pd + +# 3. 本地模块 +from myapp import config +from myapp.utils import helper + +# ✅ 行长度限制(79 或 88 字符) +# 长表达式的换行 +result = ( + long_function_name(arg1, arg2, arg3) + + another_long_function(arg4, arg5) +) + +# ✅ 空行规范 +class MyClass: + """类文档字符串""" + + def method_one(self): + pass + + def method_two(self): # 方法间一个空行 + pass + + +def top_level_function(): # 顶层定义间两个空行 + pass +``` + +### 文档字符串 + +```python +# ✅ Google 风格文档字符串 +def calculate_area(width: float, height: float) -> float: + """计算矩形面积。 + + Args: + width: 矩形的宽度(必须为正数)。 + height: 矩形的高度(必须为正数)。 + + Returns: + 矩形的面积。 + + Raises: + ValueError: 如果 width 或 height 为负数。 + + Example: + >>> calculate_area(3, 4) + 12.0 + """ + if width < 0 or height < 0: + raise ValueError("Dimensions must be positive") + return width * height + +# ✅ 类文档字符串 +class DataProcessor: + """处理和转换数据的工具类。 + + Attributes: + source: 数据来源路径。 + format: 输出格式('json' 或 'csv')。 + + Example: + >>> processor = DataProcessor("data.csv") + >>> processor.process() + """ +``` + +### 现代 Python 特性 + +```python +# ✅ f-string(Python 3.6+) +name = "World" +print(f"Hello, {name}!") + +# 带表达式 +print(f"Result: {1 + 2 = }") # "Result: 1 + 2 = 3" + +# ✅ 海象运算符(Python 3.8+) +if (n := len(items)) > 10: + print(f"List has {n} items") + +# ✅ 位置参数分隔符(Python 3.8+) +def greet(name, /, greeting="Hello", *, punctuation="!"): + """name 只能位置传参,punctuation 只能关键字传参""" + return f"{greeting}, {name}{punctuation}" + +# ✅ 模式匹配(Python 3.10+) +def handle_response(response: dict): + match response: + case {"status": "ok", "data": data}: + return process_data(data) + case {"status": "error", "message": msg}: + raise APIError(msg) + case _: + raise ValueError("Unknown response format") +``` + +--- + +## Review Checklist + +### 类型安全 +- [ ] 函数有类型注解(参数和返回值) +- [ ] 使用 `Optional` 明确可能为 None +- [ ] 泛型类型正确使用 +- [ ] mypy 检查通过(无错误) +- [ ] 避免使用 `Any`,必要时添加注释说明 + +### 异步代码 +- [ ] async/await 正确配对使用 +- [ ] 没有在异步代码中使用阻塞调用 +- [ ] 正确处理 `CancelledError` +- [ ] 使用 `asyncio.gather` 或 `TaskGroup` 并发执行 +- [ ] 资源正确清理(async context manager) + +### 异常处理 +- [ ] 捕获特定异常类型,不使用裸 `except:` +- [ ] 异常链使用 `from` 保留原因 +- [ ] 自定义异常继承自合适的基类 +- [ ] 异常信息有意义,便于调试 + +### 数据结构 +- [ ] 没有使用可变默认参数(list、dict、set) +- [ ] 类属性不是可变对象 +- [ ] 选择正确的数据结构(set vs list 查找) +- [ ] 大数据集使用生成器而非列表 + +### 测试 +- [ ] 测试覆盖率达标(建议 ≥80%) +- [ ] 测试命名清晰描述测试场景 +- [ ] 边界情况有测试覆盖 +- [ ] Mock 正确隔离外部依赖 +- [ ] 异步代码有对应的异步测试 + +### 代码风格 +- [ ] 遵循 PEP 8 风格指南 +- [ ] 函数和类有 docstring +- [ ] 导入顺序正确(标准库、第三方、本地) +- [ ] 命名一致且有意义 +- [ ] 使用现代 Python 特性(f-string、walrus operator 等) + +### 性能 +- [ ] 避免循环中重复创建对象 +- [ ] 字符串拼接使用 join +- [ ] 合理使用缓存(@lru_cache) +- [ ] IO/CPU 密集型使用合适的并行方式 diff --git a/.claude/skills/code-review-skill/reference/qt.md b/.claude/skills/code-review-skill/reference/qt.md new file mode 100644 index 0000000..c7b723d --- /dev/null +++ b/.claude/skills/code-review-skill/reference/qt.md @@ -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 obj(new MyObject()); + +// ✅ Safe pointer to prevent dangling pointers +QPointer 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 list) { + list[0] = 1; +} + +// ✅ Read-only reference +void process(const QVector& 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` 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`? \ No newline at end of file diff --git a/.claude/skills/code-review-skill/reference/react.md b/.claude/skills/code-review-skill/reference/react.md new file mode 100644 index 0000000..1cce2fc --- /dev/null +++ b/.claude/skills/code-review-skill/reference/react.md @@ -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
...
; +} + +// ✅ Hooks 必须在组件顶层调用 +function GoodComponent({ isLoggedIn }) { + const [user, setUser] = useState(null); + if (!isLoggedIn) return ; + return
{user?.name}
; +} +``` + +--- + +## 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 ; +} + +// ✅ 直接在渲染时计算,或用 useMemo +function GoodDerived({ items }) { + const filteredItems = useMemo( + () => items.filter(i => i.active), + [items] + ); + return ; +} + +// ❌ 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
{items.length}
; +}); + +function Parent({ rawItems }) { + const items = useMemo(() => processItems(rawItems), [rawItems]); + const handleClick = useCallback(() => { + console.log(items.length); + }, [items]); + return ; +} +``` + +--- + +## 组件设计 + +```tsx +// ❌ 在组件内定义组件 — 每次渲染都创建新组件 +function BadParent() { + function ChildComponent() { // 每次渲染都是新函数! + return
child
; + } + return ; +} + +// ✅ 组件定义在外部 +function ChildComponent() { + return
child
; +} +function GoodParent() { + return ; +} + +// ❌ Props 总是新对象引用 +function BadProps() { + return ( + {}} // 每次渲染新函数 + /> + ); +} + +// ✅ 稳定的引用 +const style = { color: 'red' }; +function GoodProps() { + const handleClick = useCallback(() => {}, []); + return ; +} +``` + +--- + +## Error Boundaries & Suspense + +```tsx +// ❌ 没有错误边界 +function BadApp() { + return ( + }> + {/* 错误会导致整个应用崩溃 */} + + ); +} + +// ✅ Error Boundary 包裹 Suspense +function GoodApp() { + return ( + }> + }> + + + + ); +} +``` + +--- + +## 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 ; +} + +// ✅ 交互逻辑提取到 Client Component +// app/counter.tsx +'use client'; +function Counter() { + const [count, setCount] = useState(0); + return ; +} + +// app/page.tsx (Server Component) +async function GoodServerComponent() { + const data = await fetchData(); // 可以直接 await + return ( +
+

{data.title}

+ {/* 客户端组件 */} +
+ ); +} + +// ❌ '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(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 ( + + + + {state.error &&

{state.error}

} + + ); +} +``` + +### useFormStatus + +```tsx +// ❌ Props 透传表单状态 +function BadSubmitButton({ isSubmitting }) { + return ; +} + +// ✅ useFormStatus 访问父
状态(无需 props) +import { useFormStatus } from 'react-dom'; + +function SubmitButton() { + const { pending, data, method, action } = useFormStatus(); + // 注意:必须在 内部的子组件中使用 + return ( + + ); +} + +// ❌ useFormStatus 在 form 同级组件中调用——不工作 +function BadForm() { + const { pending } = useFormStatus(); // 这里无法获取状态! + return ( + + +
+ ); +} + +// ✅ useFormStatus 必须在 form 的子组件中 +function GoodForm() { + return ( +
+ {/* useFormStatus 在这里面调用 */} + + ); +} +``` + +### 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 ; +} +``` + +### 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 ( +
+ + + + ); +} +``` + +--- + +## 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 ; + return ; +} + +// ✅ Suspense 声明式加载状态 +function NewComponent() { + return ( + }> + {/* 内部使用 use() 或支持 Suspense 的数据获取 */} + + ); +} +``` + +### 多个独立 Suspense 边界 + +```tsx +// ❌ 单一边界——所有内容一起加载 +function BadLayout() { + return ( + }> +
+ {/* 慢 */} + {/* 快 */} + + ); +} + +// ✅ 独立边界——各部分独立流式传输 +function GoodLayout() { + return ( + <> +
{/* 立即显示 */} +
+ }> + {/* 独立加载 */} + + }> + {/* 独立加载 */} + +
+ + ); +} +``` + +### Next.js 15 Streaming + +```tsx +// app/page.tsx - 自动 Streaming +export default async function Page() { + // 这个 await 不会阻塞整个页面 + const data = await fetchSlowData(); + return
{data}
; +} + +// app/loading.tsx - 自动 Suspense 边界 +export default function Loading() { + return ; +} +``` + +### use() Hook (React 19) + +```tsx +// ✅ 在组件中读取 Promise +import { use } from 'react'; + +function Comments({ commentsPromise }) { + const comments = use(commentsPromise); // 自动触发 Suspense + return ( +
    + {comments.map(c =>
  • {c.text}
  • )} +
+ ); +} + +// 父组件创建 Promise,子组件消费 +function Post({ postId }) { + const commentsPromise = fetchComments(postId); // 不 await + return ( +
+ + }> + + +
+ ); +} +``` + +--- + +## 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 ; +} + +function Parent({ userId }) { + if (!userId) return ; + return ( + }> + + + ); +} +``` + +#### 错误处理差异 + +```tsx +// ❌ useSuspenseQuery 没有 error 属性 +function BadErrorHandling() { + const { data, error } = useSuspenseQuery({...}); + if (error) return ; // error 总是 null! +} + +// ✅ 使用 Error Boundary 处理错误 +function GoodErrorHandling() { + return ( + }> + }> + + + + ); +} + +function DataComponent() { + // 错误会抛出到 Error Boundary + const { data } = useSuspenseQuery({ + queryKey: ['data'], + queryFn: fetchData, + }); + return ; +} +``` + +#### 何时选择 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 ; +} +``` + +### 乐观更新 (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 ( +
    + {todos?.map(todo => )} + {/* 乐观显示正在添加的 todo */} + {isPending && } +
+ ); +} +``` + +### v5 状态字段变化 + +```tsx +// v4: isLoading 表示首次加载或后续获取 +// v5: isPending 表示没有数据,isLoading = isPending && isFetching + +const { data, isPending, isFetching, isLoading } = useQuery({...}); + +// isPending: 缓存中没有数据(首次加载) +// isFetching: 正在请求中(包括后台刷新) +// isLoading: isPending && isFetching(首次加载中) + +// ❌ v4 代码直接迁移 +if (isLoading) return ; // v5 中行为可能不同 + +// ✅ 明确意图 +if (isPending) return ; // 没有数据时显示加载 +// 或 +if (isLoading) return ; // 首次加载中 +``` + +--- + +## 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 查询 +- [ ] 测试行为而非实现细节 diff --git a/.claude/skills/code-review-skill/reference/rust.md b/.claude/skills/code-review-skill/reference/rust.md new file mode 100644 index 0000000..048e7f3 --- /dev/null +++ b/.claude/skills/code-review-skill/reference/rust.md @@ -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> 的使用 + +```rust +// ❌ Arc> 可能隐藏不必要的共享状态 +struct BadService { + cache: Arc>>, // 真的需要共享? +} + +// ✅ 考虑是否需要共享,或者设计可以避免 +struct GoodService { + cache: HashMap, // 单一所有者 +} + +// ✅ 如果确实需要并发访问,考虑更好的数据结构 +use dashmap::DashMap; + +struct ConcurrentService { + cache: DashMap, // 更细粒度的锁 +} +``` + +### 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: 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: 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 { + 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 { + // 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 { + 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 { + 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) { + let guard = mutex.lock().unwrap(); + async_operation().await; // 持锁等待! + process(&guard); +} + +// ✅ 方案1:最小化锁范围 +async fn good_lock_scoped(mutex: &std::sync::Mutex) { + 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) { + 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; // 隐式 Box +} + +// ✅ Rust 1.75+:原生 async trait 方法 +trait Repository { + async fn find(&self, id: i64) -> Option; + + // 返回具体 Future 类型以避免 allocation + fn find_many(&self, ids: &[i64]) -> impl Future> + Send; +} + +// ✅ 对于需要 dyn 的场景 +trait DynRepository: Send + Sync { + fn find(&self, id: i64) -> Pin> + 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 { + 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 { + 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) { + 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>, +} + +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 { ... } + +// ✅ 库用 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 { ... } +``` + +### 保留错误上下文 + +```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 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::>() // 不必要! + .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 { + // 直接实现 + } +} +``` + +### Trait 对象 vs 泛型 + +```rust +// ❌ 不必要的 trait 对象(动态分发) +fn bad_process(handler: &dyn Handler) { + handler.handle(); // 虚表调用 +} + +// ✅ 使用泛型(静态分发,可内联) +fn good_process(handler: &H) { + handler.handle(); // 可能被内联 +} + +// ✅ trait 对象适用场景:异构集合 +fn store_handlers(handlers: Vec>) { + // 需要存储不同类型的 handlers +} + +// ✅ 使用 impl Trait 返回类型 +fn create_handler() -> impl Handler { + ConcreteHandler::new() +} +``` + +--- + +## Rust Review Checklist + +### 编译器不能捕获的问题 + +**业务逻辑正确性** +- [ ] 边界条件处理正确 +- [ ] 状态机转换完整 +- [ ] 并发场景下的竞态条件 + +**API 设计** +- [ ] 公共 API 难以误用 +- [ ] 类型签名清晰表达意图 +- [ ] 错误类型粒度合适 + +### 所有权与借用 + +- [ ] clone() 是有意为之,文档说明了原因 +- [ ] Arc> 真的需要共享状态吗? +- [ ] 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 选择合理 +- [ ] 热路径避免分配 +- [ ] 考虑使用 Cow 减少克隆 + +### 代码质量 + +- [ ] cargo clippy 零警告 +- [ ] cargo fmt 格式化 +- [ ] 文档注释完整 +- [ ] 测试覆盖边界条件 +- [ ] 公共 API 有文档示例 diff --git a/.claude/skills/code-review-skill/reference/security-review-guide.md b/.claude/skills/code-review-skill/reference/security-review-guide.md new file mode 100644 index 0000000..d79fcde --- /dev/null +++ b/.claude/skills/code-review-skill/reference/security-review-guide.md @@ -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
{userInput}
; // Safe +return
; // 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 | diff --git a/.claude/skills/code-review-skill/reference/svelte.md b/.claude/skills/code-review-skill/reference/svelte.md new file mode 100644 index 0000000..be2d4fb --- /dev/null +++ b/.claude/skills/code-review-skill/reference/svelte.md @@ -0,0 +1,1060 @@ +# Svelte / SvelteKit Code Review Guide + +Svelte 5 / SvelteKit 审查重点:Runes 响应式系统、Server/Client 边界、Form Actions、Store 迁移、以及安全性。 + +## 目录 + +- [Runes: $state / $derived / $effect](#runes-state--derived--effect) +- [Load 函数(Server vs Client)](#load-函数server-vs-client) +- [Form Actions](#form-actions) +- [Store 迁移(→ $state)](#store-迁移) +- [SSR vs CSR 边界](#ssr-vs-csr-边界) +- [响应式语句迁移($: → Runes)](#响应式语句迁移) +- [性能优化](#性能优化) +- [安全审查](#安全审查) +- [Review Checklist](#review-checklist) + +--- + +## Runes: $state / $derived / $effect + +### $state 基础用法 + +```svelte + + + + + +``` + +### $state.raw 与大型对象 + +```svelte + + + + + +``` + +### $state.snapshot 用于外部库 + +```svelte + + + + + +``` + +### 解构 $state 丢失响应性 + +```svelte + + +

{count}

+ + + +

{state.count}

+ + + +``` + +--- + +### $derived 正确用法 + +```svelte + + + + + +``` + +### $derived 中不应有副作用 + +```svelte + + + + + +``` + +--- + +### $effect 正确用法 + +#### $effect vs $derived + +```svelte + + + + + +``` + +#### 无限循环 + +```svelte + + + + + +``` + +#### 清理函数 + +```svelte + + + + + + + + +``` + +#### async $effect 的追踪陷阱 + +```svelte + + + + + +``` + +#### untrack 排除依赖 + +```svelte + + + + + +``` + +--- + +## Load 函数(Server vs Client) + +### +page.server.js vs +page.js + +```typescript +// ❌ 在 +page.js 中访问数据库或 secrets +// src/routes/admin/+page.js +export async function load({ fetch }) { + // universal load runs on both server and client + const data = await db.query('SELECT * FROM users'); // db not available in browser! + return { users: data }; +} + +// ✅ 服务端逻辑放在 +page.server.js +// src/routes/admin/+page.server.js +import { db } from '$lib/server/db'; + +export async function load() { + const users = await db.query('SELECT * FROM users'); + return { users }; +} +``` + +```typescript +// ✅ +page.js 用于客户端也可用的数据(如 fetch 聚合) +// src/routes/dashboard/+page.js +export async function load({ fetch, parent }) { + const [analytics, notifications] = await Promise.all([ + fetch('/api/analytics').then(r => r.json()), + fetch('/api/notifications').then(r => r.json()) + ]); + return { analytics, notifications }; +} +``` + +### await parent() 瀑布流 + +```typescript +// ❌ 顺序 await parent → 瀑布流 +// src/routes/blog/[slug]/+page.js +export async function load({ parent, fetch }) { + const parentData = await parent(); // wait for parent + const post = await fetch(`/api/posts/${parentData.blogId}`); + return { post }; +} + +// ✅ 尽可能并行,避免不必要的 parent await +// src/routes/blog/[slug]/+page.js +export async function load({ parent, fetch }) { + // only await parent if you truly need its data + const post = await fetch('/api/posts/slug'); + return { post }; +} + +// ✅ 如果确实需要 parent 数据,无法避免瀑布流,但要明确注释 +// src/routes/blog/[slug]/+page.js +export async function load({ parent, fetch }) { + const { blogId } = await parent(); // required: need blogId for post URL + const post = await fetch(`/api/posts/${blogId}`); + return { post }; +} +``` + +### 不可序列化的返回值 + +```typescript +// ❌ 从 server load 返回不可序列化的值 +// src/routes/api/+page.server.js +export async function load() { + return { + stream: fs.createReadStream('data.csv'), // not serializable! + callback: () => console.log('hi'), // functions not serializable! + date: new Date(), // OK — devalue serializes Date/Map/Set fine + }; +} + +// ✅ 只返回可序列化的数据 +// src/routes/api/+page.server.js +export async function load() { + return { + data: await readFile('data.csv', 'utf-8'), + timestamp: Date.now(), + }; +} +``` + +--- + +## Form Actions + +### 使用 POST 处理副作用 + +```svelte + + + + + +``` + +```typescript +// src/routes/users/+page.server.js +import { fail, redirect } from '@sveltejs/kit'; + +export const actions = { + delete: async ({ request, locals }) => { + const formData = await request.formData(); + const id = formData.get('id'); + + if (!id) return fail(400, { message: 'Missing id' }); + + await locals.db.users.delete(id); + throw redirect(303, '/users'); + } +}; +``` + +```svelte + + + +
+ + +
+``` + +### fail() 中不暴露敏感信息 + +```typescript +// ❌ fail() 中返回敏感信息 +// src/routes/login/+page.server.js +export const actions = { + default: async ({ request, locals }) => { + const formData = await request.formData(); + const user = await locals.db.users.findByEmail(formData.get('email')); + + return fail(401, { + password: formData.get('password'), // ❌ exposes password in page data! + hint: user.passwordHint, // ❌ leaks internal data! + }); + } +}; + +// ✅ 只返回安全的错误信息 +export const actions = { + default: async ({ request }) => { + const formData = await request.formData(); + const email = formData.get('email'); + + return fail(401, { + email, // ✅ safe to echo back + incorrect: true, // ✅ generic error flag + }); + } +}; +``` + +### use:enhance 渐进增强 + +```svelte + +
+ + +
+ + + + +
{ + return ({ update }) => { + update({ reset: false }); // customize behavior + }; +}}> + + +
+ + +
{ + submitting = true; + return ({ update }) => { + update(); + submitting = false; + }; + }} +> + +
+``` + +--- + +## Store 迁移(→ $state) + +### writable/readable → $state + +```typescript +// ❌ Legacy store pattern (Svelte 4) +// src/lib/stores/user.js +import { writable, derived } from 'svelte/store'; + +export const user = writable(null); +export const isLoggedIn = derived(user, $user => !!$user); + +// usage with $ prefix +// $user = { name: 'John' }; + +// ✅ Svelte 5: shared state in .svelte.js files +// src/lib/stores/user.svelte.js +let currentUser = $state(null); + +export function getUser() { + return currentUser; +} + +export function setUser(user: User | null) { + currentUser = user; +} + +export function isLoggedIn() { + return currentUser !== null; +} +``` + +### $ 前缀 store 语法是遗留语法 + +```svelte + + +

{$count}

+ + + +

{count}

+ + + +

{counter.value}

+``` + +### .svelte.js / .svelte.ts 扩展名 + +```typescript +// ❌ 在普通 .js 文件中使用 runes → 编译错误 +// src/lib/utils.js +let state = $state(0); // runes only work in .svelte.js files! + +// ✅ 使用 .svelte.js 扩展名 +// src/lib/utils.svelte.js +let state = $state(0); + +export function getState() { + return state; +} + +export function setState(val: number) { + state = val; +} +``` + +--- + +## SSR vs CSR 边界 + +### ssr=false SPA 模式 + +```typescript +// ❌ 在根 layout 中禁用 SSR → 全部变成 CSR +// src/routes/+layout.js +export const ssr = false; // entire app becomes SPA + +// ✅ 只在需要的页面禁用 SSR +// src/routes/admin/dashboard/+page.js +export const ssr = false; // only this page skips SSR + +// ✅ 更好的做法:按路由配置 +// src/routes/editor/+page.js +export const ssr = false; // editor needs browser APIs, skip SSR +``` + +### 浏览器全局变量在 SSR 中 + +```svelte + + + + + +``` + +### prerender 与 actions 冲突 + +```typescript +// ❌ prerender 页面中定义 actions → 编译错误 +// src/routes/contact/+page.server.js +export const prerender = true; + +export const actions = { + // Error: prerendered pages cannot have server-side form actions + default: async ({ request }) => { /* ... */ } +}; + +// ✅ prerender 页面不使用 server actions +// src/routes/about/+page.server.js +export const prerender = true; +// no actions — static page + +// ✅ 需要 actions 的页面不 prerender +// src/routes/contact/+page.server.js +export const actions = { + default: async ({ request }) => { + // handle form submission + } +}; +``` + +--- + +## 响应式语句迁移 + +### $: → $derived / $effect + +```svelte + + + + + +``` + +### export let → $props() + +```svelte + + + + + +``` + +### on:click → onclick + +```svelte + + + + + + + +``` + +### createEventDispatcher → 回调 props + +```svelte + + + + + + + + removeItem(e.id)} /> +``` + +### slot → @render children() + +```svelte + + +
+ +
+ + + + +
+ {@render children()} +
+ + + + +
+
{@render header?.()}
+
{@render children()}
+
{@render footer?.()}
+
+ + + + {#snippet header()}

Title

{/snippet} +

Body content

+ {#snippet footer()}

Footer

{/snippet} +
+``` + +### beforeUpdate / afterUpdate → $effect.pre + +```svelte + + + + + +``` + +--- + +## 性能优化 + +### $state.raw 用于大型不可变数据 + +```svelte + + + + + +``` + +### Keyed {#each} + +```svelte + +{#each items as item} +
{item.name}
+{/each} + + +{#each items as item (item.id)} +
{item.name}
+{/each} + + +{#each items as item (`${item.category}-${item.id}`)} +
{item.name}
+{/each} +``` + +### Streaming 与 load 中的 Promise + +```typescript +// ❌ 串行等待所有数据 → 页面阻塞 +// src/routes/+page.server.js +export async function load({ params }) { + const posts = await getPosts(); // slow + const comments = await getComments(); // slow + const tags = await getTags(); // slow + return { posts, comments, tags }; +} + +// ✅ 并行加载独立数据 +export async function load({ params }) { + return { + posts: getPosts(), // return promises directly for streaming + comments: getComments(), + tags: getTags(), + }; +} +``` + +```svelte + +{#await data.posts} +

Loading posts...

+{:then posts} +
    + {#each posts as post (post.id)} +
  • {post.title}
  • + {/each} +
+{:catch error} +

Failed to load posts: {error.message}

+{/await} +``` + +--- + +## 安全审查 + +### 不暴露私有环境变量 + +```typescript +// ❌ 在 universal load 中暴露服务端 secrets +// src/routes/admin/+page.js (universal — runs on client too!) +export async function load() { + return { + apiKey: process.env.SECRET_API_KEY, // exposed to client bundle! + dbUrl: import.meta.env.DATABASE_URL, // leaks to browser! + }; +} + +// ✅ 私有环境变量只在 server load 中使用 +// src/routes/admin/+page.server.js (server-only) +export async function load({ locals }) { + // secrets stay on server + const data = await fetch(process.env.SECRET_API_KEY + '/admin'); + return { data }; // only derived data is sent to client +} + +// ✅ 公开变量使用 PUBLIC_ 前缀 +// .env +// PUBLIC_API_URL=https://api.example.com +// SECRET_API_KEY=xxx (no PUBLIC_ prefix = server-only) +``` + +### $lib/server/ 服务端隔离 + +```typescript +// ❌ 服务端代码放在可被客户端导入的位置 +// src/lib/db.js +import { SECRET_DB_URL } from '$env/static/private'; +// any client component importing this gets the secret! + +// ✅ 放在 $lib/server/ 目录 → 客户端导入会编译报错 +// src/lib/server/db.js +import { SECRET_DB_URL } from '$env/static/private'; + +export async function query(sql: string) { + // safe: client cannot import from $lib/server/ +} + +// usage in server files only +// src/routes/api/users/+server.js +import { query } from '$lib/server/db'; +``` + +### CSRF 内建防护 + +```typescript +// ✅ SvelteKit 内建 CSRF 防护 +// Origin header is checked automatically for POST/PUT/DELETE/PATCH +// No additional CSRF tokens needed for form actions + +// ❌ 不要禁用 CSRF 检查(除非有充分理由) +// src/hooks.server.js +export const handle = sequence( + // do NOT do this without understanding the implications + // ({ event, resolve }) => resolve(event, { filterSerializedResponseHeaders: () => true }) +); +``` + +### Cookie 安全设置 + +```typescript +// ❌ 不安全的 Cookie 设置 +// src/hooks.server.js +export async function handle({ event, resolve }) { + const token = event.cookies.get('session'); + // cookie without httpOnly, secure, sameSite flags + event.cookies.set('session', token, { + path: '/', + // missing: httpOnly, secure, sameSite + }); +} + +// ✅ 安全的 Cookie 配置 +import { dev } from '$app/environment'; + +event.cookies.set('session', token, { + path: '/', + httpOnly: true, // not accessible via JS + secure: !dev, // HTTPS only in production + sameSite: 'lax', // CSRF protection + maxAge: 60 * 60 * 24 * 7 // 1 week, explicit expiry +}); +``` + +--- + +## Review Checklist + +### Runes: $state / $derived / $effect + +- [ ] $state 只用于会变化的值,常量直接声明 +- [ ] 大型不可变数据使用 $state.raw +- [ ] 没有解构 $state 对象(会丢失响应性) +- [ ] 外部库使用 $state.snapshot 传入普通对象 +- [ ] $derived 中没有副作用 +- [ ] 没有用 $effect 替代 $derived 做状态同步 +- [ ] $effect 中不修改被追踪的状态(避免无限循环) +- [ ] $effect 有清理函数(订阅、定时器、WebSocket) +- [ ] async $effect 在 await 前读取所有需要追踪的状态 +- [ ] 使用 untrack 排除不相关的依赖 + +### Load 函数 + +- [ ] 服务端逻辑放在 +page.server.js(不是 +page.js) +- [ ] 避免不必要的 await parent() 瀑布流 +- [ ] 独立数据并行加载(Promise.all 或直接返回 Promise) +- [ ] server load 只返回可序列化的数据 + +### Form Actions + +- [ ] 副作用操作(增删改)使用 form actions + POST +- [ ] fail() 不返回敏感信息(密码、内部数据) +- [ ] 使用 use:enhance 实现渐进增强 + +### Store 迁移 + +- [ ] writable/readable → $state 在 .svelte.js 文件中 +- [ ] 不在普通 .js 文件中使用 runes +- [ ] 不使用遗留的 $ 前缀 store 语法 + +### SSR vs CSR 边界 + +- [ ] 不在根 layout 中全局禁用 SSR +- [ ] 浏览器 API(window、document)在 onMount 或 browser guard 中使用 +- [ ] prerender 页面不包含 server actions + +### Svelte 4 → 5 迁移 + +- [ ] $: → $derived / $effect +- [ ] export let → $props() +- [ ] on:click → onclick +- [ ] createEventDispatcher → 回调 props +- [ ] slot → @render children() +- [ ] beforeUpdate/afterUpdate → $effect.pre / $effect + +### 性能优化 + +- [ ] 大型不可变数据使用 $state.raw +- [ ] {#each} 使用唯一 key +- [ ] load 函数返回 Promise 实现流式传输 +- [ ] 独立数据并行加载 + +### 安全审查 + +- [ ] 私有环境变量只在 server load 中使用 +- [ ] 服务端代码放在 $lib/server/ 目录 +- [ ] 不禁用内建 CSRF 防护 +- [ ] Cookie 设置 httpOnly、secure、sameSite +- [ ] server load 不泄露密钥和内部数据 diff --git a/.claude/skills/code-review-skill/reference/swift.md b/.claude/skills/code-review-skill/reference/swift.md new file mode 100644 index 0000000..7a14c65 --- /dev/null +++ b/.claude/skills/code-review-skill/reference/swift.md @@ -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) -> 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) -> 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? + + 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.. 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(_ 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 { + 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 { + 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 + 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) diff --git a/.claude/skills/code-review-skill/reference/typescript.md b/.claude/skills/code-review-skill/reference/typescript.md new file mode 100644 index 0000000..2d724f1 --- /dev/null +++ b/.claude/skills/code-review-skill/reference/typescript.md @@ -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(arr: T[]): T | undefined { + return arr[0]; +} +``` + +### 泛型约束 + +```typescript +// ❌ 泛型没有约束,无法访问属性 +function getProperty(obj: T, key: string) { + return obj[key]; // Error: 无法索引 +} + +// ✅ 使用 keyof 约束 +function getProperty(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 { + data: T; + status: number; + message: string; +} + +// 可以不指定泛型参数 +const response: ApiResponse = { data: null, status: 200, message: 'OK' }; +// 也可以指定 +const userResponse: ApiResponse = { ... }; +``` + +### 常见泛型工具类型 + +```typescript +// ✅ 善用内置工具类型 +interface User { + id: number; + name: string; + email: string; +} + +type PartialUser = Partial; // 所有属性可选 +type RequiredUser = Required; // 所有属性必需 +type ReadonlyUser = Readonly; // 所有属性只读 +type UserKeys = keyof User; // 'id' | 'name' | 'email' +type NameOnly = Pick; // { name: string } +type WithoutId = Omit; // { name: string; email: string } +type UserRecord = Record; // { [key: string]: User } +``` + +--- + +## 高级类型 + +### 条件类型 + +```typescript +// ✅ 根据输入类型返回不同类型 +type IsString = T extends string ? true : false; + +type A = IsString; // true +type B = IsString; // false + +// ✅ 提取数组元素类型 +type ElementType = T extends (infer U)[] ? U : never; + +type Elem = ElementType; // string + +// ✅ 提取函数返回类型(内置 ReturnType) +type MyReturnType = T extends (...args: any[]) => infer R ? R : never; +``` + +### 映射类型 + +```typescript +// ✅ 转换对象类型的所有属性 +type Nullable = { + [K in keyof T]: T[K] | null; +}; + +interface User { + name: string; + age: number; +} + +type NullableUser = Nullable; +// { name: string | null; age: number | null } + +// ✅ 添加前缀 +type Getters = { + [K in keyof T as `get${Capitalize}`]: () => T[K]; +}; + +type UserGetters = Getters; +// { getName: () => string; getAge: () => number } +``` + +### 模板字面量类型 + +```typescript +// ✅ 类型安全的事件名称 +type EventName = 'click' | 'focus' | 'blur'; +type HandlerName = `on${Capitalize}`; +// 'onClick' | 'onFocus' | 'onBlur' + +// ✅ API 路由类型 +type ApiRoute = `/api/${string}`; +const route: ApiRoute = '/api/users'; // OK +const badRoute: ApiRoute = '/users'; // Error +``` + +### Discriminated Unions + +```typescript +// ✅ 使用判别属性实现类型安全 +type Result = + | { success: true; data: T } + | { success: false; error: E }; + +function handleResult(result: Result) { + 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 { + 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 = { + readonly [K in keyof T]: T[K] extends object ? DeepReadonly : T[K]; +}; +``` + +### 不变式函数参数 + +```typescript +// ✅ 使用 as const 和 readonly 保护数据 +function createConfig(routes: T) { + return routes; +} + +const routes = createConfig(['home', 'about', 'contact'] as const); +// 类型是 readonly ['home', 'about', 'contact'] +``` + +--- + +## ESLint 规则 + +### 推荐的 @typescript-eslint 规则 + +```javascript +// eslint.config.js(flat config,typescript-eslint v8) +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + eslint.configs.recommended, + // 需要类型信息的规则集,对应旧的 recommended-requiring-type-checking + tseslint.configs.recommendedTypeChecked, + tseslint.configs.strictTypeChecked, + { + languageOptions: { + parserOptions: { + // 让带类型的规则自动找到对应 tsconfig + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + // ✅ 类型安全 + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-unsafe-assignment': 'error', + '@typescript-eslint/no-unsafe-member-access': 'error', + '@typescript-eslint/no-unsafe-call': 'error', + '@typescript-eslint/no-unsafe-return': 'error', + + // ✅ 最佳实践 + '@typescript-eslint/explicit-function-return-type': 'warn', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-misused-promises': 'error', + + // ✅ 代码风格 + '@typescript-eslint/consistent-type-imports': 'error', + '@typescript-eslint/prefer-nullish-coalescing': 'error', + '@typescript-eslint/prefer-optional-chain': 'error', + }, + }, +); +``` + +### 常见 ESLint 错误修复 + +```typescript +// ❌ no-floating-promises: Promise 必须被处理 +async function save() { ... } +save(); // Error: 未处理的 Promise + +// ✅ 显式处理 +await save(); +// 或 +save().catch(console.error); +// 或明确忽略 +void save(); + +// ❌ no-misused-promises: 不能在非 async 位置使用 Promise +const items = [1, 2, 3]; +items.forEach(async (item) => { // Error! + await processItem(item); +}); + +// ✅ 使用 for...of +for (const item of items) { + await processItem(item); +} +// 或 Promise.all +await Promise.all(items.map(processItem)); +``` + +--- + +## Review Checklist + +### 类型系统 +- [ ] 没有使用 `any`(使用 `unknown` + 类型守卫代替) +- [ ] 接口和类型定义完整且有意义的命名 +- [ ] 使用泛型提高代码复用性 +- [ ] 联合类型有正确的类型收窄 +- [ ] 善用工具类型(Partial、Pick、Omit 等) + +### 泛型 +- [ ] 泛型有适当的约束(extends) +- [ ] 泛型参数有合理的默认值 +- [ ] 避免过度泛型化(KISS 原则) + +### Strict 模式 +- [ ] tsconfig.json 启用了 strict: true +- [ ] 启用了 noUncheckedIndexedAccess +- [ ] 没有使用 @ts-ignore(改用 @ts-expect-error) + +### 异步代码 +- [ ] async 函数有错误处理 +- [ ] Promise rejection 被正确处理 +- [ ] 没有 floating promises(未处理的 Promise) +- [ ] 并发请求使用 Promise.all 或 Promise.allSettled +- [ ] 竞态条件使用 AbortController 处理 + +### 不可变性 +- [ ] 不直接修改函数参数 +- [ ] 使用 spread 操作符创建新对象/数组 +- [ ] 考虑使用 readonly 修饰符 + +### ESLint +- [ ] 使用 @typescript-eslint/recommended +- [ ] 没有 ESLint 警告或错误 +- [ ] 使用 consistent-type-imports diff --git a/.claude/skills/code-review-skill/reference/vue.md b/.claude/skills/code-review-skill/reference/vue.md new file mode 100644 index 0000000..cbd9165 --- /dev/null +++ b/.claude/skills/code-review-skill/reference/vue.md @@ -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 + + + + + + + + +``` + +### 解构 reactive 对象 + +```vue + + + + + +``` + +### computed 副作用 + +```vue + + + + + +``` + +### shallowRef 优化 + +```vue + + + + + +``` + +--- + +## Props & Emits + +### 直接修改 props + +```vue + + + + + +``` + +### defineProps 类型声明 + +```vue + + + + + +``` + +### defineEmits 类型安全 + +```vue + + + + + +``` + +--- + +## Vue 3.5 新特性 + +### Reactive Props Destructure (3.5+) + +```vue + + + + + + + + +``` + +### defineModel (3.4+) + +```vue + + + + + + + + + + + + + + + + +``` + +### useTemplateRef (3.5+) + +```vue + + + + + + + + + + +``` + +### useId (3.5+) + +```vue + + + + + + + + + + +``` + +### onWatcherCleanup (3.5+) + +```vue + + + + + +``` + +### Deferred Teleport (3.5+) + +```vue + + + + + +``` + +--- + +## Watchers + +### watch vs watchEffect + +```vue + +``` + +### watch 清理函数 + +```vue + + + + + +``` + +### watch 选项 + +```vue + +``` + +### 监听多个源 + +```vue + +``` + +--- + +## 模板最佳实践 + +### v-for 的 key + +```vue + + + + + + + + +``` + +### v-if 和 v-for 优先级 + +```vue + + + + + + + + + +``` + +### 事件处理 + +```vue + + + + + + + + + +``` + +--- + +## 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 + + + + + +``` + +### 异步 Composable + +```typescript +// ✅ 异步 composable 模式 +export function useFetch(url: MaybeRefOrGetter) { + const data = ref(null) + const error = ref(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('/api/users') +``` + +### 生命周期与清理 + +```typescript +// ✅ Composable 中正确处理生命周期 +export function useEventListener( + target: MaybeRefOrGetter, + 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 + + + + + +``` + +### defineAsyncComponent + +```vue + +``` + +### KeepAlive + +```vue + + + +``` + +### 虚拟列表 + +```vue + + + +``` + +--- + +## Review Checklist + +### 响应性系统 +- [ ] ref 用于基本类型,reactive 用于对象(或统一用 ref) +- [ ] 没有解构 reactive 对象(或使用了 toRefs) +- [ ] props 传递给 composable 时保持了响应性 +- [ ] shallowRef/shallowReactive 用于大型对象优化 +- [ ] computed 中没有副作用 + +### Props & Emits +- [ ] defineProps 使用 TypeScript 类型声明 +- [ ] 复杂默认值使用 withDefaults + 工厂函数 +- [ ] defineEmits 有完整的类型定义 +- [ ] 没有直接修改 props +- [ ] 考虑使用 defineModel 简化 v-model(Vue 3.4+) + +### Vue 3.5 新特性(如适用) +- [ ] 使用 Reactive Props Destructure 简化 props 访问 +- [ ] 使用 useTemplateRef 替代 ref 属性 +- [ ] 表单使用 useId 生成 SSR 安全的 ID +- [ ] 使用 onWatcherCleanup 处理复杂清理逻辑 + +### Watchers +- [ ] watch/watchEffect 有适当的清理函数 +- [ ] 异步 watch 处理了竞态条件 +- [ ] flush: 'post' 用于 DOM 操作的 watcher +- [ ] 避免过度使用 watcher(优先用 computed) +- [ ] 考虑 once: true 用于一次性监听 + +### 模板 +- [ ] v-for 使用唯一且稳定的 key +- [ ] v-if 和 v-for 没有在同一元素上 +- [ ] 事件处理使用方法而非内联复杂逻辑 +- [ ] 大型列表使用虚拟滚动 + +### Composables +- [ ] 相关逻辑提取到 composables +- [ ] composables 返回响应式引用(不是 .value) +- [ ] 纯函数不要包装成 composable +- [ ] 副作用在组件卸载时清理 +- [ ] 使用 effectScope 管理复杂副作用 + +### 性能 +- [ ] 大型组件拆分为小组件 +- [ ] 使用 defineAsyncComponent 懒加载 +- [ ] 避免不必要的响应式转换 +- [ ] v-memo 用于昂贵的列表渲染 +- [ ] KeepAlive 用于缓存动态组件 diff --git a/.claude/skills/code-review-skill/scripts/pr-analyzer.py b/.claude/skills/code-review-skill/scripts/pr-analyzer.py new file mode 100644 index 0000000..0e6d18d --- /dev/null +++ b/.claude/skills/code-review-skill/scripts/pr-analyzer.py @@ -0,0 +1,388 @@ +#!/usr/bin/env python3 +""" +PR Analyzer - Analyze PR complexity and suggest review approach. + +Usage: + python pr-analyzer.py [--diff-file FILE] [--stats] + + Or pipe diff directly: + git diff main...HEAD | python pr-analyzer.py +""" + +import os +import sys +import re +import argparse +from collections import defaultdict +from dataclasses import dataclass +from typing import List, Dict, Optional + +RISK_NO_TESTS = "NO_TEST_CHANGES" + + +@dataclass +class FileStats: + """Statistics for a single file.""" + filename: str + additions: int = 0 + deletions: int = 0 + is_test: bool = False + is_config: bool = False + language: str = "unknown" + + +@dataclass +class PRAnalysis: + """Complete PR analysis results.""" + total_files: int + total_additions: int + total_deletions: int + files: List[FileStats] + complexity_score: float + size_category: str + estimated_review_time: int + risk_factors: List[str] + suggestions: List[str] + + +def detect_language(filename: str) -> str: + """Detect programming language from filename.""" + _, ext = os.path.splitext(filename) + extensions = { + '.py': 'Python', + '.js': 'JavaScript', + '.ts': 'TypeScript', + '.tsx': 'TypeScript/React', + '.jsx': 'JavaScript/React', + '.rs': 'Rust', + '.go': 'Go', + '.c': 'C', + '.h': 'C/C++', + '.cpp': 'C++', + '.hpp': 'C++', + '.cc': 'C++', + '.cxx': 'C++', + '.hh': 'C++', + '.hxx': 'C++', + '.java': 'Java', + '.kt': 'Kotlin', + '.swift': 'Swift', + '.rb': 'Ruby', + '.php': 'PHP', + '.cs': 'C#', + '.vue': 'Vue', + '.svelte': 'Svelte', + '.sql': 'SQL', + '.md': 'Markdown', + '.json': 'JSON', + '.yaml': 'YAML', + '.yml': 'YAML', + '.toml': 'TOML', + '.css': 'CSS', + '.scss': 'SCSS', + '.less': 'Less', + '.html': 'HTML', + '.zig': 'Zig', + '.ex': 'Elixir', + '.exs': 'Elixir', + '.erl': 'Erlang', + '.scala': 'Scala', + '.lua': 'Lua', + } + return extensions.get(ext.lower(), 'unknown') + + +def is_test_file(filename: str) -> bool: + """Check if file is a test file.""" + test_patterns = [ + r'test_.*\.py$', + r'.*_test\.py$', + r'.*\.test\.(js|ts|tsx)$', + r'.*\.spec\.(js|ts|tsx)$', + r'tests?/', + r'__tests__/', + ] + return any(re.search(p, filename) for p in test_patterns) + + +def is_config_file(filename: str) -> bool: + """Check if file is a configuration file.""" + config_patterns = [ + r'\.env', + r'config\.', + r'\.json$', + r'\.yaml$', + r'\.yml$', + r'\.toml$', + r'Cargo\.toml$', + r'package\.json$', + r'tsconfig\.json$', + ] + return any(re.search(p, filename) for p in config_patterns) + + +def parse_diff(diff_content: str) -> List[FileStats]: + """Parse git diff output and extract file statistics.""" + files = [] + current_file = None + + for line in diff_content.split('\n'): + # New file header + if line.startswith('diff --git'): + if current_file: + files.append(current_file) + # "diff --git a/ b/" — match the b/ side via a + # backreference so a literal "b/" inside paths like lib/, web/ or + # db/ can't be mistaken for the prefix. Renames have differing + # paths, so fall back to the b/ side after the separating space. + match = re.match(r'diff --git a/(.+?) b/\1', line) + if not match: + match = re.search(r' b/(.+)$', line) + if match: + filename = match.group(1) + current_file = FileStats( + filename=filename, + language=detect_language(filename), + is_test=is_test_file(filename), + is_config=is_config_file(filename), + ) + else: + current_file = None + elif current_file: + if line.startswith('+') and not line.startswith('+++'): + current_file.additions += 1 + elif line.startswith('-') and not line.startswith('---'): + current_file.deletions += 1 + + if current_file: + files.append(current_file) + + return files + + +def calculate_complexity(files: List[FileStats]) -> float: + """Calculate complexity score (0-1 scale).""" + if not files: + return 0.0 + + total_changes = sum(f.additions + f.deletions for f in files) + + # Base complexity from size + size_factor = min(total_changes / 1000, 1.0) + + # Factor for number of files + file_factor = min(len(files) / 20, 1.0) + + # Factor for non-test code ratio + test_lines = sum(f.additions + f.deletions for f in files if f.is_test) + non_test_ratio = 1 - (test_lines / max(total_changes, 1)) + + # Factor for language diversity + languages = set(f.language for f in files if f.language != 'unknown') + lang_factor = min(len(languages) / 5, 1.0) + + complexity = ( + size_factor * 0.4 + + file_factor * 0.2 + + non_test_ratio * 0.2 + + lang_factor * 0.2 + ) + + return round(complexity, 2) + + +def categorize_size(total_changes: int) -> str: + """Categorize PR size.""" + if total_changes < 50: + return "XS (Extra Small)" + elif total_changes < 200: + return "S (Small)" + elif total_changes < 400: + return "M (Medium)" + elif total_changes < 800: + return "L (Large)" + else: + return "XL (Extra Large) - Consider splitting" + + +def estimate_review_time(files: List[FileStats], complexity: float) -> int: + """Estimate review time in minutes.""" + total_changes = sum(f.additions + f.deletions for f in files) + + # Base time: ~1 minute per 20 lines + base_time = total_changes / 20 + + # Adjust for complexity + adjusted_time = base_time * (1 + complexity) + + # Minimum 5 minutes, maximum 120 minutes + return max(5, min(120, int(adjusted_time))) + + +def identify_risk_factors(files: List[FileStats]) -> List[str]: + """Identify potential risk factors in the PR.""" + risks = [] + + total_changes = sum(f.additions + f.deletions for f in files) + test_changes = sum(f.additions + f.deletions for f in files if f.is_test) + + if total_changes > 400: + risks.append("Large PR (>400 lines) - harder to review thoroughly") + + if test_changes == 0 and total_changes > 50: + risks.append(f"{RISK_NO_TESTS}: No test changes - verify test coverage") + + if total_changes > 100 and test_changes / max(total_changes, 1) < 0.2: + risks.append("Low test ratio (<20%) - consider adding more tests") + + # Security-sensitive files + security_patterns = ['.env', 'auth', 'security', 'password', 'token', 'secret'] + for f in files: + if any(p in f.filename.lower() for p in security_patterns): + risks.append(f"Security-sensitive file: {f.filename}") + break + + # Database changes + for f in files: + if 'migration' in f.filename.lower() or f.language == 'SQL': + risks.append("Database changes detected - review carefully") + break + + # Config changes + config_files = [f for f in files if f.is_config] + if config_files: + risks.append(f"Configuration changes in {len(config_files)} file(s)") + + return risks + + +def generate_suggestions(files: List[FileStats], complexity: float, risks: List[str]) -> List[str]: + """Generate review suggestions.""" + suggestions = [] + + total_changes = sum(f.additions + f.deletions for f in files) + + if total_changes > 800: + suggestions.append("Consider splitting this PR into smaller, focused changes") + + if complexity > 0.7: + suggestions.append("High complexity - allocate extra review time") + suggestions.append("Consider pair reviewing for critical sections") + + if any(RISK_NO_TESTS in r for r in risks): + suggestions.append("Request test additions before approval") + + # Language-specific suggestions + languages = set(f.language for f in files) + if 'TypeScript' in languages or 'TypeScript/React' in languages: + suggestions.append("Check for proper type usage (avoid 'any')") + if 'Rust' in languages: + suggestions.append("Check for unwrap() usage and error handling") + if 'C' in languages or 'C++' in languages or 'C/C++' in languages: + suggestions.append("Check for memory safety, bounds checks, and UB risks") + if 'SQL' in languages: + suggestions.append("Review for SQL injection and query performance") + + if not suggestions: + suggestions.append("Standard review process should suffice") + + return suggestions + + +def analyze_pr(diff_content: str) -> PRAnalysis: + """Perform complete PR analysis.""" + files = parse_diff(diff_content) + + total_additions = sum(f.additions for f in files) + total_deletions = sum(f.deletions for f in files) + total_changes = total_additions + total_deletions + + complexity = calculate_complexity(files) + risks = identify_risk_factors(files) + suggestions = generate_suggestions(files, complexity, risks) + + return PRAnalysis( + total_files=len(files), + total_additions=total_additions, + total_deletions=total_deletions, + files=files, + complexity_score=complexity, + size_category=categorize_size(total_changes), + estimated_review_time=estimate_review_time(files, complexity), + risk_factors=risks, + suggestions=suggestions, + ) + + +def print_analysis(analysis: PRAnalysis, show_files: bool = False): + """Print analysis results.""" + print("\n" + "=" * 60) + print("PR ANALYSIS REPORT") + print("=" * 60) + + print(f"\n📊 SUMMARY") + print(f" Files changed: {analysis.total_files}") + print(f" Additions: +{analysis.total_additions}") + print(f" Deletions: -{analysis.total_deletions}") + print(f" Total changes: {analysis.total_additions + analysis.total_deletions}") + + print(f"\n📏 SIZE: {analysis.size_category}") + print(f" Complexity score: {analysis.complexity_score}/1.0") + print(f" Estimated review time: ~{analysis.estimated_review_time} minutes") + + if analysis.risk_factors: + print(f"\n⚠️ RISK FACTORS:") + for risk in analysis.risk_factors: + print(f" • {risk}") + + print(f"\n💡 SUGGESTIONS:") + for suggestion in analysis.suggestions: + print(f" • {suggestion}") + + if show_files: + print(f"\n📁 FILES:") + # Group by language + by_lang: Dict[str, List[FileStats]] = defaultdict(list) + for f in analysis.files: + by_lang[f.language].append(f) + + for lang, lang_files in sorted(by_lang.items()): + print(f"\n [{lang}]") + for f in lang_files: + prefix = "🧪" if f.is_test else "⚙️" if f.is_config else "📄" + print(f" {prefix} {f.filename} (+{f.additions}/-{f.deletions})") + + print("\n" + "=" * 60) + + +def main(): + parser = argparse.ArgumentParser(description='Analyze PR complexity') + parser.add_argument('--diff-file', '-f', help='Path to diff file') + parser.add_argument('--stats', '-s', action='store_true', help='Show file details') + args = parser.parse_args() + + # Read diff from file or stdin + try: + if args.diff_file: + with open(args.diff_file, 'r', encoding='utf-8', errors='replace') as f: + diff_content = f.read() + elif not sys.stdin.isatty(): + diff_content = sys.stdin.buffer.read().decode('utf-8', errors='replace') + else: + print("Usage: git diff main...HEAD | python pr-analyzer.py") + print(" python pr-analyzer.py -f diff.txt") + sys.exit(1) + except OSError as e: + print(f"Error reading diff input: {e}", file=sys.stderr) + sys.exit(1) + + if not diff_content.strip(): + print("No diff content provided") + sys.exit(1) + + analysis = analyze_pr(diff_content) + print_analysis(analysis, show_files=args.stats) + + +if __name__ == '__main__': + main() diff --git a/.claude/skills/code-review-skill/scripts/test_pr_analyzer.py b/.claude/skills/code-review-skill/scripts/test_pr_analyzer.py new file mode 100644 index 0000000..21c6f1a --- /dev/null +++ b/.claude/skills/code-review-skill/scripts/test_pr_analyzer.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Tests for pr-analyzer.py diff parsing (stdlib unittest, no extra deps).""" + +import importlib.util +import os +import unittest + +# The script has a hyphen in its name, so load it by path. +_HERE = os.path.dirname(os.path.abspath(__file__)) +_spec = importlib.util.spec_from_file_location( + 'pr_analyzer', os.path.join(_HERE, 'pr-analyzer.py') +) +pr_analyzer = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(pr_analyzer) + + +class ParseDiffFilenameTest(unittest.TestCase): + def test_lib_prefixed_path(self): + # "lib/" embeds a literal "b/" that the old regex swallowed. + diff = ( + "diff --git a/lib/foo.py b/lib/foo.py\n" + "index 1234567..89abcde 100644\n" + "--- a/lib/foo.py\n" + "+++ b/lib/foo.py\n" + "@@ -1,2 +1,3 @@\n" + " unchanged\n" + "+added line\n" + "-removed line\n" + ) + files = pr_analyzer.parse_diff(diff) + self.assertEqual(len(files), 1) + self.assertEqual(files[0].filename, 'lib/foo.py') + self.assertEqual(files[0].additions, 1) + self.assertEqual(files[0].deletions, 1) + + def test_normal_path(self): + diff = ( + "diff --git a/src/main.py b/src/main.py\n" + "index 1111111..2222222 100644\n" + "--- a/src/main.py\n" + "+++ b/src/main.py\n" + "@@ -0,0 +1 @@\n" + "+print('hi')\n" + ) + files = pr_analyzer.parse_diff(diff) + self.assertEqual(len(files), 1) + self.assertEqual(files[0].filename, 'src/main.py') + + def test_other_embedded_b_slash_prefixes(self): + # web/ and db/ also contain a literal "b/". + diff = ( + "diff --git a/web/x.js b/web/x.js\n" + "+++ b/web/x.js\n" + "+console.log(1)\n" + "diff --git a/db/y.sql b/db/y.sql\n" + "+++ b/db/y.sql\n" + "+SELECT 1;\n" + ) + files = pr_analyzer.parse_diff(diff) + self.assertEqual([f.filename for f in files], ['web/x.js', 'db/y.sql']) + + def test_rename_falls_back_to_b_side(self): + diff = ( + "diff --git a/old/name.py b/new/name.py\n" + "similarity index 100%\n" + "rename from old/name.py\n" + "rename to new/name.py\n" + ) + files = pr_analyzer.parse_diff(diff) + self.assertEqual(len(files), 1) + self.assertEqual(files[0].filename, 'new/name.py') + + +if __name__ == '__main__': + unittest.main()