@sun-asterisk/sunlint 1.3.47 → 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.
Files changed (185) hide show
  1. package/config/rules/rules-registry-generated.json +1717 -282
  2. package/core/architecture-integration.js +57 -15
  3. package/core/cli-action-handler.js +51 -36
  4. package/core/config-manager.js +6 -0
  5. package/core/config-merger.js +33 -0
  6. package/core/config-validator.js +37 -2
  7. package/core/file-targeting-service.js +148 -15
  8. package/core/init-command.js +118 -70
  9. package/core/output-service.js +12 -3
  10. package/core/project-detector.js +517 -0
  11. package/core/scoring-service.js +12 -6
  12. package/core/summary-report-service.js +9 -4
  13. package/core/tui-select.js +245 -0
  14. package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
  15. package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
  16. package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
  17. package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
  18. package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
  19. package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
  20. package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
  21. package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
  22. package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
  23. package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
  24. package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
  25. package/engines/impact/cli.js +54 -39
  26. package/engines/impact/config/default-config.js +105 -5
  27. package/engines/impact/core/impact-analyzer.js +12 -15
  28. package/engines/impact/core/utils/gitignore-parser.js +123 -0
  29. package/engines/impact/core/utils/method-call-graph.js +272 -87
  30. package/origin-rules/dart-en.md +1 -1
  31. package/origin-rules/go-en.md +231 -0
  32. package/origin-rules/php-en.md +107 -0
  33. package/origin-rules/python-en.md +113 -0
  34. package/origin-rules/ruby-en.md +607 -0
  35. package/package.json +1 -1
  36. package/scripts/copy-arch-detect.js +5 -1
  37. package/scripts/copy-impact-analyzer.js +5 -1
  38. package/scripts/generate-rules-registry.js +30 -14
  39. package/skill-assets/sunlint-code-quality/SKILL.md +3 -2
  40. package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
  41. package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
  42. package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
  43. package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
  44. package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
  45. package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
  46. package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
  47. package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
  48. package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
  49. package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
  50. package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
  51. package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
  52. package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
  53. package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
  54. package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
  55. package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
  56. package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
  57. package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
  58. package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
  59. package/skill-assets/sunlint-code-quality/rules/go/G001-explicit-error-handling.md +53 -0
  60. package/skill-assets/sunlint-code-quality/rules/go/G002-context-first-argument.md +44 -0
  61. package/skill-assets/sunlint-code-quality/rules/go/G003-receiver-consistency.md +38 -0
  62. package/skill-assets/sunlint-code-quality/rules/go/G004-avoid-panic.md +49 -0
  63. package/skill-assets/sunlint-code-quality/rules/go/G005-goroutine-leak-prevention.md +49 -0
  64. package/skill-assets/sunlint-code-quality/rules/go/G006-interface-consumer-definition.md +45 -0
  65. package/skill-assets/sunlint-code-quality/rules/go/GN001-gin-binding-validation.md +57 -0
  66. package/skill-assets/sunlint-code-quality/rules/go/GN002-gin-error-response.md +48 -0
  67. package/skill-assets/sunlint-code-quality/rules/go/GN003-graceful-shutdown.md +57 -0
  68. package/skill-assets/sunlint-code-quality/rules/go/GN004-gin-route-logical-grouping.md +54 -0
  69. package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
  70. package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
  71. package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
  72. package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
  73. package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
  74. package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
  75. package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
  76. package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
  77. package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
  78. package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
  79. package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
  80. package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
  81. package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
  82. package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
  83. package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
  84. package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
  85. package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
  86. package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
  87. package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
  88. package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
  89. package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
  90. package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
  91. package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
  92. package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
  93. package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
  94. package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
  95. package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
  96. package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
  97. package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
  98. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
  99. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
  100. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
  101. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
  102. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
  103. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
  104. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
  105. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
  106. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
  107. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
  108. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
  109. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
  110. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
  111. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
  112. package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
  113. package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
  114. package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
  115. package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
  116. package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
  117. package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
  118. package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
  119. package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
  120. package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
  121. package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
  122. package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
  123. package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
  124. package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
  125. package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
  126. package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
  127. package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
  128. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
  129. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
  130. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
  131. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
  132. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
  133. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
  134. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
  135. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
  136. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
  137. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
  138. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
  139. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
  140. package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
  141. package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
  142. package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
  143. package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
  144. package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
  145. package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
  146. package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
  147. package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
  148. package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
  149. package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
  150. package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
  151. package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
  152. package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
  153. package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
  154. package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
  155. package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
  156. package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
  157. package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
  158. package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
  159. package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
  160. package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
  161. package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
  162. package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
  163. package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
  164. package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
  165. package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
  166. package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
  167. package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
  168. package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
  169. package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
  170. package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
  171. package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
  172. package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
  173. package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
  174. package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
  175. package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
  176. package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
  177. package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
  178. package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
  179. package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
  180. package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
  181. package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
  182. package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
  183. package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
  184. package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
  185. package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
