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,377 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import fg from 'fast-glob';
4
+ import type {
5
+ Analyzer,
6
+ AnalysisResult,
7
+ AnalyzerOptions,
8
+ Issue,
9
+ XcodeProject,
10
+ } from '../types/index.js';
11
+
12
+ /**
13
+ * A private API pattern to detect
14
+ */
15
+ interface PrivateAPIPattern {
16
+ id: string;
17
+ title: string;
18
+ description: string;
19
+ pattern: RegExp;
20
+ severity: 'error' | 'warning';
21
+ suggestion: string;
22
+ fileTypes: string[];
23
+ }
24
+
25
+ /**
26
+ * Known private/undocumented frameworks that cause rejection
27
+ */
28
+ const PRIVATE_FRAMEWORKS = [
29
+ 'GraphicsServices',
30
+ 'BackBoardServices',
31
+ 'SpringBoardServices',
32
+ 'ChatKit',
33
+ 'MobileInstallation',
34
+ 'AppSupport',
35
+ 'TelephonyUtilities',
36
+ 'FrontBoard',
37
+ 'XCTest', // Not private but shouldn't ship in production
38
+ 'UIKitCore', // Direct access (vs UIKit) can indicate private usage
39
+ 'TextInput',
40
+ 'Celestial',
41
+ 'IOMobileFramebuffer',
42
+ 'BluetoothManager',
43
+ 'WirelessDiagnostics',
44
+ ];
45
+
46
+ /**
47
+ * Known private URL schemes that cause rejection
48
+ */
49
+ const PRIVATE_URL_SCHEMES = [
50
+ { scheme: 'cydia://', description: 'Cydia URL scheme (jailbreak-related)' },
51
+ { scheme: 'prefs://', description: 'Private preferences URL scheme (use UIApplication.openSettingsURLString)' },
52
+ { scheme: 'tel-prompt://', description: 'Private telephone prompt scheme' },
53
+ { scheme: 'app-prefs://', description: 'Private app preferences URL scheme' },
54
+ { scheme: 'dbapi://', description: 'Private debug API URL scheme' },
55
+ ];
56
+
57
+ /**
58
+ * Patterns for detecting private API usage
59
+ */
60
+ const PRIVATE_API_PATTERNS: PrivateAPIPattern[] = [
61
+ // Known private selectors with underscore prefix
62
+ {
63
+ id: 'private-underscore-selector',
64
+ title: 'Private selector access',
65
+ description: 'Accessing a selector that starts with underscore indicates private API usage.',
66
+ pattern: /NSSelectorFromString\(\s*["'`]_\w+[^"'`]*["'`]\s*\)/g,
67
+ severity: 'error',
68
+ suggestion: 'Remove usage of private selectors. Use only public APIs.',
69
+ fileTypes: ['.swift', '.m', '.mm'],
70
+ },
71
+ // NSClassFromString with private classes
72
+ {
73
+ id: 'private-class-from-string',
74
+ title: 'Private class access via NSClassFromString',
75
+ description: 'Accessing a private class by name using NSClassFromString.',
76
+ pattern: /NSClassFromString\(\s*["'`](?:_UI\w+|_NS\w+|UIStatusBar\w*Internal|_CK\w+|_MF\w+)["'`]\s*\)/g,
77
+ severity: 'error',
78
+ suggestion: 'Do not use private classes. Use only documented public APIs.',
79
+ fileTypes: ['.swift', '.m', '.mm'],
80
+ },
81
+ // performSelector with private selectors
82
+ {
83
+ id: 'private-perform-selector',
84
+ title: 'performSelector with private selector',
85
+ description: 'Using performSelector with a selector starting with underscore.',
86
+ pattern: /perform(?:Selector|#selector)\s*\(\s*(?:NSSelectorFromString\(\s*)?["'`]_\w+/g,
87
+ severity: 'error',
88
+ suggestion: 'Do not call private selectors. Use documented public APIs.',
89
+ fileTypes: ['.swift', '.m', '.mm'],
90
+ },
91
+ // valueForKey accessing private properties
92
+ {
93
+ id: 'private-value-for-key',
94
+ title: 'Accessing private property via valueForKey',
95
+ description: 'Using valueForKey/setValue to access properties starting with underscore.',
96
+ pattern: /(?:value|setValue)\s*\(\s*(?:forKey|forKeyPath)\s*:\s*["'`]_\w+["'`]\s*\)/g,
97
+ severity: 'warning',
98
+ suggestion: 'Avoid accessing private properties via KVC. Use public APIs.',
99
+ fileTypes: ['.swift', '.m', '.mm'],
100
+ },
101
+ // dlopen for private frameworks
102
+ {
103
+ id: 'private-dlopen',
104
+ title: 'Dynamic loading of framework',
105
+ description: 'Using dlopen to dynamically load frameworks may indicate private API access.',
106
+ pattern: /dlopen\s*\(\s*["'`][^"'`]*(?:PrivateFrameworks|private)[^"'`]*["'`]/gi,
107
+ severity: 'error',
108
+ suggestion: 'Do not load private frameworks. Use only public frameworks.',
109
+ fileTypes: ['.swift', '.m', '.mm', '.c', '.cpp'],
110
+ },
111
+ // dlsym usage (suspicious in iOS apps)
112
+ {
113
+ id: 'private-dlsym',
114
+ title: 'Dynamic symbol lookup (dlsym)',
115
+ description: 'Using dlsym to look up symbols dynamically may indicate private API usage.',
116
+ pattern: /dlsym\s*\(/g,
117
+ severity: 'warning',
118
+ suggestion: 'Avoid dlsym in iOS apps. Use documented public APIs directly.',
119
+ fileTypes: ['.swift', '.m', '.mm', '.c', '.cpp'],
120
+ },
121
+ // objc_msgSend with private selectors
122
+ {
123
+ id: 'private-objc-msgsend',
124
+ title: 'Direct objc_msgSend call',
125
+ description: 'Direct objc_msgSend usage may be used to call private APIs.',
126
+ pattern: /objc_msgSend\s*\(/g,
127
+ severity: 'warning',
128
+ suggestion: 'Avoid direct objc_msgSend calls. Use standard method invocations.',
129
+ fileTypes: ['.m', '.mm', '.c', '.cpp'],
130
+ },
131
+ // IOKit private APIs
132
+ {
133
+ id: 'private-iokit',
134
+ title: 'IOKit private API usage',
135
+ description: 'IOKit APIs are mostly private on iOS and can cause rejection.',
136
+ pattern: /\b(?:IOServiceGetMatchingService|IORegistryEntryCreateCFProperties|IOMasterPort|IOServiceMatching)\b/g,
137
+ severity: 'error',
138
+ suggestion: 'IOKit is a private framework on iOS. Use public APIs (e.g., UIDevice) instead.',
139
+ fileTypes: ['.swift', '.m', '.mm', '.c', '.cpp'],
140
+ },
141
+ // Private status bar manipulation
142
+ {
143
+ id: 'private-statusbar',
144
+ title: 'Private status bar API',
145
+ description: 'Accessing private UIStatusBar APIs.',
146
+ pattern: /\b_(?:setStatusBarHidden|setStatusBarStyle|statusBarHeight|statusBarWindow)\b/g,
147
+ severity: 'error',
148
+ suggestion: 'Use the public UIViewController status bar appearance APIs.',
149
+ fileTypes: ['.swift', '.m', '.mm'],
150
+ },
151
+ // Accessing app container paths that suggest sandbox escape
152
+ {
153
+ id: 'private-sandbox-escape',
154
+ title: 'Potential sandbox escape',
155
+ description: 'Accessing file paths outside the app sandbox.',
156
+ pattern: /["'`]\/(?:var\/mobile|private\/var\/(?!mobile\/Containers)|Applications\/|usr\/lib\/)[^"'`]*["'`]/g,
157
+ severity: 'error',
158
+ suggestion: 'Apps must operate within their sandbox. Use FileManager APIs for app directories.',
159
+ fileTypes: ['.swift', '.m', '.mm'],
160
+ },
161
+ ];
162
+
163
+ /**
164
+ * Analyzer that detects usage of private iOS APIs
165
+ */
166
+ export class PrivateAPIAnalyzer implements Analyzer {
167
+ name = 'Private API Scanner';
168
+ description = 'Detects usage of private iOS APIs that cause App Store rejection';
169
+
170
+ async analyze(project: XcodeProject, options: AnalyzerOptions): Promise<AnalysisResult> {
171
+ const startTime = Date.now();
172
+
173
+ const targets = options.targetName
174
+ ? project.targets.filter((t) => t.name === options.targetName)
175
+ : project.targets.filter((t) => t.type === 'application');
176
+
177
+ let sourceFiles: string[] = [];
178
+ for (const target of targets) {
179
+ sourceFiles.push(...target.sourceFiles);
180
+ }
181
+
182
+ if (sourceFiles.length === 0) {
183
+ sourceFiles = await this.findSourceFiles(options.basePath);
184
+ }
185
+
186
+ // Filter to changed files for incremental scanning
187
+ if (options.changedFiles) {
188
+ const changedSet = new Set(options.changedFiles);
189
+ sourceFiles = sourceFiles.filter((f) => changedSet.has(f));
190
+ }
191
+
192
+ const issues = await this.scanFiles(sourceFiles);
193
+
194
+ return {
195
+ analyzer: this.name,
196
+ passed: issues.filter((i) => i.severity === 'error').length === 0,
197
+ issues,
198
+ duration: Date.now() - startTime,
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Scan a specific path for private API usage
204
+ */
205
+ async scanPath(scanPath: string): Promise<AnalysisResult> {
206
+ const startTime = Date.now();
207
+
208
+ const stats = await fs.stat(scanPath);
209
+ const files = stats.isDirectory()
210
+ ? await this.findSourceFiles(scanPath)
211
+ : [scanPath];
212
+
213
+ const issues = await this.scanFiles(files);
214
+
215
+ return {
216
+ analyzer: this.name,
217
+ passed: issues.filter((i) => i.severity === 'error').length === 0,
218
+ issues,
219
+ duration: Date.now() - startTime,
220
+ };
221
+ }
222
+
223
+ private async findSourceFiles(basePath: string): Promise<string[]> {
224
+ return fg(['**/*.swift', '**/*.m', '**/*.mm', '**/*.h', '**/*.c', '**/*.cpp'], {
225
+ cwd: basePath,
226
+ absolute: true,
227
+ ignore: [
228
+ '**/Pods/**',
229
+ '**/Carthage/**',
230
+ '**/build/**',
231
+ '**/DerivedData/**',
232
+ '**/*.generated.swift',
233
+ '**/Tests/**',
234
+ '**/UITests/**',
235
+ ],
236
+ });
237
+ }
238
+
239
+ private async scanFiles(files: string[]): Promise<Issue[]> {
240
+ const issues: Issue[] = [];
241
+ const seenIssues = new Set<string>();
242
+
243
+ for (const file of files) {
244
+ const ext = path.extname(file);
245
+
246
+ try {
247
+ const content = await fs.readFile(file, 'utf-8');
248
+ const lines = content.split('\n');
249
+
250
+ // Check for private framework imports
251
+ this.checkPrivateFrameworks(content, lines, file, issues, seenIssues);
252
+
253
+ // Check for private URL schemes
254
+ this.checkPrivateURLSchemes(content, file, issues, seenIssues);
255
+
256
+ // Check regex patterns
257
+ for (const apiPattern of PRIVATE_API_PATTERNS) {
258
+ if (!apiPattern.fileTypes.includes(ext)) continue;
259
+
260
+ apiPattern.pattern.lastIndex = 0;
261
+
262
+ let match: RegExpExecArray | null;
263
+ while ((match = apiPattern.pattern.exec(content)) !== null) {
264
+ const lineNumber = content.substring(0, match.index).split('\n').length;
265
+ const issueKey = `${apiPattern.id}:${file}:${lineNumber}`;
266
+
267
+ if (seenIssues.has(issueKey)) continue;
268
+ seenIssues.add(issueKey);
269
+
270
+ // Skip commented lines
271
+ const line = lines[lineNumber - 1] ?? '';
272
+ const trimmed = line.trim();
273
+ if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) {
274
+ continue;
275
+ }
276
+
277
+ issues.push({
278
+ id: apiPattern.id,
279
+ title: apiPattern.title,
280
+ description: `${apiPattern.description}\n\nFound: \`${match[0].substring(0, 60)}\``,
281
+ severity: apiPattern.severity,
282
+ filePath: file,
283
+ lineNumber,
284
+ category: 'private-api',
285
+ guideline: 'Guideline 2.5.1 - Use of Non-Public APIs',
286
+ suggestion: apiPattern.suggestion,
287
+ });
288
+
289
+ const count = issues.filter((i) => i.id === apiPattern.id && i.filePath === file).length;
290
+ if (count >= 5) break;
291
+ }
292
+ }
293
+ } catch {
294
+ // Skip files that can't be read
295
+ }
296
+ }
297
+
298
+ return issues;
299
+ }
300
+
301
+ private checkPrivateFrameworks(
302
+ content: string,
303
+ lines: string[],
304
+ file: string,
305
+ issues: Issue[],
306
+ seenIssues: Set<string>
307
+ ): void {
308
+ for (const framework of PRIVATE_FRAMEWORKS) {
309
+ const importPatterns = [
310
+ new RegExp(`import\\s+${framework}\\b`, 'g'),
311
+ new RegExp(`#import\\s*<${framework}/`, 'g'),
312
+ new RegExp(`@import\\s+${framework}\\b`, 'g'),
313
+ ];
314
+
315
+ for (const pattern of importPatterns) {
316
+ let match: RegExpExecArray | null;
317
+ while ((match = pattern.exec(content)) !== null) {
318
+ const lineNumber = content.substring(0, match.index).split('\n').length;
319
+ const issueKey = `private-framework-${framework}:${file}:${lineNumber}`;
320
+
321
+ if (seenIssues.has(issueKey)) continue;
322
+ seenIssues.add(issueKey);
323
+
324
+ const line = lines[lineNumber - 1] ?? '';
325
+ const trimmed = line.trim();
326
+ if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) {
327
+ continue;
328
+ }
329
+
330
+ issues.push({
331
+ id: `private-framework-${framework.toLowerCase()}`,
332
+ title: `Private framework: ${framework}`,
333
+ description: `Importing private/undocumented framework \`${framework}\` will cause App Store rejection.\n\nFound: \`${match[0]}\``,
334
+ severity: 'error',
335
+ filePath: file,
336
+ lineNumber,
337
+ category: 'private-api',
338
+ guideline: 'Guideline 2.5.1 - Use of Non-Public APIs',
339
+ suggestion: `Remove the \`${framework}\` import. Use only public Apple frameworks.`,
340
+ });
341
+ }
342
+ }
343
+ }
344
+ }
345
+
346
+ private checkPrivateURLSchemes(
347
+ content: string,
348
+ file: string,
349
+ issues: Issue[],
350
+ seenIssues: Set<string>
351
+ ): void {
352
+ for (const urlScheme of PRIVATE_URL_SCHEMES) {
353
+ const pattern = new RegExp(`["'\`]${urlScheme.scheme.replace('://', '://')}`, 'gi');
354
+ let match: RegExpExecArray | null;
355
+
356
+ while ((match = pattern.exec(content)) !== null) {
357
+ const lineNumber = content.substring(0, match.index).split('\n').length;
358
+ const issueKey = `private-url-scheme:${urlScheme.scheme}:${file}:${lineNumber}`;
359
+
360
+ if (seenIssues.has(issueKey)) continue;
361
+ seenIssues.add(issueKey);
362
+
363
+ issues.push({
364
+ id: 'private-url-scheme',
365
+ title: `Private URL scheme: ${urlScheme.scheme}`,
366
+ description: `${urlScheme.description}\n\nFound: \`${match[0]}\``,
367
+ severity: 'error',
368
+ filePath: file,
369
+ lineNumber,
370
+ category: 'private-api',
371
+ guideline: 'Guideline 2.5.1 - Use of Non-Public APIs',
372
+ suggestion: 'Use only public URL schemes documented by Apple.',
373
+ });
374
+ }
375
+ }
376
+ }
377
+ }