@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.
Files changed (185) hide show
  1. package/config/rules/rules-registry-generated.json +1717 -282
  2. package/core/architecture-integration.js +57 -15
  3. package/core/cli-action-handler.js +51 -36
  4. package/core/config-manager.js +6 -0
  5. package/core/config-merger.js +33 -0
  6. package/core/config-validator.js +37 -2
  7. package/core/file-targeting-service.js +148 -15
  8. package/core/init-command.js +118 -70
  9. package/core/output-service.js +12 -3
  10. package/core/project-detector.js +517 -0
  11. package/core/scoring-service.js +12 -6
  12. package/core/summary-report-service.js +9 -4
  13. package/core/tui-select.js +245 -0
  14. package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
  15. package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
  16. package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
  17. package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
  18. package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
  19. package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
  20. package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
  21. package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
  22. package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
  23. package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
  24. package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
  25. package/engines/impact/cli.js +54 -39
  26. package/engines/impact/config/default-config.js +105 -5
  27. package/engines/impact/core/impact-analyzer.js +12 -15
  28. package/engines/impact/core/utils/gitignore-parser.js +123 -0
  29. package/engines/impact/core/utils/method-call-graph.js +272 -87
  30. package/origin-rules/dart-en.md +1 -1
  31. package/origin-rules/go-en.md +231 -0
  32. package/origin-rules/php-en.md +107 -0
  33. package/origin-rules/python-en.md +113 -0
  34. package/origin-rules/ruby-en.md +607 -0
  35. package/package.json +1 -1
  36. package/scripts/copy-arch-detect.js +5 -1
  37. package/scripts/copy-impact-analyzer.js +5 -1
  38. package/scripts/generate-rules-registry.js +30 -14
  39. package/skill-assets/sunlint-code-quality/SKILL.md +3 -2
  40. package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
  41. package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
  42. package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
  43. package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
  44. package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
  45. package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
  46. package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
  47. package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
  48. package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
  49. package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
  50. package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
  51. package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
  52. package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
  53. package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
  54. package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
  55. package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
  56. package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
  57. package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
  58. package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
  59. package/skill-assets/sunlint-code-quality/rules/go/G001-explicit-error-handling.md +53 -0
  60. package/skill-assets/sunlint-code-quality/rules/go/G002-context-first-argument.md +44 -0
  61. package/skill-assets/sunlint-code-quality/rules/go/G003-receiver-consistency.md +38 -0
  62. package/skill-assets/sunlint-code-quality/rules/go/G004-avoid-panic.md +49 -0
  63. package/skill-assets/sunlint-code-quality/rules/go/G005-goroutine-leak-prevention.md +49 -0
  64. package/skill-assets/sunlint-code-quality/rules/go/G006-interface-consumer-definition.md +45 -0
  65. package/skill-assets/sunlint-code-quality/rules/go/GN001-gin-binding-validation.md +57 -0
  66. package/skill-assets/sunlint-code-quality/rules/go/GN002-gin-error-response.md +48 -0
  67. package/skill-assets/sunlint-code-quality/rules/go/GN003-graceful-shutdown.md +57 -0
  68. package/skill-assets/sunlint-code-quality/rules/go/GN004-gin-route-logical-grouping.md +54 -0
  69. package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
  70. package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
  71. package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
  72. package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
  73. package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
  74. package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
  75. package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
  76. package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
  77. package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
  78. package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
  79. package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
  80. package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
  81. package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
  82. package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
  83. package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
  84. package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
  85. package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
  86. package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
  87. package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
  88. package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
  89. package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
  90. package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
  91. package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
  92. package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
  93. package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
  94. package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
  95. package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
  96. package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
  97. package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
  98. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
  99. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
  100. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
  101. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
  102. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
  103. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
  104. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
  105. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
  106. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
  107. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
  108. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
  109. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
  110. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
  111. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
  112. package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
  113. package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
  114. package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
  115. package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
  116. package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
  117. package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
  118. package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
  119. package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
  120. package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
  121. package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
  122. package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
  123. package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
  124. package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
  125. package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
  126. package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
  127. package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
  128. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
  129. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
  130. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
  131. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
  132. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
  133. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
  134. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
  135. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
  136. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
  137. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
  138. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
  139. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
  140. package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
  141. package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
  142. package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
  143. package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
  144. package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
  145. package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
  146. package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
  147. package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
  148. package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
  149. package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
  150. package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
  151. package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
  152. package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
  153. package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
  154. package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
  155. package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
  156. package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
  157. package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
  158. package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
  159. package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
  160. package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
  161. package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
  162. package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
  163. package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
  164. package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
  165. package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
  166. package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
  167. package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
  168. package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
  169. package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
  170. package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
  171. package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
  172. package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
  173. package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
  174. package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
  175. package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
  176. package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
  177. package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
  178. package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
  179. package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
  180. package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
  181. package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
  182. package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
  183. package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
  184. package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
  185. package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
