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.
- package/README.md +47 -10
- package/docs/images/ai-start.png +0 -0
- package/docs/images/pre-flight-check.png +0 -0
- package/hooks/pre-tool-use-guard.ts +105 -1
- package/package.json +2 -2
- package/scripts/hooks-system/.audit-reports/auto-recovery.log +3 -0
- package/scripts/hooks-system/.audit-reports/install-wizard.log +12 -0
- package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +72 -0
- package/scripts/hooks-system/bin/__tests__/cli-audit-no-stack.spec.js +22 -0
- package/scripts/hooks-system/bin/cli.js +176 -7
- package/scripts/hooks-system/bin/update-evidence.sh +8 -0
- package/scripts/hooks-system/infrastructure/ast/android/analyzers/AndroidSOLIDAnalyzer.js +33 -5
- package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +7 -1
- package/scripts/hooks-system/infrastructure/ast/common/__tests__/ast-common.spec.js +19 -0
- package/scripts/hooks-system/infrastructure/ast/common/ast-common.js +24 -19
- package/scripts/hooks-system/infrastructure/ast/ios/__tests__/forbidden-testable-import.spec.js +21 -0
- package/scripts/hooks-system/infrastructure/ast/ios/__tests__/missing-makesut-leaks.spec.js +64 -0
- package/scripts/hooks-system/infrastructure/ast/ios/ast-ios.js +74 -33
- package/scripts/hooks-system/infrastructure/ast/ios/detectors/__tests__/ios-encapsulation-public-mutable.spec.js +63 -0
- package/scripts/hooks-system/infrastructure/ast/ios/detectors/__tests__/ios-unused-imports.spec.js +34 -0
- package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-ast-intelligent-strategies.js +15 -2
- package/scripts/hooks-system/infrastructure/mcp/__tests__/preflight-check-blocks-tests.spec.js +14 -0
- package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js +154 -50
- package/scripts/hooks-system/infrastructure/orchestration/__tests__/intelligent-audit.spec.js +39 -1
- package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +67 -0
- 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
|
-
|
|
214
|
-
|
|
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 (
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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)) {
|
package/scripts/hooks-system/infrastructure/ast/ios/__tests__/forbidden-testable-import.spec.js
ADDED
|
@@ -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
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
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
|
+
});
|
package/scripts/hooks-system/infrastructure/ast/ios/detectors/__tests__/ios-unused-imports.spec.js
ADDED
|
@@ -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
|
+
});
|
package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-ast-intelligent-strategies.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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
|
};
|
package/scripts/hooks-system/infrastructure/mcp/__tests__/preflight-check-blocks-tests.spec.js
ADDED
|
@@ -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
|
+
});
|