feat: add C# and Dart language support

This commit is contained in:
Affaan Mustafa
2026-04-02 17:48:43 -07:00
parent 31c9f7c33e
commit badccc3db9
20 changed files with 2800 additions and 23 deletions

159
rules/dart/coding-style.md Normal file
View 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
View 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
View 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
View 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
View 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