@@ -0,0 +1,149 @@
1
+ # Go Gin Framework — SunLint Agent Guide
2
+
3
+ > Priority directives for AI agents working on Go + Gin projects.
4
+ > Rule files: `.agent/skills/sunlint-code-quality/rules/`
5
+
6
+ ---
7
+
8
+ ## Critical Patterns — Apply Every Time
9
+
10
+ ### Middleware Chain Termination
11
+ - After sending a response that should stop the chain → `c.AbortWithStatusJSON(code, body)` then `return`
12
+ - `c.AbortWithStatusJSON` calls `c.Abort()` internally — don't call both
13
+ - Plain `return` from a handler does NOT stop downstream handlers
14
+ - See: `GN001-abort-after-response.md`
15
+
16
+ ### Request Binding + Validation
17
+ - Every `c.ShouldBindJSON(&req)` → must be followed by `if err != nil { c.AbortWithStatusJSON(400, ...); return }`
18
+ - Declare all validation rules as struct `binding:` tags: `binding:"required,email"`, `binding:"min=1,max=255"`
19
+ - **Never** skip the error check from `ShouldBind*`
20
+ - See: `GN003-bind-error-handling.md`, `GN008-struct-validation-tags.md`
21
+
22
+ ### Context Usage
23
+ - Inside handlers: `ctx := c.Request.Context()` — pass to all downstream calls
24
+ - **Never** `context.Background()` inside a handler
25
+ - **Never** pass `*gin.Context` to a goroutine — copy scalar values first, pass `c.Request.Context()`
26
+ - See: `GN002-request-context.md`, `GN010-context-scope.md`
27
+
28
+ ### Do Not Log Sensitive DataTesting commands rõ ràng trong package.json/Makefile
29
+ Standard conventions (eslint, prettier, gofmt đã config sẵn)
30
+ 4. Test với agents trước khi commit
31
+ Quy trình recommended:
32
+
33
+ Bước 1: Tạo baseline
34
+
35
+ # Chạy agent KHÔNG có AGENTS.md trên vài tasks nhỏ
36
+ # Đo success rate, cost
37
+
38
+ Bước 2: Thêm AGENTS.md, measure lại
39
+
40
+ # So sánh với baseline
41
+ # Nếu không improve hoặc cost tăng quá nhiều → bỏ AGENTS.md
42
+
43
+ Bước 3: Iterate
44
+
45
+ # Nếu quyết định giữ AGENTS.md, hãy giữ nó concise
46
+ # Monitor agent behavior qua time
47
+
48
+ 5. Monitor Context Length
49
+ Context files nên dưới 500 tokens (t
50
+ - Logging middleware logs only: method, path, status, duration, request_id, client IP
51
+ - **Never** log request bodies, `Authorization` header values, or any field named password/token/card
52
+ - See: `GN012-no-log-sensitive.md`
53
+
54
+ ---
55
+
56
+ ## Architecture Patterns
57
+
58
+ ### Handler struct — dependency injection
59
+ Handlers are structs with injected service interfaces. No globals. No `new(Service)` inside handlers.
60
+ ```go
61
+ type OrderHandler struct {
62
+ service OrderService // interface — mockable
63
+ logger *slog.Logger
64
+ }
65
+ func NewOrderHandler(svc OrderService, logger *slog.Logger) *OrderHandler {
66
+ return &OrderHandler{service: svc, logger: logger}
67
+ }
68
+ ```
69
+ See: `GN004-dependency-injection.md`
70
+
71
+ ### Route organisation
72
+ ```go
73
+ r := gin.New()
74
+ r.Use(gin.Recovery(), RequestID(), RequestLogger(logger)) // global
75
+
76
+ public := r.Group("/api/v1")
77
+ { /* unauthenticated routes */ }
78
+
79
+ private := r.Group("/api/v1")
80
+ private.Use(JWTAuth(validator))
81
+ { /* authenticated routes */ }
82
+ ```
83
+ See: `GN005-route-groups-middleware.md`, `GN011-middleware-concerns.md`
84
+
85
+ ### Production setup checklist
86
+ ```go
87
+ gin.SetMode(os.Getenv("GIN_MODE")) // "release" in prod — GN007
88
+ r := gin.New()
89
+ r.Use(CustomRecovery(logger)) // GN009
90
+ r.Use(RequestID()) // GN011
91
+ r.Use(RequestLogger(logger)) // GN012
92
+ // ... register groups (GN005)
93
+ ```
94
+
95
+ ### HTTP Status Codes
96
+ - `POST` success → `c.JSON(http.StatusCreated, obj)` (201)
97
+ - `DELETE` no body → `c.Status(http.StatusNoContent)` (204)
98
+ - Not found → `c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "not found"})` (404)
99
+ - Validation error → `c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})` (400)
100
+ - **Always** use `net/http` constants — never magic integers
101
+ - See: `GN006-http-status-codes.md`
102
+
103
+ ### Cross-cutting concerns
104
+ Auth, rate limiting, CORS, request ID, structured logging → **always middleware**, never inside handlers.
105
+ See: `GN011-middleware-concerns.md`
106
+
107
+ ---
108
+
109
+ ## Run Commands
110
+
111
+ ```bash
112
+ # Development
113
+ go run ./cmd/api/main.go
114
+ GIN_MODE=debug go run ./cmd/api/main.go
115
+
116
+ # Build
117
+ go build -o bin/api ./cmd/api/
118
+
119
+ # Test (always with race detector)
120
+ go test -race ./...
121
+ go test -race -run TestOrderHandler ./internal/handlers/
122
+ go test -cover ./...
123
+
124
+ # Linting
125
+ golangci-lint run ./...
126
+ staticcheck ./...
127
+
128
+ # Env for production
129
+ GIN_MODE=release
130
+ PORT=8080
131
+ ```
132
+
133
+ ---
134
+
135
+ ## What NOT to do (quick reference)
136
+
137
+ | ❌ Wrong | ✅ Correct |
138
+ |---|---|
139
+ | `return` after `c.JSON` in middleware | `c.AbortWithStatusJSON(...)` then `return` |
140
+ | `context.Background()` in handler | `c.Request.Context()` |
141
+ | `go func() { use c.Get(...) }()` | Copy value before goroutine: `val := c.GetString(...)` |
142
+ | `c.ShouldBindJSON(&req)` without err check | `if err := c.ShouldBindJSON(&req); err != nil { abort }` |
143
+ | Manual `if req.Email == ""` validation | `binding:"required,email"` struct tag |
144
+ | `var db *gorm.DB` package global | Inject `db *gorm.DB` as struct field |
145
+ | Auth check inside handler | `JWTAuth()` middleware on route group |
146
+ | `log.Printf("body: %s", body)` | Log method+path+status+duration only |
147
+ | `gin.Default()` in production | `gin.New()` + explicit `Recovery()` + structured logger |
148
+ | `c.JSON(200, gin.H{"error": ...})` | `c.AbortWithStatusJSON(http.StatusBadRequest, ...)` |
149
+ | `GIN_MODE` not set (debug default) | `GIN_MODE=release` in production environment |
@@ -0,0 +1,75 @@
1
+ ---
2
+ title: "GN001 – Call c.Abort() After Terminating in Middleware"
3
+ impact: high
4
+ impactDescription: "Missing c.Abort() causes subsequent middleware handlers and the final route handler to still execute after an early response has been sent, causing duplicate writes and data leaks."
5
+ tags: [go, gin, middleware, correctness]
6
+ ---
7
+
8
+ # GN001 – Call `c.Abort()` After Terminating in Middleware
9
+
10
+ ## Rule
11
+
12
+ Any middleware that sends a response and intends to stop the chain **must** call `c.Abort()` (or `c.AbortWithStatus()` / `c.AbortWithStatusJSON()`) immediately after writing. Never rely on `return` alone.
13
+
14
+ ## Why
15
+
16
+ `c.Next()` executes subsequent handlers in sequence. A plain `return` exits the current handler but Gin still runs the remaining handlers in the chain. `c.Abort()` sets the index past all remaining handlers so the chain stops cleanly.
17
+
18
+ ## Wrong
19
+
20
+ ```go
21
+ func AuthMiddleware() gin.HandlerFunc {
22
+ return func(c *gin.Context) {
23
+ token := c.GetHeader("Authorization")
24
+ if token == "" {
25
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
26
+ return // ❌ returns from this func but downstream handlers still run
27
+ }
28
+ c.Next()
29
+ }
30
+ }
31
+
32
+ func RateLimitMiddleware() gin.HandlerFunc {
33
+ return func(c *gin.Context) {
34
+ if isRateLimited(c.ClientIP()) {
35
+ c.JSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"})
36
+ // ❌ forgot both return and Abort — next handler fires and writes again
37
+ }
38
+ c.Next()
39
+ }
40
+ }
41
+ ```
42
+
43
+ ## Correct
44
+
45
+ ```go
46
+ func AuthMiddleware() gin.HandlerFunc {
47
+ return func(c *gin.Context) {
48
+ token := c.GetHeader("Authorization")
49
+ if token == "" {
50
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
51
+ // c.Abort() is called inside AbortWithStatusJSON — no c.Next() needed
52
+ return
53
+ }
54
+ // validation passes — continue chain
55
+ c.Set("user_id", parsedUserID)
56
+ c.Next()
57
+ }
58
+ }
59
+
60
+ func RateLimitMiddleware() gin.HandlerFunc {
61
+ return func(c *gin.Context) {
62
+ if isRateLimited(c.ClientIP()) {
63
+ c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"})
64
+ return
65
+ }
66
+ c.Next()
67
+ }
68
+ }
69
+ ```
70
+
71
+ ## Notes
72
+
73
+ - `c.AbortWithStatus(code)` and `c.AbortWithStatusJSON(code, obj)` both call `c.Abort()` internally — you don't need to call `c.Abort()` separately when using these.
74
+ - After `c.Abort()`, code after the call in the same function still runs — use `return` to exit the function body.
75
+ - Verify with unit tests: register middleware + handler, send a bad request, assert the handler body was NOT executed.
@@ -0,0 +1,64 @@
1
+ ---
2
+ title: "GN002 – Use c.Request.Context(), Not context.Background()"
3
+ impact: medium
4
+ impactDescription: "context.Background() ignores client disconnects and request deadlines; c.Request.Context() propagates cancellation automatically."
5
+ tags: [go, gin, context, correctness]
6
+ ---
7
+
8
+ # GN002 – Use `c.Request.Context()` Not `context.Background()`
9
+
10
+ ## Rule
11
+
12
+ When passing a context to database calls, external HTTP requests, or any `ctx context.Context` parameter inside a Gin handler, always use `c.Request.Context()`. Never create a fresh `context.Background()` inside a handler.
13
+
14
+ ## Why
15
+
16
+ `c.Request.Context()` is derived from the HTTP request and carries the client's cancellation signal. If the client disconnects mid-request, the context is cancelled and all downstream work (DB queries, gRPC calls) is cancelled too — preventing wasted compute. `context.Background()` is never cancelled and leaks work after the client is gone.
17
+
18
+ ## Wrong
19
+
20
+ ```go
21
+ func (h *OrderHandler) Create(c *gin.Context) {
22
+ var req CreateOrderRequest
23
+ if err := c.ShouldBindJSON(&req); err != nil {
24
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
25
+ return
26
+ }
27
+
28
+ // ❌ context.Background() — ignores client disconnect and server deadlines
29
+ order, err := h.service.CreateOrder(context.Background(), req)
30
+ if err != nil {
31
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
32
+ return
33
+ }
34
+ c.JSON(http.StatusCreated, order)
35
+ }
36
+ ```
37
+
38
+ ## Correct
39
+
40
+ ```go
41
+ func (h *OrderHandler) Create(c *gin.Context) {
42
+ var req CreateOrderRequest
43
+ if err := c.ShouldBindJSON(&req); err != nil {
44
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
45
+ return
46
+ }
47
+
48
+ // ✅ propagates request lifetime, deadline, and cancellation
49
+ ctx := c.Request.Context()
50
+ order, err := h.service.CreateOrder(ctx, req)
51
+ if err != nil {
52
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
53
+ return
54
+ }
55
+ c.JSON(http.StatusCreated, order)
56
+ }
57
+ ```
58
+
59
+ ## Notes
60
+
61
+ - Extract the context once: `ctx := c.Request.Context()` at the top of the handler body.
62
+ - Do **not** store this context in a struct field — contexts must flow through function arguments.
63
+ - If you need a timeout shorter than the request's own deadline: `ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second); defer cancel()`.
64
+ - See GN010 for the rule about not storing Gin context in goroutines.
@@ -0,0 +1,70 @@
1
+ ---
2
+ title: "GN003 – Always Handle ShouldBindJSON / ShouldBind Errors"
3
+ impact: high
4
+ impactDescription: "Ignoring ShouldBindJSON errors means malformed or missing fields are silently treated as zero values, causing corrupted data writes and bypassed validation."
5
+ tags: [go, gin, validation, correctness]
6
+ ---
7
+
8
+ # GN003 – Always Handle `ShouldBindJSON` / `ShouldBind` Errors
9
+
10
+ ## Rule
11
+
12
+ Every call to `c.ShouldBindJSON`, `c.ShouldBind`, `c.ShouldBindQuery`, or `c.ShouldBindUri` must be followed by an error check. If the error is non-nil, abort immediately with a `400 Bad Request` response.
13
+
14
+ ## Why
15
+
16
+ `ShouldBind*` populates the target struct but returns an error for malformed JSON, missing required fields (enforced by `binding:"required"` tags), or type mismatches. Ignoring the error means the handler continues with a partially or incorrectly populated struct, silently writing bad data.
17
+
18
+ ## Wrong
19
+
20
+ ```go
21
+ func (h *UserHandler) Create(c *gin.Context) {
22
+ var req CreateUserRequest
23
+ c.ShouldBindJSON(&req) // ❌ error ignored — continues with zero-value struct fields
24
+
25
+ user, err := h.service.CreateUser(c.Request.Context(), req)
26
+ // ...
27
+ }
28
+
29
+ // Worse: using BindJSON (panics on error in some versions, logs but doesn't stop)
30
+ func (h *UserHandler) Update(c *gin.Context) {
31
+ var req UpdateUserRequest
32
+ c.BindJSON(&req) // ❌ BindJSON writes 400 header but handler still continues
33
+ h.service.UpdateUser(c.Request.Context(), req)
34
+ }
35
+ ```
36
+
37
+ ## Correct
38
+
39
+ ```go
40
+ type CreateUserRequest struct {
41
+ Name string `json:"name" binding:"required,min=2,max=100"`
42
+ Email string `json:"email" binding:"required,email"`
43
+ Age int `json:"age" binding:"required,min=18"`
44
+ }
45
+
46
+ func (h *UserHandler) Create(c *gin.Context) {
47
+ var req CreateUserRequest
48
+ if err := c.ShouldBindJSON(&req); err != nil {
49
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
50
+ "error": "invalid request",
51
+ "details": err.Error(),
52
+ })
53
+ return
54
+ }
55
+
56
+ user, err := h.service.CreateUser(c.Request.Context(), req)
57
+ if err != nil {
58
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "creation failed"})
59
+ return
60
+ }
61
+ c.JSON(http.StatusCreated, user)
62
+ }
63
+ ```
64
+
65
+ ## Notes
66
+
67
+ - Prefer `ShouldBind*` over `Bind*` — the `Bind*` family writes a `400` header but doesn't abort the handler, making code flow confusing.
68
+ - Use struct tags: `binding:"required"`, `binding:"email"`, `binding:"min=1,max=255"` to shift validation into the binding layer.
69
+ - For detailed error messages, type-assert the error to `validator.ValidationErrors` and format field names cleanly.
70
+ - See GN008 for struct validation tag conventions.
@@ -0,0 +1,78 @@
1
+ ---
2
+ title: "GN004 – Use Dependency Injection, Not Global Variables"
3
+ impact: high
4
+ impactDescription: "Global database connections and service instances in package-level vars are untestable, race-prone, and make initialization order fragile."
5
+ tags: [go, gin, architecture, testing]
6
+ ---
7
+
8
+ # GN004 – Use Dependency Injection, Not Global Variables
9
+
10
+ ## Rule
11
+
12
+ Database connections, service instances, configuration objects, and HTTP clients must be injected as struct fields into handlers. Never declare them as `var db *gorm.DB` at package level and access them from handler functions.
13
+
14
+ ## Why
15
+
16
+ Global state:
17
+ - Cannot be replaced with a mock in unit tests.
18
+ - Initialization order is implicit — the global may be `nil` if setup hasn't run.
19
+ - Race conditions when tests share globals.
20
+
21
+ Dependency injection via struct fields makes dependencies explicit, swappable, and testable.
22
+
23
+ ## Wrong
24
+
25
+ ```go
26
+ // db/db.go — package-level global
27
+ var DB *gorm.DB
28
+
29
+ func Init() {
30
+ DB, _ = gorm.Open(...)
31
+ }
32
+
33
+ // handlers/user.go — accesses global directly
34
+ func GetUser(c *gin.Context) {
35
+ var user User
36
+ db.DB.First(&user, c.Param("id")) // ❌ depends on global initialization order
37
+ c.JSON(200, user)
38
+ }
39
+ ```
40
+
41
+ ## Correct
42
+
43
+ ```go
44
+ // handlers/user_handler.go
45
+ type UserHandler struct {
46
+ db *gorm.DB // injected
47
+ service UserService // interface — swappable in tests
48
+ logger *slog.Logger
49
+ }
50
+
51
+ func NewUserHandler(db *gorm.DB, svc UserService, logger *slog.Logger) *UserHandler {
52
+ return &UserHandler{db: db, service: svc, logger: logger}
53
+ }
54
+
55
+ func (h *UserHandler) GetUser(c *gin.Context) {
56
+ ctx := c.Request.Context()
57
+ user, err := h.service.FindUserByID(ctx, c.Param("id"))
58
+ if err != nil {
59
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "not found"})
60
+ return
61
+ }
62
+ c.JSON(http.StatusOK, user)
63
+ }
64
+
65
+ // main.go — wiring
66
+ db := database.Connect(cfg.DSN)
67
+ userSvc := services.NewUserService(db)
68
+ userHandler := handlers.NewUserHandler(db, userSvc, logger)
69
+
70
+ r := gin.New()
71
+ r.GET("/users/:id", userHandler.GetUser)
72
+ ```
73
+
74
+ ## Notes
75
+
76
+ - Define service dependencies as Go interfaces so handlers can be unit-tested with mocks without a real DB.
77
+ - Use a DI framework (e.g., `google/wire`, `samber/do`) for large projects to automate wiring.
78
+ - Configuration (`Config` structs) should be loaded once in `main` and injected — never read from `os.Getenv` directly inside handlers.
@@ -0,0 +1,71 @@
1
+ ---
2
+ title: "GN005 – Group Routes and Apply Middleware at Group Level"
3
+ impact: medium
4
+ impactDescription: "Registering middleware per-route duplicates code and makes it easy to miss a route; group-level middleware is exhaustive by design."
5
+ tags: [go, gin, routing, middleware]
6
+ ---
7
+
8
+ # GN005 – Group Routes and Apply Middleware at Group Level
9
+
10
+ ## Rule
11
+
12
+ Use `router.Group()` to organise routes by prefix and protection level. Apply authentication, logging, and rate-limiting middleware to the group — not individual routes.
13
+
14
+ ## Why
15
+
16
+ Per-route middleware registration (`r.GET("/users", authMiddleware, handler)`) requires the developer to remember to add middleware to every new route. A missed route is a security hole. Group-level middleware is applied to all current and future routes in the group automatically.
17
+
18
+ ## Wrong
19
+
20
+ ```go
21
+ r := gin.Default()
22
+
23
+ // ❌ Auth repeated on every route — easy to miss
24
+ r.GET("/users", authMiddleware, userHandler.List)
25
+ r.POST("/users", authMiddleware, userHandler.Create)
26
+ r.PUT("/users/:id", authMiddleware, userHandler.Update)
27
+ r.DELETE("/users/:id", authMiddleware, userHandler.Delete) // ❌ forgot auth on DELETE
28
+
29
+ // ❌ Mixed public and protected routes without groups — hard to audit
30
+ r.GET("/products", productHandler.List)
31
+ r.POST("/products", authMiddleware, adminMiddleware, productHandler.Create)
32
+ ```
33
+
34
+ ## Correct
35
+
36
+ ```go
37
+ r := gin.New()
38
+ r.Use(gin.Recovery(), requestLogger()) // global middleware
39
+
40
+ // Public routes — no auth
41
+ public := r.Group("/api/v1")
42
+ {
43
+ public.POST("/auth/login", authHandler.Login)
44
+ public.POST("/auth/register", authHandler.Register)
45
+ public.GET("/products", productHandler.List)
46
+ }
47
+
48
+ // Authenticated routes
49
+ authenticated := r.Group("/api/v1")
50
+ authenticated.Use(authMiddleware())
51
+ {
52
+ authenticated.GET("/users/me", userHandler.Profile)
53
+ authenticated.PUT("/users/me", userHandler.Update)
54
+ authenticated.GET("/orders", orderHandler.List)
55
+ authenticated.POST("/orders", orderHandler.Create)
56
+ }
57
+
58
+ // Admin-only routes
59
+ admin := r.Group("/api/v1/admin")
60
+ admin.Use(authMiddleware(), adminMiddleware())
61
+ {
62
+ admin.GET("/users", adminUserHandler.List)
63
+ admin.DELETE("/users/:id", adminUserHandler.Delete)
64
+ }
65
+ ```
66
+
67
+ ## Notes
68
+
69
+ - Use curly braces `{}` inside `Group()` to make the scope visually clear (they're just Go blocks, not syntactically required).
70
+ - Apply rate limiting at the group level for public endpoints to prevent brute force on `/auth/login`.
71
+ - Version your API with a group: `r.Group("/api/v1")` makes future versioning (`/api/v2`) straightforward.
@@ -0,0 +1,91 @@
1
+ ---
2
+ title: "GN006 – Return Correct HTTP Status Codes"
3
+ impact: medium
4
+ impactDescription: "Returning 200 for all responses breaks API clients, hides errors in monitoring, and makes retry logic impossible."
5
+ tags: [go, gin, api, http]
6
+ ---
7
+
8
+ # GN006 – Return Correct HTTP Status Codes
9
+
10
+ ## Rule
11
+
12
+ Always pass the correct HTTP status code to `c.JSON()`, `c.AbortWithStatusJSON()`, and `c.Status()`. Never return `200 OK` for errors, resource creation, or empty responses.
13
+
14
+ ## Why
15
+
16
+ HTTP semantics are the contract between your API and its clients. A `200` with `{"error": "not found"}` is invisible to load balancers, monitoring tools, and client SDKs. Correct status codes enable automatic retry logic, circuit breakers, and accurate error dashboards.
17
+
18
+ ## Wrong
19
+
20
+ ```go
21
+ func (h *UserHandler) Create(c *gin.Context) {
22
+ // ...
23
+ user, err := h.service.CreateUser(ctx, req)
24
+ if err != nil {
25
+ c.JSON(200, gin.H{"error": err.Error()}) // ❌ success code + error body
26
+ return
27
+ }
28
+ c.JSON(200, user) // ❌ should be 201 for newly created resource
29
+ }
30
+
31
+ func (h *UserHandler) Delete(c *gin.Context) {
32
+ h.service.DeleteUser(ctx, id)
33
+ c.JSON(200, gin.H{"message": "deleted"}) // ❌ should be 204 with no body
34
+ }
35
+ ```
36
+
37
+ ## Correct
38
+
39
+ ```go
40
+ import "net/http"
41
+
42
+ func (h *UserHandler) Create(c *gin.Context) {
43
+ var req CreateUserRequest
44
+ if err := c.ShouldBindJSON(&req); err != nil {
45
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) // 400
46
+ return
47
+ }
48
+ user, err := h.service.CreateUser(c.Request.Context(), req)
49
+ if err != nil {
50
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "creation failed"}) // 500
51
+ return
52
+ }
53
+ c.JSON(http.StatusCreated, user) // 201 ✅
54
+ }
55
+
56
+ func (h *UserHandler) GetUser(c *gin.Context) {
57
+ user, err := h.service.FindUserByID(c.Request.Context(), c.Param("id"))
58
+ if errors.Is(err, ErrNotFound) {
59
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "user not found"}) // 404
60
+ return
61
+ }
62
+ c.JSON(http.StatusOK, user) // 200
63
+ }
64
+
65
+ func (h *UserHandler) Delete(c *gin.Context) {
66
+ if err := h.service.DeleteUser(c.Request.Context(), c.Param("id")); err != nil {
67
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "not found"}) // 404
68
+ return
69
+ }
70
+ c.Status(http.StatusNoContent) // 204, no body ✅
71
+ }
72
+ ```
73
+
74
+ ## Status code reference
75
+
76
+ | Scenario | Code | Constant |
77
+ |---|---|---|
78
+ | Read success | 200 | `http.StatusOK` |
79
+ | Create success | 201 | `http.StatusCreated` |
80
+ | Delete / no body | 204 | `http.StatusNoContent` |
81
+ | Validation failed | 400 | `http.StatusBadRequest` |
82
+ | Not authenticated | 401 | `http.StatusUnauthorized` |
83
+ | Not authorized | 403 | `http.StatusForbidden` |
84
+ | Not found | 404 | `http.StatusNotFound` |
85
+ | Business rule violation | 422 | `http.StatusUnprocessableEntity` |
86
+ | Rate limited | 429 | `http.StatusTooManyRequests` |
87
+
88
+ ## Notes
89
+
90
+ - Use `net/http` constants — never magic numbers like `c.JSON(201, ...)`.
91
+ - Map service-layer sentinel errors (e.g. `ErrNotFound`, `ErrConflict`) to status codes in a central error-mapping function.
@@ -0,0 +1,64 @@
1
+ ---
2
+ title: "GN007 – Set gin.ReleaseMode in Production"
3
+ impact: medium
4
+ impactDescription: "The default gin.DebugMode prints every registered route and request detail to stdout, leaking your API surface and increasing log noise in production."
5
+ tags: [go, gin, security, configuration]
6
+ ---
7
+
8
+ # GN007 – Set `gin.ReleaseMode` in Production
9
+
10
+ ## Rule
11
+
12
+ Set `gin.SetMode(gin.ReleaseMode)` before calling `gin.New()` or `gin.Default()` in production builds. Read the mode from an environment variable so it can differ between environments.
13
+
14
+ ## Why
15
+
16
+ Debug mode outputs all routes on startup and logs detailed request info. In production this:
17
+ - Leaks your API surface to anyone with log access.
18
+ - Adds unnecessary CPU cost for string formatting.
19
+ - Makes it harder to find meaningful log entries.
20
+
21
+ ## Wrong
22
+
23
+ ```go
24
+ // main.go — mode not set, defaults to DebugMode
25
+ func main() {
26
+ r := gin.Default() // ❌ runs in debug mode, dumps all routes to stdout
27
+ r.GET("/internal/admin", adminHandler) // ❌ exposed in startup log
28
+ r.Run(":8080")
29
+ }
30
+
31
+ // Hardcoded mode
32
+ gin.SetMode(gin.DebugMode) // ❌ always debug, regardless of environment
33
+ ```
34
+
35
+ ## Correct
36
+
37
+ ```go
38
+ func main() {
39
+ // Read from environment — "release", "debug", or "test"
40
+ ginMode := os.Getenv("GIN_MODE")
41
+ if ginMode == "" {
42
+ ginMode = gin.ReleaseMode // safe default
43
+ }
44
+ gin.SetMode(ginMode)
45
+
46
+ r := gin.New()
47
+ r.Use(gin.Recovery()) // see GN009 for Recovery middleware
48
+ // ... register routes
49
+ r.Run(":" + os.Getenv("PORT"))
50
+ }
51
+ ```
52
+
53
+ Or rely on Gin's built-in env var support: Gin automatically reads `GIN_MODE` — set it to `release` in your container/systemd environment and `gin.SetMode` is not even needed explicitly.
54
+
55
+ ```bash
56
+ # Production container / systemd
57
+ GIN_MODE=release
58
+ ```
59
+
60
+ ## Notes
61
+
62
+ - `gin.Default()` adds `gin.Logger()` and `gin.Recovery()` automatically in debug mode — in production, use `gin.New()` and add your structured logger and Recovery explicitly (see GN009).
63
+ - Never call `gin.SetMode` after `gin.New()` — the mode must be set before the engine is created.
64
+ - In tests, set `gin.SetMode(gin.TestMode)` in `TestMain` to silence route debug output.