pumuki-ast-hooks 5.6.5 → 5.6.6

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 (26) hide show
  1. package/README.md +47 -10
  2. package/docs/images/ai-start.png +0 -0
  3. package/docs/images/pre-flight-check.png +0 -0
  4. package/hooks/pre-tool-use-guard.ts +105 -1
  5. package/package.json +2 -2
  6. package/scripts/hooks-system/.audit-reports/auto-recovery.log +3 -0
  7. package/scripts/hooks-system/.audit-reports/install-wizard.log +12 -0
  8. package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +72 -0
  9. package/scripts/hooks-system/bin/__tests__/cli-audit-no-stack.spec.js +22 -0
  10. package/scripts/hooks-system/bin/cli.js +176 -7
  11. package/scripts/hooks-system/bin/update-evidence.sh +8 -0
  12. package/scripts/hooks-system/infrastructure/ast/android/analyzers/AndroidSOLIDAnalyzer.js +33 -5
  13. package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +7 -1
  14. package/scripts/hooks-system/infrastructure/ast/common/__tests__/ast-common.spec.js +19 -0
  15. package/scripts/hooks-system/infrastructure/ast/common/ast-common.js +24 -19
  16. package/scripts/hooks-system/infrastructure/ast/ios/__tests__/forbidden-testable-import.spec.js +21 -0
  17. package/scripts/hooks-system/infrastructure/ast/ios/__tests__/missing-makesut-leaks.spec.js +64 -0
  18. package/scripts/hooks-system/infrastructure/ast/ios/ast-ios.js +74 -33
  19. package/scripts/hooks-system/infrastructure/ast/ios/detectors/__tests__/ios-encapsulation-public-mutable.spec.js +63 -0
  20. package/scripts/hooks-system/infrastructure/ast/ios/detectors/__tests__/ios-unused-imports.spec.js +34 -0
  21. package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-ast-intelligent-strategies.js +15 -2
  22. package/scripts/hooks-system/infrastructure/mcp/__tests__/preflight-check-blocks-tests.spec.js +14 -0
  23. package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js +154 -50
  24. package/scripts/hooks-system/infrastructure/orchestration/__tests__/intelligent-audit.spec.js +39 -1
  25. package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +67 -0
  26. package/scripts/hooks-system/infrastructure/watchdog/__tests__/.audit-reports/token-monitor.log +9 -0
@@ -26,3 +26,11 @@ fi
26
26
 
27
27
  AUTO_EVIDENCE_TRIGGER="$AUTO_TRIGGER" AUTO_EVIDENCE_REASON="$AUTO_REASON" AUTO_EVIDENCE_SUMMARY="$AUTO_SUMMARY" \
28
28
  node "$CLI" evidence:full-update
29
+
30
+ EXIT_CODE=$?
31
+ if [[ "$EXIT_CODE" -ne 0 ]]; then
32
+ echo " Evidence updated but gate reported violations (exit code: $EXIT_CODE)." >&2
33
+ exit 0
34
+ fi
35
+
36
+ exit 0
@@ -20,6 +20,34 @@ class AndroidSOLIDAnalyzer {
20
20
  this.findings = [];
21
21
  }
22
22
 
