@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,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
|
+
|