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,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
|
+
}
|