@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.
Files changed (152) hide show
  1. package/core/file-targeting-service.js +148 -15
  2. package/core/init-command.js +118 -70
  3. package/core/project-detector.js +517 -0
  4. package/core/tui-select.js +245 -0
  5. package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
  6. package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
  7. package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
  8. package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
  9. package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
  10. package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
  11. package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
  12. package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
  13. package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
  14. package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
  15. package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
  16. package/package.json +1 -1
  17. package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
  18. package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
  19. package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
  20. package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
  21. package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
  22. package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
  23. package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
  24. package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
  25. package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
  26. package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
  27. package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
  28. package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
  29. package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
  30. package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
  31. package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
  32. package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
  33. package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
  34. package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
  35. package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
  36. package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
  37. package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
  38. package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
  39. package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
  40. package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
  41. package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
  42. package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
  43. package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
  44. package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
  45. package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
  46. package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
  47. package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
  48. package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
  49. package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
  50. package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
  51. package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
  52. package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
  53. package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
  54. package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
  55. package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
  56. package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
  57. package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
  58. package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
  59. package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
  60. package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
  61. package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
  62. package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
  63. package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
  64. package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
  65. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
  66. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
  67. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
  68. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
  69. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
  70. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
  71. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
  72. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
  73. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
  74. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
  75. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
  76. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
  77. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
  78. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
  79. package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
  80. package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
  81. package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
  82. package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
  83. package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
  84. package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
  85. package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
  86. package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
  87. package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
  88. package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
  89. package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
  90. package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
  91. package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
  92. package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
  93. package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
  94. package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
  95. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
  96. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
  97. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
  98. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
  99. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
  100. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
  101. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
  102. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
  103. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
  104. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
  105. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
  106. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
  107. package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
  108. package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
  109. package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
  110. package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
  111. package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
  112. package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
  113. package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
  114. package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
  115. package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
  116. package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
  117. package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
  118. package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
  119. package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
  120. package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
  121. package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
  122. package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
  123. package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
  124. package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
  125. package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
  126. package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
  127. package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
  128. package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
  129. package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
  130. package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
  131. package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
  132. package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
  133. package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
  134. package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
  135. package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
  136. package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
  137. package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
  138. package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
  139. package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
  140. package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
  141. package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
  142. package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
  143. package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
  144. package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
  145. package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
  146. package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
  147. package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
  148. package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
  149. package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
  150. package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
  151. package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
  152. package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
