pumuki-ast-hooks 5.5.49 → 5.5.51
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/hooks/git-status-monitor.ts +5 -0
- package/hooks/notify-macos.ts +1 -0
- package/hooks/pre-tool-use-evidence-validator.ts +1 -0
- package/package.json +1 -1
- package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +56 -0
- package/scripts/hooks-system/application/services/guard/GuardConfig.js +4 -2
- package/scripts/hooks-system/application/services/installation/GitEnvironmentService.js +20 -2
- package/scripts/hooks-system/application/services/installation/HookAssetsInstaller.js +0 -0
- package/scripts/hooks-system/application/services/installation/McpConfigurator.js +84 -18
- package/scripts/hooks-system/application/services/monitoring/EvidenceMonitor.js +5 -146
- package/scripts/hooks-system/application/services/monitoring/EvidenceRefreshRunner.js +161 -0
- package/scripts/hooks-system/infrastructure/ast/ast-core.js +13 -1
- package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +2 -3
- package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +1 -4
- package/scripts/hooks-system/infrastructure/ast/backend/detectors/god-class-detector.js +1 -2
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/StagedSwiftFilePreparer.js +59 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/SwiftAstRunner.js +51 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/SwiftToolchainResolver.js +57 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +27 -137
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSAstAnalysisOrchestrator.js +32 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseAnalyzer.js +34 -397
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseChecks.js +350 -0
- package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js +407 -5
- package/scripts/hooks-system/infrastructure/orchestration/__tests__/intelligent-audit.spec.js +16 -0
- package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +1 -1
- package/scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh +33 -11
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const fs = require('fs').promises;
|
|
3
3
|
const { SourceKittenParser } = require('../parsers/SourceKittenParser');
|
|
4
|
-
const
|
|
4
|
+
const checks = require('./iOSEnterpriseChecks');
|
|
5
5
|
|
|
6
6
|
class iOSEnterpriseAnalyzer {
|
|
7
7
|
constructor() {
|
|
@@ -9,230 +9,6 @@ class iOSEnterpriseAnalyzer {
|
|
|
9
9
|
this.findings = [];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
async analyzeFile(filePath, findings) {
|
|
13
|
-
this.findings = findings;
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
const ast = await this.parser.parseFile(filePath);
|
|
17
|
-
|
|
18
|
-
if (!ast.parsed) {
|
|
19
|
-
console.warn(`[iOS Enterprise] Could not parse ${filePath}: ${ast.error}`);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
24
|
-
|
|
25
|
-
const classes = this.parser.extractClasses(ast);
|
|
26
|
-
const functions = this.parser.extractFunctions(ast);
|
|
27
|
-
const properties = this.parser.extractProperties(ast);
|
|
28
|
-
const protocols = this.parser.extractProtocols(ast);
|
|
29
|
-
|
|
30
|
-
await this.analyzeSwiftModerno(ast, content, filePath);
|
|
31
|
-
await this.analyzeSwiftUI(ast, classes, filePath);
|
|
32
|
-
await this.analyzeUIKit(ast, classes, filePath);
|
|
33
|
-
await this.analyzeProtocolOriented(protocols, filePath);
|
|
34
|
-
await this.analyzeValueTypes(classes, filePath);
|
|
35
|
-
await this.analyzeMemoryManagement(content, filePath);
|
|
36
|
-
await this.analyzeOptionals(content, filePath);
|
|
37
|
-
await this.analyzeDependencyInjection(classes, filePath);
|
|
38
|
-
await this.analyzeNetworking(content, filePath);
|
|
39
|
-
await this.analyzePersistence(content, filePath);
|
|
40
|
-
await this.analyzeCombine(content, filePath);
|
|
41
|
-
await this.analyzeConcurrency(content, filePath);
|
|
42
|
-
await this.analyzeTesting(content, filePath);
|
|
43
|
-
await this.analyzeUITesting(content, filePath);
|
|
44
|
-
await this.analyzeSecurity(content, filePath);
|
|
45
|
-
await this.analyzeAccessibility(content, filePath);
|
|
46
|
-
await this.analyzeLocalization(content, filePath);
|
|
47
|
-
await this.analyzeArchitecturePatterns(classes, functions, filePath);
|
|
48
|
-
await this.analyzePerformance(functions, content, filePath);
|
|
49
|
-
await this.analyzeCodeOrganization(filePath, content);
|
|
50
|
-
|
|
51
|
-
} catch (error) {
|
|
52
|
-
console.error(`[iOS Enterprise] Error analyzing ${filePath}:`, error.message);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async analyzeSwiftModerno(ast, content, filePath) {
|
|
57
|
-
if (content.includes('completion:') && !content.includes('async ')) {
|
|
58
|
-
this.addFinding('ios.async_await_missing', 'medium', filePath, 1,
|
|
59
|
-
'Using completion handlers instead of async/await (Swift 5.9+ required)');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const taskCount = (content.match(/\bTask\s*\{/g) || []).length;
|
|
63
|
-
if (taskCount > 3 && !content.includes('TaskGroup')) {
|
|
64
|
-
this.addFinding('ios.structured_concurrency_missing', 'medium', filePath, 1,
|
|
65
|
-
`Multiple Task blocks (${taskCount}) without TaskGroup - use structured concurrency`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (content.includes('actor ') && !content.includes(': Sendable')) {
|
|
69
|
-
this.addFinding('ios.sendable_missing', 'low', filePath, 1,
|
|
70
|
-
'Actor should conform to Sendable protocol for thread-safe types');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (content.includes('func ') && content.includes('-> View') && !content.includes('some View')) {
|
|
74
|
-
this.addFinding('ios.opaque_types_missing', 'low', filePath, 1,
|
|
75
|
-
'Use "some View" instead of explicit View protocol return');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (content.includes('UIViewController') && !content.includes('@State') && !content.includes('@Binding')) {
|
|
79
|
-
this.addFinding('ios.property_wrappers_missing', 'info', filePath, 1,
|
|
80
|
-
'Consider using SwiftUI property wrappers (@State, @Binding) for state management');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const functions = this.parser.extractFunctions(ast);
|
|
84
|
-
functions.forEach(fn => {
|
|
85
|
-
if (fn.name.includes('Array') || fn.name.includes('Collection')) {
|
|
86
|
-
if (!content.includes('<T>') && !content.includes('<Element>')) {
|
|
87
|
-
this.addFinding('ios.generics_missing', 'low', filePath, fn.line,
|
|
88
|
-
`Function ${fn.name} should use generics for type safety`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async analyzeSwiftUI(ast, classes, filePath) {
|
|
95
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
96
|
-
const usesSwiftUI = this.parser.usesSwiftUI(ast);
|
|
97
|
-
const usesUIKit = this.parser.usesUIKit(ast);
|
|
98
|
-
|
|
99
|
-
if (usesUIKit && !usesSwiftUI) {
|
|
100
|
-
this.addFinding('ios.swiftui_first', 'medium', filePath, 1,
|
|
101
|
-
'Consider migrating to SwiftUI for new views (UIKit only when strictly necessary)');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (usesSwiftUI) {
|
|
105
|
-
if (!content.includes('@State')) {
|
|
106
|
-
this.addFinding('ios.state_local_missing', 'info', filePath, 1,
|
|
107
|
-
'SwiftUI view without @State - consider if local state is needed');
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (content.includes('ObservableObject') && !content.includes('@StateObject')) {
|
|
111
|
-
this.addFinding('ios.stateobject_missing', 'high', filePath, 1,
|
|
112
|
-
'ObservableObject should be owned with @StateObject, not @ObservedObject');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (content.includes('class') && content.includes('ObservableObject') && !content.includes('@EnvironmentObject')) {
|
|
116
|
-
this.addFinding('ios.environmentobject_missing', 'info', filePath, 1,
|
|
117
|
-
'Consider using @EnvironmentObject for dependency injection in SwiftUI');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (content.includes('.frame(') && content.includes('CGRect(')) {
|
|
121
|
-
this.addFinding('ios.declarativo_missing', 'medium', filePath, 1,
|
|
122
|
-
'Using imperative CGRect in SwiftUI - use declarative .frame() modifiers');
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const geometryReaderCount = (content.match(/GeometryReader/g) || []).length;
|
|
126
|
-
if (geometryReaderCount > 2) {
|
|
127
|
-
this.addFinding('ios.geometryreader_moderation', 'medium', filePath, 1,
|
|
128
|
-
`Excessive GeometryReader usage (${geometryReaderCount}x) - use only when necessary`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async analyzeUIKit(ast, classes, filePath) {
|
|
134
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
135
|
-
|
|
136
|
-
classes.forEach(cls => {
|
|
137
|
-
if (cls.name.includes('ViewController')) {
|
|
138
|
-
const linesCount = cls.substructure.length * 10;
|
|
139
|
-
if (linesCount > 300) {
|
|
140
|
-
this.addFinding('ios.massive_viewcontrollers', 'high', filePath, cls.line,
|
|
141
|
-
`Massive ViewController ${cls.name} (~${linesCount} lines) - break down into smaller components`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (!content.includes('ViewModel')) {
|
|
145
|
-
this.addFinding('ios.uikit.viewmodel_delegation', 'medium', filePath, cls.line,
|
|
146
|
-
`ViewController ${cls.name} should delegate logic to ViewModel (MVVM pattern)`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
if (filePath.endsWith('.swift') && !filePath.includes('analyzer') && !filePath.includes('detector')) {
|
|
152
|
-
if (content.includes('storyboard') || content.includes('.xib') || content.includes('.nib')) {
|
|
153
|
-
this.addFinding('ios.storyboards', 'high', filePath, 1,
|
|
154
|
-
'Storyboard/XIB detected - use programmatic UI for better version control');
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async analyzeProtocolOriented(protocols, filePath) {
|
|
160
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
161
|
-
|
|
162
|
-
if (protocols.length > 0 && !content.includes('extension ')) {
|
|
163
|
-
this.addFinding('ios.pop.missing_extensions', 'low', filePath, 1,
|
|
164
|
-
'Protocols detected but no extensions - consider protocol extensions for default implementations');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (content.includes('class ') && content.includes(': ')) {
|
|
168
|
-
const inheritanceCount = (content.match(/class\s+\w+\s*:\s*\w+/g) || []).length;
|
|
169
|
-
if (inheritanceCount > 2) {
|
|
170
|
-
this.addFinding('ios.pop.missing_composition_over_inheritance', 'medium', filePath, 1,
|
|
171
|
-
`Excessive class inheritance (${inheritanceCount}x) - prefer protocol composition`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async analyzeValueTypes(classes, filePath) {
|
|
177
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
178
|
-
|
|
179
|
-
classes.forEach(cls => {
|
|
180
|
-
if (!cls.inheritedTypes.length && !content.includes('ObservableObject')) {
|
|
181
|
-
this.addFinding('ios.values.classes_instead_structs', 'medium', filePath, cls.line,
|
|
182
|
-
`Class ${cls.name} without inheritance - consider struct for value semantics`);
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const varCount = (content.match(/\bvar\s+/g) || []).length;
|
|
187
|
-
const letCount = (content.match(/\blet\s+/g) || []).length;
|
|
188
|
-
if (varCount > letCount) {
|
|
189
|
-
this.addFinding('ios.values.mutability', 'low', filePath, 1,
|
|
190
|
-
`More var (${varCount}) than let (${letCount}) - prefer immutability`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async analyzeMemoryManagement(content, filePath) {
|
|
195
|
-
const closureMatches = content.match(/\{\s*\[/g);
|
|
196
|
-
const weakSelfMatches = content.match(/\[weak self\]/g);
|
|
197
|
-
if (closureMatches && closureMatches.length > (weakSelfMatches?.length || 0)) {
|
|
198
|
-
this.addFinding('ios.memory.missing_weak_self', 'high', filePath, 1,
|
|
199
|
-
'Closures without [weak self] - potential retain cycles');
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (content.includes('self.') && content.includes('{') && !content.includes('[weak self]')) {
|
|
203
|
-
this.addFinding('ios.memory.retain_cycles', 'high', filePath, 1,
|
|
204
|
-
'Potential retain cycle - closure captures self without [weak self]');
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (content.includes('class ') && !content.includes('deinit')) {
|
|
208
|
-
this.addFinding('ios.memory.missing_deinit', 'low', filePath, 1,
|
|
209
|
-
'Class without deinit - consider adding for cleanup verification');
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async analyzeOptionals(content, filePath) {
|
|
214
|
-
const forceUnwraps = content.match(/(\w+)\s*!/g);
|
|
215
|
-
if (forceUnwraps && forceUnwraps.length > 0) {
|
|
216
|
-
const nonIBOutlets = forceUnwraps.filter(match => !content.includes(`@IBOutlet`));
|
|
217
|
-
if (nonIBOutlets.length > 0) {
|
|
218
|
-
this.addFinding('ios.force_unwrapping', 'high', filePath, 1,
|
|
219
|
-
`Force unwrapping (!) detected ${nonIBOutlets.length}x - use if let or guard let`);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const ifLetCount = (content.match(/if\s+let\s+/g) || []).length;
|
|
224
|
-
const guardLetCount = (content.match(/guard\s+let\s+/g) || []).length;
|
|
225
|
-
if (ifLetCount === 0 && guardLetCount === 0 && content.includes('?')) {
|
|
226
|
-
this.addFinding('ios.optionals.optional_binding', 'medium', filePath, 1,
|
|
227
|
-
'Optionals present but no optional binding - use if let or guard let');
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (content.includes('?') && !content.includes('??')) {
|
|
231
|
-
this.addFinding('ios.optionals.missing_nil_coalescing', 'info', filePath, 1,
|
|
232
|
-
'Consider using nil coalescing operator (??) for default values');
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
12
|
addFinding(ruleId, severity, filePath, line, message) {
|
|
237
13
|
this.findings.push({
|
|
238
14
|
ruleId,
|
|
@@ -244,180 +20,41 @@ class iOSEnterpriseAnalyzer {
|
|
|
244
20
|
});
|
|
245
21
|
}
|
|
246
22
|
|
|
247
|
-
async
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
classes.forEach(cls => {
|
|
256
|
-
if (cls.name.includes('ViewModel') || cls.name.includes('Service')) {
|
|
257
|
-
const hasInit = content.includes(`init(`);
|
|
258
|
-
if (!hasInit) {
|
|
259
|
-
this.addFinding('ios.di.missing_protocol_injection', 'medium', filePath, cls.line,
|
|
260
|
-
`${cls.name} should inject dependencies via initializer`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
if (content.includes('init(') && content.match(/init\([^)]{50,}\)/)) {
|
|
266
|
-
this.addFinding('ios.di.missing_factory', 'low', filePath, 1,
|
|
267
|
-
'Complex initialization - consider factory pattern');
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
async analyzeNetworking(content, filePath) {
|
|
272
|
-
if (String(filePath || '').endsWith('/Package.swift') || String(filePath || '').endsWith('Package.swift')) {
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
if (!content.includes('URLSession') && !content.includes('Alamofire')) {
|
|
276
|
-
if (content.includes('http://') || content.includes('https://')) {
|
|
277
|
-
this.addFinding('ios.networking.missing_urlsession', 'high', filePath, 1,
|
|
278
|
-
'Network URLs detected but no URLSession/Alamofire usage');
|
|
23
|
+
async analyzeFile(filePath, findings) {
|
|
24
|
+
this.findings = findings;
|
|
25
|
+
try {
|
|
26
|
+
const ast = await this.parser.parseFile(filePath);
|
|
27
|
+
if (!ast.parsed) {
|
|
28
|
+
console.warn(`[iOS Enterprise] Could not parse ${filePath}: ${ast.error}`);
|
|
29
|
+
return;
|
|
279
30
|
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (content.includes('URLSession') && content.includes('completionHandler:') && !content.includes('async')) {
|
|
283
|
-
this.addFinding('ios.networking.completion_handlers_instead_async', 'medium', filePath, 1,
|
|
284
|
-
'Using completion handlers with URLSession - migrate to async/await');
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (content.includes('JSONSerialization') && !content.includes('Codable')) {
|
|
288
|
-
this.addFinding('ios.networking.missing_codable', 'medium', filePath, 1,
|
|
289
|
-
'Manual JSON parsing - use Codable for type safety');
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (content.includes('URLSession') && !content.includes('NetworkError')) {
|
|
293
|
-
this.addFinding('ios.networking.missing_error_handling', 'high', filePath, 1,
|
|
294
|
-
'Network code without custom NetworkError enum');
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Check for SSL pinning implementation
|
|
298
|
-
const hasSSLPinningImplementation =
|
|
299
|
-
content.includes('serverTrustPolicy') ||
|
|
300
|
-
content.includes('pinning') ||
|
|
301
|
-
(content.includes('URLSessionDelegate') && content.includes('URLAuthenticationChallenge'));
|
|
302
|
-
|
|
303
|
-
if (content.includes('URLSession') && !hasSSLPinningImplementation) {
|
|
304
|
-
this.addFinding('ios.networking.missing_ssl_pinning', 'medium', filePath, 1,
|
|
305
|
-
'Consider SSL pinning for high-security apps');
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (content.includes('URLSession') && !content.includes('retry')) {
|
|
309
|
-
this.addFinding('ios.networking.missing_retry', 'low', filePath, 1,
|
|
310
|
-
'Network requests without retry logic');
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
async analyzePersistence(content, filePath) {
|
|
315
|
-
if (content.includes('UserDefaults') && (content.includes('password') || content.includes('token') || content.includes('auth'))) {
|
|
316
|
-
this.addFinding('ios.persistence.userdefaults_sensitive', 'critical', filePath, 1,
|
|
317
|
-
'Sensitive data in UserDefaults - use Keychain instead');
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if ((content.includes('password') || content.includes('token')) && !content.includes('Keychain') && !content.includes('Security')) {
|
|
321
|
-
this.addFinding('ios.persistence.missing_keychain', 'critical', filePath, 1,
|
|
322
|
-
'Sensitive data detected but no Keychain usage');
|
|
323
|
-
}
|
|
324
31
|
|
|
325
|
-
|
|
326
|
-
this.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if (content.includes('Future<') && !content.includes('async')) {
|
|
353
|
-
this.addFinding('ios.combine.prefer_async_await', 'low', filePath, 1,
|
|
354
|
-
'Combine Future for single value - consider async/await instead');
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
async analyzeConcurrency(content, filePath) {
|
|
359
|
-
if (content.includes('DispatchQueue') && !content.includes('async func')) {
|
|
360
|
-
this.addFinding('ios.concurrency.dispatchqueue_old', 'medium', filePath, 1,
|
|
361
|
-
'Using DispatchQueue - prefer async/await for new code');
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (content.includes('DispatchQueue.main') && content.includes('UI')) {
|
|
365
|
-
this.addFinding('ios.concurrency.missing_mainactor', 'medium', filePath, 1,
|
|
366
|
-
'Manual main thread dispatch - use @MainActor annotation');
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (content.includes('Task {') && !content.includes('.cancel()') && !content.includes('Task.isCancelled')) {
|
|
370
|
-
this.addFinding('ios.concurrency.task_cancellation', 'low', filePath, 1,
|
|
371
|
-
'Task without cancellation handling');
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (content.includes('var ') && content.includes('queue') && !content.includes('actor')) {
|
|
375
|
-
this.addFinding('ios.concurrency.actor_missing', 'medium', filePath, 1,
|
|
376
|
-
'Manual synchronization with queue - consider actor for thread safety');
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
async analyzeTesting(content, filePath) {
|
|
381
|
-
if (filePath.includes('Test') && !content.includes('XCTest') && !content.includes('Quick')) {
|
|
382
|
-
this.addFinding('ios.testing.missing_xctest', 'high', filePath, 1,
|
|
383
|
-
'Test file without XCTest or Quick import');
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (filePath.includes('Test') && !content.includes('makeSUT') && content.includes('func test')) {
|
|
387
|
-
this.addFinding('ios.testing.missing_makesut', 'medium', filePath, 1,
|
|
388
|
-
'Test without makeSUT pattern - centralize system under test creation');
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (filePath.includes('Test') && !content.includes('trackForMemoryLeaks') && content.includes('class')) {
|
|
392
|
-
this.addFinding('ios.testing.missing_memory_leak_tracking', 'medium', filePath, 1,
|
|
393
|
-
'Test without trackForMemoryLeaks helper');
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (filePath.includes('Test') && content.includes('init(') && !content.includes('Protocol')) {
|
|
397
|
-
this.addFinding('ios.testing.concrete_dependencies', 'medium', filePath, 1,
|
|
398
|
-
'Test using concrete dependencies - inject protocols for testability');
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
async analyzeUITesting(content, filePath) {
|
|
403
|
-
if (filePath.includes('UITest') && !content.includes('XCUIApplication')) {
|
|
404
|
-
this.addFinding('ios.uitesting.missing_xcuitest', 'high', filePath, 1,
|
|
405
|
-
'UI test file without XCUIApplication');
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
if (filePath.includes('UITest') && !content.includes('accessibilityIdentifier')) {
|
|
409
|
-
this.addFinding('ios.uitesting.missing_accessibility', 'medium', filePath, 1,
|
|
410
|
-
'UI test without accessibility identifiers for element location');
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (filePath.includes('UITest') && content.includes('XCUIElement') && !content.includes('Page')) {
|
|
414
|
-
this.addFinding('ios.uitesting.missing_page_object', 'low', filePath, 1,
|
|
415
|
-
'UI test without Page Object pattern for encapsulation');
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (filePath.includes('UITest') && content.includes('.tap()') && !content.includes('waitForExistence')) {
|
|
419
|
-
this.addFinding('ios.uitesting.missing_wait', 'high', filePath, 1,
|
|
420
|
-
'UI test tapping without waitForExistence - flaky test');
|
|
32
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
33
|
+
const classes = this.parser.extractClasses(ast) || [];
|
|
34
|
+
const functions = this.parser.extractFunctions(ast) || [];
|
|
35
|
+
const protocols = this.parser.extractProtocols(ast) || [];
|
|
36
|
+
const usesSwiftUI = typeof this.parser.usesSwiftUI === 'function' ? this.parser.usesSwiftUI(ast) : false;
|
|
37
|
+
const usesUIKit = typeof this.parser.usesUIKit === 'function' ? this.parser.usesUIKit(ast) : false;
|
|
38
|
+
|
|
39
|
+
const add = (ruleId, severity, line, message) =>
|
|
40
|
+
this.addFinding(ruleId, severity, filePath, line, message);
|
|
41
|
+
|
|
42
|
+
checks.analyzeSwiftModerno({ content, functions, filePath, addFinding: add });
|
|
43
|
+
checks.analyzeSwiftUI({ usesSwiftUI, usesUIKit, content, classes, filePath, addFinding: add });
|
|
44
|
+
checks.analyzeUIKit({ classes, content, filePath, addFinding: add });
|
|
45
|
+
checks.analyzeProtocolOriented({ protocols, content, filePath, addFinding: add });
|
|
46
|
+
checks.analyzeValueTypes({ classes, content, filePath, addFinding: add });
|
|
47
|
+
checks.analyzeMemoryManagement({ content, filePath, addFinding: add });
|
|
48
|
+
checks.analyzeOptionals({ content, filePath, addFinding: add });
|
|
49
|
+
checks.analyzeDependencyInjection({ classes, content, filePath, addFinding: add });
|
|
50
|
+
checks.analyzeNetworking({ content, filePath, addFinding: add });
|
|
51
|
+
checks.analyzePersistence({ content, filePath, addFinding: add });
|
|
52
|
+
checks.analyzeCombine({ content, filePath, addFinding: add });
|
|
53
|
+
checks.analyzeConcurrency({ content, filePath, addFinding: add });
|
|
54
|
+
checks.analyzeTesting({ content, filePath, addFinding: add });
|
|
55
|
+
checks.analyzeUITesting({ content, filePath, addFinding: add });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(`[iOS Enterprise] Error analyzing ${filePath}:`, error.message);
|
|
421
58
|
}
|
|
422
59
|
}
|
|
423
60
|
|