@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,108 @@
1
+ ---
2
+ title: Never Hardcode Cryptographic Keys or Initialization Vectors
3
+ impact: HIGH
4
+ impactDescription: hardcoded crypto keys or IVs give attackers permanent access to all encrypted data — compromised keys cannot be rotated without code changes
5
+ tags: security, cryptography, java, secrets
6
+ ---
7
+
8
+ ## Never Hardcode Cryptographic Keys or Initialization Vectors
9
+
10
+ Hardcoding cryptographic keys, passwords, secrets, or initialization vectors (IVs) directly in source code is a critical security vulnerability (OWASP A02: Cryptographic Failures). Once the code is in source control or compiled into a binary, the key is permanently exposed. Attackers who access the binary, source code, or logs can decrypt all data protected by that key.
11
+
12
+ **Incorrect (hardcoded key/IV):**
13
+
14
+ ```java
15
+ import javax.crypto.SecretKey;
16
+ import javax.crypto.spec.SecretKeySpec;
17
+ import javax.crypto.spec.IvParameterSpec;
18
+
19
+ public class EncryptionService {
20
+ // CRITICAL VULNERABILITY: key is hardcoded in source code
21
+ private static final byte[] SECRET_KEY = "MySuperSecretKey".getBytes(); // 16 bytes = AES-128
22
+ private static final byte[] IV = "InitVector123456".getBytes(); // hardcoded IV
23
+
24
+ public byte[] encrypt(byte[] data) throws Exception {
25
+ SecretKey key = new SecretKeySpec(SECRET_KEY, "AES");
26
+ IvParameterSpec ivSpec = new IvParameterSpec(IV);
27
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
28
+ cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
29
+ return cipher.doFinal(data);
30
+ }
31
+ }
32
+
33
+ // Also bad: hardcoded passwords for key derivation
34
+ public class KeyDerivation {
35
+ private static final String PASSWORD = "hardcoded_password_123"; // exposed in binary
36
+ private static final byte[] SALT = "fixed_salt".getBytes(); // static salt defeats PBKDF2
37
+ }
38
+ ```
39
+
40
+ **Correct (keys from secure storage):**
41
+
42
+ ```java
43
+ import javax.crypto.*;
44
+ import javax.crypto.spec.*;
45
+ import java.security.SecureRandom;
46
+
47
+ public class EncryptionService {
48
+ private final SecretKey key;
49
+
50
+ // Inject key via constructor — loaded from secure storage, not hardcoded
51
+ public EncryptionService(SecretKey key) {
52
+ this.key = key;
53
+ }
54
+
55
+ public EncryptedData encrypt(byte[] data) throws Exception {
56
+ // Always generate a random IV per encryption operation
57
+ byte[] iv = new byte[16];
58
+ new SecureRandom().nextBytes(iv); // cryptographically random
59
+ IvParameterSpec ivSpec = new IvParameterSpec(iv);
60
+
61
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
62
+ cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
63
+ byte[] ciphertext = cipher.doFinal(data);
64
+
65
+ return new EncryptedData(iv, ciphertext); // store IV alongside ciphertext
66
+ }
67
+ }
68
+
69
+ // Key loading from environment or secrets manager
70
+ public class KeyLoader {
71
+ public SecretKey loadKeyFromEnv() {
72
+ String base64Key = System.getenv("AES_SECRET_KEY"); // from environment
73
+ if (base64Key == null) {
74
+ throw new IllegalStateException("AES_SECRET_KEY environment variable not set");
75
+ }
76
+ byte[] keyBytes = Base64.getDecoder().decode(base64Key);
77
+ return new SecretKeySpec(keyBytes, "AES");
78
+ }
79
+
80
+ // Or use Java KeyStore (JKS/PKCS12):
81
+ public SecretKey loadKeyFromKeystore(String keystorePath, char[] storePassword,
82
+ String alias, char[] keyPassword) throws Exception {
83
+ KeyStore ks = KeyStore.getInstance("PKCS12");
84
+ try (InputStream fis = new FileInputStream(keystorePath)) {
85
+ ks.load(fis, storePassword);
86
+ }
87
+ return (SecretKey) ks.getKey(alias, keyPassword);
88
+ }
89
+ }
90
+ ```
91
+
92
+ **Secure key management options:**
93
+
94
+ | Method | Description |
95
+ |--------|-------------|
96
+ | Environment variables | Simple, good for containers (`AES_SECRET_KEY`) |
97
+ | Java KeyStore (JKS/PKCS12) | Standard Java key storage |
98
+ | AWS Secrets Manager / KMS | Cloud-managed, auditable |
99
+ | HashiCorp Vault | Enterprise secret management |
100
+ | Google Cloud Secret Manager | GCP-native secrets |
101
+
102
+ **Additional rules:**
103
+ - Always use a **random IV** per encryption — never a fixed IV.
104
+ - IVs need not be secret but must be unique per encryption.
105
+ - Use AES-GCM instead of AES-CBC where possible (provides authentication).
106
+ - Key length: AES-128 minimum, AES-256 recommended.
107
+
108
+ **Tools:** PMD (`HardCodedCryptoKey`, `InsecureCryptoIv`), SonarQube (`S2068`, `S3329`), SpotBugs (`HardCodedKey`)
@@ -0,0 +1,109 @@
1
+ ---
2
+ title: Use Optional Instead of Returning null for Absent Values
3
+ impact: MEDIUM
4
+ impactDescription: returning null from methods forces callers to guess whether null is possible, leading to unchecked NullPointerExceptions
5
+ tags: null-safety, design, java, clean-code
6
+ ---
7
+
8
+ ## Use Optional Instead of Returning null for Absent Values
9
+
10
+ `null` is the most common source of `NullPointerException` in Java. When a method may or may not return a value (as opposed to returning a collection), use `java.util.Optional<T>` to make the "absence" explicit in the API contract. `Optional` forces callers to consciously handle the absent case.
11
+
12
+ This is described in *Effective Java* Item 55: *"Return Optionals Judiciously."*
13
+
14
+ **Incorrect (returning null):**
15
+
16
+ ```java
17
+ // Caller cannot tell from the signature whether null is possible
18
+ public User findUserByEmail(String email) {
19
+ return userRepository.findByEmail(email); // may return null — not obvious
20
+ }
21
+
22
+ public String getPreferredLanguage(Long userId) {
23
+ UserPreferences prefs = preferencesRepo.findByUser(userId);
24
+ if (prefs == null) return null; // unclear API contract
25
+ return prefs.getLanguage();
26
+ }
27
+
28
+ // Callers must remember to null-check:
29
+ User user = userService.findUserByEmail("a@b.com");
30
+ user.getName(); // NPE if forgotten
31
+ ```
32
+
33
+ **Correct (using Optional):**
34
+
35
+ ```java
36
+ import java.util.Optional;
37
+
38
+ // Method signature clearly states: "this might not exist"
39
+ public Optional<User> findUserByEmail(String email) {
40
+ User user = userRepository.findByEmail(email);
41
+ return Optional.ofNullable(user); // wraps null into empty Optional
42
+ }
43
+
44
+ public Optional<String> getPreferredLanguage(Long userId) {
45
+ return Optional.ofNullable(preferencesRepo.findByUser(userId))
46
+ .map(UserPreferences::getLanguage);
47
+ }
48
+ ```
49
+
50
+ **Handling Optional at call sites:**
51
+
52
+ ```java
53
+ // Option 1: provide a default value
54
+ String name = userService.findUserByEmail("a@b.com")
55
+ .map(User::getName)
56
+ .orElse("Guest");
57
+
58
+ // Option 2: throw a meaningful exception if absent
59
+ User user = userService.findUserByEmail("a@b.com")
60
+ .orElseThrow(() -> new UserNotFoundException("User not found: " + email));
61
+
62
+ // Option 3: only proceed if present
63
+ userService.findUserByEmail("a@b.com")
64
+ .ifPresent(user -> sendWelcomeEmail(user));
65
+
66
+ // Option 4: ifPresentOrElse (Java 9+)
67
+ userService.findUserByEmail("a@b.com")
68
+ .ifPresentOrElse(
69
+ user -> logger.info("Found user: {}", user.getId()),
70
+ () -> logger.info("User not found")
71
+ );
72
+
73
+ // Option 5: transform and fall back
74
+ String language = getPreferredLanguage(userId)
75
+ .filter(lang -> supportedLanguages.contains(lang))
76
+ .orElse("en");
77
+ ```
78
+
79
+ **When NOT to use Optional:**
80
+
81
+ ```java
82
+ // 1. Do NOT use Optional for fields — use null or default values
83
+ public class User {
84
+ private Optional<String> middleName; // bad: not serializable, adds overhead
85
+ private String middleName; // good: use null or ""
86
+ }
87
+
88
+ // 2. Do NOT use Optional for method parameters — use overloading or @Nullable
89
+ public void createUser(Optional<String> role) { ... } // bad
90
+ public void createUser(String role) { ... } // good: role can be nullable
91
+ public void createUser() { ... } // good: overload for absent role
92
+
93
+ // 3. Do NOT use Optional for collections — return empty collection instead
94
+ public Optional<List<Order>> getOrders() { ... } // bad: double-wrap
95
+ public List<Order> getOrders() { ... } // good: return empty list
96
+
97
+ // 4. Do NOT use Optional.get() without checking — defeats the purpose
98
+ user.get().getName(); // same as null, throws NoSuchElementException
99
+ ```
100
+
101
+ **Optional factory methods:**
102
+
103
+ ```java
104
+ Optional.of(value) // value must be non-null; throws NPE if null
105
+ Optional.ofNullable(value) // safe: wraps null into empty Optional
106
+ Optional.empty() // explicitly empty
107
+ ```
108
+
109
+ **Tools:** IntelliJ Inspections (`OptionalUsedAsFieldOrParameterType`), SonarQube (`S3553`, `S2789`), PMD
@@ -0,0 +1,124 @@
1
+ # Laravel Framework — SunLint Agent Guide
2
+
3
+ > Priority directives for AI agents working on Laravel projects.
4
+ > Rule files: `.agent/skills/sunlint-code-quality/rules/`
5
+
6
+ ---
7
+
8
+ ## Critical Patterns — Apply Every Time
9
+
10
+ ### Validation
11
+ - **Always** use `php artisan make:request` → `FormRequest` class, never `$request->validate()` in controller
12
+ - In the controller use `$request->validated()` — never `$request->all()` or `$request->input()` wholesale
13
+ - See: `LV001-form-request-validation.md`
14
+
15
+ ### N+1 Queries
16
+ - **Always** check: does this controller/service access a relation in a loop or collection?
17
+ - If yes → add `->with(['relation'])` to the query **before** the loop
18
+ - Use `->withCount('relation')` instead of `->relation->count()` in loops
19
+ - See: `LV002-eager-load-no-n-plus-1.md`
20
+
21
+ ### Mass Assignment
22
+ - Every new Model must define explicit `$fillable = [...]`
23
+ - **Never**: `protected $guarded = []`
24
+ - **Never**: `Model::create($request->all())` — use `$request->validated()`
25
+ - See: `LV004-fillable-mass-assignment.md`
26
+
27
+ ### Passwords
28
+ - `Hash::make($password)` to store, `Hash::check($plain, $hash)` to verify
29
+ - **Never** `md5()`, `sha1()`, or raw `password_hash()` for passwords
30
+ - See: `LV007-hash-passwords.md`
31
+
32
+ ### Authorization
33
+ - **Never** `if ($user->role === 'admin')` scattered in controllers
34
+ - Generate a Policy: `php artisan make:policy ModelPolicy --model=Model`
35
+ - Use `$this->authorize('action', $model)` or Form Request `authorize()`
36
+ - See: `LV005-policies-gates-authorization.md`
37
+
38
+ ---
39
+
40
+ ## Architecture Patterns
41
+
42
+ ### Controller responsibility
43
+ Controllers do exactly 3 things: validate input → call service → return response.
44
+ No business logic, no DB queries, no calculations in controllers.
45
+ ```php
46
+ // Controller
47
+ public function store(StoreOrderRequest $request): JsonResponse
48
+ {
49
+ $order = $this->orderService->placeOrder($request->user(), $request->validated());
50
+ return OrderResource::make($order)->response()->setStatusCode(201);
51
+ }
52
+ ```
53
+ See: `LV012-service-layer.md`
54
+
55
+ ### API Responses
56
+ - Always use `php artisan make:resource` → never `->toArray()`, `->toJson()` or raw `response()->json($model)`
57
+ - Sensitive columns (`password`, `remember_token`) must be in `$hidden` on the model
58
+ - See: `LV009-api-resources.md`
59
+
60
+ ### Configuration
61
+ - `env()` appears **only** in `config/*.php` files — never in app code (breaks after `config:cache`)
62
+ - App code always uses `config('key.sub_key')` with optional default
63
+ - See: `LV003-config-not-env.md`
64
+
65
+ ### Heavy Operations (email, API calls, reports)
66
+ - Any external call or operation > ~200ms → `Job::dispatch()->onQueue('name')`
67
+ - Job class implements `ShouldQueue`, handles `failed()` method
68
+ - See: `LV006-queue-heavy-tasks.md`
69
+
70
+ ### Large Data Sets
71
+ - No `Model::all()` or unbounded `->get()` in commands/jobs
72
+ - Use `->chunkById(500, fn($batch) => ...)` or `->cursor()` for iteration
73
+ - See: `LV010-chunk-large-datasets.md`
74
+
75
+ ### Multi-step Writes
76
+ - Any sequence of 2+ DB writes that must be atomic → wrap in `DB::transaction(fn() => { ... })`
77
+ - dispatch jobs inside transactions: `Job::dispatch($model)->afterCommit()`
78
+ - See: `LV011-db-transactions.md`
79
+
80
+ ### Route Model Binding
81
+ - Route params matching a Model name auto-resolve — no manual `findOrFail()`
82
+ - `Route::get('/posts/{post}', ...)` → controller receives `Post $post` directly
83
+ - See: `LV008-route-model-binding.md`
84
+
85
+ ### Dependency Injection
86
+ - Register services in `AppServiceProvider::register()` as singletons/bindings
87
+ - Controllers receive via constructor — never `new Service()` inside a method
88
+ - See: `LV014-service-container.md`
89
+
90
+ ---
91
+
92
+ ## Run Commands
93
+
94
+ ```bash
95
+ php artisan make:request StoreXxxRequest # validation
96
+ php artisan make:policy XxxPolicy --model=Xxx
97
+ php artisan make:resource XxxResource
98
+ php artisan make:job ProcessXxx
99
+ php artisan make:service XxxService # (if using stubs)
100
+
101
+ php artisan test --filter ClassName # run specific test
102
+ php artisan test --coverage # coverage report
103
+ php artisan config:cache # validate no env() in app code
104
+ php artisan route:list --path=api # audit routes
105
+ php artisan telescope:install # install query/request debugger
106
+ ```
107
+
108
+ ---
109
+
110
+ ## What NOT to do (quick reference)
111
+
112
+ | ❌ Wrong | ✅ Correct |
113
+ |---|---|
114
+ | `$request->validate([...])` in controller | `FormRequest` class |
115
+ | `$request->all()` | `$request->validated()` |
116
+ | `Post::all()` | `Post::with([...])->paginate(20)` |
117
+ | `$post->comments->count()` in loop | `Post::withCount('comments')` |
118
+ | `env('KEY')` in service | `config('prefix.key')` |
119
+ | `$guarded = []` | `$fillable = ['col1', 'col2']` |
120
+ | `if ($user->role === 'admin')` | `$this->authorize('action', $model)` |
121
+ | `md5($password)` | `Hash::make($password)` |
122
+ | `response()->json($model)` | `ModelResource::make($model)` |
123
+ | `Mail::send()` in controller | `SendMailJob::dispatch()->onQueue(...)` |
124
+ | `new OrderService()` in controller | Constructor DI auto-resolved by container |
@@ -0,0 +1,64 @@
1
+ ---
2
+ title: Use Form Request Classes for Validation
3
+ impact: HIGH
4
+ impactDescription: keeps controllers thin, centralizes validation logic and authorization, enables reuse
5
+ tags: validation, form-request, controller, laravel, clean-architecture
6
+ ---
7
+
8
+ ## Use Form Request Classes for Validation
9
+
10
+ Putting `$request->validate()` directly in controller actions mixes concerns, prevents reuse, and inflates controllers. Form Requests encapsulate validation + authorization as a dedicated class.
11
+
12
+ **Wrong:**
13
+
14
+ ```php
15
+ // Controller bloated with validation logic
16
+ public function store(Request $request)
17
+ {
18
+ $request->validate([
19
+ 'name' => 'required|string|max:255',
20
+ 'email' => 'required|email|unique:users',
21
+ 'age' => 'required|integer|min:18',
22
+ ]);
23
+
24
+ User::create($request->all());
25
+ }
26
+ ```
27
+
28
+ **Correct:**
29
+
30
+ ```bash
31
+ # Generate a Form Request
32
+ php artisan make:request StoreUserRequest
33
+ ```
34
+
35
+ ```php
36
+ // app/Http/Requests/StoreUserRequest.php
37
+ class StoreUserRequest extends FormRequest
38
+ {
39
+ public function authorize(): bool
40
+ {
41
+ return $this->user()->can('create', User::class);
42
+ }
43
+
44
+ public function rules(): array
45
+ {
46
+ return [
47
+ 'name' => 'required|string|max:255',
48
+ 'email' => 'required|email|unique:users',
49
+ 'age' => 'required|integer|min:18',
50
+ ];
51
+ }
52
+ }
53
+
54
+ // Controller is now thin
55
+ public function store(StoreUserRequest $request)
56
+ {
57
+ User::create($request->validated()); // never $request->all()
58
+ }
59
+ ```
60
+
61
+ **Key points:**
62
+ - Always use `$request->validated()` not `$request->all()` — only returns fields that passed validation
63
+ - Authorization logic belongs in `authorize()`, not the controller
64
+ - One Form Request per action (StoreUserRequest, UpdateUserRequest are separate)
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: Eager Load Relationships to Prevent N+1 Queries
3
+ impact: CRITICAL
4
+ impactDescription: prevents N+1 query explosion that causes severe performance degradation in production
5
+ tags: n+1, eager-loading, eloquent, performance, with, laravel
6
+ ---
7
+
8
+ ## Eager Load Relationships to Prevent N+1 Queries
9
+
10
+ Accessing a relationship inside a loop without eager loading fires one query per iteration. With 1000 posts, that is 1001 queries. Always eager load with `with()` when you know you'll access the relationship.
11
+
12
+ **Wrong:**
13
+
14
+ ```php
15
+ // Executes 1 + N queries (one per post)
16
+ $posts = Post::all();
17
+
18
+ foreach ($posts as $post) {
19
+ echo $post->author->name; // <- SELECT * FROM users WHERE id = ? (×N)
20
+ echo $post->category->name; // <- SELECT * FROM categories WHERE id = ? (×N)
21
+ }
22
+ ```
23
+
24
+ **Correct:**
25
+
26
+ ```php
27
+ // Executes exactly 3 queries total
28
+ $posts = Post::with(['author', 'category'])->get();
29
+
30
+ foreach ($posts as $post) {
31
+ echo $post->author->name;
32
+ echo $post->category->name;
33
+ }
34
+
35
+ // Nested relationships
36
+ $posts = Post::with(['author.profile', 'tags'])->get();
37
+
38
+ // Selective columns to reduce memory
39
+ $posts = Post::with(['author:id,name,avatar'])->get();
40
+
41
+ // Conditional eager loading
42
+ $posts = Post::with(['comments' => function ($q) {
43
+ $q->where('approved', true)->latest()->limit(5);
44
+ }])->get();
45
+ ```
46
+
47
+ **Detection signal:**
48
+ - Any `->relationship` access inside a `foreach`, `map`, `each`, or `Collection` method without preceding `with()`
49
+ - Use Laravel Telescope or Debugbar in development to detect N+1 — queries > 10 for a single request is a red flag
50
+
51
+ **Also applies to:**
52
+ ```php
53
+ // WRONG: counting without withCount
54
+ $posts->each(fn($p) => $p->comments->count()); // N queries
55
+
56
+ // CORRECT: withCount
57
+ Post::withCount('comments')->get(); // 1 query
58
+ ```
@@ -0,0 +1,54 @@
1
+ ---
2
+ title: Use config() Helper, Never env() Outside Config Files
3
+ impact: HIGH
4
+ impactDescription: config caching (php artisan config:cache) breaks env() calls at runtime — app silently returns null
5
+ tags: config, env, caching, laravel, environment
6
+ ---
7
+
8
+ ## Use config() Instead of env() Outside Config Files
9
+
10
+ `env()` only works reliably before config is cached. Once you run `php artisan config:cache` (required in production), `env()` returns `null` everywhere except in `config/*.php` files.
11
+
12
+ **Wrong:**
13
+
14
+ ```php
15
+ // Service class — breaks after config:cache
16
+ class PaymentService
17
+ {
18
+ public function charge()
19
+ {
20
+ $key = env('STRIPE_SECRET'); // NULL in production!
21
+ }
22
+ }
23
+
24
+ // Controller
25
+ public function show()
26
+ {
27
+ $debug = env('APP_DEBUG'); // NULL after config:cache
28
+ }
29
+ ```
30
+
31
+ **Correct:**
32
+
33
+ ```php
34
+ // 1. Define in config/services.php
35
+ return [
36
+ 'stripe' => [
37
+ 'secret' => env('STRIPE_SECRET'), // env() ONLY here
38
+ ],
39
+ ];
40
+
41
+ // 2. Use config() everywhere else
42
+ class PaymentService
43
+ {
44
+ public function charge()
45
+ {
46
+ $key = config('services.stripe.secret'); // always works
47
+ }
48
+ }
49
+
50
+ // 3. With default fallback
51
+ $debug = config('app.debug', false);
52
+ ```
53
+
54
+ **Rule:** `env()` appears only in `config/` directory. Everywhere else → `config()`.
@@ -0,0 +1,51 @@
1
+ ---
2
+ title: Define $fillable — Never Use $guarded = []
3
+ impact: CRITICAL
4
+ impactDescription: disabling mass assignment protection allows attackers to write arbitrary model fields via crafted HTTP requests
5
+ tags: mass-assignment, fillable, guarded, security, eloquent, laravel
6
+ ---
7
+
8
+ ## Define $fillable — Never Use $guarded = []
9
+
10
+ Mass assignment attacks occur when a user passes unexpected fields (e.g. `is_admin=1`) that get written to the database. `$guarded = []` disables all protection.
11
+
12
+ **Wrong:**
13
+
14
+ ```php
15
+ class User extends Model
16
+ {
17
+ protected $guarded = []; // All fields writable — DANGEROUS
18
+
19
+ // Or: no $fillable defined at all (same effect with $guarded=[])
20
+ }
21
+
22
+ // Attacker sends: POST /users { "name": "Bob", "is_admin": 1 }
23
+ User::create($request->all()); // is_admin gets written!
24
+ ```
25
+
26
+ **Correct:**
27
+
28
+ ```php
29
+ class User extends Model
30
+ {
31
+ // Explicit allowlist
32
+ protected $fillable = [
33
+ 'name',
34
+ 'email',
35
+ 'password',
36
+ ];
37
+
38
+ // Fields like is_admin, email_verified_at are NOT here
39
+ // They are set explicitly:
40
+ }
41
+
42
+ // Controller
43
+ User::create($request->validated()); // only validated+fillable fields written
44
+
45
+ // When you must set a guarded field, use direct assignment:
46
+ $user = new User($request->validated());
47
+ $user->is_admin = false; // explicit, intentional
48
+ $user->save();
49
+ ```
50
+
51
+ **Also:** use `$request->validated()` not `$request->all()` so only schema-declared fields reach the model.
@@ -0,0 +1,71 @@
1
+ ---
2
+ title: Use Policies and Gates for Authorization
3
+ impact: HIGH
4
+ impactDescription: centralizes authorization logic, prevents scattered role checks, enables testing and auditing
5
+ tags: authorization, policy, gate, roles, security, laravel
6
+ ---
7
+
8
+ ## Use Policies and Gates for Authorization
9
+
10
+ Manual role checks scattered across controllers (`if ($user->role === 'admin')`) are fragile, hard to audit, and often inconsistently applied.
11
+
12
+ **Wrong:**
13
+
14
+ ```php
15
+ // Controller — authorization logic throughout
16
+ public function update(Request $request, Post $post)
17
+ {
18
+ if ($request->user()->id !== $post->user_id) {
19
+ abort(403);
20
+ }
21
+ if ($request->user()->role !== 'editor' && $request->user()->role !== 'admin') {
22
+ abort(403); // duplicated in show, destroy, etc.
23
+ }
24
+ $post->update($request->validated());
25
+ }
26
+ ```
27
+
28
+ **Correct:**
29
+
30
+ ```bash
31
+ php artisan make:policy PostPolicy --model=Post
32
+ ```
33
+
34
+ ```php
35
+ // app/Policies/PostPolicy.php
36
+ class PostPolicy
37
+ {
38
+ public function update(User $user, Post $post): bool
39
+ {
40
+ return $user->id === $post->user_id || $user->isEditor();
41
+ }
42
+
43
+ public function delete(User $user, Post $post): bool
44
+ {
45
+ return $user->id === $post->user_id || $user->isAdmin();
46
+ }
47
+ }
48
+
49
+ // Controller — thin and readable
50
+ public function update(UpdatePostRequest $request, Post $post)
51
+ {
52
+ $this->authorize('update', $post); // throws 403 if denied
53
+
54
+ $post->update($request->validated());
55
+ }
56
+
57
+ // Or in Form Request
58
+ public function authorize(): bool
59
+ {
60
+ return $this->user()->can('update', $this->route('post'));
61
+ }
62
+ ```
63
+
64
+ **Gates for non-model actions:**
65
+ ```php
66
+ // AppServiceProvider
67
+ Gate::define('access-reports', fn(User $u) => $u->hasRole('analyst'));
68
+
69
+ // Anywhere
70
+ if (Gate::denies('access-reports')) abort(403);
71
+ ```