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,408 @@
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
+ * Patterns to scan for in source code
14
+ */
15
+ interface ScanPattern {
16
+ id: string;
17
+ title: string;
18
+ description: string;
19
+ severity: 'error' | 'warning' | 'info';
20
+ pattern: RegExp;
21
+ guideline?: string;
22
+ suggestion?: string;
23
+ fileTypes?: string[];
24
+ }
25
+
26
+ const SCAN_PATTERNS: ScanPattern[] = [
27
+ // IPv4 hardcoded addresses (IPv6 compliance)
28
+ {
29
+ id: 'hardcoded-ipv4',
30
+ title: 'Hardcoded IPv4 address',
31
+ description: 'Hardcoded IPv4 addresses may cause issues on IPv6-only networks.',
32
+ severity: 'warning',
33
+ pattern: /["'`](\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})["'`]/g,
34
+ guideline: 'Guideline 2.5.1 - IPv6 Compatibility',
35
+ suggestion: 'Use hostnames instead of hardcoded IP addresses for IPv6 compatibility.',
36
+ },
37
+ // Hardcoded API keys and secrets
38
+ {
39
+ id: 'hardcoded-api-key',
40
+ title: 'Potential hardcoded API key',
41
+ description: 'This appears to be a hardcoded API key or secret.',
42
+ severity: 'error',
43
+ pattern:
44
+ /(?:api[_-]?key|apikey|secret[_-]?key|secretkey|auth[_-]?token|access[_-]?token|private[_-]?key)\s*[:=]\s*["'`][A-Za-z0-9_\-]{16,}["'`]/gi,
45
+ guideline: 'Security Best Practice',
46
+ suggestion:
47
+ 'Store sensitive keys in environment variables, Keychain, or a secure configuration system.',
48
+ },
49
+ // AWS keys
50
+ {
51
+ id: 'aws-key',
52
+ title: 'Potential AWS access key',
53
+ description: 'This appears to be a hardcoded AWS access key.',
54
+ severity: 'error',
55
+ pattern: /["'`](AKIA[0-9A-Z]{16})["'`]/g,
56
+ guideline: 'Security Best Practice',
57
+ suggestion: 'Never commit AWS keys to source code. Use IAM roles or secure key management.',
58
+ },
59
+ // Test/Debug server URLs
60
+ {
61
+ id: 'test-server-url',
62
+ title: 'Test/staging server URL',
63
+ description: 'This appears to be a test or staging server URL that should not be in production.',
64
+ severity: 'warning',
65
+ pattern:
66
+ /["'`]https?:\/\/(?:localhost|127\.0\.0\.1|192\.168\.\d+\.\d+|10\.\d+\.\d+\.\d+|staging\.|test\.|dev\.)[^"'`]*["'`]/gi,
67
+ suggestion:
68
+ 'Ensure test/staging URLs are not used in release builds. Use build configurations or environment variables.',
69
+ },
70
+ // Print/NSLog statements
71
+ {
72
+ id: 'print-statement',
73
+ title: 'Print/logging statement',
74
+ description: 'Console logging statements should be removed or disabled in release builds.',
75
+ severity: 'info',
76
+ pattern: /\b(?:print|NSLog|debugPrint)\s*\(/g,
77
+ suggestion: 'Use conditional logging that is disabled in release builds.',
78
+ fileTypes: ['.swift', '.m', '.mm'],
79
+ },
80
+ // TODO/FIXME comments
81
+ {
82
+ id: 'todo-comment',
83
+ title: 'TODO/FIXME comment',
84
+ description: 'Incomplete task marker found. Ensure all TODOs are addressed before release.',
85
+ severity: 'info',
86
+ pattern: /\/\/\s*(?:TODO|FIXME|HACK|XXX|BUG)[\s:]/gi,
87
+ suggestion: 'Address or remove TODO/FIXME comments before App Store submission.',
88
+ },
89
+ // Force unwrapping
90
+ {
91
+ id: 'force-unwrap',
92
+ title: 'Force unwrap operator',
93
+ description: 'Force unwrapping (!) can cause crashes if the value is nil.',
94
+ severity: 'info',
95
+ pattern: /\w+\s*!\s*(?:\.|$|\)|\])/g,
96
+ suggestion: 'Consider using optional binding (if let) or nil-coalescing (??) instead.',
97
+ fileTypes: ['.swift'],
98
+ },
99
+ // Hardcoded credentials
100
+ {
101
+ id: 'hardcoded-password',
102
+ title: 'Potential hardcoded password',
103
+ description: 'This appears to be a hardcoded password or credential.',
104
+ severity: 'error',
105
+ pattern: /(?:password|passwd|pwd|credential)\s*[:=]\s*["'`][^"'`]{4,}["'`]/gi,
106
+ guideline: 'Security Best Practice',
107
+ suggestion: 'Never hardcode passwords. Use Keychain or secure credential storage.',
108
+ },
109
+ // HTTP URLs (non-HTTPS)
110
+ {
111
+ id: 'insecure-http',
112
+ title: 'Insecure HTTP URL',
113
+ description: 'HTTP URLs are insecure. Use HTTPS for all network connections.',
114
+ severity: 'warning',
115
+ pattern: /["'`]http:\/\/(?!localhost|127\.0\.0\.1)[^"'`]+["'`]/gi,
116
+ guideline: 'Guideline 2.5.4 - Security',
117
+ suggestion: 'Use HTTPS for all external URLs. Configure App Transport Security appropriately.',
118
+ },
119
+ // Placeholder text
120
+ {
121
+ id: 'placeholder-text',
122
+ title: 'Placeholder text',
123
+ description: 'Lorem ipsum or placeholder text detected.',
124
+ severity: 'warning',
125
+ pattern: /["'`](?:lorem\s+ipsum|placeholder|sample\s+text|dummy\s+text)[^"'`]*["'`]/gi,
126
+ guideline: 'Guideline 2.3 - Accurate Metadata',
127
+ suggestion: 'Replace placeholder text with actual content before submission.',
128
+ },
129
+ // #if DEBUG with potentially problematic code
130
+ {
131
+ id: 'debug-ifdef',
132
+ title: '#if DEBUG block',
133
+ description: 'Debug-only code block detected. Verify it does not affect release functionality.',
134
+ severity: 'info',
135
+ pattern: /#if\s+DEBUG/g,
136
+ suggestion: 'Review DEBUG blocks to ensure they do not contain required functionality.',
137
+ fileTypes: ['.swift'],
138
+ },
139
+ // Deprecated UIWebView
140
+ {
141
+ id: 'deprecated-uiwebview',
142
+ title: 'Deprecated UIWebView usage',
143
+ description:
144
+ 'UIWebView is deprecated and Apple rejects new apps using it. Use WKWebView instead.',
145
+ severity: 'error',
146
+ pattern: /\bUIWebView\b/g,
147
+ guideline: 'ITMS-90809',
148
+ suggestion: 'Migrate to WKWebView. UIWebView is no longer accepted.',
149
+ },
150
+ // Deprecated APIs
151
+ {
152
+ id: 'deprecated-addressbook',
153
+ title: 'Deprecated AddressBook framework',
154
+ description: 'AddressBook framework is deprecated. Use Contacts framework instead.',
155
+ severity: 'warning',
156
+ pattern: /\bABAddressBook\w*\b/g,
157
+ suggestion: 'Migrate to the Contacts framework.',
158
+ fileTypes: ['.swift', '.m', '.mm'],
159
+ },
160
+ ];
161
+
162
+ /**
163
+ * Code scanner for detecting common issues
164
+ */
165
+ export class CodeScanner implements Analyzer {
166
+ name = 'Code Scanner';
167
+ description = 'Scans source code for common App Store rejection issues';
168
+
169
+ async analyze(project: XcodeProject, options: AnalyzerOptions): Promise<AnalysisResult> {
170
+ const startTime = Date.now();
171
+
172
+ // Get source files from targets or scan directory
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 no source files from project, scan directory
183
+ if (sourceFiles.length === 0) {
184
+ sourceFiles = await this.findSourceFiles(options.basePath);
185
+ }
186
+
187
+ // Filter to changed files for incremental scanning
188
+ if (options.changedFiles) {
189
+ const changedSet = new Set(options.changedFiles);
190
+ sourceFiles = sourceFiles.filter((f) => changedSet.has(f));
191
+ }
192
+
193
+ const issues = await this.scanFiles(sourceFiles);
194
+
195
+ return {
196
+ analyzer: this.name,
197
+ passed: issues.filter((i) => i.severity === 'error').length === 0,
198
+ issues,
199
+ duration: Date.now() - startTime,
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Scan a specific path (file or directory)
205
+ */
206
+ async scanPath(scanPath: string, patterns?: string[]): Promise<AnalysisResult> {
207
+ const startTime = Date.now();
208
+
209
+ const stats = await fs.stat(scanPath);
210
+ const files = stats.isDirectory()
211
+ ? await this.findSourceFiles(scanPath)
212
+ : [scanPath];
213
+
214
+ const activePatterns = patterns
215
+ ? SCAN_PATTERNS.filter((p) => patterns.includes(p.id))
216
+ : SCAN_PATTERNS;
217
+
218
+ const issues = await this.scanFiles(files, activePatterns);
219
+
220
+ return {
221
+ analyzer: this.name,
222
+ passed: issues.filter((i) => i.severity === 'error').length === 0,
223
+ issues,
224
+ duration: Date.now() - startTime,
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Find all source files in a directory
230
+ */
231
+ private async findSourceFiles(basePath: string): Promise<string[]> {
232
+ return fg(['**/*.swift', '**/*.m', '**/*.mm', '**/*.h'], {
233
+ cwd: basePath,
234
+ absolute: true,
235
+ ignore: [
236
+ '**/Pods/**',
237
+ '**/Carthage/**',
238
+ '**/build/**',
239
+ '**/DerivedData/**',
240
+ '**/*.generated.swift',
241
+ '**/Tests/**',
242
+ '**/UITests/**',
243
+ ],
244
+ });
245
+ }
246
+
247
+ /**
248
+ * Scan files for issues
249
+ */
250
+ private async scanFiles(
251
+ files: string[],
252
+ patterns: ScanPattern[] = SCAN_PATTERNS
253
+ ): Promise<Issue[]> {
254
+ const issues: Issue[] = [];
255
+ const seenIssues = new Set<string>();
256
+
257
+ for (const file of files) {
258
+ const ext = path.extname(file);
259
+
260
+ try {
261
+ const content = await fs.readFile(file, 'utf-8');
262
+ const lines = content.split('\n');
263
+
264
+ for (const pattern of patterns) {
265
+ // Skip if pattern is for specific file types and this isn't one
266
+ if (pattern.fileTypes && !pattern.fileTypes.includes(ext)) {
267
+ continue;
268
+ }
269
+
270
+ // Reset regex state
271
+ pattern.pattern.lastIndex = 0;
272
+
273
+ let match: RegExpExecArray | null;
274
+ while ((match = pattern.pattern.exec(content)) !== null) {
275
+ // Find line number
276
+ const lineNumber = this.getLineNumber(content, match.index);
277
+ const issueKey = `${pattern.id}:${file}:${lineNumber}`;
278
+
279
+ // Avoid duplicate issues at the same location
280
+ if (seenIssues.has(issueKey)) {
281
+ continue;
282
+ }
283
+ seenIssues.add(issueKey);
284
+
285
+ // Skip false positives
286
+ if (this.isFalsePositive(pattern.id, match[0], lines[lineNumber - 1] ?? '')) {
287
+ continue;
288
+ }
289
+
290
+ const issue: Issue = {
291
+ id: pattern.id,
292
+ title: pattern.title,
293
+ description: `${pattern.description}\n\nFound: \`${this.truncate(match[0], 50)}\``,
294
+ severity: pattern.severity,
295
+ filePath: file,
296
+ lineNumber,
297
+ category: 'code',
298
+ };
299
+ if (pattern.guideline) {
300
+ issue.guideline = pattern.guideline;
301
+ }
302
+ if (pattern.suggestion) {
303
+ issue.suggestion = pattern.suggestion;
304
+ }
305
+ issues.push(issue);
306
+
307
+ // Limit issues per pattern per file
308
+ const issuesForPattern = issues.filter(
309
+ (i) => i.id === pattern.id && i.filePath === file
310
+ );
311
+ if (issuesForPattern.length >= 5) {
312
+ break;
313
+ }
314
+ }
315
+ }
316
+ } catch {
317
+ // Skip files that can't be read
318
+ }
319
+ }
320
+
321
+ return issues;
322
+ }
323
+
324
+ /**
325
+ * Get line number from character index
326
+ */
327
+ private getLineNumber(content: string, index: number): number {
328
+ return content.substring(0, index).split('\n').length;
329
+ }
330
+
331
+ /**
332
+ * Check if a match is a false positive
333
+ */
334
+ private isFalsePositive(patternId: string, match: string, line: string): boolean {
335
+ // Skip commented lines
336
+ const trimmedLine = line.trim();
337
+ if (trimmedLine.startsWith('//') || trimmedLine.startsWith('*') || trimmedLine.startsWith('/*')) {
338
+ // Allow TODO/FIXME in comments (that's what we're looking for)
339
+ if (patternId !== 'todo-comment') {
340
+ return true;
341
+ }
342
+ }
343
+
344
+ // Skip test files content
345
+ if (line.includes('XCTest') || line.includes('@testable')) {
346
+ return true;
347
+ }
348
+
349
+ // Pattern-specific false positive handling
350
+ switch (patternId) {
351
+ case 'hardcoded-ipv4':
352
+ // Skip version numbers that look like IPs
353
+ if (/\d+\.\d+\.\d+\.\d+/.test(match)) {
354
+ const ip = match.match(/\d+\.\d+\.\d+\.\d+/)?.[0];
355
+ if (ip) {
356
+ const parts = ip.split('.').map(Number);
357
+ // Skip if any part > 255 (not a valid IP)
358
+ if (parts.some((p) => p !== undefined && p > 255)) {
359
+ return true;
360
+ }
361
+ // Skip localhost
362
+ if (ip === '127.0.0.1' || ip === '0.0.0.0') {
363
+ return true;
364
+ }
365
+ }
366
+ }
367
+ break;
368
+
369
+ case 'force-unwrap':
370
+ // Skip IBOutlets and known safe patterns
371
+ if (line.includes('@IBOutlet') || line.includes('@IBAction')) {
372
+ return true;
373
+ }
374
+ // Skip try! and as!
375
+ if (/try\s*!/.test(match) || /as\s*!/.test(match)) {
376
+ // These are separate issues, not force unwrap
377
+ return true;
378
+ }
379
+ break;
380
+
381
+ case 'print-statement':
382
+ // Skip if inside #if DEBUG
383
+ if (line.includes('#if DEBUG') || line.includes('#if debug')) {
384
+ return true;
385
+ }
386
+ break;
387
+
388
+ case 'insecure-http':
389
+ // Skip App Transport Security exception domains
390
+ if (line.includes('NSExceptionDomains') || line.includes('Exception')) {
391
+ return true;
392
+ }
393
+ break;
394
+ }
395
+
396
+ return false;
397
+ }
398
+
399
+ /**
400
+ * Truncate string for display
401
+ */
402
+ private truncate(str: string, maxLength: number): string {
403
+ if (str.length <= maxLength) {
404
+ return str;
405
+ }
406
+ return str.substring(0, maxLength - 3) + '...';
407
+ }
408
+ }