mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-03 15:43:31 +08:00
feat: add C# and Dart language support
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Everything Claude Code (ECC) — Agent Instructions
|
# Everything Claude Code (ECC) — Agent Instructions
|
||||||
|
|
||||||
This is a **production-ready AI coding plugin** providing 36 specialized agents, 151 skills, 68 commands, and automated hook workflows for software development.
|
This is a **production-ready AI coding plugin** providing 38 specialized agents, 154 skills, 71 commands, and automated hook workflows for software development.
|
||||||
|
|
||||||
**Version:** 1.9.0
|
**Version:** 1.9.0
|
||||||
|
|
||||||
@@ -145,9 +145,9 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat
|
|||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
agents/ — 36 specialized subagents
|
agents/ — 38 specialized subagents
|
||||||
skills/ — 151 workflow skills and domain knowledge
|
skills/ — 154 workflow skills and domain knowledge
|
||||||
commands/ — 68 slash commands
|
commands/ — 71 slash commands
|
||||||
hooks/ — Trigger-based automations
|
hooks/ — Trigger-based automations
|
||||||
rules/ — Always-follow guidelines (common + per-language)
|
rules/ — Always-follow guidelines (common + per-language)
|
||||||
scripts/ — Cross-platform Node.js utilities
|
scripts/ — Cross-platform Node.js utilities
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -225,7 +225,7 @@ For manual install instructions see the README in the `rules/` folder. When copy
|
|||||||
/plugin list everything-claude-code@everything-claude-code
|
/plugin list everything-claude-code@everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
**That's it!** You now have access to 36 agents, 151 skills, and 68 legacy command shims.
|
**That's it!** You now have access to 38 agents, 154 skills, and 71 legacy command shims.
|
||||||
|
|
||||||
### Multi-model commands require additional setup
|
### Multi-model commands require additional setup
|
||||||
|
|
||||||
@@ -1118,9 +1118,9 @@ The configuration is automatically detected from `.opencode/opencode.json`.
|
|||||||
|
|
||||||
| Feature | Claude Code | OpenCode | Status |
|
| Feature | Claude Code | OpenCode | Status |
|
||||||
|---------|-------------|----------|--------|
|
|---------|-------------|----------|--------|
|
||||||
| Agents | PASS: 36 agents | PASS: 12 agents | **Claude Code leads** |
|
| Agents | PASS: 38 agents | PASS: 12 agents | **Claude Code leads** |
|
||||||
| Commands | PASS: 68 commands | PASS: 31 commands | **Claude Code leads** |
|
| Commands | PASS: 71 commands | PASS: 31 commands | **Claude Code leads** |
|
||||||
| Skills | PASS: 151 skills | PASS: 37 skills | **Claude Code leads** |
|
| Skills | PASS: 154 skills | PASS: 37 skills | **Claude Code leads** |
|
||||||
| Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** |
|
| Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** |
|
||||||
| Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** |
|
| Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** |
|
||||||
| MCP Servers | PASS: 14 servers | PASS: Full | **Full parity** |
|
| MCP Servers | PASS: 14 servers | PASS: Full | **Full parity** |
|
||||||
@@ -1227,9 +1227,9 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e
|
|||||||
|
|
||||||
| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |
|
| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |
|
||||||
|---------|------------|------------|-----------|----------|
|
|---------|------------|------------|-----------|----------|
|
||||||
| **Agents** | 36 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |
|
| **Agents** | 38 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |
|
||||||
| **Commands** | 68 | Shared | Instruction-based | 31 |
|
| **Commands** | 71 | Shared | Instruction-based | 31 |
|
||||||
| **Skills** | 151 | Shared | 10 (native format) | 37 |
|
| **Skills** | 154 | Shared | 10 (native format) | 37 |
|
||||||
| **Hook Events** | 8 types | 15 types | None yet | 11 types |
|
| **Hook Events** | 8 types | 15 types | None yet | 11 types |
|
||||||
| **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks |
|
| **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks |
|
||||||
| **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions |
|
| **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions |
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ cp -r everything-claude-code/rules/perl ~/.claude/rules/
|
|||||||
/plugin list everything-claude-code@everything-claude-code
|
/plugin list everything-claude-code@everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
**完成!** 你现在可以使用 36 个代理、151 个技能和 68 个命令。
|
**完成!** 你现在可以使用 38 个代理、154 个技能和 71 个命令。
|
||||||
|
|
||||||
### multi-* 命令需要额外配置
|
### multi-* 命令需要额外配置
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,13 @@ Keep this file detailed for only the current sprint, blockers, and next actions.
|
|||||||
|
|
||||||
## Latest Execution Notes
|
## Latest Execution Notes
|
||||||
|
|
||||||
|
- 2026-04-02: `ECC-Tools/main` shipped `9566637` (`fix: prefer commit lookup over git ref resolution`). The PR-analysis fire is now fixed in the app repo by preferring explicit commit resolution before `git.getRef`, with regression coverage for pull refs and plain branch refs. Mirrored public tracking issue `#1184` in this repo was closed as resolved upstream.
|
||||||
|
- 2026-04-02: Direct-ported the clean native-support core of `#1043` into `main`: `agents/csharp-reviewer.md`, `skills/dotnet-patterns/SKILL.md`, and `skills/csharp-testing/SKILL.md`. This fills the gap between existing C# rule/docs mentions and actual shipped C# review/testing guidance.
|
||||||
|
- 2026-04-02: Direct-ported the clean native-support core of `#1055` into `main`: `agents/dart-build-resolver.md`, `commands/flutter-build.md`, `commands/flutter-review.md`, `commands/flutter-test.md`, `rules/dart/*`, and `skills/dart-flutter-patterns/SKILL.md`. The skill paths were wired into the current `framework-language` module instead of replaying the older PR's separate `flutter-dart` module layout.
|
||||||
|
- 2026-04-02: Closed `#1125` after full diff audit. The bundle/skill-router lane hardcoded many non-existent or non-canonical surfaces and created a second routing abstraction instead of a small ECC-native index layer.
|
||||||
|
- 2026-04-02: Closed `#1124` after full diff audit. The added agent roster was thoughtfully written, but it duplicated the existing ECC agent surface with a second competing catalog (`dispatch`, `explore`, `verifier`, `executor`, etc.) instead of strengthening canonical agents already in-tree.
|
||||||
|
- 2026-04-02: Closed the full Argus cluster `#1098`, `#1099`, `#1100`, `#1101`, and `#1102` after full diff audit. The common failure mode was the same across all five PRs: external multi-CLI dispatch was treated as a first-class runtime dependency of shipped ECC surfaces. Any useful protocol ideas should be re-ported later into ECC-native orchestration, review, or reflection lanes without external CLI fan-out assumptions.
|
||||||
|
- 2026-04-02: The active open PR queue is now reduced to four items: `#1081`, `#1055`, `#1043`, and `#894`. Next audit focus should stay on native-support or integration lanes that can plausibly be rebuilt inside ECC without external branded dependencies.
|
||||||
- 2026-04-01: `main` CI was restored locally with `1723/1723` tests passing after lockfile and hook validation fixes.
|
- 2026-04-01: `main` CI was restored locally with `1723/1723` tests passing after lockfile and hook validation fixes.
|
||||||
- 2026-04-01: Auto-generated ECC bundle PRs `#1068` and `#1069` were closed instead of merged; useful ideas must be ported manually after explicit diff audit.
|
- 2026-04-01: Auto-generated ECC bundle PRs `#1068` and `#1069` were closed instead of merged; useful ideas must be ported manually after explicit diff audit.
|
||||||
- 2026-04-01: Major-version ESLint bump PRs `#1063` and `#1064` were closed; revisit only inside a planned ESLint 10 migration lane.
|
- 2026-04-01: Major-version ESLint bump PRs `#1063` and `#1064` were closed; revisit only inside a planned ESLint 10 migration lane.
|
||||||
|
|||||||
101
agents/csharp-reviewer.md
Normal file
101
agents/csharp-reviewer.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
---
|
||||||
|
name: csharp-reviewer
|
||||||
|
description: Expert C# code reviewer specializing in .NET conventions, async patterns, security, nullable reference types, and performance. Use for all C# code changes. MUST BE USED for C# projects.
|
||||||
|
tools: ["Read", "Grep", "Glob", "Bash"]
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a senior C# code reviewer ensuring high standards of idiomatic .NET code and best practices.
|
||||||
|
|
||||||
|
When invoked:
|
||||||
|
1. Run `git diff -- '*.cs'` to see recent C# file changes
|
||||||
|
2. Run `dotnet build` and `dotnet format --verify-no-changes` if available
|
||||||
|
3. Focus on modified `.cs` files
|
||||||
|
4. Begin review immediately
|
||||||
|
|
||||||
|
## Review Priorities
|
||||||
|
|
||||||
|
### CRITICAL — Security
|
||||||
|
- **SQL Injection**: String concatenation/interpolation in queries — use parameterized queries or EF Core
|
||||||
|
- **Command Injection**: Unvalidated input in `Process.Start` — validate and sanitize
|
||||||
|
- **Path Traversal**: User-controlled file paths — use `Path.GetFullPath` + prefix check
|
||||||
|
- **Insecure Deserialization**: `BinaryFormatter`, `JsonSerializer` with `TypeNameHandling.All`
|
||||||
|
- **Hardcoded secrets**: API keys, connection strings in source — use configuration/secret manager
|
||||||
|
- **CSRF/XSS**: Missing `[ValidateAntiForgeryToken]`, unencoded output in Razor
|
||||||
|
|
||||||
|
### CRITICAL — Error Handling
|
||||||
|
- **Empty catch blocks**: `catch { }` or `catch (Exception) { }` — handle or rethrow
|
||||||
|
- **Swallowed exceptions**: `catch { return null; }` — log context, throw specific
|
||||||
|
- **Missing `using`/`await using`**: Manual disposal of `IDisposable`/`IAsyncDisposable`
|
||||||
|
- **Blocking async**: `.Result`, `.Wait()`, `.GetAwaiter().GetResult()` — use `await`
|
||||||
|
|
||||||
|
### HIGH — Async Patterns
|
||||||
|
- **Missing CancellationToken**: Public async APIs without cancellation support
|
||||||
|
- **Fire-and-forget**: `async void` except event handlers — return `Task`
|
||||||
|
- **ConfigureAwait misuse**: Library code missing `ConfigureAwait(false)`
|
||||||
|
- **Sync-over-async**: Blocking calls in async context causing deadlocks
|
||||||
|
|
||||||
|
### HIGH — Type Safety
|
||||||
|
- **Nullable reference types**: Nullable warnings ignored or suppressed with `!`
|
||||||
|
- **Unsafe casts**: `(T)obj` without type check — use `obj is T t` or `obj as T`
|
||||||
|
- **Raw strings as identifiers**: Magic strings for config keys, routes — use constants or `nameof`
|
||||||
|
- **`dynamic` usage**: Avoid `dynamic` in application code — use generics or explicit models
|
||||||
|
|
||||||
|
### HIGH — Code Quality
|
||||||
|
- **Large methods**: Over 50 lines — extract helper methods
|
||||||
|
- **Deep nesting**: More than 4 levels — use early returns, guard clauses
|
||||||
|
- **God classes**: Classes with too many responsibilities — apply SRP
|
||||||
|
- **Mutable shared state**: Static mutable fields — use `ConcurrentDictionary`, `Interlocked`, or DI scoping
|
||||||
|
|
||||||
|
### MEDIUM — Performance
|
||||||
|
- **String concatenation in loops**: Use `StringBuilder` or `string.Join`
|
||||||
|
- **LINQ in hot paths**: Excessive allocations — consider `for` loops with pre-allocated buffers
|
||||||
|
- **N+1 queries**: EF Core lazy loading in loops — use `Include`/`ThenInclude`
|
||||||
|
- **Missing `AsNoTracking`**: Read-only queries tracking entities unnecessarily
|
||||||
|
|
||||||
|
### MEDIUM — Best Practices
|
||||||
|
- **Naming conventions**: PascalCase for public members, `_camelCase` for private fields
|
||||||
|
- **Record vs class**: Value-like immutable models should be `record` or `record struct`
|
||||||
|
- **Dependency injection**: `new`-ing services instead of injecting — use constructor injection
|
||||||
|
- **`IEnumerable` multiple enumeration**: Materialize with `.ToList()` when enumerated more than once
|
||||||
|
- **Missing `sealed`**: Non-inherited classes should be `sealed` for clarity and performance
|
||||||
|
|
||||||
|
## Diagnostic Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build # Compilation check
|
||||||
|
dotnet format --verify-no-changes # Format check
|
||||||
|
dotnet test --no-build # Run tests
|
||||||
|
dotnet test --collect:"XPlat Code Coverage" # Coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
## Review Output Format
|
||||||
|
|
||||||
|
```text
|
||||||
|
[SEVERITY] Issue title
|
||||||
|
File: path/to/File.cs:42
|
||||||
|
Issue: Description
|
||||||
|
Fix: What to change
|
||||||
|
```
|
||||||
|
|
||||||
|
## Approval Criteria
|
||||||
|
|
||||||
|
- **Approve**: No CRITICAL or HIGH issues
|
||||||
|
- **Warning**: MEDIUM issues only (can merge with caution)
|
||||||
|
- **Block**: CRITICAL or HIGH issues found
|
||||||
|
|
||||||
|
## Framework Checks
|
||||||
|
|
||||||
|
- **ASP.NET Core**: Model validation, auth policies, middleware order, `IOptions<T>` pattern
|
||||||
|
- **EF Core**: Migration safety, `Include` for eager loading, `AsNoTracking` for reads
|
||||||
|
- **Minimal APIs**: Route grouping, endpoint filters, proper `TypedResults`
|
||||||
|
- **Blazor**: Component lifecycle, `StateHasChanged` usage, JS interop disposal
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
For detailed C# patterns, see skill: `dotnet-patterns`.
|
||||||
|
For testing guidelines, see skill: `csharp-testing`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Review with the mindset: "Would this code pass review at a top .NET shop or open-source project?"
|
||||||
201
agents/dart-build-resolver.md
Normal file
201
agents/dart-build-resolver.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
---
|
||||||
|
name: dart-build-resolver
|
||||||
|
description: Dart/Flutter build, analysis, and dependency error resolution specialist. Fixes `dart analyze` errors, Flutter compilation failures, pub dependency conflicts, and build_runner issues with minimal, surgical changes. Use when Dart/Flutter builds fail.
|
||||||
|
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
|
|
||||||
|
# Dart/Flutter Build Error Resolver
|
||||||
|
|
||||||
|
You are an expert Dart/Flutter build error resolution specialist. Your mission is to fix Dart analyzer errors, Flutter compilation issues, pub dependency conflicts, and build_runner failures with **minimal, surgical changes**.
|
||||||
|
|
||||||
|
## Core Responsibilities
|
||||||
|
|
||||||
|
1. Diagnose `dart analyze` and `flutter analyze` errors
|
||||||
|
2. Fix Dart type errors, null safety violations, and missing imports
|
||||||
|
3. Resolve `pubspec.yaml` dependency conflicts and version constraints
|
||||||
|
4. Fix `build_runner` code generation failures
|
||||||
|
5. Handle Flutter-specific build errors (Android Gradle, iOS CocoaPods, web)
|
||||||
|
|
||||||
|
## Diagnostic Commands
|
||||||
|
|
||||||
|
Run these in order:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Dart/Flutter analysis errors
|
||||||
|
flutter analyze 2>&1
|
||||||
|
# or for pure Dart projects
|
||||||
|
dart analyze 2>&1
|
||||||
|
|
||||||
|
# Check pub dependency resolution
|
||||||
|
flutter pub get 2>&1
|
||||||
|
|
||||||
|
# Check if code generation is stale
|
||||||
|
dart run build_runner build --delete-conflicting-outputs 2>&1
|
||||||
|
|
||||||
|
# Flutter build for target platform
|
||||||
|
flutter build apk 2>&1 # Android
|
||||||
|
flutter build ipa --no-codesign 2>&1 # iOS (CI without signing)
|
||||||
|
flutter build web 2>&1 # Web
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resolution Workflow
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. flutter analyze -> Parse error messages
|
||||||
|
2. Read affected file -> Understand context
|
||||||
|
3. Apply minimal fix -> Only what's needed
|
||||||
|
4. flutter analyze -> Verify fix
|
||||||
|
5. flutter test -> Ensure nothing broke
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Fix Patterns
|
||||||
|
|
||||||
|
| Error | Cause | Fix |
|
||||||
|
|-------|-------|-----|
|
||||||
|
| `The name 'X' isn't defined` | Missing import or typo | Add correct `import` or fix name |
|
||||||
|
| `A value of type 'X?' can't be assigned to type 'X'` | Null safety — nullable not handled | Add `!`, `?? default`, or null check |
|
||||||
|
| `The argument type 'X' can't be assigned to 'Y'` | Type mismatch | Fix type, add explicit cast, or correct API call |
|
||||||
|
| `Non-nullable instance field 'x' must be initialized` | Missing initializer | Add initializer, mark `late`, or make nullable |
|
||||||
|
| `The method 'X' isn't defined for type 'Y'` | Wrong type or wrong import | Check type and imports |
|
||||||
|
| `'await' applied to non-Future` | Awaiting a non-async value | Remove `await` or make function async |
|
||||||
|
| `Missing concrete implementation of 'X'` | Abstract interface not fully implemented | Add missing method implementations |
|
||||||
|
| `The class 'X' doesn't implement 'Y'` | Missing `implements` or missing method | Add method or fix class signature |
|
||||||
|
| `Because X depends on Y >=A and Z depends on Y <B, version solving failed` | Pub version conflict | Adjust version constraints or add `dependency_overrides` |
|
||||||
|
| `Could not find a file named "pubspec.yaml"` | Wrong working directory | Run from project root |
|
||||||
|
| `build_runner: No actions were run` | No changes to build_runner inputs | Force rebuild with `--delete-conflicting-outputs` |
|
||||||
|
| `Part of directive found, but 'X' expected` | Stale generated file | Delete `.g.dart` file and re-run build_runner |
|
||||||
|
|
||||||
|
## Pub Dependency Troubleshooting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show full dependency tree
|
||||||
|
flutter pub deps
|
||||||
|
|
||||||
|
# Check why a specific package version was chosen
|
||||||
|
flutter pub deps --style=compact | grep <package>
|
||||||
|
|
||||||
|
# Upgrade packages to latest compatible versions
|
||||||
|
flutter pub upgrade
|
||||||
|
|
||||||
|
# Upgrade specific package
|
||||||
|
flutter pub upgrade <package_name>
|
||||||
|
|
||||||
|
# Clear pub cache if metadata is corrupted
|
||||||
|
flutter pub cache repair
|
||||||
|
|
||||||
|
# Verify pubspec.lock is consistent
|
||||||
|
flutter pub get --enforce-lockfile
|
||||||
|
```
|
||||||
|
|
||||||
|
## Null Safety Fix Patterns
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Error: A value of type 'String?' can't be assigned to type 'String'
|
||||||
|
// BAD — force unwrap
|
||||||
|
final name = user.name!;
|
||||||
|
|
||||||
|
// GOOD — provide fallback
|
||||||
|
final name = user.name ?? 'Unknown';
|
||||||
|
|
||||||
|
// GOOD — guard and return early
|
||||||
|
if (user.name == null) return;
|
||||||
|
final name = user.name!; // safe after null check
|
||||||
|
|
||||||
|
// GOOD — Dart 3 pattern matching
|
||||||
|
final name = switch (user.name) {
|
||||||
|
final n? => n,
|
||||||
|
null => 'Unknown',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Error Fix Patterns
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Error: The argument type 'List<dynamic>' can't be assigned to 'List<String>'
|
||||||
|
// BAD
|
||||||
|
final ids = jsonList; // inferred as List<dynamic>
|
||||||
|
|
||||||
|
// GOOD
|
||||||
|
final ids = List<String>.from(jsonList);
|
||||||
|
// or
|
||||||
|
final ids = (jsonList as List).cast<String>();
|
||||||
|
```
|
||||||
|
|
||||||
|
## build_runner Troubleshooting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clean and regenerate all files
|
||||||
|
dart run build_runner clean
|
||||||
|
dart run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
|
# Watch mode for development
|
||||||
|
dart run build_runner watch --delete-conflicting-outputs
|
||||||
|
|
||||||
|
# Check for missing build_runner dependencies in pubspec.yaml
|
||||||
|
# Required: build_runner, json_serializable / freezed / riverpod_generator (as dev_dependencies)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Android Build Troubleshooting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clean Android build cache
|
||||||
|
cd android && ./gradlew clean && cd ..
|
||||||
|
|
||||||
|
# Invalidate Flutter tool cache
|
||||||
|
flutter clean
|
||||||
|
|
||||||
|
# Rebuild
|
||||||
|
flutter pub get && flutter build apk
|
||||||
|
|
||||||
|
# Check Gradle/JDK version compatibility
|
||||||
|
cd android && ./gradlew --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## iOS Build Troubleshooting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update CocoaPods
|
||||||
|
cd ios && pod install --repo-update && cd ..
|
||||||
|
|
||||||
|
# Clean iOS build
|
||||||
|
flutter clean && cd ios && pod deintegrate && pod install && cd ..
|
||||||
|
|
||||||
|
# Check for platform version mismatches in Podfile
|
||||||
|
# Ensure ios platform version >= minimum required by all pods
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Principles
|
||||||
|
|
||||||
|
- **Surgical fixes only** — don't refactor, just fix the error
|
||||||
|
- **Never** add `// ignore:` suppressions without approval
|
||||||
|
- **Never** use `dynamic` to silence type errors
|
||||||
|
- **Always** run `flutter analyze` after each fix to verify
|
||||||
|
- Fix root cause over suppressing symptoms
|
||||||
|
- Prefer null-safe patterns over bang operators (`!`)
|
||||||
|
|
||||||
|
## Stop Conditions
|
||||||
|
|
||||||
|
Stop and report if:
|
||||||
|
- Same error persists after 3 fix attempts
|
||||||
|
- Fix introduces more errors than it resolves
|
||||||
|
- Requires architectural changes or package upgrades that change behavior
|
||||||
|
- Conflicting platform constraints need user decision
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
```text
|
||||||
|
[FIXED] lib/features/cart/data/cart_repository_impl.dart:42
|
||||||
|
Error: A value of type 'String?' can't be assigned to type 'String'
|
||||||
|
Fix: Changed `final id = response.id` to `final id = response.id ?? ''`
|
||||||
|
Remaining errors: 2
|
||||||
|
|
||||||
|
[FIXED] pubspec.yaml
|
||||||
|
Error: Version solving failed — http >=0.13.0 required by dio and <0.13.0 required by retrofit
|
||||||
|
Fix: Upgraded dio to ^5.3.0 which allows http >=0.13.0
|
||||||
|
Remaining errors: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Final: `Build Status: SUCCESS/FAILED | Errors Fixed: N | Files Modified: list`
|
||||||
|
|
||||||
|
For detailed Dart patterns and code examples, see `skill: flutter-dart-code-review`.
|
||||||
164
commands/flutter-build.md
Normal file
164
commands/flutter-build.md
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
---
|
||||||
|
description: Fix Dart analyzer errors and Flutter build failures incrementally. Invokes the dart-build-resolver agent for minimal, surgical fixes.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Flutter Build and Fix
|
||||||
|
|
||||||
|
This command invokes the **dart-build-resolver** agent to incrementally fix Dart/Flutter build errors with minimal changes.
|
||||||
|
|
||||||
|
## What This Command Does
|
||||||
|
|
||||||
|
1. **Run Diagnostics**: Execute `flutter analyze`, `flutter pub get`
|
||||||
|
2. **Parse Errors**: Group by file and sort by severity
|
||||||
|
3. **Fix Incrementally**: One error at a time
|
||||||
|
4. **Verify Each Fix**: Re-run analysis after each change
|
||||||
|
5. **Report Summary**: Show what was fixed and what remains
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
Use `/flutter-build` when:
|
||||||
|
- `flutter analyze` reports errors
|
||||||
|
- `flutter build` fails for any platform
|
||||||
|
- `dart pub get` / `flutter pub get` fails with version conflicts
|
||||||
|
- `build_runner` fails to generate code
|
||||||
|
- After pulling changes that break the build
|
||||||
|
|
||||||
|
## Diagnostic Commands Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Analysis
|
||||||
|
flutter analyze 2>&1
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
flutter pub get 2>&1
|
||||||
|
|
||||||
|
# Code generation (if project uses build_runner)
|
||||||
|
dart run build_runner build --delete-conflicting-outputs 2>&1
|
||||||
|
|
||||||
|
# Platform builds
|
||||||
|
flutter build apk 2>&1
|
||||||
|
flutter build web 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Session
|
||||||
|
|
||||||
|
````text
|
||||||
|
User: /flutter-build
|
||||||
|
|
||||||
|
Agent:
|
||||||
|
# Flutter Build Resolution
|
||||||
|
|
||||||
|
## Initial Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
$ flutter analyze
|
||||||
|
Analyzing project...
|
||||||
|
|
||||||
|
error • lib/features/user/data/user_repository_impl.dart:34:16 •
|
||||||
|
A value of type 'String?' can't be assigned to a variable of type 'String'. •
|
||||||
|
invalid_assignment
|
||||||
|
|
||||||
|
error • lib/features/cart/presentation/cart_page.dart:58:5 •
|
||||||
|
The method 'add' isn't defined for the type 'List<Item>'.
|
||||||
|
Try correcting the name to the name of an existing method, or defining a method named 'add'. •
|
||||||
|
undefined_method
|
||||||
|
|
||||||
|
2 errors found.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fix 1: Null Safety
|
||||||
|
|
||||||
|
File: lib/features/user/data/user_repository_impl.dart:34
|
||||||
|
Error: A value of type 'String?' can't be assigned to type 'String'
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
```dart
|
||||||
|
final id = response.id;
|
||||||
|
```
|
||||||
|
To:
|
||||||
|
```dart
|
||||||
|
final id = response.id ?? '';
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
$ flutter analyze
|
||||||
|
1 error found.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fix 2: Immutable List
|
||||||
|
|
||||||
|
File: lib/features/cart/presentation/cart_page.dart:58
|
||||||
|
Error: The method 'add' isn't defined for the type 'List<Item>'
|
||||||
|
Cause: State holds an unmodifiable list; mutation goes through Cubit
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
```dart
|
||||||
|
state.items.add(item);
|
||||||
|
```
|
||||||
|
To:
|
||||||
|
```dart
|
||||||
|
context.read<CartCubit>().addItem(item);
|
||||||
|
// Note: Cubit exposes named methods (addItem, removeItem);
|
||||||
|
// .add(event) is the BLoC event API — don't mix them.
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
$ flutter analyze
|
||||||
|
No issues found!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Final Verification
|
||||||
|
|
||||||
|
```
|
||||||
|
$ flutter test
|
||||||
|
All tests passed.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Metric | Count |
|
||||||
|
|--------|-------|
|
||||||
|
| Analysis errors fixed | 2 |
|
||||||
|
| Files modified | 2 |
|
||||||
|
| Remaining issues | 0 |
|
||||||
|
|
||||||
|
Build Status: PASS ✓
|
||||||
|
````
|
||||||
|
|
||||||
|
## Common Errors Fixed
|
||||||
|
|
||||||
|
| Error | Typical Fix |
|
||||||
|
|-------|-------------|
|
||||||
|
| `A value of type 'X?' can't be assigned to 'X'` | Add `?? default` or null guard |
|
||||||
|
| `The name 'X' isn't defined` | Add import or fix typo |
|
||||||
|
| `Non-nullable instance field must be initialized` | Add initializer or `late` |
|
||||||
|
| `Version solving failed` | Adjust version constraints in pubspec.yaml |
|
||||||
|
| `Missing concrete implementation of 'X'` | Implement missing interface method |
|
||||||
|
| `build_runner: Part of X expected` | Delete stale `.g.dart` and rebuild |
|
||||||
|
|
||||||
|
## Fix Strategy
|
||||||
|
|
||||||
|
1. **Analysis errors first** — code must be error-free
|
||||||
|
2. **Warning triage second** — fix warnings that could cause runtime bugs
|
||||||
|
3. **pub conflicts third** — fix dependency resolution
|
||||||
|
4. **One fix at a time** — verify each change
|
||||||
|
5. **Minimal changes** — don't refactor, just fix
|
||||||
|
|
||||||
|
## Stop Conditions
|
||||||
|
|
||||||
|
The agent will stop and report if:
|
||||||
|
- Same error persists after 3 attempts
|
||||||
|
- Fix introduces more errors
|
||||||
|
- Requires architectural changes
|
||||||
|
- Package upgrade conflicts need user decision
|
||||||
|
|
||||||
|
## Related Commands
|
||||||
|
|
||||||
|
- `/flutter-test` — Run tests after build succeeds
|
||||||
|
- `/flutter-review` — Review code quality
|
||||||
|
- `/verify` — Full verification loop
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- Agent: `agents/dart-build-resolver.md`
|
||||||
|
- Skill: `skills/flutter-dart-code-review/`
|
||||||
116
commands/flutter-review.md
Normal file
116
commands/flutter-review.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
---
|
||||||
|
description: Review Flutter/Dart code for idiomatic patterns, widget best practices, state management, performance, accessibility, and security. Invokes the flutter-reviewer agent.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Flutter Code Review
|
||||||
|
|
||||||
|
This command invokes the **flutter-reviewer** agent to review Flutter/Dart code changes.
|
||||||
|
|
||||||
|
## What This Command Does
|
||||||
|
|
||||||
|
1. **Gather Context**: Review `git diff --staged` and `git diff`
|
||||||
|
2. **Inspect Project**: Check `pubspec.yaml`, `analysis_options.yaml`, state management solution
|
||||||
|
3. **Security Pre-scan**: Check for hardcoded secrets and critical security issues
|
||||||
|
4. **Full Review**: Apply the complete review checklist
|
||||||
|
5. **Report Findings**: Output issues grouped by severity with fix guidance
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before running `/flutter-review`, ensure:
|
||||||
|
1. **Build passes** — run `/flutter-build` first; a review on broken code is incomplete
|
||||||
|
2. **Tests pass** — run `/flutter-test` to confirm no regressions
|
||||||
|
3. **No merge conflicts** — resolve all conflicts so the diff reflects only intentional changes
|
||||||
|
4. **`flutter analyze` is clean** — fix analyzer warnings before review
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
Use `/flutter-review` when:
|
||||||
|
- Before submitting a PR with Flutter/Dart changes (after build and tests pass)
|
||||||
|
- After implementing a new feature to catch issues early
|
||||||
|
- When reviewing someone else's Flutter code
|
||||||
|
- To audit a widget, state management component, or service class
|
||||||
|
- Before a production release
|
||||||
|
|
||||||
|
## Review Areas
|
||||||
|
|
||||||
|
| Area | Severity |
|
||||||
|
|------|----------|
|
||||||
|
| Hardcoded secrets, cleartext HTTP | CRITICAL |
|
||||||
|
| Architecture violations, state management anti-patterns | CRITICAL |
|
||||||
|
| Widget rebuild issues, resource leaks | HIGH |
|
||||||
|
| Missing `dispose()`, `BuildContext` after await | HIGH |
|
||||||
|
| Dart null safety, missing error/loading states | HIGH |
|
||||||
|
| Const propagation, widget composition | HIGH |
|
||||||
|
| Performance: expensive work in `build()` | HIGH |
|
||||||
|
| Accessibility, semantic labels | MEDIUM |
|
||||||
|
| Missing tests for state transitions | HIGH |
|
||||||
|
| Hardcoded strings (l10n) | MEDIUM |
|
||||||
|
| Pub dependency hygiene | LOW |
|
||||||
|
|
||||||
|
## Example Session
|
||||||
|
|
||||||
|
````text
|
||||||
|
User: /flutter-review
|
||||||
|
|
||||||
|
Agent:
|
||||||
|
# Flutter Code Review
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Changed files:
|
||||||
|
- lib/features/auth/presentation/login_page.dart
|
||||||
|
- lib/features/auth/data/auth_repository_impl.dart
|
||||||
|
|
||||||
|
State management: Riverpod (detected from pubspec.yaml)
|
||||||
|
Architecture: feature-first
|
||||||
|
|
||||||
|
## Security Pre-scan
|
||||||
|
|
||||||
|
✓ No hardcoded secrets detected
|
||||||
|
✓ No cleartext HTTP calls
|
||||||
|
|
||||||
|
## Review Findings
|
||||||
|
|
||||||
|
[HIGH] BuildContext used after async gap without mounted check
|
||||||
|
File: lib/features/auth/presentation/login_page.dart:67
|
||||||
|
Issue: `context.go('/home')` called after `await auth.login(...)` with no `mounted` check.
|
||||||
|
Fix: Add `if (!context.mounted) return;` before any navigation after awaits (Flutter 3.7+).
|
||||||
|
|
||||||
|
[HIGH] AsyncValue error state not handled
|
||||||
|
File: lib/features/auth/presentation/login_page.dart:42
|
||||||
|
Issue: `ref.watch(authProvider)` switches on loading/data but has no `error` branch.
|
||||||
|
Fix: Add error case to the switch expression or `when()` call to show a user-facing error message.
|
||||||
|
|
||||||
|
[MEDIUM] Hardcoded string not localized
|
||||||
|
File: lib/features/auth/presentation/login_page.dart:89
|
||||||
|
Issue: `Text('Login')` — user-visible string not using localization system.
|
||||||
|
Fix: Use the project's l10n accessor: `Text(context.l10n.loginButton)`.
|
||||||
|
|
||||||
|
## Review Summary
|
||||||
|
|
||||||
|
| Severity | Count | Status |
|
||||||
|
|----------|-------|--------|
|
||||||
|
| CRITICAL | 0 | pass |
|
||||||
|
| HIGH | 2 | block |
|
||||||
|
| MEDIUM | 1 | info |
|
||||||
|
| LOW | 0 | note |
|
||||||
|
|
||||||
|
Verdict: BLOCK — HIGH issues must be fixed before merge.
|
||||||
|
````
|
||||||
|
|
||||||
|
## Approval Criteria
|
||||||
|
|
||||||
|
- **Approve**: No CRITICAL or HIGH issues
|
||||||
|
- **Block**: Any CRITICAL or HIGH issues must be fixed before merge
|
||||||
|
|
||||||
|
## Related Commands
|
||||||
|
|
||||||
|
- `/flutter-build` — Fix build errors first
|
||||||
|
- `/flutter-test` — Run tests before reviewing
|
||||||
|
- `/code-review` — General code review (language-agnostic)
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- Agent: `agents/flutter-reviewer.md`
|
||||||
|
- Skill: `skills/flutter-dart-code-review/`
|
||||||
|
- Rules: `rules/dart/`
|
||||||
144
commands/flutter-test.md
Normal file
144
commands/flutter-test.md
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
---
|
||||||
|
description: Run Flutter/Dart tests, report failures, and incrementally fix test issues. Covers unit, widget, golden, and integration tests.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Flutter Test
|
||||||
|
|
||||||
|
This command runs the Flutter test suite and reports results. When failures occur, it diagnoses and fixes issues incrementally.
|
||||||
|
|
||||||
|
## What This Command Does
|
||||||
|
|
||||||
|
1. **Run Tests**: Execute `flutter test` (or scope to changed files)
|
||||||
|
2. **Parse Failures**: Identify failing tests by type and cause
|
||||||
|
3. **Fix Incrementally**: One failure at a time where possible
|
||||||
|
4. **Verify**: Re-run after each fix
|
||||||
|
5. **Report**: Show coverage summary and remaining failures
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
Use `/flutter-test` when:
|
||||||
|
- After implementing a feature to verify nothing broke
|
||||||
|
- After running `/flutter-build` to ensure tests pass
|
||||||
|
- To check test coverage on new code
|
||||||
|
- When a specific test file is failing
|
||||||
|
- Before submitting a PR
|
||||||
|
|
||||||
|
## Commands Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
flutter test 2>&1
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
flutter test --coverage 2>&1
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
flutter test test/unit/domain/usecases/get_user_test.dart 2>&1
|
||||||
|
|
||||||
|
# Run tests matching a name pattern
|
||||||
|
flutter test --name "CartBloc" 2>&1
|
||||||
|
|
||||||
|
# Run integration tests (requires device/emulator)
|
||||||
|
flutter test integration_test/ 2>&1
|
||||||
|
|
||||||
|
# Update golden files when intentional visual changes are made
|
||||||
|
flutter test --update-goldens 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Session
|
||||||
|
|
||||||
|
````text
|
||||||
|
User: /flutter-test
|
||||||
|
|
||||||
|
Agent:
|
||||||
|
# Flutter Test Run
|
||||||
|
|
||||||
|
## Test Execution
|
||||||
|
|
||||||
|
```
|
||||||
|
$ flutter test
|
||||||
|
00:05 +42 -2: Some tests failed.
|
||||||
|
|
||||||
|
FAILED: test/unit/data/user_repository_test.dart
|
||||||
|
UserRepository getById
|
||||||
|
returns null when user not found
|
||||||
|
Expected: null
|
||||||
|
Actual: User(id: 'test-id', name: 'Test User')
|
||||||
|
|
||||||
|
FAILED: test/widget/cart_page_test.dart
|
||||||
|
CartPage
|
||||||
|
shows empty state when cart is empty
|
||||||
|
Expected: exactly one widget with text 'Your cart is empty'
|
||||||
|
Found: no widget with text 'Your cart is empty'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fix 1: Repository Test
|
||||||
|
|
||||||
|
File: test/unit/data/user_repository_test.dart
|
||||||
|
Failure: `getById` returns a user when it should return null for missing ID
|
||||||
|
|
||||||
|
Root cause: Test setup adds a user with ID 'test-id' but queries with 'missing-id'.
|
||||||
|
Fix: Updated test to query with 'missing-id' — setup was correct, query was wrong.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ flutter test test/unit/data/user_repository_test.dart
|
||||||
|
1 test passed.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fix 2: Widget Test
|
||||||
|
|
||||||
|
File: test/widget/cart_page_test.dart
|
||||||
|
Failure: Empty state text widget not found
|
||||||
|
|
||||||
|
Root cause: Empty state message was renamed from 'Your cart is empty' to 'Cart is empty' in the widget.
|
||||||
|
Fix: Updated test string to match current widget copy.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ flutter test test/widget/cart_page_test.dart
|
||||||
|
1 test passed.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Final Run
|
||||||
|
|
||||||
|
```
|
||||||
|
$ flutter test --coverage
|
||||||
|
All 44 tests passed.
|
||||||
|
Coverage: 84.2% (target: 80%)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Total tests | 44 |
|
||||||
|
| Passed | 44 |
|
||||||
|
| Failed | 0 |
|
||||||
|
| Coverage | 84.2% |
|
||||||
|
|
||||||
|
Test Status: PASS ✓
|
||||||
|
````
|
||||||
|
|
||||||
|
## Common Test Failures
|
||||||
|
|
||||||
|
| Failure | Typical Fix |
|
||||||
|
|---------|-------------|
|
||||||
|
| `Expected: <X> Actual: <Y>` | Update assertion or fix implementation |
|
||||||
|
| `Widget not found` | Fix finder selector or update test after widget rename |
|
||||||
|
| `Golden file not found` | Run `flutter test --update-goldens` to generate |
|
||||||
|
| `Golden mismatch` | Inspect diff; run `--update-goldens` if change was intentional |
|
||||||
|
| `MissingPluginException` | Mock platform channel in test setup |
|
||||||
|
| `LateInitializationError` | Initialize `late` fields in `setUp()` |
|
||||||
|
| `pumpAndSettle timed out` | Replace with explicit `pump(Duration)` calls |
|
||||||
|
|
||||||
|
## Related Commands
|
||||||
|
|
||||||
|
- `/flutter-build` — Fix build errors before running tests
|
||||||
|
- `/flutter-review` — Review code after tests pass
|
||||||
|
- `/tdd` — Test-driven development workflow
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- Agent: `agents/flutter-reviewer.md`
|
||||||
|
- Agent: `agents/dart-build-resolver.md`
|
||||||
|
- Skill: `skills/flutter-dart-code-review/`
|
||||||
|
- Rules: `rules/dart/testing.md`
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Everything Claude Code (ECC) — 智能体指令
|
# Everything Claude Code (ECC) — 智能体指令
|
||||||
|
|
||||||
这是一个**生产就绪的 AI 编码插件**,提供 36 个专业代理、151 项技能、68 条命令以及自动化钩子工作流,用于软件开发。
|
这是一个**生产就绪的 AI 编码插件**,提供 38 个专业代理、154 项技能、71 条命令以及自动化钩子工作流,用于软件开发。
|
||||||
|
|
||||||
**版本:** 1.9.0
|
**版本:** 1.9.0
|
||||||
|
|
||||||
@@ -146,9 +146,9 @@
|
|||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
agents/ — 36 个专业子代理
|
agents/ — 38 个专业子代理
|
||||||
skills/ — 151 个工作流技能和领域知识
|
skills/ — 154 个工作流技能和领域知识
|
||||||
commands/ — 68 个斜杠命令
|
commands/ — 71 个斜杠命令
|
||||||
hooks/ — 基于触发的自动化
|
hooks/ — 基于触发的自动化
|
||||||
rules/ — 始终遵循的指导方针(通用 + 每种语言)
|
rules/ — 始终遵循的指导方针(通用 + 每种语言)
|
||||||
scripts/ — 跨平台 Node.js 实用工具
|
scripts/ — 跨平台 Node.js 实用工具
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ npx ecc-install typescript
|
|||||||
/plugin list everything-claude-code@everything-claude-code
|
/plugin list everything-claude-code@everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
**搞定!** 你现在可以使用 36 个智能体、151 项技能和 68 个命令了。
|
**搞定!** 你现在可以使用 38 个智能体、154 项技能和 71 个命令了。
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
@@ -1094,9 +1094,9 @@ opencode
|
|||||||
|
|
||||||
| 功能特性 | Claude Code | OpenCode | 状态 |
|
| 功能特性 | Claude Code | OpenCode | 状态 |
|
||||||
|---------|-------------|----------|--------|
|
|---------|-------------|----------|--------|
|
||||||
| 智能体 | PASS: 36 个 | PASS: 12 个 | **Claude Code 领先** |
|
| 智能体 | PASS: 38 个 | PASS: 12 个 | **Claude Code 领先** |
|
||||||
| 命令 | PASS: 68 个 | PASS: 31 个 | **Claude Code 领先** |
|
| 命令 | PASS: 71 个 | PASS: 31 个 | **Claude Code 领先** |
|
||||||
| 技能 | PASS: 151 项 | PASS: 37 项 | **Claude Code 领先** |
|
| 技能 | PASS: 154 项 | PASS: 37 项 | **Claude Code 领先** |
|
||||||
| 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** |
|
| 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** |
|
||||||
| 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** |
|
| 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** |
|
||||||
| MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** |
|
| MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** |
|
||||||
@@ -1206,9 +1206,9 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以
|
|||||||
|
|
||||||
| 功能特性 | Claude Code | Cursor IDE | Codex CLI | OpenCode |
|
| 功能特性 | Claude Code | Cursor IDE | Codex CLI | OpenCode |
|
||||||
|---------|------------|------------|-----------|----------|
|
|---------|------------|------------|-----------|----------|
|
||||||
| **智能体** | 36 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
|
| **智能体** | 38 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
|
||||||
| **命令** | 68 | 共享 | 基于指令 | 31 |
|
| **命令** | 71 | 共享 | 基于指令 | 31 |
|
||||||
| **技能** | 151 | 共享 | 10 (原生格式) | 37 |
|
| **技能** | 154 | 共享 | 10 (原生格式) | 37 |
|
||||||
| **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 |
|
| **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 |
|
||||||
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 |
|
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 |
|
||||||
| **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 |
|
| **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 |
|
||||||
|
|||||||
@@ -117,11 +117,14 @@
|
|||||||
"skills/backend-patterns",
|
"skills/backend-patterns",
|
||||||
"skills/coding-standards",
|
"skills/coding-standards",
|
||||||
"skills/compose-multiplatform-patterns",
|
"skills/compose-multiplatform-patterns",
|
||||||
|
"skills/csharp-testing",
|
||||||
"skills/cpp-coding-standards",
|
"skills/cpp-coding-standards",
|
||||||
"skills/cpp-testing",
|
"skills/cpp-testing",
|
||||||
|
"skills/dart-flutter-patterns",
|
||||||
"skills/django-patterns",
|
"skills/django-patterns",
|
||||||
"skills/django-tdd",
|
"skills/django-tdd",
|
||||||
"skills/django-verification",
|
"skills/django-verification",
|
||||||
|
"skills/dotnet-patterns",
|
||||||
"skills/frontend-patterns",
|
"skills/frontend-patterns",
|
||||||
"skills/frontend-slides",
|
"skills/frontend-slides",
|
||||||
"skills/golang-patterns",
|
"skills/golang-patterns",
|
||||||
|
|||||||
159
rules/dart/coding-style.md
Normal file
159
rules/dart/coding-style.md
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
---
|
||||||
|
paths:
|
||||||
|
- "**/*.dart"
|
||||||
|
- "**/pubspec.yaml"
|
||||||
|
- "**/analysis_options.yaml"
|
||||||
|
---
|
||||||
|
# Dart/Flutter Coding Style
|
||||||
|
|
||||||
|
> This file extends [common/coding-style.md](../common/coding-style.md) with Dart and Flutter-specific content.
|
||||||
|
|
||||||
|
## Formatting
|
||||||
|
|
||||||
|
- **dart format** for all `.dart` files — enforced in CI (`dart format --set-exit-if-changed .`)
|
||||||
|
- Line length: 80 characters (dart format default)
|
||||||
|
- Trailing commas on multi-line argument/parameter lists to improve diffs and formatting
|
||||||
|
|
||||||
|
## Immutability
|
||||||
|
|
||||||
|
- Prefer `final` for local variables and `const` for compile-time constants
|
||||||
|
- Use `const` constructors wherever all fields are `final`
|
||||||
|
- Return unmodifiable collections from public APIs (`List.unmodifiable`, `Map.unmodifiable`)
|
||||||
|
- Use `copyWith()` for state mutations in immutable state classes
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// BAD
|
||||||
|
var count = 0;
|
||||||
|
List<String> items = ['a', 'b'];
|
||||||
|
|
||||||
|
// GOOD
|
||||||
|
final count = 0;
|
||||||
|
const items = ['a', 'b'];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Naming
|
||||||
|
|
||||||
|
Follow Dart conventions:
|
||||||
|
- `camelCase` for variables, parameters, and named constructors
|
||||||
|
- `PascalCase` for classes, enums, typedefs, and extensions
|
||||||
|
- `snake_case` for file names and library names
|
||||||
|
- `SCREAMING_SNAKE_CASE` for constants declared with `const` at top level
|
||||||
|
- Prefix private members with `_`
|
||||||
|
- Extension names describe the type they extend: `StringExtensions`, not `MyHelpers`
|
||||||
|
|
||||||
|
## Null Safety
|
||||||
|
|
||||||
|
- Avoid `!` (bang operator) — prefer `?.`, `??`, `if (x != null)`, or Dart 3 pattern matching; reserve `!` only where a null value is a programming error and crashing is the right behaviour
|
||||||
|
- Avoid `late` unless initialization is guaranteed before first use (prefer nullable or constructor init)
|
||||||
|
- Use `required` for constructor parameters that must always be provided
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// BAD — crashes at runtime if user is null
|
||||||
|
final name = user!.name;
|
||||||
|
|
||||||
|
// GOOD — null-aware operators
|
||||||
|
final name = user?.name ?? 'Unknown';
|
||||||
|
|
||||||
|
// GOOD — Dart 3 pattern matching (exhaustive, compiler-checked)
|
||||||
|
final name = switch (user) {
|
||||||
|
User(:final name) => name,
|
||||||
|
null => 'Unknown',
|
||||||
|
};
|
||||||
|
|
||||||
|
// GOOD — early-return null guard
|
||||||
|
String getUserName(User? user) {
|
||||||
|
if (user == null) return 'Unknown';
|
||||||
|
return user.name; // promoted to non-null after the guard
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sealed Types and Pattern Matching (Dart 3+)
|
||||||
|
|
||||||
|
Use sealed classes to model closed state hierarchies:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
sealed class AsyncState<T> {
|
||||||
|
const AsyncState();
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Loading<T> extends AsyncState<T> {
|
||||||
|
const Loading();
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Success<T> extends AsyncState<T> {
|
||||||
|
const Success(this.data);
|
||||||
|
final T data;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Failure<T> extends AsyncState<T> {
|
||||||
|
const Failure(this.error);
|
||||||
|
final Object error;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Always use exhaustive `switch` with sealed types — no default/wildcard:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// BAD
|
||||||
|
if (state is Loading) { ... }
|
||||||
|
|
||||||
|
// GOOD
|
||||||
|
return switch (state) {
|
||||||
|
Loading() => const CircularProgressIndicator(),
|
||||||
|
Success(:final data) => DataWidget(data),
|
||||||
|
Failure(:final error) => ErrorWidget(error.toString()),
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- Specify exception types in `on` clauses — never use bare `catch (e)`
|
||||||
|
- Never catch `Error` subtypes — they indicate programming bugs
|
||||||
|
- Use `Result`-style types or sealed classes for recoverable errors
|
||||||
|
- Avoid using exceptions for control flow
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// BAD
|
||||||
|
try {
|
||||||
|
await fetchUser();
|
||||||
|
} catch (e) {
|
||||||
|
log(e.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD
|
||||||
|
try {
|
||||||
|
await fetchUser();
|
||||||
|
} on NetworkException catch (e) {
|
||||||
|
log('Network error: ${e.message}');
|
||||||
|
} on NotFoundException {
|
||||||
|
handleNotFound();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Async / Futures
|
||||||
|
|
||||||
|
- Always `await` Futures or explicitly call `unawaited()` to signal intentional fire-and-forget
|
||||||
|
- Never mark a function `async` if it never `await`s anything
|
||||||
|
- Use `Future.wait` / `Future.any` for concurrent operations
|
||||||
|
- Check `context.mounted` before using `BuildContext` after any `await` (Flutter 3.7+)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// BAD — ignoring Future
|
||||||
|
fetchData(); // fire-and-forget without marking intent
|
||||||
|
|
||||||
|
// GOOD
|
||||||
|
unawaited(fetchData()); // explicit fire-and-forget
|
||||||
|
await fetchData(); // or properly awaited
|
||||||
|
```
|
||||||
|
|
||||||
|
## Imports
|
||||||
|
|
||||||
|
- Use `package:` imports throughout — never relative imports (`../`) for cross-feature or cross-layer code
|
||||||
|
- Order: `dart:` → external `package:` → internal `package:` (same package)
|
||||||
|
- No unused imports — `dart analyze` enforces this with `unused_import`
|
||||||
|
|
||||||
|
## Code Generation
|
||||||
|
|
||||||
|
- Generated files (`.g.dart`, `.freezed.dart`, `.gr.dart`) must be committed or gitignored consistently — pick one strategy per project
|
||||||
|
- Never manually edit generated files
|
||||||
|
- Keep generator annotations (`@JsonSerializable`, `@freezed`, `@riverpod`, etc.) on the canonical source file only
|
||||||
66
rules/dart/hooks.md
Normal file
66
rules/dart/hooks.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
paths:
|
||||||
|
- "**/*.dart"
|
||||||
|
- "**/pubspec.yaml"
|
||||||
|
- "**/analysis_options.yaml"
|
||||||
|
---
|
||||||
|
# Dart/Flutter Hooks
|
||||||
|
|
||||||
|
> This file extends [common/hooks.md](../common/hooks.md) with Dart and Flutter-specific content.
|
||||||
|
|
||||||
|
## PostToolUse Hooks
|
||||||
|
|
||||||
|
Configure in `~/.claude/settings.json`:
|
||||||
|
|
||||||
|
- **dart format**: Auto-format `.dart` files after edit
|
||||||
|
- **dart analyze**: Run static analysis after editing Dart files and surface warnings
|
||||||
|
- **flutter test**: Optionally run affected tests after significant changes
|
||||||
|
|
||||||
|
## Recommended Hook Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PostToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": { "tool_name": "Edit", "file_paths": ["**/*.dart"] },
|
||||||
|
"hooks": [
|
||||||
|
{ "type": "command", "command": "dart format $CLAUDE_FILE_PATHS" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pre-commit Checks
|
||||||
|
|
||||||
|
Run before committing Dart/Flutter changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dart format --set-exit-if-changed .
|
||||||
|
dart analyze --fatal-infos
|
||||||
|
flutter test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Useful One-liners
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Format all Dart files
|
||||||
|
dart format .
|
||||||
|
|
||||||
|
# Analyze and report issues
|
||||||
|
dart analyze
|
||||||
|
|
||||||
|
# Run all tests with coverage
|
||||||
|
flutter test --coverage
|
||||||
|
|
||||||
|
# Regenerate code-gen files
|
||||||
|
dart run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
|
# Check for outdated packages
|
||||||
|
flutter pub outdated
|
||||||
|
|
||||||
|
# Upgrade packages within constraints
|
||||||
|
flutter pub upgrade
|
||||||
|
```
|
||||||
261
rules/dart/patterns.md
Normal file
261
rules/dart/patterns.md
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
---
|
||||||
|
paths:
|
||||||
|
- "**/*.dart"
|
||||||
|
- "**/pubspec.yaml"
|
||||||
|
---
|
||||||
|
# Dart/Flutter Patterns
|
||||||
|
|
||||||
|
> This file extends [common/patterns.md](../common/patterns.md) with Dart, Flutter, and common ecosystem-specific content.
|
||||||
|
|
||||||
|
## Repository Pattern
|
||||||
|
|
||||||
|
```dart
|
||||||
|
abstract interface class UserRepository {
|
||||||
|
Future<User?> getById(String id);
|
||||||
|
Future<List<User>> getAll();
|
||||||
|
Stream<List<User>> watchAll();
|
||||||
|
Future<void> save(User user);
|
||||||
|
Future<void> delete(String id);
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserRepositoryImpl implements UserRepository {
|
||||||
|
const UserRepositoryImpl(this._remote, this._local);
|
||||||
|
|
||||||
|
final UserRemoteDataSource _remote;
|
||||||
|
final UserLocalDataSource _local;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<User?> getById(String id) async {
|
||||||
|
final local = await _local.getById(id);
|
||||||
|
if (local != null) return local;
|
||||||
|
final remote = await _remote.getById(id);
|
||||||
|
if (remote != null) await _local.save(remote);
|
||||||
|
return remote;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<User>> getAll() async {
|
||||||
|
final remote = await _remote.getAll();
|
||||||
|
for (final user in remote) {
|
||||||
|
await _local.save(user);
|
||||||
|
}
|
||||||
|
return remote;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<User>> watchAll() => _local.watchAll();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> save(User user) => _local.save(user);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete(String id) async {
|
||||||
|
await _remote.delete(id);
|
||||||
|
await _local.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Management: BLoC/Cubit
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Cubit — simple state transitions
|
||||||
|
class CounterCubit extends Cubit<int> {
|
||||||
|
CounterCubit() : super(0);
|
||||||
|
|
||||||
|
void increment() => emit(state + 1);
|
||||||
|
void decrement() => emit(state - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// BLoC — event-driven
|
||||||
|
@immutable
|
||||||
|
sealed class CartEvent {}
|
||||||
|
class CartItemAdded extends CartEvent { CartItemAdded(this.item); final Item item; }
|
||||||
|
class CartItemRemoved extends CartEvent { CartItemRemoved(this.id); final String id; }
|
||||||
|
class CartCleared extends CartEvent {}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class CartState {
|
||||||
|
const CartState({this.items = const []});
|
||||||
|
final List<Item> items;
|
||||||
|
CartState copyWith({List<Item>? items}) => CartState(items: items ?? this.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CartBloc extends Bloc<CartEvent, CartState> {
|
||||||
|
CartBloc() : super(const CartState()) {
|
||||||
|
on<CartItemAdded>((event, emit) =>
|
||||||
|
emit(state.copyWith(items: [...state.items, event.item])));
|
||||||
|
on<CartItemRemoved>((event, emit) =>
|
||||||
|
emit(state.copyWith(items: state.items.where((i) => i.id != event.id).toList())));
|
||||||
|
on<CartCleared>((_, emit) => emit(const CartState()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Management: Riverpod
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Simple provider
|
||||||
|
@riverpod
|
||||||
|
Future<List<User>> users(Ref ref) async {
|
||||||
|
final repo = ref.watch(userRepositoryProvider);
|
||||||
|
return repo.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notifier for mutable state
|
||||||
|
@riverpod
|
||||||
|
class CartNotifier extends _$CartNotifier {
|
||||||
|
@override
|
||||||
|
List<Item> build() => [];
|
||||||
|
|
||||||
|
void add(Item item) => state = [...state, item];
|
||||||
|
void remove(String id) => state = state.where((i) => i.id != id).toList();
|
||||||
|
void clear() => state = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConsumerWidget
|
||||||
|
class CartPage extends ConsumerWidget {
|
||||||
|
const CartPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final items = ref.watch(cartNotifierProvider);
|
||||||
|
return ListView(
|
||||||
|
children: items.map((item) => CartItemTile(item: item)).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency Injection
|
||||||
|
|
||||||
|
Constructor injection is preferred. Use `get_it` or Riverpod providers at composition root:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// get_it registration (in a setup file)
|
||||||
|
void setupDependencies() {
|
||||||
|
final di = GetIt.instance;
|
||||||
|
di.registerSingleton<ApiClient>(ApiClient(baseUrl: Env.apiUrl));
|
||||||
|
di.registerSingleton<UserRepository>(
|
||||||
|
UserRepositoryImpl(di<ApiClient>(), di<LocalDatabase>()),
|
||||||
|
);
|
||||||
|
di.registerFactory(() => UserListViewModel(di<UserRepository>()));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ViewModel Pattern (without BLoC/Riverpod)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class UserListViewModel extends ChangeNotifier {
|
||||||
|
UserListViewModel(this._repository);
|
||||||
|
|
||||||
|
final UserRepository _repository;
|
||||||
|
|
||||||
|
AsyncState<List<User>> _state = const Loading();
|
||||||
|
AsyncState<List<User>> get state => _state;
|
||||||
|
|
||||||
|
Future<void> load() async {
|
||||||
|
_state = const Loading();
|
||||||
|
notifyListeners();
|
||||||
|
try {
|
||||||
|
final users = await _repository.getAll();
|
||||||
|
_state = Success(users);
|
||||||
|
} on Exception catch (e) {
|
||||||
|
_state = Failure(e);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## UseCase Pattern
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class GetUserUseCase {
|
||||||
|
const GetUserUseCase(this._repository);
|
||||||
|
final UserRepository _repository;
|
||||||
|
|
||||||
|
Future<User?> call(String id) => _repository.getById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreateUserUseCase {
|
||||||
|
const CreateUserUseCase(this._repository, this._idGenerator);
|
||||||
|
final UserRepository _repository;
|
||||||
|
final IdGenerator _idGenerator; // injected — domain layer must not depend on uuid package directly
|
||||||
|
|
||||||
|
Future<void> call(CreateUserInput input) async {
|
||||||
|
// Validate, apply business rules, then persist
|
||||||
|
final user = User(id: _idGenerator.generate(), name: input.name, email: input.email);
|
||||||
|
await _repository.save(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Immutable State with freezed
|
||||||
|
|
||||||
|
```dart
|
||||||
|
@freezed
|
||||||
|
class UserState with _$UserState {
|
||||||
|
const factory UserState({
|
||||||
|
@Default([]) List<User> users,
|
||||||
|
@Default(false) bool isLoading,
|
||||||
|
String? errorMessage,
|
||||||
|
}) = _UserState;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Clean Architecture Layer Boundaries
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
├── domain/ # Pure Dart — no Flutter, no external packages
|
||||||
|
│ ├── entities/
|
||||||
|
│ ├── repositories/ # Abstract interfaces
|
||||||
|
│ └── usecases/
|
||||||
|
├── data/ # Implements domain interfaces
|
||||||
|
│ ├── datasources/
|
||||||
|
│ ├── models/ # DTOs with fromJson/toJson
|
||||||
|
│ └── repositories/
|
||||||
|
└── presentation/ # Flutter widgets + state management
|
||||||
|
├── pages/
|
||||||
|
├── widgets/
|
||||||
|
└── providers/ (or blocs/ or viewmodels/)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Domain must not import `package:flutter` or any data-layer package
|
||||||
|
- Data layer maps DTOs to domain entities at repository boundaries
|
||||||
|
- Presentation calls use cases, not repositories directly
|
||||||
|
|
||||||
|
## Navigation (GoRouter)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final router = GoRouter(
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/',
|
||||||
|
builder: (context, state) => const HomePage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/users/:id',
|
||||||
|
builder: (context, state) {
|
||||||
|
final id = state.pathParameters['id']!;
|
||||||
|
return UserDetailPage(userId: id);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
// refreshListenable re-evaluates redirect whenever auth state changes
|
||||||
|
refreshListenable: GoRouterRefreshStream(authCubit.stream),
|
||||||
|
redirect: (context, state) {
|
||||||
|
final isLoggedIn = context.read<AuthCubit>().state is AuthAuthenticated;
|
||||||
|
if (!isLoggedIn && !state.matchedLocation.startsWith('/login')) {
|
||||||
|
return '/login';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
See skill: `flutter-dart-code-review` for the comprehensive review checklist.
|
||||||
|
See skill: `compose-multiplatform-patterns` for Kotlin Multiplatform/Flutter interop patterns.
|
||||||
135
rules/dart/security.md
Normal file
135
rules/dart/security.md
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
---
|
||||||
|
paths:
|
||||||
|
- "**/*.dart"
|
||||||
|
- "**/pubspec.yaml"
|
||||||
|
- "**/AndroidManifest.xml"
|
||||||
|
- "**/Info.plist"
|
||||||
|
---
|
||||||
|
# Dart/Flutter Security
|
||||||
|
|
||||||
|
> This file extends [common/security.md](../common/security.md) with Dart, Flutter, and mobile-specific content.
|
||||||
|
|
||||||
|
## Secrets Management
|
||||||
|
|
||||||
|
- Never hardcode API keys, tokens, or credentials in Dart source
|
||||||
|
- Use `--dart-define` or `--dart-define-from-file` for compile-time config (values are not truly secret — use a backend proxy for server-side secrets)
|
||||||
|
- Use `flutter_dotenv` or equivalent, with `.env` files listed in `.gitignore`
|
||||||
|
- Store runtime secrets in platform-secure storage: `flutter_secure_storage` (Keychain on iOS, EncryptedSharedPreferences on Android)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// BAD
|
||||||
|
const apiKey = 'sk-abc123...';
|
||||||
|
|
||||||
|
// GOOD — compile-time config (not secret, just configurable)
|
||||||
|
const apiKey = String.fromEnvironment('API_KEY');
|
||||||
|
|
||||||
|
// GOOD — runtime secret from secure storage
|
||||||
|
final token = await secureStorage.read(key: 'auth_token');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Network Security
|
||||||
|
|
||||||
|
- Enforce HTTPS — no `http://` calls in production
|
||||||
|
- Configure Android `network_security_config.xml` to block cleartext traffic
|
||||||
|
- Set `NSAppTransportSecurity` in `Info.plist` to disallow arbitrary loads
|
||||||
|
- Set request timeouts on all HTTP clients — never leave defaults
|
||||||
|
- Consider certificate pinning for high-security endpoints
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Dio with timeout and HTTPS enforcement
|
||||||
|
final dio = Dio(BaseOptions(
|
||||||
|
baseUrl: 'https://api.example.com',
|
||||||
|
connectTimeout: const Duration(seconds: 10),
|
||||||
|
receiveTimeout: const Duration(seconds: 30),
|
||||||
|
));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Input Validation
|
||||||
|
|
||||||
|
- Validate and sanitize all user input before sending to API or storage
|
||||||
|
- Never pass unsanitized input to SQL queries — use parameterized queries (sqflite, drift)
|
||||||
|
- Sanitize deep link URLs before navigation — validate scheme, host, and path parameters
|
||||||
|
- Use `Uri.tryParse` and validate before navigating
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// BAD — SQL injection
|
||||||
|
await db.rawQuery("SELECT * FROM users WHERE email = '$userInput'");
|
||||||
|
|
||||||
|
// GOOD — parameterized
|
||||||
|
await db.query('users', where: 'email = ?', whereArgs: [userInput]);
|
||||||
|
|
||||||
|
// BAD — unvalidated deep link
|
||||||
|
final uri = Uri.parse(incomingLink);
|
||||||
|
context.go(uri.path); // could navigate to any route
|
||||||
|
|
||||||
|
// GOOD — validated deep link
|
||||||
|
final uri = Uri.tryParse(incomingLink);
|
||||||
|
if (uri != null && uri.host == 'myapp.com' && _allowedPaths.contains(uri.path)) {
|
||||||
|
context.go(uri.path);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Protection
|
||||||
|
|
||||||
|
- Store tokens, PII, and credentials only in `flutter_secure_storage`
|
||||||
|
- Never write sensitive data to `SharedPreferences` or local files in plaintext
|
||||||
|
- Clear auth state on logout: tokens, cached user data, cookies
|
||||||
|
- Use biometric authentication (`local_auth`) for sensitive operations
|
||||||
|
- Avoid logging sensitive data — no `print(token)` or `debugPrint(password)`
|
||||||
|
|
||||||
|
## Android-Specific
|
||||||
|
|
||||||
|
- Declare only required permissions in `AndroidManifest.xml`
|
||||||
|
- Export Android components (`Activity`, `Service`, `BroadcastReceiver`) only when necessary; add `android:exported="false"` where not needed
|
||||||
|
- Review intent filters — exported components with implicit intent filters are accessible by any app
|
||||||
|
- Use `FLAG_SECURE` for screens displaying sensitive data (prevents screenshots)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- AndroidManifest.xml — restrict exported components -->
|
||||||
|
<activity android:name=".MainActivity" android:exported="true">
|
||||||
|
<!-- Only the launcher activity needs exported=true -->
|
||||||
|
</activity>
|
||||||
|
<activity android:name=".SensitiveActivity" android:exported="false" />
|
||||||
|
```
|
||||||
|
|
||||||
|
## iOS-Specific
|
||||||
|
|
||||||
|
- Declare only required usage descriptions in `Info.plist` (`NSCameraUsageDescription`, etc.)
|
||||||
|
- Store secrets in Keychain — `flutter_secure_storage` uses Keychain on iOS
|
||||||
|
- Use App Transport Security (ATS) — disallow arbitrary loads
|
||||||
|
- Enable data protection entitlement for sensitive files
|
||||||
|
|
||||||
|
## WebView Security
|
||||||
|
|
||||||
|
- Use `webview_flutter` v4+ (`WebViewController` / `WebViewWidget`) — the legacy `WebView` widget is removed
|
||||||
|
- Disable JavaScript unless explicitly required (`JavaScriptMode.disabled`)
|
||||||
|
- Validate URLs before loading — never load arbitrary URLs from deep links
|
||||||
|
- Never expose Dart callbacks to JavaScript unless absolutely needed and carefully sandboxed
|
||||||
|
- Use `NavigationDelegate.onNavigationRequest` to intercept and validate navigation requests
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// webview_flutter v4+ API (WebViewController + WebViewWidget)
|
||||||
|
final controller = WebViewController()
|
||||||
|
..setJavaScriptMode(JavaScriptMode.disabled) // disabled unless required
|
||||||
|
..setNavigationDelegate(
|
||||||
|
NavigationDelegate(
|
||||||
|
onNavigationRequest: (request) {
|
||||||
|
final uri = Uri.tryParse(request.url);
|
||||||
|
if (uri == null || uri.host != 'trusted.example.com') {
|
||||||
|
return NavigationDecision.prevent;
|
||||||
|
}
|
||||||
|
return NavigationDecision.navigate;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// In your widget tree:
|
||||||
|
WebViewWidget(controller: controller)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Obfuscation and Build Security
|
||||||
|
|
||||||
|
- Enable obfuscation in release builds: `flutter build apk --obfuscate --split-debug-info=./debug-info/`
|
||||||
|
- Keep `--split-debug-info` output out of version control (used for crash symbolication only)
|
||||||
|
- Ensure ProGuard/R8 rules don't inadvertently expose serialized classes
|
||||||
|
- Run `flutter analyze` and address all warnings before release
|
||||||
215
rules/dart/testing.md
Normal file
215
rules/dart/testing.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
---
|
||||||
|
paths:
|
||||||
|
- "**/*.dart"
|
||||||
|
- "**/pubspec.yaml"
|
||||||
|
- "**/analysis_options.yaml"
|
||||||
|
---
|
||||||
|
# Dart/Flutter Testing
|
||||||
|
|
||||||
|
> This file extends [common/testing.md](../common/testing.md) with Dart and Flutter-specific content.
|
||||||
|
|
||||||
|
## Test Framework
|
||||||
|
|
||||||
|
- **flutter_test** / **dart:test** — built-in test runner
|
||||||
|
- **mockito** (with `@GenerateMocks`) or **mocktail** (no codegen) for mocking
|
||||||
|
- **bloc_test** for BLoC/Cubit unit tests
|
||||||
|
- **fake_async** for controlling time in unit tests
|
||||||
|
- **integration_test** for end-to-end device tests
|
||||||
|
|
||||||
|
## Test Types
|
||||||
|
|
||||||
|
| Type | Tool | Location | When to Write |
|
||||||
|
|------|------|----------|---------------|
|
||||||
|
| Unit | `dart:test` | `test/unit/` | All domain logic, state managers, repositories |
|
||||||
|
| Widget | `flutter_test` | `test/widget/` | All widgets with meaningful behavior |
|
||||||
|
| Golden | `flutter_test` | `test/golden/` | Design-critical UI components |
|
||||||
|
| Integration | `integration_test` | `integration_test/` | Critical user flows on real device/emulator |
|
||||||
|
|
||||||
|
## Unit Tests: State Managers
|
||||||
|
|
||||||
|
### BLoC with `bloc_test`
|
||||||
|
|
||||||
|
```dart
|
||||||
|
group('CartBloc', () {
|
||||||
|
late CartBloc bloc;
|
||||||
|
late MockCartRepository repository;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
repository = MockCartRepository();
|
||||||
|
bloc = CartBloc(repository);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() => bloc.close());
|
||||||
|
|
||||||
|
blocTest<CartBloc, CartState>(
|
||||||
|
'emits updated items when CartItemAdded',
|
||||||
|
build: () => bloc,
|
||||||
|
act: (b) => b.add(CartItemAdded(testItem)),
|
||||||
|
expect: () => [CartState(items: [testItem])],
|
||||||
|
);
|
||||||
|
|
||||||
|
blocTest<CartBloc, CartState>(
|
||||||
|
'emits empty cart when CartCleared',
|
||||||
|
seed: () => CartState(items: [testItem]),
|
||||||
|
build: () => bloc,
|
||||||
|
act: (b) => b.add(CartCleared()),
|
||||||
|
expect: () => [const CartState()],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Riverpod with `ProviderContainer`
|
||||||
|
|
||||||
|
```dart
|
||||||
|
test('usersProvider loads users from repository', () async {
|
||||||
|
final container = ProviderContainer(
|
||||||
|
overrides: [userRepositoryProvider.overrideWithValue(FakeUserRepository())],
|
||||||
|
);
|
||||||
|
addTearDown(container.dispose);
|
||||||
|
|
||||||
|
final result = await container.read(usersProvider.future);
|
||||||
|
expect(result, isNotEmpty);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Widget Tests
|
||||||
|
|
||||||
|
```dart
|
||||||
|
testWidgets('CartPage shows item count badge', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
cartNotifierProvider.overrideWith(() => FakeCartNotifier([testItem])),
|
||||||
|
],
|
||||||
|
child: const MaterialApp(home: CartPage()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('1'), findsOneWidget);
|
||||||
|
expect(find.byType(CartItemTile), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows empty state when cart is empty', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ProviderScope(
|
||||||
|
overrides: [cartNotifierProvider.overrideWith(() => FakeCartNotifier([]))],
|
||||||
|
child: const MaterialApp(home: CartPage()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Your cart is empty'), findsOneWidget);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fakes Over Mocks
|
||||||
|
|
||||||
|
Prefer hand-written fakes for complex dependencies:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class FakeUserRepository implements UserRepository {
|
||||||
|
final _users = <String, User>{};
|
||||||
|
Object? fetchError;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<User?> getById(String id) async {
|
||||||
|
if (fetchError != null) throw fetchError!;
|
||||||
|
return _users[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<User>> getAll() async {
|
||||||
|
if (fetchError != null) throw fetchError!;
|
||||||
|
return _users.values.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<User>> watchAll() => Stream.value(_users.values.toList());
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> save(User user) async {
|
||||||
|
_users[user.id] = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete(String id) async {
|
||||||
|
_users.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addUser(User user) => _users[user.id] = user;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Async Testing
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Use fake_async for controlling timers and Futures
|
||||||
|
test('debounce triggers after 300ms', () {
|
||||||
|
fakeAsync((async) {
|
||||||
|
final debouncer = Debouncer(delay: const Duration(milliseconds: 300));
|
||||||
|
var callCount = 0;
|
||||||
|
debouncer.run(() => callCount++);
|
||||||
|
expect(callCount, 0);
|
||||||
|
async.elapse(const Duration(milliseconds: 200));
|
||||||
|
expect(callCount, 0);
|
||||||
|
async.elapse(const Duration(milliseconds: 200));
|
||||||
|
expect(callCount, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Golden Tests
|
||||||
|
|
||||||
|
```dart
|
||||||
|
testWidgets('UserCard golden test', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(home: UserCard(user: testUser)),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
find.byType(UserCard),
|
||||||
|
matchesGoldenFile('goldens/user_card.png'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `flutter test --update-goldens` when intentional visual changes are made.
|
||||||
|
|
||||||
|
## Test Naming
|
||||||
|
|
||||||
|
Use descriptive, behavior-focused names:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
test('returns null when user does not exist', () { ... });
|
||||||
|
test('throws NotFoundException when id is empty string', () { ... });
|
||||||
|
testWidgets('disables submit button while form is invalid', (tester) async { ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Organization
|
||||||
|
|
||||||
|
```
|
||||||
|
test/
|
||||||
|
├── unit/
|
||||||
|
│ ├── domain/
|
||||||
|
│ │ └── usecases/
|
||||||
|
│ └── data/
|
||||||
|
│ └── repositories/
|
||||||
|
├── widget/
|
||||||
|
│ └── presentation/
|
||||||
|
│ └── pages/
|
||||||
|
└── golden/
|
||||||
|
└── widgets/
|
||||||
|
|
||||||
|
integration_test/
|
||||||
|
└── flows/
|
||||||
|
├── login_flow_test.dart
|
||||||
|
└── checkout_flow_test.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
|
||||||
|
- Target 80%+ line coverage for business logic (domain + state managers)
|
||||||
|
- All state transitions must have tests: loading → success, loading → error, retry
|
||||||
|
- Run `flutter test --coverage` and inspect `lcov.info` with a coverage reporter
|
||||||
|
- Coverage failures should block CI when below threshold
|
||||||
321
skills/csharp-testing/SKILL.md
Normal file
321
skills/csharp-testing/SKILL.md
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
---
|
||||||
|
name: csharp-testing
|
||||||
|
description: C# and .NET testing patterns with xUnit, FluentAssertions, mocking, integration tests, and test organization best practices.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# C# Testing Patterns
|
||||||
|
|
||||||
|
Comprehensive testing patterns for .NET applications using xUnit, FluentAssertions, and modern testing practices.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- Writing new tests for C# code
|
||||||
|
- Reviewing test quality and coverage
|
||||||
|
- Setting up test infrastructure for .NET projects
|
||||||
|
- Debugging flaky or slow tests
|
||||||
|
|
||||||
|
## Test Framework Stack
|
||||||
|
|
||||||
|
| Tool | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| **xUnit** | Test framework (preferred for .NET) |
|
||||||
|
| **FluentAssertions** | Readable assertion syntax |
|
||||||
|
| **NSubstitute** or **Moq** | Mocking dependencies |
|
||||||
|
| **Testcontainers** | Real infrastructure in integration tests |
|
||||||
|
| **WebApplicationFactory** | ASP.NET Core integration tests |
|
||||||
|
| **Bogus** | Realistic test data generation |
|
||||||
|
|
||||||
|
## Unit Test Structure
|
||||||
|
|
||||||
|
### Arrange-Act-Assert
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class OrderServiceTests
|
||||||
|
{
|
||||||
|
private readonly IOrderRepository _repository = Substitute.For<IOrderRepository>();
|
||||||
|
private readonly ILogger<OrderService> _logger = Substitute.For<ILogger<OrderService>>();
|
||||||
|
private readonly OrderService _sut;
|
||||||
|
|
||||||
|
public OrderServiceTests()
|
||||||
|
{
|
||||||
|
_sut = new OrderService(_repository, _logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PlaceOrderAsync_ReturnsSuccess_WhenRequestIsValid()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = new CreateOrderRequest
|
||||||
|
{
|
||||||
|
CustomerId = "cust-123",
|
||||||
|
Items = [new OrderItem("SKU-001", 2, 29.99m)]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.PlaceOrderAsync(request, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
result.Value.Should().NotBeNull();
|
||||||
|
result.Value!.CustomerId.Should().Be("cust-123");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PlaceOrderAsync_ReturnsFailure_WhenNoItems()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = new CreateOrderRequest
|
||||||
|
{
|
||||||
|
CustomerId = "cust-123",
|
||||||
|
Items = []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.PlaceOrderAsync(request, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.IsSuccess.Should().BeFalse();
|
||||||
|
result.Error.Should().Contain("at least one item");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameterized Tests with Theory
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Theory]
|
||||||
|
[InlineData("", false)]
|
||||||
|
[InlineData("a", false)]
|
||||||
|
[InlineData("ab@c.d", false)]
|
||||||
|
[InlineData("user@example.com", true)]
|
||||||
|
[InlineData("user+tag@example.co.uk", true)]
|
||||||
|
public void IsValidEmail_ReturnsExpected(string email, bool expected)
|
||||||
|
{
|
||||||
|
EmailValidator.IsValid(email).Should().Be(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(InvalidOrderCases))]
|
||||||
|
public async Task PlaceOrderAsync_RejectsInvalidOrders(CreateOrderRequest request, string expectedError)
|
||||||
|
{
|
||||||
|
var result = await _sut.PlaceOrderAsync(request, CancellationToken.None);
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeFalse();
|
||||||
|
result.Error.Should().Contain(expectedError);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData<CreateOrderRequest, string> InvalidOrderCases => new()
|
||||||
|
{
|
||||||
|
{ new() { CustomerId = "", Items = [ValidItem()] }, "CustomerId" },
|
||||||
|
{ new() { CustomerId = "c1", Items = [] }, "at least one item" },
|
||||||
|
{ new() { CustomerId = "c1", Items = [new("", 1, 10m)] }, "SKU" },
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mocking with NSubstitute
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task GetOrderAsync_ReturnsNull_WhenNotFound()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var orderId = Guid.NewGuid();
|
||||||
|
_repository.FindByIdAsync(orderId, Arg.Any<CancellationToken>())
|
||||||
|
.Returns((Order?)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sut.GetOrderAsync(orderId, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PlaceOrderAsync_PersistsOrder()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = ValidOrderRequest();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.PlaceOrderAsync(request, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert — verify the repository was called
|
||||||
|
await _repository.Received(1).AddAsync(
|
||||||
|
Arg.Is<Order>(o => o.CustomerId == request.CustomerId),
|
||||||
|
Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ASP.NET Core Integration Tests
|
||||||
|
|
||||||
|
### WebApplicationFactory Setup
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class OrderApiTests : IClassFixture<WebApplicationFactory<Program>>
|
||||||
|
{
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
|
||||||
|
public OrderApiTests(WebApplicationFactory<Program> factory)
|
||||||
|
{
|
||||||
|
_client = factory.WithWebHostBuilder(builder =>
|
||||||
|
{
|
||||||
|
builder.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
// Replace real DB with in-memory for tests
|
||||||
|
services.RemoveAll<DbContextOptions<AppDbContext>>();
|
||||||
|
services.AddDbContext<AppDbContext>(options =>
|
||||||
|
options.UseInMemoryDatabase("TestDb"));
|
||||||
|
});
|
||||||
|
}).CreateClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetOrder_Returns404_WhenNotFound()
|
||||||
|
{
|
||||||
|
var response = await _client.GetAsync($"/api/orders/{Guid.NewGuid()}");
|
||||||
|
|
||||||
|
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateOrder_Returns201_WithValidRequest()
|
||||||
|
{
|
||||||
|
var request = new CreateOrderRequest
|
||||||
|
{
|
||||||
|
CustomerId = "cust-1",
|
||||||
|
Items = [new("SKU-001", 1, 19.99m)]
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync("/api/orders", request);
|
||||||
|
|
||||||
|
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||||
|
response.Headers.Location.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing with Testcontainers
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class PostgresOrderRepositoryTests : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
|
||||||
|
.WithImage("postgres:16-alpine")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
private AppDbContext _db = null!;
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
await _postgres.StartAsync();
|
||||||
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||||
|
.UseNpgsql(_postgres.GetConnectionString())
|
||||||
|
.Options;
|
||||||
|
_db = new AppDbContext(options);
|
||||||
|
await _db.Database.MigrateAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DisposeAsync()
|
||||||
|
{
|
||||||
|
await _db.DisposeAsync();
|
||||||
|
await _postgres.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddAsync_PersistsOrder()
|
||||||
|
{
|
||||||
|
var repo = new SqlOrderRepository(_db);
|
||||||
|
var order = Order.Create("cust-1", [new OrderItem("SKU-001", 2, 10m)]);
|
||||||
|
|
||||||
|
await repo.AddAsync(order, CancellationToken.None);
|
||||||
|
|
||||||
|
var found = await repo.FindByIdAsync(order.Id, CancellationToken.None);
|
||||||
|
found.Should().NotBeNull();
|
||||||
|
found!.Items.Should().HaveCount(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Organization
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
MyApp.UnitTests/
|
||||||
|
Services/
|
||||||
|
OrderServiceTests.cs
|
||||||
|
PaymentServiceTests.cs
|
||||||
|
Validators/
|
||||||
|
EmailValidatorTests.cs
|
||||||
|
MyApp.IntegrationTests/
|
||||||
|
Api/
|
||||||
|
OrderApiTests.cs
|
||||||
|
Repositories/
|
||||||
|
OrderRepositoryTests.cs
|
||||||
|
MyApp.TestHelpers/
|
||||||
|
Builders/
|
||||||
|
OrderBuilder.cs
|
||||||
|
Fixtures/
|
||||||
|
DatabaseFixture.cs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Data Builders
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class OrderBuilder
|
||||||
|
{
|
||||||
|
private string _customerId = "cust-default";
|
||||||
|
private readonly List<OrderItem> _items = [new("SKU-001", 1, 10m)];
|
||||||
|
|
||||||
|
public OrderBuilder WithCustomer(string customerId)
|
||||||
|
{
|
||||||
|
_customerId = customerId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OrderBuilder WithItem(string sku, int quantity, decimal price)
|
||||||
|
{
|
||||||
|
_items.Add(new OrderItem(sku, quantity, price));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Order Build() => Order.Create(_customerId, _items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage in tests
|
||||||
|
var order = new OrderBuilder()
|
||||||
|
.WithCustomer("cust-vip")
|
||||||
|
.WithItem("SKU-PREMIUM", 3, 99.99m)
|
||||||
|
.Build();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Anti-Patterns
|
||||||
|
|
||||||
|
| Anti-Pattern | Fix |
|
||||||
|
|---|---|
|
||||||
|
| Testing implementation details | Test behavior and outcomes |
|
||||||
|
| Shared mutable test state | Fresh instance per test (xUnit does this via constructors) |
|
||||||
|
| `Thread.Sleep` in async tests | Use `Task.Delay` with timeout, or polling helpers |
|
||||||
|
| Asserting on `ToString()` output | Assert on typed properties |
|
||||||
|
| One giant assertion per test | One logical assertion per test |
|
||||||
|
| Test names describing implementation | Name by behavior: `Method_ExpectedResult_WhenCondition` |
|
||||||
|
| Ignoring `CancellationToken` | Always pass and verify cancellation |
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
dotnet test
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
dotnet test --collect:"XPlat Code Coverage"
|
||||||
|
|
||||||
|
# Run specific project
|
||||||
|
dotnet test tests/MyApp.UnitTests/
|
||||||
|
|
||||||
|
# Filter by test name
|
||||||
|
dotnet test --filter "FullyQualifiedName~OrderService"
|
||||||
|
|
||||||
|
# Watch mode during development
|
||||||
|
dotnet watch test --project tests/MyApp.UnitTests/
|
||||||
|
```
|
||||||
563
skills/dart-flutter-patterns/SKILL.md
Normal file
563
skills/dart-flutter-patterns/SKILL.md
Normal file
@@ -0,0 +1,563 @@
|
|||||||
|
---
|
||||||
|
name: dart-flutter-patterns
|
||||||
|
description: Production-ready Dart and Flutter patterns covering null safety, immutable state, async composition, widget architecture, popular state management frameworks (BLoC, Riverpod, Provider), GoRouter navigation, Dio networking, Freezed code generation, and clean architecture.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Dart/Flutter Patterns
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
Use this skill when:
|
||||||
|
- Starting a new Flutter feature and need idiomatic patterns for state management, navigation, or data access
|
||||||
|
- Reviewing or writing Dart code and need guidance on null safety, sealed types, or async composition
|
||||||
|
- Setting up a new Flutter project and choosing between BLoC, Riverpod, or Provider
|
||||||
|
- Implementing secure HTTP clients, WebView integration, or local storage
|
||||||
|
- Writing tests for Flutter widgets, Cubits, or Riverpod providers
|
||||||
|
- Wiring up GoRouter with authentication guards
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
This skill provides copy-paste-ready Dart/Flutter code patterns organized by concern:
|
||||||
|
1. **Null safety** — avoid `!`, prefer `?.`/`??`/pattern matching
|
||||||
|
2. **Immutable state** — sealed classes, `freezed`, `copyWith`
|
||||||
|
3. **Async composition** — concurrent `Future.wait`, safe `BuildContext` after `await`
|
||||||
|
4. **Widget architecture** — extract to classes (not methods), `const` propagation, scoped rebuilds
|
||||||
|
5. **State management** — BLoC/Cubit events, Riverpod notifiers and derived providers
|
||||||
|
6. **Navigation** — GoRouter with reactive auth guards via `refreshListenable`
|
||||||
|
7. **Networking** — Dio with interceptors, token refresh with one-time retry guard
|
||||||
|
8. **Error handling** — global capture, `ErrorWidget.builder`, crashlytics wiring
|
||||||
|
9. **Testing** — unit (BLoC test), widget (ProviderScope overrides), fakes over mocks
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Sealed state — prevents impossible states
|
||||||
|
sealed class AsyncState<T> {}
|
||||||
|
final class Loading<T> extends AsyncState<T> {}
|
||||||
|
final class Success<T> extends AsyncState<T> { final T data; const Success(this.data); }
|
||||||
|
final class Failure<T> extends AsyncState<T> { final Object error; const Failure(this.error); }
|
||||||
|
|
||||||
|
// GoRouter with reactive auth redirect
|
||||||
|
final router = GoRouter(
|
||||||
|
refreshListenable: GoRouterRefreshStream(authCubit.stream),
|
||||||
|
redirect: (context, state) {
|
||||||
|
final authed = context.read<AuthCubit>().state is AuthAuthenticated;
|
||||||
|
if (!authed && !state.matchedLocation.startsWith('/login')) return '/login';
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
routes: [...],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Riverpod derived provider with safe firstWhereOrNull
|
||||||
|
@riverpod
|
||||||
|
double cartTotal(Ref ref) {
|
||||||
|
final cart = ref.watch(cartNotifierProvider);
|
||||||
|
final products = ref.watch(productsProvider).valueOrNull ?? [];
|
||||||
|
return cart.fold(0.0, (total, item) {
|
||||||
|
final product = products.firstWhereOrNull((p) => p.id == item.productId);
|
||||||
|
return total + (product?.price ?? 0) * item.quantity;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Practical, production-ready patterns for Dart and Flutter applications. Library-agnostic where possible, with explicit coverage of the most common ecosystem packages.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Null Safety Fundamentals
|
||||||
|
|
||||||
|
### Prefer Patterns Over Bang Operator
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// BAD — crashes at runtime if null
|
||||||
|
final name = user!.name;
|
||||||
|
|
||||||
|
// GOOD — provide fallback
|
||||||
|
final name = user?.name ?? 'Unknown';
|
||||||
|
|
||||||
|
// GOOD — Dart 3 pattern matching (preferred for complex cases)
|
||||||
|
final display = switch (user) {
|
||||||
|
User(:final name, :final email) => '$name <$email>',
|
||||||
|
null => 'Guest',
|
||||||
|
};
|
||||||
|
|
||||||
|
// GOOD — guard early return
|
||||||
|
String getUserName(User? user) {
|
||||||
|
if (user == null) return 'Unknown';
|
||||||
|
return user.name; // promoted to non-null after check
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Avoid `late` Overuse
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// BAD — defers null error to runtime
|
||||||
|
late String userId;
|
||||||
|
|
||||||
|
// GOOD — nullable with explicit initialization
|
||||||
|
String? userId;
|
||||||
|
|
||||||
|
// OK — use late only when initialization is guaranteed before first access
|
||||||
|
// (e.g., in initState() before any widget interaction)
|
||||||
|
late final AnimationController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Immutable State
|
||||||
|
|
||||||
|
### Sealed Classes for State Hierarchies
|
||||||
|
|
||||||
|
```dart
|
||||||
|
sealed class UserState {}
|
||||||
|
|
||||||
|
final class UserInitial extends UserState {}
|
||||||
|
|
||||||
|
final class UserLoading extends UserState {}
|
||||||
|
|
||||||
|
final class UserLoaded extends UserState {
|
||||||
|
const UserLoaded(this.user);
|
||||||
|
final User user;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UserError extends UserState {
|
||||||
|
const UserError(this.message);
|
||||||
|
final String message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exhaustive switch — compiler enforces all branches
|
||||||
|
Widget buildFrom(UserState state) => switch (state) {
|
||||||
|
UserInitial() => const SizedBox.shrink(),
|
||||||
|
UserLoading() => const CircularProgressIndicator(),
|
||||||
|
UserLoaded(:final user) => UserCard(user: user),
|
||||||
|
UserError(:final message) => ErrorText(message),
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Freezed for Boilerplate-Free Immutability
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'user.freezed.dart';
|
||||||
|
part 'user.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class User with _$User {
|
||||||
|
const factory User({
|
||||||
|
required String id,
|
||||||
|
required String name,
|
||||||
|
required String email,
|
||||||
|
@Default(false) bool isAdmin,
|
||||||
|
}) = _User;
|
||||||
|
|
||||||
|
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
final user = User(id: '1', name: 'Alice', email: 'alice@example.com');
|
||||||
|
final updated = user.copyWith(name: 'Alice Smith'); // immutable update
|
||||||
|
final json = user.toJson();
|
||||||
|
final fromJson = User.fromJson(json);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Async Composition
|
||||||
|
|
||||||
|
### Structured Concurrency with Future.wait
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<DashboardData> loadDashboard(UserRepository users, OrderRepository orders) async {
|
||||||
|
// Run concurrently — don't await sequentially
|
||||||
|
final (userList, orderList) = await (
|
||||||
|
users.getAll(),
|
||||||
|
orders.getRecent(),
|
||||||
|
).wait; // Dart 3 record destructuring + Future.wait extension
|
||||||
|
|
||||||
|
return DashboardData(users: userList, orders: orderList);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stream Patterns
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Repository exposes reactive streams for live data
|
||||||
|
Stream<List<Item>> watchCartItems() => _db
|
||||||
|
.watchTable('cart_items')
|
||||||
|
.map((rows) => rows.map(Item.fromRow).toList());
|
||||||
|
|
||||||
|
// In widget layer — declarative, no manual subscription
|
||||||
|
StreamBuilder<List<Item>>(
|
||||||
|
stream: cartRepository.watchCartItems(),
|
||||||
|
builder: (context, snapshot) => switch (snapshot) {
|
||||||
|
AsyncSnapshot(connectionState: ConnectionState.waiting) =>
|
||||||
|
const CircularProgressIndicator(),
|
||||||
|
AsyncSnapshot(:final error?) => ErrorWidget(error.toString()),
|
||||||
|
AsyncSnapshot(:final data?) => CartList(items: data),
|
||||||
|
_ => const SizedBox.shrink(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### BuildContext After Await
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// CRITICAL — always check mounted after any await in StatefulWidget
|
||||||
|
Future<void> _handleSubmit() async {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
try {
|
||||||
|
await authService.login(_email, _password);
|
||||||
|
if (!mounted) return; // ← guard before using context
|
||||||
|
context.go('/home');
|
||||||
|
} on AuthException catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.message)));
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Widget Architecture
|
||||||
|
|
||||||
|
### Extract to Classes, Not Methods
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// BAD — private method returning widget, prevents optimization
|
||||||
|
Widget _buildHeader() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Text(title, style: Theme.of(context).textTheme.headlineMedium),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD — separate widget class, enables const, element reuse
|
||||||
|
class _PageHeader extends StatelessWidget {
|
||||||
|
const _PageHeader(this.title);
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Text(title, style: Theme.of(context).textTheme.headlineMedium),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### const Propagation
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// BAD — new instances every rebuild
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16.0), // not const
|
||||||
|
child: Icon(Icons.home, size: 24.0), // not const
|
||||||
|
)
|
||||||
|
|
||||||
|
// GOOD — const stops rebuild propagation
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: Icon(Icons.home, size: 24.0),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scoped Rebuilds
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// BAD — entire page rebuilds on every counter change
|
||||||
|
class CounterPage extends ConsumerWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final count = ref.watch(counterProvider); // rebuilds everything
|
||||||
|
return Scaffold(
|
||||||
|
body: Column(children: [
|
||||||
|
const ExpensiveHeader(), // unnecessarily rebuilt
|
||||||
|
Text('$count'),
|
||||||
|
const ExpensiveFooter(), // unnecessarily rebuilt
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD — isolate the rebuilding part
|
||||||
|
class CounterPage extends StatelessWidget {
|
||||||
|
const CounterPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Scaffold(
|
||||||
|
body: Column(children: [
|
||||||
|
ExpensiveHeader(), // never rebuilt (const)
|
||||||
|
_CounterDisplay(), // only this rebuilds
|
||||||
|
ExpensiveFooter(), // never rebuilt (const)
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CounterDisplay extends ConsumerWidget {
|
||||||
|
const _CounterDisplay();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final count = ref.watch(counterProvider);
|
||||||
|
return Text('$count');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. State Management: BLoC/Cubit
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Cubit — synchronous or simple async state
|
||||||
|
class AuthCubit extends Cubit<AuthState> {
|
||||||
|
AuthCubit(this._authService) : super(const AuthState.initial());
|
||||||
|
final AuthService _authService;
|
||||||
|
|
||||||
|
Future<void> login(String email, String password) async {
|
||||||
|
emit(const AuthState.loading());
|
||||||
|
try {
|
||||||
|
final user = await _authService.login(email, password);
|
||||||
|
emit(AuthState.authenticated(user));
|
||||||
|
} on AuthException catch (e) {
|
||||||
|
emit(AuthState.error(e.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void logout() {
|
||||||
|
_authService.logout();
|
||||||
|
emit(const AuthState.initial());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In widget
|
||||||
|
BlocBuilder<AuthCubit, AuthState>(
|
||||||
|
builder: (context, state) => switch (state) {
|
||||||
|
AuthInitial() => const LoginForm(),
|
||||||
|
AuthLoading() => const CircularProgressIndicator(),
|
||||||
|
AuthAuthenticated(:final user) => HomePage(user: user),
|
||||||
|
AuthError(:final message) => ErrorView(message: message),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. State Management: Riverpod
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Auto-dispose async provider
|
||||||
|
@riverpod
|
||||||
|
Future<List<Product>> products(Ref ref) async {
|
||||||
|
final repo = ref.watch(productRepositoryProvider);
|
||||||
|
return repo.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notifier with complex mutations
|
||||||
|
@riverpod
|
||||||
|
class CartNotifier extends _$CartNotifier {
|
||||||
|
@override
|
||||||
|
List<CartItem> build() => [];
|
||||||
|
|
||||||
|
void add(Product product) {
|
||||||
|
final existing = state.where((i) => i.productId == product.id).firstOrNull;
|
||||||
|
if (existing != null) {
|
||||||
|
state = [
|
||||||
|
for (final item in state)
|
||||||
|
if (item.productId == product.id) item.copyWith(quantity: item.quantity + 1)
|
||||||
|
else item,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
state = [...state, CartItem(productId: product.id, quantity: 1)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove(String productId) =>
|
||||||
|
state = state.where((i) => i.productId != productId).toList();
|
||||||
|
|
||||||
|
void clear() => state = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derived provider (selector pattern)
|
||||||
|
@riverpod
|
||||||
|
int cartCount(Ref ref) => ref.watch(cartNotifierProvider).length;
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
double cartTotal(Ref ref) {
|
||||||
|
final cart = ref.watch(cartNotifierProvider);
|
||||||
|
final products = ref.watch(productsProvider).valueOrNull ?? [];
|
||||||
|
return cart.fold(0.0, (total, item) {
|
||||||
|
// firstWhereOrNull (from collection package) avoids StateError when product is missing
|
||||||
|
final product = products.firstWhereOrNull((p) => p.id == item.productId);
|
||||||
|
return total + (product?.price ?? 0) * item.quantity;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Navigation with GoRouter
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final router = GoRouter(
|
||||||
|
initialLocation: '/',
|
||||||
|
// refreshListenable re-evaluates redirect whenever auth state changes
|
||||||
|
refreshListenable: GoRouterRefreshStream(authCubit.stream),
|
||||||
|
redirect: (context, state) {
|
||||||
|
final isLoggedIn = context.read<AuthCubit>().state is AuthAuthenticated;
|
||||||
|
final isGoingToLogin = state.matchedLocation == '/login';
|
||||||
|
if (!isLoggedIn && !isGoingToLogin) return '/login';
|
||||||
|
if (isLoggedIn && isGoingToLogin) return '/';
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
GoRoute(path: '/login', builder: (_, __) => const LoginPage()),
|
||||||
|
ShellRoute(
|
||||||
|
builder: (context, state, child) => AppShell(child: child),
|
||||||
|
routes: [
|
||||||
|
GoRoute(path: '/', builder: (_, __) => const HomePage()),
|
||||||
|
GoRoute(
|
||||||
|
path: '/products/:id',
|
||||||
|
builder: (context, state) =>
|
||||||
|
ProductDetailPage(id: state.pathParameters['id']!),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. HTTP with Dio
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final dio = Dio(BaseOptions(
|
||||||
|
baseUrl: const String.fromEnvironment('API_URL'),
|
||||||
|
connectTimeout: const Duration(seconds: 10),
|
||||||
|
receiveTimeout: const Duration(seconds: 30),
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
));
|
||||||
|
|
||||||
|
// Add auth interceptor
|
||||||
|
dio.interceptors.add(InterceptorsWrapper(
|
||||||
|
onRequest: (options, handler) async {
|
||||||
|
final token = await secureStorage.read(key: 'auth_token');
|
||||||
|
if (token != null) options.headers['Authorization'] = 'Bearer $token';
|
||||||
|
handler.next(options);
|
||||||
|
},
|
||||||
|
onError: (error, handler) async {
|
||||||
|
// Guard against infinite retry loops: only attempt refresh once per request
|
||||||
|
final isRetry = error.requestOptions.extra['_isRetry'] == true;
|
||||||
|
if (!isRetry && error.response?.statusCode == 401) {
|
||||||
|
final refreshed = await attemptTokenRefresh();
|
||||||
|
if (refreshed) {
|
||||||
|
error.requestOptions.extra['_isRetry'] = true;
|
||||||
|
return handler.resolve(await dio.fetch(error.requestOptions));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.next(error);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
// Repository using Dio
|
||||||
|
class UserApiDataSource {
|
||||||
|
const UserApiDataSource(this._dio);
|
||||||
|
final Dio _dio;
|
||||||
|
|
||||||
|
Future<User> getById(String id) async {
|
||||||
|
final response = await _dio.get<Map<String, dynamic>>('/users/$id');
|
||||||
|
return User.fromJson(response.data!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Error Handling Architecture
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Global error capture — set up in main()
|
||||||
|
void main() {
|
||||||
|
FlutterError.onError = (details) {
|
||||||
|
FlutterError.presentError(details);
|
||||||
|
crashlytics.recordFlutterFatalError(details);
|
||||||
|
};
|
||||||
|
|
||||||
|
PlatformDispatcher.instance.onError = (error, stack) {
|
||||||
|
crashlytics.recordError(error, stack, fatal: true);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
runApp(const App());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom ErrorWidget for production
|
||||||
|
class App extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
ErrorWidget.builder = (details) => ProductionErrorWidget(details);
|
||||||
|
return MaterialApp.router(routerConfig: router);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Testing Quick Reference
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Unit test — use case
|
||||||
|
test('GetUserUseCase returns null for missing user', () async {
|
||||||
|
final repo = FakeUserRepository();
|
||||||
|
final useCase = GetUserUseCase(repo);
|
||||||
|
expect(await useCase('missing-id'), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
// BLoC test
|
||||||
|
blocTest<AuthCubit, AuthState>(
|
||||||
|
'emits loading then error on failed login',
|
||||||
|
build: () => AuthCubit(FakeAuthService(throwsOn: 'login')),
|
||||||
|
act: (cubit) => cubit.login('user@test.com', 'wrong'),
|
||||||
|
expect: () => [const AuthState.loading(), isA<AuthError>()],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Widget test
|
||||||
|
testWidgets('CartBadge shows item count', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ProviderScope(
|
||||||
|
overrides: [cartNotifierProvider.overrideWith(() => FakeCartNotifier(count: 3))],
|
||||||
|
child: const MaterialApp(home: CartBadge()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(find.text('3'), findsOneWidget);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Effective Dart: Design](https://dart.dev/effective-dart/design)
|
||||||
|
- [Flutter Performance Best Practices](https://docs.flutter.dev/perf/best-practices)
|
||||||
|
- [Riverpod Documentation](https://riverpod.dev/)
|
||||||
|
- [BLoC Library](https://bloclibrary.dev/)
|
||||||
|
- [GoRouter](https://pub.dev/packages/go_router)
|
||||||
|
- [Freezed](https://pub.dev/packages/freezed)
|
||||||
|
- Skill: `flutter-dart-code-review` — comprehensive review checklist
|
||||||
|
- Rules: `rules/dart/` — coding style, patterns, security, testing, hooks
|
||||||
321
skills/dotnet-patterns/SKILL.md
Normal file
321
skills/dotnet-patterns/SKILL.md
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
---
|
||||||
|
name: dotnet-patterns
|
||||||
|
description: Idiomatic C# and .NET patterns, conventions, dependency injection, async/await, and best practices for building robust, maintainable .NET applications.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# .NET Development Patterns
|
||||||
|
|
||||||
|
Idiomatic C# and .NET patterns for building robust, performant, and maintainable applications.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- Writing new C# code
|
||||||
|
- Reviewing C# code
|
||||||
|
- Refactoring existing .NET applications
|
||||||
|
- Designing service architectures with ASP.NET Core
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
### 1. Prefer Immutability
|
||||||
|
|
||||||
|
Use records and init-only properties for data models. Mutability should be an explicit, justified choice.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Good: Immutable value object
|
||||||
|
public sealed record Money(decimal Amount, string Currency);
|
||||||
|
|
||||||
|
// Good: Immutable DTO with init setters
|
||||||
|
public sealed class CreateOrderRequest
|
||||||
|
{
|
||||||
|
public required string CustomerId { get; init; }
|
||||||
|
public required IReadOnlyList<OrderItem> Items { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad: Mutable model with public setters
|
||||||
|
public class Order
|
||||||
|
{
|
||||||
|
public string CustomerId { get; set; }
|
||||||
|
public List<OrderItem> Items { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Explicit Over Implicit
|
||||||
|
|
||||||
|
Be clear about nullability, access modifiers, and intent.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Good: Explicit access modifiers and nullability
|
||||||
|
public sealed class UserService
|
||||||
|
{
|
||||||
|
private readonly IUserRepository _repository;
|
||||||
|
private readonly ILogger<UserService> _logger;
|
||||||
|
|
||||||
|
public UserService(IUserRepository repository, ILogger<UserService> logger)
|
||||||
|
{
|
||||||
|
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User?> FindByIdAsync(Guid id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _repository.FindByIdAsync(id, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Depend on Abstractions
|
||||||
|
|
||||||
|
Use interfaces for service boundaries. Register via DI container.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Good: Interface-based dependency
|
||||||
|
public interface IOrderRepository
|
||||||
|
{
|
||||||
|
Task<Order?> FindByIdAsync(Guid id, CancellationToken cancellationToken);
|
||||||
|
Task<IReadOnlyList<Order>> FindByCustomerAsync(string customerId, CancellationToken cancellationToken);
|
||||||
|
Task AddAsync(Order order, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registration
|
||||||
|
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Async/Await Patterns
|
||||||
|
|
||||||
|
### Proper Async Usage
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Good: Async all the way, with CancellationToken
|
||||||
|
public async Task<OrderSummary> GetOrderSummaryAsync(
|
||||||
|
Guid orderId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var order = await _repository.FindByIdAsync(orderId, cancellationToken)
|
||||||
|
?? throw new NotFoundException($"Order {orderId} not found");
|
||||||
|
|
||||||
|
var customer = await _customerService.GetAsync(order.CustomerId, cancellationToken);
|
||||||
|
|
||||||
|
return new OrderSummary(order, customer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad: Blocking on async
|
||||||
|
public OrderSummary GetOrderSummary(Guid orderId)
|
||||||
|
{
|
||||||
|
var order = _repository.FindByIdAsync(orderId, CancellationToken.None).Result; // Deadlock risk
|
||||||
|
return new OrderSummary(order);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parallel Async Operations
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Good: Concurrent independent operations
|
||||||
|
public async Task<DashboardData> LoadDashboardAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var ordersTask = _orderService.GetRecentAsync(cancellationToken);
|
||||||
|
var metricsTask = _metricsService.GetCurrentAsync(cancellationToken);
|
||||||
|
var alertsTask = _alertService.GetActiveAsync(cancellationToken);
|
||||||
|
|
||||||
|
await Task.WhenAll(ordersTask, metricsTask, alertsTask);
|
||||||
|
|
||||||
|
return new DashboardData(
|
||||||
|
Orders: await ordersTask,
|
||||||
|
Metrics: await metricsTask,
|
||||||
|
Alerts: await alertsTask);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options Pattern
|
||||||
|
|
||||||
|
Bind configuration sections to strongly-typed objects.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class SmtpOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "Smtp";
|
||||||
|
|
||||||
|
public required string Host { get; init; }
|
||||||
|
public required int Port { get; init; }
|
||||||
|
public required string Username { get; init; }
|
||||||
|
public bool UseSsl { get; init; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registration
|
||||||
|
builder.Services.Configure<SmtpOptions>(
|
||||||
|
builder.Configuration.GetSection(SmtpOptions.SectionName));
|
||||||
|
|
||||||
|
// Usage via injection
|
||||||
|
public class EmailService(IOptions<SmtpOptions> options)
|
||||||
|
{
|
||||||
|
private readonly SmtpOptions _smtp = options.Value;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Result Pattern
|
||||||
|
|
||||||
|
Return explicit success/failure instead of throwing for expected failures.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed record Result<T>
|
||||||
|
{
|
||||||
|
public bool IsSuccess { get; }
|
||||||
|
public T? Value { get; }
|
||||||
|
public string? Error { get; }
|
||||||
|
|
||||||
|
private Result(T value) { IsSuccess = true; Value = value; }
|
||||||
|
private Result(string error) { IsSuccess = false; Error = error; }
|
||||||
|
|
||||||
|
public static Result<T> Success(T value) => new(value);
|
||||||
|
public static Result<T> Failure(string error) => new(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
public async Task<Result<Order>> PlaceOrderAsync(CreateOrderRequest request)
|
||||||
|
{
|
||||||
|
if (request.Items.Count == 0)
|
||||||
|
return Result<Order>.Failure("Order must contain at least one item");
|
||||||
|
|
||||||
|
var order = Order.Create(request);
|
||||||
|
await _repository.AddAsync(order, CancellationToken.None);
|
||||||
|
return Result<Order>.Success(order);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Repository Pattern with EF Core
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class SqlOrderRepository : IOrderRepository
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
|
||||||
|
public SqlOrderRepository(AppDbContext db) => _db = db;
|
||||||
|
|
||||||
|
public async Task<Order?> FindByIdAsync(Guid id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _db.Orders
|
||||||
|
.Include(o => o.Items)
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<Order>> FindByCustomerAsync(
|
||||||
|
string customerId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _db.Orders
|
||||||
|
.Where(o => o.CustomerId == customerId)
|
||||||
|
.OrderByDescending(o => o.CreatedAt)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddAsync(Order order, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_db.Orders.Add(order);
|
||||||
|
await _db.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Middleware and Pipeline
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Custom middleware
|
||||||
|
public sealed class RequestTimingMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger<RequestTimingMiddleware> _logger;
|
||||||
|
|
||||||
|
public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Request {Method} {Path} completed in {ElapsedMs}ms with status {StatusCode}",
|
||||||
|
context.Request.Method,
|
||||||
|
context.Request.Path,
|
||||||
|
stopwatch.ElapsedMilliseconds,
|
||||||
|
context.Response.StatusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Minimal API Patterns
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Organized with route groups
|
||||||
|
var orders = app.MapGroup("/api/orders")
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithTags("Orders");
|
||||||
|
|
||||||
|
orders.MapGet("/{id:guid}", async (
|
||||||
|
Guid id,
|
||||||
|
IOrderRepository repository,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
var order = await repository.FindByIdAsync(id, cancellationToken);
|
||||||
|
return order is not null
|
||||||
|
? TypedResults.Ok(order)
|
||||||
|
: TypedResults.NotFound();
|
||||||
|
});
|
||||||
|
|
||||||
|
orders.MapPost("/", async (
|
||||||
|
CreateOrderRequest request,
|
||||||
|
IOrderService service,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
var result = await service.PlaceOrderAsync(request, cancellationToken);
|
||||||
|
return result.IsSuccess
|
||||||
|
? TypedResults.Created($"/api/orders/{result.Value!.Id}", result.Value)
|
||||||
|
: TypedResults.BadRequest(result.Error);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Guard Clauses
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Good: Early returns with clear validation
|
||||||
|
public async Task<ProcessResult> ProcessPaymentAsync(
|
||||||
|
PaymentRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
|
||||||
|
if (request.Amount <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(request.Amount), "Amount must be positive");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(request.Currency))
|
||||||
|
throw new ArgumentException("Currency is required", nameof(request.Currency));
|
||||||
|
|
||||||
|
// Happy path continues here without nesting
|
||||||
|
var gateway = _gatewayFactory.Create(request.Currency);
|
||||||
|
return await gateway.ChargeAsync(request, cancellationToken);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Anti-Patterns to Avoid
|
||||||
|
|
||||||
|
| Anti-Pattern | Fix |
|
||||||
|
|---|---|
|
||||||
|
| `async void` methods | Return `Task` (except event handlers) |
|
||||||
|
| `.Result` or `.Wait()` | Use `await` |
|
||||||
|
| `catch (Exception) { }` | Handle or rethrow with context |
|
||||||
|
| `new Service()` in constructors | Use constructor injection |
|
||||||
|
| `public` fields | Use properties with appropriate accessors |
|
||||||
|
| `dynamic` in business logic | Use generics or explicit types |
|
||||||
|
| Mutable `static` state | Use DI scoping or `ConcurrentDictionary` |
|
||||||
|
| `string.Format` in loops | Use `StringBuilder` or interpolated string handlers |
|
||||||
Reference in New Issue
Block a user