@sun-asterisk/sunlint 1.3.47 → 1.3.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/rules/rules-registry-generated.json +1717 -282
- package/core/architecture-integration.js +57 -15
- package/core/cli-action-handler.js +51 -36
- package/core/config-manager.js +6 -0
- package/core/config-merger.js +33 -0
- package/core/config-validator.js +37 -2
- package/core/file-targeting-service.js +148 -15
- package/core/init-command.js +118 -70
- package/core/output-service.js +12 -3
- package/core/project-detector.js +517 -0
- package/core/scoring-service.js +12 -6
- package/core/summary-report-service.js +9 -4
- 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/engines/impact/cli.js +54 -39
- package/engines/impact/config/default-config.js +105 -5
- package/engines/impact/core/impact-analyzer.js +12 -15
- package/engines/impact/core/utils/gitignore-parser.js +123 -0
- package/engines/impact/core/utils/method-call-graph.js +272 -87
- package/origin-rules/dart-en.md +1 -1
- package/origin-rules/go-en.md +231 -0
- package/origin-rules/php-en.md +107 -0
- package/origin-rules/python-en.md +113 -0
- package/origin-rules/ruby-en.md +607 -0
- package/package.json +1 -1
- package/scripts/copy-arch-detect.js +5 -1
- package/scripts/copy-impact-analyzer.js +5 -1
- package/scripts/generate-rules-registry.js +30 -14
- package/skill-assets/sunlint-code-quality/SKILL.md +3 -2
- 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/G001-explicit-error-handling.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/go/G002-context-first-argument.md +44 -0
- package/skill-assets/sunlint-code-quality/rules/go/G003-receiver-consistency.md +38 -0
- package/skill-assets/sunlint-code-quality/rules/go/G004-avoid-panic.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/G005-goroutine-leak-prevention.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/G006-interface-consumer-definition.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN001-gin-binding-validation.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN002-gin-error-response.md +48 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN003-graceful-shutdown.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN004-gin-route-logical-grouping.md +54 -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,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN008 – Use Struct Validation Binding Tags"
|
|
3
|
+
impact: medium
|
|
4
|
+
impactDescription: "Manual if/else validation in handler code is incomplete, inconsistent, and untested; binding tags enforce validation at the deserialization boundary."
|
|
5
|
+
tags: [go, gin, validation, correctness]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN008 – Use Struct Validation Binding Tags
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Define all input validation rules using `binding:` struct tags on request structs. Do not write manual validation logic in handler functions. Handle binding errors as described in GN003.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Gin uses the `go-playground/validator` library under the hood for `ShouldBind*`. Struct tags declare constraints once, close to the field, and are validated automatically when binding. Scattered `if req.Age < 18` checks in handlers are often incomplete and inconsistent.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
type CreateUserRequest struct {
|
|
22
|
+
Name string `json:"name"`
|
|
23
|
+
Email string `json:"email"`
|
|
24
|
+
Age int `json:"age"`
|
|
25
|
+
Role string `json:"role"`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Handler with manual validation — verbose, incomplete, inconsistent
|
|
29
|
+
func (h *UserHandler) Create(c *gin.Context) {
|
|
30
|
+
var req CreateUserRequest
|
|
31
|
+
c.ShouldBindJSON(&req)
|
|
32
|
+
|
|
33
|
+
if req.Name == "" {
|
|
34
|
+
c.JSON(400, gin.H{"error": "name required"}) // ❌ still runs if bind errored
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
if !strings.Contains(req.Email, "@") { // ❌ incomplete email check
|
|
38
|
+
c.JSON(400, gin.H{"error": "invalid email"})
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
// ❌ forgot to validate Age and Role
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Correct
|
|
46
|
+
|
|
47
|
+
```go
|
|
48
|
+
type CreateUserRequest struct {
|
|
49
|
+
Name string `json:"name" binding:"required,min=2,max=100"`
|
|
50
|
+
Email string `json:"email" binding:"required,email"`
|
|
51
|
+
Age int `json:"age" binding:"required,min=18,max=120"`
|
|
52
|
+
Role string `json:"role" binding:"required,oneof=user admin moderator"`
|
|
53
|
+
Phone string `json:"phone" binding:"omitempty,e164"` // optional but validated if present
|
|
54
|
+
Website string `json:"website" binding:"omitempty,url"`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func (h *UserHandler) Create(c *gin.Context) {
|
|
58
|
+
var req CreateUserRequest
|
|
59
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
60
|
+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
|
61
|
+
"error": "validation failed",
|
|
62
|
+
"details": formatValidationErrors(err), // see note below
|
|
63
|
+
})
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
// req is fully validated here — safe to use
|
|
67
|
+
user, err := h.service.CreateUser(c.Request.Context(), req)
|
|
68
|
+
// ...
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Common binding tags
|
|
73
|
+
|
|
74
|
+
| Tag | Meaning |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `required` | Field must be present and non-zero |
|
|
77
|
+
| `omitempty` | Skip validation if field is absent/zero |
|
|
78
|
+
| `min=N,max=M` | Min/max length for strings, min/max value for numbers |
|
|
79
|
+
| `email` | Valid email address format |
|
|
80
|
+
| `url` | Valid URL |
|
|
81
|
+
| `oneof=a b c` | Value must be one of the listed options |
|
|
82
|
+
| `uuid4` | Valid UUID v4 |
|
|
83
|
+
| `e164` | Valid international phone number |
|
|
84
|
+
| `gt=0` | Greater than 0 (useful for IDs) |
|
|
85
|
+
|
|
86
|
+
## Notes
|
|
87
|
+
|
|
88
|
+
- To return human-readable errors, type-assert `err` to `validator.ValidationErrors` and format field names.
|
|
89
|
+
- Custom validators: `binding.Validator.RegisterValidation("custom_rule", myFunc)`.
|
|
90
|
+
- For query parameters: `c.ShouldBindQuery(&req)` with `form:"field" binding:"required"` tags.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN009 – Add gin.Recovery() Middleware in Production"
|
|
3
|
+
impact: high
|
|
4
|
+
impactDescription: "Without Recovery(), an unhandled panic terminates the goroutine and returns a broken TCP connection or empty response to the client; with it, the server stays up and returns 500."
|
|
5
|
+
tags: [go, gin, reliability, middleware]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN009 – Add `gin.Recovery()` Middleware in Production
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Always include `gin.Recovery()` (or a custom recovery middleware) in the production middleware stack. Use `gin.New()` rather than `gin.Default()` and add middleware explicitly so you control what runs.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Go panics propagate up the call stack and terminate the goroutine. In a Gin HTTP handler, an unrecovered panic kills the request-handling goroutine and the server returns a broken connection. `gin.Recovery()` catches the panic, logs a stack trace, and returns `500 Internal Server Error`, keeping the server alive.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
func main() {
|
|
22
|
+
r := gin.New() // ❌ no Recovery — a panic crashes the goroutine silently
|
|
23
|
+
r.GET("/users/:id", userHandler.Get)
|
|
24
|
+
r.Run(":8080")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// gin.Default() adds Logger + Recovery but also adds noise to structured log setups
|
|
28
|
+
r := gin.Default() // acceptable but not recommended for production structured logging
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Correct
|
|
32
|
+
|
|
33
|
+
```go
|
|
34
|
+
func main() {
|
|
35
|
+
gin.SetMode(os.Getenv("GIN_MODE")) // see GN007
|
|
36
|
+
|
|
37
|
+
r := gin.New()
|
|
38
|
+
|
|
39
|
+
// Custom structured logger (replace gin.Logger())
|
|
40
|
+
r.Use(RequestLogger(logger))
|
|
41
|
+
|
|
42
|
+
// Recovery — MUST be registered before route handlers
|
|
43
|
+
r.Use(gin.RecoveryWithWriter(gin.DefaultErrorWriter))
|
|
44
|
+
// OR a custom recovery that logs stack traces to your logger:
|
|
45
|
+
r.Use(CustomRecovery(logger))
|
|
46
|
+
|
|
47
|
+
registerRoutes(r)
|
|
48
|
+
r.Run(":" + cfg.Port)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Custom recovery middleware with structured logging
|
|
52
|
+
func CustomRecovery(logger *slog.Logger) gin.HandlerFunc {
|
|
53
|
+
return gin.CustomRecoveryWithWriter(gin.DefaultErrorWriter, func(c *gin.Context, recovered any) {
|
|
54
|
+
logger.Error("panic recovered",
|
|
55
|
+
slog.Any("error", recovered),
|
|
56
|
+
slog.String("path", c.Request.URL.Path),
|
|
57
|
+
slog.String("method", c.Request.Method),
|
|
58
|
+
)
|
|
59
|
+
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Notes
|
|
65
|
+
|
|
66
|
+
- Recovery middleware must be registered **before** route handlers — Gin runs middleware in registration order.
|
|
67
|
+
- Don't expose the panic value or stack trace in the HTTP response — log it server-side only.
|
|
68
|
+
- For observability, send panic events to your error tracker (Sentry, Rollbar) inside the custom recovery handler.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN010 – Do Not Store gin.Context in Goroutines Beyond the Handler"
|
|
3
|
+
impact: high
|
|
4
|
+
impactDescription: "gin.Context is recycled after the handler returns; storing it in a goroutine creates a data race on pool-recycled memory."
|
|
5
|
+
tags: [go, gin, concurrency, correctness]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN010 – Do Not Store `gin.Context` in Goroutines Beyond the Handler
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Never pass `*gin.Context` to a goroutine that outlives the handler function. Copy the values you need (request data, context) before spawning the goroutine.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Gin uses `sync.Pool` to reuse `*gin.Context` instances for performance. Once the handler function returns, Gin resets and recycles the context object. A goroutine holding a reference to the old `*gin.Context` now reads from a recycled object used by a completely different request — this is an undetected data race and a source of subtle, hard-to-reproduce bugs.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
func (h *NotificationHandler) Send(c *gin.Context) {
|
|
22
|
+
var req SendNotificationRequest
|
|
23
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
24
|
+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ❌ goroutine holds *gin.Context — races after handler returns
|
|
29
|
+
go func() {
|
|
30
|
+
userID := c.GetString("user_id") // c is already recycled here
|
|
31
|
+
h.service.Notify(context.Background(), userID, req)
|
|
32
|
+
}()
|
|
33
|
+
|
|
34
|
+
c.JSON(http.StatusAccepted, gin.H{"status": "queued"})
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Correct
|
|
39
|
+
|
|
40
|
+
```go
|
|
41
|
+
func (h *NotificationHandler) Send(c *gin.Context) {
|
|
42
|
+
var req SendNotificationRequest
|
|
43
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
44
|
+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ✅ Copy values from context BEFORE spawning the goroutine
|
|
49
|
+
userID := c.GetString("user_id") // copy string value
|
|
50
|
+
ctx := c.Request.Context() // copy the stdlib context (safe to pass to goroutine)
|
|
51
|
+
|
|
52
|
+
go func() {
|
|
53
|
+
// Use copied values — no reference to *gin.Context
|
|
54
|
+
if err := h.service.Notify(ctx, userID, req); err != nil {
|
|
55
|
+
h.logger.Error("notification failed", slog.String("user_id", userID), slog.Any("error", err))
|
|
56
|
+
}
|
|
57
|
+
}()
|
|
58
|
+
|
|
59
|
+
c.JSON(http.StatusAccepted, gin.H{"status": "queued"})
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Notes
|
|
64
|
+
|
|
65
|
+
- `c.Request.Context()` is a standard `context.Context` backed by the HTTP request — safe to copy and pass to goroutines.
|
|
66
|
+
- Extract all required values (`c.Param()`, `c.GetHeader()`, `c.Get()`) into local variables before the goroutine.
|
|
67
|
+
- The Go race detector (`go test -race`) will catch this violation in tests if the goroutine runs fast enough — enable it in CI.
|
|
68
|
+
- Prefer `c.Copy()` if you absolutely need a snapshot of the whole context, but this is memory-heavier than copying specific fields.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN011 – Apply Cross-Cutting Concerns via Middleware"
|
|
3
|
+
impact: medium
|
|
4
|
+
impactDescription: "Duplicating auth checks, rate limiting, or logging inside individual handlers makes enforcement inconsistent and maintenance expensive."
|
|
5
|
+
tags: [go, gin, middleware, architecture]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN011 – Apply Cross-Cutting Concerns via Middleware
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Authentication, authorization, rate limiting, request logging, request ID injection, and CORS must be implemented as Gin middleware functions and applied at the router/group level — not duplicated inside individual handlers.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
When authentication logic lives inside each handler, a single missed handler creates a security gap. Middleware ensures uniform enforcement. It also separates concerns: handlers contain only business logic; infrastructure concerns live in middleware.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
// ❌ Auth duplicated in every handler
|
|
22
|
+
func (h *OrderHandler) List(c *gin.Context) {
|
|
23
|
+
token := c.GetHeader("Authorization")
|
|
24
|
+
userID, err := h.auth.ValidateToken(token)
|
|
25
|
+
if err != nil {
|
|
26
|
+
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
// ... business logic
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func (h *OrderHandler) Create(c *gin.Context) {
|
|
33
|
+
token := c.GetHeader("Authorization") // ❌ duplicated
|
|
34
|
+
userID, err := h.auth.ValidateToken(token)
|
|
35
|
+
// ...
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Correct
|
|
40
|
+
|
|
41
|
+
```go
|
|
42
|
+
// middleware/auth.go
|
|
43
|
+
func JWTAuth(tokenValidator TokenValidator) gin.HandlerFunc {
|
|
44
|
+
return func(c *gin.Context) {
|
|
45
|
+
token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
|
|
46
|
+
claims, err := tokenValidator.Validate(token)
|
|
47
|
+
if err != nil {
|
|
48
|
+
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
c.Set("user_id", claims.UserID)
|
|
52
|
+
c.Set("user_role", claims.Role)
|
|
53
|
+
c.Next()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// middleware/request_id.go
|
|
58
|
+
func RequestID() gin.HandlerFunc {
|
|
59
|
+
return func(c *gin.Context) {
|
|
60
|
+
id := c.GetHeader("X-Request-ID")
|
|
61
|
+
if id == "" {
|
|
62
|
+
id = uuid.New().String()
|
|
63
|
+
}
|
|
64
|
+
c.Set("request_id", id)
|
|
65
|
+
c.Header("X-Request-ID", id)
|
|
66
|
+
c.Next()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// main.go — applied at group level (see GN005)
|
|
71
|
+
r := gin.New()
|
|
72
|
+
r.Use(gin.Recovery(), RequestID(), StructuredLogger(logger))
|
|
73
|
+
|
|
74
|
+
api := r.Group("/api/v1")
|
|
75
|
+
api.Use(JWTAuth(tokenValidator))
|
|
76
|
+
api.Use(RateLimit(100, time.Minute))
|
|
77
|
+
|
|
78
|
+
api.GET("/orders", orderHandler.List) // auth + rate limit automatically applied
|
|
79
|
+
api.POST("/orders", orderHandler.Create)
|
|
80
|
+
|
|
81
|
+
// handlers are clean — no auth logic
|
|
82
|
+
func (h *OrderHandler) List(c *gin.Context) {
|
|
83
|
+
userID := c.GetString("user_id") // set by middleware
|
|
84
|
+
// ... just business logic
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Notes
|
|
89
|
+
|
|
90
|
+
- Middleware should only set values in the context and call `c.Next()` or `c.Abort()` — no response writing for non-auth middleware.
|
|
91
|
+
- Return factory functions (`func JWTAuth(dep Dep) gin.HandlerFunc`) so middleware is injectable and testable.
|
|
92
|
+
- Always document what a middleware sets in the context (e.g. `"user_id"`, `"request_id"`) so handlers know what keys are available.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN012 – Do Not Log Request Bodies Containing Sensitive Data"
|
|
3
|
+
impact: high
|
|
4
|
+
impactDescription: "Logging raw request bodies exposes passwords, tokens, credit card numbers, and PII to anyone with log access, violating GDPR, PCI-DSS, and basic security hygiene."
|
|
5
|
+
tags: [go, gin, security, logging]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN012 – Do Not Log Request Bodies Containing Sensitive Data
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Never log the raw HTTP request body. Log only request metadata (method, path, status, duration, request ID). If you must log body fields for debugging, explicitly allowlist safe fields and mask or omit sensitive ones.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Request bodies for auth endpoints contain passwords. Payment endpoints contain card data. Profile endpoints contain PII. Logging bodies stores this data in plaintext log files, log aggregators, and monitoring tools — where it lives far beyond the request lifetime and is often accessible to many people.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
// ❌ Logging the full request body
|
|
22
|
+
func RequestBodyLogger() gin.HandlerFunc {
|
|
23
|
+
return func(c *gin.Context) {
|
|
24
|
+
body, _ := io.ReadAll(c.Request.Body)
|
|
25
|
+
log.Printf("Request body: %s", body) // ❌ logs passwords, tokens, PII
|
|
26
|
+
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
|
|
27
|
+
c.Next()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ❌ Logging bound request struct directly (may contain passwords)
|
|
32
|
+
func (h *AuthHandler) Login(c *gin.Context) {
|
|
33
|
+
var req LoginRequest
|
|
34
|
+
c.ShouldBindJSON(&req)
|
|
35
|
+
log.Printf("Login attempt: %+v", req) // ❌ req.Password logged in plaintext
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Correct
|
|
40
|
+
|
|
41
|
+
```go
|
|
42
|
+
// ✅ Log only safe metadata — never raw body
|
|
43
|
+
func RequestLogger(logger *slog.Logger) gin.HandlerFunc {
|
|
44
|
+
return func(c *gin.Context) {
|
|
45
|
+
start := time.Now()
|
|
46
|
+
c.Next()
|
|
47
|
+
logger.Info("request",
|
|
48
|
+
slog.String("method", c.Request.Method),
|
|
49
|
+
slog.String("path", c.Request.URL.Path),
|
|
50
|
+
slog.Int("status", c.Writer.Status()),
|
|
51
|
+
slog.Duration("duration", time.Since(start)),
|
|
52
|
+
slog.String("ip", c.ClientIP()),
|
|
53
|
+
slog.String("request_id", c.GetString("request_id")),
|
|
54
|
+
// ✅ no body, no headers that might contain auth tokens
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ✅ Log only safe fields from struct - never password/token fields
|
|
60
|
+
func (h *AuthHandler) Login(c *gin.Context) {
|
|
61
|
+
var req LoginRequest
|
|
62
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
63
|
+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
// ✅ Log only the email, not the password
|
|
67
|
+
h.logger.Info("login attempt", slog.String("email", req.Email))
|
|
68
|
+
// ...
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Sensitive fields to never log
|
|
73
|
+
|
|
74
|
+
- `password`, `password_confirmation`, `current_password`
|
|
75
|
+
- `token`, `access_token`, `refresh_token`, `api_key`, `secret`
|
|
76
|
+
- `card_number`, `cvv`, `expiry`
|
|
77
|
+
- `ssn`, `national_id`, `date_of_birth` (PII)
|
|
78
|
+
- `Authorization` header value
|
|
79
|
+
|
|
80
|
+
## Notes
|
|
81
|
+
|
|
82
|
+
- If you need request body logging for debugging, create a separate debug middleware that is only enabled when `GIN_MODE=debug`.
|
|
83
|
+
- Use log scrubbing libraries (e.g., `go-sanitize`) as a last-resort safety net, but don't rely on them — fix the root cause.
|
|
84
|
+
- `c.Request.Header` can contain `Authorization: Bearer <token>` — log header names only, never values.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Always Use try-with-resources for AutoCloseable
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: prevents resource leaks by ensuring streams, connections, and other closeable resources are always closed
|
|
5
|
+
tags: resource-management, best-practice, java, error-prone
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Always Use try-with-resources for AutoCloseable
|
|
9
|
+
|
|
10
|
+
Since Java 7, the `try`-with-resources statement automatically closes any resource that implements `AutoCloseable` or `Closeable`. Using manual `finally` blocks to close resources is verbose, error-prone, and can silently swallow exceptions thrown during close.
|
|
11
|
+
|
|
12
|
+
**Incorrect (manual finally block):**
|
|
13
|
+
|
|
14
|
+
```java
|
|
15
|
+
public void readFile(String path) throws IOException {
|
|
16
|
+
InputStream in = null;
|
|
17
|
+
try {
|
|
18
|
+
in = new FileInputStream(path);
|
|
19
|
+
// process stream
|
|
20
|
+
int b = in.read();
|
|
21
|
+
} catch (IOException e) {
|
|
22
|
+
logger.error("Failed to read file", e);
|
|
23
|
+
throw e;
|
|
24
|
+
} finally {
|
|
25
|
+
if (in != null) {
|
|
26
|
+
try {
|
|
27
|
+
in.close(); // exception here swallows the original
|
|
28
|
+
} catch (IOException ignored) {}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public void queryDatabase(Connection conn) throws SQLException {
|
|
34
|
+
Statement stmt = null;
|
|
35
|
+
ResultSet rs = null;
|
|
36
|
+
try {
|
|
37
|
+
stmt = conn.createStatement();
|
|
38
|
+
rs = stmt.executeQuery("SELECT * FROM users");
|
|
39
|
+
} finally {
|
|
40
|
+
if (rs != null) rs.close(); // may throw, hiding original
|
|
41
|
+
if (stmt != null) stmt.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Correct (try-with-resources):**
|
|
47
|
+
|
|
48
|
+
```java
|
|
49
|
+
public void readFile(String path) throws IOException {
|
|
50
|
+
try (InputStream in = new FileInputStream(path)) {
|
|
51
|
+
// process stream
|
|
52
|
+
int b = in.read();
|
|
53
|
+
}
|
|
54
|
+
// in is automatically closed even if an exception occurs
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public void queryDatabase(Connection conn) throws SQLException {
|
|
58
|
+
try (Statement stmt = conn.createStatement();
|
|
59
|
+
ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {
|
|
60
|
+
while (rs.next()) {
|
|
61
|
+
// process results
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// both stmt and rs are closed in reverse declaration order
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Also works with custom AutoCloseable resources
|
|
68
|
+
public void processResource() throws Exception {
|
|
69
|
+
try (MyService service = new MyService()) {
|
|
70
|
+
service.execute();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Key benefits:**
|
|
76
|
+
- Resources are closed in reverse declaration order, automatically.
|
|
77
|
+
- Original exceptions are never swallowed by close failures (suppressed exceptions).
|
|
78
|
+
- Cleaner, less boilerplate code.
|
|
79
|
+
|
|
80
|
+
**Resources that must use try-with-resources:**
|
|
81
|
+
- `InputStream` / `OutputStream` / `Reader` / `Writer`
|
|
82
|
+
- `Connection` / `Statement` / `ResultSet` (JDBC)
|
|
83
|
+
- `HttpClient` / `Socket`
|
|
84
|
+
- Any class implementing `AutoCloseable`
|
|
85
|
+
|
|
86
|
+
**Tools:** PMD (`UseTryWithResources`), SonarQube (`S2093`), IntelliJ Inspections
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Override equals() and hashCode() Together
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: violating the equals-hashCode contract breaks HashMap, HashSet, and other hash-based collections silently
|
|
5
|
+
tags: correctness, contract, java, error-prone
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Override equals() and hashCode() Together
|
|
9
|
+
|
|
10
|
+
In Java, `equals()` and `hashCode()` share a contract: objects that are equal (via `equals()`) **must** have the same hash code. If you override one without overriding the other, hash-based collections (`HashMap`, `HashSet`, `Hashtable`) will malfunction — objects may become unreachable or duplicates may appear.
|
|
11
|
+
|
|
12
|
+
**Incorrect (only overrides equals):**
|
|
13
|
+
|
|
14
|
+
```java
|
|
15
|
+
public class User {
|
|
16
|
+
private Long id;
|
|
17
|
+
private String email;
|
|
18
|
+
|
|
19
|
+
@Override
|
|
20
|
+
public boolean equals(Object o) {
|
|
21
|
+
if (this == o) return true;
|
|
22
|
+
if (!(o instanceof User)) return false;
|
|
23
|
+
User user = (User) o;
|
|
24
|
+
return Objects.equals(id, user.id);
|
|
25
|
+
}
|
|
26
|
+
// Missing hashCode! — HashSet will treat two equal Users as different objects
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Result: map.put(user1); map.get(user2) returns null even if user1.equals(user2)
|
|
30
|
+
Set<User> set = new HashSet<>();
|
|
31
|
+
set.add(new User(1L, "a@b.com"));
|
|
32
|
+
set.contains(new User(1L, "a@b.com")); // false! Bug!
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Incorrect (only overrides hashCode):**
|
|
36
|
+
|
|
37
|
+
```java
|
|
38
|
+
public class Order {
|
|
39
|
+
private Long id;
|
|
40
|
+
|
|
41
|
+
@Override
|
|
42
|
+
public int hashCode() {
|
|
43
|
+
return Objects.hash(id);
|
|
44
|
+
}
|
|
45
|
+
// Missing equals! — equals uses identity (==) by default
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Correct (both overridden consistently):**
|
|
50
|
+
|
|
51
|
+
```java
|
|
52
|
+
public class User {
|
|
53
|
+
private Long id;
|
|
54
|
+
private String email;
|
|
55
|
+
|
|
56
|
+
@Override
|
|
57
|
+
public boolean equals(Object o) {
|
|
58
|
+
if (this == o) return true;
|
|
59
|
+
if (!(o instanceof User)) return false;
|
|
60
|
+
User user = (User) o;
|
|
61
|
+
return Objects.equals(id, user.id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Override
|
|
65
|
+
public int hashCode() {
|
|
66
|
+
return Objects.hash(id); // same fields as equals
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Using records (Java 16+): equals and hashCode are auto-generated
|
|
71
|
+
public record User(Long id, String email) {}
|
|
72
|
+
|
|
73
|
+
// Using Lombok:
|
|
74
|
+
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
|
75
|
+
public class User {
|
|
76
|
+
@EqualsAndHashCode.Include
|
|
77
|
+
private Long id;
|
|
78
|
+
private String email;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Rules to follow:**
|
|
83
|
+
- Use the **same fields** in both `equals()` and `hashCode()`.
|
|
84
|
+
- Prefer `Objects.equals()` and `Objects.hash()` over manual null checks.
|
|
85
|
+
- Consider using Lombok `@EqualsAndHashCode` or Java Records for value objects.
|
|
86
|
+
- Never include mutable fields in `hashCode()` if the object will be stored in a hash collection.
|
|
87
|
+
|
|
88
|
+
**Tools:** PMD (`OverrideBothEqualsAndHashcode`), Checkstyle (`EqualsHashCode`), IntelliJ Inspections
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use equals() for String and Object Comparison, Not ==
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: using == compares references, not values, causing silent bugs that only appear at runtime
|
|
5
|
+
tags: correctness, java, error-prone, string
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use equals() for String and Object Comparison, Not ==
|
|
9
|
+
|
|
10
|
+
In Java, `==` checks **reference equality** (are these the same object in memory?). For Strings and other objects, you almost always want **value equality** — use `.equals()`. While string literals may be interned (sharing references), strings from variables, method returns, or `new String(...)` will have different references, making `==` unreliable.
|
|
11
|
+
|
|
12
|
+
**Incorrect (using == for string comparison):**
|
|
13
|
+
|
|
14
|
+
```java
|
|
15
|
+
String status = getStatusFromDatabase(); // returns "ACTIVE"
|
|
16
|
+
if (status == "ACTIVE") { // BUG: may be false even when value is "ACTIVE"
|
|
17
|
+
activateUser();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
String s1 = new String("hello");
|
|
21
|
+
String s2 = new String("hello");
|
|
22
|
+
if (s1 == s2) { // always false — different objects
|
|
23
|
+
// never executed
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public boolean isAdmin(String role) {
|
|
27
|
+
return role == "ADMIN"; // unreliable
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Correct (using equals for string comparison):**
|
|
32
|
+
|
|
33
|
+
```java
|
|
34
|
+
String status = getStatusFromDatabase();
|
|
35
|
+
if ("ACTIVE".equals(status)) { // null-safe: won't NPE if status is null
|
|
36
|
+
activateUser();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Or with Objects.equals for null-safety on both sides:
|
|
40
|
+
if (Objects.equals(status, "ACTIVE")) {
|
|
41
|
+
activateUser();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public boolean isAdmin(String role) {
|
|
45
|
+
return "ADMIN".equals(role); // preferred: literal first to avoid NPE
|
|
46
|
+
// or: return role != null && role.equals("ADMIN");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// For enums: use == (enums are singletons, == is correct and preferred)
|
|
50
|
+
if (status == Status.ACTIVE) { // correct for enums
|
|
51
|
+
activateUser();
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Literal-first pattern:**
|
|
56
|
+
|
|
57
|
+
Placing the literal on the left side of `equals()` is a common defensive pattern — if the variable is `null`, the call returns `false` instead of throwing `NullPointerException`:
|
|
58
|
+
|
|
59
|
+
```java
|
|
60
|
+
// Risky: may throw NPE if name is null
|
|
61
|
+
if (name.equals("John")) { ... }
|
|
62
|
+
|
|
63
|
+
// Safe: returns false if name is null
|
|
64
|
+
if ("John".equals(name)) { ... }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Key distinctions:**
|
|
68
|
+
- `String` and all objects: use `.equals()` or `Objects.equals()`
|
|
69
|
+
- `enum` types: use `==` (enums are singleton instances)
|
|
70
|
+
- Primitives (`int`, `boolean`, etc.): use `==`
|
|
71
|
+
|
|
72
|
+
**Tools:** PMD (`CompareObjectsWithEquals`, `UseEqualsToCompareStrings`), FindBugs/SpotBugs (`ES_COMPARING_STRINGS_WITH_EQ`), IntelliJ Inspections
|