@sun-asterisk/sunlint 1.3.48 → 1.3.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/file-targeting-service.js +148 -15
- package/core/init-command.js +118 -70
- package/core/project-detector.js +517 -0
- package/core/tui-select.js +245 -0
- package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
- package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
- package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
- package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
- package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
- package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
- package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
- package/package.json +1 -1
- package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
- package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
- package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
- package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
|
@@ -0,0 +1,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
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Tách biệt logic xử lý và truy cập dữ liệu trong tầng service
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Kết hợp logic nghiệp vụ và data access trong cùng một class làm khó test, khó thay đổi storage layer và vi phạm Single Responsibility.
|
|
5
|
+
tags: swift, ios, architecture, repository-pattern, service, code-quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Tách biệt logic xử lý và truy cập dữ liệu trong tầng service
|
|
9
|
+
|
|
10
|
+
ViewModel/Presenter không nên gọi trực tiếp Core Data, UserDefaults, hay network. Tách thành **Repository** (data access) và **UseCase/Service** (business logic). Điều này tuân theo kiến trúc Clean Architecture được Apple khuyến nghị.
|
|
11
|
+
|
|
12
|
+
**Incorrect (trộn lẫn nghiệp vụ và data access):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
class OrderViewModel {
|
|
16
|
+
func placeOrder(items: [CartItem], userId: String) {
|
|
17
|
+
// Gọi API trực tiếp trong ViewModel
|
|
18
|
+
let request = URLRequest(url: URL(string: "https://api.example.com/orders")!)
|
|
19
|
+
URLSession.shared.dataTask(with: request) { data, _, error in
|
|
20
|
+
// Xử lý JSON thủ công trong ViewModel
|
|
21
|
+
guard let data = data,
|
|
22
|
+
let order = try? JSONDecoder().decode(Order.self, from: data) else { return }
|
|
23
|
+
|
|
24
|
+
// Lưu Core Data trực tiếp trong ViewModel
|
|
25
|
+
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
|
|
26
|
+
let entity = OrderEntity(context: context)
|
|
27
|
+
entity.id = order.id
|
|
28
|
+
try? context.save()
|
|
29
|
+
}.resume()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Correct (Repository + UseCase tách biệt):**
|
|
35
|
+
|
|
36
|
+
```swift
|
|
37
|
+
// Repository: chỉ lo data access
|
|
38
|
+
protocol OrderRepositoryProtocol {
|
|
39
|
+
func createOrder(request: CreateOrderRequest) async throws -> Order
|
|
40
|
+
func saveOrderLocally(_ order: Order) throws
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class OrderRepository: OrderRepositoryProtocol {
|
|
44
|
+
private let apiClient: APIClientProtocol
|
|
45
|
+
private let coreDataManager: CoreDataManagerProtocol
|
|
46
|
+
|
|
47
|
+
init(apiClient: APIClientProtocol, coreDataManager: CoreDataManagerProtocol) {
|
|
48
|
+
self.apiClient = apiClient
|
|
49
|
+
self.coreDataManager = coreDataManager
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func createOrder(request: CreateOrderRequest) async throws -> Order {
|
|
53
|
+
return try await apiClient.post("/orders", body: request)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func saveOrderLocally(_ order: Order) throws {
|
|
57
|
+
try coreDataManager.insert(order, entityType: OrderEntity.self)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// UseCase: chỉ lo nghiệp vụ
|
|
62
|
+
class PlaceOrderUseCase {
|
|
63
|
+
private let orderRepository: OrderRepositoryProtocol
|
|
64
|
+
private let inventoryService: InventoryServiceProtocol
|
|
65
|
+
|
|
66
|
+
func execute(items: [CartItem], userId: String) async throws -> Order {
|
|
67
|
+
// Kiểm tra tồn kho
|
|
68
|
+
try await inventoryService.validateAvailability(items: items)
|
|
69
|
+
let request = CreateOrderRequest(items: items, userId: userId)
|
|
70
|
+
let order = try await orderRepository.createOrder(request: request)
|
|
71
|
+
try orderRepository.saveOrderLocally(order)
|
|
72
|
+
return order
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ViewModel: chỉ gọi UseCase
|
|
77
|
+
class OrderViewModel {
|
|
78
|
+
private let placeOrderUseCase: PlaceOrderUseCase
|
|
79
|
+
|
|
80
|
+
func placeOrder(items: [CartItem]) async {
|
|
81
|
+
do {
|
|
82
|
+
order = try await placeOrderUseCase.execute(items: items, userId: currentUserId)
|
|
83
|
+
} catch { /* handle */ }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Tools:** Code Review, Unit Tests (mock repository dễ dàng)
|
|
89
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Log đầy đủ context khi xử lý lỗi
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Thiếu context trong log làm mất khả năng correlate sự kiện, không tái hiện được lỗi production và tăng MTTR (Mean Time To Resolve).
|
|
5
|
+
tags: swift, ios, logging, error-handling, debugging, observability
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Log đầy đủ context khi xử lý lỗi
|
|
9
|
+
|
|
10
|
+
Khi log lỗi, phải bao gồm: định danh entity (userId, orderId), action đang thực hiện, loại lỗi cụ thể. Dùng `os.Logger` với privacy annotation để tránh log PII ra console.
|
|
11
|
+
|
|
12
|
+
**Incorrect (log thiếu context):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
private let logger = Logger()
|
|
16
|
+
|
|
17
|
+
func processPayment(_ payment: Payment) async {
|
|
18
|
+
do {
|
|
19
|
+
try await paymentService.charge(payment)
|
|
20
|
+
} catch {
|
|
21
|
+
logger.error("Payment failed") // !! Không biết payment nào, tại sao?
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func syncUserData() async {
|
|
26
|
+
do {
|
|
27
|
+
try await syncService.fullSync()
|
|
28
|
+
} catch {
|
|
29
|
+
print("Sync error: \(error)") // print thay vì logger, không có user context
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Correct (log đầy đủ context với privacy):**
|
|
35
|
+
|
|
36
|
+
```swift
|
|
37
|
+
import OSLog
|
|
38
|
+
|
|
39
|
+
private let logger = Logger(subsystem: "com.app.payment", category: "Processing")
|
|
40
|
+
|
|
41
|
+
func processPayment(_ payment: Payment) async {
|
|
42
|
+
do {
|
|
43
|
+
try await paymentService.charge(payment)
|
|
44
|
+
logger.info("Payment succeeded. paymentId=\(payment.id, privacy: .public), amount=\(payment.amount)")
|
|
45
|
+
} catch PaymentError.insufficientFunds {
|
|
46
|
+
// Lỗi business - warning
|
|
47
|
+
logger.warning("Insufficient funds. paymentId=\(payment.id, privacy: .public), userId=\(payment.userId, privacy: .private)")
|
|
48
|
+
} catch PaymentError.cardDeclined(let code) {
|
|
49
|
+
logger.error("Card declined. paymentId=\(payment.id, privacy: .public), declineCode=\(code, privacy: .public)")
|
|
50
|
+
} catch {
|
|
51
|
+
// Lỗi không ngờ - error level với full context
|
|
52
|
+
logger.error("Unexpected payment failure. paymentId=\(payment.id, privacy: .public), error=\(error.localizedDescription, privacy: .public)")
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Privacy annotation cho os.Logger:**
|
|
58
|
+
|
|
59
|
+
| Annotation | Khi nào dùng |
|
|
60
|
+
|-----------|-------------|
|
|
61
|
+
| `.public` | ID kỹ thuật, error code (an toàn để log) |
|
|
62
|
+
| `.private` | Email, tên, phone (ẩn trong log, chỉ hiện khi debug) |
|
|
63
|
+
| `.sensitive` | Token, mật khẩu (luôn ẩn) |
|
|
64
|
+
|
|
65
|
+
**Tools:** `os.Logger` với Privacy API, Code Review
|
|
66
|
+
|