ios-app-review-plugin 1.0.0
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/.claude/settings.local.json +42 -0
- package/.github/actions/ios-review/action.yml +106 -0
- package/.github/workflows/ci.yml +103 -0
- package/.github/workflows/publish.yml +57 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +175 -0
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/bitrise/step.sh +128 -0
- package/bitrise/step.yml +101 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzers/asc-iap.d.ts.map +1 -0
- package/dist/analyzers/asc-metadata.d.ts.map +1 -0
- package/dist/analyzers/asc-screenshots.d.ts.map +1 -0
- package/dist/analyzers/asc-version.d.ts.map +1 -0
- package/dist/analyzers/code-scanner.d.ts.map +1 -0
- package/dist/analyzers/deprecated-api.d.ts.map +1 -0
- package/dist/analyzers/entitlements.d.ts.map +1 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/info-plist.d.ts.map +1 -0
- package/dist/analyzers/privacy.d.ts.map +1 -0
- package/dist/analyzers/private-api.d.ts.map +1 -0
- package/dist/analyzers/security.d.ts.map +1 -0
- package/dist/analyzers/ui-ux.d.ts.map +1 -0
- package/dist/asc/auth.d.ts.map +1 -0
- package/dist/asc/client.d.ts.map +1 -0
- package/dist/asc/endpoints/apps.d.ts.map +1 -0
- package/dist/asc/endpoints/iap.d.ts.map +1 -0
- package/dist/asc/endpoints/screenshots.d.ts.map +1 -0
- package/dist/asc/endpoints/versions.d.ts.map +1 -0
- package/dist/asc/errors.d.ts.map +1 -0
- package/dist/asc/index.d.ts.map +1 -0
- package/dist/asc/types.d.ts.map +1 -0
- package/dist/badge/generator.d.ts.map +1 -0
- package/dist/badge/index.d.ts.map +1 -0
- package/dist/badge/types.d.ts.map +1 -0
- package/dist/cache/file-cache.d.ts.map +1 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cli/commands/help.d.ts.map +1 -0
- package/dist/cli/commands/scan.d.ts.map +1 -0
- package/dist/cli/commands/version.d.ts.map +1 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/git/diff.d.ts.map +1 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/types.d.ts.map +1 -0
- package/dist/guidelines/database.d.ts.map +1 -0
- package/dist/guidelines/index.d.ts.map +1 -0
- package/dist/guidelines/matcher.d.ts.map +1 -0
- package/dist/guidelines/types.d.ts.map +1 -0
- package/dist/history/comparator.d.ts.map +1 -0
- package/dist/history/index.d.ts.map +1 -0
- package/dist/history/store.d.ts.map +1 -0
- package/dist/history/types.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +994 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/plist.d.ts.map +1 -0
- package/dist/parsers/xcodeproj.d.ts.map +1 -0
- package/dist/progress/index.d.ts.map +1 -0
- package/dist/progress/reporter.d.ts.map +1 -0
- package/dist/progress/types.d.ts.map +1 -0
- package/dist/reports/html.d.ts.map +1 -0
- package/dist/reports/index.d.ts.map +1 -0
- package/dist/reports/json.d.ts.map +1 -0
- package/dist/reports/markdown.d.ts.map +1 -0
- package/dist/reports/types.d.ts.map +1 -0
- package/dist/rules/engine.d.ts.map +1 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/loader.d.ts.map +1 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/types/index.d.ts.map +1 -0
- package/docs/ANALYZERS.md +237 -0
- package/docs/API.md +308 -0
- package/docs/BADGES.md +130 -0
- package/docs/CI_CD.md +283 -0
- package/docs/CLI.md +140 -0
- package/docs/REPORTS.md +212 -0
- package/docs/ROADMAP.md +267 -0
- package/docs/RULES.md +182 -0
- package/docs/SECURITY.md +89 -0
- package/docs/TROUBLESHOOTING.md +227 -0
- package/docs/tutorials/ASC_SETUP.md +188 -0
- package/docs/tutorials/CI_INTEGRATION.md +292 -0
- package/docs/tutorials/CUSTOM_RULES.md +291 -0
- package/docs/tutorials/GETTING_STARTED.md +226 -0
- package/docs/video-scripts/01-introduction.md +106 -0
- package/docs/video-scripts/02-cli-usage.md +120 -0
- package/docs/video-scripts/03-ci-integration.md +198 -0
- package/eslint.config.js +33 -0
- package/examples/.ios-review-rules.json +82 -0
- package/examples/bitrise-workflow.yml +129 -0
- package/examples/fastlane-lane.rb +71 -0
- package/examples/github-action.yml +147 -0
- package/fastlane/Fastfile.example +114 -0
- package/fastlane/README.md +99 -0
- package/jest.config.js +36 -0
- package/package.json +65 -0
- package/scripts/benchmark.ts +112 -0
- package/scripts/debug-parser.ts +37 -0
- package/scripts/debug-pbxproj.ts +36 -0
- package/scripts/debug-specific.ts +47 -0
- package/scripts/test-analyze.ts +67 -0
- package/scripts/xcode-cloud-review.sh +167 -0
- package/src/analyzer.ts +227 -0
- package/src/analyzers/asc-iap.ts +300 -0
- package/src/analyzers/asc-metadata.ts +326 -0
- package/src/analyzers/asc-screenshots.ts +310 -0
- package/src/analyzers/asc-version.ts +368 -0
- package/src/analyzers/code-scanner.ts +408 -0
- package/src/analyzers/deprecated-api.ts +390 -0
- package/src/analyzers/entitlements.ts +345 -0
- package/src/analyzers/index.ts +12 -0
- package/src/analyzers/info-plist.ts +409 -0
- package/src/analyzers/privacy.ts +376 -0
- package/src/analyzers/private-api.ts +377 -0
- package/src/analyzers/security.ts +327 -0
- package/src/analyzers/ui-ux.ts +509 -0
- package/src/asc/auth.ts +204 -0
- package/src/asc/client.ts +258 -0
- package/src/asc/endpoints/apps.ts +115 -0
- package/src/asc/endpoints/iap.ts +171 -0
- package/src/asc/endpoints/screenshots.ts +164 -0
- package/src/asc/endpoints/versions.ts +174 -0
- package/src/asc/errors.ts +109 -0
- package/src/asc/index.ts +108 -0
- package/src/asc/types.ts +369 -0
- package/src/badge/generator.ts +48 -0
- package/src/badge/index.ts +2 -0
- package/src/badge/types.ts +5 -0
- package/src/cache/file-cache.ts +75 -0
- package/src/cache/index.ts +2 -0
- package/src/cache/types.ts +10 -0
- package/src/cli/commands/help.ts +41 -0
- package/src/cli/commands/scan.ts +44 -0
- package/src/cli/commands/version.ts +12 -0
- package/src/cli/index.ts +92 -0
- package/src/cli/types.ts +17 -0
- package/src/git/diff.ts +21 -0
- package/src/git/index.ts +2 -0
- package/src/git/types.ts +5 -0
- package/src/guidelines/database.ts +344 -0
- package/src/guidelines/index.ts +4 -0
- package/src/guidelines/matcher.ts +84 -0
- package/src/guidelines/types.ts +28 -0
- package/src/history/comparator.ts +114 -0
- package/src/history/index.ts +3 -0
- package/src/history/store.ts +135 -0
- package/src/history/types.ts +40 -0
- package/src/index.ts +1113 -0
- package/src/parsers/index.ts +3 -0
- package/src/parsers/plist.ts +253 -0
- package/src/parsers/xcodeproj.ts +265 -0
- package/src/progress/index.ts +2 -0
- package/src/progress/reporter.ts +65 -0
- package/src/progress/types.ts +9 -0
- package/src/reports/html.ts +322 -0
- package/src/reports/index.ts +20 -0
- package/src/reports/json.ts +92 -0
- package/src/reports/markdown.ts +187 -0
- package/src/reports/types.ts +26 -0
- package/src/rules/engine.ts +121 -0
- package/src/rules/index.ts +3 -0
- package/src/rules/loader.ts +83 -0
- package/src/rules/types.ts +25 -0
- package/src/types/index.ts +247 -0
- package/tests/analyzer.test.ts +142 -0
- package/tests/analyzers/asc-iap.test.ts +228 -0
- package/tests/analyzers/asc-metadata.test.ts +210 -0
- package/tests/analyzers/asc-screenshots.test.ts +135 -0
- package/tests/analyzers/asc-version.test.ts +259 -0
- package/tests/analyzers/code-scanner.test.ts +745 -0
- package/tests/analyzers/deprecated-api.test.ts +286 -0
- package/tests/analyzers/entitlements.test.ts +411 -0
- package/tests/analyzers/info-plist.test.ts +148 -0
- package/tests/analyzers/privacy.test.ts +623 -0
- package/tests/analyzers/private-api.test.ts +255 -0
- package/tests/analyzers/security.test.ts +300 -0
- package/tests/analyzers/ui-ux.test.ts +357 -0
- package/tests/asc/auth.test.ts +189 -0
- package/tests/asc/client.test.ts +207 -0
- package/tests/asc/endpoints.test.ts +1359 -0
- package/tests/badge/generator.test.ts +73 -0
- package/tests/cache/file-cache.test.ts +124 -0
- package/tests/cli/cli-index.test.ts +510 -0
- package/tests/cli/commands.test.ts +67 -0
- package/tests/cli/scan.test.ts +152 -0
- package/tests/git/diff.test.ts +69 -0
- package/tests/guidelines/matcher.test.ts +209 -0
- package/tests/history/comparator.test.ts +272 -0
- package/tests/history/store.test.ts +200 -0
- package/tests/integration/cli.test.ts +95 -0
- package/tests/integration/e2e.test.ts +130 -0
- package/tests/parsers/plist.test.ts +240 -0
- package/tests/parsers/xcodeproj.test.ts +289 -0
- package/tests/progress/reporter.test.ts +117 -0
- package/tests/reports/html.test.ts +176 -0
- package/tests/reports/json.test.ts +235 -0
- package/tests/reports/markdown.test.ts +196 -0
- package/tests/rules/engine.test.ts +229 -0
- package/tests/rules/loader.test.ts +187 -0
- package/tests/setup.ts +15 -0
- package/tsconfig.json +27 -0
- package/tsconfig.test.json +9 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { CustomRuleEngine } from '../../src/rules/engine.js';
|
|
5
|
+
import type { CompiledRule, CustomRuleConfig } from '../../src/rules/types.js';
|
|
6
|
+
|
|
7
|
+
describe('CustomRuleEngine', () => {
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rule-engine-test-'));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterAll(async () => {
|
|
15
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
jest.clearAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const testForceCastRule: CompiledRule = {
|
|
23
|
+
id: 'test-force-cast',
|
|
24
|
+
title: 'Force cast detected',
|
|
25
|
+
description: 'Force casts can cause crashes',
|
|
26
|
+
severity: 'warning',
|
|
27
|
+
pattern: 'as!\\s+\\w+',
|
|
28
|
+
regex: /as!\s+\w+/g,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const testForceUnwrapRule: CompiledRule = {
|
|
32
|
+
id: 'test-force-unwrap',
|
|
33
|
+
title: 'Force unwrap detected',
|
|
34
|
+
description: 'Force unwraps can cause crashes',
|
|
35
|
+
severity: 'warning',
|
|
36
|
+
pattern: '\\w+!\\.',
|
|
37
|
+
regex: /\w+!\./g,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const testPrintRule: CompiledRule = {
|
|
41
|
+
id: 'test-no-print',
|
|
42
|
+
title: 'Print statement detected',
|
|
43
|
+
description: 'Remove print statements before release',
|
|
44
|
+
severity: 'info',
|
|
45
|
+
pattern: '\\bprint\\(',
|
|
46
|
+
regex: /\bprint\(/g,
|
|
47
|
+
fileTypes: ['.swift'],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
it('should find matches for custom regex patterns', async () => {
|
|
51
|
+
const filePath = path.join(tempDir, 'ForceCast.swift');
|
|
52
|
+
await fs.writeFile(
|
|
53
|
+
filePath,
|
|
54
|
+
`import UIKit
|
|
55
|
+
|
|
56
|
+
class ViewController: UIViewController {
|
|
57
|
+
func setup() {
|
|
58
|
+
let view = self.view as! UITableView
|
|
59
|
+
let label = someView as! UILabel
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
`
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const engine = new CustomRuleEngine();
|
|
66
|
+
const result = await engine.scan(filePath, [testForceCastRule]);
|
|
67
|
+
|
|
68
|
+
expect(result.issues.length).toBeGreaterThanOrEqual(2);
|
|
69
|
+
expect(result.issues.every((i) => i.id === 'test-force-cast')).toBe(true);
|
|
70
|
+
expect(result.issues[0]!.filePath).toBe(filePath);
|
|
71
|
+
expect(result.issues[0]!.lineNumber).toBeDefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should respect fileTypes filter', async () => {
|
|
75
|
+
const swiftFile = path.join(tempDir, 'PrintSwift.swift');
|
|
76
|
+
await fs.writeFile(swiftFile, 'print("hello from swift")\n');
|
|
77
|
+
|
|
78
|
+
const objcFile = path.join(tempDir, 'PrintObjC.m');
|
|
79
|
+
await fs.writeFile(objcFile, 'print("hello from objc")\n');
|
|
80
|
+
|
|
81
|
+
const dir = path.join(tempDir, 'filetype-test');
|
|
82
|
+
await fs.mkdir(dir, { recursive: true });
|
|
83
|
+
await fs.writeFile(path.join(dir, 'App.swift'), 'print("swift print")\n');
|
|
84
|
+
await fs.writeFile(path.join(dir, 'Helper.m'), 'print("objc print")\n');
|
|
85
|
+
|
|
86
|
+
const engine = new CustomRuleEngine();
|
|
87
|
+
|
|
88
|
+
// When scanning the directory, the print rule (fileTypes: ['.swift']) should only match .swift files
|
|
89
|
+
const result = await engine.scan(dir, [testPrintRule]);
|
|
90
|
+
const matchedFiles = result.issues.map((i) => i.filePath);
|
|
91
|
+
const hasObjcFile = matchedFiles.some((f) => f?.endsWith('.m'));
|
|
92
|
+
expect(hasObjcFile).toBe(false);
|
|
93
|
+
expect(result.issues.length).toBeGreaterThanOrEqual(1);
|
|
94
|
+
expect(result.issues.every((i) => i.filePath?.endsWith('.swift'))).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should ignore disabled rules from config', async () => {
|
|
98
|
+
const filePath = path.join(tempDir, 'DisabledRule.swift');
|
|
99
|
+
await fs.writeFile(
|
|
100
|
+
filePath,
|
|
101
|
+
`let view = self.view as! UITableView
|
|
102
|
+
let x = optional!.value
|
|
103
|
+
`
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const config: CustomRuleConfig = {
|
|
107
|
+
version: 1,
|
|
108
|
+
rules: [],
|
|
109
|
+
disabledRules: ['test-force-cast'],
|
|
110
|
+
};
|
|
111
|
+
const engine = new CustomRuleEngine(config);
|
|
112
|
+
const result = await engine.scan(filePath, [testForceCastRule, testForceUnwrapRule]);
|
|
113
|
+
|
|
114
|
+
// test-force-cast should be disabled, only test-force-unwrap should match
|
|
115
|
+
const forceCastIssues = result.issues.filter((i) => i.id === 'test-force-cast');
|
|
116
|
+
const forceUnwrapIssues = result.issues.filter((i) => i.id === 'test-force-unwrap');
|
|
117
|
+
expect(forceCastIssues).toHaveLength(0);
|
|
118
|
+
expect(forceUnwrapIssues.length).toBeGreaterThanOrEqual(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should apply severity overrides', async () => {
|
|
122
|
+
const filePath = path.join(tempDir, 'SeverityOverride.swift');
|
|
123
|
+
await fs.writeFile(filePath, 'let view = self.view as! UITableView\n');
|
|
124
|
+
|
|
125
|
+
const config: CustomRuleConfig = {
|
|
126
|
+
version: 1,
|
|
127
|
+
rules: [],
|
|
128
|
+
severityOverrides: {
|
|
129
|
+
'test-force-cast': 'error',
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
const engine = new CustomRuleEngine(config);
|
|
133
|
+
const result = await engine.scan(filePath, [testForceCastRule]);
|
|
134
|
+
|
|
135
|
+
expect(result.issues.length).toBeGreaterThanOrEqual(1);
|
|
136
|
+
// The rule has severity 'warning' but the override should make it 'error'
|
|
137
|
+
expect(result.issues[0]!.severity).toBe('error');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should honor // ios-review-disable-next-line rule-id comments', async () => {
|
|
141
|
+
const filePath = path.join(tempDir, 'DisableLine.swift');
|
|
142
|
+
await fs.writeFile(
|
|
143
|
+
filePath,
|
|
144
|
+
`import UIKit
|
|
145
|
+
|
|
146
|
+
class VC: UIViewController {
|
|
147
|
+
func setup() {
|
|
148
|
+
// ios-review-disable-next-line test-force-cast
|
|
149
|
+
let view = self.view as! UITableView
|
|
150
|
+
let label = someView as! UILabel
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
`
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const engine = new CustomRuleEngine();
|
|
157
|
+
const result = await engine.scan(filePath, [testForceCastRule]);
|
|
158
|
+
|
|
159
|
+
// The first force cast (line 6) should be suppressed by the disable comment on line 5
|
|
160
|
+
// The second force cast (line 7) should still be reported
|
|
161
|
+
const issues = result.issues.filter((i) => i.id === 'test-force-cast');
|
|
162
|
+
expect(issues).toHaveLength(1);
|
|
163
|
+
expect(issues[0]!.lineNumber).toBe(7);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should handle scanning a single file', async () => {
|
|
167
|
+
const filePath = path.join(tempDir, 'SingleFile.swift');
|
|
168
|
+
await fs.writeFile(filePath, 'let x = obj as! String\n');
|
|
169
|
+
|
|
170
|
+
const engine = new CustomRuleEngine();
|
|
171
|
+
const result = await engine.scan(filePath, [testForceCastRule]);
|
|
172
|
+
|
|
173
|
+
expect(result.analyzer).toBe('Custom Rules');
|
|
174
|
+
expect(result.issues.length).toBeGreaterThanOrEqual(1);
|
|
175
|
+
expect(result.issues[0]!.filePath).toBe(filePath);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should handle scanning a directory', async () => {
|
|
179
|
+
const dir = path.join(tempDir, 'scan-dir');
|
|
180
|
+
await fs.mkdir(dir, { recursive: true });
|
|
181
|
+
await fs.writeFile(path.join(dir, 'A.swift'), 'let a = x as! Int\n');
|
|
182
|
+
await fs.writeFile(path.join(dir, 'B.swift'), 'let b = y as! String\n');
|
|
183
|
+
|
|
184
|
+
const engine = new CustomRuleEngine();
|
|
185
|
+
const result = await engine.scan(dir, [testForceCastRule]);
|
|
186
|
+
|
|
187
|
+
expect(result.issues.length).toBeGreaterThanOrEqual(2);
|
|
188
|
+
const filePaths = result.issues.map((i) => i.filePath);
|
|
189
|
+
expect(filePaths.some((f) => f?.endsWith('A.swift'))).toBe(true);
|
|
190
|
+
expect(filePaths.some((f) => f?.endsWith('B.swift'))).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should return empty issues when no rules match', async () => {
|
|
194
|
+
const filePath = path.join(tempDir, 'CleanCode.swift');
|
|
195
|
+
await fs.writeFile(
|
|
196
|
+
filePath,
|
|
197
|
+
`import UIKit
|
|
198
|
+
|
|
199
|
+
class ViewController: UIViewController {
|
|
200
|
+
func setup() {
|
|
201
|
+
let view: UITableView? = self.view as? UITableView
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
`
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const engine = new CustomRuleEngine();
|
|
208
|
+
const result = await engine.scan(filePath, [testForceCastRule]);
|
|
209
|
+
|
|
210
|
+
expect(result.issues).toHaveLength(0);
|
|
211
|
+
expect(result.passed).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should set passed to false when an error-severity issue is found', async () => {
|
|
215
|
+
const filePath = path.join(tempDir, 'ErrorSeverity.swift');
|
|
216
|
+
await fs.writeFile(filePath, 'let view = self.view as! UITableView\n');
|
|
217
|
+
|
|
218
|
+
const errorRule: CompiledRule = {
|
|
219
|
+
...testForceCastRule,
|
|
220
|
+
severity: 'error',
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const engine = new CustomRuleEngine();
|
|
224
|
+
const result = await engine.scan(filePath, [errorRule]);
|
|
225
|
+
|
|
226
|
+
expect(result.issues.length).toBeGreaterThanOrEqual(1);
|
|
227
|
+
expect(result.passed).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { RuleLoader } from '../../src/rules/loader.js';
|
|
5
|
+
import type { CustomRuleConfig } from '../../src/rules/types.js';
|
|
6
|
+
|
|
7
|
+
describe('RuleLoader', () => {
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
let loader: RuleLoader;
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rule-loader-test-'));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterAll(async () => {
|
|
16
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
loader = new RuleLoader();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const validConfig: CustomRuleConfig = {
|
|
25
|
+
version: 1,
|
|
26
|
+
rules: [
|
|
27
|
+
{
|
|
28
|
+
id: 'test-force-cast',
|
|
29
|
+
title: 'Force cast detected',
|
|
30
|
+
description: 'Force casts can cause runtime crashes',
|
|
31
|
+
severity: 'warning',
|
|
32
|
+
pattern: 'as!\\s+\\w+',
|
|
33
|
+
fileTypes: ['.swift'],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'test-force-unwrap',
|
|
37
|
+
title: 'Force unwrap detected',
|
|
38
|
+
description: 'Force unwraps can cause runtime crashes',
|
|
39
|
+
severity: 'warning',
|
|
40
|
+
pattern: '\\w+!\\.',
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
disabledRules: ['test-force-unwrap'],
|
|
44
|
+
severityOverrides: {
|
|
45
|
+
'test-force-cast': 'error',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
describe('findConfig()', () => {
|
|
50
|
+
it('should find config in the current directory', async () => {
|
|
51
|
+
const dir = path.join(tempDir, 'find-current');
|
|
52
|
+
await fs.mkdir(dir, { recursive: true });
|
|
53
|
+
const configPath = path.join(dir, '.ios-review-rules.json');
|
|
54
|
+
await fs.writeFile(configPath, JSON.stringify(validConfig));
|
|
55
|
+
|
|
56
|
+
const result = await loader.findConfig(dir);
|
|
57
|
+
expect(result).toBe(configPath);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should find config in a parent directory', async () => {
|
|
61
|
+
const parentDir = path.join(tempDir, 'find-parent');
|
|
62
|
+
const childDir = path.join(parentDir, 'sub', 'deep');
|
|
63
|
+
await fs.mkdir(childDir, { recursive: true });
|
|
64
|
+
const configPath = path.join(parentDir, '.ios-review-rules.json');
|
|
65
|
+
await fs.writeFile(configPath, JSON.stringify(validConfig));
|
|
66
|
+
|
|
67
|
+
const result = await loader.findConfig(childDir);
|
|
68
|
+
expect(result).toBe(configPath);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should return null when no config exists', async () => {
|
|
72
|
+
const dir = path.join(tempDir, 'find-none');
|
|
73
|
+
await fs.mkdir(dir, { recursive: true });
|
|
74
|
+
|
|
75
|
+
const result = await loader.findConfig(dir);
|
|
76
|
+
expect(result).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('loadConfig()', () => {
|
|
81
|
+
it('should parse a valid config file', async () => {
|
|
82
|
+
const configPath = path.join(tempDir, 'valid-config.json');
|
|
83
|
+
await fs.writeFile(configPath, JSON.stringify(validConfig));
|
|
84
|
+
|
|
85
|
+
const config = await loader.loadConfig(configPath);
|
|
86
|
+
expect(config.version).toBe(1);
|
|
87
|
+
expect(config.rules).toHaveLength(2);
|
|
88
|
+
expect(config.rules[0]!.id).toBe('test-force-cast');
|
|
89
|
+
expect(config.rules[1]!.id).toBe('test-force-unwrap');
|
|
90
|
+
expect(config.disabledRules).toEqual(['test-force-unwrap']);
|
|
91
|
+
expect(config.severityOverrides).toEqual({ 'test-force-cast': 'error' });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should throw on invalid JSON', async () => {
|
|
95
|
+
const configPath = path.join(tempDir, 'invalid-json.json');
|
|
96
|
+
await fs.writeFile(configPath, '{ this is not valid json }');
|
|
97
|
+
|
|
98
|
+
await expect(loader.loadConfig(configPath)).rejects.toThrow();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should throw on invalid schema (missing required fields)', async () => {
|
|
102
|
+
const configPath = path.join(tempDir, 'invalid-schema.json');
|
|
103
|
+
const invalidConfig = {
|
|
104
|
+
version: 1,
|
|
105
|
+
rules: [
|
|
106
|
+
{
|
|
107
|
+
// Missing required fields: id, title, description, severity, pattern
|
|
108
|
+
id: 'incomplete',
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
await fs.writeFile(configPath, JSON.stringify(invalidConfig));
|
|
113
|
+
|
|
114
|
+
await expect(loader.loadConfig(configPath)).rejects.toThrow();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('compileRules()', () => {
|
|
119
|
+
it('should compile regex patterns with default g flag', () => {
|
|
120
|
+
const config: CustomRuleConfig = {
|
|
121
|
+
version: 1,
|
|
122
|
+
rules: [
|
|
123
|
+
{
|
|
124
|
+
id: 'test-rule',
|
|
125
|
+
title: 'Test Rule',
|
|
126
|
+
description: 'A test rule',
|
|
127
|
+
severity: 'warning',
|
|
128
|
+
pattern: 'as!\\s+\\w+',
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const compiled = loader.compileRules(config);
|
|
134
|
+
expect(compiled).toHaveLength(1);
|
|
135
|
+
expect(compiled[0]!.regex).toBeInstanceOf(RegExp);
|
|
136
|
+
expect(compiled[0]!.regex.flags).toBe('g');
|
|
137
|
+
expect(compiled[0]!.regex.source).toBe('as!\\s+\\w+');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should respect custom flags', () => {
|
|
141
|
+
const config: CustomRuleConfig = {
|
|
142
|
+
version: 1,
|
|
143
|
+
rules: [
|
|
144
|
+
{
|
|
145
|
+
id: 'test-case-insensitive',
|
|
146
|
+
title: 'Case Insensitive Test',
|
|
147
|
+
description: 'A case insensitive test',
|
|
148
|
+
severity: 'info',
|
|
149
|
+
pattern: 'todo',
|
|
150
|
+
flags: 'gi',
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const compiled = loader.compileRules(config);
|
|
156
|
+
expect(compiled).toHaveLength(1);
|
|
157
|
+
expect(compiled[0]!.regex.flags).toContain('g');
|
|
158
|
+
expect(compiled[0]!.regex.flags).toContain('i');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('loadFromProject()', () => {
|
|
163
|
+
it('should return null when no config found', async () => {
|
|
164
|
+
const dir = path.join(tempDir, 'no-project-config');
|
|
165
|
+
await fs.mkdir(dir, { recursive: true });
|
|
166
|
+
|
|
167
|
+
const result = await loader.loadFromProject(dir);
|
|
168
|
+
expect(result).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should find, load, and compile rules from project directory', async () => {
|
|
172
|
+
const dir = path.join(tempDir, 'full-load-project');
|
|
173
|
+
await fs.mkdir(dir, { recursive: true });
|
|
174
|
+
await fs.writeFile(
|
|
175
|
+
path.join(dir, '.ios-review-rules.json'),
|
|
176
|
+
JSON.stringify(validConfig)
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const result = await loader.loadFromProject(dir);
|
|
180
|
+
expect(result).not.toBeNull();
|
|
181
|
+
expect(result!.config.version).toBe(1);
|
|
182
|
+
expect(result!.config.rules).toHaveLength(2);
|
|
183
|
+
expect(result!.rules).toHaveLength(2);
|
|
184
|
+
expect(result!.rules[0]!.regex).toBeInstanceOf(RegExp);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
package/tests/setup.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Jest setup file
|
|
2
|
+
// Add any global test configuration here
|
|
3
|
+
|
|
4
|
+
// Increase timeout for integration tests
|
|
5
|
+
jest.setTimeout(30000);
|
|
6
|
+
|
|
7
|
+
// Suppress console output during tests unless explicitly needed
|
|
8
|
+
if (process.env['SUPPRESS_CONSOLE'] !== 'false') {
|
|
9
|
+
global.console = {
|
|
10
|
+
...console,
|
|
11
|
+
log: jest.fn(),
|
|
12
|
+
debug: jest.fn(),
|
|
13
|
+
info: jest.fn(),
|
|
14
|
+
};
|
|
15
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"noUncheckedIndexedAccess": true,
|
|
18
|
+
"noImplicitReturns": true,
|
|
19
|
+
"noFallthroughCasesInSwitch": true,
|
|
20
|
+
"noUnusedLocals": true,
|
|
21
|
+
"noUnusedParameters": true,
|
|
22
|
+
"exactOptionalPropertyTypes": true,
|
|
23
|
+
"noPropertyAccessFromIndexSignature": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["src/**/*"],
|
|
26
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
27
|
+
}
|