@@ -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
+
@@ -0,0 +1,55 @@
1
+ ---
2
+ title: Không để code chết trong codebase
3
+ impact: LOW
4
+ impactDescription: Code không dùng làm tăng kích thước binary, gây nhầm lẫn khi đọc và bảo trì, khiến reviewer tốn thời gian.
5
+ tags: swift, ios, dead-code, cleanup, code-quality
6
+ ---
7
+
8
+ ## Không để code chết trong codebase
9
+
10
+ Code không còn được gọi đến, biến không sử dụng, hàm private không được gọi, hay `#if false` block cần được xóa. Dùng Git history nếu cần khôi phục.
11
+
12
+ **Incorrect (code chết):**
13
+
14
+ ```swift
15
+ class OrderViewController: UIViewController {
16
+
17
+ // Không bao giờ được dùng
18
+ private func legacyFetchOrders() {
19
+ // TODO: remove this
20
+ }
21
+
22
+ #if false
23
+ private func debugHelper() {
24
+ print("Debug info: \(someVar)")
25
+ }
26
+ #endif
27
+
28
+ // Biến khai báo nhưng không dùng
29
+ private let unusedFormatter = DateFormatter()
30
+
31
+ override func viewDidLoad() {
32
+ super.viewDidLoad()
33
+ loadOrders()
34
+ }
35
+
36
+ private func loadOrders() { ... }
37
+ }
38
+ ```
39
+
40
+ **Correct (chỉ giữ code đang được dùng):**
41
+
42
+ ```swift
43
+ class OrderViewController: UIViewController {
44
+
45
+ override func viewDidLoad() {
46
+ super.viewDidLoad()
47
+ loadOrders()
48
+ }
49
+
50
+ private func loadOrders() { ... }
51
+ }
52
+ ```
53
+
54
+ **Tools:** SwiftLint (`unused_private_declaration`), Xcode Warnings, SwiftFormat (`unusedPrivateDeclarations`)
55
+
@@ -0,0 +1,69 @@
1
+ ---
2
+ title: Dùng Dependency Injection thay vì khởi tạo trực tiếp
3
+ impact: HIGH
4
+ impactDescription: DI qua protocol giúp dễ dàng mock trong unit test, tách biệt các tầng và tránh coupling cứng giữa các class.
5
+ tags: swift, ios, dependency-injection, testing, architecture, protocol
6
+ ---
7
+
8
+ ## Dùng Dependency Injection thay vì khởi tạo trực tiếp
9
+
10
+ Thay vì khởi tạo dependency trực tiếp bên trong class, hãy inject chúng qua `init` sử dụng protocol. Điều này đặc biệt quan trọng trong iOS khi viết unit test cho ViewModel, Interactor, hay Service.
11
+
12
+ **Incorrect (khởi tạo trực tiếp):**
13
+
14
+ ```swift
15
+ class OrderViewModel {
16
+ // Coupling cứng - không thể mock khi test
17
+ private let apiService = OrderAPIService()
18
+ private let database = CoreDataManager()
19
+ private let analytics = FirebaseAnalytics()
20
+
21
+ func loadOrders() {
22
+ apiService.fetchOrders { [weak self] result in
23
+ // xử lý
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ **Correct (inject qua protocol):**
30
+
31
+ ```swift
32
+ protocol OrderAPIServiceProtocol {
33
+ func fetchOrders(completion: @escaping (Result<[Order], Error>) -> Void)
34
+ }
35
+
36
+ protocol AnalyticsProtocol {
37
+ func track(event: String)
38
+ }
39
+
40
+ class OrderViewModel {
41
+ private let apiService: OrderAPIServiceProtocol
42
+ private let analytics: AnalyticsProtocol
43
+
44
+ init(
45
+ apiService: OrderAPIServiceProtocol = OrderAPIService(),
46
+ analytics: AnalyticsProtocol = FirebaseAnalytics()
47
+ ) {
48
+ self.apiService = apiService
49
+ self.analytics = analytics
50
+ }
51
+
52
+ func loadOrders() {
53
+ apiService.fetchOrders { [weak self] result in
54
+ // xử lý
55
+ }
56
+ }
57
+ }
58
+
59
+ // Unit test dễ dàng
60
+ class MockOrderAPIService: OrderAPIServiceProtocol {
61
+ func fetchOrders(completion: @escaping (Result<[Order], Error>) -> Void) {
62
+ completion(.success([Order(id: "1", total: 100)]))
63
+ }
64
+ }
65
+ let vm = OrderViewModel(apiService: MockOrderAPIService(), analytics: MockAnalytics())
66
+ ```
67
+
68
+ **Tools:** Code Review, Needle/Swinject (DI Framework)
69
+
@@ -0,0 +1,66 @@
1
+ ---
2
+ title: Không đặt logic nghiệp vụ trong init
3
+ impact: MEDIUM
4
+ impactDescription: Logic phức tạp trong init làm khó test, gây side-effect không mong muốn và vi phạm nguyên tắc Single Responsibility.
5
+ tags: swift, ios, init, constructor, architecture, code-quality
6
+ ---
7
+
8
+ ## Không đặt logic nghiệp vụ trong init
9
+
10
+ `init` chỉ nên gán giá trị cho các property. Đừng gọi API, đọc file, khởi động timer, hay thực hiện tính toán phức tạp bên trong `init`. Nếu cần, tách ra thành hàm `configure()` hoặc `setup()` gọi sau khi khởi tạo.
11
+
12
+ **Incorrect (logic trong init):**
13
+
14
+ ```swift
15
+ class UserProfileViewModel {
16
+ var user: User?
17
+ var recentOrders: [Order] = []
18
+
19
+ init(userId: String) {
20
+ // Gọi API trong init - không thể control được
21
+ let user = UserAPIService.shared.fetchUserSync(userId: userId)
22
+ self.user = user
23
+
24
+ // Logic nghiệp vụ trong init
25
+ if let user = user, user.isPremium {
26
+ recentOrders = OrderAPIService.shared.fetchOrdersSync(userId: userId)
27
+ }
28
+
29
+ // Đọc file trong init
30
+ let config = try? JSONDecoder().decode(Config.self, from: Data(contentsOf: configURL))
31
+ }
32
+ }
33
+ ```
34
+
35
+ **Correct (init đơn giản + hàm setup riêng):**
36
+
37
+ ```swift
38
+ class UserProfileViewModel {
39
+ private let userId: String
40
+ private let apiService: UserAPIServiceProtocol
41
+
42
+ var user: User?
43
+ var recentOrders: [Order] = []
44
+
45
+ init(userId: String, apiService: UserAPIServiceProtocol) {
46
+ self.userId = userId
47
+ self.apiService = apiService
48
+ // Không có logic nghiệp vụ ở đây
49
+ }
50
+
51
+ // Gọi sau khi khởi tạo - dễ test, dễ control
52
+ func loadData() async {
53
+ do {
54
+ user = try await apiService.fetchUser(userId: userId)
55
+ if user?.isPremium == true {
56
+ recentOrders = try await apiService.fetchOrders(userId: userId)
57
+ }
58
+ } catch {
59
+ // xử lý lỗi
60
+ }
61
+ }
62
+ }
63
+ ```
64
+
65
+ **Tools:** Code Review, SwiftLint custom rule
66
+