pumuki-ast-hooks 5.5.42 → 5.5.45
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/docs/USAGE.md +27 -1
- package/package.json +1 -1
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSModernPracticesRules.js +412 -0
- package/scripts/hooks-system/infrastructure/ast/ios/ast-ios.js +8 -0
- package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +8 -0
- package/scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh +20 -5
package/docs/USAGE.md
CHANGED
|
@@ -61,7 +61,33 @@ Done! You now have AST Intelligence working in your project.
|
|
|
61
61
|
|
|
62
62
|
### Code Analysis
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
### Evidence Guard (auto-refresh)
|
|
65
|
+
|
|
66
|
+
The Evidence Guard daemon refreshes `.AI_EVIDENCE.json` periodically (default: every 180s).
|
|
67
|
+
|
|
68
|
+
Notes:
|
|
69
|
+
- The refresh updates evidence and records the current quality gate status.
|
|
70
|
+
- In auto-refresh mode, a failing quality gate does not break the daemon.
|
|
71
|
+
|
|
72
|
+
Useful commands:
|
|
73
|
+
```bash
|
|
74
|
+
# Daemon control
|
|
75
|
+
npm run ast:guard:start
|
|
76
|
+
npm run ast:guard:stop
|
|
77
|
+
npm run ast:guard:status
|
|
78
|
+
npm run ast:guard:logs
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Gate scope:
|
|
82
|
+
```bash
|
|
83
|
+
# Default is staging (only staged files)
|
|
84
|
+
AI_GATE_SCOPE=staging bash ./scripts/hooks-system/bin/update-evidence.sh --auto
|
|
85
|
+
|
|
86
|
+
# Repository-wide gate evaluation
|
|
87
|
+
AI_GATE_SCOPE=repo bash ./scripts/hooks-system/bin/update-evidence.sh --auto
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Interactive Menu (Recommended)
|
|
65
91
|
|
|
66
92
|
The library includes an **interactive menu** for selecting audit options:
|
|
67
93
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki-ast-hooks",
|
|
3
|
-
"version": "5.5.
|
|
3
|
+
"version": "5.5.45",
|
|
4
4
|
"description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iOS Modern Practices Rules (Swift 6.2 / iOS 17+ / 2026)
|
|
3
|
+
*
|
|
4
|
+
* Enforces modern Swift/iOS practices:
|
|
5
|
+
* - Forbidden third-party libraries (Alamofire, Swinject, Quick/Nimble, etc.)
|
|
6
|
+
* - Forbidden dependency managers (CocoaPods, Carthage)
|
|
7
|
+
* - Forbidden legacy patterns (GCD, ObservableObject, NavigationView, etc.)
|
|
8
|
+
* - Modern alternatives enforcement (@Observable, NavigationStack, Swift Testing, etc.)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { pushFinding } = require('../../ast-core');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const FORBIDDEN_IMPORTS = {
|
|
16
|
+
'Alamofire': {
|
|
17
|
+
severity: 'critical',
|
|
18
|
+
message: 'Alamofire is forbidden - use URLSession with async/await',
|
|
19
|
+
suggestion: 'Replace with native URLSession. See rulesios.mdc for APIClient example.'
|
|
20
|
+
},
|
|
21
|
+
'Swinject': {
|
|
22
|
+
severity: 'critical',
|
|
23
|
+
message: 'Swinject is forbidden - use manual DI or @Environment',
|
|
24
|
+
suggestion: 'Use initializer injection or SwiftUI @Environment for dependency injection.'
|
|
25
|
+
},
|
|
26
|
+
'Quick': {
|
|
27
|
+
severity: 'critical',
|
|
28
|
+
message: 'Quick is forbidden - use Swift Testing framework',
|
|
29
|
+
suggestion: 'Migrate to Swift Testing with @Test, @Suite, #expect, #require.'
|
|
30
|
+
},
|
|
31
|
+
'Nimble': {
|
|
32
|
+
severity: 'critical',
|
|
33
|
+
message: 'Nimble is forbidden - use Swift Testing framework',
|
|
34
|
+
suggestion: 'Use #expect and #require from Swift Testing instead of Nimble matchers.'
|
|
35
|
+
},
|
|
36
|
+
'RxSwift': {
|
|
37
|
+
severity: 'high',
|
|
38
|
+
message: 'RxSwift is discouraged - use Combine or async/await',
|
|
39
|
+
suggestion: 'Migrate to Combine for reactive streams or async/await for single values.'
|
|
40
|
+
},
|
|
41
|
+
'RxCocoa': {
|
|
42
|
+
severity: 'high',
|
|
43
|
+
message: 'RxCocoa is discouraged - use Combine or async/await',
|
|
44
|
+
suggestion: 'Migrate to Combine for UI bindings.'
|
|
45
|
+
},
|
|
46
|
+
'Realm': {
|
|
47
|
+
severity: 'high',
|
|
48
|
+
message: 'Realm is discouraged - use SwiftData (iOS 17+) or Core Data',
|
|
49
|
+
suggestion: 'Migrate to SwiftData for modern persistence.'
|
|
50
|
+
},
|
|
51
|
+
'GRDB': {
|
|
52
|
+
severity: 'medium',
|
|
53
|
+
message: 'GRDB is discouraged - prefer SwiftData for new projects',
|
|
54
|
+
suggestion: 'Consider SwiftData for new development.'
|
|
55
|
+
},
|
|
56
|
+
'KeychainAccess': {
|
|
57
|
+
severity: 'medium',
|
|
58
|
+
message: 'KeychainAccess wrapper is discouraged - use native KeychainServices',
|
|
59
|
+
suggestion: 'Use Security framework KeychainServices directly.'
|
|
60
|
+
},
|
|
61
|
+
'SwiftyJSON': {
|
|
62
|
+
severity: 'critical',
|
|
63
|
+
message: 'SwiftyJSON is forbidden - use Codable',
|
|
64
|
+
suggestion: 'Use native Codable protocol for JSON parsing.'
|
|
65
|
+
},
|
|
66
|
+
'ObjectMapper': {
|
|
67
|
+
severity: 'critical',
|
|
68
|
+
message: 'ObjectMapper is forbidden - use Codable',
|
|
69
|
+
suggestion: 'Use native Codable protocol for JSON mapping.'
|
|
70
|
+
},
|
|
71
|
+
'Kingfisher': {
|
|
72
|
+
severity: 'medium',
|
|
73
|
+
message: 'Kingfisher is discouraged - use AsyncImage (iOS 15+)',
|
|
74
|
+
suggestion: 'Use SwiftUI AsyncImage for image loading.'
|
|
75
|
+
},
|
|
76
|
+
'SDWebImage': {
|
|
77
|
+
severity: 'medium',
|
|
78
|
+
message: 'SDWebImage is discouraged - use AsyncImage (iOS 15+)',
|
|
79
|
+
suggestion: 'Use SwiftUI AsyncImage for image loading.'
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const FORBIDDEN_PATTERNS = [
|
|
84
|
+
{
|
|
85
|
+
pattern: /DispatchQueue\.main\.async\s*\{/,
|
|
86
|
+
ruleId: 'ios.concurrency.forbidden_gcd_main',
|
|
87
|
+
severity: 'high',
|
|
88
|
+
message: 'DispatchQueue.main.async is forbidden - use @MainActor or MainActor.run',
|
|
89
|
+
suggestion: 'Use: await MainActor.run { } or mark function with @MainActor'
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
pattern: /DispatchQueue\.global\(\)\.async\s*\{/,
|
|
93
|
+
ruleId: 'ios.concurrency.forbidden_gcd_global',
|
|
94
|
+
severity: 'high',
|
|
95
|
+
message: 'DispatchQueue.global().async is forbidden - use Task { }',
|
|
96
|
+
suggestion: 'Use: Task { await ... } for background work'
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
pattern: /DispatchGroup\s*\(/,
|
|
100
|
+
ruleId: 'ios.concurrency.forbidden_dispatch_group',
|
|
101
|
+
severity: 'high',
|
|
102
|
+
message: 'DispatchGroup is forbidden - use TaskGroup',
|
|
103
|
+
suggestion: 'Use: await withTaskGroup(of:) { group in ... }'
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
pattern: /DispatchSemaphore\s*\(/,
|
|
107
|
+
ruleId: 'ios.concurrency.forbidden_dispatch_semaphore',
|
|
108
|
+
severity: 'high',
|
|
109
|
+
message: 'DispatchSemaphore is forbidden - use actor or async/await',
|
|
110
|
+
suggestion: 'Use actor for thread-safe state or AsyncStream for synchronization'
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
pattern: /OperationQueue\s*\(/,
|
|
114
|
+
ruleId: 'ios.concurrency.forbidden_operation_queue',
|
|
115
|
+
severity: 'medium',
|
|
116
|
+
message: 'OperationQueue is discouraged - use TaskGroup for most cases',
|
|
117
|
+
suggestion: 'Use TaskGroup unless you need complex cancellation dependencies'
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
pattern: /:\s*ObservableObject\b/,
|
|
121
|
+
ruleId: 'ios.swiftui.deprecated_observable_object',
|
|
122
|
+
severity: 'high',
|
|
123
|
+
message: 'ObservableObject is deprecated for iOS 17+ - use @Observable macro',
|
|
124
|
+
suggestion: 'Replace class: ObservableObject with @Observable final class'
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
pattern: /@Published\s+var\b/,
|
|
128
|
+
ruleId: 'ios.swiftui.deprecated_published',
|
|
129
|
+
severity: 'medium',
|
|
130
|
+
message: '@Published is deprecated for iOS 17+ - use @Observable macro',
|
|
131
|
+
suggestion: 'With @Observable, properties are automatically observed without @Published'
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
pattern: /@StateObject\s+var\b/,
|
|
135
|
+
ruleId: 'ios.swiftui.deprecated_state_object',
|
|
136
|
+
severity: 'medium',
|
|
137
|
+
message: '@StateObject is deprecated for iOS 17+ - use @State with @Observable',
|
|
138
|
+
suggestion: 'Use @State var viewModel = ViewModel() with @Observable ViewModel'
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
pattern: /@ObservedObject\s+var\b/,
|
|
142
|
+
ruleId: 'ios.swiftui.deprecated_observed_object',
|
|
143
|
+
severity: 'medium',
|
|
144
|
+
message: '@ObservedObject is deprecated for iOS 17+ - use @Bindable',
|
|
145
|
+
suggestion: 'Use @Bindable var viewModel for @Observable objects passed as parameters'
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
pattern: /@EnvironmentObject\s+var\b/,
|
|
149
|
+
ruleId: 'ios.swiftui.deprecated_environment_object',
|
|
150
|
+
severity: 'medium',
|
|
151
|
+
message: '@EnvironmentObject is deprecated for iOS 17+ - use @Environment',
|
|
152
|
+
suggestion: 'Use @Environment with custom EnvironmentKey for DI'
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
pattern: /\bNavigationView\s*\{/,
|
|
156
|
+
ruleId: 'ios.swiftui.deprecated_navigation_view',
|
|
157
|
+
severity: 'high',
|
|
158
|
+
message: 'NavigationView is deprecated - use NavigationStack (iOS 16+)',
|
|
159
|
+
suggestion: 'Replace NavigationView with NavigationStack for modern navigation'
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
pattern: /\bAnyView\s*\(/,
|
|
163
|
+
ruleId: 'ios.swiftui.forbidden_any_view',
|
|
164
|
+
severity: 'high',
|
|
165
|
+
message: 'AnyView is forbidden - it breaks SwiftUI diffing and hurts performance',
|
|
166
|
+
suggestion: 'Use @ViewBuilder, generics, or conditional views instead'
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
pattern: /NSLocalizedString\s*\(/,
|
|
170
|
+
ruleId: 'ios.i18n.deprecated_nslocalized_string',
|
|
171
|
+
severity: 'medium',
|
|
172
|
+
message: 'NSLocalizedString is deprecated - use String(localized:)',
|
|
173
|
+
suggestion: 'Use String(localized: "key") for iOS 16+ or String Catalogs'
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
pattern: /JSONSerialization\./,
|
|
177
|
+
ruleId: 'ios.codable.forbidden_json_serialization',
|
|
178
|
+
severity: 'critical',
|
|
179
|
+
message: 'JSONSerialization is forbidden - use Codable',
|
|
180
|
+
suggestion: 'Use JSONDecoder/JSONEncoder with Codable structs'
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
pattern: /NSAttributedString\s*\(/,
|
|
184
|
+
ruleId: 'ios.modern.deprecated_nsattributed_string',
|
|
185
|
+
severity: 'low',
|
|
186
|
+
message: 'NSAttributedString is legacy - consider AttributedString (iOS 15+)',
|
|
187
|
+
suggestion: 'Use AttributedString for new code'
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
pattern: /\.onAppear\s*\{\s*Task\s*\{/,
|
|
191
|
+
ruleId: 'ios.swiftui.onappear_task_antipattern',
|
|
192
|
+
severity: 'medium',
|
|
193
|
+
message: '.onAppear { Task { } } is an anti-pattern - use .task modifier',
|
|
194
|
+
suggestion: 'Use .task { await loadData() } which auto-cancels when View disappears'
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
pattern: /completion\s*:\s*@escaping\s*\([^)]*\)\s*->\s*Void/,
|
|
198
|
+
ruleId: 'ios.concurrency.completion_handler',
|
|
199
|
+
severity: 'medium',
|
|
200
|
+
message: 'Completion handlers are discouraged - use async/await',
|
|
201
|
+
suggestion: 'Convert to async function: func fetch() async throws -> Data'
|
|
202
|
+
}
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
const FORBIDDEN_FILES = [
|
|
206
|
+
{
|
|
207
|
+
filename: 'Podfile',
|
|
208
|
+
ruleId: 'ios.deps.forbidden_cocoapods',
|
|
209
|
+
severity: 'critical',
|
|
210
|
+
message: 'CocoaPods is forbidden - use Swift Package Manager',
|
|
211
|
+
suggestion: 'Migrate dependencies to Package.swift'
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
filename: 'Podfile.lock',
|
|
215
|
+
ruleId: 'ios.deps.forbidden_cocoapods',
|
|
216
|
+
severity: 'critical',
|
|
217
|
+
message: 'CocoaPods is forbidden - use Swift Package Manager',
|
|
218
|
+
suggestion: 'Remove Podfile.lock and migrate to SPM'
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
filename: 'Cartfile',
|
|
222
|
+
ruleId: 'ios.deps.forbidden_carthage',
|
|
223
|
+
severity: 'critical',
|
|
224
|
+
message: 'Carthage is forbidden - use Swift Package Manager',
|
|
225
|
+
suggestion: 'Migrate dependencies to Package.swift'
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
filename: 'Cartfile.resolved',
|
|
229
|
+
ruleId: 'ios.deps.forbidden_carthage',
|
|
230
|
+
severity: 'critical',
|
|
231
|
+
message: 'Carthage is forbidden - use Swift Package Manager',
|
|
232
|
+
suggestion: 'Remove Cartfile.resolved and migrate to SPM'
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
filename: 'Localizable.strings',
|
|
236
|
+
ruleId: 'ios.i18n.deprecated_localizable_strings',
|
|
237
|
+
severity: 'medium',
|
|
238
|
+
message: 'Localizable.strings is deprecated - use String Catalogs (.xcstrings)',
|
|
239
|
+
suggestion: 'Migrate to String Catalogs in Xcode 15+'
|
|
240
|
+
}
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
class iOSModernPracticesRules {
|
|
244
|
+
constructor(findings, projectRoot) {
|
|
245
|
+
this.findings = findings;
|
|
246
|
+
this.projectRoot = projectRoot;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
analyze() {
|
|
250
|
+
this.checkForbiddenFiles();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
analyzeFile(filePath, content) {
|
|
254
|
+
if (!content) {
|
|
255
|
+
try {
|
|
256
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
257
|
+
} catch {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.checkForbiddenImports(filePath, content);
|
|
263
|
+
this.checkForbiddenPatterns(filePath, content);
|
|
264
|
+
this.checkModernAlternatives(filePath, content);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
checkForbiddenFiles() {
|
|
268
|
+
for (const forbidden of FORBIDDEN_FILES) {
|
|
269
|
+
const possiblePaths = [
|
|
270
|
+
path.join(this.projectRoot, forbidden.filename),
|
|
271
|
+
path.join(this.projectRoot, 'ios', forbidden.filename),
|
|
272
|
+
path.join(this.projectRoot, 'iOS', forbidden.filename)
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
for (const filePath of possiblePaths) {
|
|
276
|
+
if (fs.existsSync(filePath)) {
|
|
277
|
+
pushFinding(this.findings, {
|
|
278
|
+
ruleId: forbidden.ruleId,
|
|
279
|
+
severity: forbidden.severity,
|
|
280
|
+
message: forbidden.message,
|
|
281
|
+
filePath: filePath,
|
|
282
|
+
line: 1,
|
|
283
|
+
suggestion: forbidden.suggestion
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
checkForbiddenImports(filePath, content) {
|
|
291
|
+
const lines = content.split('\n');
|
|
292
|
+
|
|
293
|
+
for (let i = 0; i < lines.length; i++) {
|
|
294
|
+
const line = lines[i].trim();
|
|
295
|
+
|
|
296
|
+
if (line.startsWith('import ')) {
|
|
297
|
+
const importName = line.replace('import ', '').trim();
|
|
298
|
+
|
|
299
|
+
if (FORBIDDEN_IMPORTS[importName]) {
|
|
300
|
+
const rule = FORBIDDEN_IMPORTS[importName];
|
|
301
|
+
pushFinding(this.findings, {
|
|
302
|
+
ruleId: `ios.imports.forbidden_${importName.toLowerCase()}`,
|
|
303
|
+
severity: rule.severity,
|
|
304
|
+
message: rule.message,
|
|
305
|
+
filePath,
|
|
306
|
+
line: i + 1,
|
|
307
|
+
suggestion: rule.suggestion
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
checkForbiddenPatterns(filePath, content) {
|
|
315
|
+
const isTestFile = /\.(spec|test)\.swift$/i.test(filePath) || filePath.includes('Tests/');
|
|
316
|
+
|
|
317
|
+
for (const pattern of FORBIDDEN_PATTERNS) {
|
|
318
|
+
if (pattern.ruleId.includes('deprecated_') && isTestFile) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const match = content.match(pattern.pattern);
|
|
323
|
+
if (match) {
|
|
324
|
+
const line = this.findLineNumber(content, match[0]);
|
|
325
|
+
pushFinding(this.findings, {
|
|
326
|
+
ruleId: pattern.ruleId,
|
|
327
|
+
severity: pattern.severity,
|
|
328
|
+
message: pattern.message,
|
|
329
|
+
filePath,
|
|
330
|
+
line,
|
|
331
|
+
suggestion: pattern.suggestion
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
checkModernAlternatives(filePath, content) {
|
|
338
|
+
const hasSwiftUI = content.includes('import SwiftUI');
|
|
339
|
+
|
|
340
|
+
if (hasSwiftUI) {
|
|
341
|
+
const hasNavigationStack = content.includes('NavigationStack');
|
|
342
|
+
const hasNavigationLink = content.includes('NavigationLink');
|
|
343
|
+
const hasNavigationDestination = content.includes('.navigationDestination');
|
|
344
|
+
|
|
345
|
+
if (hasNavigationLink && !hasNavigationStack && !hasNavigationDestination) {
|
|
346
|
+
pushFinding(this.findings, {
|
|
347
|
+
ruleId: 'ios.swiftui.legacy_navigation',
|
|
348
|
+
severity: 'medium',
|
|
349
|
+
message: 'Using NavigationLink without NavigationStack - consider modern navigation',
|
|
350
|
+
filePath,
|
|
351
|
+
line: this.findLineNumber(content, 'NavigationLink'),
|
|
352
|
+
suggestion: 'Use NavigationStack with .navigationDestination(for:) for type-safe navigation'
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const hasViewThatFits = content.includes('ViewThatFits');
|
|
357
|
+
const hasGeometryReader = (content.match(/GeometryReader/g) || []).length;
|
|
358
|
+
|
|
359
|
+
if (hasGeometryReader > 2 && !hasViewThatFits) {
|
|
360
|
+
pushFinding(this.findings, {
|
|
361
|
+
ruleId: 'ios.swiftui.excessive_geometry_reader',
|
|
362
|
+
severity: 'low',
|
|
363
|
+
message: `Multiple GeometryReader usage (${hasGeometryReader}) - consider ViewThatFits or Layout protocol`,
|
|
364
|
+
filePath,
|
|
365
|
+
line: this.findLineNumber(content, 'GeometryReader'),
|
|
366
|
+
suggestion: 'Use ViewThatFits for adaptive layouts or custom Layout protocol (iOS 16+)'
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const hasXCTest = content.includes('import XCTest');
|
|
372
|
+
const hasSwiftTesting = content.includes('import Testing');
|
|
373
|
+
const isTestFile = filePath.includes('Tests') || filePath.includes('Spec');
|
|
374
|
+
|
|
375
|
+
if (isTestFile && hasXCTest && !hasSwiftTesting) {
|
|
376
|
+
const hasTestMacro = content.includes('@Test');
|
|
377
|
+
|
|
378
|
+
if (!hasTestMacro) {
|
|
379
|
+
pushFinding(this.findings, {
|
|
380
|
+
ruleId: 'ios.testing.legacy_xctest',
|
|
381
|
+
severity: 'low',
|
|
382
|
+
message: 'Using XCTest - consider Swift Testing for new tests',
|
|
383
|
+
filePath,
|
|
384
|
+
line: this.findLineNumber(content, 'import XCTest'),
|
|
385
|
+
suggestion: 'Use import Testing with @Test, @Suite, #expect, #require for modern testing'
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (content.includes('any ') && !content.includes('// any intentional')) {
|
|
391
|
+
const anyCount = (content.match(/\bany\s+\w+Protocol\b/g) || []).length;
|
|
392
|
+
if (anyCount > 3) {
|
|
393
|
+
pushFinding(this.findings, {
|
|
394
|
+
ruleId: 'ios.generics.excessive_type_erasure',
|
|
395
|
+
severity: 'medium',
|
|
396
|
+
message: `Excessive type erasure with 'any' (${anyCount} occurrences) - use generics`,
|
|
397
|
+
filePath,
|
|
398
|
+
line: 1,
|
|
399
|
+
suggestion: 'Use generic constraints instead of any for better performance'
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
findLineNumber(content, text) {
|
|
406
|
+
const idx = content.indexOf(text);
|
|
407
|
+
if (idx === -1) return 1;
|
|
408
|
+
return content.substring(0, idx).split('\n').length;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
module.exports = { iOSModernPracticesRules };
|
|
@@ -16,6 +16,7 @@ const { iOSNetworkingAdvancedRules } = require(path.join(__dirname, 'analyzers/i
|
|
|
16
16
|
const { iOSCICDRules } = require(path.join(__dirname, 'analyzers/iOSCICDRules'));
|
|
17
17
|
const { iOSForbiddenLiteralsAnalyzer } = require(path.join(__dirname, 'analyzers/iOSForbiddenLiteralsAnalyzer'));
|
|
18
18
|
const { iOSASTIntelligentAnalyzer } = require(path.join(__dirname, 'analyzers/iOSASTIntelligentAnalyzer'));
|
|
19
|
+
const { iOSModernPracticesRules } = require(path.join(__dirname, 'analyzers/iOSModernPracticesRules'));
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Run iOS-specific AST intelligence analysis
|
|
@@ -181,6 +182,13 @@ async function runIOSIntelligence(project, findings, platform) {
|
|
|
181
182
|
const cicdRules = new iOSCICDRules(findings, repoRoot);
|
|
182
183
|
cicdRules.analyze();
|
|
183
184
|
|
|
185
|
+
console.error(`[iOS Modern Practices] Analyzing Swift 6.2 / iOS 17+ compliance...`);
|
|
186
|
+
const modernPractices = new iOSModernPracticesRules(findings, repoRoot);
|
|
187
|
+
modernPractices.analyze();
|
|
188
|
+
swiftFiles.forEach(swiftFile => {
|
|
189
|
+
modernPractices.analyzeFile(swiftFile, null);
|
|
190
|
+
});
|
|
191
|
+
|
|
184
192
|
const analyzer = new iOSEnterpriseAnalyzer();
|
|
185
193
|
|
|
186
194
|
for (const swiftFile of swiftFiles) {
|
|
@@ -254,6 +254,9 @@ async function runIntelligentAudit() {
|
|
|
254
254
|
const rawViolations = loadRawViolations();
|
|
255
255
|
console.log(`[Intelligent Audit] Loaded ${rawViolations.length} violations from AST`);
|
|
256
256
|
|
|
257
|
+
const autoEvidenceTrigger = String(env.get('AUTO_EVIDENCE_TRIGGER', process.env.AUTO_EVIDENCE_TRIGGER || '') || '').trim().toLowerCase();
|
|
258
|
+
const isAutoEvidenceRefresh = autoEvidenceTrigger === 'auto';
|
|
259
|
+
|
|
257
260
|
const gateScope = String(env.get('AI_GATE_SCOPE', 'staging') || 'staging').trim().toLowerCase();
|
|
258
261
|
const isRepoScope = gateScope === 'repo' || gateScope === 'repository';
|
|
259
262
|
|
|
@@ -311,6 +314,11 @@ async function runIntelligentAudit() {
|
|
|
311
314
|
const gatePolicies = new GatePolicies();
|
|
312
315
|
const gateResult = gatePolicies.apply(enhancedViolations);
|
|
313
316
|
|
|
317
|
+
if (isAutoEvidenceRefresh && !gateResult.passed) {
|
|
318
|
+
console.log('[Intelligent Audit] ℹ️ Auto evidence refresh: preserving gate status but not failing process exit code');
|
|
319
|
+
gateResult.exitCode = 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
314
322
|
console.log(`[Intelligent Audit] Gate status: ${gateResult.passed ? '✅ PASSED' : '❌ FAILED'}`);
|
|
315
323
|
if (gateResult.blockedBy) {
|
|
316
324
|
console.log(`[Intelligent Audit] Blocked by: ${gateResult.blockedBy} violations`);
|
|
@@ -94,12 +94,27 @@ refresh_evidence() {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
lint_hooks_system() {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
local repo_pkg="${REPO_ROOT}/package.json"
|
|
98
|
+
local hooks_pkg="${REPO_ROOT}/scripts/hooks-system/package.json"
|
|
99
|
+
|
|
100
|
+
if [[ -f "${repo_pkg}" ]]; then
|
|
101
|
+
printf "${CYAN}🔎 Ejecutando lint (repo root)...${NC}\n"
|
|
102
|
+
if npm --prefix "${REPO_ROOT}" run lint:hooks; then
|
|
103
|
+
printf "${GREEN}✅ Lint hooks-system OK.${NC}\n"
|
|
104
|
+
return 0
|
|
105
|
+
fi
|
|
102
106
|
fi
|
|
107
|
+
|
|
108
|
+
if [[ -f "${hooks_pkg}" ]]; then
|
|
109
|
+
printf "${CYAN}🔎 Ejecutando lint (scripts/hooks-system)...${NC}\n"
|
|
110
|
+
if npm --prefix "${REPO_ROOT}/scripts/hooks-system" run lint:hooks; then
|
|
111
|
+
printf "${GREEN}✅ Lint hooks-system OK.${NC}\n"
|
|
112
|
+
return 0
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
printf "${RED}❌ Lint hooks-system falló.${NC}\n"
|
|
117
|
+
return 1
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
run_mobile_checks() {
|