From fe6f187a47216af774de5d35f89bb317e729d67d Mon Sep 17 00:00:00 2001 From: John Lambert Date: Tue, 5 May 2026 15:46:41 -0400 Subject: [PATCH] chore: separate non-render cleanup Keep build/test helper cleanup out of the render commit. Keep agent, installer, and xWorks cleanup separate. Normalize TonePars generated-file comparisons for Windows paths. --- .github/instructions/build.instructions.md | 5 + .github/instructions/csharp.instructions.md | 72 + .../instructions/debugging.instructions.md | 4 +- .../dotnet-framework.instructions.md | 81 +- .github/instructions/repo.instructions.md | 3 - .github/instructions/terminal.instructions.md | 2 - .github/prompts/opsx-archive.prompt.md | 4 +- .github/prompts/opsx-bulk-archive.prompt.md | 2 +- .github/prompts/opsx-explore.prompt.md | 7 +- .github/prompts/opsx-ff.prompt.md | 5 +- .github/prompts/opsx-onboard.prompt.md | 53 +- .../skills/code-review-skill-main/.gitignore | 25 + .../code-review-skill-main/CONTRIBUTING.md | 321 +++++ .github/skills/code-review-skill-main/LICENSE | 21 + .../skills/code-review-skill-main/README.md | 389 ++++++ .../skills/code-review-skill-main/SKILL.md | 197 +++ .../assets/pr-review-template.md | 114 ++ .../assets/review-checklist.md | 121 ++ .../reference/architecture-review-guide.md | 472 +++++++ .../code-review-skill-main/reference/c.md | 285 ++++ .../reference/code-review-best-practices.md | 136 ++ .../reference/common-bugs-checklist.md | 1227 +++++++++++++++++ .../code-review-skill-main/reference/cpp.md | 385 ++++++ .../reference/css-less-sass.md | 656 +++++++++ .../code-review-skill-main/reference/go.md | 989 +++++++++++++ .../code-review-skill-main/reference/java.md | 405 ++++++ .../reference/performance-review-guide.md | 752 ++++++++++ .../reference/python.md | 1069 ++++++++++++++ .../code-review-skill-main/reference/react.md | 871 ++++++++++++ .../code-review-skill-main/reference/rust.md | 840 +++++++++++ .../reference/security-review-guide.md | 265 ++++ .../reference/typescript.md | 543 ++++++++ .../code-review-skill-main/reference/vue.md | 924 +++++++++++++ .../scripts/pr-analyzer.py | 366 +++++ .github/skills/openspec-apply-change/SKILL.md | 2 +- .../skills/openspec-archive-change/SKILL.md | 4 +- .../openspec-bulk-archive-change/SKILL.md | 4 +- .../skills/openspec-continue-change/SKILL.md | 2 +- .github/skills/openspec-explore/SKILL.md | 12 +- .github/skills/openspec-ff-change/SKILL.md | 2 +- .github/skills/openspec-new-change/SKILL.md | 2 +- .github/skills/openspec-onboard/SKILL.md | 55 +- .github/skills/openspec-sync-specs/SKILL.md | 2 +- .../skills/openspec-verify-change/SKILL.md | 2 +- .github/workflows/patch-installer-cd.yml | 10 +- .vscode/settings.json | 9 +- AGENTS.md | 13 +- Build/Agent/FwBuildHelpers.psm1 | 109 +- Build/Agent/README.md | 1 + Build/Agent/Verify-FwDependencies.ps1 | 16 +- Build/Agent/fix-whitespace.ps1 | 34 +- Build/Installer.legacy.targets | 4 + Build/Src/FwBuildTasks/CollectTargets.cs | 8 + Build/Src/NativeBuild/NativeBuild.csproj | 5 +- Build/scripts/Invoke-CppTest.ps1 | 6 +- Directory.Build.props | 14 + Directory.Build.targets | 16 + Docs/CONTRIBUTING.md | 5 +- FLExInstaller/Directory.Packages.props | 11 +- .../CustomActions/CustomActions.csproj | 4 +- ReadMe.md | 8 + SDK_MIGRATION.md | 2 +- Src/AppForTests.config | 51 +- .../ToneParsInvokerTests.cs | 23 +- Src/xWorks/GeneratedHtmlViewer.cs | 2 +- Test.runsettings | 4 +- build.ps1 | 33 +- scripts/Agent/Git-Search.ps1 | 17 +- scripts/toolshims/environ.cmd | 3 +- 69 files changed, 11875 insertions(+), 231 deletions(-) create mode 100644 .github/instructions/csharp.instructions.md create mode 100644 .github/skills/code-review-skill-main/.gitignore create mode 100644 .github/skills/code-review-skill-main/CONTRIBUTING.md create mode 100644 .github/skills/code-review-skill-main/LICENSE create mode 100644 .github/skills/code-review-skill-main/README.md create mode 100644 .github/skills/code-review-skill-main/SKILL.md create mode 100644 .github/skills/code-review-skill-main/assets/pr-review-template.md create mode 100644 .github/skills/code-review-skill-main/assets/review-checklist.md create mode 100644 .github/skills/code-review-skill-main/reference/architecture-review-guide.md create mode 100644 .github/skills/code-review-skill-main/reference/c.md create mode 100644 .github/skills/code-review-skill-main/reference/code-review-best-practices.md create mode 100644 .github/skills/code-review-skill-main/reference/common-bugs-checklist.md create mode 100644 .github/skills/code-review-skill-main/reference/cpp.md create mode 100644 .github/skills/code-review-skill-main/reference/css-less-sass.md create mode 100644 .github/skills/code-review-skill-main/reference/go.md create mode 100644 .github/skills/code-review-skill-main/reference/java.md create mode 100644 .github/skills/code-review-skill-main/reference/performance-review-guide.md create mode 100644 .github/skills/code-review-skill-main/reference/python.md create mode 100644 .github/skills/code-review-skill-main/reference/react.md create mode 100644 .github/skills/code-review-skill-main/reference/rust.md create mode 100644 .github/skills/code-review-skill-main/reference/security-review-guide.md create mode 100644 .github/skills/code-review-skill-main/reference/typescript.md create mode 100644 .github/skills/code-review-skill-main/reference/vue.md create mode 100644 .github/skills/code-review-skill-main/scripts/pr-analyzer.py diff --git a/.github/instructions/build.instructions.md b/.github/instructions/build.instructions.md index 9e5f6b559a..0908db41fd 100644 --- a/.github/instructions/build.instructions.md +++ b/.github/instructions/build.instructions.md @@ -10,6 +10,11 @@ This file documents the **supported** build workflow for FieldWorks. FieldWorks is **Windows-first** and **x64-only**. Use the repo scripts so build ordering (native before managed) is correct. +## Non-Windows hosts +- Linux and macOS are supported for editing, code search, documentation, specs, and agent workflows only. +- Do not attempt to build, test, or run installer/setup flows on non-Windows hosts. +- `build.ps1` and `test.ps1` intentionally fail fast on non-Windows hosts so automation notices the environment mismatch. + ## Quick start (PowerShell) ```powershell # Full traversal build (Debug/x64 defaults) diff --git a/.github/instructions/csharp.instructions.md b/.github/instructions/csharp.instructions.md new file mode 100644 index 0000000000..284cb4f942 --- /dev/null +++ b/.github/instructions/csharp.instructions.md @@ -0,0 +1,72 @@ +--- +description: 'Guidelines for building C# applications' +applyTo: '**/*.cs' +--- + +# C# Development + +## Scope +- FieldWorks managed code is a Windows desktop codebase built on .NET Framework 4.8 and native dependencies. +- Treat this file as guidance for existing FieldWorks C# projects, not for ASP.NET or service-oriented .NET applications. +- On Linux and macOS, limit work to editing, search, docs, and specs. Do not claim to have built or run managed binaries there. + +## Language Version And Features +- Use the repo default C# language version unless a project explicitly overrides it. Today that default is C# 8.0 via `Directory.Build.props`. +- Do not introduce C# features that require a newer language version without first updating repo-wide build policy. +- Nullable reference types are disabled by default. Only use nullable annotations and nullable-flow assumptions in projects that opt in explicitly. +- Prefer syntax that is already common in the touched area. Consistency with nearby code is more important than using the newest available syntax. + +## General Instructions +- Make only high-confidence suggestions when reviewing code changes. +- Fix the root cause when practical, but keep edits narrow and compatible with existing behavior. +- Handle edge cases and exception paths explicitly. Do not swallow exceptions without a documented reason. +- Treat native interop, COM, registry-free COM, file formats, and serialization boundaries as high-risk areas that need extra care. + +## Naming Conventions +- Follow PascalCase for types, methods, properties, events, and public members. +- Use camelCase for locals and private fields. +- Prefix interfaces with `I`. +- Keep namespaces aligned with the existing project root namespace instead of inventing new top-level naming schemes. + +## Formatting And Style +- Apply the formatting rules defined in `.editorconfig` and match surrounding code. +- Prefer block-scoped namespaces. Do not default to file-scoped namespaces in this repo. +- Keep using directives simple and consistent with the file you are editing. +- Use `nameof` instead of string literals when referencing member names. +- Use pattern matching where it improves clarity and is supported by the project language version. Do not force newer syntax into older-looking code. + +## Documentation And Comments +- Public APIs should have XML documentation comments. +- Add code comments only when intent, invariants, or interop constraints are not obvious from the code itself. +- Do not add boilerplate comments to every method. + +## Project Conventions +- Most managed projects here are SDK-style `.csproj` files targeting `net48`. +- Keep `GenerateAssemblyInfo` disabled where the project relies on linked `CommonAssemblyInfo.cs`. +- Preserve project-specific build settings such as warnings-as-errors, x64 assumptions, WinExe/WindowsDesktop settings, and registration-free COM behavior. +- When adding new files, update the project file only if the specific project format requires it. + +## Desktop, UI, And Localization +- FieldWorks is a desktop application. Favor guidance relevant to WinForms, WPF, dialogs, view models, threading, and long-running UI work. +- UI-affecting code must respect the UI thread. Avoid blocking calls that can freeze the application. +- Keep user-visible strings in `.resx` resources and follow existing localization patterns. Do not hardcode new UI strings. +- Preserve designer compatibility for WinForms and avoid edits that break generated code patterns. + +## Nullability And Defensive Code +- Because nullable reference types are usually disabled, write explicit null checks at public entry points and interop boundaries when required by the surrounding code. +- Prefer `is null` and `is not null` checks when adding new null checks. +- Do not pretend the compiler will enforce null-state safety unless the project has opted into nullable analysis. + +## Testing +- For behavior changes and bug fixes, add or update tests when practical. +- Follow nearby test naming and structure conventions. Do not add `Arrange`, `Act`, or `Assert` comments unless the existing file already uses them. +- Prefer fast, deterministic NUnit tests for managed code. +- Use `./test.ps1` on Windows to run tests, and `./build.ps1` when you need a supporting build first. Do not recommend ad-hoc `dotnet test` or `msbuild` commands as the normal path for this repo. + +## Build And Validation +- Use `./build.ps1` for builds and `./test.ps1` for tests in normal repo workflows. +- Avoid changing build, packaging, COM, or registry behavior without checking the existing build instructions and affected tests. +- Treat compiler warnings as actionable unless the repo already documents a specific exception. + +## What Not To Assume +- Do not assume ASP.NET Core, Minimal APIs, Entity Framework Core, Swagger/OpenAPI, cloud deployment, container publishing, or JWT authentication are relevant unless the user is explicitly working in a repo area that adds those technologies. diff --git a/.github/instructions/debugging.instructions.md b/.github/instructions/debugging.instructions.md index fa5b2dfffe..51a7438910 100644 --- a/.github/instructions/debugging.instructions.md +++ b/.github/instructions/debugging.instructions.md @@ -49,7 +49,7 @@ description: "Runtime debugging and tracing guidance for FieldWorks" ## Dev switch (auto config) - FieldWorks now supports a swappable diagnostics config via `FieldWorks.Diagnostics.config`. -- Default is quiet. `build.ps1` now enables the dev diagnostics config automatically for Debug builds unless you override `/p:UseDevTraceConfig`. You can also force it via `UseDevTraceConfig=true`, by setting environment variable `FW_TRACE_LOG` before the build, or by passing `-TraceCrashes` to `build.ps1`; the dev diagnostics file is copied as `FieldWorks.Diagnostics.config` in the output. +- Default is quiet. Use `build.ps1 -EnableTracing` to copy the dev diagnostics file into the output as `FieldWorks.Diagnostics.config`. You can also force it via `UseDevTraceConfig=true` or by setting environment variable `FW_TRACE_LOG` before the build. - Dev log location: `Output/Debug/FieldWorks.trace.log` (relative to the app folder) so it’s easy to collect alongside binaries. - Dev config logs to `%temp%/FieldWorks.trace.log` and turns on the core switches above. Edit `Src/Common/FieldWorks/FieldWorks.Diagnostics.dev.config` to change log path or switches. @@ -59,5 +59,5 @@ description: "Runtime debugging and tracing guidance for FieldWorks" ## Proposed Improvements (dev-only) - Add `Docs/FieldWorks.trace.sample.config` with the snippet above for easy reuse. -- Introduce a dev flag (`UseDevTraceConfig=true` or `FW_TRACE_LOG` env var) that copies the dev diagnostics file next to `FieldWorks.exe` in Debug builds so tracing is on by default for local runs. +- Keep `build.ps1 -EnableTracing` as the single scripted path that copies the dev diagnostics file next to `FieldWorks.exe` for local trace-enabled runs. - Document standard trace switches in `Docs/logging.md` and keep `EnvVarTraceListener` as the default listener for dev traces. diff --git a/.github/instructions/dotnet-framework.instructions.md b/.github/instructions/dotnet-framework.instructions.md index 9b796f612a..46cdcace02 100644 --- a/.github/instructions/dotnet-framework.instructions.md +++ b/.github/instructions/dotnet-framework.instructions.md @@ -6,44 +6,46 @@ applyTo: '**/*.csproj, **/*.cs' # .NET Framework Development ## Build and Compilation Requirements -- Always use `msbuild /t:rebuild` to build the solution or projects instead of `dotnet build` +- Use `./build.ps1` for normal builds and `./test.ps1` for normal test runs. +- Do not use `dotnet build` for the main repo workflow. +- Avoid direct `msbuild` or project-only builds unless you are explicitly debugging build infrastructure. ## Project File Management -### Non-SDK Style Project Structure -.NET Framework projects use the legacy project format, which differs significantly from modern SDK-style projects: +### SDK-style net48 projects are common +Most managed projects in this repo use SDK-style `.csproj` files while still targeting `.NET Framework` `net48`. -- **Explicit File Inclusion**: All new source files **MUST** be explicitly added to the project file (`.csproj`) using a `` element - - .NET Framework projects do not automatically include files in the directory like SDK-style projects - - Example: `` +- **Implicit file inclusion is common**: SDK-style projects usually include new `.cs` files automatically. +- **Check the touched project before editing**: Some projects still carry explicit includes, linked files, designer items, generated code, or custom metadata that must be preserved. +- **Assembly metadata is managed explicitly**: Keep `GenerateAssemblyInfo` disabled where the project links `CommonAssemblyInfo.cs`. +- **Preserve project-specific settings**: Keep existing settings for x64, WinExe, WindowsDesktop, COM, resources, warnings-as-errors, and custom MSBuild targets. -- **No Implicit Imports**: Unlike SDK-style projects, .NET Framework projects do not automatically import common namespaces or assemblies - -- **Build Configuration**: Contains explicit `` sections for Debug/Release configurations +- **Do not normalize projects unnecessarily**: Avoid converting project structure, import style, or target declarations unless that is the task. -- **Output Paths**: Explicit `` and `` definitions - -- **Target Framework**: Uses `` instead of `` - - Example: `v4.7.2` +### Legacy project caveat +Some non-SDK-style or tool-specific projects may still exist. When you encounter one: +- Keep explicit `` items and other legacy structure intact. +- Update the project file only as much as the change requires. +- Do not assume you can migrate it to SDK-style as part of routine code edits. ## NuGet Package Management - Installing and updating NuGet packages in .NET Framework projects is a complex task requiring coordinated changes to multiple files. Therefore, **do not attempt to install or update NuGet packages** in this project. - Instead, if changes to NuGet references are required, ask the user to install or update NuGet packages using the Visual Studio NuGet Package Manager or Visual Studio package manager console. - When recommending NuGet packages, ensure they are compatible with .NET Framework or .NET Standard 2.0 (not only .NET Core or .NET 5+). -## C# Language Version is 7.3 -- This project is limited to C# 7.3 features only. Please avoid using: +## C# Language Version +- The repo-wide default language version for C# projects is 8.0. +- Do not introduce features that require a newer language version unless the specific project or repo policy is updated first. +- Prefer syntax already used in the touched area so edits remain consistent with surrounding code. + +### C# 8.0 features available by default +- Switch expressions and pattern matching enhancements. +- Using declarations where disposal scope remains clear. +- Null-coalescing assignment and range/index operators when they improve clarity. -### C# 8.0+ Features (NOT SUPPORTED): - - Using declarations (`using var stream = ...`) - - Await using statements (`await using var resource = ...`) - - Switch expressions (`variable switch { ... }`) - - Null-coalescing assignment (`??=`) - - Range and index operators (`array[1..^1]`, `array[^1]`) - - Default interface methods - - Readonly members in structs - - Static local functions - - Nullable reference types (`string?`, `#nullable enable`) +### Features not safe to assume repo-wide +- Nullable reference types are not enabled repo-wide. Do not introduce `string?`, `#nullable enable`, or nullable-flow assumptions unless the project explicitly opts in. +- File-scoped namespaces are not available under the repo default language version and should not be introduced. ### C# 9.0+ Features (NOT SUPPORTED): - Records (`public record Person(string Name)`) @@ -58,26 +60,25 @@ applyTo: '**/*.csproj, **/*.cs' - Record structs - Required members -### Use Instead (C# 7.3 Compatible): - - Traditional using statements with braces - - Switch statements instead of switch expressions - - Explicit null checks instead of null-coalescing assignment - - Array slicing with manual indexing - - Abstract classes or interfaces instead of default interface methods +### Use instead when staying within repo defaults + - Block-scoped namespaces + - Explicit null checks unless the project has enabled nullable analysis + - Established project patterns over newer syntax for its own sake ## Environment Considerations (Windows environment) -- Use Windows-style paths with backslashes (e.g., `C:\path\to\file.cs`) -- Use Windows-appropriate commands when suggesting terminal operations -- Consider Windows-specific behaviors when working with file system operations +- FieldWorks builds and managed test runs are Windows-only. +- On non-Windows hosts, limit work to editing, code search, docs, and specs. +- When suggesting build or test commands, prefer the repo PowerShell scripts and Windows-oriented workflows. ## Common .NET Framework Pitfalls and Best Practices ### Async/Await Patterns -- **ConfigureAwait(false)**: Always use `ConfigureAwait(false)` in library code to avoid deadlocks: +- **ConfigureAwait(false)**: Use it deliberately in library or background code when you do not need to resume on the UI thread: ```csharp var result = await SomeAsyncMethod().ConfigureAwait(false); ``` -- **Avoid sync-over-async**: Don't use `.Result` or `.Wait()` or `.GetAwaiter().GetResult()`. These sync-over-async patterns can lead to deadlocks and poor performance. Always use `await` for asynchronous calls. +- **Avoid sync-over-async**: Don't use `.Result`, `.Wait()`, or `.GetAwaiter().GetResult()` unless you have a clear, reviewed reason. These patterns can deadlock desktop UI code. +- **Respect the UI thread**: Do not move UI-bound code onto background threads or block the UI while waiting on long-running work. ### DateTime Handling - **Use DateTimeOffset for timestamps**: Prefer `DateTimeOffset` over `DateTime` for absolute time points @@ -106,6 +107,14 @@ applyTo: '**/*.csproj, **/*.cs' - **Don't swallow exceptions**: Always log or re-throw exceptions appropriately - **Use using for disposable resources**: Ensures proper cleanup even when exceptions occur +### Resources and localization +- Put user-visible strings into `.resx` resources and follow existing localization patterns. +- Avoid hardcoding new UI strings in C# code. + +### Interop and COM +- Treat P/Invoke, COM, registration-free COM, and serialization boundaries as high risk. +- Preserve existing manifest, marshaling, and build settings unless the task explicitly requires changing them. + ### Performance Considerations - **Avoid boxing**: Be aware of boxing/unboxing with value types and generics - **String interning**: Use `string.Intern()` judiciously for frequently used strings diff --git a/.github/instructions/repo.instructions.md b/.github/instructions/repo.instructions.md index d4645a4cb7..cddb75eb91 100644 --- a/.github/instructions/repo.instructions.md +++ b/.github/instructions/repo.instructions.md @@ -12,6 +12,3 @@ Provide clear, concise, and enforceable rules that help AI coding agents and aut ## Rules (high-impact, short) - Prefer the repository top-level build (`.\build.ps1`) and solution (`FieldWorks.sln`) for full builds. - Keep localization consistent: use `.resx` and follow `crowdin.json` for crowdin integration. -- Before commit/push, run the existing VS Code task `CI: Full local check`. -- After rebase/merge/cherry-pick conflict resolution, run `CI: Whitespace check`; if it fixes files, restage them and rerun. -- When rewriting history or adding commits, run `CI: Commit messages` before pushing. diff --git a/.github/instructions/terminal.instructions.md b/.github/instructions/terminal.instructions.md index 6a28b49ef8..927d119e99 100644 --- a/.github/instructions/terminal.instructions.md +++ b/.github/instructions/terminal.instructions.md @@ -13,8 +13,6 @@ Placement policy for wrappers: **MCP-first:** When the `ps-tools` MCP server is running, prefer MCP tools (`Git-Search`, `Read-FileContent`, `Invoke-AgentTask`, `build`, `test`, agent tools) instead of direct terminal commands. Use wrappers only when MCP is unavailable. -**Task-first for CI parity:** Prefer the existing VS Code tasks `CI: Whitespace check`, `CI: Commit messages`, and `CI: Full local check` over ad-hoc shell commands. These tasks are already allowed for agent use and match the repository CI checks. - ## Transformations | ❌ Blocked | ✅ Use Instead | diff --git a/.github/prompts/opsx-archive.prompt.md b/.github/prompts/opsx-archive.prompt.md index 4e2ee18914..1163776da4 100644 --- a/.github/prompts/opsx-archive.prompt.md +++ b/.github/prompts/opsx-archive.prompt.md @@ -56,7 +56,7 @@ Archive a completed change in the experimental workflow. - If changes needed: "Sync now (recommended)", "Archive without syncing" - If already synced: "Archive now", "Sync anyway", "Cancel" - If user chooses sync, execute `/opsx:sync` logic. Proceed to archive regardless of choice. + If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''. Delta spec analysis: "). Proceed to archive regardless of choice. 5. **Perform the archive** @@ -150,5 +150,5 @@ Target archive directory already exists. - Don't block archive on warnings - just inform and confirm - Preserve .openspec.yaml when moving to archive (it moves with the directory) - Show clear summary of what happened -- If sync is requested, use /opsx:sync approach (agent-driven) +- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven) - If delta specs exist, always run the sync assessment and show the combined summary before prompting diff --git a/.github/prompts/opsx-bulk-archive.prompt.md b/.github/prompts/opsx-bulk-archive.prompt.md index f8e773fed0..be3f901900 100644 --- a/.github/prompts/opsx-bulk-archive.prompt.md +++ b/.github/prompts/opsx-bulk-archive.prompt.md @@ -222,7 +222,7 @@ Failed K changes: ``` ## No Changes to Archive -No active changes found. Use `/opsx:new` to create a new change. +No active changes found. Create a new change to get started. ``` **Guardrails** diff --git a/.github/prompts/opsx-explore.prompt.md b/.github/prompts/opsx-explore.prompt.md index fd588622a3..b21a2266dc 100644 --- a/.github/prompts/opsx-explore.prompt.md +++ b/.github/prompts/opsx-explore.prompt.md @@ -4,7 +4,7 @@ description: Enter explore mode - think through ideas, investigate problems, cla Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. -**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first (e.g., start a change with `/opsx:new` or `/opsx:ff`). You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. +**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. **This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore. @@ -97,8 +97,7 @@ If the user mentioned a specific change name, read its artifacts for context. Think freely. When insights crystallize, you might offer: -- "This feels solid enough to start a change. Want me to create one?" - → Can transition to `/opsx:new` or `/opsx:ff` +- "This feels solid enough to start a change. Want me to create a proposal?" - Or keep exploring - no pressure to formalize ### When a change exists @@ -150,7 +149,7 @@ If the user mentions a change or you detect one is relevant: There's no required ending. Discovery might: -- **Flow into action**: "Ready to start? `/opsx:new` or `/opsx:ff`" +- **Flow into a proposal**: "Ready to start? I can create a change proposal." - **Result in artifact updates**: "Updated design.md with these decisions" - **Just provide clarity**: User has what they need, moves on - **Continue later**: "We can pick this up anytime" diff --git a/.github/prompts/opsx-ff.prompt.md b/.github/prompts/opsx-ff.prompt.md index 6b3dc00c77..06cea28020 100644 --- a/.github/prompts/opsx-ff.prompt.md +++ b/.github/prompts/opsx-ff.prompt.md @@ -81,7 +81,10 @@ After completing all artifacts, summarize: - Follow the `instruction` field from `openspec instructions` for each artifact type - The schema defines what each artifact should contain - follow it - Read dependency artifacts for context before creating new ones -- Use the `template` as a starting point, filling in based on context +- Use `template` as the structure for your output file - fill in its sections +- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file + - Do NOT copy ``, ``, `` blocks into the artifact + - These guide what you write, but should never appear in the output **Guardrails** - Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`) diff --git a/.github/prompts/opsx-onboard.prompt.md b/.github/prompts/opsx-onboard.prompt.md index 1414f1e1a7..8100b39057 100644 --- a/.github/prompts/opsx-onboard.prompt.md +++ b/.github/prompts/opsx-onboard.prompt.md @@ -8,16 +8,19 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t ## Preflight -Before starting, check if OpenSpec is initialized: +Before starting, check if the OpenSpec CLI is installed: ```bash -openspec status --json 2>&1 || echo "NOT_INITIALIZED" +# Unix/macOS +openspec --version 2>&1 || echo "CLI_NOT_INSTALLED" +# Windows (PowerShell) +# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" } ``` -**If not initialized:** -> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`. +**If CLI not installed:** +> OpenSpec CLI is not installed. Install it first, then come back to `/opsx:onboard`. -Stop here if not initialized. +Stop here if not installed. --- @@ -60,7 +63,10 @@ Scan the codebase for small improvement opportunities. Look for: Also check recent git activity: ```bash +# Unix/macOS git log --oneline -10 2>/dev/null || echo "No git history" +# Windows (PowerShell) +# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" } ``` ### Present Suggestions @@ -255,7 +261,10 @@ For a small task like this, we might only need one spec file. **DO:** Create the spec file: ```bash +# Unix/macOS mkdir -p openspec/changes//specs/ +# Windows (PowerShell) +# New-Item -ItemType Directory -Force -Path "openspec/changes//specs/" ``` Draft the spec content: @@ -450,21 +459,29 @@ This same rhythm works for any size change—a small fix or a major feature. ## Command Reference +**Core workflow:** + | Command | What it does | |---------|--------------| +| `/opsx:propose` | Create a change and generate all artifacts | | `/opsx:explore` | Think through problems before/during work | -| `/opsx:new` | Start a new change, step through artifacts | -| `/opsx:ff` | Fast-forward: create all artifacts at once | -| `/opsx:continue` | Continue working on an existing change | | `/opsx:apply` | Implement tasks from a change | -| `/opsx:verify` | Verify implementation matches artifacts | | `/opsx:archive` | Archive a completed change | +**Additional commands:** + +| Command | What it does | +|---------|--------------| +| `/opsx:new` | Start a new change, step through artifacts one at a time | +| `/opsx:continue` | Continue working on an existing change | +| `/opsx:ff` | Fast-forward: create all artifacts at once | +| `/opsx:verify` | Verify implementation matches artifacts | + --- ## What's Next? -Try `/opsx:new` or `/opsx:ff` on something you actually want to build. You've got the rhythm now! +Try `/opsx:propose` on something you actually want to build. You've got the rhythm now! ``` --- @@ -494,17 +511,25 @@ If the user says they just want to see the commands or skip the tutorial: ``` ## OpenSpec Quick Reference +**Core workflow:** + | Command | What it does | |---------|--------------| +| `/opsx:propose ` | Create a change and generate all artifacts | | `/opsx:explore` | Think through problems (no code changes) | +| `/opsx:apply ` | Implement tasks | +| `/opsx:archive ` | Archive when done | + +**Additional commands:** + +| Command | What it does | +|---------|--------------| | `/opsx:new ` | Start a new change, step by step | -| `/opsx:ff ` | Fast-forward: all artifacts at once | | `/opsx:continue ` | Continue an existing change | -| `/opsx:apply ` | Implement tasks | +| `/opsx:ff ` | Fast-forward: all artifacts at once | | `/opsx:verify ` | Verify implementation | -| `/opsx:archive ` | Archive when done | -Try `/opsx:new` to start your first change, or `/opsx:ff` if you want to move fast. +Try `/opsx:propose` to start your first change. ``` Exit gracefully. diff --git a/.github/skills/code-review-skill-main/.gitignore b/.github/skills/code-review-skill-main/.gitignore new file mode 100644 index 0000000000..3f7a9f039e --- /dev/null +++ b/.github/skills/code-review-skill-main/.gitignore @@ -0,0 +1,25 @@ +# OS files +.DS_Store +Thumbs.db + +# Editor files +*.swp +*.swo +*~ +.idea/ +.vscode/ + +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.eggs/ +dist/ +build/ + +# Logs +*.log + +# Local config +.env +.env.local diff --git a/.github/skills/code-review-skill-main/CONTRIBUTING.md b/.github/skills/code-review-skill-main/CONTRIBUTING.md new file mode 100644 index 0000000000..3cfb4f4c88 --- /dev/null +++ b/.github/skills/code-review-skill-main/CONTRIBUTING.md @@ -0,0 +1,321 @@ +# Contributing to AI Code Review Guide + +Thank you for your interest in contributing! This document provides guidelines for contributing to this Claude Code Skill project. + +## Claude Code Skill 开发规范 + +本项目是一个 Claude Code Skill,贡献者需要遵循以下规范。 + +### 目录结构 + +``` +ai-code-review-guide/ +├── SKILL.md # 必需:主文件(始终加载) +├── README.md # 项目说明文档 +├── CONTRIBUTING.md # 贡献指南(本文件) +├── LICENSE # 许可证 +├── reference/ # 按需加载的详细指南 +│ ├── react.md +│ ├── vue.md +│ ├── rust.md +│ ├── typescript.md +│ ├── python.md +│ ├── c.md +│ ├── cpp.md +│ ├── common-bugs-checklist.md +│ ├── security-review-guide.md +│ └── code-review-best-practices.md +├── assets/ # 模板和快速参考 +│ ├── review-checklist.md +│ └── pr-review-template.md +└── scripts/ # 工具脚本 + └── pr-analyzer.py +``` + +### Frontmatter 规范 + +SKILL.md 必须包含 YAML frontmatter: + +```yaml +--- +name: skill-name +description: | + 功能描述。触发条件说明。 + Use when [具体使用场景]。 +allowed-tools: ["Read", "Grep", "Glob"] # 可选:限制工具访问 +--- +``` + +#### 必需字段 + +| 字段 | 说明 | 约束 | +|------|------|------| +| `name` | Skill 标识符 | 小写字母、数字、连字符;最多 64 字符 | +| `description` | 功能和激活条件 | 最多 1024 字符;必须包含 "Use when" | + +#### 可选字段 + +| 字段 | 说明 | 示例 | +|------|------|------| +| `allowed-tools` | 限制工具访问 | `["Read", "Grep", "Glob"]` | + +### 命名约定 + +**Skill 名称规则**: +- 仅使用小写字母、数字和连字符(kebab-case) +- 最多 64 个字符 +- 避免下划线或大写字母 + +``` +✅ 正确:code-review-excellence, typescript-advanced-types +❌ 错误:CodeReview, code_review, TYPESCRIPT +``` + +**文件命名规则**: +- reference 文件使用小写:`react.md`, `vue.md` +- 多词文件使用连字符:`common-bugs-checklist.md` + +### Description 写法规范 + +Description 必须包含两部分: + +1. **功能陈述**:具体说明 Skill 能做什么 +2. **触发条件**:以 "Use when" 开头,说明何时激活 + +```yaml +# ✅ 正确示例 +description: | + Provides comprehensive code review guidance for React 19, Vue 3, Rust, + TypeScript, Java, Python, and C/C++. + Helps catch bugs, improve code quality, and give constructive feedback. + Use when reviewing pull requests, conducting PR reviews, establishing + review standards, or mentoring developers through code reviews. + +# ❌ 错误示例(太模糊,缺少触发条件) +description: | + Helps with code review. +``` + +### Progressive Disclosure(渐进式披露) + +Claude 只在需要时加载支持文件,不会一次性加载所有内容。 + +#### 文件职责划分 + +| 文件 | 加载时机 | 内容 | +|------|----------|------| +| `SKILL.md` | 始终加载 | 核心原则、快速索引、何时使用 | +| `reference/*.md` | 按需加载 | 语言/框架的详细指南 | +| `assets/*.md` | 明确需要时 | 模板、清单 | +| `scripts/*.py` | 明确指引时 | 工具脚本 | + +#### 内容组织原则 + +**SKILL.md**(~200 行以内): +- 简述:2-3 句话说明用途 +- 核心原则和方法论 +- 语言/框架索引表(链接到 reference/) +- 何时使用此 Skill + +**reference/*.md**(详细内容): +- 完整的代码示例 +- 所有最佳实践 +- Review Checklist +- 边界情况和陷阱 + +### 文件引用规范 + +在 SKILL.md 中引用其他文件时: + +```markdown +# ✅ 正确:使用 Markdown 链接格式 +| **React** | [React Guide](reference/react.md) | Hooks, React 19, RSC | +| **Vue 3** | [Vue Guide](reference/vue.md) | Composition API | + +详见 [React Guide](reference/react.md) 获取完整指南。 + +# ❌ 错误:使用代码块格式 +参考 `reference/react.md` 文件。 +``` + +**路径规则**: +- 使用相对路径(相对于 Skill 目录) +- 使用正斜杠 `/`,不使用反斜杠 +- 不需要 `./` 前缀 + +--- + +## 贡献类型 + +### 添加新语言支持 + +1. 在 `reference/` 目录创建新文件(如 `go.md`) +2. 遵循以下结构: + +```markdown +# [Language] Code Review Guide + +> 简短描述,一句话说明覆盖内容。 + +## 目录 +- [主题1](#主题1) +- [主题2](#主题2) +- [Review Checklist](#review-checklist) + +--- + +## 主题1 + +### 子主题 + +```[language] +// ❌ Bad pattern - 说明为什么不好 +bad_code_example() + +// ✅ Good pattern - 说明为什么好 +good_code_example() +``` + +--- + +## Review Checklist + +### 类别1 +- [ ] 检查项 1 +- [ ] 检查项 2 +``` + +3. 在 `SKILL.md` 的索引表中添加链接 +4. 更新 `README.md` 的统计信息 + +### 添加框架模式 + +1. 确保引用官方文档 +2. 包含版本号(如 "React 19", "Vue 3.5+") +3. 提供可运行的代码示例 +4. 添加对应的 checklist 项 + +### 改进现有内容 + +- 修复拼写或语法错误 +- 更新过时的模式(注明版本变化) +- 添加边界情况示例 +- 改进代码示例的清晰度 + +--- + +## 代码示例规范 + +### 格式要求 + +```markdown +// ❌ 问题描述 - 解释为什么这样做不好 +problematic_code() + +// ✅ 推荐做法 - 解释为什么这样做更好 +recommended_code() +``` + +### 质量标准 + +- 示例应基于真实场景,避免人为构造 +- 同时展示问题和解决方案 +- 保持示例简洁聚焦 +- 包含必要的上下文(import 语句等) + +--- + +## 提交流程 + +### Issue 报告 + +- 使用 GitHub Issues 报告问题或建议 +- 提供清晰的描述和示例 +- 标注相关的语言/框架 + +### Pull Request 流程 + +1. Fork 仓库 +2. 创建功能分支:`git checkout -b feature/add-go-support` +3. 进行修改 +4. 提交(见下文 commit 格式) +5. 推送到 fork:`git push origin feature/add-go-support` +6. 创建 Pull Request + +### Commit 消息格式 + +``` +类型: 简短描述 + +详细说明(如需要) + +- 具体变更 1 +- 具体变更 2 +``` + +**类型**: +- `feat`: 新功能或新内容 +- `fix`: 修复错误 +- `docs`: 仅文档变更 +- `refactor`: 重构(不改变功能) +- `chore`: 维护性工作 + +**示例**: +``` +feat: 添加 Go 语言代码审查指南 + +- 新增 reference/go.md +- 覆盖错误处理、并发、接口设计 +- 更新 SKILL.md 索引表 +``` + +--- + +## Skill 设计原则 + +### 单一职责 + +每个 Skill 专注一个核心能力。本 Skill 专注于**代码审查**,不应扩展到: +- 代码生成 +- 项目初始化 +- 部署配置 + +### 版本管理 + +- 在 reference 文件中标注框架/语言版本 +- 更新时在 commit 中说明版本变化 +- 过时内容应更新而非删除(除非完全废弃) + +### 内容质量 + +- 所有建议应有依据(官方文档、最佳实践) +- 避免主观偏好(如代码风格),专注于客观问题 +- 优先覆盖常见陷阱和安全问题 + +--- + +## 常见问题 + +### Q: 如何测试我的更改? + +将修改后的 Skill 复制到 `~/.claude/skills/` 目录,然后在 Claude Code 中测试: +```bash +cp -r ai-code-review-guide ~/.claude/skills/code-review-excellence +``` + +### Q: 我应该更新 SKILL.md 还是 reference 文件? + +- **SKILL.md**:只修改索引表或核心原则 +- **reference/*.md**:添加/更新具体的语言或框架内容 + +### Q: 如何处理过时的内容? + +1. 标注版本变化(如 "React 18 → React 19") +2. 保留旧版本内容(如果仍有用户使用) +3. 在 checklist 中更新相关项 + +--- + +## 问题咨询 + +如有任何问题,欢迎在 GitHub Issues 中提问。 diff --git a/.github/skills/code-review-skill-main/LICENSE b/.github/skills/code-review-skill-main/LICENSE new file mode 100644 index 0000000000..8217668259 --- /dev/null +++ b/.github/skills/code-review-skill-main/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 tt-a1i + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.github/skills/code-review-skill-main/README.md b/.github/skills/code-review-skill-main/README.md new file mode 100644 index 0000000000..084d637565 --- /dev/null +++ b/.github/skills/code-review-skill-main/README.md @@ -0,0 +1,389 @@ +# Code Review Excellence + +[English](#english) | [中文](#中文) + +--- + +## English + +> A modular code review skill for Claude Code, covering React 19, Vue 3, Rust, TypeScript, Java, Python, C/C++, CSS/Less/Sass, architecture design, and performance optimization. + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +### Overview + +This is a Claude Code skill designed to help developers conduct effective code reviews. It provides: + +- **Language-specific patterns** for React 19, Vue 3, Rust, TypeScript/JavaScript, Java, Python, C/C++ +- **Modern framework support** including React Server Components, TanStack Query v5, Suspense & Streaming +- **Comprehensive checklists** for security, performance, and code quality +- **Best practices** for giving constructive feedback +- **Modular structure** for on-demand loading (reduces context usage) + +### Features + +#### Supported Languages & Frameworks + +| Category | Coverage | +|----------|----------| +| **React** | Hooks rules, useEffect patterns, useMemo/useCallback, React 19 Actions (useActionState, useFormStatus, useOptimistic), Server Components, Suspense & Streaming | +| **Vue 3** | Composition API, reactivity system, defineProps/defineEmits, watch cleanup | +| **Rust** | Ownership & borrowing, unsafe code review, async/await, error handling (thiserror vs anyhow) | +| **TypeScript** | Type safety, async/await patterns, common pitfalls | +| **Java** | Java 17/21 features (Records, Switch), Spring Boot 3, Virtual Threads, Stream API best practices | +| **Go** | Error handling, goroutines/channels, context propagation, interface design, testing patterns | +| **C** | Pointer safety, UB pitfalls, resource management, error handling | +| **C++** | RAII, ownership, move semantics, exception safety, performance | +| **CSS/Less/Sass** | CSS variables, !important usage, performance optimization, responsive design, browser compatibility | +| **TanStack Query** | v5 best practices, queryOptions, useSuspenseQuery, optimistic updates | +| **Architecture** | SOLID principles, anti-patterns, coupling/cohesion, layered architecture | +| **Performance** | Core Web Vitals, N+1 queries, memory leaks, algorithm complexity | + +#### Content Statistics + +| File | Lines | Description | +|------|-------|-------------| +| **SKILL.md** | ~190 | Core principles + index (loads on skill activation) | +| **reference/react.md** | ~870 | React 19/Next.js/TanStack Query v5 patterns (on-demand) | +| **reference/vue.md** | ~920 | Vue 3.5 patterns + Composition API (on-demand) | +| **reference/rust.md** | ~840 | Rust async/ownership/cancellation safety (on-demand) | +| **reference/typescript.md** | ~540 | TypeScript generics/strict mode/ESLint (on-demand) | +| **reference/java.md** | ~800 | Java 17/21 & Spring Boot 3 patterns (on-demand) | +| **reference/python.md** | ~1070 | Python async/typing/pytest (on-demand) | +| **reference/go.md** | ~990 | Go goroutines/channels/context/interfaces (on-demand) | +| **reference/c.md** | ~210 | C memory safety/UB/error handling (on-demand) | +| **reference/cpp.md** | ~300 | C++ RAII/lifetime/move semantics (on-demand) | +| **reference/css-less-sass.md** | ~660 | CSS/Less/Sass variables/performance/responsive (on-demand) | +| **reference/architecture-review-guide.md** | ~470 | SOLID/anti-patterns/coupling analysis (on-demand) | +| **reference/performance-review-guide.md** | ~750 | Core Web Vitals/N+1/memory/complexity (on-demand) | + +**Total: ~9,500 lines** of review guidelines and code examples, loaded on-demand per language. + +### Installation + +#### For Claude Code Users + +Copy the skill to your Claude Code skills directory: + +```bash +# Clone the repository +git clone https://github.com/tt-a1i/code-review-skill.git + +# Copy to Claude Code skills directory +cp -r code-review-skill ~/.claude/skills/code-review-excellence +``` + +Or add to your existing Claude Code plugin: + +```bash +# Copy the entire directory structure +cp -r code-review-skill ~/.claude/plugins/your-plugin/skills/code-review/ +``` + +### Usage + +Once installed, you can invoke the skill in Claude Code: + +``` +Use code-review-excellence skill to review this PR +``` + +Or reference it in your custom commands. + +### File Structure + +``` +code-review-skill/ +├── SKILL.md # Core skill (loads immediately) +├── README.md # This file +├── LICENSE # MIT License +├── CONTRIBUTING.md # Contribution guidelines +├── reference/ # On-demand loaded guides +│ ├── react.md # React/Next.js patterns (on-demand) +│ ├── vue.md # Vue 3 patterns (on-demand) +│ ├── rust.md # Rust patterns (on-demand) +│ ├── typescript.md # TypeScript/JS patterns (on-demand) +│ ├── java.md # Java patterns (on-demand) +│ ├── python.md # Python patterns (on-demand) +│ ├── go.md # Go patterns (on-demand) +│ ├── c.md # C patterns (on-demand) +│ ├── cpp.md # C++ patterns (on-demand) +│ ├── css-less-sass.md # CSS/Less/Sass patterns (on-demand) +│ ├── architecture-review-guide.md # Architecture design review (on-demand) +│ ├── performance-review-guide.md # Performance review (on-demand) +│ ├── common-bugs-checklist.md # Language-specific bug patterns +│ ├── security-review-guide.md # Security review checklist +│ └── code-review-best-practices.md +├── assets/ +│ ├── review-checklist.md # Quick reference checklist +│ └── pr-review-template.md # PR review comment template +└── scripts/ + └── pr-analyzer.py # PR complexity analyzer +``` + +### On-Demand Loading + +This skill uses **Progressive Disclosure** to minimize context usage: + +1. **SKILL.md** (~180 lines) loads when the skill is activated +2. **Language-specific files** load only when reviewing that language +3. **Reference files** load only when explicitly needed + +This means reviewing a React PR only loads SKILL.md + react.md, not Vue/Rust/Python content. + +### Key Topics Covered + +#### Java & Spring Boot + +- **Java 17/21 Features**: Records, Pattern Matching for Switch, Text Blocks +- **Virtual Threads**: High-throughput I/O with Project Loom +- **Spring Boot 3**: Constructor Injection, `@ConfigurationProperties`, ProblemDetail +- **JPA Performance**: Solving N+1 problems, correct Entity design (equals/hashCode) + +#### React 19 + +- `useActionState` - Unified form state management +- `useFormStatus` - Access parent form status without prop drilling +- `useOptimistic` - Optimistic UI updates with automatic rollback +- Server Actions integration with Next.js 15+ + +#### Suspense & Streaming SSR + +- Suspense boundary design patterns +- Error Boundary integration +- Next.js 15 streaming with `loading.tsx` +- `use()` Hook for Promise consumption + +#### TanStack Query v5 + +- `queryOptions` for type-safe query definitions +- `useSuspenseQuery` best practices +- Optimistic updates (simplified v5 approach) +- `isPending` vs `isLoading` vs `isFetching` + +#### Rust + +- Ownership patterns and common pitfalls +- `unsafe` code review requirements (SAFETY comments) +- Async/await patterns (avoiding blocking in async context) +- Error handling (thiserror for libraries, anyhow for applications) + +#### C/C++ + +- **C**: Pointer safety, UB pitfalls, resource cleanup, integer overflow +- **C++**: RAII ownership, Rule of 0/3/5, move semantics, exception safety + +### Contributing + +Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +#### Areas for Contribution + +- Additional language support (C#, Swift, Kotlin, etc.) +- More framework-specific patterns +- Translations to other languages +- Bug pattern submissions + +### License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +### References + +- [React v19 Official Documentation](https://react.dev/blog/2024/12/05/react-19) +- [TanStack Query v5 Documentation](https://tanstack.com/query/latest) +- [Vue 3 Composition API](https://vuejs.org/guide/extras/composition-api-faq.html) +- [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) + +--- + +## 中文 + +> 一个模块化的 Claude Code 代码审查技能,覆盖 React 19、Vue 3、Rust、TypeScript、Java、Python、C/C++、CSS/Less/Sass、架构设计和性能优化。 + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +### 概述 + +这是一个为 Claude Code 设计的代码审查技能,旨在帮助开发者进行高效的代码审查。它提供: + +- **语言特定模式**:覆盖 React 19、Vue 3、Rust、TypeScript/JavaScript、Java、Python、C/C++ +- **现代框架支持**:包括 React Server Components、TanStack Query v5、Suspense & Streaming +- **全面的检查清单**:安全、性能和代码质量检查 +- **最佳实践**:如何提供建设性的反馈 +- **模块化结构**:按需加载,减少上下文占用 + +### 特性 + +#### 支持的语言和框架 + +| 分类 | 覆盖内容 | +|------|----------| +| **React** | Hooks 规则、useEffect 模式、useMemo/useCallback、React 19 Actions(useActionState、useFormStatus、useOptimistic)、Server Components、Suspense & Streaming | +| **Vue 3** | Composition API、响应性系统、defineProps/defineEmits、watch 清理 | +| **Rust** | 所有权与借用、unsafe 代码审查、async/await、错误处理(thiserror vs anyhow) | +| **TypeScript** | 类型安全、async/await 模式、常见陷阱 | +| **Java** | Java 17/21 特性(Records, Switch)、Spring Boot 3、虚拟线程、Stream API 最佳实践 | +| **Go** | 错误处理、goroutine/channel、context 传播、接口设计、测试模式 | +| **C** | 指针/缓冲区安全、UB、资源管理、错误处理 | +| **C++** | RAII、生命周期、Rule of 0/3/5、异常安全 | +| **CSS/Less/Sass** | CSS 变量规范、!important 使用、性能优化、响应式设计、浏览器兼容性 | +| **TanStack Query** | v5 最佳实践、queryOptions、useSuspenseQuery、乐观更新 | +| **架构设计** | SOLID 原则、架构反模式、耦合度/内聚性、分层架构 | +| **性能优化** | Core Web Vitals、N+1 查询、内存泄漏、算法复杂度 | + +#### 内容统计 + +| 文件 | 行数 | 描述 | +|------|------|------| +| **SKILL.md** | ~190 | 核心原则 + 索引(技能激活时加载)| +| **reference/react.md** | ~870 | React 19/Next.js/TanStack Query v5(按需加载)| +| **reference/vue.md** | ~920 | Vue 3.5 + Composition API(按需加载)| +| **reference/rust.md** | ~840 | Rust async/所有权/取消安全性(按需加载)| +| **reference/typescript.md** | ~540 | TypeScript 泛型/strict 模式/ESLint(按需加载)| +| **reference/java.md** | ~800 | Java 17/21 & Spring Boot 3 模式(按需加载)| +| **reference/python.md** | ~1070 | Python async/类型注解/pytest(按需加载)| +| **reference/go.md** | ~990 | Go goroutine/channel/context/接口(按需加载)| +| **reference/c.md** | ~210 | C 内存安全/UB/错误处理(按需加载)| +| **reference/cpp.md** | ~300 | C++ RAII/生命周期/移动语义(按需加载)| +| **reference/css-less-sass.md** | ~660 | CSS/Less/Sass 变量/性能/响应式(按需加载)| +| **reference/architecture-review-guide.md** | ~470 | SOLID/反模式/耦合度分析(按需加载)| +| **reference/performance-review-guide.md** | ~750 | Core Web Vitals/N+1/内存/复杂度(按需加载)| + +**总计:9,500 行**审查指南和代码示例,按语言按需加载。 + +### 安装 + +#### Claude Code 用户 + +将技能复制到 Claude Code skills 目录: + +```bash +# 克隆仓库 +git clone https://github.com/tt-a1i/code-review-skill.git + +# 复制到 Claude Code skills 目录 +cp -r code-review-skill ~/.claude/skills/code-review-excellence +``` + +或添加到现有的 Claude Code 插件: + +```bash +# 复制整个目录结构 +cp -r code-review-skill ~/.claude/plugins/your-plugin/skills/code-review/ +``` + +### 使用方法 + +安装后,可以在 Claude Code 中调用该技能: + +``` +使用 code-review-excellence skill 来审查这个 PR +``` + +或在自定义命令中引用。 + +### 文件结构 + +``` +code-review-skill/ +├── SKILL.md # 核心技能(立即加载) +├── README.md # 本文件 +├── LICENSE # MIT 许可证 +├── CONTRIBUTING.md # 贡献指南 +├── reference/ # 按需加载的指南 +│ ├── react.md # React/Next.js 模式(按需加载) +│ ├── vue.md # Vue 3 模式(按需加载) +│ ├── rust.md # Rust 模式(按需加载) +│ ├── typescript.md # TypeScript/JS 模式(按需加载) +│ ├── java.md # Java 模式(按需加载) +│ ├── python.md # Python 模式(按需加载) +│ ├── go.md # Go 模式(按需加载) +│ ├── c.md # C 模式(按需加载) +│ ├── cpp.md # C++ 模式(按需加载) +│ ├── css-less-sass.md # CSS/Less/Sass 模式(按需加载) +│ ├── architecture-review-guide.md # 架构设计审查(按需加载) +│ ├── performance-review-guide.md # 性能审查(按需加载) +│ ├── common-bugs-checklist.md # 语言特定的错误模式 +│ ├── security-review-guide.md # 安全审查清单 +│ └── code-review-best-practices.md +├── assets/ +│ ├── review-checklist.md # 快速参考清单 +│ └── pr-review-template.md # PR 审查评论模板 +└── scripts/ + └── pr-analyzer.py # PR 复杂度分析器 +``` + +### 按需加载机制 + +此技能使用 **Progressive Disclosure(渐进式披露)** 来最小化上下文占用: + +1. **SKILL.md**(~180 行)在技能激活时加载 +2. **语言特定文件** 仅在审查该语言时加载 +3. **参考文件** 仅在明确需要时加载 + +这意味着审查 React PR 时只加载 SKILL.md + react.md,不会加载 Vue/Rust/Python 内容。 + +### 核心内容 + +#### Java & Spring Boot + +- **Java 17/21 特性**:Records、Switch 模式匹配、文本块 +- **虚拟线程**:Project Loom 带来的高吞吐量 I/O +- **Spring Boot 3**:构造器注入、`@ConfigurationProperties`、ProblemDetail +- **JPA 性能**:解决 N+1 问题、正确的 Entity 设计(equals/hashCode) + +#### React 19 + +- `useActionState` - 统一的表单状态管理 +- `useFormStatus` - 无需 props 透传即可访问父表单状态 +- `useOptimistic` - 带自动回滚的乐观 UI 更新 +- 与 Next.js 15+ Server Actions 集成 + +#### Suspense & Streaming SSR + +- Suspense 边界设计模式 +- Error Boundary 集成 +- Next.js 15 streaming 与 `loading.tsx` +- `use()` Hook 消费 Promise + +#### TanStack Query v5 + +- `queryOptions` 类型安全的查询定义 +- `useSuspenseQuery` 最佳实践 +- 乐观更新(v5 简化方案) +- `isPending` vs `isLoading` vs `isFetching` 区别 + +#### Rust + +- 所有权模式和常见陷阱 +- `unsafe` 代码审查要求(SAFETY 注释) +- Async/await 模式(避免在异步上下文中阻塞) +- 错误处理(库用 thiserror,应用用 anyhow) + +#### C/C++ + +- **C**:指针/缓冲区安全、UB、资源清理、整数溢出 +- **C++**:RAII 所有权、Rule of 0/3/5、移动语义、异常安全 + +### 贡献 + +欢迎贡献!请阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 了解贡献指南。 + +#### 可贡献的方向 + +- 添加更多语言支持(C#、Swift、Kotlin 等) +- 更多框架特定模式 +- 翻译成其他语言 +- 提交错误模式 + +### 许可证 + +本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件。 + +### 参考资料 + +- [React v19 官方文档](https://react.dev/blog/2024/12/05/react-19) +- [TanStack Query v5 文档](https://tanstack.com/query/latest) +- [Vue 3 Composition API](https://vuejs.org/guide/extras/composition-api-faq.html) +- [Rust API 指南](https://rust-lang.github.io/api-guidelines/) diff --git a/.github/skills/code-review-skill-main/SKILL.md b/.github/skills/code-review-skill-main/SKILL.md new file mode 100644 index 0000000000..c5b9c6d88c --- /dev/null +++ b/.github/skills/code-review-skill-main/SKILL.md @@ -0,0 +1,197 @@ +--- +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, 性能优化, 响应式, 兼容性 | + +## 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/.github/skills/code-review-skill-main/assets/pr-review-template.md b/.github/skills/code-review-skill-main/assets/pr-review-template.md new file mode 100644 index 0000000000..33f7e71daa --- /dev/null +++ b/.github/skills/code-review-skill-main/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/.github/skills/code-review-skill-main/assets/review-checklist.md b/.github/skills/code-review-skill-main/assets/review-checklist.md new file mode 100644 index 0000000000..4ff3b8b6ac --- /dev/null +++ b/.github/skills/code-review-skill-main/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/.github/skills/code-review-skill-main/reference/architecture-review-guide.md b/.github/skills/code-review-skill-main/reference/architecture-review-guide.md new file mode 100644 index 0000000000..abde68ce48 --- /dev/null +++ b/.github/skills/code-review-skill-main/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/.github/skills/code-review-skill-main/reference/c.md b/.github/skills/code-review-skill-main/reference/c.md new file mode 100644 index 0000000000..cfd31ed1d1 --- /dev/null +++ b/.github/skills/code-review-skill-main/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/.github/skills/code-review-skill-main/reference/code-review-best-practices.md b/.github/skills/code-review-skill-main/reference/code-review-best-practices.md new file mode 100644 index 0000000000..8c6b9cdb19 --- /dev/null +++ b/.github/skills/code-review-skill-main/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/.github/skills/code-review-skill-main/reference/common-bugs-checklist.md b/.github/skills/code-review-skill-main/reference/common-bugs-checklist.md new file mode 100644 index 0000000000..97e2e637f2 --- /dev/null +++ b/.github/skills/code-review-skill-main/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/.github/skills/code-review-skill-main/reference/cpp.md b/.github/skills/code-review-skill-main/reference/cpp.md new file mode 100644 index 0000000000..58743f688b --- /dev/null +++ b/.github/skills/code-review-skill-main/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/.github/skills/code-review-skill-main/reference/css-less-sass.md b/.github/skills/code-review-skill-main/reference/css-less-sass.md new file mode 100644 index 0000000000..638854a584 --- /dev/null +++ b/.github/skills/code-review-skill-main/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/.github/skills/code-review-skill-main/reference/go.md b/.github/skills/code-review-skill-main/reference/go.md new file mode 100644 index 0000000000..2438fea5b7 --- /dev/null +++ b/.github/skills/code-review-skill-main/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/.github/skills/code-review-skill-main/reference/java.md b/.github/skills/code-review-skill-main/reference/java.md new file mode 100644 index 0000000000..2e4e177c36 --- /dev/null +++ b/.github/skills/code-review-skill-main/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/.github/skills/code-review-skill-main/reference/performance-review-guide.md b/.github/skills/code-review-skill-main/reference/performance-review-guide.md new file mode 100644 index 0000000000..87a8ba7371 --- /dev/null +++ b/.github/skills/code-review-skill-main/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/.github/skills/code-review-skill-main/reference/python.md b/.github/skills/code-review-skill-main/reference/python.md new file mode 100644 index 0000000000..764db0e1df --- /dev/null +++ b/.github/skills/code-review-skill-main/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/.github/skills/code-review-skill-main/reference/react.md b/.github/skills/code-review-skill-main/reference/react.md new file mode 100644 index 0000000000..1cce2fc680 --- /dev/null +++ b/.github/skills/code-review-skill-main/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/.github/skills/code-review-skill-main/reference/rust.md b/.github/skills/code-review-skill-main/reference/rust.md new file mode 100644 index 0000000000..1fa062ca96 --- /dev/null +++ b/.github/skills/code-review-skill-main/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/.github/skills/code-review-skill-main/reference/security-review-guide.md b/.github/skills/code-review-skill-main/reference/security-review-guide.md new file mode 100644 index 0000000000..80d10bc093 --- /dev/null +++ b/.github/skills/code-review-skill-main/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/.github/skills/code-review-skill-main/reference/typescript.md b/.github/skills/code-review-skill-main/reference/typescript.md new file mode 100644 index 0000000000..4699f6bf3b --- /dev/null +++ b/.github/skills/code-review-skill-main/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/.github/skills/code-review-skill-main/reference/vue.md b/.github/skills/code-review-skill-main/reference/vue.md new file mode 100644 index 0000000000..cbd9165d32 --- /dev/null +++ b/.github/skills/code-review-skill-main/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/.github/skills/code-review-skill-main/scripts/pr-analyzer.py b/.github/skills/code-review-skill-main/scripts/pr-analyzer.py new file mode 100644 index 0000000000..7b594e7df0 --- /dev/null +++ b/.github/skills/code-review-skill-main/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/.github/skills/openspec-apply-change/SKILL.md b/.github/skills/openspec-apply-change/SKILL.md index 4d9b8d127c..d474dc1355 100644 --- a/.github/skills/openspec-apply-change/SKILL.md +++ b/.github/skills/openspec-apply-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.1.0" + generatedBy: "1.2.0" --- Implement tasks from an OpenSpec change. diff --git a/.github/skills/openspec-archive-change/SKILL.md b/.github/skills/openspec-archive-change/SKILL.md index d17be3b1b0..9b1f851aaa 100644 --- a/.github/skills/openspec-archive-change/SKILL.md +++ b/.github/skills/openspec-archive-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.1.0" + generatedBy: "1.2.0" --- Archive a completed change in the experimental workflow. @@ -63,7 +63,7 @@ Archive a completed change in the experimental workflow. - If changes needed: "Sync now (recommended)", "Archive without syncing" - If already synced: "Archive now", "Sync anyway", "Cancel" - If user chooses sync, execute /opsx:sync logic (use the openspec-sync-specs skill). Proceed to archive regardless of choice. + If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''. Delta spec analysis: "). Proceed to archive regardless of choice. 5. **Perform the archive** diff --git a/.github/skills/openspec-bulk-archive-change/SKILL.md b/.github/skills/openspec-bulk-archive-change/SKILL.md index 781d9770b6..d2f199af97 100644 --- a/.github/skills/openspec-bulk-archive-change/SKILL.md +++ b/.github/skills/openspec-bulk-archive-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.1.0" + generatedBy: "1.2.0" --- Archive multiple completed changes in a single operation. @@ -229,7 +229,7 @@ Failed K changes: ``` ## No Changes to Archive -No active changes found. Use `/opsx:new` to create a new change. +No active changes found. Create a new change to get started. ``` **Guardrails** diff --git a/.github/skills/openspec-continue-change/SKILL.md b/.github/skills/openspec-continue-change/SKILL.md index bf63f20b2e..a2856f0496 100644 --- a/.github/skills/openspec-continue-change/SKILL.md +++ b/.github/skills/openspec-continue-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.1.0" + generatedBy: "1.2.0" --- Continue working on a change by creating the next artifact. diff --git a/.github/skills/openspec-explore/SKILL.md b/.github/skills/openspec-explore/SKILL.md index 2f63fb9483..ffa10cad72 100644 --- a/.github/skills/openspec-explore/SKILL.md +++ b/.github/skills/openspec-explore/SKILL.md @@ -6,12 +6,12 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.1.0" + generatedBy: "1.2.0" --- Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. -**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first (e.g., start a change with `/opsx:new` or `/opsx:ff`). You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. +**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. **This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore. @@ -95,8 +95,7 @@ This tells you: Think freely. When insights crystallize, you might offer: -- "This feels solid enough to start a change. Want me to create one?" - → Can transition to `/opsx:new` or `/opsx:ff` +- "This feels solid enough to start a change. Want me to create a proposal?" - Or keep exploring - no pressure to formalize ### When a change exists @@ -252,7 +251,7 @@ You: That changes everything. There's no required ending. Discovery might: -- **Flow into action**: "Ready to start? /opsx:new or /opsx:ff" +- **Flow into a proposal**: "Ready to start? I can create a change proposal." - **Result in artifact updates**: "Updated design.md with these decisions" - **Just provide clarity**: User has what they need, moves on - **Continue later**: "We can pick this up anytime" @@ -269,8 +268,7 @@ When it feels like things are crystallizing, you might summarize: **Open questions**: [if any remain] **Next steps** (if ready): -- Create a change: /opsx:new -- Fast-forward to tasks: /opsx:ff +- Create a change proposal - Keep exploring: just keep talking ``` diff --git a/.github/skills/openspec-ff-change/SKILL.md b/.github/skills/openspec-ff-change/SKILL.md index 8b2dbbe48f..d5f12043c4 100644 --- a/.github/skills/openspec-ff-change/SKILL.md +++ b/.github/skills/openspec-ff-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.1.0" + generatedBy: "1.2.0" --- Fast-forward through artifact creation - generate everything needed to start implementation in one go. diff --git a/.github/skills/openspec-new-change/SKILL.md b/.github/skills/openspec-new-change/SKILL.md index 195a6cdcf5..607391aac8 100644 --- a/.github/skills/openspec-new-change/SKILL.md +++ b/.github/skills/openspec-new-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.1.0" + generatedBy: "1.2.0" --- Start a new change using the experimental artifact-driven approach. diff --git a/.github/skills/openspec-onboard/SKILL.md b/.github/skills/openspec-onboard/SKILL.md index 0f018dbdfd..9076b5d84d 100644 --- a/.github/skills/openspec-onboard/SKILL.md +++ b/.github/skills/openspec-onboard/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.1.0" + generatedBy: "1.2.0" --- Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step. @@ -15,16 +15,19 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t ## Preflight -Before starting, check if OpenSpec is initialized: +Before starting, check if the OpenSpec CLI is installed: ```bash -openspec status --json 2>&1 || echo "NOT_INITIALIZED" +# Unix/macOS +openspec --version 2>&1 || echo "CLI_NOT_INSTALLED" +# Windows (PowerShell) +# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" } ``` -**If not initialized:** -> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`. +**If CLI not installed:** +> OpenSpec CLI is not installed. Install it first, then come back to `/opsx:onboard`. -Stop here if not initialized. +Stop here if not installed. --- @@ -67,7 +70,10 @@ Scan the codebase for small improvement opportunities. Look for: Also check recent git activity: ```bash +# Unix/macOS git log --oneline -10 2>/dev/null || echo "No git history" +# Windows (PowerShell) +# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" } ``` ### Present Suggestions @@ -262,7 +268,10 @@ For a small task like this, we might only need one spec file. **DO:** Create the spec file: ```bash +# Unix/macOS mkdir -p openspec/changes//specs/ +# Windows (PowerShell) +# New-Item -ItemType Directory -Force -Path "openspec/changes//specs/" ``` Draft the spec content: @@ -457,21 +466,29 @@ This same rhythm works for any size change—a small fix or a major feature. ## Command Reference +**Core workflow:** + | Command | What it does | |---------|--------------| +| `/opsx:propose` | Create a change and generate all artifacts | | `/opsx:explore` | Think through problems before/during work | -| `/opsx:new` | Start a new change, step through artifacts | -| `/opsx:ff` | Fast-forward: create all artifacts at once | -| `/opsx:continue` | Continue working on an existing change | | `/opsx:apply` | Implement tasks from a change | -| `/opsx:verify` | Verify implementation matches artifacts | | `/opsx:archive` | Archive a completed change | +**Additional commands:** + +| Command | What it does | +|---------|--------------| +| `/opsx:new` | Start a new change, step through artifacts one at a time | +| `/opsx:continue` | Continue working on an existing change | +| `/opsx:ff` | Fast-forward: create all artifacts at once | +| `/opsx:verify` | Verify implementation matches artifacts | + --- ## What's Next? -Try `/opsx:new` or `/opsx:ff` on something you actually want to build. You've got the rhythm now! +Try `/opsx:propose` on something you actually want to build. You've got the rhythm now! ``` --- @@ -501,17 +518,25 @@ If the user says they just want to see the commands or skip the tutorial: ``` ## OpenSpec Quick Reference +**Core workflow:** + | Command | What it does | |---------|--------------| +| `/opsx:propose ` | Create a change and generate all artifacts | | `/opsx:explore` | Think through problems (no code changes) | +| `/opsx:apply ` | Implement tasks | +| `/opsx:archive ` | Archive when done | + +**Additional commands:** + +| Command | What it does | +|---------|--------------| | `/opsx:new ` | Start a new change, step by step | -| `/opsx:ff ` | Fast-forward: all artifacts at once | | `/opsx:continue ` | Continue an existing change | -| `/opsx:apply ` | Implement tasks | +| `/opsx:ff ` | Fast-forward: all artifacts at once | | `/opsx:verify ` | Verify implementation | -| `/opsx:archive ` | Archive when done | -Try `/opsx:new` to start your first change, or `/opsx:ff` if you want to move fast. +Try `/opsx:propose` to start your first change. ``` Exit gracefully. diff --git a/.github/skills/openspec-sync-specs/SKILL.md b/.github/skills/openspec-sync-specs/SKILL.md index 7e8dff16e8..353bfac96b 100644 --- a/.github/skills/openspec-sync-specs/SKILL.md +++ b/.github/skills/openspec-sync-specs/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.1.0" + generatedBy: "1.2.0" --- Sync delta specs from a change to main specs. diff --git a/.github/skills/openspec-verify-change/SKILL.md b/.github/skills/openspec-verify-change/SKILL.md index 77a0c71217..744a08839d 100644 --- a/.github/skills/openspec-verify-change/SKILL.md +++ b/.github/skills/openspec-verify-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.1.0" + generatedBy: "1.2.0" --- Verify that an implementation matches the change artifacts (specs, tasks, design). diff --git a/.github/workflows/patch-installer-cd.yml b/.github/workflows/patch-installer-cd.yml index 4233836bc1..02b397a555 100644 --- a/.github/workflows/patch-installer-cd.yml +++ b/.github/workflows/patch-installer-cd.yml @@ -151,7 +151,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v5 with: - dotnet-version: | + dotnet-version: | 3.1.x 5.0.x @@ -166,7 +166,7 @@ jobs: choco install wixtoolset --version 3.11.2 --allow-downgrade --force echo "C:\Program Files (x86)\WiX Toolset v3.11\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append if: github.event_name != 'pull_request' - + - name: Import Base Build Artifacts shell: pwsh run: | @@ -211,7 +211,7 @@ jobs: ) $valueName = "Switch.System.DisableTempFileCollectionDirectoryFeature" $expectedValue = "true" - + foreach ($path in $regPaths) { Write-Host "Adding or updating registry value in $path..." if (-not (Test-Path $path)) { @@ -238,11 +238,11 @@ jobs: $results = Select-String -Path "build.log" -Pattern "^\s*[1-9][0-9]* Error\(s\)" if ($results) { foreach ($result in $results) { - Write-Host "Found errors in build.log $($result.LineNumber): $($result.Line)" -ForegroundColor red + Write-Host "Found errors in build.log $($result.LineNumber): $($result.Line)" -ForegroundColor red } exit 1 } else { - Write-Host "No errors found" -ForegroundColor green + Write-Host "No errors found" -ForegroundColor green exit 0 } diff --git a/.vscode/settings.json b/.vscode/settings.json index 91244032f3..8db5c83c67 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -96,11 +96,7 @@ // Tasks not listed require manual confirmation. // ============================================================================ "chat.tools.tasks.allowedTasks": [ - // Build tasks (safe - only produce artifacts) - "Build", - "Build Release", - - // Test tasks (safe - no external changes) + // Test tasks remain auto-approved so Linux hosts surface the not-supported message. "Test", "Test (no build)", @@ -126,6 +122,9 @@ ] // ============================================================================ // TASKS REQUIRING MANUAL APPROVAL (not in allowedTasks): + // - Build (Windows only) + // - Build Release (Windows only) + // - Build Tests (Windows only) // - Test (with filter) (has user input prompt) // - Test (specific project) (has user input prompt) // - Agent: Propose updates (modifies files) diff --git a/AGENTS.md b/AGENTS.md index 888a0a1cdb..340f53a961 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,10 +8,6 @@ Minimal, high-signal guidance for coding agents in this repository. - Build with `.\build.ps1`. - Test with `.\test.ps1`. - Do not bypass repository scripts for normal build/test work. -- Before committing or pushing, run the existing VS Code task `CI: Full local check`. -- After any rebase, merge, cherry-pick, or manual conflict resolution, run `CI: Whitespace check` before committing. -- If `CI: Whitespace check` rewrites files, review and restage those files, then rerun the task until it passes cleanly. -- When commit history changes, run `CI: Commit messages` before pushing. ## Critical constraints @@ -25,6 +21,10 @@ Minimal, high-signal guidance for coding agents in this repository. - Apply `.github/instructions/navigation.instructions.md` for structural navigation and hidden-dependency handling. - Use `Src/AGENTS.md`, `.github/AGENTS.md`, `FLExInstaller/AGENTS.md`, and `openspec/AGENTS.md` for area-specific guidance. +## External Dependencies (LibLcm) + +FieldWorks is built upon the `liblcm` (Language & Culture Model) repository, which provides the main data model and FDO (FieldWorks Data Objects) layers used by FieldWorks. The liblcm library is the core FieldWorks model for language and culture data and includes interfaces like `IScrFootnoteFactory` that FieldWorks consumes. If you cannot find a core data model definition within this workspace, ask for access to the `liblcm` repository to reference the source. + ## Serena navigation - Prefer Serena symbolic tools for code discovery/navigation before broad file reads. @@ -40,6 +40,5 @@ Minimal, high-signal guidance for coding agents in this repository. ## Validation checklist 1. Run the relevant build/test scripts for touched areas. -2. Run `CI: Full local check` before commit/push; use `CI: Whitespace check` immediately after conflict resolution. -3. Keep edits scoped and avoid unrelated refactors. -4. Update docs only when behavior/contracts/process changed. +2. Keep edits scoped and avoid unrelated refactors. +3. Update docs only when behavior/contracts/process changed. diff --git a/Build/Agent/FwBuildHelpers.psm1 b/Build/Agent/FwBuildHelpers.psm1 index a785423157..3a6fc213c8 100644 --- a/Build/Agent/FwBuildHelpers.psm1 +++ b/Build/Agent/FwBuildHelpers.psm1 @@ -474,6 +474,110 @@ function Test-GitTrackedFile { } } +function Get-NewestWriteTimeUtc { + <#! + .SYNOPSIS + Returns the newest LastWriteTimeUtc among matching files under one or more roots. + #> + param( + [Parameter(Mandatory)][string[]]$Paths, + [string[]]$IncludePatterns = @() + ) + + $latest = [datetime]::MinValue + foreach ($path in $Paths) { + if (-not (Test-Path $path)) { + continue + } + + $items = Get-ChildItem -Path $path -Recurse -File -ErrorAction SilentlyContinue + if ($IncludePatterns.Count -gt 0) { + $items = $items | Where-Object { + $fileName = $_.Name + foreach ($pattern in $IncludePatterns) { + if ($fileName -like $pattern) { + return $true + } + } + return $false + } + } + + foreach ($item in $items) { + if ($item.LastWriteTimeUtc -gt $latest) { + $latest = $item.LastWriteTimeUtc + } + } + } + + return $latest +} + +function Get-OldestWriteTimeUtc { + <#! + .SYNOPSIS + Returns the oldest LastWriteTimeUtc across a set of required artifact files. + Returns $null if any required artifact is missing. + #> + param( + [Parameter(Mandatory)][string[]]$Paths + ) + + $oldest = [datetime]::MaxValue + $foundAny = $false + foreach ($path in $Paths) { + if (-not (Test-Path $path)) { + return $null + } + + $item = Get-Item -LiteralPath $path -ErrorAction Stop + $foundAny = $true + if ($item.LastWriteTimeUtc -lt $oldest) { + $oldest = $item.LastWriteTimeUtc + } + } + + if (-not $foundAny) { + return $null + } + + return $oldest +} + +function Test-ViewsNativeArtifactsStale { + <#! + .SYNOPSIS + Returns $true when the Views/FwKernel native artifacts are missing or older than relevant native inputs. + #> + param( + [Parameter(Mandatory)][string]$RepoRoot, + [Parameter(Mandatory)][string]$Configuration + ) + + $sourceRoots = @( + (Join-Path $RepoRoot 'Src\views'), + (Join-Path $RepoRoot 'Src\Kernel'), + (Join-Path $RepoRoot 'Src\Generic'), + (Join-Path $RepoRoot 'Include') + ) + $sourcePatterns = @('*.cpp', '*.c', '*.cc', '*.h', '*.hpp', '*.ixx', '*.idl', '*.rc', '*.mak', '*.def', '*.bat') + $artifactPaths = @( + (Join-Path $RepoRoot "Output\$Configuration\Views.dll"), + (Join-Path $RepoRoot "Output\$Configuration\views.lib"), + (Join-Path $RepoRoot "Output\$Configuration\Common\FwKernelTlb.h"), + (Join-Path $RepoRoot "Obj\$Configuration\Views\autopch\VwRootBox.obj") + ) + + $latestSource = Get-NewestWriteTimeUtc -Paths $sourceRoots -IncludePatterns $sourcePatterns + $oldestArtifact = Get-OldestWriteTimeUtc -Paths $artifactPaths + + if ($oldestArtifact -eq $null) { + return $true + } + + return $latestSource -gt $oldestArtifact +} + # ============================================================================= # Module Exports # ============================================================================= @@ -493,5 +597,8 @@ Export-ModuleMember -Function @( 'Exit-WorktreeLock', 'Remove-StaleObjFolders', 'Test-IsFileLockError', - 'Invoke-WithFileLockRetry' + 'Invoke-WithFileLockRetry', + 'Get-NewestWriteTimeUtc', + 'Get-OldestWriteTimeUtc', + 'Test-ViewsNativeArtifactsStale' ) diff --git a/Build/Agent/README.md b/Build/Agent/README.md index b7f4e6b0a1..3f6ae8bd72 100644 --- a/Build/Agent/README.md +++ b/Build/Agent/README.md @@ -11,6 +11,7 @@ PowerShell scripts for build, test, and CI orchestration. | Script | Purpose | |--------|---------| +| `Run-AllRenders.ps1` | Runs the DetailControls and RootSite render suites sequentially from one command without adding a meta test project. | | `Summarize-NativeTestResults.ps1` | Parses native Unit++ logs and appends a pass/fail summary table to GitHub step summary. | ## GitHub Actions usage diff --git a/Build/Agent/Verify-FwDependencies.ps1 b/Build/Agent/Verify-FwDependencies.ps1 index 10c943dc16..94c86ee200 100644 --- a/Build/Agent/Verify-FwDependencies.ps1 +++ b/Build/Agent/Verify-FwDependencies.ps1 @@ -64,6 +64,8 @@ function Test-Dependency { [string]$Required = "Required" ) + $isRequired = ($Required -eq "Required") + try { $result = & $Check if ($result) { @@ -73,18 +75,18 @@ function Test-Dependency { Write-Host " $result" -ForegroundColor DarkGray } } - return @{ Name = $Name; Found = $true; IsRequired = ($Required -eq "Required"); Info = $result } + return @{ Name = $Name; Found = $true; IsRequired = $isRequired; Info = $result } } else { throw "Check returned null/false" } } catch { - $color = if ($Required -eq "Required") { "Red" } else { "Yellow" } - $status = if ($Required -eq "Required") { "[FAIL]" } else { "[WARN]" } + $color = if ($isRequired) { "Red" } else { "Yellow" } + $status = if ($isRequired) { "[FAIL]" } else { "[WARN]" } Write-Host "$status $Name" -ForegroundColor $color Write-Host " $_" -ForegroundColor DarkGray - return @{ Name = $Name; Found = $false; IsRequired = ($Required -eq "Required"); Error = $_.ToString() } + return @{ Name = $Name; Found = $false; IsRequired = $isRequired; Error = $_.ToString() } } } @@ -297,9 +299,9 @@ if ($Detailed) { Write-Host "=== Summary ===" -ForegroundColor Cyan } -$required = $results | Where-Object { $_.IsRequired -ne $false } -$missing = $required | Where-Object { -not $_.Found } -$optional = $results | Where-Object { $_.IsRequired -eq $false } +$required = @($results | Where-Object { $_.IsRequired -ne $false }) +$missing = @($required | Where-Object { -not $_.Found }) +$optional = @($results | Where-Object { $_.IsRequired -eq $false }) $totalRequired = ($required | Measure-Object).Count $foundRequired = ($required | Where-Object { $_.Found } | Measure-Object).Count diff --git a/Build/Agent/fix-whitespace.ps1 b/Build/Agent/fix-whitespace.ps1 index 324a254a2f..ba24f5200f 100644 --- a/Build/Agent/fix-whitespace.ps1 +++ b/Build/Agent/fix-whitespace.ps1 @@ -10,39 +10,10 @@ function Get-BaseRef { return (Get-DefaultBranchRef) } -function Test-HasUtf8Bom { - param([Parameter(Mandatory)][string]$Path) - - # Read only the first three bytes to check for a UTF-8 BOM to avoid loading the entire file. - $buffer = [byte[]]::new(3) - $stream = [System.IO.File]::OpenRead($Path) - try { - $bytesRead = $stream.Read($buffer, 0, 3) - } - finally { - $stream.Dispose() - } - - if ($bytesRead -lt 3) { return $false } - return $buffer[0] -eq 0xEF -and $buffer[1] -eq 0xBB -and $buffer[2] -eq 0xBF -} - -function Write-Utf8Text { - param( - [Parameter(Mandatory)][string]$Path, - [Parameter(Mandatory)][string]$Content, - [Parameter(Mandatory)][bool]$EmitBom - ) - - $encoding = New-Object System.Text.UTF8Encoding($EmitBom) - [System.IO.File]::WriteAllText($Path, $Content, $encoding) -} - function Format-FileWhitespace { param([Parameter(Mandatory)][string]$Path) if (-not (Test-Path -LiteralPath $Path)) { return } try { - $hasUtf8Bom = Test-HasUtf8Bom -Path $Path $raw = Get-Content -LiteralPath $Path -Raw -Encoding utf8 } catch { @@ -62,7 +33,7 @@ function Format-FileWhitespace { # Join back and ensure exactly one trailing newline $new = ($lines -join "`n") + "`n" if ($new -ne $orig) { - Write-Utf8Text -Path $Path -Content $new -EmitBom $hasUtf8Bom + Set-Content -LiteralPath $Path -Value $new -Encoding utf8 -NoNewline Write-Host "Fixed whitespace: $Path" } } @@ -85,6 +56,5 @@ $files = $fixFiles | Where-Object { $_ -and (Test-Path $_) } foreach ($f in $files) { Format-FileWhitespace -Path $f } -Write-Host "Whitespace fix completed. Review and stage the updated files before committing." -Write-Host "If check-whitespace reported an older commit in origin/main..HEAD, rewrite history with amend, squash, or rebase so that offending commit is no longer part of the branch." +Write-Host "Whitespace fix completed. Review changes, commit, and rebase as needed." exit 0 diff --git a/Build/Installer.legacy.targets b/Build/Installer.legacy.targets index 52afd1e5bb..39fd2b540c 100644 --- a/Build/Installer.legacy.targets +++ b/Build/Installer.legacy.targets @@ -272,6 +272,10 @@ + + + + diff --git a/Build/Src/FwBuildTasks/CollectTargets.cs b/Build/Src/FwBuildTasks/CollectTargets.cs index 6c757b0659..33936bfccf 100644 --- a/Build/Src/FwBuildTasks/CollectTargets.cs +++ b/Build/Src/FwBuildTasks/CollectTargets.cs @@ -129,6 +129,8 @@ public void Generate() ); var infoEth = new DirectoryInfo(Path.Combine(m_fwroot, "Lib/src/Ethnologue")); CollectInfo(infoEth); + var infoScr2 = new DirectoryInfo(Path.Combine(m_fwroot, "Lib/src/ScrChecks")); + CollectInfo(infoScr2); var infoObj = new DirectoryInfo(Path.Combine(m_fwroot, "Lib/src/ObjectBrowser")); CollectInfo(infoObj); @@ -550,6 +552,12 @@ private void WriteTargetFiles() writer.Write("\t - + none all diff --git a/Build/scripts/Invoke-CppTest.ps1 b/Build/scripts/Invoke-CppTest.ps1 index 87f90598ac..e985a4e713 100644 --- a/Build/scripts/Invoke-CppTest.ps1 +++ b/Build/scripts/Invoke-CppTest.ps1 @@ -219,11 +219,9 @@ function Ensure-UnitPlusPlusLibrary { function Ensure-TestViewsPrerequisites { if ($TestProject -ne 'TestViews') { return } - $fwKernelHeader = Join-Path $WorktreePath "Output\$Configuration\Common\FwKernelTlb.h" - $viewsObj = Join-Path $WorktreePath "Obj\$Configuration\Views\autopch\VwRootBox.obj" - if ((Test-Path $fwKernelHeader) -and (Test-Path $viewsObj)) { return } + if (-not (Test-ViewsNativeArtifactsStale -RepoRoot $WorktreePath -Configuration $Configuration)) { return } - Write-Host "[INFO] Missing native artifacts or generated headers required for TestViews." -ForegroundColor Yellow + Write-Host "[INFO] Refreshing native artifacts and generated headers required for TestViews." -ForegroundColor Yellow Build-NativeArtifacts Build-ViewsInterfacesArtifacts } diff --git a/Directory.Build.props b/Directory.Build.props index 7495546d05..4e0bbd7529 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -80,10 +80,24 @@ $(MSBuildThisFileDirectory) + + $(MSBuildThisFileDirectory) + FieldWorks $(FwRoot)DistFiles\ $(FwRoot)Output\ $(FwOutput)$(Configuration)\ $(FwRoot)Obj\ + + $(DefaultItemExcludes);*_wpftmp* + + + <_StaleWpfTemp Include="$(MSBuildProjectDirectory)\*_wpftmp*" /> + + + + \ No newline at end of file diff --git a/Docs/CONTRIBUTING.md b/Docs/CONTRIBUTING.md index 0ce5523f87..ade291d845 100644 --- a/Docs/CONTRIBUTING.md +++ b/Docs/CONTRIBUTING.md @@ -14,7 +14,8 @@ There are several ways you can contribute to the development of FieldWorks: The following steps are required for setting up a FieldWorks development environment on Windows. -> **Note**: FieldWorks is Windows-only. Linux builds are no longer supported. +> **Note**: FieldWorks build, test, installer, and setup workflows are Windows-only. +> Linux and macOS are supported for editing, code search, documentation, specs, and agent work only. ### 1. Install Required Software @@ -109,6 +110,8 @@ Build FieldWorks using the PowerShell build script: For more build options, see [.github/instructions/build.instructions.md](../.github/instructions/build.instructions.md). +On Linux or macOS, do not run `build.ps1` or `test.ps1`; those entry points intentionally fail fast with a not-supported message. + ### 5. VS Code and Visual Studio usage Default recommendation: diff --git a/FLExInstaller/Directory.Packages.props b/FLExInstaller/Directory.Packages.props index e88cc42dea..f8c53cb264 100644 --- a/FLExInstaller/Directory.Packages.props +++ b/FLExInstaller/Directory.Packages.props @@ -2,9 +2,14 @@ - false + true + + + + + diff --git a/FLExInstaller/wix6/Shared/CustomActions/CustomActions/CustomActions.csproj b/FLExInstaller/wix6/Shared/CustomActions/CustomActions/CustomActions.csproj index df40f3c071..fbdc12dfa4 100644 --- a/FLExInstaller/wix6/Shared/CustomActions/CustomActions/CustomActions.csproj +++ b/FLExInstaller/wix6/Shared/CustomActions/CustomActions/CustomActions.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/ReadMe.md b/ReadMe.md index 9d908aaf1f..95265c6e4f 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -9,6 +9,14 @@ New to FieldWorks development? Start here: > **Note**: We are migrating documentation from the [FwDocumentation wiki](https://github.com/sillsdev/FwDocumentation/wiki) into this repository. Some wiki content may be more recent until migration is complete. +## Linux and macOS + +This repository can be opened on Linux or macOS for editing, code search, documentation, specs, and agent-assisted review. + +Builds, tests, installer work, and developer-environment setup are Windows-only and are intentionally disabled on non-Windows hosts. + +If you need runnable output, use a Windows machine and run `./build.ps1` or `./test.ps1` there. + ## Developer Machine Setup For first-time setup on a Windows development machine: diff --git a/SDK_MIGRATION.md b/SDK_MIGRATION.md index e0a7b25139..6c9024aa09 100644 --- a/SDK_MIGRATION.md +++ b/SDK_MIGRATION.md @@ -262,7 +262,7 @@ With CPM ensuring all projects resolve to the same package version, most manual - Initializes VS Developer environment - Supports `/m` parallel builds - **Stale DLL detection**: Runs `Remove-StaleDlls.ps1` pre-build to catch version-mismatched binaries -- **Diagnostics config**: Optionally copies dev trace config for Debug builds (`-TraceCrashes` or `UseDevTraceConfig`) +- **Diagnostics config**: Optionally copies dev trace config for Debug builds (`-EnableTracing` or `UseDevTraceConfig`) - **Installer support**: `-BuildInstaller` flag triggers full installer build pipeline **Note**: `build.sh` is not supported in this repo (FieldWorks is Windows-first). Use `.\build.ps1`. diff --git a/Src/AppForTests.config b/Src/AppForTests.config index 8beff9f556..6c4cfd8841 100644 --- a/Src/AppForTests.config +++ b/Src/AppForTests.config @@ -1,26 +1,29 @@ - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/Src/Utilities/pcpatrflex/DisambiguateInFLExDB/DisambiguateInFLExDBTests/ToneParsInvokerTests.cs b/Src/Utilities/pcpatrflex/DisambiguateInFLExDB/DisambiguateInFLExDBTests/ToneParsInvokerTests.cs index 898a185dee..9581c148da 100644 --- a/Src/Utilities/pcpatrflex/DisambiguateInFLExDB/DisambiguateInFLExDBTests/ToneParsInvokerTests.cs +++ b/Src/Utilities/pcpatrflex/DisambiguateInFLExDB/DisambiguateInFLExDBTests/ToneParsInvokerTests.cs @@ -183,28 +183,7 @@ private void CompareResultToExpectedFile(string expectedFileString, string actua private string NormalizeContent(string input) { - string tp = "tonepars64"; - string normalized = NormalizeViaIndex(input, "AppData", ""); - normalized = NormalizeViaIndex(normalized, "TestData", ""); - normalized = NormalizeViaIndex(normalized, tp, tp); - return normalized; - } - - private static string NormalizeViaIndex(string input, string match, string change) - { - // I tried to use regular expressions but never got them to match... - int iAppData = input.IndexOf(match); - if (iAppData != -1) - { - int iColon = input.IndexOf(":"); - if (iColon != -1) - { - iColon--; // skip the drive letter, too - string appdataPath = input.Substring(iColon, iAppData - iColon); - input = input.Replace(appdataPath, change); - } - } - return input; + return Regex.Replace(input, @"[A-Za-z]:\\(?:[^\\\r\n""]+\\)*", ""); } private void CreateExpectedFileStrings() diff --git a/Src/xWorks/GeneratedHtmlViewer.cs b/Src/xWorks/GeneratedHtmlViewer.cs index fecc6b7956..2fa2324a13 100644 --- a/Src/xWorks/GeneratedHtmlViewer.cs +++ b/Src/xWorks/GeneratedHtmlViewer.cs @@ -1111,7 +1111,7 @@ private void FindInBrowser(bool forward) } else { - resultIndex = resultIndex-- > 0 ? resultIndex : resultCount - 1; + resultIndex = resultIndex-- > 0 ? resultIndex : resultCount > 0 ? resultCount - 1 : 0; } // Javascript query to execute in the browser var browserJsQuery = "scrollToStoredPosition(" + resultIndex + ", " + forward.ToString().ToLower() + ")"; diff --git a/Test.runsettings b/Test.runsettings index 67453c8423..0dcbd4df0e 100644 --- a/Test.runsettings +++ b/Test.runsettings @@ -22,8 +22,8 @@ x64 net48 - - 900000 + + 900000