@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,99 @@
1
+ ---
2
+ title: "RR003 – Extract Business Logic into Service Objects"
3
+ impact: high
4
+ impactDescription: "Fat models and fat controllers are untestable, hard to reuse, and prone to circular dependencies. Service objects isolate business logic into a single callable unit."
5
+ tags: [ruby, rails, architecture, service-layer]
6
+ ---
7
+
8
+ # RR003 – Extract Business Logic into Service Objects
9
+
10
+ ## Rule
11
+
12
+ Business logic that involves more than a single model update, an external API call, or conditional branching belongs in a dedicated service object under `app/services/`, not in models or controllers.
13
+
14
+ ## Why
15
+
16
+ - Fat models accumulate unrelated behaviour, making them hard to test in isolation.
17
+ - Fat controllers mix HTTP concerns (params, redirects) with domain rules.
18
+ - Service objects are plain Ruby objects (POROs) — no Rails magic needed to test them.
19
+
20
+ ## Wrong
21
+
22
+ ```ruby
23
+ # Fat controller: business logic mixed with HTTP handling
24
+ class OrdersController < ApplicationController
25
+ def create
26
+ @order = Order.new(order_params)
27
+ @order.total = @order.items.sum(&:price) * (1 - current_user.discount)
28
+ if @order.save
29
+ Stripe::Charge.create(amount: @order.total_cents, ...)
30
+ OrderMailer.confirmation(@order).deliver_later
31
+ redirect_to @order
32
+ else
33
+ render :new
34
+ end
35
+ end
36
+ end
37
+
38
+ # Fat model: calling external APIs from ActiveRecord callbacks
39
+ class Order < ApplicationRecord
40
+ after_create :charge_card
41
+ def charge_card
42
+ Stripe::Charge.create(...)
43
+ end
44
+ end
45
+ ```
46
+
47
+ ## Correct
48
+
49
+ ```ruby
50
+ # app/services/place_order_service.rb
51
+ class PlaceOrderService
52
+ Result = Struct.new(:order, :success?, :error, keyword_init: true)
53
+
54
+ def initialize(user:, params:, payment_gateway: Stripe)
55
+ @user = user
56
+ @params = params
57
+ @payment_gateway = payment_gateway
58
+ end
59
+
60
+ def call
61
+ order = build_order
62
+ return Result.new(order: order, success?: false, error: order.errors) unless order.save
63
+
64
+ charge_payment(order)
65
+ OrderMailer.confirmation(order).deliver_later
66
+ Result.new(order: order, success?: true)
67
+ rescue PaymentError => e
68
+ order.destroy
69
+ Result.new(success?: false, error: e.message)
70
+ end
71
+
72
+ private
73
+
74
+ def build_order
75
+ Order.new(**@params, total: calculate_total, user: @user)
76
+ end
77
+ # ...
78
+ end
79
+
80
+ # Controller stays thin
81
+ class OrdersController < ApplicationController
82
+ def create
83
+ result = PlaceOrderService.new(user: current_user, params: order_params).call
84
+ if result.success?
85
+ redirect_to result.order
86
+ else
87
+ @order = result.order
88
+ render :new
89
+ end
90
+ end
91
+ end
92
+ ```
93
+
94
+ ## Notes
95
+
96
+ - Service objects should be callable via `#call` (standard Ruby convention, works with `Proc#call` duck-typing).
97
+ - Return a `Result` struct or use `dry-monads` for explicit success/failure signalling.
98
+ - Place in `app/services/` — Rails autoloads from this directory by default.
99
+ - Keep one public method (`call`) — additional extraction goes into private methods.
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: "RR004 – Use ActiveJob for Background Work"
3
+ impact: high
4
+ impactDescription: "Spawning raw threads or doing heavy work inline blocks the Puma thread, causing request timeouts and memory leaks."
5
+ tags: [ruby, rails, performance, background-jobs]
6
+ ---
7
+
8
+ # RR004 – Use ActiveJob for Background Work
9
+
10
+ ## Rule
11
+
12
+ Any operation that is slow (email, external API, report generation, file processing) or must retry on failure must be enqueued via `ActiveJob`, not executed inline in the request or in a raw `Thread.new`.
13
+
14
+ ## Why
15
+
16
+ - `Thread.new` in a Rails controller shares the request's database connection until it's reclaimed, causing connection pool exhaustion.
17
+ - Inline work blocks the Puma thread until completion — the user waits.
18
+ - ActiveJob provides retry semantics, dead-letter queues, and observable failure via your queue backend (Sidekiq, GoodJob, Solid Queue).
19
+
20
+ ## Wrong
21
+
22
+ ```ruby
23
+ # Inline — user waits for CSV export to complete
24
+ def export
25
+ @orders = Order.all
26
+ csv = generate_csv(@orders) # could take 10+ seconds
27
+ send_data csv, filename: 'orders.csv'
28
+ end
29
+
30
+ # Raw thread — races on connection pool, no retry, no observability
31
+ def send_welcome_email
32
+ Thread.new { UserMailer.welcome(@user).deliver_now }
33
+ redirect_to root_path
34
+ end
35
+ ```
36
+
37
+ ## Correct
38
+
39
+ ```ruby
40
+ # app/jobs/generate_order_export_job.rb
41
+ class GenerateOrderExportJob < ApplicationJob
42
+ queue_as :exports
43
+ retry_on Net::TimeoutError, wait: 5.seconds, attempts: 3
44
+ discard_on ActiveRecord::RecordNotFound
45
+
46
+ def perform(user_id, filters)
47
+ user = User.find(user_id)
48
+ csv = OrderExportService.new(filters).generate
49
+ UserMailer.export_ready(user, csv).deliver_now
50
+ end
51
+ end
52
+
53
+ # Controller
54
+ def export
55
+ GenerateOrderExportJob.perform_later(current_user.id, export_params.to_h)
56
+ redirect_to orders_path, notice: 'Export will be emailed to you.'
57
+ end
58
+
59
+ # Mailer — always defer with deliver_later
60
+ UserMailer.welcome(@user).deliver_later
61
+ ```
62
+
63
+ ## Notes
64
+
65
+ - Use `perform_later` (async) in production; `perform_now` only in tests / sync scenarios.
66
+ - Configure the queue backend in `config/application.rb`: `config.active_job.queue_adapter = :sidekiq`.
67
+ - Serialize only primitive-safe arguments (IDs, not AR objects) — AR objects go stale between enqueue and perform.
@@ -0,0 +1,53 @@
1
+ ---
2
+ title: "RR005 – Paginate All Collection Endpoints"
3
+ impact: medium
4
+ impactDescription: "Loading unbounded collections into memory causes OOM crashes and unacceptably slow responses at scale."
5
+ tags: [ruby, rails, performance, pagination]
6
+ ---
7
+
8
+ # RR005 – Paginate All Collection Endpoints
9
+
10
+ ## Rule
11
+
12
+ Every controller action that returns a list must apply pagination. Never return an unbounded collection with `Model.all` or `.where(...)` without a `.page` or `.limit` call.
13
+
14
+ ## Why
15
+
16
+ A table that currently has 1,000 rows will have millions. An unguarded `Order.all` will load all records into memory on each request. Pagination (Kaminari or Pagy) is the standard Rails solution.
17
+
18
+ ## Wrong
19
+
20
+ ```ruby
21
+ def index
22
+ @orders = Order.all # ❌ unbounded
23
+ @users = User.where(active: true) # ❌ unbounded
24
+ render json: @orders
25
+ end
26
+ ```
27
+
28
+ ## Correct
29
+
30
+ ```ruby
31
+ # Gemfile: gem 'pagy' —OR— gem 'kaminari'
32
+
33
+ # With Pagy (preferred — fastest)
34
+ include Pagy::Backend
35
+
36
+ def index
37
+ @pagy, @orders = pagy(Order.order(created_at: :desc), items: 25)
38
+ render json: { orders: @orders, meta: pagy_metadata(@pagy) }
39
+ end
40
+
41
+ # With Kaminari
42
+ def index
43
+ @orders = Order.order(created_at: :desc).page(params[:page]).per(25)
44
+ render json: @orders
45
+ end
46
+ ```
47
+
48
+ ## Notes
49
+
50
+ - Default page size should be configurable but capped (e.g., max 100 per page).
51
+ - Pass `params[:per_page]` through an explicit allowlist — never directly into `.per()`.
52
+ - Pagy is significantly faster than Kaminari for large tables; prefer it in new projects.
53
+ - For cursor-based APIs (infinite scroll / mobile), use `pagy_cursor` or write manual `WHERE id > last_id LIMIT n` queries.
@@ -0,0 +1,53 @@
1
+ ---
2
+ title: "RR006 – Use find_each / in_batches for Bulk Iteration"
3
+ impact: high
4
+ impactDescription: "find_each limits memory to batch size (1000 rows) instead of loading all records at once, preventing OOM in background jobs and rake tasks."
5
+ tags: [ruby, rails, performance, database]
6
+ ---
7
+
8
+ # RR006 – Use find_each / in_batches for Bulk Iteration
9
+
10
+ ## Rule
11
+
12
+ When iterating over all records (in jobs, scripts, migrations, Rake tasks), always use `find_each` or `in_batches` instead of `all.each` or `where(...).each`.
13
+
14
+ ## Why
15
+
16
+ `Model.all.each` fetches all matching rows in one SQL query and holds them all in memory. For large tables this causes OOM. `find_each` internally runs batched queries (`LIMIT 1000 ORDER BY id`) and yields one record at a time.
17
+
18
+ ## Wrong
19
+
20
+ ```ruby
21
+ # Loads every user into memory at once
22
+ User.all.each do |user|
23
+ UserMailer.renewal(user).deliver_later
24
+ end
25
+
26
+ # Same problem with a where scope
27
+ Order.where(status: 'pending').each { |o| processOrder(o) }
28
+ ```
29
+
30
+ ## Correct
31
+
32
+ ```ruby
33
+ # find_each: yields one record at a time, batched under the hood
34
+ User.find_each(batch_size: 500) do |user|
35
+ UserMailer.renewal(user).deliver_later
36
+ end
37
+
38
+ # in_batches: yields an ActiveRecord::Relation per batch (useful for bulk updates)
39
+ Order.where(status: 'pending').in_batches(of: 200) do |batch|
40
+ batch.update_all(processed: true)
41
+ end
42
+
43
+ # start: / finish: let you resume from a checkpoint
44
+ User.find_each(start: last_processed_id) do |user|
45
+ # ...
46
+ end
47
+ ```
48
+
49
+ ## Notes
50
+
51
+ - `find_each` requires a primary key — does not work with `select` that omits the PK.
52
+ - Avoid adding an `ORDER BY` other than PK with `find_each`; use `in_batches` if you need custom ordering.
53
+ - For read-only iteration on PostgreSQL, consider `cursor()` from the `activerecord-cursor-pagination` gem for server-side cursors.
@@ -0,0 +1,76 @@
1
+ ---
2
+ title: "RR007 – Return Correct HTTP Status Codes"
3
+ impact: medium
4
+ impactDescription: "Returning 200 OK for errors silently breaks API clients, prevents correct retry logic, and hides failures in monitoring."
5
+ tags: [ruby, rails, api, http]
6
+ ---
7
+
8
+ # RR007 – Return Correct HTTP Status Codes
9
+
10
+ ## Rule
11
+
12
+ Always pass an explicit `status:` argument to `render json:`. Never return HTTP 200 for validation errors, authorization failures, or not-found resources.
13
+
14
+ ## Wrong
15
+
16
+ ```ruby
17
+ def create
18
+ @user = User.new(user_params)
19
+ if @user.save
20
+ render json: @user # ❌ no status — defaults to 200
21
+ else
22
+ render json: { errors: @user.errors } # ❌ 200 on failure
23
+ end
24
+ end
25
+
26
+ def show
27
+ @user = User.find_by(id: params[:id])
28
+ render json: @user # ❌ returns 200 with null body if not found
29
+ end
30
+ ```
31
+
32
+ ## Correct
33
+
34
+ ```ruby
35
+ def create
36
+ @user = User.new(user_params)
37
+ if @user.save
38
+ render json: UserResource.new(@user), status: :created # 201
39
+ else
40
+ render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity # 422
41
+ end
42
+ end
43
+
44
+ def show
45
+ @user = User.find(params[:id]) # raises ActiveRecord::RecordNotFound → rescued to 404
46
+ render json: UserResource.new(@user), status: :ok
47
+ end
48
+
49
+ # In ApplicationController
50
+ rescue_from ActiveRecord::RecordNotFound, with: :not_found
51
+ rescue_from Pundit::NotAuthorizedError, with: :forbidden
52
+
53
+ def not_found
54
+ render json: { error: 'Not found' }, status: :not_found # 404
55
+ end
56
+ def forbidden
57
+ render json: { error: 'Forbidden' }, status: :forbidden # 403
58
+ end
59
+ ```
60
+
61
+ ## Status code reference
62
+
63
+ | Scenario | Status |
64
+ |---|---|
65
+ | Create success | `201 :created` |
66
+ | Update/delete success | `200 :ok` |
67
+ | No content | `204 :no_content` |
68
+ | Validation failed | `422 :unprocessable_entity` |
69
+ | Not found | `404 :not_found` |
70
+ | Unauthorized (not logged in) | `401 :unauthorized` |
71
+ | Forbidden (not allowed) | `403 :forbidden` |
72
+
73
+ ## Notes
74
+
75
+ - Use symbol form (`:created`) — it's self-documenting and immune to numeric typos.
76
+ - Centralize rescue_from in `ApplicationController` or a concern rather than per-action `rescue`.
@@ -0,0 +1,77 @@
1
+ ---
2
+ title: "RR008 – Enforce Authentication with before_action"
3
+ impact: high
4
+ impactDescription: "Missing before_action :authenticate_user! leaves actions publicly accessible by default — opt-out is safer than opt-in."
5
+ tags: [ruby, rails, security, authentication]
6
+ ---
7
+
8
+ # RR008 – Enforce Authentication with before_action
9
+
10
+ ## Rule
11
+
12
+ Place `before_action :authenticate_user!` in `ApplicationController`. Use `skip_before_action` only for explicitly public endpoints. Never rely on per-action checks or assume private routes are protected.
13
+
14
+ ## Why
15
+
16
+ If authentication is applied per-action (opt-in), a new action will be public by default — a common source of auth bypass vulnerabilities. Opt-out (skip for public endpoints) is the safe default.
17
+
18
+ ## Wrong
19
+
20
+ ```ruby
21
+ # Opt-in per controller — new actions are public by default
22
+ class PostsController < ApplicationController
23
+ def index
24
+ authenticate_user! # ❌ developer must remember to call this
25
+ @posts = Post.all
26
+ end
27
+
28
+ def create
29
+ # ❌ forgot authenticate_user! — publicly writable
30
+ Post.create!(post_params)
31
+ end
32
+ end
33
+
34
+ # ApplicationController with no global hook
35
+ class ApplicationController < ActionController::API
36
+ # nothing — every action is public until protected
37
+ end
38
+ ```
39
+
40
+ ## Correct
41
+
42
+ ```ruby
43
+ # ApplicationController — everything requires auth
44
+ class ApplicationController < ActionController::API
45
+ include Devise::Controllers::Helpers # or your auth layer
46
+ before_action :authenticate_user!
47
+
48
+ private
49
+
50
+ def current_user_id
51
+ current_user.id
52
+ end
53
+ end
54
+
55
+ # Public endpoints explicitly opt out
56
+ class SessionsController < ApplicationController
57
+ skip_before_action :authenticate_user!, only: [:create, :destroy]
58
+
59
+ def create
60
+ # login — intentionally public
61
+ end
62
+ end
63
+
64
+ class HealthController < ApplicationController
65
+ skip_before_action :authenticate_user!
66
+
67
+ def show
68
+ render json: { status: 'ok' }
69
+ end
70
+ end
71
+ ```
72
+
73
+ ## Notes
74
+
75
+ - For Devise APIs, use `authenticate_user!` from `Devise::Controllers::Helpers` or the Devise-JWT extension.
76
+ - Documenting `skip_before_action` with a comment explaining WHY the action is public improves auditability.
77
+ - Add an integration test that requests every route without auth and asserts `status: 401` for protected ones.
@@ -0,0 +1,61 @@
1
+ ---
2
+ title: "RR009 – Store Secrets in Rails Credentials, Not in Code"
3
+ impact: high
4
+ impactDescription: "Hardcoded secrets in source code are committed to Git history and exposed permanently even after deletion."
5
+ tags: [ruby, rails, security, secrets]
6
+ ---
7
+
8
+ # RR009 – Store Secrets in Rails Credentials
9
+
10
+ ## Rule
11
+
12
+ All secret values (API keys, tokens, signing secrets, passwords) must live in `config/credentials.yml.enc` or environment variables. Never hardcode them in source files, `config/initializers/`, or `database.yml`.
13
+
14
+ ## Why
15
+
16
+ Once a secret is committed, it lives in Git history forever. Rotating it requires a force-push and re-access revocation. Rails credentials are encrypted at rest; the master key (`config/master.key` or `RAILS_MASTER_KEY`) is the only secret that must stay outside source control.
17
+
18
+ ## Wrong
19
+
20
+ ```ruby
21
+ # config/initializers/stripe.rb
22
+ Stripe.api_key = 'sk_live_XXXXXXXXXXXXX' # ❌ hardcoded
23
+
24
+ # config/database.yml
25
+ production:
26
+ password: 'my_db_password_123' # ❌ hardcoded
27
+
28
+ # app/services/sendgrid_service.rb
29
+ API_KEY = 'SG.xxxxxxxxxxxx' # ❌ constant with embedded secret
30
+ ```
31
+
32
+ ## Correct
33
+
34
+ ```bash
35
+ # Add / edit secrets (encrypts automatically)
36
+ bin/rails credentials:edit --environment production
37
+
38
+ # config/credentials/production.yml.enc (decrypted view)
39
+ stripe:
40
+ api_key: sk_live_XXXXXXXXXXXXX
41
+ sendgrid:
42
+ api_key: SG.xxxxxxxxxxxx
43
+ ```
44
+
45
+ ```ruby
46
+ # Access anywhere in application code
47
+ Stripe.api_key = Rails.application.credentials.stripe[:api_key]
48
+
49
+ # Or via environment variable (suitable for container deployments)
50
+ Stripe.api_key = ENV.fetch('STRIPE_API_KEY')
51
+
52
+ # config/database.yml (env var form)
53
+ production:
54
+ password: <%= ENV['DATABASE_PASSWORD'] %>
55
+ ```
56
+
57
+ ## Notes
58
+
59
+ - `config/master.key` and `config/credentials/production.key` must be in `.gitignore` and never committed.
60
+ - In CI/CD, inject `RAILS_MASTER_KEY` as a masked environment variable, not in cleartext config.
61
+ - For team environments with multiple people needing access, prefer environment variables or a vault (e.g., HashiCorp Vault, AWS Secrets Manager) over shared credentials files.
@@ -0,0 +1,57 @@
1
+ ---
2
+ title: "RR010 – Define Reusable Query Scopes in Models"
3
+ impact: medium
4
+ impactDescription: "Inline where() chains scattered across controllers make conditions hard to reuse, test, and change in one place."
5
+ tags: [ruby, rails, code-quality, database]
6
+ ---
7
+
8
+ # RR010 – Define Reusable Query Scopes in Models
9
+
10
+ ## Rule
11
+
12
+ Encapsulate frequently used `where`, `order`, and `joins` conditions as named scopes on the model. Controllers call scopes by name — they do not build query logic inline.
13
+
14
+ ## Why
15
+
16
+ Query conditions duplicated across controllers and services violate DRY and make maintenance hazardous. A scope is a named, composable, chainable query fragment that belongs to the model (the owner of the data).
17
+
18
+ ## Wrong
19
+
20
+ ```ruby
21
+ # Controller 1
22
+ @posts = Post.where(published: true).where('created_at > ?', 30.days.ago).order(created_at: :desc)
23
+
24
+ # Controller 2 (duplicate, slightly different)
25
+ @featured = Post.where(published: true).where(featured: true).order(created_at: :desc)
26
+
27
+ # Controller 3 (wrong date, bug introduced by copy-paste)
28
+ @recent = Post.where(published: true).where('created_at > ?', 7.days.ago).order(created_at: :asc)
29
+ ```
30
+
31
+ ## Correct
32
+
33
+ ```ruby
34
+ # app/models/post.rb
35
+ class Post < ApplicationRecord
36
+ scope :published, -> { where(published: true) }
37
+ scope :featured, -> { where(featured: true) }
38
+ scope :recent, -> { where('created_at > ?', 30.days.ago) }
39
+ scope :by_newest, -> { order(created_at: :desc) }
40
+
41
+ # Scopes with parameters
42
+ scope :created_after, ->(date) { where('created_at > ?', date) }
43
+ scope :by_author, ->(author_id) { where(author_id: author_id) }
44
+ end
45
+
46
+ # Controllers compose scopes — clean, readable, no SQL inline
47
+ @posts = Post.published.recent.by_newest
48
+ @featured = Post.published.featured.by_newest
49
+ @mine = Post.published.by_author(current_user.id).recent
50
+ ```
51
+
52
+ ## Notes
53
+
54
+ - Scopes return `ActiveRecord::Relation` — they are chainable by design.
55
+ - Scopes should be tested independently in model specs: `Post.published` should only return published posts.
56
+ - For complex multi-table logic, consider a Query Object (like a service object) that wraps the relation.
57
+ - Avoid scopes with side effects (no writes, callbacks, or network calls).
@@ -0,0 +1,59 @@
1
+ ---
2
+ title: "RR011 – Use counter_cache to Avoid Count N+1"
3
+ impact: medium
4
+ impactDescription: "post.comments.count in a collection loop fires one COUNT query per row; counter_cache collapses this to zero extra queries."
5
+ tags: [ruby, rails, performance, database]
6
+ ---
7
+
8
+ # RR011 – Use counter_cache to Avoid Count N+1
9
+
10
+ ## Rule
11
+
12
+ When displaying association counts in collection views or API responses (comment counts, like counts, etc.), use `counter_cache: true` on the `belongs_to` side instead of computing `association.count` in-loop.
13
+
14
+ ## Why
15
+
16
+ `post.comments.count` (or `.size` when collection is not loaded) executes a `COUNT(*)` SQL query. In a list of 50 posts this fires 50 extra queries. `counter_cache` maintains a denormalized `comments_count` column on the parent table updated atomically by Rails on create/destroy.
17
+
18
+ ## Wrong
19
+
20
+ ```ruby
21
+ # In API serializer or view — fires N COUNT queries
22
+ posts.each do |post|
23
+ count = post.comments.count # ❌ 1 COUNT query per post
24
+ end
25
+
26
+ # JSON response that triggers N+1
27
+ render json: @posts.map { |p| { id: p.id, comment_count: p.comments.count } }
28
+ ```
29
+
30
+ ## Correct
31
+
32
+ ```ruby
33
+ # Migration: add the cache column
34
+ add_column :posts, :comments_count, :integer, default: 0, null: false
35
+ # Backfill: Post.find_each { |p| Post.reset_counters(p.id, :comments) }
36
+
37
+ # Model association
38
+ class Comment < ApplicationRecord
39
+ belongs_to :post, counter_cache: true
40
+ # Rails now auto-increments/decrements posts.comments_count
41
+ end
42
+
43
+ class Post < ApplicationRecord
44
+ has_many :comments
45
+ end
46
+
47
+ # Usage — no extra query
48
+ posts.each do |post|
49
+ count = post.comments_count # ✅ reads the cached column, zero extra queries
50
+ end
51
+
52
+ render json: @posts.map { |p| { id: p.id, comment_count: p.comments_count } }
53
+ ```
54
+
55
+ ## Notes
56
+
57
+ - Reset cache after backfills: `Post.find_each { |p| Post.reset_counters(p.id, :comments) }`.
58
+ - If using soft-delete (`acts_as_paranoid`, `discard`), counter_cache won't exclude soft-deleted records — use a custom query in that case.
59
+ - Alternative when migration is not possible: `Post.includes(:comments)` + `post.comments.size` (`.size` uses already-loaded collection, no extra query).
@@ -0,0 +1,42 @@
1
+ ---
2
+ title: "RR012 – Always Include status: in render json:"
3
+ impact: medium
4
+ impactDescription: "Omitting status: defaults to 200, masking errors and breaking API client error-handling."
5
+ tags: [ruby, rails, api, http]
6
+ ---
7
+
8
+ # RR012 – Always Include `status:` in `render json:`
9
+
10
+ ## Rule
11
+
12
+ Every `render json:` call must include an explicit `status:` keyword argument. Do not rely on the implicit `200` default.
13
+
14
+ ## Why
15
+
16
+ This is a codified subset of RR007 focused on a single easy-to-miss omission. Implicit 200 is the default only because it's the most common case — it doesn't mean it's always correct. Explicit status makes intent clear in code review and prevents errors from silently appearing as successes to API clients.
17
+
18
+ ## Wrong
19
+
20
+ ```ruby
21
+ render json: @user # ❌ implicit 200
22
+ render json: { errors: @user.errors.full_messages } # ❌ looks like success to client
23
+ render json: { message: 'Deleted' } # ❌ should be 200 or 204 — unclear
24
+ ```
25
+
26
+ ## Correct
27
+
28
+ ```ruby
29
+ render json: UserResource.new(@user), status: :ok # 200
30
+ render json: UserResource.new(@user), status: :created # 201 for POST
31
+ render json: { errors: @user.errors.full_messages },
32
+ status: :unprocessable_entity # 422
33
+ render json: { message: 'Deleted' }, status: :ok # 200
34
+ # OR for no-body deletes:
35
+ head :no_content # 204, no body
36
+ ```
37
+
38
+ ## Notes
39
+
40
+ - Use Rails symbol names (`:ok`, `:created`, `:no_content`) — not integers — for readability.
41
+ - `head :no_content` is the cleanest response for DELETE with no return body.
42
+ - Pair this rule with consistent `rescue_from` in ApplicationController so all error paths also have correct status codes (see RR007).
@@ -0,0 +1,37 @@
1
+ ---
2
+ title: Tên hàm phải là động từ hoặc động từ + danh từ
3
+ impact: MEDIUM
4
+ impactDescription: Tên hàm mô tả đúng hành động giúp code dễ đọc và giảm cognitive load khi review PR.
5
+ tags: swift, ios, naming, readability, code-quality
6
+ ---
7
+
8
+ ## Tên hàm phải là động từ hoặc động từ + danh từ
9
+
10
+ Tên hàm phải bắt đầu bằng động từ hoặc cụm động từ + danh từ, mô tả rõ ràng hành động mà hàm thực hiện. Tránh đặt tên mơ hồ như `data()`, `handle()`, `process()`.
11
+
12
+ **Incorrect (tên không rõ hành động):**
13
+
14
+ ```swift
15
+ class UserManager {
16
+ func data() -> User? { ... } // Không rõ là lấy hay tạo
17
+ func handle(_ error: Error) { ... } // handle là gì?
18
+ func process(_ payment: Payment) { ... } // process không nói lên kết quả
19
+ func userInfo(for id: String) { ... } // thiếu động từ
20
+ }
21
+ ```
22
+
23
+ **Correct (tên thể hiện hành động rõ ràng):**
24
+
25
+ ```swift
26
+ class UserManager {
27
+ func fetchUser(by id: String) -> User? { ... }
28
+ func logError(_ error: Error) { ... }
29
+ func submitPayment(_ payment: Payment) throws { ... }
30
+ func loadUserProfile(userId: String) async throws -> UserProfile { ... }
31
+ func validateEmail(_ email: String) -> Bool { ... }
32
+ func clearCache() { ... }
33
+ }
34
+ ```
35
+
36
+ **Tools:** SwiftLint custom rule, Code Review
37
+