pumuki-ast-hooks 5.5.46 → 5.5.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/docs/CODE_STANDARDS.md +5 -0
  2. package/docs/VIOLATIONS_RESOLUTION_PLAN.md +22 -34
  3. package/package.json +2 -2
  4. package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +36 -0
  5. package/scripts/hooks-system/application/services/installation/FileSystemInstallerService.js +1 -1
  6. package/scripts/hooks-system/application/services/installation/VSCodeTaskConfigurator.js +8 -2
  7. package/scripts/hooks-system/bin/gitflow-cycle.js +0 -0
  8. package/scripts/hooks-system/config/project.config.json +1 -1
  9. package/scripts/hooks-system/infrastructure/ast/android/analyzers/AndroidSOLIDAnalyzer.js +11 -255
  10. package/scripts/hooks-system/infrastructure/ast/android/detectors/android-solid-detectors.js +227 -0
  11. package/scripts/hooks-system/infrastructure/ast/ast-core.js +12 -3
  12. package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +36 -13
  13. package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +10 -83
  14. package/scripts/hooks-system/infrastructure/ast/backend/detectors/god-class-detector.js +83 -0
  15. package/scripts/hooks-system/infrastructure/ast/common/ast-common.js +17 -2
  16. package/scripts/hooks-system/infrastructure/ast/frontend/analyzers/FrontendArchitectureDetector.js +12 -142
  17. package/scripts/hooks-system/infrastructure/ast/frontend/detectors/frontend-architecture-strategies.js +126 -0
  18. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +30 -783
  19. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSArchitectureDetector.js +21 -224
  20. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSArchitectureRules.js +18 -605
  21. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSModernPracticesRules.js +4 -1
  22. package/scripts/hooks-system/infrastructure/ast/ios/ast-ios.js +4 -1
  23. package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-architecture-rules-strategies.js +595 -0
  24. package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-architecture-strategies.js +192 -0
  25. package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-ast-intelligent-strategies.js +789 -0
  26. package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-god-class-detector.js +79 -0
  27. package/scripts/hooks-system/infrastructure/ast/ios/native-bridge.js +4 -1
  28. package/skills/android-guidelines/SKILL.md +1 -0
  29. package/skills/backend-guidelines/SKILL.md +1 -0
  30. package/skills/frontend-guidelines/SKILL.md +1 -0
  31. package/skills/ios-guidelines/SKILL.md +1 -0
