pumuki-ast-hooks 5.5.47 → 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 (30) 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/VSCodeTaskConfigurator.js +3 -1
  6. package/scripts/hooks-system/bin/gitflow-cycle.js +0 -0
  7. package/scripts/hooks-system/config/project.config.json +1 -1
  8. package/scripts/hooks-system/infrastructure/ast/android/analyzers/AndroidSOLIDAnalyzer.js +11 -255
  9. package/scripts/hooks-system/infrastructure/ast/android/detectors/android-solid-detectors.js +227 -0
  10. package/scripts/hooks-system/infrastructure/ast/ast-core.js +12 -3
  11. package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +36 -13
  12. package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +10 -83
  13. package/scripts/hooks-system/infrastructure/ast/backend/detectors/god-class-detector.js +83 -0
  14. package/scripts/hooks-system/infrastructure/ast/common/ast-common.js +17 -2
  15. package/scripts/hooks-system/infrastructure/ast/frontend/analyzers/FrontendArchitectureDetector.js +12 -142
  16. package/scripts/hooks-system/infrastructure/ast/frontend/detectors/frontend-architecture-strategies.js +126 -0
  17. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +30 -783
  18. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSArchitectureDetector.js +21 -224
  19. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSArchitectureRules.js +18 -605
  20. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSModernPracticesRules.js +4 -1
  21. package/scripts/hooks-system/infrastructure/ast/ios/ast-ios.js +4 -1
  22. package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-architecture-rules-strategies.js +595 -0
  23. package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-architecture-strategies.js +192 -0
  24. package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-ast-intelligent-strategies.js +789 -0
  25. package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-god-class-detector.js +79 -0
  26. package/scripts/hooks-system/infrastructure/ast/ios/native-bridge.js +4 -1
  27. package/skills/android-guidelines/SKILL.md +1 -0
  28. package/skills/backend-guidelines/SKILL.md +1 -0
  29. package/skills/frontend-guidelines/SKILL.md +1 -0
  30. package/skills/ios-guidelines/SKILL.md +1 -0
@@ -1,9 +1,14 @@
1
-
2
1
  const { execSync } = require('child_process');
3
2
  const fs = require('fs');
4
3
  const path = require('path');
5
4
  const crypto = require('crypto');
6
5
  const env = require(path.join(__dirname, '../../../../config/env'));
6
+ const {
7
+ resetCollections,
8
+ collectAllNodes,
9
+ analyzeCollectedNodes,
10
+ finalizeGodClassDetection,
11
+ } = require('../detectors/ios-ast-intelligent-strategies');
7
12
 