23
+ analyzeOCP(sf, findings, pushFinding) {
24
+ return analyzeOCP(sf, findings, pushFinding);
25
+ }
26
+
27
+ analyzeDIP(sf, findings, pushFinding) {
28
+ return analyzeDIP(sf, findings, pushFinding);
29
+ }
30
+
31
+ analyzeSRP(sf, findings, pushFinding) {
32
+ return analyzeSRP(sf, findings, pushFinding);
33
+ }
34
+
35
+ analyzeISP(sf, findings, pushFinding) {
36
+ return analyzeISP(sf, findings, pushFinding);
37
+ }
38
+
39
+ detectMethodConcern(methodName) {
40
+ const name = String(methodName || '').toLowerCase();
41
+
42
+ if (/^(get|fetch|load|read|find|query)/.test(name)) return 'data-access';
43
+ if (/^(set|update|save|create|delete|remove|insert)/.test(name)) return 'data-mutation';
44
+ if (/^(validate|verify|check|ensure)/.test(name)) return 'validation';
45
+ if (/^(format|map|transform|convert|parse)/.test(name)) return 'transformation';
46
+ if (/^(render|draw|display|show)/.test(name)) return 'rendering';
47
+
48
+ return 'unknown';
49
+ }
50
+
23
51
  /**
24
52
  * Analyze source file for SOLID violations
25
53
  * @param {SourceFile} sf - TypeScript morph source file
@@ -33,11 +61,11 @@ class AndroidSOLIDAnalyzer {
33
61
  this.findings = findings;
34
62
  this.pushFinding = pushFinding;
35
63
 
36
- analyzeOCP(sf, findings, pushFinding);
37
- analyzeDIP(sf, findings, pushFinding);
38
- analyzeSRP(sf, findings, pushFinding);
39
- analyzeISP(sf, findings, pushFinding);
64
+ this.analyzeOCP(sf, findings, pushFinding);
65
+ this.analyzeDIP(sf, findings, pushFinding);
66
+ this.analyzeSRP(sf, findings, pushFinding);
67
+ this.analyzeISP(sf, findings, pushFinding);
40
68
  }
41
69
  }
42
70
 
43
- module.exports = AndroidSOLIDAnalyzer;
71
+ module.exports = { AndroidSOLIDAnalyzer };
@@ -236,6 +236,7 @@ function runBackendIntelligence(project, findings, platform) {
236
236
 
237
237
  for (const match of matches) {
238
238
  const fullMatch = match[0];
239
+ const secretField = String(match[1] || '').toLowerCase();
239
240
  const secretValue = match[2];
240
241
 
241
242
  const matchIndex = match.index || 0;
@@ -274,6 +275,11 @@ function runBackendIntelligence(project, findings, platform) {
274
275
 
275
276
  const matchesSecretEntropyPattern = secretEntropyPattern.test(secretValue);
276
277
 
278
+ const isGenericKeyField = secretField === 'key';
279
+ const isLikelyNonSecretKeyValue = isGenericKeyField &&
280
+ secretValue.length < 20 &&
281
+ !matchesSecretEntropyPattern;
282
+
277
283
  const isConstantKey = /(?:const|let|var)\s+\w*(?:KEY|TOKEN|STORAGE)\s*=/i.test(fullLine) &&
278
284
  secretValue.length < 30 &&
279
285
  !matchesSecretEntropyPattern;
@@ -281,7 +287,7 @@ function runBackendIntelligence(project, findings, platform) {
281
287
 
282
288
  const isTestData = isTestFilePath && secretValue.length < 50 && !matchesSecretEntropyPattern;
283
289
 
284
- if (!isEnvVar && !isPlaceholder && !isKnownConfigValue && !isComment && !isRuleDefinition && !isTestContext && !isStorageKey && !isCacheKey && !isConstantKey && !isRolesDecorator && !isTestData && secretValue.length >= 8) {
290
+ if (!isEnvVar && !isPlaceholder && !isKnownConfigValue && !isComment && !isRuleDefinition && !isTestContext && !isStorageKey && !isCacheKey && !isLikelyNonSecretKeyValue && !isConstantKey && !isRolesDecorator && !isTestData && secretValue.length >= 8) {
285
291
  pushFinding("backend.config.secrets_in_code", "critical", sf, sf, "Hardcoded secret detected - replace with environment variable (process.env)", findings);
286
292
  }
287
293
  }
@@ -1,3 +1,8 @@
1
+ jest.mock('../BDDTDDWorkflowRules', () => ({
2
+ BDDTDDWorkflowRules: jest.fn().mockImplementation(() => ({ analyze: jest.fn() }))
3
+ }));
4
+
5
+ const { Project } = require('../../ast-core');
1
6
  const { runCommonIntelligence } = require('../ast-common');
2
7
 
3
8
  describe('AST Common Module', () => {
@@ -9,6 +14,20 @@ describe('AST Common Module', () => {
9
14
  it('should be callable', () => {
10
15
  expect(runCommonIntelligence).toBeDefined();
11
16
  });
17
+
18
+ it('does not apply makeSUT/trackForMemoryLeaks rules to Jest spec files', () => {
19
+ const project = new Project({
20
+ useInMemoryFileSystem: true,
21
+ skipAddingFilesFromTsConfig: true,
22
+ });
23
+ project.createSourceFile('/tmp/foo.spec.js', "import XCTest\n\ndescribe('x', () => {})");
24
+
25
+ const findings = [];
26
+ runCommonIntelligence(project, findings);
27
+
28
+ expect(findings.some(f => f.ruleId === 'common.testing.missing_makesut')).toBe(false);
29
+ expect(findings.some(f => f.ruleId === 'common.testing.missing_track_for_memory_leaks')).toBe(false);
30
+ });
12
31
  });
13
32
 
14
33
  describe('exports', () => {
@@ -210,26 +210,31 @@ function runCommonIntelligence(project, findings) {
210
210
  if (isTestFilePath(filePath)) {
211
211
  const content = sf.getFullText();
212
212
 
213
- if (!shouldSkipMakeSUTRule(filePath) && !hasMakeSUT(content)) {
214
- pushFinding(
215
- 'common.testing.missing_makesut',
216
- 'high',
217
- sf,
218
- sf,
219
- 'Test file without makeSUT factory - use makeSUT pattern for consistent DI and teardown',
220
- findings
221
- );
222
- }
213
+ const ext = path.extname(filePath).toLowerCase();
214
+ const isSwiftOrKotlinTest = ext === '.swift' || ext === '.kt' || ext === '.kts';
223
215
 
224
- if (!shouldSkipTrackForMemoryLeaksRule(filePath) && !hasTrackForMemoryLeaksEvidence(content)) {
225
- pushFinding(
226
- 'common.testing.missing_track_for_memory_leaks',
227
- 'critical',
228
- sf,
229
- sf,
230
- 'Test file without trackForMemoryLeaks - add memory leak tracking to prevent leaks and cross-test interference',
231
- findings
232
- );
216
+ if (isSwiftOrKotlinTest) {
217
+ if (!shouldSkipMakeSUTRule(filePath) && !hasMakeSUT(content)) {
218
+ pushFinding(
219
+ 'common.testing.missing_makesut',
220
+ 'high',
221
+ sf,
222
+ sf,
223
+ 'Test file without makeSUT factory - use makeSUT pattern for consistent DI and teardown',
224
+ findings
225
+ );
226
+ }
227
+
228
+ if (!shouldSkipTrackForMemoryLeaksRule(filePath) && !hasTrackForMemoryLeaksEvidence(content)) {
229
+ pushFinding(
230
+ 'common.testing.missing_track_for_memory_leaks',
231
+ 'critical',
232
+ sf,
233
+ sf,
234
+ 'Test file without trackForMemoryLeaks - add memory leak tracking to prevent leaks and cross-test interference',
235
+ findings
236
+ );
237
+ }
233
238
  }
234
239
 
235
240
  if (!shouldSkipSpyOverMockRule(filePath)) {
@@ -0,0 +1,21 @@
1
+ const { detectForbiddenTestableImport } = require('../ast-ios');
2
+
3
+ describe('ios.imports.forbidden_testable', () => {
4
+ it('returns a finding when @testable is present in XCTest test file', () => {
5
+ const filePath = '/SomeModule/Tests/FooTests.swift';
6
+ const content = 'import XCTest\n@testable import Foo\nfinal class FooTests: XCTestCase {}';
7
+
8
+ const finding = detectForbiddenTestableImport({ filePath, content });
9
+
10
+ expect(finding).not.toBeNull();
11
+ expect(finding.ruleId).toBe('ios.imports.forbidden_testable');
12
+ expect(finding.severity).toBe('high');
13
+ });
14
+
15
+ it('returns null when @testable is not present', () => {
16
+ const filePath = '/SomeModule/Tests/FooTests.swift';
17
+ const content = 'import XCTest\nimport Foo\nfinal class FooTests: XCTestCase {}';
18
+
19
+ expect(detectForbiddenTestableImport({ filePath, content })).toBeNull();
20
+ });
21
+ });
@@ -0,0 +1,64 @@
1
+ const { detectMissingMakeSUT, detectMissingLeakTracking } = require('../ast-ios');
2
+
3
+ describe('iOS testing rules - makeSUT / leak tracking', () => {
4
+ it('flags missing makeSUT as HIGH when tests exist', () => {
5
+ const filePath = '/SomeModule/Tests/FooTests.swift';
6
+ const content = [
7
+ 'import XCTest',
8
+ 'final class FooTests: XCTestCase {',
9
+ ' func test_one() { XCTAssertTrue(true) }',
10
+ '}'
11
+ ].join('\n');
12
+
13
+ const finding = detectMissingMakeSUT({ filePath, content });
14
+ expect(finding).not.toBeNull();
15
+ expect(finding.ruleId).toBe('ios.testing.missing_make_sut');
16
+ expect(finding.severity).toBe('high');
17
+ });
18
+
19
+ it('does not flag missing makeSUT when file defines makeSUT()', () => {
20
+ const filePath = '/SomeModule/Tests/FooTests.swift';
21
+ const content = [
22
+ 'import XCTest',
23
+ 'final class FooTests: XCTestCase {',
24
+ ' func makeSUT() -> Foo { Foo() }',
25
+ ' func test_one() { let _ = makeSUT() }',
26
+ '}'
27
+ ].join('\n');
28
+
29
+ expect(detectMissingMakeSUT({ filePath, content })).toBeNull();
30
+ });
31
+
32
+ it('flags missing leak tracking as HIGH when tests exist and no tracking is present', () => {
33
+ const filePath = '/SomeModule/Tests/FooTests.swift';
34
+ const content = [
35
+ 'import XCTest',
36
+ 'final class FooTests: XCTestCase {',
37
+ ' func makeSUT() -> Foo { Foo() }',
38
+ ' func test_one() { let _ = makeSUT() }',
39
+ '}'
40
+ ].join('\n');
41
+
42
+ const finding = detectMissingLeakTracking({ filePath, content });
43
+ expect(finding).not.toBeNull();
44
+ expect(finding.ruleId).toBe('ios.testing.missing_leak_tracking');
45
+ expect(finding.severity).toBe('high');
46
+ });
47
+
48
+ it('does not flag leak tracking when makeSUT includes trackForMemoryLeaks', () => {
49
+ const filePath = '/SomeModule/Tests/FooTests.swift';
50
+ const content = [
51
+ 'import XCTest',
52
+ 'final class FooTests: XCTestCase {',
53
+ ' func makeSUT() -> Foo {',
54
+ ' let sut = Foo()',
55
+ ' trackForMemoryLeaks(sut)',
56
+ ' return sut',
57
+ ' }',
58
+ ' func test_one() { _ = makeSUT() }',
59
+ '}'
60
+ ].join('\n');
61
+
62
+ expect(detectMissingLeakTracking({ filePath, content })).toBeNull();
63
+ });
64
+ });
@@ -18,6 +18,48 @@ const { iOSForbiddenLiteralsAnalyzer } = require(path.join(__dirname, 'analyzers
18
18
  const { iOSASTIntelligentAnalyzer } = require(path.join(__dirname, 'analyzers/iOSASTIntelligentAnalyzer'));
19
19
  const { iOSModernPracticesRules } = require(path.join(__dirname, 'analyzers/iOSModernPracticesRules'));
20
20
 
21
+ function detectForbiddenTestableImport({ filePath, content }) {
22
+ if (!filePath || !content) return null;
23
+ if (!filePath.includes('Tests')) return null;
24
+ if (!content.includes('XCTest')) return null;
25
+ if (!/@testable\s+import\b/.test(content)) return null;
26
+ return {
27
+ ruleId: 'ios.imports.forbidden_testable',
28
+ severity: 'high',
29
+ message: 'Forbidden @testable import in tests - expose required production APIs as public/open (or via a dedicated TestUtilities module) instead of using @testable'
30
+ };
31
+ }
32
+
33
+ function detectMissingMakeSUT({ filePath, content }) {
34
+ if (!filePath || !content) return null;
35
+ if (!filePath.includes('Test')) return null;
36
+ if (!content.includes('XCTest')) return null;
37
+ const hasTests = /func\s+test\w*\s*\(/.test(content);
38
+ if (!hasTests) return null;
39
+ const hasMakeSUT = /func\s+makeSUT\b/.test(content);
40
+ if (hasMakeSUT) return null;
41
+ return {
42
+ ruleId: 'ios.testing.missing_make_sut',
43
+ severity: 'high',
44
+ message: 'Test file missing makeSUT() factory - extract SUT creation for reuse and to enable consistent leak tracking.'
45
+ };
46
+ }
47
+
48
+ function detectMissingLeakTracking({ filePath, content }) {
49
+ if (!filePath || !content) return null;
50
+ if (!filePath.includes('Test')) return null;
51
+ if (!content.includes('XCTest')) return null;
52
+ const hasTests = /func\s+test\w*\s*\(/.test(content);
53
+ if (!hasTests) return null;
54
+ const hasLeakTracking = content.includes('trackForMemoryLeaks') || content.includes('addTeardownBlock');
55
+ if (hasLeakTracking) return null;
56
+ return {
57
+ ruleId: 'ios.testing.missing_leak_tracking',
58
+ severity: 'high',
59
+ message: 'Test file missing memory leak tracking (trackForMemoryLeaks/addTeardownBlock). Template:\n\nfunc makeSUT() -> SUT {\n let sut = SUT()\n trackForMemoryLeaks(sut)\n return sut\n}\n\nfunc test_example() {\n let sut = makeSUT()\n // ...\n}'
60
+ };
61
+ }
62
+
21
63
  /**
22
64
  * Run iOS-specific AST intelligence analysis
23
65
  * Uses both TypeScript AST (for .ts/.tsx) and SourceKitten (for .swift)
@@ -863,28 +905,28 @@ async function runIOSIntelligence(project, findings, platform) {
863
905
  }
864
906
 
865
907
 
866
- if (filePath.includes('Test') && content.includes('XCTest')) {
867
- if (!content.includes('makeSUT') && !content.includes('func make')) {
868
- pushFinding(
869
- "ios.testing.missing_make_sut",
870
- "medium",
871
- sf,
872
- sf,
873
- 'Test file without makeSUT factory - extract SUT creation for reusability',
874
- findings
875
- );
876
- }
908
+ const missingMakeSUT = detectMissingMakeSUT({ filePath, content });
909
+ if (missingMakeSUT) {
910
+ pushFinding(
911
+ missingMakeSUT.ruleId,
912
+ missingMakeSUT.severity,
913
+ sf,
914
+ sf,
915
+ missingMakeSUT.message,
916
+ findings
917
+ );
918
+ }
877
919
 
878
- if (!content.includes('trackForMemoryLeaks') && !content.includes('addTeardownBlock')) {
879
- pushFinding(
880
- "ios.testing.missing_leak_tracking",
881
- "medium",
882
- sf,
883
- sf,
884
- 'Test file without memory leak tracking - add trackForMemoryLeaks helper',
885
- findings
886
- );
887
- }
920
+ const missingLeakTracking = detectMissingLeakTracking({ filePath, content });
921
+ if (missingLeakTracking) {
922
+ pushFinding(
923
+ missingLeakTracking.ruleId,
924
+ missingLeakTracking.severity,
925
+ sf,
926
+ sf,
927
+ missingLeakTracking.message,
928
+ findings
929
+ );
888
930
  }
889
931
 
890
932
  if (!filePath.includes('Test') && (content.includes('Mock') || content.includes('Spy') || content.includes('Stub'))) {
@@ -1704,17 +1746,16 @@ async function runIOSIntelligence(project, findings, platform) {
1704
1746
  }
1705
1747
  }
1706
1748
 
1707
- if (filePath.includes('Tests') && content.includes('import ') && !content.includes('@testable')) {
1708
- if (content.includes('XCTest')) {
1709
- pushFinding(
1710
- "ios.testing.missing_testable",
1711
- "low",
1712
- sf,
1713
- sf,
1714
- 'Test file without @testable import - use @testable for accessing internal types',
1715
- findings
1716
- );
1717
- }
1749
+ const forbiddenTestable = detectForbiddenTestableImport({ filePath, content });
1750
+ if (forbiddenTestable) {
1751
+ pushFinding(
1752
+ forbiddenTestable.ruleId,
1753
+ forbiddenTestable.severity,
1754
+ sf,
1755
+ sf,
1756
+ forbiddenTestable.message,
1757
+ findings
1758
+ );
1718
1759
  }
1719
1760
 
1720
1761
  if (content.includes('protocol') && !content.includes('extension') && (content.match(/func\s+\w+/g) || []).length > 3) {
@@ -2224,4 +2265,4 @@ async function runIOSIntelligence(project, findings, platform) {
2224
2265
  });
2225
2266
  }
2226
2267
 
2227
- module.exports = { runIOSIntelligence };
2268
+ module.exports = { runIOSIntelligence, detectForbiddenTestableImport, detectMissingMakeSUT, detectMissingLeakTracking };
@@ -0,0 +1,63 @@
1
+ const { analyzePropertyAST } = require('../ios-ast-intelligent-strategies');
2
+
3
+ describe('ios.encapsulation.public_mutable', () => {
4
+ function makeAnalyzer() {
5
+ const findings = [];
6
+ return {
7
+ findings,
8
+ getAttributes: () => [],
9
+ pushFinding: (ruleId, severity, filePath, line, message) => {
10
+ findings.push({ ruleId, severity, filePath, line, message });
11
+ }
12
+ };
13
+ }
14
+
15
+ it('does not flag public computed get-only property', () => {
16
+ const analyzer = makeAnalyzer();
17
+ const node = {
18
+ 'key.name': 'state',
19
+ 'key.line': 10,
20
+ 'key.kind': 'source.lang.swift.decl.var.instance',
21
+ 'key.accessibility': 'source.lang.swift.accessibility.public',
22
+ 'key.substructure': [
23
+ { 'key.kind': 'source.lang.swift.decl.function.accessor.get' }
24
+ ]
25
+ };
26
+
27
+ analyzePropertyAST(analyzer, node, '/tmp/Foo.swift');
28
+
29
+ expect(analyzer.findings.find(f => f.ruleId === 'ios.encapsulation.public_mutable')).toBeUndefined();
30
+ });
31
+
32
+ it('flags public stored property (mutable)', () => {
33
+ const analyzer = makeAnalyzer();
34
+ const node = {
35
+ 'key.name': 'state',
36
+ 'key.line': 10,
37
+ 'key.kind': 'source.lang.swift.decl.var.instance',
38
+ 'key.accessibility': 'source.lang.swift.accessibility.public'
39
+ };
40
+
41
+ analyzePropertyAST(analyzer, node, '/tmp/Foo.swift');
42
+
43
+ expect(analyzer.findings.find(f => f.ruleId === 'ios.encapsulation.public_mutable')).toBeDefined();
44
+ });
45
+
46
+ it('flags public computed property with setter', () => {
47
+ const analyzer = makeAnalyzer();
48
+ const node = {
49
+ 'key.name': 'state',
50
+ 'key.line': 10,
51
+ 'key.kind': 'source.lang.swift.decl.var.instance',
52
+ 'key.accessibility': 'source.lang.swift.accessibility.public',
53
+ 'key.substructure': [
54
+ { 'key.kind': 'source.lang.swift.decl.function.accessor.get' },
55
+ { 'key.kind': 'source.lang.swift.decl.function.accessor.set' }
56
+ ]
57
+ };
58
+
59
+ analyzePropertyAST(analyzer, node, '/tmp/Foo.swift');
60
+
61
+ expect(analyzer.findings.find(f => f.ruleId === 'ios.encapsulation.public_mutable')).toBeDefined();
62
+ });
63
+ });
@@ -0,0 +1,34 @@
1
+ const { analyzeImportsAST } = require('../ios-ast-intelligent-strategies');
2
+
3
+ describe('ios.imports.unused', () => {
4
+ function makeAnalyzer({ fileContent, allNodes }) {
5
+ const findings = [];
6
+ return {
7
+ fileContent,
8
+ allNodes,
9
+ imports: [],
10
+ functions: [],
11
+ hasAttribute: () => false,
12
+ pushFinding: (ruleId, severity, filePath, line, message) => {
13
+ findings.push({ ruleId, severity, filePath, line, message });
14
+ },
15
+ findings,
16
+ };
17
+ }
18
+
19
+ it('does not report unused imports when types use the module name as a prefix', () => {
20
+ const filePath = '/tmp/Foo.swift';
21
+ const analyzer = makeAnalyzer({
22
+ fileContent: 'import Authentication\n\nstruct Foo { let s: AuthenticationState }',
23
+ allNodes: [
24
+ { 'key.typename': 'AuthenticationState' },
25
+ ]
26
+ });
27
+
28
+ analyzer.imports = [{ name: 'Authentication', line: 1 }];
29
+
30
+ analyzeImportsAST(analyzer, filePath);
31
+
32
+ expect(analyzer.findings.find(f => f.ruleId === 'ios.imports.unused')).toBeUndefined();
33
+ });
34
+ });
@@ -84,6 +84,9 @@ function extractImports(analyzer) {
84
84
  }
85
85
 
86
86
  function analyzeImportsAST(analyzer, filePath) {
87
+ if (filePath.includes('Tests')) {
88
+ return;
89
+ }
87
90
  const importNames = analyzer.imports.map((i) => i.name);
88
91
 
89
92
  const hasUIKit = importNames.includes('UIKit');
@@ -112,8 +115,10 @@ function analyzeImportsAST(analyzer, filePath) {
112
115
  );
113
116
  }
114
117
 
118
+ const unusedImportAllowlist = new Set(['Foundation', 'SwiftUI', 'UIKit', 'Combine']);
119
+
115
120
  for (const imp of analyzer.imports) {
116
- if (['Foundation', 'SwiftUI', 'UIKit', 'Combine'].includes(imp.name)) continue;
121
+ if (!unusedImportAllowlist.has(imp.name)) continue;
117
122
 
118
123
  const isUsed = analyzer.allNodes.some((n) => {
119
124
  const typename = n['key.typename'] || '';
@@ -441,7 +446,13 @@ function analyzePropertyAST(analyzer, node, filePath) {
441
446
  const isPublic = (node['key.accessibility'] || '').includes('public');
442
447
  const isInstance = kind.includes('var.instance');
443
448
 
444
- if (isPublic && isInstance && !attributes.includes('setter_access')) {
449
+ const sub = Array.isArray(node['key.substructure']) ? node['key.substructure'] : [];
450
+ const hasAccessorGet = sub.some(s => (s['key.kind'] || '').includes('accessor.get'));
451
+ const hasAccessorSet = sub.some(s => (s['key.kind'] || '').includes('accessor.set'));
452
+ const isComputed = hasAccessorGet || hasAccessorSet;
453
+ const isMutable = isComputed ? hasAccessorSet : true;
454
+
455
+ if (isPublic && isInstance && isMutable && !attributes.includes('setter_access')) {
445
456
  analyzer.pushFinding('ios.encapsulation.public_mutable', 'medium', filePath, line, `Public mutable property '${name}' - consider private(set)`);
446
457
  }
447
458
  }
@@ -801,5 +812,7 @@ module.exports = {
801
812
  resetCollections,
802
813
  collectAllNodes,
803
814
  analyzeCollectedNodes,
815
+ analyzeImportsAST,
816
+ analyzePropertyAST,
804
817
  finalizeGodClassDetection,
805
818
  };
@@ -0,0 +1,14 @@
1
+ const { preFlightCheck } = require('../ast-intelligence-automation');
2
+
3
+ describe('pre_flight_check - tests are first-class', () => {
4
+ it('blocks test edits when AST analysis finds CRITICAL violations', () => {
5
+ const result = preFlightCheck({
6
+ action_type: 'edit',
7
+ target_file: '/SomeModule/Tests/FooTests.swift',
8
+ proposed_code: 'import XCTest\nfinal class FooTests: XCTestCase { func testX() { do { } catch { } } }'
9
+ });
10
+
11
+ expect(result.allowed).toBe(false);
12
+ expect(result.blocked).toBe(true);
13
+ });
14
+ });