@sun-asterisk/sunlint 1.3.48 → 1.3.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/file-targeting-service.js +148 -15
- package/core/init-command.js +118 -70
- package/core/project-detector.js +517 -0
- package/core/tui-select.js +245 -0
- package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
- package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
- package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
- package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
- package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
- package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
- package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
- package/package.json +1 -1
- package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
- package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
- package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
- package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Do Not Leave Unused Variables
|
|
3
|
+
impact: LOW
|
|
4
|
+
impactDescription: reduces code noise and potential bugs
|
|
5
|
+
tags: variables, cleanup, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Do Not Leave Unused Variables
|
|
9
|
+
|
|
10
|
+
Unused variables suggest incomplete refactoring or bugs.
|
|
11
|
+
|
|
12
|
+
**Incorrect (unused variables):**
|
|
13
|
+
|
|
14
|
+
```dart
|
|
15
|
+
void processOrder(Order order) {
|
|
16
|
+
final user = order.user; // Never used
|
|
17
|
+
final total = order.total; // Never used
|
|
18
|
+
final items = order.items;
|
|
19
|
+
|
|
20
|
+
return items.map((i) => i.name).toList();
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Correct (only needed variables):**
|
|
25
|
+
|
|
26
|
+
```dart
|
|
27
|
+
void processOrder(Order order) {
|
|
28
|
+
return order.items.map((i) => i.name).toList();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// If destructuring/pattern matching, prefix with _ for intentionally ignored
|
|
32
|
+
void handleEvent(Map<String, dynamic> event) {
|
|
33
|
+
final type = event['type'] as String;
|
|
34
|
+
final _ = event['payload']; // Intentionally ignored
|
|
35
|
+
print('Event: $type');
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Enable via analysis_options.yaml:**
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
# analysis_options.yaml
|
|
43
|
+
linter:
|
|
44
|
+
rules:
|
|
45
|
+
- unused_local_variable
|
|
46
|
+
- unused_element
|
|
47
|
+
- avoid_unused_constructor_parameters
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Tools:** dart analyze, `unused_local_variable` lint rule
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: No Duplicate Variable Names In Scope
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: prevents shadowing bugs
|
|
5
|
+
tags: variables, shadowing, scope, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## No Duplicate Variable Names In Scope
|
|
9
|
+
|
|
10
|
+
Variable shadowing causes subtle bugs where inner variables hide outer ones.
|
|
11
|
+
|
|
12
|
+
**Incorrect (shadowed variables):**
|
|
13
|
+
|
|
14
|
+
```dart
|
|
15
|
+
final user = getCurrentUser();
|
|
16
|
+
|
|
17
|
+
void processOrder(Order order) {
|
|
18
|
+
final user = order.user; // Shadows outer user!
|
|
19
|
+
|
|
20
|
+
// Which user is this?
|
|
21
|
+
print(user.name);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Same name in nested scope
|
|
25
|
+
for (final item in items) {
|
|
26
|
+
final item = transform(item); // Shadows loop variable!
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Correct (unique names):**
|
|
31
|
+
|
|
32
|
+
```dart
|
|
33
|
+
final currentUser = getCurrentUser();
|
|
34
|
+
|
|
35
|
+
void processOrder(Order order) {
|
|
36
|
+
final orderUser = order.user; // Clear distinction
|
|
37
|
+
|
|
38
|
+
print(orderUser.name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Different names in nested scope
|
|
42
|
+
for (final item in items) {
|
|
43
|
+
final transformedItem = transform(item);
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Enable via analysis_options.yaml:**
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
# analysis_options.yaml
|
|
51
|
+
linter:
|
|
52
|
+
rules:
|
|
53
|
+
- no_leading_underscores_for_local_identifiers
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Tools:** dart analyze, Code Review
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Centralize Constants
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: makes values easy to find and update
|
|
5
|
+
tags: constants, magic-numbers, configuration, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Centralize Constants
|
|
9
|
+
|
|
10
|
+
Magic numbers scattered throughout code are hard to find and change.
|
|
11
|
+
|
|
12
|
+
**Incorrect (magic numbers):**
|
|
13
|
+
|
|
14
|
+
```dart
|
|
15
|
+
if (password.length < 8) { }
|
|
16
|
+
if (retryCount > 3) { }
|
|
17
|
+
if (status == 1) { }
|
|
18
|
+
await Future.delayed(const Duration(milliseconds: 300000));
|
|
19
|
+
if (user.role == 'admin') { }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Correct (centralized constants):**
|
|
23
|
+
|
|
24
|
+
```dart
|
|
25
|
+
// lib/constants/app_constants.dart
|
|
26
|
+
class AppConstants {
|
|
27
|
+
AppConstants._(); // Prevent instantiation
|
|
28
|
+
|
|
29
|
+
static const int passwordMinLength = 8;
|
|
30
|
+
static const int maxRetryAttempts = 3;
|
|
31
|
+
static const Duration sessionTimeout = Duration(minutes: 5);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class OrderStatus {
|
|
35
|
+
OrderStatus._();
|
|
36
|
+
|
|
37
|
+
static const int pending = 1;
|
|
38
|
+
static const int approved = 2;
|
|
39
|
+
static const int shipped = 3;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class UserRole {
|
|
43
|
+
UserRole._();
|
|
44
|
+
|
|
45
|
+
static const String admin = 'admin';
|
|
46
|
+
static const String user = 'user';
|
|
47
|
+
static const String guest = 'guest';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Usage
|
|
51
|
+
if (password.length < AppConstants.passwordMinLength) { }
|
|
52
|
+
if (retryCount > AppConstants.maxRetryAttempts) { }
|
|
53
|
+
if (status == OrderStatus.pending) { }
|
|
54
|
+
await Future.delayed(AppConstants.sessionTimeout);
|
|
55
|
+
if (user.role == UserRole.admin) { }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Alternative: use enums for type-safe values:**
|
|
59
|
+
|
|
60
|
+
```dart
|
|
61
|
+
enum OrderStatus { pending, approved, shipped }
|
|
62
|
+
enum UserRole { admin, user, guest }
|
|
63
|
+
|
|
64
|
+
// Usage
|
|
65
|
+
if (order.status == OrderStatus.pending) { }
|
|
66
|
+
if (user.role == UserRole.admin) { }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Benefits:**
|
|
70
|
+
- Single source of truth
|
|
71
|
+
- Self-documenting
|
|
72
|
+
- Easy to update
|
|
73
|
+
- Type safety with enums
|
|
74
|
+
|
|
75
|
+
**Tools:** dart analyze, Code Review
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: All Catch Blocks Must Log Root Cause
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: enables debugging and incident response
|
|
5
|
+
tags: error-handling, logging, debugging, observability, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## All Catch Blocks Must Log Root Cause
|
|
9
|
+
|
|
10
|
+
Silent failures make debugging impossible. Without proper logging, you cannot trace issues in production.
|
|
11
|
+
|
|
12
|
+
**Incorrect (silent or minimal logging):**
|
|
13
|
+
|
|
14
|
+
```dart
|
|
15
|
+
try {
|
|
16
|
+
await processPayment(order);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
// Empty catch - silent failure!
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await saveUser(user);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return null; // No logging, no context
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Correct (comprehensive error logging):**
|
|
29
|
+
|
|
30
|
+
```dart
|
|
31
|
+
try {
|
|
32
|
+
await processPayment(order);
|
|
33
|
+
} catch (e, st) {
|
|
34
|
+
logger.e(
|
|
35
|
+
'Payment processing failed',
|
|
36
|
+
error: e,
|
|
37
|
+
stackTrace: st,
|
|
38
|
+
);
|
|
39
|
+
logger.e('Context: orderId=${order.id}, userId=${order.userId}, amount=${order.amount}');
|
|
40
|
+
throw PaymentFailedException(
|
|
41
|
+
'Payment could not be processed',
|
|
42
|
+
cause: e,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Log context should include:**
|
|
48
|
+
- Error object and stack trace (`error:`, `stackTrace:`)
|
|
49
|
+
- Relevant entity IDs (order, user, etc.)
|
|
50
|
+
- Input that caused the error
|
|
51
|
+
- Timing information
|
|
52
|
+
|
|
53
|
+
**Tools:** logger package, Code Review
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Custom Exception Classes
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: enables proper error categorization and handling
|
|
5
|
+
tags: error-handling, custom-errors, exceptions, patterns, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use Custom Exception Classes
|
|
9
|
+
|
|
10
|
+
Custom exception classes enable proper error handling, categorization, and monitoring. They provide clear contracts for callers.
|
|
11
|
+
|
|
12
|
+
**Incorrect (generic exceptions):**
|
|
13
|
+
|
|
14
|
+
```dart
|
|
15
|
+
throw Exception('User not found');
|
|
16
|
+
throw Exception('Invalid input');
|
|
17
|
+
throw Exception('Network error');
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Correct (custom exception hierarchy):**
|
|
21
|
+
|
|
22
|
+
```dart
|
|
23
|
+
// Base application exception
|
|
24
|
+
class AppException implements Exception {
|
|
25
|
+
const AppException(this.message, {this.code, this.context});
|
|
26
|
+
|
|
27
|
+
final String message;
|
|
28
|
+
final String? code;
|
|
29
|
+
final Map<String, dynamic>? context;
|
|
30
|
+
|
|
31
|
+
@override
|
|
32
|
+
String toString() => 'AppException($code): $message';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Specific exceptions
|
|
36
|
+
class UserNotFoundException extends AppException {
|
|
37
|
+
UserNotFoundException(String userId)
|
|
38
|
+
: super(
|
|
39
|
+
'User $userId not found',
|
|
40
|
+
code: 'USER_NOT_FOUND',
|
|
41
|
+
context: {'userId': userId},
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class ValidationException extends AppException {
|
|
46
|
+
ValidationException({required String field, required String message})
|
|
47
|
+
: super(
|
|
48
|
+
message,
|
|
49
|
+
code: 'VALIDATION_ERROR',
|
|
50
|
+
context: {'field': field},
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class NetworkException extends AppException {
|
|
55
|
+
NetworkException({required int statusCode, required String endpoint})
|
|
56
|
+
: super(
|
|
57
|
+
'Network request failed with status $statusCode',
|
|
58
|
+
code: 'NETWORK_ERROR',
|
|
59
|
+
context: {'statusCode': statusCode, 'endpoint': endpoint},
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Usage
|
|
64
|
+
throw UserNotFoundException(userId);
|
|
65
|
+
throw ValidationException(field: 'email', message: 'Invalid email format');
|
|
66
|
+
throw NetworkException(statusCode: response.statusCode, endpoint: '/users');
|
|
67
|
+
|
|
68
|
+
// Handling
|
|
69
|
+
try {
|
|
70
|
+
await fetchUser(id);
|
|
71
|
+
} on UserNotFoundException catch (e) {
|
|
72
|
+
showNotFoundScreen();
|
|
73
|
+
} on NetworkException catch (e) {
|
|
74
|
+
showNetworkError();
|
|
75
|
+
} on AppException catch (e) {
|
|
76
|
+
showGenericError(e.message);
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Benefits:**
|
|
81
|
+
- Type-safe error handling with `on`
|
|
82
|
+
- Structured error context
|
|
83
|
+
- Consistent error format
|
|
84
|
+
- Clear error hierarchy
|
|
85
|
+
|
|
86
|
+
**Tools:** Code Review, dart_analyzer
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Separate Processing And Data Access
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: enables testable business logic
|
|
5
|
+
tags: separation, repository, service, architecture, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Separate Processing And Data Access
|
|
9
|
+
|
|
10
|
+
Mixing business logic with database/API access creates tight coupling and makes testing require real databases or network.
|
|
11
|
+
|
|
12
|
+
**Incorrect (mixed concerns):**
|
|
13
|
+
|
|
14
|
+
```dart
|
|
15
|
+
class OrderService {
|
|
16
|
+
final database = DatabaseHelper.instance;
|
|
17
|
+
|
|
18
|
+
Future<double> calculateDiscount(String userId) async {
|
|
19
|
+
// Business logic mixed with data access
|
|
20
|
+
final userMap = await database.query(
|
|
21
|
+
'users',
|
|
22
|
+
where: 'id = ?',
|
|
23
|
+
whereArgs: [userId],
|
|
24
|
+
);
|
|
25
|
+
final orderMaps = await database.query(
|
|
26
|
+
'orders',
|
|
27
|
+
where: 'user_id = ?',
|
|
28
|
+
whereArgs: [userId],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
double discount = 0;
|
|
32
|
+
if (orderMaps.length > 10) discount += 5;
|
|
33
|
+
if (userMap.first['is_premium'] == 1) discount += 10;
|
|
34
|
+
|
|
35
|
+
return discount;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Correct (separated layers):**
|
|
41
|
+
|
|
42
|
+
```dart
|
|
43
|
+
// Repository - data access only
|
|
44
|
+
abstract class IUserRepository {
|
|
45
|
+
Future<User?> findById(String userId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
abstract class IOrderRepository {
|
|
49
|
+
Future<List<Order>> findByUserId(String userId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class SqlUserRepository implements IUserRepository {
|
|
53
|
+
SqlUserRepository(this._db);
|
|
54
|
+
final DatabaseHelper _db;
|
|
55
|
+
|
|
56
|
+
@override
|
|
57
|
+
Future<User?> findById(String userId) async {
|
|
58
|
+
final maps = await _db.query('users', where: 'id = ?', whereArgs: [userId]);
|
|
59
|
+
return maps.isEmpty ? null : User.fromMap(maps.first);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Service - business logic only
|
|
64
|
+
class DiscountService {
|
|
65
|
+
DiscountService({
|
|
66
|
+
required IUserRepository userRepo,
|
|
67
|
+
required IOrderRepository orderRepo,
|
|
68
|
+
}) : _userRepo = userRepo,
|
|
69
|
+
_orderRepo = orderRepo;
|
|
70
|
+
|
|
71
|
+
final IUserRepository _userRepo;
|
|
72
|
+
final IOrderRepository _orderRepo;
|
|
73
|
+
|
|
74
|
+
Future<double> calculateDiscount(String userId) async {
|
|
75
|
+
final user = await _userRepo.findById(userId);
|
|
76
|
+
final orders = await _orderRepo.findByUserId(userId);
|
|
77
|
+
|
|
78
|
+
return _computeDiscount(user, orders);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
double _computeDiscount(User? user, List<Order> orders) {
|
|
82
|
+
double discount = 0;
|
|
83
|
+
if (orders.length > 10) discount += 5;
|
|
84
|
+
if (user?.isPremium == true) discount += 10;
|
|
85
|
+
return discount;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Tools:** Code Review, Architecture Review
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Log All Relevant Context On Errors
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: enables quick debugging and incident response
|
|
5
|
+
tags: error-handling, logging, context, debugging, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Log All Relevant Context On Errors
|
|
9
|
+
|
|
10
|
+
Context-rich logs enable quick debugging. Without proper context, finding root causes is like finding a needle in a haystack.
|
|
11
|
+
|
|
12
|
+
**Incorrect (minimal context):**
|
|
13
|
+
|
|
14
|
+
```dart
|
|
15
|
+
logger.e('Error occurred');
|
|
16
|
+
logger.e(e.toString());
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Correct (comprehensive context):**
|
|
20
|
+
|
|
21
|
+
```dart
|
|
22
|
+
final stopwatch = Stopwatch()..start();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await processOrder(order);
|
|
26
|
+
} catch (e, st) {
|
|
27
|
+
logger.e(
|
|
28
|
+
'Failed to process order - '
|
|
29
|
+
'orderId=${order.id}, userId=${user.id}, '
|
|
30
|
+
'itemCount=${order.items.length}, total=${order.total}, '
|
|
31
|
+
'processingTimeMs=${stopwatch.elapsedMilliseconds}',
|
|
32
|
+
error: e,
|
|
33
|
+
stackTrace: st,
|
|
34
|
+
);
|
|
35
|
+
rethrow;
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Essential context to include:**
|
|
40
|
+
- Error object and stack trace (`error:`, `stackTrace:`)
|
|
41
|
+
- Entity identifiers (orderId, userId, etc.)
|
|
42
|
+
- Input that caused the issue (item count, amounts)
|
|
43
|
+
- Timing information (`elapsedMilliseconds`)
|
|
44
|
+
- Device/session context where applicable (Flutter)
|
|
45
|
+
|
|
46
|
+
**Flutter-specific context:**
|
|
47
|
+
|
|
48
|
+
```dart
|
|
49
|
+
try {
|
|
50
|
+
await submitForm(formData);
|
|
51
|
+
} catch (e, st) {
|
|
52
|
+
logger.e(
|
|
53
|
+
'Form submission failed - '
|
|
54
|
+
'screen=${ModalRoute.of(context)?.settings.name}, '
|
|
55
|
+
'userId=${authProvider.userId}',
|
|
56
|
+
error: e,
|
|
57
|
+
stackTrace: st,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Tools:** logger package, Code Review
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: No Hardcoded Secrets
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: prevents credential leakage from source code
|
|
5
|
+
tags: security, secrets, credentials, api-keys, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## No Hardcoded Secrets
|
|
9
|
+
|
|
10
|
+
Secrets hardcoded in source code will be exposed in version control, app binaries, and decompilation.
|
|
11
|
+
|
|
12
|
+
**Incorrect (hardcoded secrets):**
|
|
13
|
+
|
|
14
|
+
```dart
|
|
15
|
+
// BAD: Hardcoded API key in source code
|
|
16
|
+
const apiKey = 'sk_live_abc123xyz456';
|
|
17
|
+
const dbPassword = 'MySecretP@ss!';
|
|
18
|
+
const jwtSecret = 'super-secret-jwt-key';
|
|
19
|
+
const stripeKey = 'pk_live_51Abc...';
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Correct (externalized secrets - Flutter):**
|
|
23
|
+
|
|
24
|
+
```dart
|
|
25
|
+
// Option 1: dart-define at build time (recommended for CI/CD)
|
|
26
|
+
// Run: flutter build apk --dart-define=API_KEY=sk_live_xxx
|
|
27
|
+
const apiKey = String.fromEnvironment('API_KEY');
|
|
28
|
+
const apiUrl = String.fromEnvironment('API_URL', defaultValue: 'https://api.dev.example.com');
|
|
29
|
+
|
|
30
|
+
// Option 2: flutter_secure_storage for runtime secrets
|
|
31
|
+
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
32
|
+
|
|
33
|
+
class SecretRepository {
|
|
34
|
+
final _storage = const FlutterSecureStorage();
|
|
35
|
+
|
|
36
|
+
Future<String?> getApiKey() => _storage.read(key: 'api_key');
|
|
37
|
+
Future<void> saveApiKey(String key) => _storage.write(key: 'api_key', value: key);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Option 3: Remote config (Firebase Remote Config)
|
|
41
|
+
final remoteConfig = FirebaseRemoteConfig.instance;
|
|
42
|
+
final featureFlag = remoteConfig.getBool('new_feature_enabled');
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**For .env files (development only):**
|
|
46
|
+
|
|
47
|
+
```dart
|
|
48
|
+
// pubspec.yaml - add envied package
|
|
49
|
+
// dev_dependencies:
|
|
50
|
+
// envied_generator: ^0.5.0
|
|
51
|
+
|
|
52
|
+
// lib/config/env.dart
|
|
53
|
+
import 'package:envied/envied.dart';
|
|
54
|
+
part 'env.g.dart';
|
|
55
|
+
|
|
56
|
+
@Envied(path: '.env')
|
|
57
|
+
abstract class Env {
|
|
58
|
+
@EnviedField(varName: 'API_KEY', obfuscate: true)
|
|
59
|
+
static final String apiKey = _Env.apiKey;
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Never commit:**
|
|
64
|
+
|
|
65
|
+
```gitignore
|
|
66
|
+
# .gitignore
|
|
67
|
+
.env
|
|
68
|
+
.env.*
|
|
69
|
+
*.keystore
|
|
70
|
+
*.jks
|
|
71
|
+
google-services.json # Contains API keys
|
|
72
|
+
GoogleService-Info.plist
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Tools:** GitLeaks, git-secrets, flutter_secure_storage, envied
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Boolean Names Is/Has/Should
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: makes conditions instantly readable
|
|
5
|
+
tags: naming, booleans, readability, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Boolean Names Is/Has/Should
|
|
9
|
+
|
|
10
|
+
Boolean variable prefixes make conditions instantly readable.
|
|
11
|
+
|
|
12
|
+
**Incorrect (unclear boolean variable names):**
|
|
13
|
+
|
|
14
|
+
```dart
|
|
15
|
+
final active = user.status == 'active';
|
|
16
|
+
final admin = checkAdminRole(user);
|
|
17
|
+
final items = cart.isNotEmpty;
|
|
18
|
+
final update = needsRefresh();
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Correct (boolean prefixes for variables):**
|
|
22
|
+
|
|
23
|
+
```dart
|
|
24
|
+
final isActive = user.status == 'active';
|
|
25
|
+
final isAdmin = checkAdminRole(user);
|
|
26
|
+
final hasItems = cart.isNotEmpty;
|
|
27
|
+
final shouldUpdate = needsRefresh();
|
|
28
|
+
final canEdit = hasPermission(user, 'edit');
|
|
29
|
+
final willExpire = expirationDate.isBefore(tomorrow);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Boolean prefixes:**
|
|
33
|
+
|
|
34
|
+
| Prefix | Use For |
|
|
35
|
+
|--------|---------|
|
|
36
|
+
| `is` | State (isActive, isEnabled, isLoading) |
|
|
37
|
+
| `has` | Ownership (hasPermission, hasError, hasItems) |
|
|
38
|
+
| `should` | Decision (shouldUpdate, shouldRetry) |
|
|
39
|
+
| `can` | Capability (canEdit, canDelete, canSubmit) |
|
|
40
|
+
| `will` | Future (willExpire, willRetry) |
|
|
41
|
+
|
|
42
|
+
**In Flutter widgets:**
|
|
43
|
+
|
|
44
|
+
```dart
|
|
45
|
+
class OrderCard extends StatelessWidget {
|
|
46
|
+
const OrderCard({
|
|
47
|
+
super.key,
|
|
48
|
+
required this.order,
|
|
49
|
+
required this.isSelected,
|
|
50
|
+
required this.canCancel,
|
|
51
|
+
required this.onTap,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
final Order order;
|
|
55
|
+
final bool isSelected;
|
|
56
|
+
final bool canCancel;
|
|
57
|
+
final VoidCallback onTap;
|
|
58
|
+
|
|
59
|
+
@override
|
|
60
|
+
Widget build(BuildContext context) {
|
|
61
|
+
return Card(
|
|
62
|
+
color: isSelected ? Colors.blue[100] : Colors.white,
|
|
63
|
+
child: ListTile(
|
|
64
|
+
title: Text(order.id),
|
|
65
|
+
trailing: canCancel ? CancelButton(order: order) : null,
|
|
66
|
+
onTap: onTap,
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Tools:** Code Review, dart_analyzer
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Separate Parsing From Widgets And Controllers
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: keeps widgets/controllers thin and focused
|
|
5
|
+
tags: widget, controller, parsing, transformation, patterns, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Separate Parsing From Widgets And Controllers
|
|
9
|
+
|
|
10
|
+
Widgets and controllers should be thin — only handling UI/flow concerns. Data transformation logic should be extracted into mappers or presenters.
|
|
11
|
+
|
|
12
|
+
**Incorrect (transformation in widget/controller):**
|
|
13
|
+
|
|
14
|
+
```dart
|
|
15
|
+
class UserProfilePage extends StatelessWidget {
|
|
16
|
+
const UserProfilePage({super.key, required this.user});
|
|
17
|
+
final User user;
|
|
18
|
+
|
|
19
|
+
@override
|
|
20
|
+
Widget build(BuildContext context) {
|
|
21
|
+
// Transformation logic inside widget
|
|
22
|
+
final fullName = '${user.firstName} ${user.lastName}';
|
|
23
|
+
final email = user.email.toLowerCase();
|
|
24
|
+
final createdAt = DateFormat('yyyy-MM-dd').format(user.createdAt);
|
|
25
|
+
final age = DateTime.now().difference(user.birthDate).inDays ~/ 365;
|
|
26
|
+
final avatarUrl = 'https://cdn.example.com/avatars/${user.id}.jpg';
|
|
27
|
+
|
|
28
|
+
return Column(
|
|
29
|
+
children: [
|
|
30
|
+
Text(fullName),
|
|
31
|
+
Text(email),
|
|
32
|
+
Text('Joined: $createdAt'),
|
|
33
|
+
Text('Age: $age'),
|
|
34
|
+
Image.network(avatarUrl),
|
|
35
|
+
],
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Correct (separate mapper/presenter):**
|
|
42
|
+
|
|
43
|
+
```dart
|
|
44
|
+
// Presenter / ViewModel
|
|
45
|
+
class UserProfilePresenter {
|
|
46
|
+
UserProfilePresenter(this._user);
|
|
47
|
+
final User _user;
|
|
48
|
+
|
|
49
|
+
String get fullName => '${_user.firstName} ${_user.lastName}';
|
|
50
|
+
String get email => _user.email.toLowerCase();
|
|
51
|
+
String get joinedDate => DateFormat('yyyy-MM-dd').format(_user.createdAt);
|
|
52
|
+
int get age => DateTime.now().difference(_user.birthDate).inDays ~/ 365;
|
|
53
|
+
String get avatarUrl => 'https://cdn.example.com/avatars/${_user.id}.jpg';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Thin widget
|
|
57
|
+
class UserProfilePage extends StatelessWidget {
|
|
58
|
+
const UserProfilePage({super.key, required this.user});
|
|
59
|
+
final User user;
|
|
60
|
+
|
|
61
|
+
@override
|
|
62
|
+
Widget build(BuildContext context) {
|
|
63
|
+
final presenter = UserProfilePresenter(user);
|
|
64
|
+
|
|
65
|
+
return Column(
|
|
66
|
+
children: [
|
|
67
|
+
Text(presenter.fullName),
|
|
68
|
+
Text(presenter.email),
|
|
69
|
+
Text('Joined: ${presenter.joinedDate}'),
|
|
70
|
+
Text('Age: ${presenter.age}'),
|
|
71
|
+
Image.network(presenter.avatarUrl),
|
|
72
|
+
],
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Benefits:**
|
|
79
|
+
- Reusable transformation logic
|
|
80
|
+
- Testable presenters/mappers
|
|
81
|
+
- Clean, readable widgets
|
|
82
|
+
- Consistent display format
|
|
83
|
+
|
|
84
|
+
**Tools:** Code Review, Architecture Review
|