@@ -0,0 +1,789 @@
1
+ const path = require('path');
2
+
3
+ function resetCollections(analyzer) {
4
+ analyzer.allNodes = [];
5
+ analyzer.imports = [];
6
+ analyzer.classes = [];
7
+ analyzer.structs = [];
8
+ analyzer.protocols = [];
9
+ analyzer.functions = [];
10
+ analyzer.properties = [];
11
+ analyzer.closures = [];
12
+ }
13
+
14
+ function collectAllNodes(analyzer, nodes, parent) {
15
+ if (!Array.isArray(nodes)) return;
16
+
17
+ for (const node of nodes) {
18
+ const kind = node['key.kind'] || '';
19
+ node._parent = parent;
20
+ analyzer.allNodes.push(node);
21
+
22
+ if (kind === 'source.lang.swift.decl.class') {
23
+ analyzer.classes.push(node);
24
+ } else if (kind === 'source.lang.swift.decl.struct') {
25
+ analyzer.structs.push(node);
26
+ } else if (kind === 'source.lang.swift.decl.protocol') {
27
+ analyzer.protocols.push(node);
28
+ } else if (kind.includes('function')) {
29
+ analyzer.functions.push(node);
30
+ } else if (kind.includes('var')) {
31
+ analyzer.properties.push(node);
32
+ } else if (kind.includes('closure')) {
33
+ analyzer.closures.push(node);
34
+ }
35
+
36
+ collectAllNodes(analyzer, node['key.substructure'] || [], node);
37
+ }
38
+ }
39
+
40
+ function analyzeCollectedNodes(analyzer, filePath) {
41
+ extractImports(analyzer);
42
+
43
+ analyzeImportsAST(analyzer, filePath);
44
+
45
+ for (const cls of analyzer.classes) {
46
+ analyzeClassAST(analyzer, cls, filePath);
47
+ }
48
+
49
+ for (const struct of analyzer.structs) {
50
+ analyzeStructAST(analyzer, struct, filePath);
51
+ }
52
+
53
+ for (const proto of analyzer.protocols) {
54
+ analyzeProtocolAST(analyzer, proto, filePath);
55
+ }
56
+
57
+ for (const func of analyzer.functions) {
58
+ analyzeFunctionAST(analyzer, func, filePath);
59
+ }
60
+
61
+ for (const prop of analyzer.properties) {
62
+ analyzePropertyAST(analyzer, prop, filePath);
63
+ }
64
+
65
+ analyzeClosuresAST(analyzer, filePath);
66
+ analyzeCleanArchitectureAST(analyzer, filePath);
67
+ analyzeAdditionalRules(analyzer, filePath);
68
+ }
69
+
70
+ function extractImports(analyzer) {
71
+ const lines = (analyzer.fileContent || '').split('\n');
72
+ for (let i = 0; i < lines.length; i++) {
73
+ const line = lines[i].trim();
74
+ if (line.startsWith('import ')) {
75
+ analyzer.imports.push({
76
+ name: line.replace('import ', '').trim(),
77
+ line: i + 1,
78
+ });
79
+ }
80
+ }
81
+ }
82
+
83
+ function analyzeImportsAST(analyzer, filePath) {
84
+ const importNames = analyzer.imports.map((i) => i.name);
85
+
86
+ const hasUIKit = importNames.includes('UIKit');
87
+ const hasSwiftUI = importNames.includes('SwiftUI');
88
+ const hasCombine = importNames.includes('Combine');
89
+
90
+ if (hasUIKit && hasSwiftUI) {
91
+ analyzer.pushFinding(
92
+ 'ios.architecture.mixed_ui_frameworks',
93
+ 'medium',
94
+ filePath,
95
+ 1,
96
+ 'Mixing UIKit and SwiftUI - consider separating concerns'
97
+ );
98
+ }
99
+
100
+ const hasAsyncFunction = analyzer.functions.some((f) => analyzer.hasAttribute(f, 'async'));
101
+
102
+ if (hasCombine && !hasAsyncFunction) {
103
+ analyzer.pushFinding(
104
+ 'ios.concurrency.combine_without_async',
105
+ 'low',
106
+ filePath,
107
+ 1,
108
+ 'Using Combine - consider async/await for simpler async code'
109
+ );
110
+ }
111
+
112
+ for (const imp of analyzer.imports) {
113
+ if (['Foundation', 'SwiftUI', 'UIKit', 'Combine'].includes(imp.name)) continue;
114
+
115
+ const isUsed = analyzer.allNodes.some((n) => {
116
+ const typename = n['key.typename'] || '';
117
+ const name = n['key.name'] || '';
118
+ return typename.includes(imp.name) || name.includes(imp.name);
119
+ });
120
+
121
+ if (!isUsed) {
122
+ analyzer.pushFinding('ios.imports.unused', 'low', filePath, imp.line, `Unused import: ${imp.name}`);
123
+ }
124
+ }
125
+
126
+ if (filePath.includes('/Domain/') && (hasUIKit || hasSwiftUI)) {
127
+ analyzer.pushFinding(
128
+ 'ios.architecture.domain_ui_import',
129
+ 'critical',
130
+ filePath,
131
+ 1,
132
+ 'Domain layer imports UI framework - violates Clean Architecture'
133
+ );
134
+ }
135
+ }
136
+
137
+ function analyzeClassAST(analyzer, node, filePath) {
138
+ const name = node['key.name'] || '';
139
+ const line = node['key.line'] || 1;
140
+ const bodyLength = countLinesInBody(analyzer, node) || 0;
141
+ const substructure = node['key.substructure'] || [];
142
+ const inheritedTypes = (node['key.inheritedtypes'] || []).map((t) => t['key.name']);
143
+ const attributes = analyzer.getAttributes(node);
144
+
145
+ const methods = substructure.filter((n) => (n['key.kind'] || '').includes('function'));
146
+ const properties = substructure.filter((n) => (n['key.kind'] || '').includes('var'));
147
+ const inits = methods.filter((m) => (m['key.name'] || '').startsWith('init'));
148
+ const significantMethods = methods.filter((m) => {
149
+ const methodName = String(m['key.name'] || '');
150
+ if (methodName.length === 0) return false;
151
+ if (methodName === 'deinit') return false;
152
+ if (methodName.startsWith('init')) return false;
153
+ return true;
154
+ });
155
+
156
+ if (name && !/Spec$|Test$|Mock/.test(name)) {
157
+ const complexity = calculateComplexityAST(substructure);
158
+ analyzer.godClassCandidates.push({
159
+ name,
160
+ filePath,
161
+ line,
162
+ methodsCount: methods.length,
163
+ significantMethodsCount: significantMethods.length,
164
+ propertiesCount: properties.length,
165
+ bodyLength,
166
+ complexity,
167
+ });
168
+ }
169
+
170
+ if (name.includes('ViewController')) {
171
+ if (bodyLength > 250 || methods.length > 8) {
172
+ analyzer.pushFinding(
173
+ 'ios.architecture.massive_viewcontroller',
174
+ 'high',
175
+ filePath,
176
+ line,
177
+ `Massive ViewController '${name}': ${bodyLength} lines - extract to ViewModel`
178
+ );
179
+ }
180
+
181
+ const hasBusinessLogic = methods.some((m) => {
182
+ const methodName = m['key.name'] || '';
183
+ return /calculate|process|validate|fetch|save|delete|update/i.test(methodName);
184
+ });
185
+
186
+ if (hasBusinessLogic) {
187
+ analyzer.pushFinding(
188
+ 'ios.architecture.vc_business_logic',
189
+ 'high',
190
+ filePath,
191
+ line,
192
+ `ViewController '${name}' contains business logic - move to UseCase`
193
+ );
194
+ }
195
+ }
196
+
197
+ if (name.includes('ViewModel')) {
198
+ const hasMainActor = attributes.includes('MainActor');
199
+ if (!hasMainActor) {
200
+ analyzer.pushFinding(
201
+ 'ios.concurrency.viewmodel_mainactor',
202
+ 'high',
203
+ filePath,
204
+ line,
205
+ `ViewModel '${name}' should be @MainActor for UI safety`
206
+ );
207
+ }
208
+
209
+ const hasObservable = inheritedTypes.includes('ObservableObject') || attributes.includes('Observable');
210
+ if (!hasObservable) {
211
+ analyzer.pushFinding(
212
+ 'ios.swiftui.viewmodel_observable',
213
+ 'medium',
214
+ filePath,
215
+ line,
216
+ `ViewModel '${name}' should conform to ObservableObject`
217
+ );
218
+ }
219
+
220
+ if (inits.length === 0 && properties.length > 0) {
221
+ analyzer.pushFinding(
222
+ 'ios.architecture.viewmodel_no_di',
223
+ 'high',
224
+ filePath,
225
+ line,
226
+ `ViewModel '${name}' has no init - use dependency injection`
227
+ );
228
+ }
229
+ }
230
+
231
+ if (/Manager$|Helper$|Utils$|Handler$/.test(name)) {
232
+ analyzer.pushFinding('ios.naming.god_naming', 'medium', filePath, line, `Suspicious class name '${name}' - often indicates SRP violation`);
233
+ }
234
+
235
+ const unusedProps = findUnusedPropertiesAST(analyzer, properties, methods);
236
+ for (const prop of unusedProps) {
237
+ analyzer.pushFinding(
238
+ 'ios.solid.isp.unused_dependency',
239
+ 'high',
240
+ filePath,
241
+ line,
242
+ `Unused property '${prop}' in '${name}' - ISP violation`
243
+ );
244
+ }
245
+
246
+ checkDependencyInjectionAST(analyzer, properties, filePath, name, line);
247
+
248
+ const hasDeinit = methods.some((m) => m['key.name'] === 'deinit');
249
+ const hasObservers = analyzer.closures.some((c) => c._parent === node) || properties.some((p) => analyzer.hasAttribute(p, 'Published'));
250
+
251
+ if (!hasDeinit && hasObservers && !name.includes('ViewModel')) {
252
+ analyzer.pushFinding(
253
+ 'ios.memory.missing_deinit',
254
+ 'high',
255
+ filePath,
256
+ line,
257
+ `Class '${name}' has observers but no deinit - potential memory leak`
258
+ );
259
+ }
260
+
261
+ const isFinal = attributes.includes('final');
262
+ if (!isFinal && inheritedTypes.length === 0 && name !== 'AppDelegate') {
263
+ analyzer.pushFinding('ios.performance.non_final_class', 'low', filePath, line, `Class '${name}' is not final - consider final for performance`);
264
+ }
265
+
266
+ const hasSingleton = properties.some((p) => {
267
+ const propName = p['key.name'] || '';
268
+ const isStatic = (p['key.kind'] || '').includes('static');
269
+ return isStatic && propName === 'shared';
270
+ });
271
+
272
+ if (hasSingleton) {
273
+ analyzer.pushFinding('ios.antipattern.singleton', 'high', filePath, line, `Singleton pattern in '${name}' - use dependency injection`);
274
+ }
275
+
276
+ if (filePath.includes('Test') && name.includes('Test')) {
277
+ const hasMakeSUT = methods.some((m) => (m['key.name'] || '').includes('makeSUT'));
278
+ if (!hasMakeSUT && methods.length > 2) {
279
+ analyzer.pushFinding('ios.testing.missing_makesut', 'medium', filePath, line, `Test class '${name}' missing makeSUT() factory`);
280
+ }
281
+ }
282
+
283
+ if (name.includes('Repository') && !name.includes('Protocol')) {
284
+ const hasProtocol = inheritedTypes.some((t) => t.includes('Protocol'));
285
+ if (!hasProtocol && !filePath.includes('/Domain/')) {
286
+ analyzer.pushFinding('ios.architecture.repository_no_protocol', 'high', filePath, line, `Repository '${name}' should implement a protocol`);
287
+ }
288
+ }
289
+
290
+ if (name.includes('UseCase')) {
291
+ const hasExecute = methods.some((m) => (m['key.name'] || '').includes('execute'));
292
+ if (!hasExecute) {
293
+ analyzer.pushFinding('ios.architecture.usecase_no_execute', 'medium', filePath, line, `UseCase '${name}' missing execute() method`);
294
+ }
295
+ }
296
+ }
297
+
298
+ function analyzeStructAST(analyzer, node, filePath) {
299
+ const name = node['key.name'] || '';
300
+ const line = node['key.line'] || 1;
301
+ const substructure = node['key.substructure'] || [];
302
+ const inheritedTypes = (node['key.inheritedtypes'] || []).map((t) => t['key.name']);
303
+
304
+ const methods = substructure.filter((n) => (n['key.kind'] || '').includes('function'));
305
+ const properties = substructure.filter((n) => (n['key.kind'] || '').includes('var'));
306
+
307
+ if (inheritedTypes.includes('View')) {
308
+ analyzeSwiftUIViewAST(analyzer, node, filePath, name, line, methods, properties);
309
+ }
310
+
311
+ if (inheritedTypes.includes('Codable') || inheritedTypes.includes('Decodable')) {
312
+ const hasOptionalProps = properties.some((p) => (p['key.typename'] || '').includes('?'));
313
+ const hasCodingKeys = substructure.some((n) => n['key.name'] === 'CodingKeys');
314
+
315
+ if (hasOptionalProps && !hasCodingKeys) {
316
+ analyzer.pushFinding('ios.codable.missing_coding_keys', 'low', filePath, line, `Struct '${name}' has optional properties - consider CodingKeys`);
317
+ }
318
+ }
319
+
320
+ if ((inheritedTypes.includes('Equatable') || inheritedTypes.includes('Hashable')) && properties.length > 5) {
321
+ analyzer.pushFinding('ios.performance.large_equatable', 'low', filePath, line, `Struct '${name}' has ${properties.length} properties with Equatable`);
322
+ }
323
+ }
324
+
325
+ function analyzeSwiftUIViewAST(analyzer, _node, filePath, name, line, methods, properties) {
326
+ const bodyMethod = methods.find((m) => m['key.name'] === 'body');
327
+ if (bodyMethod) {
328
+ const bodyLength = countLinesInBody(analyzer, bodyMethod) || 0;
329
+ if (bodyLength > 100) {
330
+ analyzer.pushFinding('ios.swiftui.complex_body', 'high', filePath, line, `View '${name}' has complex body (${bodyLength} lines) - extract subviews`);
331
+ }
332
+ }
333
+
334
+ const stateProps = properties.filter((p) => analyzer.hasAttribute(p, 'State') || analyzer.hasAttribute(p, 'Binding'));
335
+
336
+ if (stateProps.length > 5) {
337
+ analyzer.pushFinding('ios.swiftui.too_many_state', 'medium', filePath, line, `View '${name}' has ${stateProps.length} @State/@Binding - consider ViewModel`);
338
+ }
339
+
340
+ const hasObservedObject = properties.some((p) => analyzer.hasAttribute(p, 'ObservedObject'));
341
+ const hasStateObject = properties.some((p) => analyzer.hasAttribute(p, 'StateObject'));
342
+
343
+ if (hasObservedObject && !hasStateObject) {
344
+ analyzer.pushFinding('ios.swiftui.observed_without_state', 'medium', filePath, line, `View '${name}' uses @ObservedObject - consider @StateObject for ownership`);
345
+ }
346
+ }
347
+
348
+ function analyzeProtocolAST(analyzer, node, filePath) {
349
+ const name = node['key.name'] || '';
350
+ const line = node['key.line'] || 1;
351
+ const substructure = node['key.substructure'] || [];
352
+
353
+ const requirements = substructure.filter((n) => (n['key.kind'] || '').includes('function') || (n['key.kind'] || '').includes('var'));
354
+
355
+ if (requirements.length > 5) {
356
+ analyzer.pushFinding('ios.solid.isp.fat_protocol', 'medium', filePath, line, `Protocol '${name}' has ${requirements.length} requirements - consider splitting (ISP)`);
357
+ }
358
+ }
359
+
360
+ function countLinesInBody(analyzer, node) {
361
+ const offset = Number(node['key.bodyoffset']);
362
+ const length = Number(node['key.bodylength']);
363
+ if (!Number.isFinite(offset) || !Number.isFinite(length) || offset < 0 || length <= 0) {
364
+ return 0;
365
+ }
366
+
367
+ try {
368
+ const buf = Buffer.from(analyzer.fileContent || '', 'utf8');
369
+ const slice = buf.subarray(offset, Math.min(buf.length, offset + length));
370
+ const text = slice.toString('utf8');
371
+ if (!text) return 0;
372
+ return text.split('\n').length;
373
+ } catch (error) {
374
+ if (process.env.DEBUG) {
375
+ console.debug(`[iOSASTIntelligentAnalyzer] Failed to count lines in body: ${error.message}`);
376
+ }
377
+ return 0;
378
+ }
379
+ }
380
+
381
+ function analyzeFunctionAST(analyzer, node, filePath) {
382
+ const name = node['key.name'] || '';
383
+ const line = node['key.line'] || 1;
384
+ const bodyLength = countLinesInBody(analyzer, node) || 0;
385
+ const attributes = analyzer.getAttributes(node);
386
+ const substructure = node['key.substructure'] || [];
387
+
388
+ if (bodyLength > 50) {
389
+ analyzer.pushFinding('ios.quality.long_function', 'high', filePath, line, `Function '${name}' is too long (${bodyLength} lines)`);
390
+ }
391
+
392
+ const complexity = calculateComplexityAST(substructure);
393
+ if (complexity > 10) {
394
+ analyzer.pushFinding('ios.quality.high_complexity', 'high', filePath, line, `Function '${name}' has high complexity (${complexity})`);
395
+ }
396
+
397
+ const params = substructure.filter((n) => (n['key.kind'] || '').includes('var.parameter'));
398
+ if (params.length > 5) {
399
+ analyzer.pushFinding('ios.quality.too_many_params', 'medium', filePath, line, `Function '${name}' has ${params.length} parameters - use struct`);
400
+ }
401
+
402
+ const closuresInFunc = countClosuresInNode(node);
403
+ if (closuresInFunc > 3) {
404
+ analyzer.pushFinding('ios.quality.nested_closures', 'medium', filePath, line, `Function '${name}' has ${closuresInFunc} nested closures - use async/await`);
405
+ }
406
+
407
+ const ifStatements = countStatementsOfType(substructure, 'stmt.if');
408
+ const guardStatements = countStatementsOfType(substructure, 'stmt.guard');
409
+
410
+ if (ifStatements > 3 && guardStatements === 0) {
411
+ analyzer.pushFinding('ios.quality.pyramid_of_doom', 'medium', filePath, line, `Function '${name}' has ${ifStatements} nested ifs - use guard clauses`);
412
+ }
413
+
414
+ const isAsync = attributes.includes('async');
415
+ const parentClass = node._parent;
416
+ const parentIsView = parentClass && (parentClass['key.inheritedtypes'] || []).some((t) => t['key.name'] === 'View');
417
+
418
+ if (isAsync && parentIsView && !attributes.includes('MainActor')) {
419
+ analyzer.pushFinding('ios.concurrency.async_ui_update', 'high', filePath, line, `Async function '${name}' in View - consider @MainActor`);
420
+ }
421
+ }
422
+
423
+ function analyzePropertyAST(analyzer, node, filePath) {
424
+ const name = node['key.name'] || '';
425
+ const line = node['key.line'] || 1;
426
+ const typename = node['key.typename'] || '';
427
+ const attributes = analyzer.getAttributes(node);
428
+ const kind = node['key.kind'] || '';
429
+
430
+ if (typename.includes('!') && !attributes.includes('IBOutlet')) {
431
+ analyzer.pushFinding('ios.safety.force_unwrap_property', 'high', filePath, line, `Force unwrapped property '${name}: ${typename}'`);
432
+ }
433
+
434
+ const isPublic = (node['key.accessibility'] || '').includes('public');
435
+ const isInstance = kind.includes('var.instance');
436
+
437
+ if (isPublic && isInstance && !attributes.includes('setter_access')) {
438
+ analyzer.pushFinding('ios.encapsulation.public_mutable', 'medium', filePath, line, `Public mutable property '${name}' - consider private(set)`);
439
+ }
440
+ }
441
+
442
+ function analyzeClosuresAST(analyzer, filePath) {
443
+ for (const closure of analyzer.closures) {
444
+ const closureText = analyzer.safeStringify(closure);
445
+ const hasSelfReference = closureText.includes('"self"') || closureText.includes('key.name":"self');
446
+
447
+ const parentFunc = closure._parent;
448
+ const isEscaping = parentFunc && (parentFunc['key.typename'] || '').includes('@escaping');
449
+
450
+ if (hasSelfReference && isEscaping) {
451
+ const hasWeakCapture = closureText.includes('weak') || closureText.includes('unowned');
452
+
453
+ if (!hasWeakCapture) {
454
+ const line = closure['key.line'] || 1;
455
+ analyzer.pushFinding('ios.memory.missing_weak_self', 'high', filePath, line, 'Escaping closure captures self without [weak self]');
456
+ }
457
+ }
458
+ }
459
+ }
460
+
461
+ function analyzeCleanArchitectureAST(analyzer, filePath) {
462
+ const fileName = path.basename(filePath);
463
+
464
+ if (fileName.includes('UseCase')) {
465
+ const hasVoidReturn = analyzer.functions.some((f) => {
466
+ const typename = f['key.typename'] || '';
467
+ return (f['key.name'] || '').includes('execute') && typename.includes('Void');
468
+ });
469
+
470
+ if (hasVoidReturn) {
471
+ analyzer.pushFinding('ios.architecture.usecase_void', 'medium', filePath, 1, 'UseCase returns Void - consider returning Result');
472
+ }
473
+
474
+ if (!filePath.includes('/Domain/') && !filePath.includes('/Application/')) {
475
+ analyzer.pushFinding('ios.architecture.usecase_wrong_layer', 'high', filePath, 1, 'UseCase should be in Domain or Application layer');
476
+ }
477
+ }
478
+
479
+ if (filePath.includes('/Infrastructure/')) {
480
+ const hasUIImport = analyzer.imports.some((i) => ['SwiftUI', 'UIKit'].includes(i.name));
481
+ if (hasUIImport) {
482
+ analyzer.pushFinding('ios.architecture.infrastructure_ui', 'critical', filePath, 1, 'Infrastructure layer imports UI framework - violates Clean Architecture');
483
+ }
484
+ }
485
+
486
+ if (filePath.includes('/Presentation/') || filePath.includes('/Views/')) {
487
+ const hasInfraImport = analyzer.imports.some((i) => i.name.includes('Alamofire') || i.name.includes('Realm') || i.name.includes('CoreData'));
488
+ if (hasInfraImport) {
489
+ analyzer.pushFinding('ios.architecture.presentation_infra', 'high', filePath, 1, 'Presentation imports Infrastructure directly - use Domain layer');
490
+ }
491
+ }
492
+ }
493
+
494
+ function analyzeAdditionalRules(analyzer, filePath) {
495
+ const hasSwiftUIViewType = (analyzer.imports || []).some((i) => i && i.name === 'SwiftUI') &&
496
+ (analyzer.structs || []).some((s) => (s['key.inheritedtypes'] || []).some((t) => t && t['key.name'] === 'View'));
497
+
498
+ if ((analyzer.fileContent || '').includes('pushViewController') || (analyzer.fileContent || '').includes('popViewController') || (analyzer.fileContent || '').includes('present(')) {
499
+ const line = analyzer.findLineNumber('pushViewController') || analyzer.findLineNumber('popViewController') || analyzer.findLineNumber('present(');
500
+ analyzer.pushFinding('ios.navigation.imperative_navigation', 'critical', filePath, line, 'Imperative navigation detected - use event-driven navigation/coordinator');
501
+ }
502
+
503
+ const swiftuiNavTokens = ['NavigationLink', 'NavigationStack', 'NavigationSplitView', '.navigationDestination'];
504
+ const hasSwiftUINavigation = swiftuiNavTokens.some((token) => (analyzer.fileContent || '').includes(token));
505
+ if (hasSwiftUINavigation && !hasSwiftUIViewType) {
506
+ const line = analyzer.findLineNumber('NavigationLink') || analyzer.findLineNumber('NavigationStack') || analyzer.findLineNumber('.navigationDestination');
507
+ analyzer.pushFinding('ios.navigation.swiftui_navigation_outside_view', 'critical', filePath, line, 'SwiftUI navigation API detected outside View types');
508
+ }
509
+
510
+ if (filePath.includes('ViewModel') && (analyzer.fileContent || '').includes('NavigationLink')) {
511
+ const hasCoordinator = analyzer.imports.some((i) => i.name.includes('Coordinator'));
512
+ if (!hasCoordinator) {
513
+ analyzer.pushFinding('ios.architecture.missing_coordinator', 'medium', filePath, 1, 'Navigation in ViewModel - consider Coordinator pattern');
514
+ }
515
+ }
516
+
517
+ if ((analyzer.fileContent || '').includes('DispatchQueue.main') || (analyzer.fileContent || '').includes('DispatchQueue.global')) {
518
+ const line = analyzer.findLineNumber('DispatchQueue');
519
+ analyzer.pushFinding('ios.concurrency.dispatch_queue', 'medium', filePath, line, 'DispatchQueue detected - use async/await in new code');
520
+ }
521
+
522
+ if (/Task\s*\{/.test(analyzer.fileContent || '') && !/Task\s*\{[^}]*do\s*\{/.test(analyzer.fileContent || '')) {
523
+ const line = analyzer.findLineNumber('Task {');
524
+ analyzer.pushFinding('ios.concurrency.task_no_error_handling', 'high', filePath, line, 'Task without do-catch - handle errors');
525
+ }
526
+
527
+ if ((analyzer.fileContent || '').includes('UserDefaults') && /password|token|secret|key/i.test(analyzer.fileContent || '')) {
528
+ const line = analyzer.findLineNumber('UserDefaults');
529
+ analyzer.pushFinding('ios.security.sensitive_userdefaults', 'critical', filePath, line, 'Sensitive data in UserDefaults - use Keychain');
530
+ }
531
+
532
+ const hardcodedStrings = (analyzer.fileContent || '').match(/Text\s*\(\s*"[^"]{10,}"\s*\)/g) || [];
533
+ if (hardcodedStrings.length > 3) {
534
+ analyzer.pushFinding('ios.i18n.hardcoded_strings', 'medium', filePath, 1, `${hardcodedStrings.length} hardcoded strings - use NSLocalizedString`);
535
+ }
536
+
537
+ if ((analyzer.fileContent || '').includes('Image(') && !(analyzer.fileContent || '').includes('.accessibilityLabel')) {
538
+ const imageCount = ((analyzer.fileContent || '').match(/Image\s*\(/g) || []).length;
539
+ const labelCount = ((analyzer.fileContent || '').match(/\.accessibilityLabel/g) || []).length;
540
+ if (imageCount > labelCount + 2) {
541
+ analyzer.pushFinding('ios.accessibility.missing_labels', 'medium', filePath, 1, 'Images without accessibilityLabel - add for VoiceOver');
542
+ }
543
+ }
544
+
545
+ if ((analyzer.fileContent || '').includes('@IBOutlet') || (analyzer.fileContent || '').includes('@IBAction')) {
546
+ const line = analyzer.findLineNumber('@IB');
547
+ analyzer.pushFinding('ios.deprecated.storyboard', 'low', filePath, line, 'Storyboard/XIB detected - consider SwiftUI or programmatic UI');
548
+ }
549
+
550
+ const completionCount = ((analyzer.fileContent || '').match(/completion\s*:\s*@escaping/g) || []).length;
551
+ if (completionCount > 2) {
552
+ analyzer.pushFinding('ios.concurrency.completion_handlers', 'medium', filePath, 1, `${completionCount} completion handlers - use async/await`);
553
+ }
554
+
555
+ for (const cls of analyzer.classes) {
556
+ const hasSharedState = analyzer.properties.some((p) => (p['key.kind'] || '').includes('static') && !analyzer.hasAttribute(p, 'MainActor') && !analyzer.hasAttribute(p, 'nonisolated'));
557
+ if (hasSharedState && !(analyzer.fileContent || '').includes('actor ')) {
558
+ const name = cls['key.name'] || '';
559
+ const line = cls['key.line'] || 1;
560
+ analyzer.pushFinding('ios.concurrency.missing_actor', 'high', filePath, line, `Class '${name}' has shared state - consider actor for thread safety`);
561
+ }
562
+ }
563
+
564
+ for (const cls of analyzer.classes) {
565
+ const name = cls['key.name'] || '';
566
+ const inheritedTypes = (cls['key.inheritedtypes'] || []).map((t) => t['key.name']);
567
+ const hasAsync = analyzer.functions.some((f) => analyzer.hasAttribute(f, 'async'));
568
+
569
+ if (hasAsync && !inheritedTypes.includes('Sendable') && !name.includes('ViewModel')) {
570
+ const line = cls['key.line'] || 1;
571
+ analyzer.pushFinding('ios.concurrency.missing_sendable', 'medium', filePath, line, `Class '${name}' used in async context - consider Sendable conformance`);
572
+ }
573
+ }
574
+ }
575
+
576
+ function findUnusedPropertiesAST(analyzer, properties, methods) {
577
+ const unused = [];
578
+
579
+ for (const prop of properties) {
580
+ const propName = prop['key.name'];
581
+ if (!propName) continue;
582
+
583
+ if (analyzer.hasAttribute(prop, 'Published') || analyzer.hasAttribute(prop, 'State') || analyzer.hasAttribute(prop, 'Binding')) {
584
+ continue;
585
+ }
586
+
587
+ let usageCount = 0;
588
+ for (const method of methods) {
589
+ const methodText = analyzer.safeStringify(method);
590
+ if (methodText.includes(`"${propName}"`)) {
591
+ usageCount++;
592
+ }
593
+ }
594
+
595
+ if (usageCount === 0) {
596
+ unused.push(propName);
597
+ }
598
+ }
599
+
600
+ return unused;
601
+ }
602
+
603
+ function calculateComplexityAST(substructure) {
604
+ let complexity = 1;
605
+
606
+ const traverse = (nodes) => {
607
+ if (!Array.isArray(nodes)) return;
608
+
609
+ for (const node of nodes) {
610
+ const kind = node['key.kind'] || '';
611
+
612
+ if (kind.includes('stmt.if') || kind.includes('stmt.guard') || kind.includes('stmt.switch') || kind.includes('stmt.for') || kind.includes('stmt.while') || kind.includes('stmt.catch')) {
613
+ complexity++;
614
+ }
615
+
616
+ traverse(node['key.substructure'] || []);
617
+ }
618
+ };
619
+
620
+ traverse(substructure);
621
+ return complexity;
622
+ }
623
+
624
+ function countClosuresInNode(node) {
625
+ let count = 0;
626
+ const traverse = (n) => {
627
+ if ((n['key.kind'] || '').includes('closure')) count++;
628
+ for (const child of (n['key.substructure'] || [])) {
629
+ traverse(child);
630
+ }
631
+ };
632
+ traverse(node);
633
+ return count;
634
+ }
635
+
636
+ function countStatementsOfType(substructure, stmtType) {
637
+ let count = 0;
638
+ const traverse = (nodes) => {
639
+ if (!Array.isArray(nodes)) return;
640
+ for (const node of nodes) {
641
+ if ((node['key.kind'] || '').includes(stmtType)) count++;
642
+ traverse(node['key.substructure'] || []);
643
+ }
644
+ };
645
+ traverse(substructure);
646
+ return count;
647
+ }
648
+
649
+ function checkDependencyInjectionAST(analyzer, properties, filePath, className, line) {
650
+ if (!className.includes('ViewModel') && !className.includes('Service') && !className.includes('Repository') && !className.includes('UseCase')) {
651
+ return;
652
+ }
653
+
654
+ for (const prop of properties) {
655
+ const typename = prop['key.typename'] || '';
656
+
657
+ if (['String', 'Int', 'Bool', 'Double', 'Float', 'Date', 'URL', 'Data'].includes(typename)) {
658
+ continue;
659
+ }
660
+
661
+ const isConcreteService = /Service$|Repository$|UseCase$|Client$/.test(typename) && !typename.includes('Protocol') && !typename.includes('any ') && !typename.includes('some ');
662
+
663
+ if (isConcreteService) {
664
+ analyzer.pushFinding('ios.solid.dip.concrete_dependency', 'high', filePath, line, `'${className}' depends on concrete '${typename}' - use protocol`);
665
+ }
666
+ }
667
+ }
668
+
669
+ function finalizeGodClassDetection(analyzer) {
670
+ if (!analyzer.godClassCandidates || analyzer.godClassCandidates.length < 10) return;
671
+
672
+ const quantile = (values, p) => {
673
+ if (!values || values.length === 0) return 0;
674
+ const sorted = [...values].filter((v) => Number.isFinite(v)).sort((a, b) => a - b);
675
+ if (sorted.length === 0) return 0;
676
+ const idx = Math.max(0, Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1));
677
+ return sorted[idx];
678
+ };
679
+
680
+ const median = (values) => {
681
+ if (!values || values.length === 0) return 0;
682
+ const sorted = [...values].filter((v) => Number.isFinite(v)).sort((a, b) => a - b);
683
+ if (sorted.length === 0) return 0;
684
+ const mid = Math.floor(sorted.length / 2);
685
+ if (sorted.length % 2 === 0) return (sorted[mid - 1] + sorted[mid]) / 2;
686
+ return sorted[mid];
687
+ };
688
+
689
+ const mad = (values) => {
690
+ const med = median(values);
691
+ const deviations = (values || []).map((v) => Math.abs(v - med));
692
+ return median(deviations);
693
+ };
694
+
695
+ const robustZ = (x, med, madValue) => {
696
+ if (!Number.isFinite(x) || !Number.isFinite(med) || !Number.isFinite(madValue) || madValue === 0) return 0;
697
+ return 0.6745 * (x - med) / madValue;
698
+ };
699
+
700
+ const env = require(path.join(__dirname, '../../../../config/env'));
701
+ const pOutlier = env.getNumber('AST_GODCLASS_P_OUTLIER', 90);
702
+ const pExtreme = env.getNumber('AST_GODCLASS_P_EXTREME', 97);
703
+
704
+ const methods = analyzer.godClassCandidates.map((c) => c.methodsCount);
705
+ const props = analyzer.godClassCandidates.map((c) => c.propertiesCount);
706
+ const bodies = analyzer.godClassCandidates.map((c) => c.bodyLength);
707
+ const complexities = analyzer.godClassCandidates.map((c) => c.complexity);
708
+
709
+ const med = {
710
+ methodsCount: median(methods),
711
+ propertiesCount: median(props),
712
+ bodyLength: median(bodies),
713
+ complexity: median(complexities),
714
+ };
715
+ const madValue = {
716
+ methodsCount: mad(methods),
717
+ propertiesCount: mad(props),
718
+ bodyLength: mad(bodies),
719
+ complexity: mad(complexities),
720
+ };
721
+
722
+ const z = {
723
+ methodsCount: methods.map((v) => robustZ(v, med.methodsCount, madValue.methodsCount)),
724
+ propertiesCount: props.map((v) => robustZ(v, med.propertiesCount, madValue.propertiesCount)),
725
+ bodyLength: bodies.map((v) => robustZ(v, med.bodyLength, madValue.bodyLength)),
726
+ complexity: complexities.map((v) => robustZ(v, med.complexity, madValue.complexity)),
727
+ };
728
+
729
+ const thresholds = {
730
+ outlier: {
731
+ methodsCountZ: quantile(z.methodsCount, pOutlier),
732
+ propertiesCountZ: quantile(z.propertiesCount, pOutlier),
733
+ bodyLengthZ: quantile(z.bodyLength, pOutlier),
734
+ complexityZ: quantile(z.complexity, pOutlier),
735
+ },
736
+ extreme: {
737
+ methodsCountZ: quantile(z.methodsCount, pExtreme),
738
+ propertiesCountZ: quantile(z.propertiesCount, pExtreme),
739
+ bodyLengthZ: quantile(z.bodyLength, pExtreme),
740
+ complexityZ: quantile(z.complexity, pExtreme),
741
+ },
742
+ };
743
+
744
+ for (const c of analyzer.godClassCandidates) {
745
+ const significantMethods = Number.isFinite(Number(c.significantMethodsCount))
746
+ ? Number(c.significantMethodsCount)
747
+ : Math.max(0, Number(c.methodsCount) - 1);
748
+ const hasNoProperties = Number(c.propertiesCount) === 0;
749
+ if (hasNoProperties || significantMethods < 2) {
750
+ continue;
751
+ }
752
+
753
+ const methodsZ = robustZ(c.methodsCount, med.methodsCount, madValue.methodsCount);
754
+ const propsZ = robustZ(c.propertiesCount, med.propertiesCount, madValue.propertiesCount);
755
+ const bodyZ = robustZ(c.bodyLength, med.bodyLength, madValue.bodyLength);
756
+ const complexityZ = robustZ(c.complexity, med.complexity, madValue.complexity);
757
+
758
+ const sizeOutlier =
759
+ (methodsZ > 0 && methodsZ >= thresholds.outlier.methodsCountZ) ||
760
+ (propsZ > 0 && propsZ >= thresholds.outlier.propertiesCountZ) ||
761
+ (bodyZ > 0 && bodyZ >= thresholds.outlier.bodyLengthZ);
762
+ const complexityOutlier = complexityZ > 0 && complexityZ >= thresholds.outlier.complexityZ;
763
+
764
+ const extremeOutlier =
765
+ (methodsZ > 0 && methodsZ >= thresholds.extreme.methodsCountZ) ||
766
+ (propsZ > 0 && propsZ >= thresholds.extreme.propertiesCountZ) ||
767
+ (bodyZ > 0 && bodyZ >= thresholds.extreme.bodyLengthZ) ||
768
+ (complexityZ > 0 && complexityZ >= thresholds.extreme.complexityZ);
769
+
770
+ const signalCount = [sizeOutlier, complexityOutlier].filter(Boolean).length;
771
+
772
+ if (extremeOutlier || signalCount >= 2) {
773
+ analyzer.pushFinding(
774
+ 'ios.solid.srp.god_class',
775
+ 'critical',
776
+ c.filePath,
777
+ c.line,
778
+ `God class '${c.name}': ${c.methodsCount} methods (z=${methodsZ.toFixed(2)}), ${c.propertiesCount} properties (z=${propsZ.toFixed(2)}), body ${c.bodyLength} (z=${bodyZ.toFixed(2)}), complexity ${c.complexity} (z=${complexityZ.toFixed(2)}) - VIOLATES SRP`
779
+ );
780
+ }
781
+ }
782
+ }
783
+
784
+ module.exports = {
785
+ resetCollections,
786
+ collectAllNodes,
787
+ analyzeCollectedNodes,
788
+ finalizeGodClassDetection,
789
+ };