8
13
  class iOSASTIntelligentAnalyzer {
9
14
  constructor(findings) {
@@ -43,7 +48,10 @@ class iOSASTIntelligentAnalyzer {
43
48
  readStagedFileContent(repoRoot, relPath) {
44
49
  try {
45
50
  return execSync(`git show :"${relPath}"`, { encoding: 'utf8', cwd: repoRoot, stdio: ['ignore', 'pipe', 'pipe'] });
46
- } catch {
51
+ } catch (error) {
52
+ if (process.env.DEBUG) {
53
+ console.debug(`[iOSASTIntelligentAnalyzer] Failed to read staged file ${relPath}: ${error.message}`);
54
+ }
47
55
  return null;
48
56
  }
49
57
  }
@@ -52,7 +60,10 @@ class iOSASTIntelligentAnalyzer {
52
60
  try {
53
61
  execSync(`${this.sourceKittenPath} version`, { stdio: 'pipe' });
54
62
  return true;
55
- } catch {
63
+ } catch (error) {
64
+ if (process.env.DEBUG) {
65
+ console.debug(`[iOSASTIntelligentAnalyzer] SourceKitten not available at ${this.sourceKittenPath}: ${error.message}`);
66
+ }
56
67
  return false;
57
68
  }
58
69
  }
@@ -103,7 +114,10 @@ class iOSASTIntelligentAnalyzer {
103
114
  { encoding: 'utf8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] }
104
115
  );
105
116
  return JSON.parse(result);
106
- } catch {
117
+ } catch (error) {
118
+ if (process.env.DEBUG) {
119
+ console.debug(`[iOSASTIntelligentAnalyzer] SourceKitten parse failed for ${filePath}: ${error.message}`);
120
+ }
107
121
  return null;
108
122
  }
109
123
  }
@@ -128,7 +142,10 @@ class iOSASTIntelligentAnalyzer {
128
142
  fs.writeFileSync(tmpPath, stagedContent, 'utf8');
129
143
  parsePath = tmpPath;
130
144
  contentOverride = stagedContent;
131
- } catch {
145
+ } catch (error) {
146
+ if (process.env.DEBUG) {
147
+ console.debug(`[iOSASTIntelligentAnalyzer] Failed to write temp staged file ${tmpPath}: ${error.message}`);
148
+ }
132
149
  // Fall back to working tree file
133
150
  }
134
151
  }
@@ -142,429 +159,23 @@ class iOSASTIntelligentAnalyzer {
142
159
  if (!ast) return;
143
160
 
144
161
  this.currentFilePath = displayPath;
145
- this.resetCollections();
162
+ resetCollections(this);
146
163
 
147
164
  try {
148
165
  this.fileContent = typeof contentOverride === 'string'
149
166
  ? contentOverride
150
167
  : fs.readFileSync(parsePath, 'utf8');
151
- } catch {
168
+ } catch (error) {
169
+ if (process.env.DEBUG) {
170
+ console.debug(`[iOSASTIntelligentAnalyzer] Failed to read file content ${parsePath}: ${error.message}`);
171
+ }
152
172
  this.fileContent = '';
153
173
  }
154
174
 
155
175
  const substructure = ast['key.substructure'] || [];
156
176
 
157
- this.collectAllNodes(substructure, null);
158
- this.analyzeCollectedNodes(displayPath);
159
- }
160
-
161
- resetCollections() {
162
- this.allNodes = [];
163
- this.imports = [];
164
- this.classes = [];
165
- this.structs = [];
166
- this.protocols = [];
167
- this.functions = [];
168
- this.properties = [];
169
- this.closures = [];
170
- }
171
-
172
- collectAllNodes(nodes, parent) {
173
- if (!Array.isArray(nodes)) return;
174
-
175
- for (const node of nodes) {
176
- const kind = node['key.kind'] || '';
177
- node._parent = parent;
178
- this.allNodes.push(node);
179
-
180
- if (kind === 'source.lang.swift.decl.class') {
181
- this.classes.push(node);
182
- } else if (kind === 'source.lang.swift.decl.struct') {
183
- this.structs.push(node);
184
- } else if (kind === 'source.lang.swift.decl.protocol') {
185
- this.protocols.push(node);
186
- } else if (kind.includes('function')) {
187
- this.functions.push(node);
188
- } else if (kind.includes('var')) {
189
- this.properties.push(node);
190
- } else if (kind.includes('closure')) {
191
- this.closures.push(node);
192
- }
193
-
194
- this.collectAllNodes(node['key.substructure'] || [], node);
195
- }
196
- }
197
-
198
- analyzeCollectedNodes(filePath) {
199
- this.extractImports();
200
-
201
- this.analyzeImportsAST(filePath);
202
-
203
- for (const cls of this.classes) {
204
- this.analyzeClassAST(cls, filePath);
205
- }
206
-
207
- for (const struct of this.structs) {
208
- this.analyzeStructAST(struct, filePath);
209
- }
210
-
211
- for (const proto of this.protocols) {
212
- this.analyzeProtocolAST(proto, filePath);
213
- }
214
-
215
- for (const func of this.functions) {
216
- this.analyzeFunctionAST(func, filePath);
217
- }
218
-
219
- for (const prop of this.properties) {
220
- this.analyzePropertyAST(prop, filePath);
221
- }
222
-
223
- this.analyzeClosuresAST(filePath);
224
- this.analyzeCleanArchitectureAST(filePath);
225
- this.analyzeAdditionalRules(filePath);
226
- }
227
-
228
- extractImports() {
229
- const lines = this.fileContent.split('\n');
230
- for (let i = 0; i < lines.length; i++) {
231
- const line = lines[i].trim();
232
- if (line.startsWith('import ')) {
233
- this.imports.push({
234
- name: line.replace('import ', '').trim(),
235
- line: i + 1
236
- });
237
- }
238
- }
239
- }
240
-
241
- analyzeImportsAST(filePath) {
242
- const importNames = this.imports.map(i => i.name);
243
-
244
- const hasUIKit = importNames.includes('UIKit');
245
- const hasSwiftUI = importNames.includes('SwiftUI');
246
- const hasCombine = importNames.includes('Combine');
247
-
248
- if (hasUIKit && hasSwiftUI) {
249
- this.pushFinding('ios.architecture.mixed_ui_frameworks', 'medium', filePath, 1,
250
- 'Mixing UIKit and SwiftUI - consider separating concerns');
251
- }
252
-
253
- const hasAsyncFunction = this.functions.some(f =>
254
- this.hasAttribute(f, 'async')
255
- );
256
-
257
- if (hasCombine && !hasAsyncFunction) {
258
- this.pushFinding('ios.concurrency.combine_without_async', 'low', filePath, 1,
259
- 'Using Combine - consider async/await for simpler async code');
260
- }
261
-
262
- for (const imp of this.imports) {
263
- if (['Foundation', 'SwiftUI', 'UIKit', 'Combine'].includes(imp.name)) continue;
264
-
265
- const isUsed = this.allNodes.some(n => {
266
- const typename = n['key.typename'] || '';
267
- const name = n['key.name'] || '';
268
- return typename.includes(imp.name) || name.includes(imp.name);
269
- });
270
-
271
- if (!isUsed) {
272
- this.pushFinding('ios.imports.unused', 'low', filePath, imp.line,
273
- `Unused import: ${imp.name}`);
274
- }
275
- }
276
-
277
- if (filePath.includes('/Domain/') && (hasUIKit || hasSwiftUI)) {
278
- this.pushFinding('ios.architecture.domain_ui_import', 'critical', filePath, 1,
279
- 'Domain layer imports UI framework - violates Clean Architecture');
280
- }
281
- }
282
-
283
- analyzeClassAST(node, filePath) {
284
- const name = node['key.name'] || '';
285
- const line = node['key.line'] || 1;
286
- const bodyLength = node['key.bodylength'] || 0;
287
- const substructure = node['key.substructure'] || [];
288
- const inheritedTypes = (node['key.inheritedtypes'] || []).map(t => t['key.name']);
289
- const attributes = this.getAttributes(node);
290
-
291
- const methods = substructure.filter(n => (n['key.kind'] || '').includes('function'));
292
- const properties = substructure.filter(n => (n['key.kind'] || '').includes('var'));
293
- const inits = methods.filter(m => (m['key.name'] || '').startsWith('init'));
294
-
295
- if (name && !/Spec$|Test$|Mock/.test(name)) {
296
- const complexity = this.calculateComplexityAST(substructure);
297
- this.godClassCandidates.push({
298
- name,
299
- filePath,
300
- line,
301
- methodsCount: methods.length,
302
- propertiesCount: properties.length,
303
- bodyLength,
304
- complexity,
305
- });
306
- }
307
-
308
- if (name.includes('ViewController')) {
309
- if (bodyLength > 250 || methods.length > 8) {
310
- this.pushFinding('ios.architecture.massive_viewcontroller', 'high', filePath, line,
311
- `Massive ViewController '${name}': ${bodyLength} lines - extract to ViewModel`);
312
- }
313
-
314
- const hasBusinessLogic = methods.some(m => {
315
- const methodName = m['key.name'] || '';
316
- return /calculate|process|validate|fetch|save|delete|update/i.test(methodName);
317
- });
318
-
319
- if (hasBusinessLogic) {
320
- this.pushFinding('ios.architecture.vc_business_logic', 'high', filePath, line,
321
- `ViewController '${name}' contains business logic - move to UseCase`);
322
- }
323
- }
324
-
325
- if (name.includes('ViewModel')) {
326
- const hasMainActor = attributes.includes('MainActor');
327
- if (!hasMainActor) {
328
- this.pushFinding('ios.concurrency.viewmodel_mainactor', 'high', filePath, line,
329
- `ViewModel '${name}' should be @MainActor for UI safety`);
330
- }
331
-
332
- const hasObservable = inheritedTypes.includes('ObservableObject') ||
333
- attributes.includes('Observable');
334
- if (!hasObservable) {
335
- this.pushFinding('ios.swiftui.viewmodel_observable', 'medium', filePath, line,
336
- `ViewModel '${name}' should conform to ObservableObject`);
337
- }
338
-
339
- if (inits.length === 0 && properties.length > 0) {
340
- this.pushFinding('ios.architecture.viewmodel_no_di', 'high', filePath, line,
341
- `ViewModel '${name}' has no init - use dependency injection`);
342
- }
343
- }
344
-
345
- if (/Manager$|Helper$|Utils$|Handler$/.test(name)) {
346
- this.pushFinding('ios.naming.god_naming', 'medium', filePath, line,
347
- `Suspicious class name '${name}' - often indicates SRP violation`);
348
- }
349
-
350
- const unusedProps = this.findUnusedPropertiesAST(properties, methods);
351
- for (const prop of unusedProps) {
352
- this.pushFinding('ios.solid.isp.unused_dependency', 'high', filePath, line,
353
- `Unused property '${prop}' in '${name}' - ISP violation`);
354
- }
355
-
356
- this.checkDependencyInjectionAST(properties, filePath, name, line);
357
-
358
- const hasDeinit = methods.some(m => m['key.name'] === 'deinit');
359
- const hasObservers = this.closures.some(c => c._parent === node) ||
360
- properties.some(p => this.hasAttribute(p, 'Published'));
361
-
362
- if (!hasDeinit && hasObservers && !name.includes('ViewModel')) {
363
- this.pushFinding('ios.memory.missing_deinit', 'high', filePath, line,
364
- `Class '${name}' has observers but no deinit - potential memory leak`);
365
- }
366
-
367
- const isFinal = attributes.includes('final');
368
- if (!isFinal && inheritedTypes.length === 0 && name !== 'AppDelegate') {
369
- this.pushFinding('ios.performance.non_final_class', 'low', filePath, line,
370
- `Class '${name}' is not final - consider final for performance`);
371
- }
372
-
373
- const hasSingleton = properties.some(p => {
374
- const propName = p['key.name'] || '';
375
- const isStatic = (p['key.kind'] || '').includes('static');
376
- return isStatic && propName === 'shared';
377
- });
378
-
379
- if (hasSingleton) {
380
- this.pushFinding('ios.antipattern.singleton', 'high', filePath, line,
381
- `Singleton pattern in '${name}' - use dependency injection`);
382
- }
383
-
384
- if (filePath.includes('Test') && name.includes('Test')) {
385
- const hasMakeSUT = methods.some(m => (m['key.name'] || '').includes('makeSUT'));
386
- if (!hasMakeSUT && methods.length > 2) {
387
- this.pushFinding('ios.testing.missing_makesut', 'medium', filePath, line,
388
- `Test class '${name}' missing makeSUT() factory`);
389
- }
390
- }
391
-
392
- if (name.includes('Repository') && !name.includes('Protocol')) {
393
- const hasProtocol = inheritedTypes.some(t => t.includes('Protocol'));
394
- if (!hasProtocol && !filePath.includes('/Domain/')) {
395
- this.pushFinding('ios.architecture.repository_no_protocol', 'high', filePath, line,
396
- `Repository '${name}' should implement a protocol`);
397
- }
398
- }
399
-
400
- if (name.includes('UseCase')) {
401
- const hasExecute = methods.some(m => (m['key.name'] || '').includes('execute'));
402
- if (!hasExecute) {
403
- this.pushFinding('ios.architecture.usecase_no_execute', 'medium', filePath, line,
404
- `UseCase '${name}' missing execute() method`);
405
- }
406
- }
407
- }
408
-
409
- analyzeStructAST(node, filePath) {
410
- const name = node['key.name'] || '';
411
- const line = node['key.line'] || 1;
412
- const substructure = node['key.substructure'] || [];
413
- const inheritedTypes = (node['key.inheritedtypes'] || []).map(t => t['key.name']);
414
-
415
- const methods = substructure.filter(n => (n['key.kind'] || '').includes('function'));
416
- const properties = substructure.filter(n => (n['key.kind'] || '').includes('var'));
417
-
418
- if (inheritedTypes.includes('View')) {
419
- this.analyzeSwiftUIViewAST(node, filePath, name, line, methods, properties);
420
- }
421
-
422
- if (inheritedTypes.includes('Codable') || inheritedTypes.includes('Decodable')) {
423
- const hasOptionalProps = properties.some(p => (p['key.typename'] || '').includes('?'));
424
- const hasCodingKeys = substructure.some(n => n['key.name'] === 'CodingKeys');
425
-
426
- if (hasOptionalProps && !hasCodingKeys) {
427
- this.pushFinding('ios.codable.missing_coding_keys', 'low', filePath, line,
428
- `Struct '${name}' has optional properties - consider CodingKeys`);
429
- }
430
- }
431
-
432
- if ((inheritedTypes.includes('Equatable') || inheritedTypes.includes('Hashable')) &&
433
- properties.length > 5) {
434
- this.pushFinding('ios.performance.large_equatable', 'low', filePath, line,
435
- `Struct '${name}' has ${properties.length} properties with Equatable`);
436
- }
437
- }
438
-
439
- analyzeSwiftUIViewAST(node, filePath, name, line, methods, properties) {
440
- const bodyMethod = methods.find(m => m['key.name'] === 'body');
441
- if (bodyMethod) {
442
- const bodyLength = bodyMethod['key.bodylength'] || 0;
443
- if (bodyLength > 100) {
444
- this.pushFinding('ios.swiftui.complex_body', 'high', filePath, line,
445
- `View '${name}' has complex body (${bodyLength} lines) - extract subviews`);
446
- }
447
- }
448
-
449
- const stateProps = properties.filter(p =>
450
- this.hasAttribute(p, 'State') || this.hasAttribute(p, 'Binding')
451
- );
452
-
453
- if (stateProps.length > 5) {
454
- this.pushFinding('ios.swiftui.too_many_state', 'medium', filePath, line,
455
- `View '${name}' has ${stateProps.length} @State/@Binding - consider ViewModel`);
456
- }
457
-
458
- const hasObservedObject = properties.some(p => this.hasAttribute(p, 'ObservedObject'));
459
- const hasStateObject = properties.some(p => this.hasAttribute(p, 'StateObject'));
460
-
461
- if (hasObservedObject && !hasStateObject) {
462
- this.pushFinding('ios.swiftui.observed_without_state', 'medium', filePath, line,
463
- `View '${name}' uses @ObservedObject - consider @StateObject for ownership`);
464
- }
465
- }
466
-
467
- analyzeProtocolAST(node, filePath) {
468
- const name = node['key.name'] || '';
469
- const line = node['key.line'] || 1;
470
- const substructure = node['key.substructure'] || [];
471
-
472
- const requirements = substructure.filter(n =>
473
- (n['key.kind'] || '').includes('function') || (n['key.kind'] || '').includes('var')
474
- );
475
-
476
- if (requirements.length > 5) {
477
- this.pushFinding('ios.solid.isp.fat_protocol', 'medium', filePath, line,
478
- `Protocol '${name}' has ${requirements.length} requirements - consider splitting (ISP)`);
479
- }
480
- }
481
-
482
- countLinesInBody(node) {
483
- const offset = Number(node['key.bodyoffset']);
484
- const length = Number(node['key.bodylength']);
485
- if (!Number.isFinite(offset) || !Number.isFinite(length) || offset < 0 || length <= 0) {
486
- return 0;
487
- }
488
-
489
- try {
490
- const buf = Buffer.from(this.fileContent || '', 'utf8');
491
- const slice = buf.subarray(offset, Math.min(buf.length, offset + length));
492
- const text = slice.toString('utf8');
493
- if (!text) return 0;
494
- return text.split('\n').length;
495
- } catch {
496
- return 0;
497
- }
498
- }
499
-
500
- analyzeFunctionAST(node, filePath) {
501
- const name = node['key.name'] || '';
502
- const line = node['key.line'] || 1;
503
- const bodyLength = this.countLinesInBody(node) || 0;
504
- const attributes = this.getAttributes(node);
505
- const substructure = node['key.substructure'] || [];
506
-
507
- if (bodyLength > 50) {
508
- this.pushFinding('ios.quality.long_function', 'high', filePath, line,
509
- `Function '${name}' is too long (${bodyLength} lines)`);
510
- }
511
-
512
- const complexity = this.calculateComplexityAST(substructure);
513
- if (complexity > 10) {
514
- this.pushFinding('ios.quality.high_complexity', 'high', filePath, line,
515
- `Function '${name}' has high complexity (${complexity})`);
516
- }
517
-
518
- const params = substructure.filter(n => (n['key.kind'] || '').includes('var.parameter'));
519
- if (params.length > 5) {
520
- this.pushFinding('ios.quality.too_many_params', 'medium', filePath, line,
521
- `Function '${name}' has ${params.length} parameters - use struct`);
522
- }
523
-
524
- const closuresInFunc = this.countClosuresInNode(node);
525
- if (closuresInFunc > 3) {
526
- this.pushFinding('ios.quality.nested_closures', 'medium', filePath, line,
527
- `Function '${name}' has ${closuresInFunc} nested closures - use async/await`);
528
- }
529
-
530
- const ifStatements = this.countStatementsOfType(substructure, 'stmt.if');
531
- const guardStatements = this.countStatementsOfType(substructure, 'stmt.guard');
532
-
533
- if (ifStatements > 3 && guardStatements === 0) {
534
- this.pushFinding('ios.quality.pyramid_of_doom', 'medium', filePath, line,
535
- `Function '${name}' has ${ifStatements} nested ifs - use guard clauses`);
536
- }
537
-
538
- const isAsync = attributes.includes('async');
539
- const parentClass = node._parent;
540
- const parentIsView = parentClass &&
541
- (parentClass['key.inheritedtypes'] || []).some(t => t['key.name'] === 'View');
542
-
543
- if (isAsync && parentIsView && !attributes.includes('MainActor')) {
544
- this.pushFinding('ios.concurrency.async_ui_update', 'high', filePath, line,
545
- `Async function '${name}' in View - consider @MainActor`);
546
- }
547
- }
548
-
549
- analyzePropertyAST(node, filePath) {
550
- const name = node['key.name'] || '';
551
- const line = node['key.line'] || 1;
552
- const typename = node['key.typename'] || '';
553
- const attributes = this.getAttributes(node);
554
- const kind = node['key.kind'] || '';
555
-
556
- if (typename.includes('!') && !attributes.includes('IBOutlet')) {
557
- this.pushFinding('ios.safety.force_unwrap_property', 'high', filePath, line,
558
- `Force unwrapped property '${name}: ${typename}'`);
559
- }
560
-
561
- const isPublic = (node['key.accessibility'] || '').includes('public');
562
- const isInstance = kind.includes('var.instance');
563
-
564
- if (isPublic && isInstance && !attributes.includes('setter_access')) {
565
- this.pushFinding('ios.encapsulation.public_mutable', 'medium', filePath, line,
566
- `Public mutable property '${name}' - consider private(set)`);
567
- }
177
+ collectAllNodes(this, substructure, null);
178
+ analyzeCollectedNodes(this, displayPath);
568
179
  }
569
180
 
570
181
  safeStringify(obj) {
@@ -579,166 +190,6 @@ class iOSASTIntelligentAnalyzer {
579
190
  });
580
191
  }
