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.
Files changed (205) hide show
  1. package/.claude/settings.local.json +42 -0
  2. package/.github/actions/ios-review/action.yml +106 -0
  3. package/.github/workflows/ci.yml +103 -0
  4. package/.github/workflows/publish.yml +57 -0
  5. package/CHANGELOG.md +66 -0
  6. package/CONTRIBUTING.md +175 -0
  7. package/LICENSE +21 -0
  8. package/README.md +205 -0
  9. package/bitrise/step.sh +128 -0
  10. package/bitrise/step.yml +101 -0
  11. package/dist/analyzer.d.ts.map +1 -0
  12. package/dist/analyzers/asc-iap.d.ts.map +1 -0
  13. package/dist/analyzers/asc-metadata.d.ts.map +1 -0
  14. package/dist/analyzers/asc-screenshots.d.ts.map +1 -0
  15. package/dist/analyzers/asc-version.d.ts.map +1 -0
  16. package/dist/analyzers/code-scanner.d.ts.map +1 -0
  17. package/dist/analyzers/deprecated-api.d.ts.map +1 -0
  18. package/dist/analyzers/entitlements.d.ts.map +1 -0
  19. package/dist/analyzers/index.d.ts.map +1 -0
  20. package/dist/analyzers/info-plist.d.ts.map +1 -0
  21. package/dist/analyzers/privacy.d.ts.map +1 -0
  22. package/dist/analyzers/private-api.d.ts.map +1 -0
  23. package/dist/analyzers/security.d.ts.map +1 -0
  24. package/dist/analyzers/ui-ux.d.ts.map +1 -0
  25. package/dist/asc/auth.d.ts.map +1 -0
  26. package/dist/asc/client.d.ts.map +1 -0
  27. package/dist/asc/endpoints/apps.d.ts.map +1 -0
  28. package/dist/asc/endpoints/iap.d.ts.map +1 -0
  29. package/dist/asc/endpoints/screenshots.d.ts.map +1 -0
  30. package/dist/asc/endpoints/versions.d.ts.map +1 -0
  31. package/dist/asc/errors.d.ts.map +1 -0
  32. package/dist/asc/index.d.ts.map +1 -0
  33. package/dist/asc/types.d.ts.map +1 -0
  34. package/dist/badge/generator.d.ts.map +1 -0
  35. package/dist/badge/index.d.ts.map +1 -0
  36. package/dist/badge/types.d.ts.map +1 -0
  37. package/dist/cache/file-cache.d.ts.map +1 -0
  38. package/dist/cache/index.d.ts.map +1 -0
  39. package/dist/cache/types.d.ts.map +1 -0
  40. package/dist/cli/commands/help.d.ts.map +1 -0
  41. package/dist/cli/commands/scan.d.ts.map +1 -0
  42. package/dist/cli/commands/version.d.ts.map +1 -0
  43. package/dist/cli/index.d.ts.map +1 -0
  44. package/dist/cli/types.d.ts.map +1 -0
  45. package/dist/git/diff.d.ts.map +1 -0
  46. package/dist/git/index.d.ts.map +1 -0
  47. package/dist/git/types.d.ts.map +1 -0
  48. package/dist/guidelines/database.d.ts.map +1 -0
  49. package/dist/guidelines/index.d.ts.map +1 -0
  50. package/dist/guidelines/matcher.d.ts.map +1 -0
  51. package/dist/guidelines/types.d.ts.map +1 -0
  52. package/dist/history/comparator.d.ts.map +1 -0
  53. package/dist/history/index.d.ts.map +1 -0
  54. package/dist/history/store.d.ts.map +1 -0
  55. package/dist/history/types.d.ts.map +1 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +994 -0
  58. package/dist/parsers/index.d.ts.map +1 -0
  59. package/dist/parsers/plist.d.ts.map +1 -0
  60. package/dist/parsers/xcodeproj.d.ts.map +1 -0
  61. package/dist/progress/index.d.ts.map +1 -0
  62. package/dist/progress/reporter.d.ts.map +1 -0
  63. package/dist/progress/types.d.ts.map +1 -0
  64. package/dist/reports/html.d.ts.map +1 -0
  65. package/dist/reports/index.d.ts.map +1 -0
  66. package/dist/reports/json.d.ts.map +1 -0
  67. package/dist/reports/markdown.d.ts.map +1 -0
  68. package/dist/reports/types.d.ts.map +1 -0
  69. package/dist/rules/engine.d.ts.map +1 -0
  70. package/dist/rules/index.d.ts.map +1 -0
  71. package/dist/rules/loader.d.ts.map +1 -0
  72. package/dist/rules/types.d.ts.map +1 -0
  73. package/dist/types/index.d.ts.map +1 -0
  74. package/docs/ANALYZERS.md +237 -0
  75. package/docs/API.md +308 -0
  76. package/docs/BADGES.md +130 -0
  77. package/docs/CI_CD.md +283 -0
  78. package/docs/CLI.md +140 -0
  79. package/docs/REPORTS.md +212 -0
  80. package/docs/ROADMAP.md +267 -0
  81. package/docs/RULES.md +182 -0
  82. package/docs/SECURITY.md +89 -0
  83. package/docs/TROUBLESHOOTING.md +227 -0
  84. package/docs/tutorials/ASC_SETUP.md +188 -0
  85. package/docs/tutorials/CI_INTEGRATION.md +292 -0
  86. package/docs/tutorials/CUSTOM_RULES.md +291 -0
  87. package/docs/tutorials/GETTING_STARTED.md +226 -0
  88. package/docs/video-scripts/01-introduction.md +106 -0
  89. package/docs/video-scripts/02-cli-usage.md +120 -0
  90. package/docs/video-scripts/03-ci-integration.md +198 -0
  91. package/eslint.config.js +33 -0
  92. package/examples/.ios-review-rules.json +82 -0
  93. package/examples/bitrise-workflow.yml +129 -0
  94. package/examples/fastlane-lane.rb +71 -0
  95. package/examples/github-action.yml +147 -0
  96. package/fastlane/Fastfile.example +114 -0
  97. package/fastlane/README.md +99 -0
  98. package/jest.config.js +36 -0
  99. package/package.json +65 -0
  100. package/scripts/benchmark.ts +112 -0
  101. package/scripts/debug-parser.ts +37 -0
  102. package/scripts/debug-pbxproj.ts +36 -0
  103. package/scripts/debug-specific.ts +47 -0
  104. package/scripts/test-analyze.ts +67 -0
  105. package/scripts/xcode-cloud-review.sh +167 -0
  106. package/src/analyzer.ts +227 -0
  107. package/src/analyzers/asc-iap.ts +300 -0
  108. package/src/analyzers/asc-metadata.ts +326 -0
  109. package/src/analyzers/asc-screenshots.ts +310 -0
  110. package/src/analyzers/asc-version.ts +368 -0
  111. package/src/analyzers/code-scanner.ts +408 -0
  112. package/src/analyzers/deprecated-api.ts +390 -0
  113. package/src/analyzers/entitlements.ts +345 -0
  114. package/src/analyzers/index.ts +12 -0
  115. package/src/analyzers/info-plist.ts +409 -0
  116. package/src/analyzers/privacy.ts +376 -0
  117. package/src/analyzers/private-api.ts +377 -0
  118. package/src/analyzers/security.ts +327 -0
  119. package/src/analyzers/ui-ux.ts +509 -0
  120. package/src/asc/auth.ts +204 -0
  121. package/src/asc/client.ts +258 -0
  122. package/src/asc/endpoints/apps.ts +115 -0
  123. package/src/asc/endpoints/iap.ts +171 -0
  124. package/src/asc/endpoints/screenshots.ts +164 -0
  125. package/src/asc/endpoints/versions.ts +174 -0
  126. package/src/asc/errors.ts +109 -0
  127. package/src/asc/index.ts +108 -0
  128. package/src/asc/types.ts +369 -0
  129. package/src/badge/generator.ts +48 -0
  130. package/src/badge/index.ts +2 -0
  131. package/src/badge/types.ts +5 -0
  132. package/src/cache/file-cache.ts +75 -0
  133. package/src/cache/index.ts +2 -0
  134. package/src/cache/types.ts +10 -0
  135. package/src/cli/commands/help.ts +41 -0
  136. package/src/cli/commands/scan.ts +44 -0
  137. package/src/cli/commands/version.ts +12 -0
  138. package/src/cli/index.ts +92 -0
  139. package/src/cli/types.ts +17 -0
  140. package/src/git/diff.ts +21 -0
  141. package/src/git/index.ts +2 -0
  142. package/src/git/types.ts +5 -0
  143. package/src/guidelines/database.ts +344 -0
  144. package/src/guidelines/index.ts +4 -0
  145. package/src/guidelines/matcher.ts +84 -0
  146. package/src/guidelines/types.ts +28 -0
  147. package/src/history/comparator.ts +114 -0
  148. package/src/history/index.ts +3 -0
  149. package/src/history/store.ts +135 -0
  150. package/src/history/types.ts +40 -0
  151. package/src/index.ts +1113 -0
  152. package/src/parsers/index.ts +3 -0
  153. package/src/parsers/plist.ts +253 -0
  154. package/src/parsers/xcodeproj.ts +265 -0
  155. package/src/progress/index.ts +2 -0
  156. package/src/progress/reporter.ts +65 -0
  157. package/src/progress/types.ts +9 -0
  158. package/src/reports/html.ts +322 -0
  159. package/src/reports/index.ts +20 -0
  160. package/src/reports/json.ts +92 -0
  161. package/src/reports/markdown.ts +187 -0
  162. package/src/reports/types.ts +26 -0
  163. package/src/rules/engine.ts +121 -0
  164. package/src/rules/index.ts +3 -0
  165. package/src/rules/loader.ts +83 -0
  166. package/src/rules/types.ts +25 -0
  167. package/src/types/index.ts +247 -0
  168. package/tests/analyzer.test.ts +142 -0
  169. package/tests/analyzers/asc-iap.test.ts +228 -0
  170. package/tests/analyzers/asc-metadata.test.ts +210 -0
  171. package/tests/analyzers/asc-screenshots.test.ts +135 -0
  172. package/tests/analyzers/asc-version.test.ts +259 -0
  173. package/tests/analyzers/code-scanner.test.ts +745 -0
  174. package/tests/analyzers/deprecated-api.test.ts +286 -0
  175. package/tests/analyzers/entitlements.test.ts +411 -0
  176. package/tests/analyzers/info-plist.test.ts +148 -0
  177. package/tests/analyzers/privacy.test.ts +623 -0
  178. package/tests/analyzers/private-api.test.ts +255 -0
  179. package/tests/analyzers/security.test.ts +300 -0
  180. package/tests/analyzers/ui-ux.test.ts +357 -0
  181. package/tests/asc/auth.test.ts +189 -0
  182. package/tests/asc/client.test.ts +207 -0
  183. package/tests/asc/endpoints.test.ts +1359 -0
  184. package/tests/badge/generator.test.ts +73 -0
  185. package/tests/cache/file-cache.test.ts +124 -0
  186. package/tests/cli/cli-index.test.ts +510 -0
  187. package/tests/cli/commands.test.ts +67 -0
  188. package/tests/cli/scan.test.ts +152 -0
  189. package/tests/git/diff.test.ts +69 -0
  190. package/tests/guidelines/matcher.test.ts +209 -0
  191. package/tests/history/comparator.test.ts +272 -0
  192. package/tests/history/store.test.ts +200 -0
  193. package/tests/integration/cli.test.ts +95 -0
  194. package/tests/integration/e2e.test.ts +130 -0
  195. package/tests/parsers/plist.test.ts +240 -0
  196. package/tests/parsers/xcodeproj.test.ts +289 -0
  197. package/tests/progress/reporter.test.ts +117 -0
  198. package/tests/reports/html.test.ts +176 -0
  199. package/tests/reports/json.test.ts +235 -0
  200. package/tests/reports/markdown.test.ts +196 -0
  201. package/tests/rules/engine.test.ts +229 -0
  202. package/tests/rules/loader.test.ts +187 -0
  203. package/tests/setup.ts +15 -0
  204. package/tsconfig.json +27 -0
  205. 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
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "isolatedModules": true,
5
+ "noUnusedLocals": false,
6
+ "noUnusedParameters": false
7
+ },
8
+ "include": ["src/**/*", "tests/**/*"]
9
+ }