@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,55 @@
1
+ ---
2
+ title: Không để code chết trong codebase
3
+ impact: LOW
4
+ impactDescription: Code không dùng làm tăng kích thước binary, gây nhầm lẫn khi đọc và bảo trì, khiến reviewer tốn thời gian.
5
+ tags: swift, ios, dead-code, cleanup, code-quality
6
+ ---
7
+
8
+ ## Không để code chết trong codebase
9
+
10
+ Code không còn được gọi đến, biến không sử dụng, hàm private không được gọi, hay `#if false` block cần được xóa. Dùng Git history nếu cần khôi phục.
11
+
12
+ **Incorrect (code chết):**
13
+
14
+ ```swift
15
+ class OrderViewController: UIViewController {
16
+
17
+ // Không bao giờ được dùng
18
+ private func legacyFetchOrders() {
19
+ // TODO: remove this
20
+ }
21
+
22
+ #if false
23
+ private func debugHelper() {
24
+ print("Debug info: \(someVar)")
25
+ }
26
+ #endif
27
+
28
+ // Biến khai báo nhưng không dùng
29
+ private let unusedFormatter = DateFormatter()
30
+
31
+ override func viewDidLoad() {
32
+ super.viewDidLoad()
33
+ loadOrders()
34
+ }
35
+
36
+ private func loadOrders() { ... }
37
+ }
38
+ ```
39
+
40
+ **Correct (chỉ giữ code đang được dùng):**
41
+
42
+ ```swift
43
+ class OrderViewController: UIViewController {
44
+
45
+ override func viewDidLoad() {
46
+ super.viewDidLoad()
47
+ loadOrders()
48
+ }
49
+
50
+ private func loadOrders() { ... }
51
+ }
52
+ ```
53
+
54
+ **Tools:** SwiftLint (`unused_private_declaration`), Xcode Warnings, SwiftFormat (`unusedPrivateDeclarations`)
55
+
@@ -0,0 +1,69 @@
1
+ ---
2
+ title: Dùng Dependency Injection thay vì khởi tạo trực tiếp
3
+ impact: HIGH
4
+ impactDescription: DI qua protocol giúp dễ dàng mock trong unit test, tách biệt các tầng và tránh coupling cứng giữa các class.
5
+ tags: swift, ios, dependency-injection, testing, architecture, protocol
6
+ ---
7
+
8
+ ## Dùng Dependency Injection thay vì khởi tạo trực tiếp
9
+
10
+ Thay vì khởi tạo dependency trực tiếp bên trong class, hãy inject chúng qua `init` sử dụng protocol. Điều này đặc biệt quan trọng trong iOS khi viết unit test cho ViewModel, Interactor, hay Service.
11
+
12
+ **Incorrect (khởi tạo trực tiếp):**
13
+
14
+ ```swift
15
+ class OrderViewModel {
16
+ // Coupling cứng - không thể mock khi test
17
+ private let apiService = OrderAPIService()
18
+ private let database = CoreDataManager()
19
+ private let analytics = FirebaseAnalytics()
20
+
21
+ func loadOrders() {
22
+ apiService.fetchOrders { [weak self] result in
23
+ // xử lý
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ **Correct (inject qua protocol):**
30
+
31
+ ```swift
32
+ protocol OrderAPIServiceProtocol {
33
+ func fetchOrders(completion: @escaping (Result<[Order], Error>) -> Void)
34
+ }
35
+
36
+ protocol AnalyticsProtocol {
37
+ func track(event: String)
38
+ }
39
+
40
+ class OrderViewModel {
41
+ private let apiService: OrderAPIServiceProtocol
42
+ private let analytics: AnalyticsProtocol
43
+
44
+ init(
45
+ apiService: OrderAPIServiceProtocol = OrderAPIService(),
46
+ analytics: AnalyticsProtocol = FirebaseAnalytics()
47
+ ) {
48
+ self.apiService = apiService
49
+ self.analytics = analytics
50
+ }
51
+
52
+ func loadOrders() {
53
+ apiService.fetchOrders { [weak self] result in
54
+ // xử lý
55
+ }
56
+ }
57
+ }
58
+
59
+ // Unit test dễ dàng
60
+ class MockOrderAPIService: OrderAPIServiceProtocol {
61
+ func fetchOrders(completion: @escaping (Result<[Order], Error>) -> Void) {
62
+ completion(.success([Order(id: "1", total: 100)]))
63
+ }
64
+ }
65
+ let vm = OrderViewModel(apiService: MockOrderAPIService(), analytics: MockAnalytics())
66
+ ```
67
+
68
+ **Tools:** Code Review, Needle/Swinject (DI Framework)
69
+
@@ -0,0 +1,66 @@
1
+ ---
2
+ title: Không đặt logic nghiệp vụ trong init
3
+ impact: MEDIUM
4
+ impactDescription: Logic phức tạp trong init làm khó test, gây side-effect không mong muốn và vi phạm nguyên tắc Single Responsibility.
5
+ tags: swift, ios, init, constructor, architecture, code-quality
6
+ ---
7
+
8
+ ## Không đặt logic nghiệp vụ trong init
9
+
10
+ `init` chỉ nên gán giá trị cho các property. Đừng gọi API, đọc file, khởi động timer, hay thực hiện tính toán phức tạp bên trong `init`. Nếu cần, tách ra thành hàm `configure()` hoặc `setup()` gọi sau khi khởi tạo.
11
+
12
+ **Incorrect (logic trong init):**
13
+
14
+ ```swift
15
+ class UserProfileViewModel {
16
+ var user: User?
17
+ var recentOrders: [Order] = []
18
+
19
+ init(userId: String) {
20
+ // Gọi API trong init - không thể control được
21
+ let user = UserAPIService.shared.fetchUserSync(userId: userId)
22
+ self.user = user
23
+
24
+ // Logic nghiệp vụ trong init
25
+ if let user = user, user.isPremium {
26
+ recentOrders = OrderAPIService.shared.fetchOrdersSync(userId: userId)
27
+ }
28
+
29
+ // Đọc file trong init
30
+ let config = try? JSONDecoder().decode(Config.self, from: Data(contentsOf: configURL))
31
+ }
32
+ }
33
+ ```
34
+
35
+ **Correct (init đơn giản + hàm setup riêng):**
36
+
37
+ ```swift
38
+ class UserProfileViewModel {
39
+ private let userId: String
40
+ private let apiService: UserAPIServiceProtocol
41
+
42
+ var user: User?
43
+ var recentOrders: [Order] = []
44
+
45
+ init(userId: String, apiService: UserAPIServiceProtocol) {
46
+ self.userId = userId
47
+ self.apiService = apiService
48
+ // Không có logic nghiệp vụ ở đây
49
+ }
50
+
51
+ // Gọi sau khi khởi tạo - dễ test, dễ control
52
+ func loadData() async {
53
+ do {
54
+ user = try await apiService.fetchUser(userId: userId)
55
+ if user?.isPremium == true {
56
+ recentOrders = try await apiService.fetchOrders(userId: userId)
57
+ }
58
+ } catch {
59
+ // xử lý lỗi
60
+ }
61
+ }
62
+ }
63
+ ```
64
+
65
+ **Tools:** Code Review, SwiftLint custom rule
66
+
@@ -0,0 +1,64 @@
1
+ ---
2
+ title: Không throw lỗi generic - dùng Error enum có ngữ cảnh
3
+ impact: MEDIUM
4
+ impactDescription: Lỗi generic không cung cấp đủ thông tin để debug, xử lý hay hiển thị thông báo phù hợp cho người dùng.
5
+ tags: swift, ios, error-handling, enum, code-quality
6
+ ---
7
+
8
+ ## Không throw lỗi generic - dùng Error enum có ngữ cảnh
9
+
10
+ Không dùng `NSError`, `Error` chung chung hay string message tùy tiện. Thay vào đó, định nghĩa enum conform protocol `Error` với từng case cụ thể, mô tả rõ trường hợp thất bại.
11
+
12
+ **Incorrect (lỗi generic):**
13
+
14
+ ```swift
15
+ func login(email: String, password: String) throws -> User {
16
+ guard !email.isEmpty else {
17
+ throw NSError(domain: "error", code: 400, userInfo: nil) // không có ngữ cảnh
18
+ }
19
+ guard isValidEmail(email) else {
20
+ throw NSError(domain: "Invalid email", code: 0, userInfo: nil)
21
+ }
22
+ // ...
23
+ }
24
+ ```
25
+
26
+ **Correct (Error enum mô tả rõ ràng):**
27
+
28
+ ```swift
29
+ enum AuthError: Error, LocalizedError {
30
+ case emptyCredentials
31
+ case invalidEmailFormat
32
+ case incorrectPassword
33
+ case accountLocked(remainingAttempts: Int)
34
+ case networkUnavailable
35
+
36
+ var errorDescription: String? {
37
+ switch self {
38
+ case .emptyCredentials:
39
+ return "Email và mật khẩu không được để trống."
40
+ case .invalidEmailFormat:
41
+ return "Định dạng email không hợp lệ."
42
+ case .incorrectPassword:
43
+ return "Mật khẩu không đúng."
44
+ case .accountLocked(let attempts):
45
+ return "Tài khoản bị khóa. Còn \(attempts) lần thử."
46
+ case .networkUnavailable:
47
+ return "Không có kết nối mạng."
48
+ }
49
+ }
50
+ }
51
+
52
+ func login(email: String, password: String) throws -> User {
53
+ guard !email.isEmpty, !password.isEmpty else {
54
+ throw AuthError.emptyCredentials
55
+ }
56
+ guard isValidEmail(email) else {
57
+ throw AuthError.invalidEmailFormat
58
+ }
59
+ // ...
60
+ }
61
+ ```
62
+
63
+ **Tools:** Code Review, SwiftLint
64
+
@@ -0,0 +1,64 @@
1
+ ---
2
+ title: Không dùng mức log error cho lỗi không nghiêm trọng
3
+ impact: LOW
4
+ impactDescription: Lạm dụng .error hay .fault làm nhiễu hệ thống monitoring, khiến việc triage sự cố thực sự mất nhiều thời gian hơn.
5
+ tags: swift, ios, logging, os-log, error-level, code-quality
6
+ ---
7
+
8
+ ## Không dùng mức log error cho lỗi không nghiêm trọng
9
+
10
+ Khi dùng `os_log` hoặc `Logger` (iOS 14+), hãy chọn mức log phù hợp với mức độ nghiêm trọng. `.error` và `.fault` chỉ nên dùng khi có lỗi thực sự ảnh hưởng đến tính năng. Lỗi mạng có thể retry, không có session, hay validation thất bại nên dùng `.warning` hoặc `.info`.
11
+
12
+ **Incorrect (lạm dụng .error):**
13
+
14
+ ```swift
15
+ import OSLog
16
+
17
+ let logger = Logger(subsystem: "com.app.network", category: "API")
18
+
19
+ func fetchUserProfile(userId: String) async {
20
+ do {
21
+ let profile = try await apiService.fetchProfile(userId: userId)
22
+ logger.error("Fetched profile: \(userId)") // .error cho thành công??
23
+ } catch URLError.notConnectedToInternet {
24
+ logger.error("No internet - will retry") // nên là .warning
25
+ } catch let DecodingError.keyNotFound(key, _) {
26
+ logger.error("Missing key: \(key)") // có thể là .warning
27
+ }
28
+ }
29
+ ```
30
+
31
+ **Correct (dùng đúng mức log):**
32
+
33
+ ```swift
34
+ import OSLog
35
+
36
+ let logger = Logger(subsystem: "com.app.network", category: "API")
37
+
38
+ func fetchUserProfile(userId: String) async {
39
+ do {
40
+ let profile = try await apiService.fetchProfile(userId: userId)
41
+ logger.info("Fetched profile for userId: \(userId)")
42
+ } catch URLError.notConnectedToInternet {
43
+ logger.warning("No internet connection, retry scheduled")
44
+ } catch let DecodingError.keyNotFound(key, _) {
45
+ logger.warning("API response missing expected key: \(key.stringValue)")
46
+ } catch {
47
+ // Lỗi thực sự không lường trước - dùng .error
48
+ logger.error("Unexpected failure fetching profile userId=\(userId): \(error.localizedDescription)")
49
+ }
50
+ }
51
+ ```
52
+
53
+ **Mức log và khi nào dùng:**
54
+
55
+ | Mức | Khi nào dùng |
56
+ |-----|-------------|
57
+ | `.debug` | Thông tin chi tiết chỉ cần lúc development |
58
+ | `.info` | Sự kiện bình thường trong flow nghiệp vụ |
59
+ | `.warning` | Lỗi có thể recover, retry được |
60
+ | `.error` | Lỗi ảnh hưởng tính năng, cần điều tra |
61
+ | `.fault` | Lỗi nghiêm trọng - bug trong hệ thống |
62
+
63
+ **Tools:** `os.Logger`, OSLog framework
64
+
@@ -0,0 +1,47 @@
1
+ ---
2
+ title: Không import module không được sử dụng
3
+ impact: LOW
4
+ impactDescription: Import thừa tăng thời gian compile và tạo nhầm lẫn về dependencies thực sự của file.
5
+ tags: swift, ios, imports, compile-time, code-quality
6
+ ---
7
+
8
+ ## Không import module không được sử dụng
9
+
10
+ Xóa tất cả các `import` mà không có symbol nào trong file đang sử dụng. Xcode và SwiftFormat có thể tự phát hiện import thừa.
11
+
12
+ **Incorrect (import thừa):**
13
+
14
+ ```swift
15
+ import UIKit
16
+ import Foundation
17
+ import Combine // Không dùng Combine ở đây
18
+ import CoreLocation // Không dùng location
19
+ import SwiftUI // File này là UIKit thuần
20
+
21
+ class ProfileViewController: UIViewController {
22
+ private let viewModel: ProfileViewModel
23
+
24
+ override func viewDidLoad() {
25
+ super.viewDidLoad()
26
+ viewModel.loadProfile()
27
+ }
28
+ }
29
+ ```
30
+
31
+ **Correct (chỉ import module đang dùng):**
32
+
33
+ ```swift
34
+ import UIKit
35
+
36
+ class ProfileViewController: UIViewController {
37
+ private let viewModel: ProfileViewModel
38
+
39
+ override func viewDidLoad() {
40
+ super.viewDidLoad()
41
+ viewModel.loadProfile()
42
+ }
43
+ }
44
+ ```
45
+
46
+ **Tools:** SwiftFormat (`duplicateImports`), Xcode Warnings, periphery
47
+
@@ -0,0 +1,46 @@
1
+ ---
2
+ title: Không khai báo biến không dùng
3
+ impact: LOW
4
+ impactDescription: Biến không dùng làm tốn bộ nhớ, gây nhầm lẫn khi đọc code và có thể che giấu lỗi logic.
5
+ tags: swift, ios, unused-variables, warnings, code-quality
6
+ ---
7
+
8
+ ## Không khai báo biến không dùng
9
+
10
+ Hãy xóa tất cả biến được khai báo nhưng chưa bao giờ được dùng đến. Nếu cần bỏ qua giá trị trả về, dùng `_` (wildcard).
11
+
12
+ **Incorrect (biến không dùng):**
13
+
14
+ ```swift
15
+ func processPayment(_ payment: Payment) {
16
+ let transactionId = UUID().uuidString // Không bao giờ dùng transactionId
17
+
18
+ let result = paymentGateway.charge(payment) // result không được kiểm tra
19
+
20
+ let formatter = NumberFormatter() // formatter khai báo nhưng không dùng
21
+ let amount = payment.amount
22
+ print("Processing \(amount)")
23
+ }
24
+ ```
25
+
26
+ **Correct (dùng _ cho giá trị bỏ qua):**
27
+
28
+ ```swift
29
+ func processPayment(_ payment: Payment) {
30
+ let result = paymentGateway.charge(payment)
31
+ guard result.isSuccess else {
32
+ logger.error("Payment failed: \(result.errorMessage ?? "unknown")")
33
+ return
34
+ }
35
+
36
+ let amount = payment.amount
37
+ let formattedAmount = NumberFormatter.currency.string(from: NSNumber(value: amount)) ?? "\(amount)"
38
+ logger.info("Processed payment: \(formattedAmount)")
39
+ }
40
+
41
+ // Khi cần bỏ qua return value một cách có chủ ý:
42
+ _ = someOptionalFunction()
43
+ ```
44
+
45
+ **Tools:** Xcode Warnings (`unused variable`), SwiftLint (`unused_closure_parameter`), SwiftFormat (`unusedArguments`)
46
+
@@ -0,0 +1,55 @@
1
+ ---
2
+ title: Không khai báo tên trùng lặp trong cùng scope
3
+ impact: LOW
4
+ impactDescription: Shadowing biến gây nhầm lẫn về giá trị nào đang được dùng, dẫn đến bug khó phát hiện.
5
+ tags: swift, ios, shadowing, naming, code-quality
6
+ ---
7
+
8
+ ## Không khai báo tên trùng lặp trong cùng scope
9
+
10
+ Tránh re-declare một biến với tên giống biến bên ngoài scope (variable shadowing). Trường hợp ngoại lệ hợp lý duy nhất là optional binding (`if let user = user`), nhưng cần hạn chế và sử dụng shorthand `if let user` (Swift 5.7+).
11
+
12
+ **Incorrect (shadowing gây nhầm lẫn):**
13
+
14
+ ```swift
15
+ class CartViewModel {
16
+ var items: [CartItem] = []
17
+
18
+ func calculateTotal(items: [CartItem]) -> Double {
19
+ // `items` parameter shadowing `self.items`!
20
+ return items.reduce(0) { $0 + $1.price } // items nào đây??
21
+ }
22
+
23
+ func applyDiscount(_ discount: Double) {
24
+ var discount = discount * 0.01 // re-declare discount - confusing!
25
+ if discount > 0.5 { discount = 0.5 }
26
+ totalPrice *= (1 - discount)
27
+ }
28
+ }
29
+ ```
30
+
31
+ **Correct (tên rõ ràng, không shadow):**
32
+
33
+ ```swift
34
+ class CartViewModel {
35
+ var items: [CartItem] = []
36
+
37
+ func calculateTotal(for cartItems: [CartItem]) -> Double {
38
+ return cartItems.reduce(0) { $0 + $1.price }
39
+ }
40
+
41
+ func applyDiscount(_ discountValue: Double) {
42
+ let cappedDiscount = min(discountValue * 0.01, 0.5)
43
+ totalPrice *= (1 - cappedDiscount)
44
+ }
45
+ }
46
+
47
+ // Optional binding: Dùng shorthand Swift 5.7+
48
+ func loadProfile(user: User?) {
49
+ guard let user else { return } // OK - shorthand, không shadow
50
+ displayName.text = user.name
51
+ }
52
+ ```
53
+
54
+ **Tools:** SwiftLint (`shadowed_variable`), Xcode Warnings
55
+
@@ -0,0 +1,68 @@
1
+ ---
2
+ title: Tập trung hằng số vào enum hoặc struct
3
+ impact: MEDIUM
4
+ impactDescription: Magic number, hardcoded string rải rác khắp nơi làm khó bảo trì và dễ gây lỗi khi cần thay đổi giá trị.
5
+ tags: swift, ios, constants, magic-numbers, maintainability, code-quality
6
+ ---
7
+
8
+ ## Tập trung hằng số vào enum hoặc struct
9
+
10
+ Không để magic number, string literal, hay URL hardcoded rải rác trong code. Tập trung vào `enum` (namespace không khởi tạo được) hoặc `struct` với `static let`. Dùng `#imageLiteral` và `Asset` enum (thường được SwiftGen tạo) cho resource.
11
+
12
+ **Incorrect (magic number/string rải rác):**
13
+
14
+ ```swift
15
+ class PaymentViewController: UIViewController {
16
+ func setupUI() {
17
+ view.layer.cornerRadius = 12 // magic number
18
+ submitButton.layer.cornerRadius = 8 // magic number khác
19
+ }
20
+
21
+ func validateCard(number: String) -> Bool {
22
+ return number.count == 16 // số thẻ phải đúng 16 số?
23
+ }
24
+
25
+ func buildAPIRequest() -> URLRequest {
26
+ var request = URLRequest(url: URL(string: "https://api.example.com/v2/payments")!)
27
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
28
+ request.setValue("Bearer sk_live_abc123", forHTTPHeaderField: "Authorization")
29
+ request.timeoutInterval = 30
30
+ return request
31
+ }
32
+ }
33
+ ```
34
+
35
+ **Correct (hằng số tập trung):**
36
+
37
+ ```swift
38
+ // Constants.swift
39
+ enum Constants {
40
+ enum UI {
41
+ static let cardCornerRadius: CGFloat = 12
42
+ static let buttonCornerRadius: CGFloat = 8
43
+ }
44
+ enum Payment {
45
+ static let creditCardLength = 16
46
+ static let requestTimeout: TimeInterval = 30
47
+ }
48
+ enum API {
49
+ static let baseURL = "https://api.example.com/v2"
50
+ static let contentTypeJSON = "application/json"
51
+ }
52
+ }
53
+
54
+ // Sử dụng
55
+ class PaymentViewController: UIViewController {
56
+ func setupUI() {
57
+ view.layer.cornerRadius = Constants.UI.cardCornerRadius
58
+ submitButton.layer.cornerRadius = Constants.UI.buttonCornerRadius
59
+ }
60
+
61
+ func validateCard(number: String) -> Bool {
62
+ return number.count == Constants.Payment.creditCardLength
63
+ }
64
+ }
65
+ ```
66
+
67
+ **Tools:** SwiftLint (`magic_number`), SwiftGen (tự tạo constant cho assets/colors/strings)
68
+
@@ -0,0 +1,69 @@
1
+ ---
2
+ title: Khối catch phải log nguyên nhân gốc rễ của lỗi
3
+ impact: HIGH
4
+ impactDescription: Bắt lỗi mà không log làm mất hoàn toàn thông tin debug, khiến việc tìm nguyên nhân sự cố trở nên rất khó.
5
+ tags: swift, ios, error-handling, logging, debugging, do-catch
6
+ ---
7
+
8
+ ## Khối catch phải log nguyên nhân gốc rễ của lỗi
9
+
10
+ Mỗi `catch` block phải ghi log đầy đủ thông tin: loại lỗi, ngữ cảnh (userId, orderId...) và stack trace khi cần. Không bao giờ bắt lỗi mà không làm gì (silent fail). Dùng `os.Logger` hoặc logging framework thay cho `print()`.
11
+
12
+ **Incorrect (bắt lỗi im lặng hoặc thiếu thông tin):**
13
+
14
+ ```swift
15
+ import OSLog
16
+
17
+ func loadUserData(userId: String) async {
18
+ do {
19
+ let user = try await apiService.fetchUser(userId: userId)
20
+ displayUser(user)
21
+ } catch {
22
+ // !! Silent fail - mất hoàn toàn lỗi
23
+ }
24
+ }
25
+
26
+ func saveOrder(_ order: Order) {
27
+ do {
28
+ try coreDataManager.save(order)
29
+ } catch {
30
+ print("Error saving") // Không có context, không có error detail
31
+ }
32
+ }
33
+ ```
34
+
35
+ **Correct (log đầy đủ context):**
36
+
37
+ ```swift
38
+ import OSLog
39
+
40
+ private let logger = Logger(subsystem: "com.app.user", category: "DataLoader")
41
+
42
+ func loadUserData(userId: String) async {
43
+ do {
44
+ let user = try await apiService.fetchUser(userId: userId)
45
+ displayUser(user)
46
+ } catch let apiError as APIError {
47
+ // Log lỗi có type + context
48
+ logger.error("Failed to fetch user. userId=\(userId), error=\(apiError.code): \(apiError.localizedDescription)")
49
+ showErrorBanner(message: apiError.userMessage)
50
+ } catch {
51
+ // Lỗi unexpected - log đầy đủ
52
+ logger.error("Unexpected error loading user. userId=\(userId), error=\(error)")
53
+ showErrorBanner(message: "Không thể tải thông tin người dùng.")
54
+ }
55
+ }
56
+
57
+ func saveOrder(_ order: Order) {
58
+ do {
59
+ try coreDataManager.save(order)
60
+ logger.info("Order saved successfully. orderId=\(order.id)")
61
+ } catch {
62
+ logger.error("Failed to save order. orderId=\(order.id), error=\(error)")
63
+ showSaveFailedAlert()
64
+ }
65
+ }
66
+ ```
67
+
68
+ **Tools:** `os.Logger`, SwiftLint custom rule, Code Review
69
+
@@ -0,0 +1,77 @@
1
+ ---
2
+ title: Dùng custom Error enum thay vì NSError hay Error generic
3
+ impact: MEDIUM
4
+ impactDescription: Custom error type cung cấp type-safe error handling, dễ hiển thị thông báo người dùng và không bị mất context khi propagate qua các tầng.
5
+ tags: swift, ios, error-handling, custom-error, enum, code-quality
6
+ ---
7
+
8
+ ## Dùng custom Error enum thay vì NSError hay Error generic
9
+
10
+ Định nghĩa `enum` conform `Error` (hoặc `LocalizedError`) cho từng domain nghiệp vụ. Điều này đảm bảo các lớp xử lý lỗi tường minh theo từng case và tránh mất thông tin khi lỗi propagate.
11
+
12
+ **Incorrect (lỗi generic):**
13
+
14
+ ```swift
15
+ enum AppError: Error {
16
+ case generic // quá mơ hồ
17
+ case networkError // không có detail
18
+ case unknownError // không hữu ích
19
+ }
20
+
21
+ func fetchProduct(id: String) throws -> Product {
22
+ guard !id.isEmpty else {
23
+ throw AppError.generic // caller không biết lỗi gì
24
+ }
25
+ // ...
26
+ }
27
+ ```
28
+
29
+ **Correct (custom error với đầy đủ case và message):**
30
+
31
+ ```swift
32
+ // Mỗi domain có error type riêng
33
+ enum ProductError: LocalizedError {
34
+ case emptyProductId
35
+ case productNotFound(id: String)
36
+ case outOfStock(productId: String, available: Int)
37
+ case priceMismatch(expected: Double, actual: Double)
38
+ case networkUnavailable
39
+
40
+ var errorDescription: String? {
41
+ switch self {
42
+ case .emptyProductId:
43
+ return "ID sản phẩm không được để trống."
44
+ case .productNotFound(let id):
45
+ return "Không tìm thấy sản phẩm với ID: \(id)."
46
+ case .outOfStock(let id, let available):
47
+ return "Sản phẩm \(id) còn \(available) sản phẩm."
48
+ case .priceMismatch(let expected, let actual):
49
+ return "Giá không khớp: dự kiến \(expected), thực tế \(actual)."
50
+ case .networkUnavailable:
51
+ return "Không có kết nối mạng. Vui lòng thử lại."
52
+ }
53
+ }
54
+ }
55
+
56
+ func fetchProduct(id: String) throws -> Product {
57
+ guard !id.isEmpty else {
58
+ throw ProductError.emptyProductId
59
+ }
60
+ guard let product = productRepository.find(by: id) else {
61
+ throw ProductError.productNotFound(id: id)
62
+ }
63
+ return product
64
+ }
65
+
66
+ // Caller xử lý tường minh
67
+ do {
68
+ let product = try fetchProduct(id: productId)
69
+ } catch ProductError.outOfStock(let id, let available) {
70
+ showOutOfStockAlert(productId: id, remaining: available)
71
+ } catch let error as ProductError {
72
+ showErrorAlert(message: error.localizedDescription)
73
+ }
74
+ ```
75
+
76
+ **Tools:** Swift `Error` / `LocalizedError` protocol, Code Review
77
+