@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.
- package/core/file-targeting-service.js +148 -15
- package/core/init-command.js +118 -70
- package/core/project-detector.js +517 -0
- package/core/tui-select.js +245 -0
- package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
- package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
- package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
- package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
- package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
- package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
- package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
- package/package.json +1 -1
- package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
- package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
- package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
- package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Implement Thread-Safe Singleton Correctly
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: a non-thread-safe singleton can create multiple instances under concurrent load, breaking application invariants
|
|
5
|
+
tags: concurrency, design-pattern, java, thread-safety
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Implement Thread-Safe Singleton Correctly
|
|
9
|
+
|
|
10
|
+
The Singleton pattern ensures a class has only one instance. In multi-threaded Java applications, a naive singleton implementation can create multiple instances when two threads enter `getInstance()` simultaneously. This is a classic race condition.
|
|
11
|
+
|
|
12
|
+
**Incorrect (not thread-safe):**
|
|
13
|
+
|
|
14
|
+
```java
|
|
15
|
+
// Lazy initialization — race condition when two threads call getInstance() simultaneously
|
|
16
|
+
public class ConfigManager {
|
|
17
|
+
private static ConfigManager instance;
|
|
18
|
+
|
|
19
|
+
private ConfigManager() {
|
|
20
|
+
loadConfiguration();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public static ConfigManager getInstance() {
|
|
24
|
+
if (instance == null) { // thread A and B both see null
|
|
25
|
+
instance = new ConfigManager(); // both create an instance — BUG
|
|
26
|
+
}
|
|
27
|
+
return instance;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Incorrect (broken double-checked locking without volatile):**
|
|
33
|
+
|
|
34
|
+
```java
|
|
35
|
+
public class DatabasePool {
|
|
36
|
+
private static DatabasePool instance; // missing volatile — broken!
|
|
37
|
+
|
|
38
|
+
public static DatabasePool getInstance() {
|
|
39
|
+
if (instance == null) {
|
|
40
|
+
synchronized (DatabasePool.class) {
|
|
41
|
+
if (instance == null) {
|
|
42
|
+
instance = new DatabasePool(); // may be visible as partially initialized
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return instance;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Correct (Initialization-on-demand holder idiom — preferred):**
|
|
52
|
+
|
|
53
|
+
```java
|
|
54
|
+
public class ConfigManager {
|
|
55
|
+
private ConfigManager() {
|
|
56
|
+
loadConfiguration();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// JVM class loading guarantees thread-safe, lazy initialization
|
|
60
|
+
private static final class Holder {
|
|
61
|
+
private static final ConfigManager INSTANCE = new ConfigManager();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public static ConfigManager getInstance() {
|
|
65
|
+
return Holder.INSTANCE;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Correct (double-checked locking with volatile — acceptable):**
|
|
71
|
+
|
|
72
|
+
```java
|
|
73
|
+
public class DatabasePool {
|
|
74
|
+
private static volatile DatabasePool instance; // volatile is required
|
|
75
|
+
|
|
76
|
+
private DatabasePool() {}
|
|
77
|
+
|
|
78
|
+
public static DatabasePool getInstance() {
|
|
79
|
+
if (instance == null) {
|
|
80
|
+
synchronized (DatabasePool.class) {
|
|
81
|
+
if (instance == null) {
|
|
82
|
+
instance = new DatabasePool();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return instance;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Correct (enum singleton — simplest and safest):**
|
|
92
|
+
|
|
93
|
+
```java
|
|
94
|
+
// Enum singletons are thread-safe by JVM specification and handle serialization correctly
|
|
95
|
+
public enum AppConfig {
|
|
96
|
+
INSTANCE;
|
|
97
|
+
|
|
98
|
+
private final String dbUrl;
|
|
99
|
+
|
|
100
|
+
AppConfig() {
|
|
101
|
+
dbUrl = System.getenv("DB_URL");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public String getDbUrl() {
|
|
105
|
+
return dbUrl;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Usage:
|
|
110
|
+
AppConfig.INSTANCE.getDbUrl();
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Preferred approaches (in order):**
|
|
114
|
+
1. **Enum singleton** — safest, handles serialization automatically
|
|
115
|
+
2. **Initialization-on-demand holder** — lazy, thread-safe, no synchronization overhead
|
|
116
|
+
3. **Double-checked locking with `volatile`** — acceptable for legacy code
|
|
117
|
+
4. **Spring `@Component` / `@Service`** — let the DI container manage lifecycle (best for application code)
|
|
118
|
+
|
|
119
|
+
**Tools:** PMD (`NonThreadSafeSingleton`, `DoubleCheckedLocking`), FindBugs/SpotBugs (`DC_DOUBLECHECK`, `ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD`), SonarQube (`S2168`)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Utility Classes Must Have a Private Constructor
|
|
3
|
+
impact: LOW
|
|
4
|
+
impactDescription: utility classes with public or default constructors can be accidentally instantiated, wasting memory and exposing confusing API
|
|
5
|
+
tags: design, best-practice, java, clean-code
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Utility Classes Must Have a Private Constructor
|
|
9
|
+
|
|
10
|
+
A **utility class** is a class that consists only of `static` methods and/or constants — it is never meant to be instantiated (e.g., `java.util.Collections`, `java.util.Arrays`, `Math`). Without a private constructor, a utility class:
|
|
11
|
+
- Can be instantiated by mistake, creating useless objects.
|
|
12
|
+
- May confuse callers about whether an instance has state.
|
|
13
|
+
- May be accidentally extended, inheriting a public constructor.
|
|
14
|
+
|
|
15
|
+
**Incorrect:**
|
|
16
|
+
|
|
17
|
+
```java
|
|
18
|
+
// No constructor — Java adds a public default constructor automatically
|
|
19
|
+
public class StringUtils {
|
|
20
|
+
public static String capitalize(String s) { ... }
|
|
21
|
+
public static boolean isBlank(String s) { ... }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Default-visibility constructor — package-accessible
|
|
25
|
+
public class MathUtils {
|
|
26
|
+
MathUtils() {} // package-private, should be private
|
|
27
|
+
public static int add(int a, int b) { return a + b; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Can be instantiated:
|
|
31
|
+
StringUtils utils = new StringUtils(); // meaningless object
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Correct:**
|
|
35
|
+
|
|
36
|
+
```java
|
|
37
|
+
public final class StringUtils { // final prevents subclassing
|
|
38
|
+
private StringUtils() {
|
|
39
|
+
// Utility class — do not instantiate
|
|
40
|
+
throw new UnsupportedOperationException("Utility class cannot be instantiated");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public static String capitalize(String s) {
|
|
44
|
+
if (s == null || s.isEmpty()) return s;
|
|
45
|
+
return Character.toUpperCase(s.charAt(0)) + s.substring(1).toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public static boolean isBlank(String s) {
|
|
49
|
+
return s == null || s.trim().isEmpty();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public final class DateUtils {
|
|
54
|
+
private DateUtils() {
|
|
55
|
+
throw new UnsupportedOperationException("Utility class");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public static LocalDate parseDate(String date) {
|
|
59
|
+
return LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Using Lombok `@UtilityClass`:**
|
|
65
|
+
|
|
66
|
+
```java
|
|
67
|
+
import lombok.experimental.UtilityClass;
|
|
68
|
+
|
|
69
|
+
@UtilityClass // auto-generates private constructor + throws exception + makes class final
|
|
70
|
+
public class ValidationUtils {
|
|
71
|
+
public boolean isValidEmail(String email) {
|
|
72
|
+
return email != null && email.matches("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Conditions for a utility class:**
|
|
78
|
+
- All methods are `static`
|
|
79
|
+
- No instance fields (or only `static final` constants)
|
|
80
|
+
- The class is not designed as a base class
|
|
81
|
+
|
|
82
|
+
**Tools:** Checkstyle (`HideUtilityClassConstructor`), PMD (`UseUtilityClass`), SonarQube (`S1118`)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Preserve Stack Trace When Rethrowing Exceptions
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: discarding the original exception's stack trace makes debugging nearly impossible — the root cause is permanently lost
|
|
5
|
+
tags: error-handling, best-practice, java, debugging
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Preserve Stack Trace When Rethrowing Exceptions
|
|
9
|
+
|
|
10
|
+
When catching an exception and throwing a new one, always pass the original exception as the **cause**. If you only rethrow a new exception with `e.getMessage()` or no cause at all, the original stack trace — which shows where the error actually originated — is permanently discarded.
|
|
11
|
+
|
|
12
|
+
**Incorrect (losing the stack trace):**
|
|
13
|
+
|
|
14
|
+
```java
|
|
15
|
+
public void processOrder(Order order) throws OrderException {
|
|
16
|
+
try {
|
|
17
|
+
paymentService.charge(order.getPayment());
|
|
18
|
+
} catch (PaymentException e) {
|
|
19
|
+
// Message-only: root cause stack trace is lost
|
|
20
|
+
throw new OrderException("Payment failed: " + e.getMessage());
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public User loadUser(Long id) throws ServiceException {
|
|
25
|
+
try {
|
|
26
|
+
return userRepository.findById(id).orElseThrow();
|
|
27
|
+
} catch (Exception e) {
|
|
28
|
+
// No cause: impossible to trace back to the original error
|
|
29
|
+
throw new ServiceException("User not found");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public void sendEmail(String to, String body) {
|
|
34
|
+
try {
|
|
35
|
+
emailClient.send(to, body);
|
|
36
|
+
} catch (EmailException e) {
|
|
37
|
+
logger.error("Email failed"); // No exception logged — stack trace lost
|
|
38
|
+
throw new RuntimeException("Email failed"); // No cause chained
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Correct (preserving the stack trace):**
|
|
44
|
+
|
|
45
|
+
```java
|
|
46
|
+
public void processOrder(Order order) throws OrderException {
|
|
47
|
+
try {
|
|
48
|
+
paymentService.charge(order.getPayment());
|
|
49
|
+
} catch (PaymentException e) {
|
|
50
|
+
// Pass the original exception as the cause
|
|
51
|
+
throw new OrderException("Payment failed for orderId=" + order.getId(), e);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public User loadUser(Long id) throws ServiceException {
|
|
56
|
+
try {
|
|
57
|
+
return userRepository.findById(id)
|
|
58
|
+
.orElseThrow(() -> new UserNotFoundException("User not found: id=" + id));
|
|
59
|
+
} catch (UserNotFoundException e) {
|
|
60
|
+
throw new ServiceException("User lookup failed: id=" + id, e); // chained
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public void sendEmail(String to, String body) {
|
|
65
|
+
try {
|
|
66
|
+
emailClient.send(to, body);
|
|
67
|
+
} catch (EmailException e) {
|
|
68
|
+
logger.error("Email sending failed: recipient={}", to, e); // log with cause
|
|
69
|
+
throw new NotificationException("Email failed", e); // chain the cause
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Using initCause for exceptions without cause constructors:**
|
|
75
|
+
|
|
76
|
+
```java
|
|
77
|
+
try {
|
|
78
|
+
someOperation();
|
|
79
|
+
} catch (SomeException e) {
|
|
80
|
+
RuntimeException wrapped = new RuntimeException("Operation failed");
|
|
81
|
+
wrapped.initCause(e); // alternative to constructor argument
|
|
82
|
+
throw wrapped;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Inspecting cause chains:**
|
|
87
|
+
|
|
88
|
+
```java
|
|
89
|
+
// Cause chain is accessible at runtime
|
|
90
|
+
try {
|
|
91
|
+
processOrder(order);
|
|
92
|
+
} catch (OrderException e) {
|
|
93
|
+
Throwable cause = e.getCause(); // retrieves PaymentException
|
|
94
|
+
logger.error("Root cause: {}", cause.getMessage());
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Adding suppressed exceptions (try-with-resources pattern):**
|
|
99
|
+
|
|
100
|
+
```java
|
|
101
|
+
// When closing a resource also throws — use addSuppressed
|
|
102
|
+
Exception primary = null;
|
|
103
|
+
try {
|
|
104
|
+
doWork();
|
|
105
|
+
} catch (Exception e) {
|
|
106
|
+
primary = e;
|
|
107
|
+
throw e;
|
|
108
|
+
} finally {
|
|
109
|
+
try {
|
|
110
|
+
resource.close();
|
|
111
|
+
} catch (Exception closeEx) {
|
|
112
|
+
if (primary != null) {
|
|
113
|
+
primary.addSuppressed(closeEx); // preserves both
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Tools:** PMD (`PreserveStackTrace`), SpotBugs (`REC_CATCH_EXCEPTION`), SonarQube (`S1166`)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Put Literals First in String Comparisons to Avoid NullPointerException
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: variable.equals("literal") throws NPE if variable is null; reversing operands makes comparisons null-safe without extra null checks
|
|
5
|
+
tags: null-safety, best-practice, java, error-prone
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Put Literals First in String Comparisons to Avoid NullPointerException
|
|
9
|
+
|
|
10
|
+
When comparing a string variable to a known literal value, placing the literal on the **left** side of `.equals()` prevents a `NullPointerException` if the variable is `null`. Since a string literal is never `null`, calling `.equals()` on it is always safe.
|
|
11
|
+
|
|
12
|
+
**Incorrect (variable first — may throw NPE):**
|
|
13
|
+
|
|
14
|
+
```java
|
|
15
|
+
public boolean isActive(String status) {
|
|
16
|
+
return status.equals("ACTIVE"); // NPE if status is null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public void handleRequest(HttpServletRequest request) {
|
|
20
|
+
String method = request.getMethod();
|
|
21
|
+
if (method.equals("POST")) { // NPE if method is null (unlikely but possible)
|
|
22
|
+
processPost();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public boolean checkRole(User user) {
|
|
27
|
+
String role = user.getRole(); // getRole() might return null
|
|
28
|
+
return role.equals("ADMIN"); // NPE
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Correct (literal first — null-safe):**
|
|
33
|
+
|
|
34
|
+
```java
|
|
35
|
+
public boolean isActive(String status) {
|
|
36
|
+
return "ACTIVE".equals(status); // returns false if status is null — no NPE
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public void handleRequest(HttpServletRequest request) {
|
|
40
|
+
String method = request.getMethod();
|
|
41
|
+
if ("POST".equals(method)) { // safe even if method is null
|
|
42
|
+
processPost();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public boolean checkRole(User user) {
|
|
47
|
+
String role = user.getRole();
|
|
48
|
+
return "ADMIN".equals(role); // null-safe
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Also applies to equalsIgnoreCase:
|
|
52
|
+
public boolean isAdminIgnoreCase(String role) {
|
|
53
|
+
return "admin".equalsIgnoreCase(role); // null-safe
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Constant comparison:
|
|
57
|
+
public static final String DEFAULT_LANG = "en";
|
|
58
|
+
|
|
59
|
+
public boolean isDefaultLanguage(String lang) {
|
|
60
|
+
return DEFAULT_LANG.equals(lang); // constant on left — same principle
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**When to use Objects.equals() instead:**
|
|
65
|
+
|
|
66
|
+
Use `Objects.equals(a, b)` when **both** values may be `null` and you want a symmetric comparison:
|
|
67
|
+
|
|
68
|
+
```java
|
|
69
|
+
// Objects.equals handles both nulls:
|
|
70
|
+
boolean same = Objects.equals(user.getRole(), adminRole); // null-safe on both sides
|
|
71
|
+
|
|
72
|
+
// Equivalent to:
|
|
73
|
+
// user.getRole() == adminRole || (user.getRole() != null && user.getRole().equals(adminRole))
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**compareTo / compareToIgnoreCase:**
|
|
77
|
+
|
|
78
|
+
Note that reversing the literal changes the sign of the result:
|
|
79
|
+
|
|
80
|
+
```java
|
|
81
|
+
// Original: x.compareTo("bar") > 0
|
|
82
|
+
// Reversed: "bar".compareTo(x) < 0 ← sign flipped!
|
|
83
|
+
|
|
84
|
+
// Be careful when converting compareTo usage
|
|
85
|
+
if ("bar".compareTo(x) < 0) { ... } // equivalent to x.compareTo("bar") > 0
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Tools:** PMD (`LiteralsFirstInComparisons`), SonarQube (`S1132`), IntelliJ Inspections
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use EnumSet and EnumMap for Enum Keys
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: EnumSet and EnumMap use array-backed implementations that are significantly faster and more memory-efficient than HashSet/HashMap for enum keys
|
|
5
|
+
tags: performance, best-practice, java, collections
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use EnumSet and EnumMap for Enum Keys
|
|
9
|
+
|
|
10
|
+
When working with collections whose elements or keys are enum values, `EnumSet` and `EnumMap` should be preferred over `HashSet` and `HashMap`. They use a compact array-backed representation internally — bit vectors for `EnumSet` and an array indexed by ordinal for `EnumMap` — providing better performance and less memory usage.
|
|
11
|
+
|
|
12
|
+
**Incorrect (using general-purpose collections with enum keys):**
|
|
13
|
+
|
|
14
|
+
```java
|
|
15
|
+
public enum Permission { READ, WRITE, DELETE, ADMIN }
|
|
16
|
+
|
|
17
|
+
public class User {
|
|
18
|
+
// Using HashSet for enum values — wastes memory, slower
|
|
19
|
+
private Set<Permission> permissions = new HashSet<>();
|
|
20
|
+
|
|
21
|
+
public boolean hasPermission(Permission p) {
|
|
22
|
+
return permissions.contains(p); // HashSet lookup — unnecessary overhead
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public class RolePolicy {
|
|
27
|
+
// Using HashMap for enum keys — suboptimal
|
|
28
|
+
private Map<Permission, String> descriptions = new HashMap<>();
|
|
29
|
+
|
|
30
|
+
public void setupDescriptions() {
|
|
31
|
+
descriptions.put(Permission.READ, "Can read resources");
|
|
32
|
+
descriptions.put(Permission.WRITE, "Can write resources");
|
|
33
|
+
descriptions.put(Permission.ADMIN, "Full access");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Correct (using EnumSet / EnumMap):**
|
|
39
|
+
|
|
40
|
+
```java
|
|
41
|
+
import java.util.EnumSet;
|
|
42
|
+
import java.util.EnumMap;
|
|
43
|
+
|
|
44
|
+
public enum Permission { READ, WRITE, DELETE, ADMIN }
|
|
45
|
+
|
|
46
|
+
public class User {
|
|
47
|
+
// EnumSet: compact bit-vector implementation, O(1) operations
|
|
48
|
+
private Set<Permission> permissions = EnumSet.noneOf(Permission.class);
|
|
49
|
+
|
|
50
|
+
public void grantPermission(Permission p) {
|
|
51
|
+
permissions.add(p);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public boolean hasPermission(Permission p) {
|
|
55
|
+
return permissions.contains(p);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public class RolePolicy {
|
|
60
|
+
// EnumMap: array-backed, indexed by enum ordinal
|
|
61
|
+
private Map<Permission, String> descriptions = new EnumMap<>(Permission.class);
|
|
62
|
+
|
|
63
|
+
public void setupDescriptions() {
|
|
64
|
+
descriptions.put(Permission.READ, "Can read resources");
|
|
65
|
+
descriptions.put(Permission.WRITE, "Can write resources");
|
|
66
|
+
descriptions.put(Permission.ADMIN, "Full access");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**EnumSet factory methods:**
|
|
72
|
+
|
|
73
|
+
```java
|
|
74
|
+
// Empty set
|
|
75
|
+
Set<Permission> none = EnumSet.noneOf(Permission.class);
|
|
76
|
+
|
|
77
|
+
// All values
|
|
78
|
+
Set<Permission> all = EnumSet.allOf(Permission.class);
|
|
79
|
+
|
|
80
|
+
// Specific values
|
|
81
|
+
Set<Permission> readOnly = EnumSet.of(Permission.READ);
|
|
82
|
+
|
|
83
|
+
// Range (by ordinal order)
|
|
84
|
+
Set<Permission> basic = EnumSet.range(Permission.READ, Permission.WRITE);
|
|
85
|
+
|
|
86
|
+
// Complement
|
|
87
|
+
Set<Permission> nonAdmin = EnumSet.complementOf(EnumSet.of(Permission.ADMIN));
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Performance comparison:**
|
|
91
|
+
|
|
92
|
+
| Operation | HashSet | EnumSet |
|
|
93
|
+
|-----------|---------|---------|
|
|
94
|
+
| `add` | O(1) avg | O(1) bit op |
|
|
95
|
+
| `contains` | O(1) avg | O(1) bit op |
|
|
96
|
+
| Memory | ~40 bytes/entry | 1 bit/entry |
|
|
97
|
+
| Iteration | Hash order | Enum ordinal order |
|
|
98
|
+
|
|
99
|
+
**When to apply:**
|
|
100
|
+
- Use `EnumSet` whenever you have a `Set<SomeEnum>`
|
|
101
|
+
- Use `EnumMap` whenever you have a `Map<SomeEnum, V>`
|
|
102
|
+
- Both are drop-in replacements that implement `Set` and `Map` respectively
|
|
103
|
+
|
|
104
|
+
**Tools:** PMD (`UseEnumCollections`), IntelliJ Inspections
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Return Empty Collection Instead of null
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: returning null for collections forces every caller to perform a null check, causing NullPointerExceptions when they forget
|
|
5
|
+
tags: null-safety, design, java, clean-code
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Return Empty Collection Instead of null
|
|
9
|
+
|
|
10
|
+
Methods that return collections (`List`, `Set`, `Map`, arrays) should **never** return `null` to indicate "no results." Instead, return an empty collection. This eliminates the need for null checks on every call site, reduces potential `NullPointerException` bugs, and makes the API safer and more predictable.
|
|
11
|
+
|
|
12
|
+
This principle is described in *Effective Java* by Joshua Bloch: *"Never return null in place of an empty array or collection."*
|
|
13
|
+
|
|
14
|
+
**Incorrect (returning null):**
|
|
15
|
+
|
|
16
|
+
```java
|
|
17
|
+
public List<Order> getOrdersByUser(Long userId) {
|
|
18
|
+
List<Order> orders = orderRepository.findByUserId(userId);
|
|
19
|
+
if (orders.isEmpty()) {
|
|
20
|
+
return null; // forces callers to null-check
|
|
21
|
+
}
|
|
22
|
+
return orders;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public String[] getTagsForPost(Long postId) {
|
|
26
|
+
String[] tags = tagRepository.findByPost(postId);
|
|
27
|
+
if (tags == null || tags.length == 0) {
|
|
28
|
+
return null; // callers must check for null before iterating
|
|
29
|
+
}
|
|
30
|
+
return tags;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Callers must remember to null-check — easy to forget:
|
|
34
|
+
List<Order> orders = orderService.getOrdersByUser(userId);
|
|
35
|
+
for (Order order : orders) { // NPE if orders is null
|
|
36
|
+
processOrder(order);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Correct (returning empty collections):**
|
|
41
|
+
|
|
42
|
+
```java
|
|
43
|
+
import java.util.Collections;
|
|
44
|
+
import java.util.List;
|
|
45
|
+
|
|
46
|
+
public List<Order> getOrdersByUser(Long userId) {
|
|
47
|
+
List<Order> orders = orderRepository.findByUserId(userId);
|
|
48
|
+
if (orders == null || orders.isEmpty()) {
|
|
49
|
+
return Collections.emptyList(); // immutable, no allocation overhead
|
|
50
|
+
}
|
|
51
|
+
return orders;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Or with Stream API:
|
|
55
|
+
public List<Order> getOrdersByUser(Long userId) {
|
|
56
|
+
return orderRepository.findByUserId(userId) != null
|
|
57
|
+
? orderRepository.findByUserId(userId)
|
|
58
|
+
: Collections.emptyList();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Using List.of() (Java 9+):
|
|
62
|
+
public List<String> getTagsForPost(Long postId) {
|
|
63
|
+
List<String> tags = tagRepository.findByPost(postId);
|
|
64
|
+
return tags != null ? tags : List.of(); // immutable empty list
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Arrays:
|
|
68
|
+
public String[] getPermissionsForRole(String role) {
|
|
69
|
+
String[] perms = permissionRepo.findByRole(role);
|
|
70
|
+
return perms != null ? perms : new String[0]; // empty array, not null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Callers now iterate safely without null checks:
|
|
74
|
+
List<Order> orders = orderService.getOrdersByUser(userId);
|
|
75
|
+
for (Order order : orders) { // safe — at worst iterates 0 times
|
|
76
|
+
processOrder(order);
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Preferred empty collection factories:**
|
|
81
|
+
|
|
82
|
+
| Type | Factory |
|
|
83
|
+
|------|---------|
|
|
84
|
+
| `List<T>` | `Collections.emptyList()` or `List.of()` (Java 9+) |
|
|
85
|
+
| `Set<T>` | `Collections.emptySet()` or `Set.of()` |
|
|
86
|
+
| `Map<K,V>` | `Collections.emptyMap()` or `Map.of()` |
|
|
87
|
+
| `T[]` | `new T[0]` |
|
|
88
|
+
|
|
89
|
+
**Note:** `Collections.emptyList()`, `emptySet()`, and `emptyMap()` return immutable, singleton instances — no memory allocation per call. They are the most efficient choice.
|
|
90
|
+
|
|
91
|
+
**Exception — when null has semantic meaning:**
|
|
92
|
+
If `null` and "empty" are **intentionally different states** (e.g., "not loaded yet" vs "loaded but empty"), `Optional<List<T>>` or a domain wrapper class should be used instead of `null`.
|
|
93
|
+
|
|
94
|
+
```java
|
|
95
|
+
// Distinguish "not configured" from "configured but empty"
|
|
96
|
+
public Optional<List<String>> getWhitelist(String service) {
|
|
97
|
+
if (!config.hasWhitelist(service)) return Optional.empty();
|
|
98
|
+
return Optional.of(config.getWhitelist(service));
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Tools:** PMD (`ReturnEmptyCollectionRatherThanNull`), SonarQube (`S1168`), IntelliJ Inspections
|