@@ -0,0 +1,78 @@
1
+ ---
2
+ title: Use Service Layer — Controllers Must Not Contain Business Logic
3
+ impact: HIGH
4
+ impactDescription: fat controllers are untestable, unmaintainable, and couple HTTP concerns with domain logic
5
+ tags: service-layer, architecture, clean-code, controller, laravel
6
+ ---
7
+
8
+ ## Use Service Layer — Controllers Must Not Contain Business Logic
9
+
10
+ Controllers handle HTTP: parse request, call service, return response. Domain logic (calculations, business rules, orchestration) belongs in a dedicated Service class.
11
+
12
+ **Wrong:**
13
+
14
+ ```php
15
+ // OrderController doing everything — HTTP + business + DB
16
+ public function store(StoreOrderRequest $request)
17
+ {
18
+ $subtotal = 0;
19
+ foreach ($request->items as $item) {
20
+ $product = Product::find($item['product_id']);
21
+ if ($product->stock < $item['qty']) {
22
+ return response()->json(['error' => 'Insufficient stock'], 422);
23
+ }
24
+ $subtotal += $product->price * $item['qty'];
25
+ }
26
+
27
+ $discount = $request->user()->isPremium() ? $subtotal * 0.1 : 0;
28
+ $total = $subtotal - $discount + $this->calculateShipping($request->address);
29
+
30
+ $order = Order::create([...]);
31
+ foreach ($request->items as $item) { /* create OrderItem, deduct stock */ }
32
+
33
+ Mail::to($request->user())->send(new OrderConfirmation($order));
34
+ return OrderResource::make($order);
35
+ }
36
+ ```
37
+
38
+ **Correct:**
39
+
40
+ ```php
41
+ // Service contains all business logic
42
+ class OrderService
43
+ {
44
+ public function __construct(
45
+ private readonly InventoryService $inventory,
46
+ private readonly PricingService $pricing,
47
+ ) {}
48
+
49
+ public function placeOrder(User $user, array $items, Address $address): Order
50
+ {
51
+ $this->inventory->reserveItems($items); // throws on insufficient stock
52
+ $total = $this->pricing->calculate($items, $user, $address);
53
+
54
+ return DB::transaction(function () use ($user, $items, $total) {
55
+ $order = Order::create(['user_id' => $user->id, 'total' => $total]);
56
+ $this->inventory->commitReservation($order, $items);
57
+ SendOrderConfirmation::dispatch($order);
58
+ return $order;
59
+ });
60
+ }
61
+ }
62
+
63
+ // Controller is just HTTP glue
64
+ class OrderController
65
+ {
66
+ public function __construct(private readonly OrderService $orderService) {}
67
+
68
+ public function store(StoreOrderRequest $request): JsonResponse
69
+ {
70
+ $order = $this->orderService->placeOrder(
71
+ $request->user(),
72
+ $request->validated('items'),
73
+ Address::fromRequest($request),
74
+ );
75
+ return OrderResource::make($order)->response()->setStatusCode(201);
76
+ }
77
+ }
78
+ ```
@@ -0,0 +1,75 @@
1
+ ---
2
+ title: Use RefreshDatabase + Factories in Tests — No Manual Inserts
3
+ impact: MEDIUM
4
+ impactDescription: manual DB inserts create brittle tests tightly coupled to schema; factories stay in sync with model changes
5
+ tags: testing, factories, refresh-database, pest, phpunit, laravel
6
+ ---
7
+
8
+ ## Use RefreshDatabase + Factories in Tests
9
+
10
+ Hardcoded `DB::table()->insert()` or `User::create([...])` with raw data creates brittle tests. Use model factories with `RefreshDatabase` for isolated, maintainable tests.
11
+
12
+ **Wrong:**
13
+
14
+ ```php
15
+ class OrderTest extends TestCase
16
+ {
17
+ protected function setUp(): void
18
+ {
19
+ parent::setUp();
20
+ DB::table('users')->insert(['id' => 1, 'name' => 'Test', 'email' => 'test@test.com', 'password' => 'xxx']);
21
+ DB::table('products')->insert(['id' => 1, 'name' => 'Widget', 'price' => 100, 'stock' => 10]);
22
+ // Test fails when schema changes
23
+ }
24
+
25
+ public function test_can_place_order()
26
+ {
27
+ $response = $this->post('/orders', ['product_id' => 1, 'qty' => 2]);
28
+ $this->assertEquals(200, $response->getStatusCode());
29
+ }
30
+ }
31
+ ```
32
+
33
+ **Correct (PHPUnit):**
34
+
35
+ ```php
36
+ class OrderTest extends TestCase
37
+ {
38
+ use RefreshDatabase; // wraps each test in a transaction, rolled back after
39
+
40
+ public function test_authenticated_user_can_place_order(): void
41
+ {
42
+ $user = User::factory()->create();
43
+ $product = Product::factory()->create(['price' => 100, 'stock' => 10]);
44
+
45
+ $response = $this->actingAs($user)
46
+ ->postJson('/api/orders', ['product_id' => $product->id, 'qty' => 2])
47
+ ->assertCreated()
48
+ ->assertJsonPath('data.total', 200);
49
+
50
+ $this->assertDatabaseHas('orders', ['user_id' => $user->id, 'total' => 200]);
51
+ $this->assertDatabaseHas('products', ['id' => $product->id, 'stock' => 8]);
52
+ }
53
+
54
+ public function test_order_fails_when_insufficient_stock(): void
55
+ {
56
+ $user = User::factory()->create();
57
+ $product = Product::factory()->create(['stock' => 1]);
58
+
59
+ $this->actingAs($user)
60
+ ->postJson('/api/orders', ['product_id' => $product->id, 'qty' => 5])
61
+ ->assertUnprocessable()
62
+ ->assertJsonValidationErrors(['qty']);
63
+ }
64
+ }
65
+ ```
66
+
67
+ **Factory patterns:**
68
+ ```php
69
+ // Specific state
70
+ $admin = User::factory()->admin()->create();
71
+ $banned = User::factory()->banned()->unverified()->create();
72
+
73
+ // With relations
74
+ $post = Post::factory()->for($user)->hasComments(3)->create();
75
+ ```
@@ -0,0 +1,61 @@
1
+ ---
2
+ title: Register Services in AppServiceProvider — Never new() in Controllers
3
+ impact: MEDIUM
4
+ impactDescription: direct instantiation prevents mocking in tests and creates hidden dependencies
5
+ tags: service-container, dependency-injection, ioc, laravel, testing
6
+ ---
7
+
8
+ ## Register Services via Service Container — Never new() in Controllers
9
+
10
+ Laravel's IoC container resolves dependencies automatically via constructor injection. Using `new MyService()` inside a controller or service bypasses the container, making tests hard and coupling impossible to break.
11
+
12
+ **Wrong:**
13
+
14
+ ```php
15
+ class OrderController extends Controller
16
+ {
17
+ public function store(StoreOrderRequest $request)
18
+ {
19
+ // Hidden dependencies, impossible to mock
20
+ $service = new OrderService(new InventoryService(), new PricingService());
21
+ $mailer = new OrderMailer(new SmtpTransport());
22
+ }
23
+ }
24
+ ```
25
+
26
+ **Correct:**
27
+
28
+ ```php
29
+ // 1. Bind in AppServiceProvider if needed (often not needed — auto-resolved)
30
+ class AppServiceProvider extends ServiceProvider
31
+ {
32
+ public function register(): void
33
+ {
34
+ $this->app->singleton(PaymentGateway::class, function () {
35
+ return new StripeGateway(config('services.stripe.secret'));
36
+ });
37
+
38
+ // Interface → concrete binding
39
+ $this->app->bind(NotifierInterface::class, SlackNotifier::class);
40
+ }
41
+ }
42
+
43
+ // 2. Controller receives via constructor — auto-resolved by container
44
+ class OrderController extends Controller
45
+ {
46
+ public function __construct(
47
+ private readonly OrderService $orderService,
48
+ private readonly NotifierInterface $notifier,
49
+ ) {}
50
+
51
+ public function store(StoreOrderRequest $request): JsonResponse
52
+ {
53
+ $order = $this->orderService->placeOrder($request->user(), $request->validated());
54
+ $this->notifier->notify("New order #{$order->id}");
55
+ return OrderResource::make($order)->response()->setStatusCode(201);
56
+ }
57
+ }
58
+
59
+ // 3. In tests — swap the binding
60
+ $this->app->instance(NotifierInterface::class, $mockNotifier);
61
+ ```
@@ -0,0 +1,55 @@
1
+ ---
2
+ title: Do Not Use Mutable Default Argument
3
+ impact: HIGH
4
+ impactDescription: Mutable default arguments are shared across all calls, causing subtle state bugs that are very hard to trace.
5
+ tags: python, bugs, arguments, pitfalls, quality
6
+ ---
7
+
8
+ ## Do Not Use Mutable Default Argument
9
+
10
+ In Python, default argument values are evaluated **once** at function definition time, not at each call. Using mutable objects (list, dict, set) as defaults causes all callers to share the same object, leading to unexpected side effects across calls.
11
+
12
+ **Incorrect:**
13
+ ```python
14
+ def add_item(item, collection=[]):
15
+ collection.append(item)
16
+ return collection
17
+
18
+ print(add_item("a")) # ["a"]
19
+ print(add_item("b")) # ["a", "b"] ← bug: list persists between calls
20
+
21
+ def create_user(name, roles={"admin": False}):
22
+ roles["user"] = True
23
+ return {"name": name, "roles": roles}
24
+ ```
25
+
26
+ **Correct:**
27
+ ```python
28
+ def add_item(item, collection=None):
29
+ if collection is None:
30
+ collection = []
31
+ collection.append(item)
32
+ return collection
33
+
34
+ print(add_item("a")) # ["a"]
35
+ print(add_item("b")) # ["b"] ← each call gets a fresh list
36
+
37
+ def create_user(name, roles=None):
38
+ if roles is None:
39
+ roles = {"admin": False}
40
+ roles["user"] = True
41
+ return {"name": name, "roles": roles}
42
+ ```
43
+
44
+ **Dataclass alternative (Python 3.7+):**
45
+ ```python
46
+ from dataclasses import dataclass, field
47
+ from typing import List
48
+
49
+ @dataclass
50
+ class Task:
51
+ name: str
52
+ tags: List[str] = field(default_factory=list) # safe: fresh list per instance
53
+ ```
54
+
55
+ **Tools:** Ruff `B006` (mutable-argument-default), Pylint `W0102` (dangerous-default-value), flake8-bugbear, mypy
@@ -0,0 +1,45 @@
1
+ ---
2
+ title: Specify Encoding When Opening Files
3
+ impact: MEDIUM
4
+ impactDescription: Omitting encoding when opening text files causes silent failures on systems with non-UTF-8 locale, resulting in UnicodeDecodeError or garbled data in production.
5
+ tags: python, encoding, files, portability, quality
6
+ ---
7
+
8
+ ## Specify Encoding When Opening Files
9
+
10
+ When calling `open()` in text mode without an explicit `encoding` argument, Python uses the OS's default locale encoding. This is typically UTF-8 on Linux/macOS, but `cp1252` or similar on Windows. Code that works locally can silently fail or corrupt data in other environments.
11
+
12
+ Always pass `encoding="utf-8"` (or whatever the file format requires) explicitly.
13
+
14
+ **Incorrect:**
15
+ ```python
16
+ # Relies on OS default — breaks on Windows or non-UTF-8 systems
17
+ with open("data.txt") as f:
18
+ content = f.read()
19
+
20
+ with open("output.csv", "w") as f:
21
+ f.write("café,prix\n")
22
+ ```
23
+
24
+ **Correct:**
25
+ ```python
26
+ with open("data.txt", encoding="utf-8") as f:
27
+ content = f.read()
28
+
29
+ with open("output.csv", "w", encoding="utf-8") as f:
30
+ f.write("café,prix\n")
31
+
32
+ # When reading binary, no encoding needed
33
+ with open("image.png", "rb") as f:
34
+ data = f.read()
35
+ ```
36
+
37
+ **Pathlib equivalent (also requires explicit encoding):**
38
+ ```python
39
+ from pathlib import Path
40
+
41
+ content = Path("data.txt").read_text(encoding="utf-8")
42
+ Path("output.txt").write_text("hello", encoding="utf-8")
43
+ ```
44
+
45
+ **Tools:** Ruff `W1514` / `PLW1514` (unspecified-encoding), Pylint `W1514`, flake8-bugbear
@@ -0,0 +1,54 @@
1
+ ---
2
+ title: Use Context Manager for File and Resource Handling
3
+ impact: MEDIUM
4
+ impactDescription: Not using with statement for file/network/lock resources causes resource leaks when exceptions are raised, leading to open file handles, locked databases, and memory exhaustion.
5
+ tags: python, resources, context-manager, files, quality
6
+ ---
7
+
8
+ ## Use Context Manager for File and Resource Handling
9
+
10
+ Python's `with` statement (context manager protocol) guarantees that resources are released even when exceptions occur. Always use `with` when dealing with files, network connections, database cursors, threading locks, and any object that implements `__enter__`/`__exit__`.
11
+
12
+ **Incorrect:**
13
+ ```python
14
+ # File handle not closed if exception occurs
15
+ f = open("data.txt", encoding="utf-8")
16
+ content = f.read() # If this raises, f.close() is never called
17
+ f.close()
18
+
19
+ # Lock never released if exception raised inside
20
+ lock = threading.Lock()
21
+ lock.acquire()
22
+ do_work() # Exception here leaves lock permanently acquired
23
+ lock.release()
24
+ ```
25
+
26
+ **Correct:**
27
+ ```python
28
+ # File is always closed, even on exception
29
+ with open("data.txt", encoding="utf-8") as f:
30
+ content = f.read()
31
+
32
+ # Lock always released
33
+ import threading
34
+
35
+ lock = threading.Lock()
36
+ with lock:
37
+ do_work()
38
+
39
+ # Multiple context managers in one with
40
+ with open("input.txt", encoding="utf-8") as fin, \
41
+ open("output.txt", "w", encoding="utf-8") as fout:
42
+ fout.write(fin.read())
43
+ ```
44
+
45
+ **contextlib.suppress as context manager:**
46
+ ```python
47
+ import contextlib
48
+
49
+ # Instead of try/except/pass
50
+ with contextlib.suppress(FileNotFoundError):
51
+ os.remove("temp.txt")
52
+ ```
53
+
54
+ **Tools:** Ruff `SIM115` (open-file-with-context-handler), `R1732` (consider-using-with), Pylint, flake8-simplify
@@ -0,0 +1,65 @@
1
+ ---
2
+ title: Never Use Bare except Clause
3
+ impact: HIGH
4
+ impactDescription: A bare except catches SystemExit and KeyboardInterrupt, making programs impossible to interrupt and hiding unrelated errors that should propagate.
5
+ tags: python, exceptions, error-handling, pitfalls, quality
6
+ ---
7
+
8
+ ## Never Use Bare except Clause
9
+
10
+ A bare `except:` clause (with no exception type specified) catches **every** exception, including `SystemExit` (raised by `sys.exit()`), `KeyboardInterrupt` (Ctrl+C), and `GeneratorExit`. This prevents programs from being terminated normally, swallows programming errors, and makes debugging extremely difficult.
11
+
12
+ Always specify the exception type(s) you intend to handle.
13
+
14
+ **Incorrect:**
15
+ ```python
16
+ # Catches SystemExit and KeyboardInterrupt — program cannot be stopped
17
+ try:
18
+ process_data()
19
+ except:
20
+ print("Something went wrong")
21
+
22
+ # Also problematic: too broad
23
+ try:
24
+ connect_to_db()
25
+ except Exception:
26
+ pass # silently ignores all errors including programming mistakes
27
+ ```
28
+
29
+ **Correct:**
30
+ ```python
31
+ # Catch only what you expect and can handle
32
+ try:
33
+ process_data()
34
+ except ValueError as e:
35
+ logger.warning("Invalid data: %s", e)
36
+ except OSError as e:
37
+ logger.error("I/O error during processing: %s", e)
38
+
39
+ # If you need a catch-all, at least log it and re-raise
40
+ try:
41
+ connect_to_db()
42
+ except Exception as e:
43
+ logger.error("Unexpected error connecting to DB: %s", e, exc_info=True)
44
+ raise
45
+
46
+ # Correct way to suppress a specific expected error
47
+ import contextlib
48
+
49
+ with contextlib.suppress(FileNotFoundError):
50
+ os.remove("temp.txt")
51
+ ```
52
+
53
+ **Exception hierarchy to catch:**
54
+ ```python
55
+ # Prefer specific → broad order
56
+ try:
57
+ result = int(user_input)
58
+ except ValueError:
59
+ result = 0 # handle invalid literal
60
+ except (TypeError, OverflowError) as e:
61
+ logger.warning("Type issue: %s", e)
62
+ result = 0
63
+ ```
64
+
65
+ **Tools:** Ruff `E722` (bare-except), `W0702` in Pylint, `BLE001` (blind-except), flake8, pyflakes
@@ -0,0 +1,60 @@
1
+ ---
2
+ title: Use isinstance() Instead of type() for Type Checking
3
+ impact: MEDIUM
4
+ impactDescription: Using type() == for type checks breaks polymorphism and inheritance, rejecting valid subclass instances that should be accepted by the contract.
5
+ tags: python, typing, isinstance, quality, oop
6
+ ---
7
+
8
+ ## Use isinstance() Instead of type() for Type Checking
9
+
10
+ The `type(x) == SomeClass` pattern performs an exact type match and does not account for subclasses. This violates the Liskov Substitution Principle: code that accepts a `list` should also accept `UserList` or any other list subclass.
11
+
12
+ Use `isinstance()` which checks the full MRO (method resolution order) and correctly handles subclasses and abstract base classes.
13
+
14
+ **Incorrect:**
15
+ ```python
16
+ def process(data):
17
+ if type(data) == list: # rejects OrderedList, UserList, etc.
18
+ for item in data:
19
+ handle(item)
20
+ if type(data) == dict: # rejects defaultdict, OrderedDict, etc.
21
+ process_mapping(data)
22
+
23
+ def serialize(value):
24
+ if type(value) == str: # rejects str subclasses
25
+ return value.encode("utf-8")
26
+ ```
27
+
28
+ **Correct:**
29
+ ```python
30
+ def process(data):
31
+ if isinstance(data, list): # accepts all list subclasses
32
+ for item in data:
33
+ handle(item)
34
+ if isinstance(data, dict): # accepts defaultdict, OrderedDict, etc.
35
+ process_mapping(data)
36
+
37
+ def serialize(value):
38
+ if isinstance(value, str):
39
+ return value.encode("utf-8")
40
+
41
+ # Use abstract base classes for duck typing
42
+ from collections.abc import Mapping, Sequence
43
+
44
+ def process_generic(data):
45
+ if isinstance(data, Sequence): # list, tuple, str, UserList, etc.
46
+ for item in data:
47
+ handle(item)
48
+ if isinstance(data, Mapping): # dict, defaultdict, ChainMap, etc.
49
+ process_mapping(data)
50
+ ```
51
+
52
+ **Exception — when exact type match IS needed:**
53
+ ```python
54
+ # Only use type() == when you explicitly want to exclude subclasses
55
+ # e.g., in a serialization library distinguishing bool from int:
56
+ if type(value) is bool: # True/False, not int subclasses
57
+ return str(value).lower()
58
+ ```
59
+
60
+ **Tools:** Ruff `E721` (type-comparison), Pylint `C0123` (unidiomatic-typecheck), mypy, pyright
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: Always Use Timezone-Aware Datetimes
3
+ impact: HIGH
4
+ impactDescription: Naive datetimes (without tzinfo) cause silent bugs when comparing times across timezones, leading to incorrect scheduling, expiry calculations, and audit logs.
5
+ tags: python, datetime, timezone, bugs, quality
6
+ ---
7
+
8
+ ## Always Use Timezone-Aware Datetimes
9
+
10
+ Python's `datetime` objects can be *naive* (no timezone) or *aware* (with timezone). Mixing naive and aware datetimes raises a `TypeError`, but naive datetimes used consistently silently produce wrong results when code runs in different timezones (CI server vs production, container vs host).
11
+
12
+ Always create timezone-aware datetimes by passing `tz` or `tzinfo`.
13
+
14
+ **Incorrect:**
15
+ ```python
16
+ from datetime import datetime
17
+
18
+ # Naive datetimes — "now" means different things in Tokyo vs London
19
+ created_at = datetime.now()
20
+ expires_at = datetime.utcnow() # UTC but naive — still dangerous
21
+
22
+ # Comparing naive datetimes assumes both are in same timezone (often wrong)
23
+ if datetime.now() > token_expiry: # fails if token_expiry is aware
24
+ raise TokenExpiredError()
25
+ ```
26
+
27
+ **Correct:**
28
+ ```python
29
+ from datetime import datetime, timezone
30
+
31
+ # Python 3.11+: use datetime.UTC constant
32
+ now = datetime.now(tz=timezone.utc)
33
+
34
+ # Python 3.9+: use zoneinfo for local zones
35
+ from zoneinfo import ZoneInfo
36
+
37
+ jst_now = datetime.now(tz=ZoneInfo("Asia/Tokyo"))
38
+ utc_now = datetime.now(tz=timezone.utc)
39
+
40
+ # django/pytz style (pre-3.9)
41
+ import pytz
42
+
43
+ utc_now = datetime.now(tz=pytz.utc)
44
+
45
+ # Always compare aware with aware
46
+ token_expiry: datetime # must be aware
47
+ if datetime.now(tz=timezone.utc) > token_expiry:
48
+ raise TokenExpiredError()
49
+ ```
50
+
51
+ **Converting naive to aware (migration):**
52
+ ```python
53
+ # Assume a legacy naive datetime is UTC — localize it explicitly
54
+ naive_dt = datetime(2024, 1, 15, 10, 30)
55
+ aware_dt = naive_dt.replace(tzinfo=timezone.utc)
56
+ ```
57
+
58
+ **Tools:** Ruff `DTZ001`–`DTZ012` (flake8-datetimez), Pylint `W1502`, mypy with `--strict-optional`
@@ -0,0 +1,62 @@
1
+ ---
2
+ title: Use pathlib Instead of os.path for File Operations
3
+ impact: MEDIUM
4
+ impactDescription: os.path functions return plain strings that are easy to misuse; pathlib.Path provides an object-oriented, composable, and cross-platform API that eliminates common path manipulation bugs.
5
+ tags: python, pathlib, os-path, files, quality, python3
6
+ ---
7
+
8
+ ## Use pathlib Instead of os.path for File Operations
9
+
10
+ Python 3.4 introduced `pathlib.Path` as a modern, object-oriented replacement for `os.path`. Paths are represented as objects supporting `/` operator for joining, `.stem`, `.suffix`, `.parent` for decomposition, and `.read_text()` / `.write_text()` for content — with no risk of forgetting `os.path.join` and accidentally concatenating strings.
11
+
12
+ **Incorrect:**
13
+ ```python
14
+ import os
15
+
16
+ # String concatenation — silent bugs on Windows with backslashes
17
+ config_path = base_dir + "/config/" + "settings.json"
18
+
19
+ # Verbose os.path usage
20
+ import os.path
21
+
22
+ full_path = os.path.join(base_dir, "config", "settings.json")
23
+ file_name = os.path.basename(full_path)
24
+ dir_name = os.path.dirname(full_path)
25
+ stem = os.path.splitext(file_name)[0]
26
+ ext = os.path.splitext(file_name)[1]
27
+
28
+ if os.path.exists(config_path):
29
+ with open(config_path, encoding="utf-8") as f:
30
+ content = f.read()
31
+
32
+ os.makedirs(os.path.join(base_dir, "logs"), exist_ok=True)
33
+ ```
34
+
35
+ **Correct:**
36
+ ```python
37
+ from pathlib import Path
38
+
39
+ base_dir = Path("/app")
40
+
41
+ # / operator for joining — cross-platform and clear
42
+ config_path = base_dir / "config" / "settings.json"
43
+
44
+ # Readable decomposition
45
+ file_name = config_path.name # "settings.json"
46
+ dir_name = config_path.parent # Path("/app/config")
47
+ stem = config_path.stem # "settings"
48
+ ext = config_path.suffix # ".json"
49
+
50
+ # Built-in existence check and file reading
51
+ if config_path.exists():
52
+ content = config_path.read_text(encoding="utf-8")
53
+
54
+ # Directory creation
55
+ (base_dir / "logs").mkdir(parents=True, exist_ok=True)
56
+
57
+ # Glob for files
58
+ for py_file in base_dir.rglob("*.py"):
59
+ print(py_file)
60
+ ```
61
+
62
+ **Tools:** Ruff `PTH100`–`PTH208` (flake8-use-pathlib), pyupgrade, SonarQube Python rules
@@ -0,0 +1,52 @@
1
+ ---
2
+ title: Never Use Wildcard Imports
3
+ impact: HIGH
4
+ impactDescription: Wildcard imports pollute the namespace, make it impossible to trace where a name comes from, and can silently override existing names, causing hard-to-debug subtle bugs.
5
+ tags: python, imports, namespace, quality, readability
6
+ ---
7
+
8
+ ## Never Use Wildcard Imports
9
+
10
+ `from module import *` imports every public name from a module into the current namespace. This:
11
+ - Makes it impossible to know where any name is defined without reading the imported module
12
+ - Can silently override names from prior imports or `builtins`
13
+ - Prevents static analysis tools from detecting undefined names
14
+ - Breaks auto-complete and go-to-definition in IDEs
15
+
16
+ The only acceptable use of `import *` is in a package's `__init__.py` to re-export a curated public API defined in `__all__`.
17
+
18
+ **Incorrect:**
19
+ ```python
20
+ from os.path import * # which names are now in scope?
21
+ from numpy import * # overrides Python's built-in sum, any, all, etc.
22
+ from models import * # User? Order? both? neither?
23
+ from utils import *
24
+
25
+ # Now these silently shadow built-ins:
26
+ result = sum([1, 2, 3]) # which sum? Python's or numpy's?
27
+ ```
28
+
29
+ **Correct:**
30
+ ```python
31
+ import os
32
+ from os import path as osp # or use pathlib
33
+ from pathlib import Path
34
+ import numpy as np # conventional alias
35
+ from models import User, Order, Product # explicit names
36
+ from utils import format_date, validate_email
37
+
38
+ # Clear provenance for every name
39
+ result = np.sum([1, 2, 3])
40
+ full_path = Path("/data") / "file.txt"
41
+ ```
42
+
43
+ **Acceptable use in `__init__.py`:**
44
+ ```python
45
+ # package/__init__.py — re-export public API
46
+ from .client import Client
47
+ from .exceptions import APIError, RateLimitError
48
+
49
+ __all__ = ["Client", "APIError", "RateLimitError"]
50
+ ```
51
+
52
+ **Tools:** Ruff `F403` (undefined-local-with-import-star), `W0401` in Pylint (wildcard-import), flake8, isort