581
192
 
582
- analyzeClosuresAST(filePath) {
583
- for (const closure of this.closures) {
584
- const closureText = this.safeStringify(closure);
585
- const hasSelfReference = closureText.includes('"self"') ||
586
- closureText.includes('key.name":"self');
587
-
588
- const parentFunc = closure._parent;
589
- const isEscaping = parentFunc &&
590
- (parentFunc['key.typename'] || '').includes('@escaping');
591
-
592
- if (hasSelfReference && isEscaping) {
593
- const hasWeakCapture = closureText.includes('weak') ||
594
- closureText.includes('unowned');
595
-
596
- if (!hasWeakCapture) {
597
- const line = closure['key.line'] || 1;
598
- this.pushFinding('ios.memory.missing_weak_self', 'high', filePath, line,
599
- 'Escaping closure captures self without [weak self]');
600
- }
601
- }
602
- }
603
- }
604
-
605
- analyzeCleanArchitectureAST(filePath) {
606
- const fileName = path.basename(filePath);
607
-
608
- if (fileName.includes('UseCase')) {
609
- const hasVoidReturn = this.functions.some(f => {
610
- const typename = f['key.typename'] || '';
611
- return (f['key.name'] || '').includes('execute') && typename.includes('Void');
612
- });
613
-
614
- if (hasVoidReturn) {
615
- this.pushFinding('ios.architecture.usecase_void', 'medium', filePath, 1,
616
- 'UseCase returns Void - consider returning Result');
617
- }
618
-
619
- if (!filePath.includes('/Domain/') && !filePath.includes('/Application/')) {
620
- this.pushFinding('ios.architecture.usecase_wrong_layer', 'high', filePath, 1,
621
- 'UseCase should be in Domain or Application layer');
622
- }
623
- }
624
-
625
- if (filePath.includes('/Infrastructure/')) {
626
- const hasUIImport = this.imports.some(i => ['SwiftUI', 'UIKit'].includes(i.name));
627
- if (hasUIImport) {
628
- this.pushFinding('ios.architecture.infrastructure_ui', 'critical', filePath, 1,
629
- 'Infrastructure layer imports UI framework - violates Clean Architecture');
630
- }
631
- }
632
-
633
- if (filePath.includes('/Presentation/') || filePath.includes('/Views/')) {
634
- const hasInfraImport = this.imports.some(i =>
635
- i.name.includes('Alamofire') || i.name.includes('Realm') || i.name.includes('CoreData')
636
- );
637
- if (hasInfraImport) {
638
- this.pushFinding('ios.architecture.presentation_infra', 'high', filePath, 1,
639
- 'Presentation imports Infrastructure directly - use Domain layer');
640
- }
641
- }
642
- }
643
-
644
- analyzeAdditionalRules(filePath) {
645
- const hasSwiftUIViewType = (this.imports || []).some(i => i && i.name === 'SwiftUI') &&
646
- (this.structs || []).some(s => (s['key.inheritedtypes'] || []).some(t => t && t['key.name'] === 'View'));
647
-
648
- if (this.fileContent.includes('pushViewController') || this.fileContent.includes('popViewController') || this.fileContent.includes('present(')) {
649
- const line = this.findLineNumber('pushViewController') || this.findLineNumber('popViewController') || this.findLineNumber('present(');
650
- this.pushFinding('ios.navigation.imperative_navigation', 'critical', filePath, line,
651
- 'Imperative navigation detected - use event-driven navigation/coordinator');
652
- }
653
-
654
- const swiftuiNavTokens = ['NavigationLink', 'NavigationStack', 'NavigationSplitView', '.navigationDestination'];
655
- const hasSwiftUINavigation = swiftuiNavTokens.some(token => this.fileContent.includes(token));
656
- if (hasSwiftUINavigation && !hasSwiftUIViewType) {
657
- const line = this.findLineNumber('NavigationLink') || this.findLineNumber('NavigationStack') || this.findLineNumber('.navigationDestination');
658
- this.pushFinding('ios.navigation.swiftui_navigation_outside_view', 'critical', filePath, line,
659
- 'SwiftUI navigation API detected outside View types');
660
- }
661
-
662
- if (filePath.includes('ViewModel') && this.fileContent.includes('NavigationLink')) {
663
- const hasCoordinator = this.imports.some(i => i.name.includes('Coordinator'));
664
- if (!hasCoordinator) {
665
- this.pushFinding('ios.architecture.missing_coordinator', 'medium', filePath, 1,
666
- 'Navigation in ViewModel - consider Coordinator pattern');
667
- }
668
- }
669
-
670
- if (this.fileContent.includes('DispatchQueue.main') || this.fileContent.includes('DispatchQueue.global')) {
671
- const line = this.findLineNumber('DispatchQueue');
672
- this.pushFinding('ios.concurrency.dispatch_queue', 'medium', filePath, line,
673
- 'DispatchQueue detected - use async/await in new code');
674
- }
675
-
676
- if (/Task\s*\{/.test(this.fileContent) && !/Task\s*\{[^}]*do\s*\{/.test(this.fileContent)) {
677
- const line = this.findLineNumber('Task {');
678
- this.pushFinding('ios.concurrency.task_no_error_handling', 'high', filePath, line,
679
- 'Task without do-catch - handle errors');
680
- }
681
-
682
- if (this.fileContent.includes('UserDefaults') && /password|token|secret|key/i.test(this.fileContent)) {
683
- const line = this.findLineNumber('UserDefaults');
684
- this.pushFinding('ios.security.sensitive_userdefaults', 'critical', filePath, line,
685
- 'Sensitive data in UserDefaults - use Keychain');
686
- }
687
-
688
- const hardcodedStrings = this.fileContent.match(/Text\s*\(\s*"[^"]{10,}"\s*\)/g) || [];
689
- if (hardcodedStrings.length > 3) {
690
- this.pushFinding('ios.i18n.hardcoded_strings', 'medium', filePath, 1,
691
- `${hardcodedStrings.length} hardcoded strings - use NSLocalizedString`);
692
- }
693
-
694
- if (this.fileContent.includes('Image(') && !this.fileContent.includes('.accessibilityLabel')) {
695
- const imageCount = (this.fileContent.match(/Image\s*\(/g) || []).length;
696
- const labelCount = (this.fileContent.match(/\.accessibilityLabel/g) || []).length;
697
- if (imageCount > labelCount + 2) {
698
- this.pushFinding('ios.accessibility.missing_labels', 'medium', filePath, 1,
699
- 'Images without accessibilityLabel - add for VoiceOver');
700
- }
701
- }
702
-
703
- if (this.fileContent.includes('@IBOutlet') || this.fileContent.includes('@IBAction')) {
704
- const line = this.findLineNumber('@IB');
705
- this.pushFinding('ios.deprecated.storyboard', 'low', filePath, line,
706
- 'Storyboard/XIB detected - consider SwiftUI or programmatic UI');
707
- }
708
-
709
- const completionCount = (this.fileContent.match(/completion\s*:\s*@escaping/g) || []).length;
710
- if (completionCount > 2) {
711
- this.pushFinding('ios.concurrency.completion_handlers', 'medium', filePath, 1,
712
- `${completionCount} completion handlers - use async/await`);
713
- }
714
-
715
- for (const cls of this.classes) {
716
- const hasSharedState = this.properties.some(p =>
717
- (p['key.kind'] || '').includes('static') &&
718
- !this.hasAttribute(p, 'MainActor') &&
719
- !this.hasAttribute(p, 'nonisolated')
720
- );
721
- if (hasSharedState && !this.fileContent.includes('actor ')) {
722
- const name = cls['key.name'] || '';
723
- const line = cls['key.line'] || 1;
724
- this.pushFinding('ios.concurrency.missing_actor', 'high', filePath, line,
725
- `Class '${name}' has shared state - consider actor for thread safety`);
726
- }
727
- }
728
-
729
- for (const cls of this.classes) {
730
- const name = cls['key.name'] || '';
731
- const inheritedTypes = (cls['key.inheritedtypes'] || []).map(t => t['key.name']);
732
- const hasAsync = this.functions.some(f => this.hasAttribute(f, 'async'));
733
-
734
- if (hasAsync && !inheritedTypes.includes('Sendable') && !name.includes('ViewModel')) {
735
- const line = cls['key.line'] || 1;
736
- this.pushFinding('ios.concurrency.missing_sendable', 'medium', filePath, line,
737
- `Class '${name}' used in async context - consider Sendable conformance`);
738
- }
739
- }
740
- }
741
-
742
193
  findLineNumber(text) {
743
194
  const idx = this.fileContent.indexOf(text);
744
195
  if (idx === -1) return 1;
@@ -758,107 +209,6 @@ class iOSASTIntelligentAnalyzer {
758
209
  });
759
210
  }
760
211
 
761
- findUnusedPropertiesAST(properties, methods) {
762
- const unused = [];
763
-
764
- for (const prop of properties) {
765
- const propName = prop['key.name'];
766
- if (!propName) continue;
767
-
768
- if (this.hasAttribute(prop, 'Published') ||
769
- this.hasAttribute(prop, 'State') ||
770
- this.hasAttribute(prop, 'Binding')) {
771
- continue;
772
- }
773
-
774
- let usageCount = 0;
775
- for (const method of methods) {
776
- const methodText = this.safeStringify(method);
777
- if (methodText.includes(`"${propName}"`)) {
778
- usageCount++;
779
- }
780
- }
781
-
782
- if (usageCount === 0) {
783
- unused.push(propName);
784
- }
785
- }
786
-
787
- return unused;
788
- }
789
-
790
- calculateComplexityAST(substructure) {
791
- let complexity = 1;
792
-
793
- const traverse = (nodes) => {
794
- if (!Array.isArray(nodes)) return;
795
-
796
- for (const node of nodes) {
797
- const kind = node['key.kind'] || '';
798
-
799
- if (kind.includes('stmt.if') || kind.includes('stmt.guard') ||
800
- kind.includes('stmt.switch') || kind.includes('stmt.for') ||
801
- kind.includes('stmt.while') || kind.includes('stmt.catch')) {
802
- complexity++;
803
- }
804
-
805
- traverse(node['key.substructure'] || []);
806
- }
807
- };
808
-
809
- traverse(substructure);
810
- return complexity;
811
- }
812
-
813
- countClosuresInNode(node) {
814
- let count = 0;
815
- const traverse = (n) => {
816
- if ((n['key.kind'] || '').includes('closure')) count++;
817
- for (const child of (n['key.substructure'] || [])) {
818
- traverse(child);
819
- }
820
- };
821
- traverse(node);
822
- return count;
823
- }
824
-
825
- countStatementsOfType(substructure, stmtType) {
826
- let count = 0;
827
- const traverse = (nodes) => {
828
- if (!Array.isArray(nodes)) return;
829
- for (const node of nodes) {
830
- if ((node['key.kind'] || '').includes(stmtType)) count++;
831
- traverse(node['key.substructure'] || []);
832
- }
833
- };
834
- traverse(substructure);
835
- return count;
836
- }
837
-
838
- checkDependencyInjectionAST(properties, filePath, className, line) {
839
- if (!className.includes('ViewModel') && !className.includes('Service') &&
840
- !className.includes('Repository') && !className.includes('UseCase')) {
841
- return;
842
- }
843
-
844
- for (const prop of properties) {
845
- const typename = prop['key.typename'] || '';
846
-
847
- if (['String', 'Int', 'Bool', 'Double', 'Float', 'Date', 'URL', 'Data'].includes(typename)) {
848
- continue;
849
- }
850
-
851
- const isConcreteService = /Service$|Repository$|UseCase$|Client$/.test(typename) &&
852
- !typename.includes('Protocol') &&
853
- !typename.includes('any ') &&
854
- !typename.includes('some ');
855
-
856
- if (isConcreteService) {
857
- this.pushFinding('ios.solid.dip.concrete_dependency', 'high', filePath, line,
858
- `'${className}' depends on concrete '${typename}' - use protocol`);
859
- }
860
- }
861
- }
862
212
 
863
213
  pushFinding(ruleId, severity, filePath, line, message) {
864
214
  this.findings.push({
@@ -872,110 +222,7 @@ class iOSASTIntelligentAnalyzer {
872
222
  }
873
223
 
874
224
  finalizeGodClassDetection() {
875
- if (!this.godClassCandidates || this.godClassCandidates.length < 10) return;
876
-
877
- const quantile = (values, p) => {
878
- if (!values || values.length === 0) return 0;
879
- const sorted = [...values].filter((v) => Number.isFinite(v)).sort((a, b) => a - b);
880
- if (sorted.length === 0) return 0;
881
- const idx = Math.max(0, Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1));
882
- return sorted[idx];
883
- };
884
-
885
- const median = (values) => {
886
- if (!values || values.length === 0) return 0;
887
- const sorted = [...values].filter((v) => Number.isFinite(v)).sort((a, b) => a - b);
888
- if (sorted.length === 0) return 0;
889
- const mid = Math.floor(sorted.length / 2);
890
- if (sorted.length % 2 === 0) return (sorted[mid - 1] + sorted[mid]) / 2;
891
- return sorted[mid];
892
- };
893
-
894
- const mad = (values) => {
895
- const med = median(values);
896
- const deviations = (values || []).map((v) => Math.abs(v - med));
897
- return median(deviations);
898
- };
899
-
900
- const robustZ = (x, med, madValue) => {
901
- if (!Number.isFinite(x) || !Number.isFinite(med) || !Number.isFinite(madValue) || madValue === 0) return 0;
902
- return 0.6745 * (x - med) / madValue;
903
- };
904
-
905
- const env = require('../../../../config/env');
906
- const pOutlier = env.getNumber('AST_GODCLASS_P_OUTLIER', 90);
907
- const pExtreme = env.getNumber('AST_GODCLASS_P_EXTREME', 97);
908
-
909
- const methods = this.godClassCandidates.map(c => c.methodsCount);
910
- const props = this.godClassCandidates.map(c => c.propertiesCount);
911
- const bodies = this.godClassCandidates.map(c => c.bodyLength);
912
- const complexities = this.godClassCandidates.map(c => c.complexity);
913
-
914
- const med = {
915
- methodsCount: median(methods),
916
- propertiesCount: median(props),
917
- bodyLength: median(bodies),
918
- complexity: median(complexities),
919
- };
920
- const madValue = {
921
- methodsCount: mad(methods),
922
- propertiesCount: mad(props),
923
- bodyLength: mad(bodies),
924
- complexity: mad(complexities),
925
- };
926
-
927
- const z = {
928
- methodsCount: methods.map(v => robustZ(v, med.methodsCount, madValue.methodsCount)),
929
- propertiesCount: props.map(v => robustZ(v, med.propertiesCount, madValue.propertiesCount)),
930
- bodyLength: bodies.map(v => robustZ(v, med.bodyLength, madValue.bodyLength)),
931
- complexity: complexities.map(v => robustZ(v, med.complexity, madValue.complexity)),
932
- };
933
-
934
- const thresholds = {
935
- outlier: {
936
- methodsCountZ: quantile(z.methodsCount, pOutlier),
937
- propertiesCountZ: quantile(z.propertiesCount, pOutlier),
938
- bodyLengthZ: quantile(z.bodyLength, pOutlier),
939
- complexityZ: quantile(z.complexity, pOutlier),
940
- },
941
- extreme: {
942
- methodsCountZ: quantile(z.methodsCount, pExtreme),
943
- propertiesCountZ: quantile(z.propertiesCount, pExtreme),
944
- bodyLengthZ: quantile(z.bodyLength, pExtreme),
945
- complexityZ: quantile(z.complexity, pExtreme),
946
- }
947
- };
948
-
949
- for (const c of this.godClassCandidates) {
950
- const methodsZ = robustZ(c.methodsCount, med.methodsCount, madValue.methodsCount);
951
- const propsZ = robustZ(c.propertiesCount, med.propertiesCount, madValue.propertiesCount);
952
- const bodyZ = robustZ(c.bodyLength, med.bodyLength, madValue.bodyLength);
953
- const complexityZ = robustZ(c.complexity, med.complexity, madValue.complexity);
954
-
955
- const sizeOutlier =
956
- (methodsZ > 0 && methodsZ >= thresholds.outlier.methodsCountZ) ||
957
- (propsZ > 0 && propsZ >= thresholds.outlier.propertiesCountZ) ||
958
- (bodyZ > 0 && bodyZ >= thresholds.outlier.bodyLengthZ);
959
- const complexityOutlier = complexityZ > 0 && complexityZ >= thresholds.outlier.complexityZ;
960
-
961
- const extremeOutlier =
962
- (methodsZ > 0 && methodsZ >= thresholds.extreme.methodsCountZ) ||
963
- (propsZ > 0 && propsZ >= thresholds.extreme.propertiesCountZ) ||
964
- (bodyZ > 0 && bodyZ >= thresholds.extreme.bodyLengthZ) ||
965
- (complexityZ > 0 && complexityZ >= thresholds.extreme.complexityZ);
966
-
967
- const signalCount = [sizeOutlier, complexityOutlier].filter(Boolean).length;
968
-
969
- if (extremeOutlier || signalCount >= 2) {
970
- this.pushFinding(
971
- 'ios.solid.srp.god_class',
972
- 'critical',
973
- c.filePath,
974
- c.line,
975
- `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`
976
- );
977
- }
978
- }
225
+ finalizeGodClassDetection(this);
979
226
  }
980
227
  }
981
228