@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,73 @@
1
+ ---
2
+ title: Avoid global Statement in Functions
3
+ impact: MEDIUM
4
+ impactDescription: The global statement creates hidden dependencies between functions and module-level state, making code order-dependent, untestable, and unsafe for concurrent use.
5
+ tags: python, global, state, quality, concurrency
6
+ ---
7
+
8
+ ## Avoid global Statement in Functions
9
+
10
+ Using `global var` inside a function creates an implicit dependency on module-level mutable state. The function's behavior changes depending on which other functions have already run, making it nearly impossible to test in isolation. In multi-threaded or async contexts, it introduces race conditions.
11
+
12
+ Pass state explicitly through parameters and return values, or encapsulate it in a class.
13
+
14
+ **Incorrect:**
15
+ ```python
16
+ counter = 0
17
+ last_user = None
18
+
19
+ def process_request(user_id: int) -> None:
20
+ global counter, last_user # hidden mutation of module state
21
+ counter += 1
22
+ last_user = user_id
23
+ do_work(user_id)
24
+
25
+ def get_stats() -> dict:
26
+ return {"total": counter, "last": last_user}
27
+
28
+ # Tests are order-dependent and cannot run in parallel
29
+ def test_process_request():
30
+ process_request(42)
31
+ assert counter == 1 # depends on no other test having run first
32
+ ```
33
+
34
+ **Correct:**
35
+ ```python
36
+ from dataclasses import dataclass, field
37
+ from threading import Lock
38
+
39
+ @dataclass
40
+ class RequestTracker:
41
+ _count: int = 0
42
+ _last_user: int | None = None
43
+ _lock: Lock = field(default_factory=Lock, compare=False)
44
+
45
+ def record(self, user_id: int) -> None:
46
+ with self._lock:
47
+ self._count += 1
48
+ self._last_user = user_id
49
+
50
+ def stats(self) -> dict:
51
+ return {"total": self._count, "last": self._last_user}
52
+
53
+ tracker = RequestTracker()
54
+
55
+ def process_request(user_id: int, tracker: RequestTracker) -> None:
56
+ tracker.record(user_id)
57
+ do_work(user_id)
58
+
59
+ # Tests are isolated
60
+ def test_process_request():
61
+ t = RequestTracker()
62
+ process_request(42, t)
63
+ assert t.stats()["total"] == 1
64
+ ```
65
+
66
+ **Acceptable module-level constants (no `global` needed):**
67
+ ```python
68
+ # Constants never mutated — global statement not needed or used
69
+ MAX_RETRIES = 3
70
+ DEFAULT_TIMEOUT = 30.0
71
+ ```
72
+
73
+ **Tools:** Ruff `PLW0603` (global-statement), Pylint `W0603`, SonarQube Python `S1854`, flake8
@@ -0,0 +1,66 @@
1
+ ---
2
+ title: Do Not Modify Collection While Iterating Over It
3
+ impact: HIGH
4
+ impactDescription: Adding or removing items from a list, dict, or set while iterating over it causes RuntimeError, skipped elements, or undefined behavior depending on the collection type.
5
+ tags: python, iteration, bugs, pitfalls, quality
6
+ ---
7
+
8
+ ## Do Not Modify Collection While Iterating Over It
9
+
10
+ Modifying a `dict` or `set` while iterating raises `RuntimeError: dictionary changed size during iteration` / `set changed size during iteration`. Modifying a `list` during iteration does **not** raise an error but silently skips or processes elements multiple times, producing wrong results.
11
+
12
+ Always iterate over a copy, build a new collection, or collect modifications and apply them after the loop.
13
+
14
+ **Incorrect:**
15
+ ```python
16
+ # dict — raises RuntimeError
17
+ config = {"debug": True, "deprecatedKey": "val", "timeout": 30}
18
+
19
+ for key in config:
20
+ if key.startswith("deprecated"):
21
+ del config[key] # RuntimeError: dictionary changed size during iteration
22
+
23
+ # set — raises RuntimeError
24
+ seen = {1, 2, 3, 4, 5}
25
+
26
+ for item in seen:
27
+ if item % 2 == 0:
28
+ seen.discard(item) # RuntimeError: set changed size during iteration
29
+
30
+ # list — silent bug: skips elements
31
+ items = [1, 2, 2, 3, 4]
32
+
33
+ for i, item in enumerate(items):
34
+ if item == 2:
35
+ items.remove(item) # skips the second 2 silently
36
+ ```
37
+
38
+ **Correct:**
39
+ ```python
40
+ # dict — iterate over a copy of keys
41
+ config = {"debug": True, "deprecatedKey": "val", "timeout": 30}
42
+
43
+ for key in list(config.keys()):
44
+ if key.startswith("deprecated"):
45
+ del config[key]
46
+
47
+ # Or build a new dict with dict comprehension (preferred for clarity)
48
+ config = {k: v for k, v in config.items() if not k.startswith("deprecated")}
49
+
50
+ # set — iterate over a copy
51
+ seen = {1, 2, 3, 4, 5}
52
+ to_remove = {item for item in seen if item % 2 == 0}
53
+ seen -= to_remove
54
+
55
+ # list — collect removals, apply after loop
56
+ items = [1, 2, 2, 3, 4]
57
+ items = [item for item in items if item != 2] # list comprehension
58
+
59
+ # Or for in-place modification, iterate in reverse
60
+ items = [1, 2, 2, 3, 4]
61
+ for i in range(len(items) - 1, -1, -1):
62
+ if items[i] == 2:
63
+ del items[i]
64
+ ```
65
+
66
+ **Tools:** Ruff `E4702` (modified-iterating-dict), `E4703` (modified-iterating-set), `W4701` (modified-iterating-list), Pylint `modified_iteration` checker, flake8
@@ -0,0 +1,61 @@
1
+ ---
2
+ title: Prefer f-strings Over % and str.format()
3
+ impact: MEDIUM
4
+ impactDescription: f-strings introduced in Python 3.6 are more readable, faster at runtime, and catch errors at parse time rather than producing cryptic runtime errors from mismatched argument counts.
5
+ tags: python, fstrings, readability, performance, quality, python3
6
+ ---
7
+
8
+ ## Prefer f-strings Over % and str.format()
9
+
10
+ Python 3.6 introduced f-strings (`f"..."`) as the modern, preferred way to embed expressions in string literals. They are:
11
+ - **More readable** — expression and string are co-located
12
+ - **Faster** — evaluated at the bytecode level without function dispatch
13
+ - **Safer** — syntax errors caught at parse time; no mismatched `%s` args
14
+ - **More powerful** — support arbitrary expressions and format specs inline
15
+
16
+ **Incorrect:**
17
+ ```python
18
+ name = "Alice"
19
+ score = 98.567
20
+ items = ["apple", "banana"]
21
+
22
+ # %-formatting (Python 2 style) — argument count mismatches cause runtime TypeError
23
+ msg = "Hello, %s! Your score is %.2f" % (name, score)
24
+ debug = "Processing %d items: %s" % len(items) # runtime error: int not iterable
25
+
26
+ # str.format() — verbose and error-prone with positional indices
27
+ msg = "Hello, {}! Your score is {:.2f}".format(name, score)
28
+ debug = "User {0} has {1} points ({0} is great)".format(name, score)
29
+ ```
30
+
31
+ **Correct:**
32
+ ```python
33
+ name = "Alice"
34
+ score = 98.567
35
+ user_id = 1042
36
+
37
+ # f-strings — expression evaluated inline
38
+ msg = f"Hello, {name}! Your score is {score:.2f}"
39
+ debug = f"Processing {len(items)} items: {items}"
40
+
41
+ # Supports format specs
42
+ padded = f"{user_id:05d}" # "01042"
43
+ percent = f"{score / 100:.1%}" # "98.6%"
44
+ repr_v = f"Object: {obj!r}" # calls repr()
45
+
46
+ # Multi-line f-string
47
+ report = (
48
+ f"User: {name}\n"
49
+ f"Score: {score:.2f}\n"
50
+ f"Rank: {get_rank(score)}"
51
+ )
52
+ ```
53
+
54
+ **Exception — logging calls should NOT use f-strings:**
55
+ ```python
56
+ # In logging, keep lazy % formatting for performance (see P009)
57
+ logger.debug("Processing user %s", user_id) # correct
58
+ logger.debug(f"Processing user {user_id}") # incorrect — always evaluates
59
+ ```
60
+
61
+ **Tools:** Ruff `UP031` (printf-string-formatting), `UP032` (f-string), `G004` (logging-f-string), pyupgrade, flynt
@@ -0,0 +1,121 @@
1
+ # Ruby on Rails Framework — SunLint Agent Guide
2
+
3
+ > Priority directives for AI agents working on Rails projects.
4
+ > Rule files: `.agent/skills/sunlint-code-quality/rules/`
5
+
6
+ ---
7
+
8
+ ## Critical Patterns — Apply Every Time
9
+
10
+ ### Input Filtering
11
+ - **Always** whitelist params with `params.require(:model).permit(:field1, :field2)`
12
+ - **Never** use `params[:model].to_unsafe_h`, `params.permit!`, or `Model.create(params[:model])`
13
+ - See: `RR001-strong-parameters.md`
14
+
15
+ ### N+1 Queries
16
+ - Any controller loading a collection that renders association data → add `includes(:relation)`
17
+ - Use `eager_load(:relation)` when filtering by association columns (SQL `WHERE`)
18
+ - Count in loops → use `counter_cache: true` + `model.relation_count` column
19
+ - See: `RR002-eager-load-includes.md`, `RR011-counter-cache.md`
20
+
21
+ ### Authentication
22
+ - `before_action :authenticate_user!` in `ApplicationController` — opt-out for public actions with `skip_before_action`
23
+ - **Never** add authentication checks per-action (opt-in) — new actions are public by default
24
+ - See: `RR008-before-action-auth.md`
25
+
26
+ ### Secrets
27
+ - **Never** hardcode API keys, tokens, passwords in source files
28
+ - `Rails.application.credentials.section[:key]` OR `ENV.fetch('KEY')` only
29
+ - `config/master.key` and `config/credentials/*.key` must be in `.gitignore`
30
+ - See: `RR009-rails-credentials.md`
31
+
32
+ ---
33
+
34
+ ## Architecture Patterns
35
+
36
+ ### Controller responsibility — thin controllers only
37
+ Controllers do exactly: authenticate → authorize → parse strong params → call service → render response.
38
+ ```ruby
39
+ def create
40
+ result = PlaceOrderService.new(user: current_user, params: order_params).call
41
+ if result.success?
42
+ render json: OrderResource.new(result.order), status: :created
43
+ else
44
+ render json: { errors: result.errors }, status: :unprocessable_entity
45
+ end
46
+ end
47
+ ```
48
+ See: `RR003-service-objects.md`
49
+
50
+ ### Service Objects
51
+ - Business logic with multi-model writes, external calls, or branching → `app/services/` PORO
52
+ - Expose one public method: `#call`
53
+ - Return a `Result` struct: `Result = Struct.new(:value, :success?, :error, keyword_init: true)`
54
+ - See: `RR003-service-objects.md`
55
+
56
+ ### Background Jobs
57
+ - Email, external API calls, file processing, report generation → `ActiveJob` + `perform_later`
58
+ - **Never** `Thread.new` in controllers, **never** inline slow work in the request cycle
59
+ - Pass only primitive/serializable arguments to jobs (IDs, strings) — not AR objects
60
+ - See: `RR004-active-job-background.md`
61
+
62
+ ### Pagination
63
+ - Every collection endpoint must call `.page(params[:page]).per(n)` (Kaminari) or `pagy(...)` (Pagy)
64
+ - **Never** `Model.all` without a page limit returned to the client
65
+ - See: `RR005-pagination.md`
66
+
67
+ ### Bulk Data Iteration (jobs/scripts/rake)
68
+ - `Model.all.each` → replace with `Model.find_each(batch_size: 500)`
69
+ - Bulk updates → `Model.in_batches(of: 200) { |batch| batch.update_all(...) }`
70
+ - See: `RR006-find-each-batches.md`
71
+
72
+ ### Query Scopes
73
+ - Named conditions (`published`, `recent`, `by_author`) → `scope :name, -> { where(...) }` on model
74
+ - **Never** inline `where('...')` chains in controllers — define a scope, call the scope
75
+ - See: `RR010-scopes.md`
76
+
77
+ ### HTTP Status Codes
78
+ - Every `render json:` must include `status:` — use Rails symbol names
79
+ - `POST` success → `:created` (201), validation failure → `:unprocessable_entity` (422)
80
+ - DELETE with no body → `head :no_content` (204)
81
+ - Centralize `rescue_from ActiveRecord::RecordNotFound` → `:not_found` in ApplicationController
82
+ - See: `RR007-http-status-codes.md`, `RR012-render-json-status.md`
83
+
84
+ ---
85
+
86
+ ## Run Commands
87
+
88
+ ```bash
89
+ rails generate model ModelName field:type
90
+ rails generate service OrderService # custom generator if installed
91
+ rails generate job ProcessExportJob
92
+ rails generate migration AddCounterToTable
93
+
94
+ rails test test/models/post_test.rb # run specific test file
95
+ rails test -n /test_name_pattern/ # run by name match
96
+ bundle exec rspec spec/services/ # RSpec equivalent
97
+
98
+ rails credentials:edit --environment production # edit secrets
99
+ rails credentials:show # view decrypted
100
+
101
+ bundle exec rubocop --autocorrect # lint + fix
102
+ bundle exec brakeman # security scanner
103
+ bundle exec bundle-audit check # dependency CVE scanner
104
+ ```
105
+
106
+ ---
107
+
108
+ ## What NOT to do (quick reference)
109
+
110
+ | ❌ Wrong | ✅ Correct |
111
+ |---|---|
112
+ | `params[:user].to_unsafe_h` | `params.require(:user).permit(:name, :email)` |
113
+ | `Model.create(params[:model])` | `Model.create(user_params)` (permitted) |
114
+ | `@posts = Post.all` (unbounded) | `@posts = Post.published.page(params[:page])` |
115
+ | `post.comments.count` in loop | `counter_cache: true` → `post.comments_count` |
116
+ | `Thread.new { ... }` | `MyJob.perform_later(...)` |
117
+ | `after_create :call_stripe` on model | Service object wrapping the transaction |
118
+ | Auth check per action (opt-in) | `before_action :authenticate_user!` in ApplicationController |
119
+ | `render json: { errors: ... }` | `render json: { errors: ... }, status: :unprocessable_entity` |
120
+ | `API_KEY = 'sk_live_...'` in source | `Rails.application.credentials.stripe[:api_key]` |
121
+ | Inline `where('published = true')` in controller | `scope :published, -> { where(published: true) }` |
@@ -0,0 +1,55 @@
1
+ ---
2
+ title: "RR001 – Use Strong Parameters (params.require().permit())"
3
+ impact: high
4
+ impactDescription: "Mass assignment via permit() prevents unfiltered user input from modifying forbidden attributes such as roles, admin flags, or foreign keys."
5
+ tags: [ruby, rails, security, input-validation]
6
+ ---
7
+
8
+ # RR001 – Use Strong Parameters
9
+
10
+ ## Rule
11
+
12
+ Always whitelist params with `require().permit()`. Never permit all params or bypass strong parameters.
13
+
14
+ ## Why
15
+
16
+ `params.to_unsafe_h` and raw `params` bypass Rails' Mass Assignment protection. An attacker can inject `admin: true`, `role: 'admin'`, or arbitrary foreign keys.
17
+
18
+ ## Wrong
19
+
20
+ ```ruby
21
+ # Bypasses strong parameters entirely
22
+ def create
23
+ @user = User.create(params[:user].to_unsafe_h)
24
+ end
25
+
26
+ # Permits everything
27
+ def user_params
28
+ params.require(:user).permit!
29
+ end
30
+
31
+ # Missing permit — may raise ForbiddenAttributesError or pass raw hash
32
+ def create
33
+ @user = User.create(user_params: params[:user])
34
+ end
35
+ ```
36
+
37
+ ## Correct
38
+
39
+ ```ruby
40
+ def create
41
+ @user = User.create!(user_params)
42
+ end
43
+
44
+ private
45
+
46
+ def user_params
47
+ params.require(:user).permit(:name, :email, :password, :password_confirmation)
48
+ end
49
+ ```
50
+
51
+ ## Notes
52
+
53
+ - `permit!` is only acceptable in internal admin scripts where input is completely trusted and audited.
54
+ - Nested attributes need explicit permit: `permit(:name, addresses_attributes: [:street, :city])`.
55
+ - Never store the result of `to_unsafe_h` into a model.
@@ -0,0 +1,51 @@
1
+ ---
2
+ title: "RR002 – Prevent N+1 with includes/eager_load"
3
+ impact: high
4
+ impactDescription: "Unaddressed N+1 queries grow linearly with page size; a 100-row page fires 101 queries instead of 2."
5
+ tags: [ruby, rails, performance, database]
6
+ ---
7
+
8
+ # RR002 – Prevent N+1 with includes / eager_load
9
+
10
+ ## Rule
11
+
12
+ Always eager-load associations when iterating over a collection. Use `includes`, `eager_load`, or `preload` based on whether a WHERE on the association is needed.
13
+
14
+ ## Why
15
+
16
+ Accessing `post.author` inside an `.each` loop fires one SELECT per record — the classic N+1. Rails provides declarative eager loading to collapse N+1 into one extra query.
17
+
18
+ ## Wrong
19
+
20
+ ```ruby
21
+ # N+1: fires 1 + N queries for authors
22
+ @posts = Post.all
23
+ # view iterates: @posts.each { |p| p.author.name }
24
+
25
+ # N+1: fires 1 + N queries for comment counts
26
+ @posts = Post.all
27
+ # view: post.comments.count inside loop
28
+ ```
29
+
30
+ ## Correct
31
+
32
+ ```ruby
33
+ # Two queries total: one for posts, one for authors
34
+ @posts = Post.includes(:author).all
35
+
36
+ # Three queries: posts + authors + tags
37
+ @posts = Post.includes(:author, :tags).page(params[:page])
38
+
39
+ # Need to filter by association column → use eager_load (LEFT JOIN)
40
+ @posts = Post.eager_load(:author).where(authors: { verified: true })
41
+
42
+ # Avoid count N+1 with counter_cache or withCount equivalent
43
+ @posts = Post.includes(:comments)
44
+ # or add counter_cache: true to the belongs_to and use post.comments_count
45
+ ```
46
+
47
+ ## Notes
48
+
49
+ - `includes` automatically selects between `preload` (separate queries) and `eager_load` (LEFT JOIN). Prefer `includes` unless you explicitly need one over the other.
50
+ - Use the `bullet` gem in development to surface N+1 automatically.
51
+ - Nested associations: `Post.includes(comments: :author)`.
@@ -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.