commit d9bcdc4a8d7b0f0f24d6600e1e3da935c2f208a3 Author: Jason Woltje Date: Mon Feb 16 16:03:39 2026 -0600 feat: Initial agent-skills repo — 4 adapted skills for Mosaic Stack Skills included: - pr-reviewer: Adapted for Gitea/GitHub via platform-aware scripts (dropped fetch_pr_data.py and add_inline_comment.py, kept generate_review_files.py) - code-review-excellence: Methodology and checklists (React, TS, Python, etc.) - vercel-react-best-practices: 57 rules for React/Next.js performance - tailwind-design-system: Tailwind CSS v4 patterns, CVA, design tokens New shell scripts added to ~/.claude/scripts/git/: - pr-diff.sh: Get PR diff (GitHub gh / Gitea API) - pr-metadata.sh: Get PR metadata as normalized JSON Co-Authored-By: Claude Opus 4.6 diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed6a953 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Agent Skills + +Custom agent skills for Mosaic Stack and USC projects. Platform-aware — works with both GitHub (`gh`) and Gitea (`tea`) via our abstraction scripts. + +## Skills + +| Skill | Purpose | Origin | +|-------|---------|--------| +| `pr-reviewer` | Structured PR code review workflow | Adapted from [SpillwaveSolutions/pr-reviewer-skill](https://github.com/SpillwaveSolutions/pr-reviewer-skill) | +| `code-review-excellence` | Code review methodology and checklists | Adapted from [awesome-skills/code-review-skill](https://github.com/awesome-skills/code-review-skill) | +| `vercel-react-best-practices` | React/Next.js performance optimization | From [vercel-labs/agent-skills](https://github.com/vercel-labs/agent-skills) | +| `tailwind-design-system` | Tailwind CSS v4 design system patterns | Adapted from [wshobson/agents](https://github.com/wshobson/agents) | + +## Installation + +### Manual (symlink into Claude Code) + +```bash +# Symlink individual skills +ln -s ~/src/agent-skills/skills/pr-reviewer ~/.claude/skills/pr-reviewer +ln -s ~/src/agent-skills/skills/code-review-excellence ~/.claude/skills/code-review-excellence +ln -s ~/src/agent-skills/skills/vercel-react-best-practices ~/.claude/skills/vercel-react-best-practices +ln -s ~/src/agent-skills/skills/tailwind-design-system ~/.claude/skills/tailwind-design-system +``` + +### Per-Project + +Symlink into a project's `.claude/skills/` directory for project-specific availability. + +## Dependencies + +- `~/.claude/scripts/git/` — Platform-aware git scripts (detect-platform, pr-view, pr-diff, pr-metadata, pr-review, etc.) +- `python3` — For review file generation + +## Adapting Skills + +When adding skills from the community: + +1. Replace raw `gh`/`tea` calls with our `~/.claude/scripts/git/` scripts +2. Test on both GitHub and Gitea repos +3. Remove features that don't work cross-platform (e.g., GitHub-specific inline comments) +4. Document any platform-specific limitations + +## License + +Individual skills retain their original licenses. Adaptations are MIT. diff --git a/skills/code-review-excellence/SKILL.md b/skills/code-review-excellence/SKILL.md new file mode 100644 index 0000000..bc31627 --- /dev/null +++ b/skills/code-review-excellence/SKILL.md @@ -0,0 +1,198 @@ +--- +name: code-review-excellence +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, 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 Excellence + +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 + +### 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 + +### 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! + +## 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 | +| **Rust** | [Rust Guide](reference/rust.md) | 所有权/借用, Unsafe 审查, 异步代码, 错误处理 | +| **TypeScript** | [TypeScript Guide](reference/typescript.md) | 类型安全, async/await, 不可变性 | +| **Python** | [Python Guide](reference/python.md) | 可变默认参数, 异常处理, 类属性 | +| **Java** | [Java Guide](reference/java.md) | Java 17/21 新特性, Spring Boot 3, 虚拟线程, Stream/Optional | +| **Go** | [Go Guide](reference/go.md) | 错误处理, goroutine/channel, context, 接口设计 | +| **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) | 对象模型, 信号/槽, 内存管理, 线程安全, 性能 | + +## 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/skills/code-review-excellence/assets/pr-review-template.md b/skills/code-review-excellence/assets/pr-review-template.md new file mode 100644 index 0000000..33f7e71 --- /dev/null +++ b/skills/code-review-excellence/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] + +## Questions + +❓ [Clarification needed about X] + +❓ [Question about 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] +``` + +### Question +``` +❓ **[question]** [Your question] + +I'm curious about the decision to [X]. Could you explain [Y]? +``` diff --git a/skills/code-review-excellence/assets/review-checklist.md b/skills/code-review-excellence/assets/review-checklist.md new file mode 100644 index 0000000..4ff3b8b --- /dev/null +++ b/skills/code-review-excellence/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 | +| ❓ `[question]` | Need clarity | Respond | +| 🎉 `[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/skills/code-review-excellence/reference/architecture-review-guide.md b/skills/code-review-excellence/reference/architecture-review-guide.md new file mode 100644 index 0000000..abde68c --- /dev/null +++ b/skills/code-review-excellence/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/skills/code-review-excellence/reference/c.md b/skills/code-review-excellence/reference/c.md new file mode 100644 index 0000000..cfd31ed --- /dev/null +++ b/skills/code-review-excellence/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/skills/code-review-excellence/reference/code-review-best-practices.md b/skills/code-review-excellence/reference/code-review-best-practices.md new file mode 100644 index 0000000..8c6b9cd --- /dev/null +++ b/skills/code-review-excellence/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/skills/code-review-excellence/reference/common-bugs-checklist.md b/skills/code-review-excellence/reference/common-bugs-checklist.md new file mode 100644 index 0000000..97e2e63 --- /dev/null +++ b/skills/code-review-excellence/reference/common-bugs-checklist.md @@ -0,0 +1,1227 @@ +# Common Bugs Checklist + +Language-specific bugs and issues to watch for during code review. + +## Universal Issues + +### Logic Errors +- [ ] Off-by-one errors in loops and array access +- [ ] Incorrect boolean logic (De Morgan's law violations) +- [ ] Missing null/undefined checks +- [ ] Race conditions in concurrent code +- [ ] Incorrect comparison operators (== vs ===, = vs ==) +- [ ] Integer overflow/underflow +- [ ] Floating point comparison issues + +### Resource Management +- [ ] Memory leaks (unclosed connections, listeners) +- [ ] File handles not closed +- [ ] Database connections not released +- [ ] Event listeners not removed +- [ ] Timers/intervals not cleared + +### Error Handling +- [ ] Swallowed exceptions (empty catch blocks) +- [ ] Generic exception handling hiding specific errors +- [ ] Missing error propagation +- [ ] Incorrect error types thrown +- [ ] Missing finally/cleanup blocks + +## TypeScript/JavaScript + +### Type Issues +```typescript +// ❌ Using any defeats type safety +function process(data: any) { return data.value; } + +// ✅ Use proper types +interface Data { value: string; } +function process(data: Data) { return data.value; } +``` + +### Async/Await Pitfalls +```typescript +// ❌ Missing await +async function fetch() { + const data = fetchData(); // Missing await! + return data.json(); +} + +// ❌ Unhandled promise rejection +async function risky() { + const result = await fetchData(); // No try-catch + return result; +} + +// ✅ Proper error handling +async function safe() { + try { + const result = await fetchData(); + return result; + } catch (error) { + console.error('Fetch failed:', error); + throw error; + } +} +``` + +### React Specific + +#### Hooks 规则违反 +```tsx +// ❌ 条件调用 Hooks — 违反 Hooks 规则 +function BadComponent({ show }) { + if (show) { + const [value, setValue] = useState(0); // Error! + } + return
...
; +} + +// ✅ Hooks 必须在顶层无条件调用 +function GoodComponent({ show }) { + const [value, setValue] = useState(0); + if (!show) return null; + return
{value}
; +} + +// ❌ 循环中调用 Hooks +function BadLoop({ items }) { + items.forEach(item => { + const [selected, setSelected] = useState(false); // Error! + }); +} + +// ✅ 将状态提升或使用不同的数据结构 +function GoodLoop({ items }) { + const [selectedIds, setSelectedIds] = useState>(new Set()); + return items.map(item => ( + + )); +} +``` + +#### useEffect 常见错误 +```tsx +// ❌ 依赖数组不完整 — stale closure +function StaleClosureExample({ userId, onSuccess }) { + const [data, setData] = useState(null); + useEffect(() => { + fetchData(userId).then(result => { + setData(result); + onSuccess(result); // onSuccess 可能是 stale 的! + }); + }, [userId]); // 缺少 onSuccess 依赖 +} + +// ✅ 完整的依赖数组 +useEffect(() => { + fetchData(userId).then(result => { + setData(result); + onSuccess(result); + }); +}, [userId, onSuccess]); + +// ❌ 无限循环 — 在 effect 中更新依赖 +function InfiniteLoop() { + const [count, setCount] = useState(0); + useEffect(() => { + setCount(count + 1); // 触发重渲染,又触发 effect + }, [count]); // 无限循环! +} + +// ❌ 缺少清理函数 — 内存泄漏 +function MemoryLeak({ userId }) { + const [user, setUser] = useState(null); + useEffect(() => { + fetchUser(userId).then(setUser); // 组件卸载后仍然调用 setUser + }, [userId]); +} + +// ✅ 正确的清理 +function NoLeak({ userId }) { + const [user, setUser] = useState(null); + useEffect(() => { + let cancelled = false; + fetchUser(userId).then(data => { + if (!cancelled) setUser(data); + }); + return () => { cancelled = true; }; + }, [userId]); +} + +// ❌ useEffect 用于派生状态(反模式) +function BadDerived({ items }) { + const [total, setTotal] = useState(0); + useEffect(() => { + setTotal(items.reduce((a, b) => a + b.price, 0)); + }, [items]); // 不必要的 effect + 额外渲染 +} + +// ✅ 直接计算或用 useMemo +function GoodDerived({ items }) { + const total = useMemo( + () => items.reduce((a, b) => a + b.price, 0), + [items] + ); +} + +// ❌ useEffect 用于事件响应 +function BadEvent() { + const [query, setQuery] = useState(''); + useEffect(() => { + if (query) logSearch(query); // 应该在事件处理器中 + }, [query]); +} + +// ✅ 副作用在事件处理器中 +function GoodEvent() { + const handleSearch = (q: string) => { + setQuery(q); + logSearch(q); + }; +} +``` + +#### useMemo / useCallback 误用 +```tsx +// ❌ 过度优化 — 常量不需要 memo +function OverOptimized() { + const config = useMemo(() => ({ api: '/v1' }), []); // 无意义 + const noop = useCallback(() => {}, []); // 无意义 +} + +// ❌ 空依赖的 useMemo(可能隐藏 bug) +function EmptyDeps({ user }) { + const greeting = useMemo(() => `Hello ${user.name}`, []); + // user 变化时 greeting 不更新! +} + +// ❌ useCallback 依赖总是变化 +function UselessCallback({ data }) { + const process = useCallback(() => { + return data.map(transform); + }, [data]); // 如果 data 每次都是新引用,完全无效 +} + +// ❌ useMemo/useCallback 没有配合 React.memo +function Parent() { + const data = useMemo(() => compute(), []); + const handler = useCallback(() => {}, []); + return ; + // Child 没有用 React.memo,这些优化毫无意义 +} + +// ✅ 正确的优化组合 +const MemoChild = React.memo(function Child({ data, onClick }) { + return ; +}); + +function Parent() { + const data = useMemo(() => expensiveCompute(), [dep]); + const handler = useCallback(() => {}, []); + return ; +} +``` + +#### 组件设计问题 +```tsx +// ❌ 在组件内定义组件 +function Parent() { + // 每次渲染都创建新的 Child 函数,导致完全重新挂载 + const Child = () =>
child
; + return ; +} + +// ✅ 组件定义在外部 +const Child = () =>
child
; +function Parent() { + return ; +} + +// ❌ Props 总是新引用 — 破坏 memo +function BadProps() { + return ( + handle()} // 每次渲染新函数 + items={data.filter(x => x)} // 每次渲染新数组 + /> + ); +} + +// ❌ 直接修改 props +function MutateProps({ user }) { + user.name = 'Changed'; // 永远不要这样做! + return
{user.name}
; +} +``` + +#### Server Components 错误 (React 19+) +```tsx +// ❌ 在 Server Component 中使用客户端 API +// app/page.tsx (默认是 Server Component) +export default function Page() { + const [count, setCount] = useState(0); // Error! + useEffect(() => {}, []); // Error! + return ; // Error! +} + +// ✅ 交互逻辑移到 Client Component +// app/counter.tsx +'use client'; +export function Counter() { + const [count, setCount] = useState(0); + return ; +} + +// app/page.tsx +import { Counter } from './counter'; +export default async function Page() { + const data = await fetchData(); // Server Component 可以直接 await + return ; +} + +// ❌ 在父组件标记 'use client',整个子树变成客户端 +// layout.tsx +'use client'; // 坏主意!所有子组件都变成客户端组件 +export default function Layout({ children }) { ... } +``` + +#### 测试常见错误 +```tsx +// ❌ 使用 container 查询 +const { container } = render(); +const button = container.querySelector('button'); // 不推荐 + +// ✅ 使用 screen 和语义查询 +render(); +const button = screen.getByRole('button', { name: /submit/i }); + +// ❌ 使用 fireEvent +fireEvent.click(button); + +// ✅ 使用 userEvent +await userEvent.click(button); + +// ❌ 测试实现细节 +expect(component.state.isOpen).toBe(true); + +// ✅ 测试行为 +expect(screen.getByRole('dialog')).toBeVisible(); + +// ❌ 等待同步查询 +await screen.getByText('Hello'); // getBy 是同步的 + +// ✅ 异步用 findBy +await screen.findByText('Hello'); // findBy 会等待 +``` + +### React Common Mistakes Checklist +- [ ] Hooks 不在顶层调用(条件/循环中) +- [ ] useEffect 依赖数组不完整 +- [ ] useEffect 缺少清理函数 +- [ ] useEffect 用于派生状态计算 +- [ ] useMemo/useCallback 过度使用 +- [ ] useMemo/useCallback 没配合 React.memo +- [ ] 在组件内定义子组件 +- [ ] Props 是新对象/函数引用(传给 memo 组件时) +- [ ] 直接修改 props +- [ ] 列表缺少 key 或用 index 作为 key +- [ ] Server Component 使用客户端 API +- [ ] 'use client' 放在父组件导致整个树客户端化 +- [ ] 测试使用 container 查询而非 screen +- [ ] 测试实现细节而非行为 + +### React 19 Actions & Forms 错误 + +```tsx +// === useActionState 错误 === + +// ❌ 在 Action 中直接 setState 而不是返回状态 +const [state, action] = useActionState(async (prev, formData) => { + setSomeState(newValue); // 错误!应该返回新状态 +}, initialState); + +// ✅ 返回新状态 +const [state, action] = useActionState(async (prev, formData) => { + const result = await submitForm(formData); + return { ...prev, data: result }; // 返回新状态 +}, initialState); + +// ❌ 忘记处理 isPending +const [state, action] = useActionState(submitAction, null); +return ; // 用户可以重复点击 + +// ✅ 使用 isPending 禁用按钮 +const [state, action, isPending] = useActionState(submitAction, null); +return ; + +// === useFormStatus 错误 === + +// ❌ 在 form 同级调用 useFormStatus +function Form() { + const { pending } = useFormStatus(); // 永远是 undefined! + return
; +} + +// ✅ 在子组件中调用 +function SubmitButton() { + const { pending } = useFormStatus(); + return ; +} +function Form() { + return
; +} + +// === useOptimistic 错误 === + +// ❌ 用于关键业务操作 +function PaymentButton() { + const [optimisticPaid, setPaid] = useOptimistic(false); + const handlePay = async () => { + setPaid(true); // 危险:显示已支付但可能失败 + await processPayment(); + }; +} + +// ❌ 没有处理回滚后的 UI 状态 +const [optimisticLikes, addLike] = useOptimistic(likes); +// 失败后 UI 回滚,但用户可能困惑为什么点赞消失了 + +// ✅ 提供失败反馈 +const handleLike = async () => { + addLike(1); + try { + await likePost(); + } catch { + toast.error('点赞失败,请重试'); // 通知用户 + } +}; +``` + +### React 19 Forms Checklist +- [ ] useActionState 返回新状态而不是 setState +- [ ] useActionState 正确使用 isPending 禁用提交 +- [ ] useFormStatus 在 form 子组件中调用 +- [ ] useOptimistic 不用于关键业务(支付、删除等) +- [ ] useOptimistic 失败时有用户反馈 +- [ ] Server Action 正确标记 'use server' + +### Suspense & Streaming 错误 + +```tsx +// === Suspense 边界错误 === + +// ❌ 整个页面一个 Suspense——慢内容阻塞快内容 +function BadPage() { + return ( + }> + {/* 快 */} + {/* 慢——阻塞整个页面 */} + {/* 快 */} + + ); +} + +// ✅ 独立边界,互不阻塞 +function GoodPage() { + return ( + <> + + }> + + + + + ); +} + +// ❌ 没有 Error Boundary +function NoErrorHandling() { + return ( + }> + {/* 抛错导致白屏 */} + + ); +} + +// ✅ Error Boundary + Suspense +function WithErrorHandling() { + return ( + }> + }> + + + + ); +} + +// === use() Hook 错误 === + +// ❌ 在组件外创建 Promise(每次渲染新 Promise) +function BadUse() { + const data = use(fetchData()); // 每次渲染都创建新 Promise! + return
{data}
; +} + +// ✅ 在父组件创建,通过 props 传递 +function Parent() { + const dataPromise = useMemo(() => fetchData(), []); + return ; +} +function Child({ dataPromise }) { + const data = use(dataPromise); + return
{data}
; +} + +// === Next.js Streaming 错误 === + +// ❌ 在 layout.tsx 中 await 慢数据——阻塞所有子页面 +// app/layout.tsx +export default async function Layout({ children }) { + const config = await fetchSlowConfig(); // 阻塞整个应用! + return {children}; +} + +// ✅ 将慢数据放在页面级别或使用 Suspense +// app/layout.tsx +export default function Layout({ children }) { + return ( + }> + {children} + + ); +} +``` + +### Suspense Checklist +- [ ] 慢内容有独立的 Suspense 边界 +- [ ] 每个 Suspense 有对应的 Error Boundary +- [ ] fallback 是有意义的骨架屏(不是简单 spinner) +- [ ] use() 的 Promise 不在渲染时创建 +- [ ] 没有在 layout 中 await 慢数据 +- [ ] 嵌套层级不超过 3 层 + +### TanStack Query 错误 + +```tsx +// === 查询配置错误 === + +// ❌ queryKey 不包含查询参数 +function BadQuery({ userId, filters }) { + const { data } = useQuery({ + queryKey: ['users'], // 缺少 userId 和 filters! + queryFn: () => fetchUsers(userId, filters), + }); + // userId 或 filters 变化时数据不会更新 +} + +// ✅ queryKey 包含所有影响数据的参数 +function GoodQuery({ userId, filters }) { + const { data } = useQuery({ + queryKey: ['users', userId, filters], + queryFn: () => fetchUsers(userId, filters), + }); +} + +// ❌ staleTime: 0 导致过度请求 +const { data } = useQuery({ + queryKey: ['data'], + queryFn: fetchData, + // 默认 staleTime: 0,每次组件挂载/窗口聚焦都会 refetch +}); + +// ✅ 设置合理的 staleTime +const { data } = useQuery({ + queryKey: ['data'], + queryFn: fetchData, + staleTime: 5 * 60 * 1000, // 5 分钟内不会自动 refetch +}); + +// === useSuspenseQuery 错误 === + +// ❌ useSuspenseQuery + enabled(不支持) +const { data } = useSuspenseQuery({ + queryKey: ['user', userId], + queryFn: () => fetchUser(userId), + enabled: !!userId, // 错误!useSuspenseQuery 不支持 enabled +}); + +// ✅ 条件渲染实现 +function UserQuery({ userId }) { + const { data } = useSuspenseQuery({ + queryKey: ['user', userId], + queryFn: () => fetchUser(userId), + }); + return ; +} + +function Parent({ userId }) { + if (!userId) return ; + return ( + }> + + + ); +} + +// === Mutation 错误 === + +// ❌ Mutation 成功后不 invalidate 查询 +const mutation = useMutation({ + mutationFn: updateUser, + // 忘记 invalidate,UI 显示旧数据 +}); + +// ✅ 成功后 invalidate 相关查询 +const mutation = useMutation({ + mutationFn: updateUser, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['users'] }); + }, +}); + +// ❌ 乐观更新不处理回滚 +const mutation = useMutation({ + mutationFn: updateTodo, + onMutate: async (newTodo) => { + queryClient.setQueryData(['todos'], (old) => [...old, newTodo]); + // 没有保存旧数据,失败后无法回滚! + }, +}); + +// ✅ 完整的乐观更新 +const mutation = useMutation({ + mutationFn: updateTodo, + onMutate: async (newTodo) => { + await queryClient.cancelQueries({ queryKey: ['todos'] }); + const previous = queryClient.getQueryData(['todos']); + queryClient.setQueryData(['todos'], (old) => [...old, newTodo]); + return { previous }; + }, + onError: (err, newTodo, context) => { + queryClient.setQueryData(['todos'], context.previous); + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ['todos'] }); + }, +}); + +// === v5 迁移错误 === + +// ❌ 使用废弃的 API +const { data, isLoading } = useQuery(['key'], fetchFn); // v4 语法 + +// ✅ v5 单一对象参数 +const { data, isPending } = useQuery({ + queryKey: ['key'], + queryFn: fetchFn, +}); + +// ❌ 混淆 isPending 和 isLoading +if (isLoading) return ; +// v5 中 isLoading = isPending && isFetching + +// ✅ 根据意图选择 +if (isPending) return ; // 没有缓存数据 +// 或 +if (isFetching) return ; // 正在后台刷新 +``` + +### TanStack Query Checklist +- [ ] queryKey 包含所有影响数据的参数 +- [ ] 设置了合理的 staleTime(不是默认 0) +- [ ] useSuspenseQuery 不使用 enabled +- [ ] Mutation 成功后 invalidate 相关查询 +- [ ] 乐观更新有完整的回滚逻辑 +- [ ] v5 使用单一对象参数语法 +- [ ] 理解 isPending vs isLoading vs isFetching + +### TypeScript/JavaScript Common Mistakes +- [ ] `==` instead of `===` +- [ ] Modifying array/object during iteration +- [ ] `this` context lost in callbacks +- [ ] Missing `key` prop in lists +- [ ] Closure capturing loop variable +- [ ] parseInt without radix parameter + +## Vue 3 + +### 响应性丢失 +```vue + + + + + +``` + +### Props 响应性传递 +```vue + + + + + +``` + +### Watch 清理 +```vue + + + + + +``` + +### Computed 副作用 +```vue + + + + + +``` + +### 模板常见错误 +```vue + + + + + +``` + +### Common Mistakes +- [ ] 解构 reactive 对象丢失响应性 +- [ ] props 传递给 composable 时未保持响应性 +- [ ] watch 异步回调无清理函数 +- [ ] computed 中产生副作用 +- [ ] v-for 使用 index 作为 key(列表会重排时) +- [ ] v-if 和 v-for 在同一元素上 +- [ ] defineProps 未使用 TypeScript 类型声明 +- [ ] withDefaults 对象默认值未使用工厂函数 +- [ ] 直接修改 props(而不是 emit) +- [ ] watchEffect 依赖不明确导致过度触发 + +## Python + +### Mutable Default Arguments +```python +# ❌ Bug: List shared across all calls +def add_item(item, items=[]): + items.append(item) + return items + +# ✅ Correct +def add_item(item, items=None): + if items is None: + items = [] + items.append(item) + return items +``` + +### Exception Handling +```python +# ❌ Catching everything, including KeyboardInterrupt +try: + risky_operation() +except: + pass + +# ✅ Catch specific exceptions +try: + risky_operation() +except ValueError as e: + logger.error(f"Invalid value: {e}") + raise +``` + +### Class Attributes +```python +# ❌ Shared mutable class attribute +class User: + permissions = [] # Shared across all instances! + +# ✅ Initialize in __init__ +class User: + def __init__(self): + self.permissions = [] +``` + +### Common Mistakes +- [ ] Using `is` instead of `==` for value comparison +- [ ] Forgetting `self` parameter in methods +- [ ] Modifying list while iterating +- [ ] String concatenation in loops (use join) +- [ ] Not closing files (use `with` statement) + +## Rust + +### 所有权与借用 + +```rust +// ❌ Use after move +let s = String::from("hello"); +let s2 = s; +println!("{}", s); // Error: s was moved + +// ✅ Clone if needed (but consider if clone is necessary) +let s = String::from("hello"); +let s2 = s.clone(); +println!("{}", s); // OK + +// ❌ 用 clone() 绕过借用检查器(反模式) +fn process(data: &Data) { + let owned = data.clone(); // 不必要的 clone + do_something(owned); +} + +// ✅ 正确使用借用 +fn process(data: &Data) { + do_something(data); // 传递引用 +} + +// ❌ 在结构体中存储借用(通常是坏主意) +struct Parser<'a> { + input: &'a str, // 生命周期复杂化 + position: usize, +} + +// ✅ 使用拥有的数据 +struct Parser { + input: String, // 拥有数据,简化生命周期 + position: usize, +} + +// ❌ 迭代时修改集合 +let mut vec = vec![1, 2, 3]; +for item in &vec { + vec.push(*item); // Error: cannot borrow as mutable +} + +// ✅ 收集到新集合 +let vec = vec![1, 2, 3]; +let new_vec: Vec<_> = vec.iter().map(|x| x * 2).collect(); +``` + +### Unsafe 代码审查 + +```rust +// ❌ unsafe 没有安全注释 +unsafe { + ptr::write(dest, value); +} + +// ✅ 必须有 SAFETY 注释说明不变量 +// SAFETY: dest 指针由 Vec::as_mut_ptr() 获得,保证: +// 1. 指针有效且已对齐 +// 2. 目标内存未被其他引用借用 +// 3. 写入不会超出分配的容量 +unsafe { + ptr::write(dest, value); +} + +// ❌ unsafe fn 没有 # Safety 文档 +pub unsafe fn from_raw_parts(ptr: *mut T, len: usize) -> Self { ... } + +// ✅ 必须文档化安全契约 +/// Creates a new instance from raw parts. +/// +/// # Safety +/// +/// - `ptr` must have been allocated via `GlobalAlloc` +/// - `len` must be less than or equal to the allocated capacity +/// - The caller must ensure no other references to the memory exist +pub unsafe fn from_raw_parts(ptr: *mut T, len: usize) -> Self { ... } + +// ❌ 跨模块 unsafe 不变量 +mod a { + pub fn set_flag() { FLAG = true; } // 安全代码影响 unsafe +} +mod b { + pub unsafe fn do_thing() { + if FLAG { /* assumes FLAG means something */ } + } +} + +// ✅ 将 unsafe 边界封装在单一模块 +mod safe_wrapper { + // 所有 unsafe 逻辑在一个模块内 + // 对外提供 safe API +} +``` + +### 异步/并发 + +```rust +// ❌ 在异步上下文中阻塞 +async fn bad_fetch(url: &str) -> Result { + let resp = reqwest::blocking::get(url)?; // 阻塞整个运行时! + Ok(resp.text()?) +} + +// ✅ 使用异步版本 +async fn good_fetch(url: &str) -> Result { + let resp = reqwest::get(url).await?; + Ok(resp.text().await?) +} + +// ❌ 跨 .await 持有 Mutex +async fn bad_lock(mutex: &Mutex) { + let guard = mutex.lock().unwrap(); + some_async_op().await; // 持锁跨越 await! + drop(guard); +} + +// ✅ 缩短锁持有时间 +async fn good_lock(mutex: &Mutex) { + let data = { + let guard = mutex.lock().unwrap(); + guard.clone() // 获取数据后立即释放锁 + }; + some_async_op().await; + // 处理 data +} + +// ❌ 在异步函数中使用 std::sync::Mutex +async fn bad_async_mutex(mutex: &std::sync::Mutex) { + let _guard = mutex.lock().unwrap(); // 可能死锁 + tokio::time::sleep(Duration::from_secs(1)).await; +} + +// ✅ 使用 tokio::sync::Mutex(如果必须跨 await) +async fn good_async_mutex(mutex: &tokio::sync::Mutex) { + let _guard = mutex.lock().await; + tokio::time::sleep(Duration::from_secs(1)).await; +} + +// ❌ 忘记 Future 是惰性的 +fn bad_spawn() { + let future = async_operation(); // 没有执行! + // future 被丢弃,什么都没发生 +} + +// ✅ 必须 await 或 spawn +async fn good_spawn() { + async_operation().await; // 执行 + // 或 + tokio::spawn(async_operation()); // 后台执行 +} + +// ❌ spawn 任务缺少 'static +async fn bad_spawn_lifetime(data: &str) { + tokio::spawn(async { + println!("{}", data); // Error: data 不是 'static + }); +} + +// ✅ 使用 move 或 Arc +async fn good_spawn_lifetime(data: String) { + tokio::spawn(async move { + println!("{}", data); // OK: 拥有数据 + }); +} +``` + +### 错误处理 + +```rust +// ❌ 生产代码中使用 unwrap/expect +fn bad_parse(input: &str) -> i32 { + input.parse().unwrap() // panic! +} + +// ✅ 正确传播错误 +fn good_parse(input: &str) -> Result { + input.parse() +} + +// ❌ 吞掉错误信息 +fn bad_error_handling() -> Result<()> { + match operation() { + Ok(v) => Ok(v), + Err(_) => Err(anyhow!("operation failed")) // 丢失原始错误 + } +} + +// ✅ 使用 context 添加上下文 +fn good_error_handling() -> Result<()> { + operation().context("failed to perform operation")?; + Ok(()) +} + +// ❌ 库代码使用 anyhow(应该用 thiserror) +// lib.rs +pub fn parse_config(path: &str) -> anyhow::Result { + // 调用者无法区分错误类型 +} + +// ✅ 库代码用 thiserror 定义错误类型 +#[derive(Debug, thiserror::Error)] +pub enum ConfigError { + #[error("failed to read config file: {0}")] + Io(#[from] std::io::Error), + #[error("invalid config format: {0}")] + Parse(#[from] serde_json::Error), +} + +pub fn parse_config(path: &str) -> Result { + // 调用者可以 match 不同错误 +} + +// ❌ 忽略 must_use 返回值 +fn bad_ignore_result() { + some_fallible_operation(); // 警告:unused Result +} + +// ✅ 显式处理或标记忽略 +fn good_handle_result() { + let _ = some_fallible_operation(); // 显式忽略 + // 或 + some_fallible_operation().ok(); // 转换为 Option +} +``` + +### 性能陷阱 + +```rust +// ❌ 不必要的 collect +fn bad_process(items: &[i32]) -> i32 { + items.iter() + .filter(|x| **x > 0) + .collect::>() // 不必要的分配 + .iter() + .sum() +} + +// ✅ 惰性迭代 +fn good_process(items: &[i32]) -> i32 { + items.iter() + .filter(|x| **x > 0) + .sum() +} + +// ❌ 循环中重复分配 +fn bad_loop() -> String { + let mut result = String::new(); + for i in 0..1000 { + result = result + &i.to_string(); // 每次迭代都重新分配! + } + result +} + +// ✅ 预分配或使用 push_str +fn good_loop() -> String { + let mut result = String::with_capacity(4000); // 预分配 + for i in 0..1000 { + write!(result, "{}", i).unwrap(); // 原地追加 + } + result +} + +// ❌ 过度使用 clone +fn bad_clone(data: &HashMap>) -> Vec { + data.get("key").cloned().unwrap_or_default() +} + +// ✅ 返回引用或使用 Cow +fn good_ref(data: &HashMap>) -> &[u8] { + data.get("key").map(|v| v.as_slice()).unwrap_or(&[]) +} + +// ❌ 大结构体按值传递 +fn bad_pass(data: LargeStruct) { ... } // 拷贝整个结构体 + +// ✅ 传递引用 +fn good_pass(data: &LargeStruct) { ... } + +// ❌ Box 用于小型已知类型 +fn bad_trait_object() -> Box> { + Box::new(vec![1, 2, 3].into_iter()) +} + +// ✅ 使用 impl Trait +fn good_impl_trait() -> impl Iterator { + vec![1, 2, 3].into_iter() +} + +// ❌ retain 比 filter+collect 慢(某些场景) +vec.retain(|x| x.is_valid()); // O(n) 但常数因子大 + +// ✅ 如果不需要原地修改,考虑 filter +let vec: Vec<_> = vec.into_iter().filter(|x| x.is_valid()).collect(); +``` + +### 生命周期与引用 + +```rust +// ❌ 返回局部变量的引用 +fn bad_return_ref() -> &str { + let s = String::from("hello"); + &s // Error: s will be dropped +} + +// ✅ 返回拥有的数据或静态引用 +fn good_return_owned() -> String { + String::from("hello") +} + +// ❌ 生命周期过度泛化 +fn bad_lifetime<'a, 'b>(x: &'a str, y: &'b str) -> &'a str { + x // 'b 没有被使用 +} + +// ✅ 简化生命周期 +fn good_lifetime(x: &str, _y: &str) -> &str { + x // 编译器自动推断 +} + +// ❌ 结构体持有多个相关引用但生命周期独立 +struct Bad<'a, 'b> { + name: &'a str, + data: &'b [u8], // 通常应该是同一个生命周期 +} + +// ✅ 相关数据使用相同生命周期 +struct Good<'a> { + name: &'a str, + data: &'a [u8], +} +``` + +### Rust 审查清单 + +**所有权与借用** +- [ ] clone() 是有意为之,不是绕过借用检查器 +- [ ] 避免在结构体中存储借用(除非必要) +- [ ] Rc/Arc 使用合理,没有隐藏不必要的共享状态 +- [ ] 没有不必要的 RefCell(运行时检查 vs 编译时) + +**Unsafe 代码** +- [ ] 每个 unsafe 块有 SAFETY 注释 +- [ ] unsafe fn 有 # Safety 文档 +- [ ] 安全不变量被清晰记录 +- [ ] unsafe 边界尽可能小 + +**异步/并发** +- [ ] 没有在异步上下文中阻塞 +- [ ] 没有跨 .await 持有 std::sync 锁 +- [ ] spawn 的任务满足 'static 约束 +- [ ] Future 被正确 await 或 spawn +- [ ] 锁的顺序一致(避免死锁) + +**错误处理** +- [ ] 库代码使用 thiserror,应用代码使用 anyhow +- [ ] 错误有足够的上下文信息 +- [ ] 没有在生产代码中 unwrap/expect +- [ ] must_use 返回值被正确处理 + +**性能** +- [ ] 避免不必要的 collect() +- [ ] 大数据结构传引用 +- [ ] 字符串拼接使用 String::with_capacity 或 write! +- [ ] impl Trait 优于 Box(当可能时) + +**类型系统** +- [ ] 善用 newtype 模式增加类型安全 +- [ ] 枚举穷尽匹配(没有 _ 通配符隐藏新变体) +- [ ] 生命周期尽可能简化 + +## SQL + +### Injection Vulnerabilities +```sql +-- ❌ String concatenation (SQL injection risk) +query = "SELECT * FROM users WHERE id = " + user_id + +-- ✅ Parameterized queries +query = "SELECT * FROM users WHERE id = ?" +cursor.execute(query, (user_id,)) +``` + +### Performance Issues +- [ ] Missing indexes on filtered/joined columns +- [ ] SELECT * instead of specific columns +- [ ] N+1 query patterns +- [ ] Missing LIMIT on large tables +- [ ] Inefficient subqueries vs JOINs + +### Common Mistakes +- [ ] Not handling NULL comparisons correctly +- [ ] Missing transactions for related operations +- [ ] Incorrect JOIN types +- [ ] Case sensitivity issues +- [ ] Date/timezone handling errors + +## API Design + +### REST Issues +- [ ] Inconsistent resource naming +- [ ] Wrong HTTP methods (POST for idempotent operations) +- [ ] Missing pagination for list endpoints +- [ ] Incorrect status codes +- [ ] Missing rate limiting + +### Data Validation +- [ ] Missing input validation +- [ ] Incorrect data type validation +- [ ] Missing length/range checks +- [ ] Not sanitizing user input +- [ ] Trusting client-side validation + +## Testing + +### Test Quality Issues +- [ ] Testing implementation details instead of behavior +- [ ] Missing edge case tests +- [ ] Flaky tests (non-deterministic) +- [ ] Tests with external dependencies +- [ ] Missing negative tests (error cases) +- [ ] Overly complex test setup diff --git a/skills/code-review-excellence/reference/cpp.md b/skills/code-review-excellence/reference/cpp.md new file mode 100644 index 0000000..58743f6 --- /dev/null +++ b/skills/code-review-excellence/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/skills/code-review-excellence/reference/css-less-sass.md b/skills/code-review-excellence/reference/css-less-sass.md new file mode 100644 index 0000000..638854a --- /dev/null +++ b/skills/code-review-excellence/reference/css-less-sass.md @@ -0,0 +1,656 @@ +# 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 +/* 变量 - 用于单个值 */ +$primary-color: #3b82f6; + +/* Mixin - 用于可配置的代码块 */ +@mixin button-variant($bg, $text) { + background: $bg; + color: $text; + &:hover { + background: darken($bg, 10%); + } +} + +/* Extend - 用于共享相同样式(谨慎使用) */ +%visually-hidden { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); +} + +.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/skills/code-review-excellence/reference/go.md b/skills/code-review-excellence/reference/go.md new file mode 100644 index 0000000..2438fea --- /dev/null +++ b/skills/code-review-excellence/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/skills/code-review-excellence/reference/java.md b/skills/code-review-excellence/reference/java.md new file mode 100644 index 0000000..2e4e177 --- /dev/null +++ b/skills/code-review-excellence/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 在多线程环境可能死循环或数据丢失 +// ✅ 使用 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/skills/code-review-excellence/reference/performance-review-guide.md b/skills/code-review-excellence/reference/performance-review-guide.md new file mode 100644 index 0000000..87a8ba7 --- /dev/null +++ b/skills/code-review-excellence/reference/performance-review-guide.md @@ -0,0 +1,752 @@ +# 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 监控 | + +--- + +## 参考资源 + +- [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/skills/code-review-excellence/reference/python.md b/skills/code-review-excellence/reference/python.md new file mode 100644 index 0000000..764db0e --- /dev/null +++ b/skills/code-review-excellence/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/skills/code-review-excellence/reference/qt.md b/skills/code-review-excellence/reference/qt.md new file mode 100644 index 0000000..24fef2b --- /dev/null +++ b/skills/code-review-excellence/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/skills/code-review-excellence/reference/react.md b/skills/code-review-excellence/reference/react.md new file mode 100644 index 0000000..1cce2fc --- /dev/null +++ b/skills/code-review-excellence/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/skills/code-review-excellence/reference/rust.md b/skills/code-review-excellence/reference/rust.md new file mode 100644 index 0000000..1fa062c --- /dev/null +++ b/skills/code-review-excellence/reference/rust.md @@ -0,0 +1,840 @@ +# 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! { + // 如果 timeout 先完成,read 被取消 + // 部分读取的数据可能丢失! + result = stream.read(&mut buffer) => { + handle_data(&buffer[..result?]); + } + _ = 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! { + // tokio::io::AsyncReadExt::read 是取消安全的 + // 取消时,未读取的数据留在流中 + 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/skills/code-review-excellence/reference/security-review-guide.md b/skills/code-review-excellence/reference/security-review-guide.md new file mode 100644 index 0000000..80d10bc --- /dev/null +++ b/skills/code-review-excellence/reference/security-review-guide.md @@ -0,0 +1,265 @@ +# 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 filePath = path.join('./uploads', safeName); + +// Verify it's still within uploads directory +if (!filePath.startsWith(path.resolve('./uploads'))) { + 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/skills/code-review-excellence/reference/typescript.md b/skills/code-review-excellence/reference/typescript.md new file mode 100644 index 0000000..4699f6b --- /dev/null +++ b/skills/code-review-excellence/reference/typescript.md @@ -0,0 +1,543 @@ +# 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 +// .eslintrc.js +module.exports = { + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'plugin:@typescript-eslint/strict' + ], + 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/skills/code-review-excellence/reference/vue.md b/skills/code-review-excellence/reference/vue.md new file mode 100644 index 0000000..cbd9165 --- /dev/null +++ b/skills/code-review-excellence/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/skills/code-review-excellence/scripts/pr-analyzer.py b/skills/code-review-excellence/scripts/pr-analyzer.py new file mode 100644 index 0000000..7b594e7 --- /dev/null +++ b/skills/code-review-excellence/scripts/pr-analyzer.py @@ -0,0 +1,366 @@ +#!/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 sys +import re +import argparse +from collections import defaultdict +from dataclasses import dataclass +from typing import List, Dict, Optional + + +@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.""" + 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', + '.rb': 'Ruby', + '.sql': 'SQL', + '.md': 'Markdown', + '.json': 'JSON', + '.yaml': 'YAML', + '.yml': 'YAML', + '.toml': 'TOML', + '.css': 'CSS', + '.scss': 'SCSS', + '.html': 'HTML', + } + for ext, lang in extensions.items(): + if filename.endswith(ext): + return lang + return '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) + # Extract filename from "diff --git a/path b/path" + 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), + ) + 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) + + # Large PR + if total_changes > 400: + risks.append("Large PR (>400 lines) - harder to review thoroughly") + + # No tests + if test_changes == 0 and total_changes > 50: + risks.append("No test changes - verify test coverage") + + # Low test ratio + 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 "No test changes" in str(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 + if args.diff_file: + with open(args.diff_file, 'r') as f: + diff_content = f.read() + elif not sys.stdin.isatty(): + diff_content = sys.stdin.read() + else: + print("Usage: git diff main...HEAD | python pr-analyzer.py") + print(" python pr-analyzer.py -f diff.txt") + 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/skills/pr-reviewer/SKILL.md b/skills/pr-reviewer/SKILL.md new file mode 100644 index 0000000..d33e96b --- /dev/null +++ b/skills/pr-reviewer/SKILL.md @@ -0,0 +1,194 @@ +--- +name: pr-reviewer +description: > + Structured PR code review workflow. Use when asked to "review this PR", + "code review", "review pull request", or "check this PR". Works with both + GitHub and Gitea via platform-aware scripts. Fetches PR metadata and diff, + analyzes against quality criteria, generates structured review files, and + posts feedback only after explicit approval. +version: 2.0.0 +category: code-review +triggers: + - review pr + - code review + - review pull request + - check pr + - pr review +author: Adapted from SpillwaveSolutions/pr-reviewer-skill +license: MIT +tags: + - code-review + - pull-request + - quality-assurance + - gitea + - github +--- + +# PR Reviewer + +Structured, two-stage PR code review workflow. Nothing is posted until explicit user approval. + +## Prerequisites + +- `~/.claude/scripts/git/` — Platform-aware git scripts must be available +- `python3` — For review file generation +- Current working directory must be inside the target git repository + +## Workflow + +### Stage 1: Collect and Analyze + +#### 1. Gather PR Data + +Run from inside the repo directory: + +```bash +# Get PR metadata as JSON +~/.claude/scripts/git/pr-metadata.sh -n -o /tmp/pr-review/metadata.json + +# Get PR diff +~/.claude/scripts/git/pr-diff.sh -n -o /tmp/pr-review/diff.patch + +# View PR details (human-readable) +~/.claude/scripts/git/pr-view.sh -n +``` + +#### 2. Analyze the Changes + +With the diff and metadata collected, analyze the PR against these criteria: + +| Category | Key Questions | +|----------|--------------| +| **Functionality** | Does the code solve the stated problem? Edge cases handled? | +| **Security** | OWASP top 10? Secrets in code? Input validation? | +| **Testing** | Tests exist? Cover happy paths and edge cases? | +| **Performance** | Efficient algorithms? N+1 queries? Bundle size impact? | +| **Readability** | Clear naming? DRY? Consistent with codebase patterns? | +| **Architecture** | Follows project conventions? Proper separation of concerns? | +| **PR Quality** | Focused scope? Clean commits? Clear description? | + +**Priority levels for findings:** + +- **Blocker** — Must fix before merge +- **Important** — Should be addressed +- **Nit** — Nice to have, optional +- **Suggestion** — Consider for future +- **Question** — Clarification needed +- **Praise** — Good work worth calling out + +#### 3. Generate Review Files + +Create a findings JSON and run the generator: + +```bash +python3 ~/.claude/skills/pr-reviewer/scripts/generate_review_files.py \ + /tmp/pr-review --findings /tmp/pr-review/findings.json +``` + +This creates: +- `pr/review.md` — Detailed analysis (internal use) +- `pr/human.md` — Clean version for posting (no emojis, concise) +- `pr/inline.md` — Proposed inline comments with code context + +**Findings JSON schema:** + +```json +{ + "summary": "Overall assessment of the PR", + "metadata": { + "repository": "owner/repo", + "number": 123, + "title": "PR title", + "author": "username", + "head_branch": "feature-branch", + "base_branch": "main" + }, + "blockers": [ + { + "category": "Security", + "issue": "Brief description", + "file": "src/path/file.ts", + "line": 45, + "details": "Explanation of the problem", + "fix": "Suggested solution", + "code_snippet": "relevant code" + } + ], + "important": [], + "nits": [], + "suggestions": ["Consider adding..."], + "questions": ["Is this intended to..."], + "praise": ["Excellent test coverage"], + "inline_comments": [ + { + "file": "src/path/file.ts", + "line": 42, + "comment": "Consider edge case", + "code_snippet": "relevant code", + "start_line": 40, + "end_line": 44 + } + ] +} +``` + +### Stage 2: Review and Post + +#### 4. Present to User + +Show the user: +1. The summary from `pr/review.md` +2. Count of blockers, important issues, nits +3. Overall recommendation (approve vs request changes) +4. Ask for approval before posting + +**NOTHING is posted without explicit user consent.** + +#### 5. Post the Review + +After user approval: + +```bash +# Option A: Approve +~/.claude/scripts/git/pr-review.sh -n -a approve -c "$(cat /tmp/pr-review/pr/human.md)" + +# Option B: Request changes +~/.claude/scripts/git/pr-review.sh -n -a request-changes -c "$(cat /tmp/pr-review/pr/human.md)" + +# Option C: Comment only (no verdict) +~/.claude/scripts/git/pr-review.sh -n -a comment -c "$(cat /tmp/pr-review/pr/human.md)" +``` + +## Review Criteria Reference + +See `references/review_criteria.md` for the complete checklist. + +### Quick Checklist + +- [ ] Code compiles and passes CI +- [ ] Tests cover new functionality +- [ ] No hardcoded secrets or credentials +- [ ] Error handling is appropriate +- [ ] No obvious performance issues (N+1, unnecessary re-renders, large bundles) +- [ ] Follows project CLAUDE.md and AGENTS.md conventions +- [ ] Commit messages follow project format +- [ ] PR description explains the "why" + +## Best Practices + +### Communication +- Frame feedback as suggestions, not demands +- Explain WHY something matters, not just WHAT is wrong +- Acknowledge good work — praise is part of review +- Prioritize: blockers first, nits last + +### Efficiency +- Focus on what automated tools can't catch (logic, architecture, intent) +- Don't nitpick formatting — that's the linter's job +- For large PRs (>400 lines), suggest splitting +- Review in logical chunks (API layer, then UI, then tests) + +### Platform Notes +- **Gitea**: Inline code comments are posted as regular PR comments with file/line references in the text +- **GitHub**: Full inline comment API available +- Both platforms support approve/reject/comment review actions via our scripts diff --git a/skills/pr-reviewer/references/gh_cli_guide.md b/skills/pr-reviewer/references/gh_cli_guide.md new file mode 100644 index 0000000..1287d69 --- /dev/null +++ b/skills/pr-reviewer/references/gh_cli_guide.md @@ -0,0 +1,368 @@ +# GitHub CLI (gh) Guide for PR Reviews + +This reference provides quick commands and patterns for accessing PR data using the GitHub CLI. + +## Prerequisites + +Install GitHub CLI: https://cli.github.com/ + +Authenticate: +```bash +gh auth login +``` + +## Basic PR Information + +### View PR Details +```bash +gh pr view --repo / + +# With JSON output +gh pr view --repo / --json number,title,body,state,author,headRefName,baseRefName +``` + +### View PR Diff +```bash +gh pr diff --repo / + +# Save to file +gh pr diff --repo / > pr_diff.patch +``` + +### List PR Files +```bash +gh pr view --repo / --json files --jq '.files[].path' +``` + +## PR Comments and Reviews + +### Get PR Comments (Review Comments on Code) +```bash +gh api /repos///pulls//comments + +# Paginate through all comments +gh api /repos///pulls//comments --paginate + +# With JQ filtering +gh api /repos///pulls//comments --jq '.[] | {path, line, body, user: .user.login}' +``` + +### Get PR Reviews +```bash +gh api /repos///pulls//reviews + +# With formatted output +gh api /repos///pulls//reviews --jq '.[] | {state, user: .user.login, body}' +``` + +### Get Issue Comments (General PR Comments) +```bash +gh api /repos///issues//comments +``` + +## Commit Information + +### List PR Commits +```bash +gh api /repos///pulls//commits + +# Get commit messages +gh api /repos///pulls//commits --jq '.[] | {sha: .sha[0:7], message: .commit.message}' + +# Get latest commit SHA +gh api /repos///pulls//commits --jq '.[-1].sha' +``` + +### Get Commit Details +```bash +gh api /repos///commits/ + +# Get commit diff +gh api /repos///commits/ -H "Accept: application/vnd.github.diff" +``` + +## Branches + +### Get Branch Information +```bash +# Source branch (head) +gh pr view --repo / --json headRefName --jq '.headRefName' + +# Target branch (base) +gh pr view --repo / --json baseRefName --jq '.baseRefName' +``` + +### Compare Branches +```bash +gh api /repos///compare/... + +# Get files changed +gh api /repos///compare/... --jq '.files[] | {filename, status, additions, deletions}' +``` + +## Related Issues and Tickets + +### Get Linked Issues +```bash +# Get PR body which may contain issue references +gh pr view --repo / --json body --jq '.body' + +# Search for issue references (#123 format) +gh pr view --repo / --json body --jq '.body' | grep -oE '#[0-9]+' +``` + +### Get Issue Details +```bash +gh issue view --repo / + +# JSON format +gh issue view --repo / --json number,title,body,state,labels,assignees +``` + +### Get Issue Comments +```bash +gh api /repos///issues//comments +``` + +## PR Status Checks + +### Get PR Status +```bash +gh pr checks --repo / + +# JSON format +gh api /repos///commits//status +``` + +### Get Check Runs +```bash +gh api /repos///commits//check-runs +``` + +## Adding Comments + +### Add Inline Code Comment +```bash +gh api -X POST /repos///pulls//comments \ + -f body="Your comment here" \ + -f commit_id="" \ + -f path="src/file.py" \ + -f side="RIGHT" \ + -f line=42 +``` + +### Add Multi-line Inline Comment +```bash +gh api -X POST /repos///pulls//comments \ + -f body="Multi-line comment" \ + -f commit_id="" \ + -f path="src/file.py" \ + -f side="RIGHT" \ + -f start_line=40 \ + -f start_side="RIGHT" \ + -f line=45 +``` + +### Add General PR Comment +```bash +gh pr comment --repo / --body "Your comment" + +# Or via API +gh api -X POST /repos///issues//comments \ + -f body="Your comment" +``` + +## Creating a Review + +### Create Review with Comments +```bash +gh api -X POST /repos///pulls//reviews \ + -f body="Overall review comments" \ + -f event="COMMENT" \ + -f commit_id="" \ + -f comments='[{"path":"src/file.py","line":42,"body":"Comment on line 42"}]' +``` + +### Submit Review (Approve/Request Changes) +```bash +# Approve +gh api -X POST /repos///pulls//reviews \ + -f body="LGTM!" \ + -f event="APPROVE" \ + -f commit_id="" + +# Request changes +gh api -X POST /repos///pulls//reviews \ + -f body="Please address these issues" \ + -f event="REQUEST_CHANGES" \ + -f commit_id="" +``` + +## Searching and Filtering + +### Search Code in PR +```bash +# Get PR diff and search +gh pr diff --repo / | grep "search_term" + +# Search in specific files +gh pr view --repo / --json files --jq '.files[] | select(.path | contains("search_term"))' +``` + +### Filter by File Type +```bash +gh pr view --repo / --json files --jq '.files[] | select(.path | endswith(".py"))' +``` + +## Labels, Assignees, and Metadata + +### Get Labels +```bash +gh pr view --repo / --json labels --jq '.labels[].name' +``` + +### Get Assignees +```bash +gh pr view --repo / --json assignees --jq '.assignees[].login' +``` + +### Get Reviewers +```bash +gh pr view --repo / --json reviewRequests --jq '.reviewRequests[].login' +``` + +## Advanced Queries + +### Get PR Timeline +```bash +gh api /repos///issues//timeline +``` + +### Get PR Events +```bash +gh api /repos///issues//events +``` + +### Get All PR Data +```bash +gh pr view --repo / --json \ + number,title,body,state,author,headRefName,baseRefName,\ + commits,reviews,comments,files,labels,assignees,milestone,\ + createdAt,updatedAt,mergedAt,closedAt,url,isDraft +``` + +## Common JQ Patterns + +### Extract specific fields +```bash +--jq '.field' +--jq '.array[].field' +--jq '.[] | {field1, field2}' +``` + +### Filter arrays +```bash +--jq '.[] | select(.field == "value")' +--jq '.[] | select(.field | contains("substring"))' +``` + +### Count items +```bash +--jq '. | length' +--jq '.array | length' +``` + +### Map and transform +```bash +--jq '.array | map(.field)' +--jq '.[] | {newField: .oldField}' +``` + +## Line Number Considerations for Inline Comments + +**IMPORTANT**: The `line` parameter for inline comments refers to the **line number in the diff**, not the absolute line number in the file. + +### Understanding Diff Line Numbers + +In a diff: +- Lines are numbered relative to the diff context, not the file +- The `side` parameter determines which version: + - `"RIGHT"`: New version (after changes) + - `"LEFT"`: Old version (before changes) + +### Finding Diff Line Numbers + +```bash +# Get diff with line numbers +gh pr diff --repo / | cat -n + +# Get specific file diff +gh api /repos///pulls//files --jq '.[] | select(.filename == "path/to/file")' +``` + +### Example Diff +```diff +@@ -10,7 +10,8 @@ def process_data(data): + if not data: + return None + +- result = old_function(data) ++ # New implementation ++ result = new_function(data) + return result +``` + +In this diff: +- Line 13 (old) would be `side: "LEFT"` +- Line 14-15 (new) would be `side: "RIGHT"` +- Line numbers are relative to the diff hunk starting at line 10 + +## Error Handling + +### Common Errors + +**Resource not found**: +```bash +# Check repo access +gh repo view / + +# Check PR exists +gh pr list --repo / | grep +``` + +**API rate limit**: +```bash +# Check rate limit +gh api /rate_limit + +# Use authentication to get higher limits +gh auth login +``` + +**Permission denied**: +```bash +# Check authentication +gh auth status + +# May need additional scopes +gh auth refresh -s repo +``` + +## Tips and Best Practices + +1. **Use `--paginate`** for large result sets (comments, commits) +2. **Combine with `jq`** for powerful filtering and formatting +3. **Cache results** by saving to files to avoid repeated API calls +4. **Check rate limits** when making many API calls +5. **Use `--json` output** for programmatic parsing +6. **Specify `--repo`** when outside repository directory +7. **Get latest commit** before adding inline comments +8. **Test comments** on draft PRs or test repositories first + +## Reference Links + +- GitHub CLI Manual: https://cli.github.com/manual/ +- GitHub REST API: https://docs.github.com/en/rest +- JQ Manual: https://jqlang.github.io/jq/manual/ +- PR Review Comments API: https://docs.github.com/en/rest/pulls/comments +- PR Reviews API: https://docs.github.com/en/rest/pulls/reviews diff --git a/skills/pr-reviewer/references/review_criteria.md b/skills/pr-reviewer/references/review_criteria.md new file mode 100644 index 0000000..0c48352 --- /dev/null +++ b/skills/pr-reviewer/references/review_criteria.md @@ -0,0 +1,345 @@ +# Code Review Criteria + +This document outlines the comprehensive criteria for conducting pull request code reviews. Use this as a checklist when reviewing PRs to ensure thorough, consistent, and constructive feedback. + +## Review Process Overview + +When reviewing a PR, the goal is to ensure changes are: +- **Correct**: Solves the intended problem without bugs +- **Maintainable**: Easy to understand and modify +- **Aligned**: Follows project standards and conventions +- **Secure**: Free from vulnerabilities +- **Tested**: Covered by appropriate tests + +## 1. Functionality and Correctness + +### Problem Resolution +- [ ] **Does the code solve the intended problem?** + - Verify changes address the issue or feature described in the PR + - Cross-reference with linked tickets (JIRA, GitHub issues) + - Test manually or run the code if possible + +### Bugs and Logic +- [ ] **Are there bugs or logical errors?** + - Check for off-by-one errors + - Verify null/undefined/None handling + - Review assumptions about inputs and outputs + - Look for race conditions or concurrency issues + - Check loop termination conditions + +### Edge Cases and Error Handling +- [ ] **Edge cases handled?** + - Empty collections (arrays, lists, maps) + - Null/None/undefined values + - Boundary values (min/max integers, empty strings) + - Invalid or malformed inputs + +- [ ] **Error handling implemented?** + - Network failures + - File system errors + - Database connection issues + - API errors and timeouts + - Graceful degradation + +### Compatibility +- [ ] **Works across supported environments?** + - Browser compatibility (if web app) + - OS versions (if desktop/mobile) + - Database versions + - Language/runtime versions + - Doesn't break existing features (regression check) + +## 2. Readability and Maintainability + +### Code Clarity +- [ ] **Easy to read and understand?** + - Meaningful variable names (avoid `x`, `temp`, `data`) + - Meaningful function names (verb-first, descriptive) + - Short methods/functions (ideally < 50 lines) + - Logical structure and flow + - Minimal nested complexity + +### Modularity +- [ ] **Single Responsibility Principle?** + - Functions/methods do one thing well + - Classes have a clear, focused purpose + - No "god objects" or overly complex logic + +- [ ] **Suggest refactoring if needed:** + - Extract complex logic into helper functions + - Break large functions into smaller ones + - Separate concerns (UI, business logic, data access) + +### Code Duplication +- [ ] **DRY (Don't Repeat Yourself)?** + - Repeated code abstracted into helpers + - Shared logic moved to libraries/utilities + - Avoid copy-paste programming + +### Future-Proofing +- [ ] **Allows for easy extensions?** + - Avoid hard-coded values (use constants/configs) + - Use dependency injection where appropriate + - Follow SOLID principles + - Consider extensibility without modification + +## 3. Style and Conventions + +### Style Guide Adherence +- [ ] **Follows project linter rules?** + - ESLint (JavaScript/TypeScript) + - Pylint/Flake8/Black (Python) + - RuboCop (Ruby) + - Checkstyle/PMD (Java) + - golangci-lint (Go) + +- [ ] **Formatting consistent?** + - Proper indentation (spaces vs. tabs) + - Consistent spacing + - Line length limits + - Import/require organization + +### Codebase Consistency +- [ ] **Matches existing patterns?** + - Follows established architectural patterns + - Uses existing utilities and helpers + - Consistent naming conventions + - Matches idioms of the language/framework + +### Comments and Documentation +- [ ] **Sufficient comments?** + - Complex algorithms explained + - Non-obvious decisions documented + - API contracts clarified + - TODOs tracked with ticket numbers + +- [ ] **Not excessive?** + - Code should be self-documenting where possible + - Avoid obvious comments ("increment i") + +- [ ] **Documentation updated?** + - README reflects new features + - API docs updated + - Inline docs (JSDoc, docstrings, etc.) + - Architecture diagrams current + +## 4. Performance and Efficiency + +### Resource Usage +- [ ] **Algorithm efficiency?** + - Avoid O(n²) or worse in loops + - Use appropriate data structures + - Minimize database queries (N+1 problem) + - Avoid unnecessary computations + +### Scalability +- [ ] **Performs well under load?** + - No blocking operations in critical paths + - Async/await for I/O operations + - Pagination for large datasets + - Caching where appropriate + +### Optimization Balance +- [ ] **Optimizations necessary?** + - Premature optimization avoided + - Readability not sacrificed for micro-optimizations + - Benchmark before complex optimizations + - Profile to identify actual bottlenecks + +## 5. Security and Best Practices + +### Vulnerabilities +- [ ] **Common security issues addressed?** + - SQL injection (use parameterized queries) + - XSS (Cross-Site Scripting) - proper escaping + - CSRF (Cross-Site Request Forgery) - tokens + - Command injection + - Path traversal + - Authentication/authorization checks + +### Data Handling +- [ ] **Sensitive data protected?** + - Encrypted in transit (HTTPS/TLS) + - Encrypted at rest + - Input validation and sanitization + - Output encoding + - PII handling compliance (GDPR, etc.) + +- [ ] **Secrets management?** + - No hardcoded passwords/API keys + - Use environment variables + - Use secret management systems + - No secrets in logs + +### Dependencies +- [ ] **New packages justified?** + - Actually necessary + - From trusted sources + - Up-to-date and maintained + - No known vulnerabilities + - License compatible + +- [ ] **Dependency management?** + - Lock files committed + - Minimal dependency footprint + - Consider alternatives if bloated + +## 6. Testing and Quality Assurance + +### Test Coverage +- [ ] **Tests exist for new code?** + - Unit tests for individual functions/methods + - Integration tests for workflows + - End-to-end tests for critical paths + +- [ ] **Tests cover scenarios?** + - Happy paths + - Error conditions + - Edge cases + - Boundary conditions + +### Test Quality +- [ ] **Tests are meaningful?** + - Not just for coverage metrics + - Assert actual behavior + - Test intent, not implementation + - Avoid brittle tests + +- [ ] **Test maintainability?** + - Clear test names + - Arrange-Act-Assert pattern + - Minimal test duplication + - Fast execution + +### CI/CD Integration +- [ ] **Automated checks pass?** + - Linting + - Tests (unit, integration, e2e) + - Build process + - Security scans + - Code coverage thresholds + +## 7. Overall PR Quality + +### Scope +- [ ] **PR is focused?** + - Single feature/fix per PR + - Not too large (< 400 lines ideal) + - Suggest splitting if combines unrelated changes + +### Commit History +- [ ] **Clean, atomic commits?** + - Each commit is logical unit + - Descriptive commit messages + - Follow conventional commits if applicable + - Avoid "fix", "update", "wip" vagueness + +### PR Description +- [ ] **Clear description?** + - Explains **why** changes were made + - Links to tickets/issues + - Steps to reproduce/test + - Screenshots for UI changes + - Breaking changes called out + - Migration steps if needed + +### Impact Assessment +- [ ] **Considered downstream effects?** + - API changes (breaking vs. backward-compatible) + - Database schema changes + - Impact on other teams/services + - Performance implications + - Monitoring and alerting needs + +## Review Feedback Guidelines + +### Communication Style +- **Be constructive and kind** + - Frame as suggestions: "Consider X because Y" + - Not criticism: "This is wrong" + - Acknowledge good work + - Explain the "why" behind feedback + +### Prioritization +- **Focus on critical issues first:** + 1. Bugs and correctness + 2. Security vulnerabilities + 3. Performance problems + 4. Design/architecture issues + 5. Style and conventions + +### Feedback Markers +Use clear markers to indicate severity: +- **🔴 Blocker**: Must be fixed before merge +- **🟡 Important**: Should be addressed +- **🟢 Nit**: Nice to have, optional +- **💡 Suggestion**: Consider for future +- **❓ Question**: Clarification needed +- **✅ Praise**: Good work! + +### Time Efficiency +- Review promptly (within 24 hours) +- For large PRs, review in chunks +- Request smaller PRs if too large +- Use automated tools to catch style issues + +### Decision Making +- **Approve**: Solid overall, minor nits acceptable +- **Request Changes**: Blockers must be addressed +- **Comment**: Provide feedback without blocking + +## Language/Framework-Specific Considerations + +### JavaScript/TypeScript +- Type safety (TypeScript) +- Promise handling (avoid callback hell) +- Memory leaks (event listeners) +- Bundle size impact + +### Python +- PEP 8 compliance +- Type hints (Python 3.5+) +- Virtual environment dependencies +- Generator usage for memory efficiency + +### Java +- Memory management +- Exception handling (checked vs. unchecked) +- Thread safety +- Immutability where appropriate + +### Go +- Error handling (no exceptions) +- Goroutine management +- Channel usage +- Interface design + +### SQL/Database +- Index usage +- Query performance +- Transaction boundaries +- Migration reversibility + +### Frontend (React, Vue, Angular) +- Component reusability +- State management +- Accessibility (a11y) +- Performance (re-renders, bundle size) + +## Tools and Automation + +Leverage tools to automate checks: +- **Linters**: ESLint, Pylint, RuboCop +- **Formatters**: Prettier, Black, gofmt +- **Security**: Snyk, CodeQL, Dependabot +- **Coverage**: Codecov, Coveralls +- **Performance**: Lighthouse, WebPageTest +- **Accessibility**: axe, WAVE + +## Resources + +- Google Engineering Practices: https://google.github.io/eng-practices/review/ +- GitHub Code Review Guide: https://github.com/features/code-review +- OWASP Top 10: https://owasp.org/www-project-top-ten/ +- Clean Code (Robert C. Martin) +- Code Complete (Steve McConnell) diff --git a/skills/pr-reviewer/references/scenarios.md b/skills/pr-reviewer/references/scenarios.md new file mode 100644 index 0000000..b7d27dc --- /dev/null +++ b/skills/pr-reviewer/references/scenarios.md @@ -0,0 +1,71 @@ +# Common Review Scenarios + +Detailed workflows for specific review use cases. + +## Scenario 1: Quick Review Request + +**Trigger**: User provides PR URL and requests review. + +**Workflow**: +1. Run `fetch_pr_data.py` to collect data +2. Read `SUMMARY.txt` and `metadata.json` +3. Scan `diff.patch` for obvious issues +4. Apply critical criteria (security, bugs, tests) +5. Create findings JSON with analysis +6. Run `generate_review_files.py` to create review files +7. Direct user to review `pr/review.md` and `pr/human.md` +8. Remind user to use `/show` to edit, then `/send` or `/send-decline` + +## Scenario 2: Thorough Review with Inline Comments + +**Trigger**: User requests comprehensive review with inline comments. + +**Workflow**: +1. Run `fetch_pr_data.py` with cloning enabled +2. Read all collected files (metadata, diff, comments, commits) +3. Apply full `review_criteria.md` checklist +4. Identify critical issues, important issues, and nits +5. Create findings JSON with `inline_comments` array +6. Run `generate_review_files.py` to create all files +7. Direct user to: + - Review `pr/review.md` for detailed analysis + - Edit `pr/human.md` if needed + - Check `pr/inline.md` for proposed comments + - Use `/show` to open in VS Code + - Use `/send` or `/send-decline` when ready + - Optionally post inline comments from `pr/inline.md` + +## Scenario 3: Security-Focused Review + +**Trigger**: User requests security-specific review. + +**Workflow**: +1. Fetch PR data +2. Focus on `review_criteria.md` Section 5 (Security) +3. Check for: SQL injection, XSS, CSRF, secrets exposure +4. Examine dependencies in metadata +5. Review authentication/authorization changes +6. Report security findings with severity ratings + +## Scenario 4: Review with Related Tickets + +**Trigger**: User requests review against linked JIRA/GitHub ticket. + +**Workflow**: +1. Fetch PR data (captures ticket references) +2. Read `related_issues.json` +3. Compare PR changes against ticket requirements +4. Verify all acceptance criteria met +5. Note any missing functionality +6. Suggest additional tests if needed + +## Scenario 5: Large PR Review (>400 lines) + +**Trigger**: PR contains more than 400 lines of changes. + +**Workflow**: +1. Suggest splitting into smaller PRs if feasible +2. Review in logical chunks by file or feature +3. Focus on architecture and design first +4. Document structural concerns before line-level issues +5. Prioritize security and correctness over style diff --git a/skills/pr-reviewer/references/troubleshooting.md b/skills/pr-reviewer/references/troubleshooting.md new file mode 100644 index 0000000..3651926 --- /dev/null +++ b/skills/pr-reviewer/references/troubleshooting.md @@ -0,0 +1,55 @@ +# Troubleshooting Guide + +Common issues and solutions for the PR Reviewer skill. + +## gh CLI Not Found + +Install GitHub CLI: https://cli.github.com/ + +```bash +# macOS +brew install gh + +# Linux +sudo apt install gh # or yum, dnf, etc. + +# Authenticate +gh auth login +``` + +## Permission Denied Errors + +Check authentication: + +```bash +gh auth status +gh auth refresh -s repo +``` + +## Invalid PR URL + +Ensure URL format: `https://github.com/owner/repo/pull/NUMBER` + +## Line Number Mismatch in Diff + +Inline comment line numbers are **relative to the diff**, not absolute file positions. +Use `gh pr diff ` to see diff line numbers. + +## Rate Limit Errors + +```bash +# Check rate limit +gh api /rate_limit + +# Authenticated users get higher limits +gh auth login +``` + +## Common Error Patterns + +| Error | Cause | Solution | +|-------|-------|----------| +| 401 Unauthorized | Token expired | Run `gh auth refresh` | +| 403 Forbidden | Missing scope | Run `gh auth refresh -s repo` | +| 404 Not Found | Private repo access | Verify repo permissions | +| 422 Unprocessable | Invalid request | Check command arguments | diff --git a/skills/pr-reviewer/scripts/generate_review_files.py b/skills/pr-reviewer/scripts/generate_review_files.py new file mode 100755 index 0000000..6783089 --- /dev/null +++ b/skills/pr-reviewer/scripts/generate_review_files.py @@ -0,0 +1,480 @@ +#!/usr/bin/env python3 +""" +Generate structured review files from PR analysis. + +Creates three review files: +- pr/review.md: Detailed review for internal use +- pr/human.md: Short, clean review for posting (no emojis, em-dashes, line numbers) +- pr/inline.md: List of inline comments with code snippets + +Usage: + python generate_review_files.py --findings + +Example: + python generate_review_files.py /tmp/PRs/myrepo/123 --findings findings.json +""" + +import argparse +import json +import os +import sys +from pathlib import Path +from typing import Dict, List, Any + + +def create_pr_directory(pr_review_dir: Path) -> Path: + """Create the pr/ subdirectory for review files.""" + pr_dir = pr_review_dir / "pr" + pr_dir.mkdir(parents=True, exist_ok=True) + return pr_dir + + +def load_findings(findings_file: str) -> Dict[str, Any]: + """ + Load review findings from JSON file. + + Expected structure: + { + "summary": "Overall assessment...", + "blockers": [{ + "category": "Security", + "issue": "SQL injection vulnerability", + "file": "src/db/queries.py", + "line": 45, + "details": "Using string concatenation...", + "fix": "Use parameterized queries", + "code_snippet": "result = db.execute(...)" + }], + "important": [...], + "nits": [...], + "suggestions": [...], + "questions": [...], + "praise": [...], + "inline_comments": [{ + "file": "src/app.py", + "line": 42, + "comment": "Consider edge case handling", + "code_snippet": "def process(data):\n return data.strip()", + "start_line": 41, + "end_line": 43 + }] + } + """ + with open(findings_file, 'r') as f: + return json.load(f) + + +def generate_detailed_review(findings: Dict[str, Any], metadata: Dict[str, Any]) -> str: + """Generate detailed review.md with full analysis.""" + + review = f"""# Pull Request Review - Detailed Analysis + +## PR Information + +**Repository**: {metadata.get('repository', 'N/A')} +**PR Number**: #{metadata.get('number', 'N/A')} +**Title**: {metadata.get('title', 'N/A')} +**Author**: {metadata.get('author', 'N/A')} +**Branch**: {metadata.get('head_branch', 'N/A')} → {metadata.get('base_branch', 'N/A')} + +## Summary + +{findings.get('summary', 'No summary provided')} + +""" + + # Add blockers + blockers = findings.get('blockers', []) + if blockers: + review += "## 🔴 Critical Issues (Blockers)\n\n" + review += "**These MUST be fixed before merging.**\n\n" + for i, blocker in enumerate(blockers, 1): + review += f"### {i}. {blocker.get('category', 'Issue')}: {blocker.get('issue', 'Unknown')}\n\n" + if blocker.get('file'): + review += f"**File**: `{blocker['file']}" + if blocker.get('line'): + review += f":{blocker['line']}" + review += "`\n\n" + review += f"**Problem**: {blocker.get('details', 'No details')}\n\n" + if blocker.get('fix'): + review += f"**Solution**: {blocker['fix']}\n\n" + if blocker.get('code_snippet'): + review += f"**Current Code**:\n```\n{blocker['code_snippet']}\n```\n\n" + review += "---\n\n" + + # Add important issues + important = findings.get('important', []) + if important: + review += "## 🟡 Important Issues\n\n" + review += "**Should be addressed before merging.**\n\n" + for i, issue in enumerate(important, 1): + review += f"### {i}. {issue.get('category', 'Issue')}: {issue.get('issue', 'Unknown')}\n\n" + if issue.get('file'): + review += f"**File**: `{issue['file']}" + if issue.get('line'): + review += f":{issue['line']}" + review += "`\n\n" + review += f"**Impact**: {issue.get('details', 'No details')}\n\n" + if issue.get('fix'): + review += f"**Suggestion**: {issue['fix']}\n\n" + if issue.get('code_snippet'): + review += f"**Code**:\n```\n{issue['code_snippet']}\n```\n\n" + review += "---\n\n" + + # Add nits + nits = findings.get('nits', []) + if nits: + review += "## 🟢 Minor Issues (Nits)\n\n" + review += "**Nice to have, but not blocking.**\n\n" + for i, nit in enumerate(nits, 1): + review += f"{i}. **{nit.get('category', 'Style')}**: {nit.get('issue', 'Unknown')}\n" + if nit.get('file'): + review += f" - File: `{nit['file']}`\n" + if nit.get('details'): + review += f" - {nit['details']}\n" + review += "\n" + + # Add suggestions + suggestions = findings.get('suggestions', []) + if suggestions: + review += "## 💡 Suggestions for Future\n\n" + for i, suggestion in enumerate(suggestions, 1): + review += f"{i}. {suggestion}\n" + review += "\n" + + # Add questions + questions = findings.get('questions', []) + if questions: + review += "## ❓ Questions / Clarifications Needed\n\n" + for i, question in enumerate(questions, 1): + review += f"{i}. {question}\n" + review += "\n" + + # Add praise + praise = findings.get('praise', []) + if praise: + review += "## ✅ Positive Notes\n\n" + for item in praise: + review += f"- {item}\n" + review += "\n" + + # Add overall recommendation + review += "## Overall Recommendation\n\n" + if blockers: + review += "**Request Changes** - Critical issues must be addressed.\n" + elif important: + review += "**Request Changes** - Important issues should be fixed.\n" + else: + review += "**Approve** - Looks good! Minor nits can be addressed optionally.\n" + + return review + + +def generate_human_review(findings: Dict[str, Any], metadata: Dict[str, Any]) -> str: + """ + Generate short, clean human.md for posting. + + Rules: + - No emojis + - No em dashes (use regular hyphens) + - No code line numbers + - Concise and professional + """ + + def clean_text(text: str) -> str: + """Remove em-dashes and replace with regular hyphens.""" + if not text: + return text + # Replace em dash (—) with regular hyphen (-) + # Also replace en dash (–) with regular hyphen + return text.replace('—', '-').replace('–', '-') + + title = clean_text(metadata.get('title', 'N/A')) + summary = clean_text(findings.get('summary', 'No summary provided')) + + review = f"""# Code Review + +**PR #{metadata.get('number', 'N/A')}**: {title} + +## Summary + +{summary} + +""" + + # Add blockers - no emojis + blockers = findings.get('blockers', []) + if blockers: + review += "## Critical Issues - Must Fix\n\n" + for i, blocker in enumerate(blockers, 1): + # No emojis, no em dashes, no line numbers + issue = clean_text(blocker.get('issue', 'Issue')) + details = clean_text(blocker.get('details', 'No details')) + fix = clean_text(blocker.get('fix', '')) + + review += f"{i}. **{issue}**\n" + if blocker.get('file'): + # File path without line number + review += f" - File: `{blocker['file']}`\n" + review += f" - {details}\n" + if fix: + review += f" - Fix: {fix}\n" + review += "\n" + + # Add important issues + important = findings.get('important', []) + if important: + review += "## Important Issues - Should Fix\n\n" + for i, issue_item in enumerate(important, 1): + issue = clean_text(issue_item.get('issue', 'Issue')) + details = clean_text(issue_item.get('details', 'No details')) + fix = clean_text(issue_item.get('fix', '')) + + review += f"{i}. **{issue}**\n" + if issue_item.get('file'): + review += f" - File: `{issue_item['file']}`\n" + review += f" - {details}\n" + if fix: + review += f" - Suggestion: {fix}\n" + review += "\n" + + # Add nits - keep brief + nits = findings.get('nits', []) + if nits and len(nits) <= 3: # Only include if few + review += "## Minor Issues\n\n" + for i, nit in enumerate(nits, 1): + issue = clean_text(nit.get('issue', 'Issue')) + review += f"{i}. {issue}" + if nit.get('file'): + review += f" in `{nit['file']}`" + review += "\n" + review += "\n" + + # Add praise + praise = findings.get('praise', []) + if praise: + review += "## Positive Notes\n\n" + for item in praise: + clean_item = clean_text(item) + review += f"- {clean_item}\n" + review += "\n" + + # Add overall recommendation - no emojis + if blockers: + review += "## Recommendation\n\nRequest changes - critical issues need to be addressed before merging.\n" + elif important: + review += "## Recommendation\n\nRequest changes - please address the important issues listed above.\n" + else: + review += "## Recommendation\n\nApprove - the code looks good. Minor items can be addressed optionally.\n" + + return review + + +def generate_inline_comments_file(findings: Dict[str, Any]) -> str: + """ + Generate inline.md with list of proposed inline comments. + + Includes code snippets with line number headers. + """ + + inline_comments = findings.get('inline_comments', []) + + if not inline_comments: + return "# Inline Comments\n\nNo inline comments proposed.\n" + + content = "# Proposed Inline Comments\n\n" + content += f"**Total Comments**: {len(inline_comments)}\n\n" + content += "Review these before posting. Edit as needed.\n\n" + content += "---\n\n" + + for i, comment in enumerate(inline_comments, 1): + content += f"## Comment {i}\n\n" + content += f"**File**: `{comment.get('file', 'unknown')}`\n" + content += f"**Line**: {comment.get('line', 'N/A')}\n" + + if comment.get('start_line') and comment.get('end_line'): + content += f"**Range**: Lines {comment['start_line']}-{comment['end_line']}\n" + + content += f"\n**Comment**:\n{comment.get('comment', 'No comment')}\n\n" + + if comment.get('code_snippet'): + # Add line numbers in header + start = comment.get('start_line', comment.get('line', 1)) + end = comment.get('end_line', comment.get('line', 1)) + + if start == end: + content += f"**Code (Line {start})**:\n" + else: + content += f"**Code (Lines {start}-{end})**:\n" + + content += f"```\n{comment['code_snippet']}\n```\n\n" + + # Add command to post this comment + owner = comment.get('owner', 'OWNER') + repo = comment.get('repo', 'REPO') + pr_num = comment.get('pr_number', 'PR_NUM') + + content += "**Command to post**:\n```bash\n" + content += f"python scripts/add_inline_comment.py {owner} {repo} {pr_num} latest \\\n" + content += f" \"{comment.get('file', 'file.py')}\" {comment.get('line', 42)} \\\n" + content += f" \"{comment.get('comment', 'comment')}\"\n" + content += "```\n\n" + content += "---\n\n" + + return content + + +def generate_claude_commands(pr_review_dir: Path, metadata: Dict[str, Any]): + """Generate .claude directory with custom slash commands.""" + + claude_dir = pr_review_dir / ".claude" / "commands" + claude_dir.mkdir(parents=True, exist_ok=True) + + owner = metadata.get('owner', 'owner') + repo = metadata.get('repo', 'repo') + pr_number = metadata.get('number', '123') + + # /send command - approve and post human.md + send_cmd = f"""Post the human-friendly review and approve the PR. + +Steps: +1. Read the file `pr/human.md` in the current directory +2. Post the review content as a PR comment using: + `gh pr comment {pr_number} --repo {owner}/{repo} --body-file pr/human.md` +3. Approve the PR using: + `gh pr review {pr_number} --repo {owner}/{repo} --approve` +4. Confirm to the user that the review was posted and PR was approved +""" + + with open(claude_dir / "send.md", 'w') as f: + f.write(send_cmd) + + # /send-decline command - request changes and post human.md + send_decline_cmd = f"""Post the human-friendly review and request changes on the PR. + +Steps: +1. Read the file `pr/human.md` in the current directory +2. Post the review content as a PR comment using: + `gh pr comment {pr_number} --repo {owner}/{repo} --body-file pr/human.md` +3. Request changes on the PR using: + `gh pr review {pr_number} --repo {owner}/{repo} --request-changes` +4. Confirm to the user that the review was posted and changes were requested +""" + + with open(claude_dir / "send-decline.md", 'w') as f: + f.write(send_decline_cmd) + + # /show command - open in VS Code + show_cmd = f"""Open the PR review directory in VS Code for editing. + +Steps: +1. Run `code .` to open the current directory in VS Code +2. Tell the user they can now edit the review files: + - pr/review.md (detailed review) + - pr/human.md (short review for posting) + - pr/inline.md (inline comments) +3. Remind them to use /send or /send-decline when ready to post +""" + + with open(claude_dir / "show.md", 'w') as f: + f.write(show_cmd) + + print(f"✅ Created slash commands in {claude_dir}") + print(" - /send (approve and post)") + print(" - /send-decline (request changes and post)") + print(" - /show (open in VS Code)") + + +def main(): + parser = argparse.ArgumentParser( + description='Generate structured review files from PR analysis', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__ + ) + parser.add_argument('pr_review_dir', help='PR review directory path') + parser.add_argument('--findings', required=True, help='JSON file with review findings') + parser.add_argument('--metadata', help='JSON file with PR metadata (optional)') + + args = parser.parse_args() + + try: + # Load findings + findings = load_findings(args.findings) + + # Load metadata if provided + metadata = {} + if args.metadata and os.path.exists(args.metadata): + with open(args.metadata, 'r') as f: + metadata = json.load(f) + + # Extract metadata from findings if not provided + if not metadata: + metadata = findings.get('metadata', {}) + + # Create pr directory + pr_review_dir = Path(args.pr_review_dir) + pr_dir = create_pr_directory(pr_review_dir) + + print(f"📝 Generating review files in {pr_dir}...") + + # Generate detailed review + detailed_review = generate_detailed_review(findings, metadata) + review_file = pr_dir / "review.md" + with open(review_file, 'w') as f: + f.write(detailed_review) + print(f"✅ Created detailed review: {review_file}") + + # Generate human-friendly review + human_review = generate_human_review(findings, metadata) + human_file = pr_dir / "human.md" + with open(human_file, 'w') as f: + f.write(human_review) + print(f"✅ Created human review: {human_file}") + + # Generate inline comments file + inline_comments = generate_inline_comments_file(findings) + inline_file = pr_dir / "inline.md" + with open(inline_file, 'w') as f: + f.write(inline_comments) + print(f"✅ Created inline comments: {inline_file}") + + # Generate Claude slash commands + generate_claude_commands(pr_review_dir, metadata) + + # Create summary file + summary = f"""PR Review Files Generated +======================== + +Directory: {pr_review_dir} + +Files created: +- pr/review.md - Detailed analysis for your review +- pr/human.md - Clean version for posting (no emojis, no line numbers) +- pr/inline.md - Proposed inline comments with code snippets + +Slash commands available: +- /send - Post human.md and approve PR +- /send-decline - Post human.md and request changes +- /show - Open directory in VS Code + +Next steps: +1. Review the files (use /show to open in VS Code) +2. Edit as needed +3. Use /send or /send-decline when ready to post + +IMPORTANT: Nothing will be posted until you run /send or /send-decline +""" + + summary_file = pr_review_dir / "REVIEW_READY.txt" + with open(summary_file, 'w') as f: + f.write(summary) + + print(f"\n{summary}") + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/skills/tailwind-design-system/SKILL.md b/skills/tailwind-design-system/SKILL.md new file mode 100644 index 0000000..0a8f806 --- /dev/null +++ b/skills/tailwind-design-system/SKILL.md @@ -0,0 +1,874 @@ +--- +name: tailwind-design-system +description: Build scalable design systems with Tailwind CSS v4, design tokens, component libraries, and responsive patterns. Use when creating component libraries, implementing design systems, or standardizing UI patterns. +--- + +# Tailwind Design System (v4) + +Build production-ready design systems with Tailwind CSS v4, including CSS-first configuration, design tokens, component variants, responsive patterns, and accessibility. + +> **Note**: This skill targets Tailwind CSS v4 (2024+). For v3 projects, refer to the [upgrade guide](https://tailwindcss.com/docs/upgrade-guide). + +## When to Use This Skill + +- Creating a component library with Tailwind v4 +- Implementing design tokens and theming with CSS-first configuration +- Building responsive and accessible components +- Standardizing UI patterns across a codebase +- Migrating from Tailwind v3 to v4 +- Setting up dark mode with native CSS features + +## Key v4 Changes + +| v3 Pattern | v4 Pattern | +| ------------------------------------- | --------------------------------------------------------------------- | +| `tailwind.config.ts` | `@theme` in CSS | +| `@tailwind base/components/utilities` | `@import "tailwindcss"` | +| `darkMode: "class"` | `@custom-variant dark (&:where(.dark, .dark *))` | +| `theme.extend.colors` | `@theme { --color-*: value }` | +| `require("tailwindcss-animate")` | CSS `@keyframes` in `@theme` + `@starting-style` for entry animations | + +## Quick Start + +```css +/* app.css - Tailwind v4 CSS-first configuration */ +@import "tailwindcss"; + +/* Define your theme with @theme */ +@theme { + /* Semantic color tokens using OKLCH for better color perception */ + --color-background: oklch(100% 0 0); + --color-foreground: oklch(14.5% 0.025 264); + + --color-primary: oklch(14.5% 0.025 264); + --color-primary-foreground: oklch(98% 0.01 264); + + --color-secondary: oklch(96% 0.01 264); + --color-secondary-foreground: oklch(14.5% 0.025 264); + + --color-muted: oklch(96% 0.01 264); + --color-muted-foreground: oklch(46% 0.02 264); + + --color-accent: oklch(96% 0.01 264); + --color-accent-foreground: oklch(14.5% 0.025 264); + + --color-destructive: oklch(53% 0.22 27); + --color-destructive-foreground: oklch(98% 0.01 264); + + --color-border: oklch(91% 0.01 264); + --color-ring: oklch(14.5% 0.025 264); + + --color-card: oklch(100% 0 0); + --color-card-foreground: oklch(14.5% 0.025 264); + + /* Ring offset for focus states */ + --color-ring-offset: oklch(100% 0 0); + + /* Radius tokens */ + --radius-sm: 0.25rem; + --radius-md: 0.375rem; + --radius-lg: 0.5rem; + --radius-xl: 0.75rem; + + /* Animation tokens - keyframes inside @theme are output when referenced by --animate-* variables */ + --animate-fade-in: fade-in 0.2s ease-out; + --animate-fade-out: fade-out 0.2s ease-in; + --animate-slide-in: slide-in 0.3s ease-out; + --animate-slide-out: slide-out 0.3s ease-in; + + @keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } + } + + @keyframes slide-in { + from { + transform: translateY(-0.5rem); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } + } + + @keyframes slide-out { + from { + transform: translateY(0); + opacity: 1; + } + to { + transform: translateY(-0.5rem); + opacity: 0; + } + } +} + +/* Dark mode variant - use @custom-variant for class-based dark mode */ +@custom-variant dark (&:where(.dark, .dark *)); + +/* Dark mode theme overrides */ +.dark { + --color-background: oklch(14.5% 0.025 264); + --color-foreground: oklch(98% 0.01 264); + + --color-primary: oklch(98% 0.01 264); + --color-primary-foreground: oklch(14.5% 0.025 264); + + --color-secondary: oklch(22% 0.02 264); + --color-secondary-foreground: oklch(98% 0.01 264); + + --color-muted: oklch(22% 0.02 264); + --color-muted-foreground: oklch(65% 0.02 264); + + --color-accent: oklch(22% 0.02 264); + --color-accent-foreground: oklch(98% 0.01 264); + + --color-destructive: oklch(42% 0.15 27); + --color-destructive-foreground: oklch(98% 0.01 264); + + --color-border: oklch(22% 0.02 264); + --color-ring: oklch(83% 0.02 264); + + --color-card: oklch(14.5% 0.025 264); + --color-card-foreground: oklch(98% 0.01 264); + + --color-ring-offset: oklch(14.5% 0.025 264); +} + +/* Base styles */ +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground antialiased; + } +} +``` + +## Core Concepts + +### 1. Design Token Hierarchy + +``` +Brand Tokens (abstract) + └── Semantic Tokens (purpose) + └── Component Tokens (specific) + +Example: + oklch(45% 0.2 260) → --color-primary → bg-primary +``` + +### 2. Component Architecture + +``` +Base styles → Variants → Sizes → States → Overrides +``` + +## Patterns + +### Pattern 1: CVA (Class Variance Authority) Components + +```typescript +// components/ui/button.tsx +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + // Base styles - v4 uses native CSS variables + 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + outline: 'border border-border bg-background hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + icon: 'size-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +// React 19: No forwardRef needed +export function Button({ + className, + variant, + size, + asChild = false, + ref, + ...props +}: ButtonProps & { ref?: React.Ref }) { + const Comp = asChild ? Slot : 'button' + return ( + + ) +} + +// Usage + + + +``` + +### Pattern 2: Compound Components (React 19) + +```typescript +// components/ui/card.tsx +import { cn } from '@/lib/utils' + +// React 19: ref is a regular prop, no forwardRef +export function Card({ + className, + ref, + ...props +}: React.HTMLAttributes & { ref?: React.Ref }) { + return ( +
+ ) +} + +export function CardHeader({ + className, + ref, + ...props +}: React.HTMLAttributes & { ref?: React.Ref }) { + return ( +
+ ) +} + +export function CardTitle({ + className, + ref, + ...props +}: React.HTMLAttributes & { ref?: React.Ref }) { + return ( +

+ ) +} + +export function CardDescription({ + className, + ref, + ...props +}: React.HTMLAttributes & { ref?: React.Ref }) { + return ( +

+ ) +} + +export function CardContent({ + className, + ref, + ...props +}: React.HTMLAttributes & { ref?: React.Ref }) { + return ( +

+ ) +} + +export function CardFooter({ + className, + ref, + ...props +}: React.HTMLAttributes & { ref?: React.Ref }) { + return ( +
+ ) +} + +// Usage + + + Account + Manage your account settings + + +
...
+
+ + + +
+``` + +### Pattern 3: Form Components + +```typescript +// components/ui/input.tsx +import { cn } from '@/lib/utils' + +export interface InputProps extends React.InputHTMLAttributes { + error?: string + ref?: React.Ref +} + +export function Input({ className, type, error, ref, ...props }: InputProps) { + return ( +
+ + {error && ( + + )} +
+ ) +} + +// components/ui/label.tsx +import { cva, type VariantProps } from 'class-variance-authority' + +const labelVariants = cva( + 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' +) + +export function Label({ + className, + ref, + ...props +}: React.LabelHTMLAttributes & { ref?: React.Ref }) { + return ( +