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,121 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import fg from 'fast-glob';
4
+ import type { AnalysisResult, Issue, Severity } from '../types/index.js';
5
+ import type { CompiledRule, CustomRuleConfig } from './types.js';
6
+
7
+ const SOURCE_EXTENSIONS = ['.swift', '.m', '.mm', '.h', '.c', '.cpp'];
8
+ const DISABLE_COMMENT_PATTERN = /\/\/\s*ios-review-disable-next-line\s+([\w-]+(?:\s*,\s*[\w-]+)*)/;
9
+
10
+ export class CustomRuleEngine {
11
+ private disabledRules: Set<string>;
12
+ private severityOverrides: Record<string, Severity>;
13
+
14
+ constructor(config?: CustomRuleConfig) {
15
+ this.disabledRules = new Set(config?.disabledRules ?? []);
16
+ this.severityOverrides = config?.severityOverrides ?? {};
17
+ }
18
+
19
+ isRuleDisabled(ruleId: string): boolean {
20
+ return this.disabledRules.has(ruleId);
21
+ }
22
+
23
+ getSeverityOverride(ruleId: string): Severity | undefined {
24
+ return this.severityOverrides[ruleId];
25
+ }
26
+
27
+ async scan(scanPath: string, compiledRules: CompiledRule[]): Promise<AnalysisResult> {
28
+ const startTime = Date.now();
29
+ const issues: Issue[] = [];
30
+
31
+ // Filter out disabled rules
32
+ const activeRules = compiledRules.filter((r) => !this.isRuleDisabled(r.id));
33
+
34
+ if (activeRules.length === 0) {
35
+ return {
36
+ analyzer: 'Custom Rules',
37
+ passed: true,
38
+ issues: [],
39
+ duration: Date.now() - startTime,
40
+ };
41
+ }
42
+
43
+ // Determine files to scan
44
+ const stat = await fs.stat(scanPath);
45
+ let files: string[];
46
+
47
+ if (stat.isFile()) {
48
+ files = [scanPath];
49
+ } else {
50
+ const patterns = SOURCE_EXTENSIONS.map((ext) => `**/*${ext}`);
51
+ files = await fg(patterns, {
52
+ cwd: scanPath,
53
+ absolute: true,
54
+ ignore: ['**/node_modules/**', '**/Pods/**', '**/build/**', '**/.build/**'],
55
+ });
56
+ }
57
+
58
+ for (const filePath of files) {
59
+ const ext = path.extname(filePath);
60
+ const content = await fs.readFile(filePath, 'utf-8');
61
+ const lines = content.split('\n');
62
+
63
+ for (const rule of activeRules) {
64
+ // Check fileTypes filter
65
+ if (rule.fileTypes && rule.fileTypes.length > 0 && !rule.fileTypes.includes(ext)) {
66
+ continue;
67
+ }
68
+
69
+ // Reset regex lastIndex for global patterns
70
+ rule.regex.lastIndex = 0;
71
+
72
+ let match: RegExpExecArray | null;
73
+ while ((match = rule.regex.exec(content)) !== null) {
74
+ // Find line number
75
+ const upToMatch = content.substring(0, match.index);
76
+ const lineNumber = upToMatch.split('\n').length;
77
+
78
+ // Check for disable comment on previous line
79
+ if (lineNumber >= 2) {
80
+ const prevLine = lines[lineNumber - 2]; // -2 because lineNumber is 1-based and we want prev line
81
+ if (prevLine) {
82
+ const disableMatch = DISABLE_COMMENT_PATTERN.exec(prevLine);
83
+ if (disableMatch?.[1]) {
84
+ const disabledIds = disableMatch[1].split(',').map((s) => s.trim());
85
+ if (disabledIds.includes(rule.id)) {
86
+ continue;
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ const severity = this.getSeverityOverride(rule.id) ?? rule.severity;
93
+
94
+ issues.push({
95
+ id: rule.id,
96
+ title: rule.title,
97
+ description: rule.description,
98
+ severity,
99
+ filePath,
100
+ lineNumber,
101
+ guideline: rule.guideline,
102
+ suggestion: rule.suggestion,
103
+ category: rule.category ?? 'custom',
104
+ });
105
+
106
+ // For non-global regex, break after first match
107
+ if (!rule.regex.global) {
108
+ break;
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ return {
115
+ analyzer: 'Custom Rules',
116
+ passed: issues.every((i) => i.severity !== 'error'),
117
+ issues,
118
+ duration: Date.now() - startTime,
119
+ };
120
+ }
121
+ }
@@ -0,0 +1,3 @@
1
+ export { RuleLoader } from './loader.js';
2
+ export { CustomRuleEngine } from './engine.js';
3
+ export type { CustomRule, CustomRuleConfig, CompiledRule } from './types.js';
@@ -0,0 +1,83 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { z } from 'zod';
4
+ import type { CustomRuleConfig, CompiledRule } from './types.js';
5
+
6
+ const CustomRuleSchema = z.object({
7
+ id: z.string().min(1),
8
+ title: z.string().min(1),
9
+ description: z.string().min(1),
10
+ severity: z.enum(['error', 'warning', 'info']),
11
+ pattern: z.string().min(1),
12
+ flags: z.string().optional(),
13
+ fileTypes: z.array(z.string()).optional(),
14
+ guideline: z.string().optional(),
15
+ suggestion: z.string().optional(),
16
+ category: z.string().optional(),
17
+ });
18
+
19
+ const CustomRuleConfigSchema = z.object({
20
+ version: z.literal(1),
21
+ rules: z.array(CustomRuleSchema),
22
+ disabledRules: z.array(z.string()).optional(),
23
+ severityOverrides: z.record(z.enum(['error', 'warning', 'info'])).optional(),
24
+ });
25
+
26
+ const CONFIG_FILENAME = '.ios-review-rules.json';
27
+
28
+ export class RuleLoader {
29
+ /**
30
+ * Find config file by walking up from the given directory
31
+ */
32
+ async findConfig(startDir: string): Promise<string | null> {
33
+ let dir = path.resolve(startDir);
34
+ const root = path.parse(dir).root;
35
+
36
+ while (dir !== root) {
37
+ const configPath = path.join(dir, CONFIG_FILENAME);
38
+ try {
39
+ await fs.access(configPath);
40
+ return configPath;
41
+ } catch {
42
+ dir = path.dirname(dir);
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+
48
+ /**
49
+ * Load and validate a config file
50
+ */
51
+ async loadConfig(configPath: string): Promise<CustomRuleConfig> {
52
+ const raw = await fs.readFile(configPath, 'utf-8');
53
+ const parsed = JSON.parse(raw);
54
+ return CustomRuleConfigSchema.parse(parsed) as CustomRuleConfig;
55
+ }
56
+
57
+ /**
58
+ * Compile regex patterns from rules
59
+ */
60
+ compileRules(config: CustomRuleConfig): CompiledRule[] {
61
+ const compiled: CompiledRule[] = [];
62
+
63
+ for (const rule of config.rules) {
64
+ const flags = rule.flags ?? 'g';
65
+ const regex = new RegExp(rule.pattern, flags);
66
+ compiled.push({ ...rule, regex });
67
+ }
68
+
69
+ return compiled;
70
+ }
71
+
72
+ /**
73
+ * Find, load, validate, and compile rules from project directory
74
+ */
75
+ async loadFromProject(projectDir: string): Promise<{ config: CustomRuleConfig; rules: CompiledRule[] } | null> {
76
+ const configPath = await this.findConfig(projectDir);
77
+ if (!configPath) return null;
78
+
79
+ const config = await this.loadConfig(configPath);
80
+ const rules = this.compileRules(config);
81
+ return { config, rules };
82
+ }
83
+ }
@@ -0,0 +1,25 @@
1
+ import type { Severity, IssueCategory } from '../types/index.js';
2
+
3
+ export interface CustomRule {
4
+ id: string;
5
+ title: string;
6
+ description: string;
7
+ severity: Severity;
8
+ pattern: string;
9
+ flags?: string | undefined;
10
+ fileTypes?: string[] | undefined;
11
+ guideline?: string | undefined;
12
+ suggestion?: string | undefined;
13
+ category?: IssueCategory | 'custom' | undefined;
14
+ }
15
+
16
+ export interface CustomRuleConfig {
17
+ version: 1;
18
+ rules: CustomRule[];
19
+ disabledRules?: string[] | undefined;
20
+ severityOverrides?: Record<string, Severity> | undefined;
21
+ }
22
+
23
+ export interface CompiledRule extends CustomRule {
24
+ regex: RegExp;
25
+ }
@@ -0,0 +1,247 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Severity levels for analysis issues
5
+ */
6
+ export type Severity = 'error' | 'warning' | 'info';
7
+
8
+ /**
9
+ * An issue found during analysis
10
+ */
11
+ export interface Issue {
12
+ /** Unique identifier for the issue type */
13
+ id: string;
14
+ /** Human-readable title */
15
+ title: string;
16
+ /** Detailed description of the issue */
17
+ description: string;
18
+ /** Severity level */
19
+ severity: Severity;
20
+ /** File path where the issue was found (if applicable) */
21
+ filePath?: string | undefined;
22
+ /** Line number in the file (if applicable) */
23
+ lineNumber?: number | undefined;
24
+ /** Related App Store Guideline (if applicable) */
25
+ guideline?: string | undefined;
26
+ /** Suggested fix or remediation */
27
+ suggestion?: string | undefined;
28
+ /** Category of the issue */
29
+ category: IssueCategory;
30
+ /** URL to the relevant App Store Review Guideline */
31
+ guidelineUrl?: string | undefined;
32
+ /** Excerpt from the relevant guideline */
33
+ guidelineExcerpt?: string | undefined;
34
+ /** Calculated severity score based on guideline weight */
35
+ severityScore?: number | undefined;
36
+ }
37
+
38
+ /**
39
+ * Categories for grouping issues
40
+ */
41
+ export type IssueCategory =
42
+ | 'info-plist'
43
+ | 'privacy'
44
+ | 'entitlements'
45
+ | 'code'
46
+ | 'security'
47
+ | 'metadata'
48
+ | 'screenshots'
49
+ | 'version'
50
+ | 'iap'
51
+ | 'asc'
52
+ | 'deprecated-api'
53
+ | 'private-api'
54
+ | 'ui-ux'
55
+ | 'custom';
56
+
57
+ /**
58
+ * Result from an analyzer
59
+ */
60
+ export interface AnalysisResult {
61
+ /** Name of the analyzer */
62
+ analyzer: string;
63
+ /** Whether the analysis passed (no errors) */
64
+ passed: boolean;
65
+ /** List of issues found */
66
+ issues: Issue[];
67
+ /** Time taken in milliseconds */
68
+ duration: number;
69
+ }
70
+
71
+ /**
72
+ * Complete analysis report
73
+ */
74
+ export interface AnalysisReport {
75
+ /** Project path that was analyzed */
76
+ projectPath: string;
77
+ /** Timestamp of the analysis */
78
+ timestamp: string;
79
+ /** Results from each analyzer */
80
+ results: AnalysisResult[];
81
+ /** Summary statistics */
82
+ summary: AnalysisSummary;
83
+ }
84
+
85
+ /**
86
+ * Summary statistics for an analysis
87
+ */
88
+ export interface AnalysisSummary {
89
+ /** Total number of issues */
90
+ totalIssues: number;
91
+ /** Number of errors */
92
+ errors: number;
93
+ /** Number of warnings */
94
+ warnings: number;
95
+ /** Number of info items */
96
+ info: number;
97
+ /** Overall pass/fail status */
98
+ passed: boolean;
99
+ /** Total analysis duration in milliseconds */
100
+ duration: number;
101
+ }
102
+
103
+ /**
104
+ * Xcode project information
105
+ */
106
+ export interface XcodeProject {
107
+ /** Path to the .xcodeproj directory */
108
+ path: string;
109
+ /** Project name */
110
+ name: string;
111
+ /** Build targets */
112
+ targets: XcodeTarget[];
113
+ /** Build configurations */
114
+ configurations: string[];
115
+ }
116
+
117
+ /**
118
+ * Xcode build target
119
+ */
120
+ export interface XcodeTarget {
121
+ /** Target name */
122
+ name: string;
123
+ /** Target type (app, framework, extension, etc.) */
124
+ type: TargetType;
125
+ /** Bundle identifier */
126
+ bundleIdentifier?: string | undefined;
127
+ /** Path to Info.plist */
128
+ infoPlistPath?: string | undefined;
129
+ /** Path to entitlements file */
130
+ entitlementsPath?: string | undefined;
131
+ /** Minimum deployment target */
132
+ deploymentTarget?: string | undefined;
133
+ /** Source files */
134
+ sourceFiles: string[];
135
+ }
136
+
137
+ export type TargetType =
138
+ | 'application'
139
+ | 'framework'
140
+ | 'staticLibrary'
141
+ | 'dynamicLibrary'
142
+ | 'appExtension'
143
+ | 'watchApp'
144
+ | 'watchExtension'
145
+ | 'tvExtension'
146
+ | 'unitTest'
147
+ | 'uiTest'
148
+ | 'unknown';
149
+
150
+ /**
151
+ * Parsed Info.plist contents
152
+ */
153
+ export interface InfoPlist {
154
+ /** Bundle identifier */
155
+ CFBundleIdentifier?: string;
156
+ /** Bundle name */
157
+ CFBundleName?: string;
158
+ /** Bundle display name */
159
+ CFBundleDisplayName?: string;
160
+ /** Bundle version (build number) */
161
+ CFBundleVersion?: string;
162
+ /** Short version string (marketing version) */
163
+ CFBundleShortVersionString?: string;
164
+ /** Minimum iOS version */
165
+ MinimumOSVersion?: string;
166
+ /** Privacy usage descriptions */
167
+ [key: `NS${string}UsageDescription`]: string | undefined;
168
+ /** App Transport Security settings */
169
+ NSAppTransportSecurity?: {
170
+ NSAllowsArbitraryLoads?: boolean;
171
+ NSExceptionDomains?: Record<string, unknown>;
172
+ };
173
+ /** All other properties */
174
+ [key: string]: unknown;
175
+ }
176
+
177
+ /**
178
+ * Privacy manifest (PrivacyInfo.xcprivacy) contents
179
+ */
180
+ export interface PrivacyManifest {
181
+ /** Privacy tracking enabled */
182
+ NSPrivacyTracking?: boolean;
183
+ /** Tracking domains */
184
+ NSPrivacyTrackingDomains?: string[];
185
+ /** Collected data types */
186
+ NSPrivacyCollectedDataTypes?: PrivacyCollectedDataType[];
187
+ /** Accessed API types */
188
+ NSPrivacyAccessedAPITypes?: PrivacyAccessedAPIType[];
189
+ }
190
+
191
+ export interface PrivacyCollectedDataType {
192
+ NSPrivacyCollectedDataType: string;
193
+ NSPrivacyCollectedDataTypeLinked: boolean;
194
+ NSPrivacyCollectedDataTypeTracking: boolean;
195
+ NSPrivacyCollectedDataTypePurposes: string[];
196
+ }
197
+
198
+ export interface PrivacyAccessedAPIType {
199
+ NSPrivacyAccessedAPIType: string;
200
+ NSPrivacyAccessedAPITypeReasons: string[];
201
+ }
202
+
203
+ /**
204
+ * Input schema for the analyze tool
205
+ */
206
+ export const AnalyzeInputSchema = z.object({
207
+ projectPath: z.string().describe('Path to the .xcodeproj or .xcworkspace directory'),
208
+ analyzers: z
209
+ .array(z.enum(['all', 'info-plist', 'privacy', 'entitlements', 'code', 'deprecated-api', 'private-api', 'security', 'ui-ux', 'asc-metadata', 'asc-screenshots', 'asc-version', 'asc-iap']))
210
+ .optional()
211
+ .describe('Specific analyzers to run (default: all)'),
212
+ targetName: z.string().optional().describe('Specific target to analyze'),
213
+ includeASC: z.boolean().optional().describe('Include App Store Connect validation (requires ASC credentials)'),
214
+ bundleId: z.string().optional().describe('Bundle ID for ASC validation (auto-detected if not provided)'),
215
+ format: z
216
+ .enum(['markdown', 'html', 'json'])
217
+ .optional()
218
+ .describe('Report output format (default: markdown)'),
219
+ saveToHistory: z.boolean().optional().describe('Save scan results to history for future comparison'),
220
+ customRulesPath: z.string().optional().describe('Path to custom rules config file (.ios-review-rules.json)'),
221
+ changedSince: z.string().optional().describe('Git ref to compare against for incremental scanning'),
222
+ });
223
+
224
+ export type AnalyzeInput = z.infer<typeof AnalyzeInputSchema>;
225
+
226
+ /**
227
+ * Analyzer interface that all analyzers must implement
228
+ */
229
+ export interface Analyzer {
230
+ /** Unique name of the analyzer */
231
+ name: string;
232
+ /** Description of what the analyzer checks */
233
+ description: string;
234
+ /** Run the analysis on a project */
235
+ analyze(project: XcodeProject, options?: AnalyzerOptions): Promise<AnalysisResult>;
236
+ }
237
+
238
+ export interface AnalyzerOptions {
239
+ /** Specific target to analyze */
240
+ targetName?: string | undefined;
241
+ /** Base path for resolving relative paths */
242
+ basePath: string;
243
+ /** Bundle ID for ASC validation (auto-detected from project if not provided) */
244
+ bundleId?: string | undefined;
245
+ /** List of changed files for incremental scanning */
246
+ changedFiles?: string[] | undefined;
247
+ }
@@ -0,0 +1,142 @@
1
+ import type { ProgressEvent } from '../src/progress/types.js';
2
+
3
+ // Mock the Xcode project parser
4
+ jest.mock('../src/parsers/xcodeproj.js', () => ({
5
+ parseXcodeProject: jest.fn().mockResolvedValue({
6
+ path: '/test/TestApp.xcodeproj',
7
+ name: 'TestApp',
8
+ targets: [
9
+ {
10
+ name: 'TestApp',
11
+ type: 'application',
12
+ bundleIdentifier: 'com.test.app',
13
+ deploymentTarget: '15.0',
14
+ sourceFiles: [],
15
+ },
16
+ ],
17
+ configurations: ['Debug', 'Release'],
18
+ }),
19
+ }));
20
+
21
+ // Mock the git module
22
+ jest.mock('../src/git/diff.js', () => ({
23
+ getChangedFiles: jest.fn().mockReturnValue([]),
24
+ }));
25
+
26
+ // Mock the rules module
27
+ jest.mock('../src/rules/index.js', () => ({
28
+ RuleLoader: jest.fn().mockImplementation(() => ({
29
+ loadFromProject: jest.fn().mockResolvedValue(null),
30
+ })),
31
+ CustomRuleEngine: jest.fn(),
32
+ }));
33
+
34
+ // Mock the guidelines module
35
+ jest.mock('../src/guidelines/index.js', () => ({
36
+ GuidelineMatcher: jest.fn().mockImplementation(() => ({
37
+ enrichReport: jest.fn().mockImplementation((report) => ({
38
+ ...report,
39
+ score: 85,
40
+ enrichedIssues: [],
41
+ guidelinesCited: [],
42
+ })),
43
+ })),
44
+ }));
45
+
46
+ // Use require to avoid dynamic import issues with jest
47
+ const { runAnalysis } = require('../src/analyzer.js') as typeof import('../src/analyzer.js');
48
+
49
+ describe('runAnalysis', () => {
50
+ beforeEach(() => {
51
+ jest.clearAllMocks();
52
+ });
53
+
54
+ it('should run all core analyzers by default', async () => {
55
+ const report = await runAnalysis({
56
+ projectPath: '/test/TestApp.xcodeproj',
57
+ });
58
+
59
+ // Should have results from all 8 core analyzers
60
+ expect(report.results.length).toBe(8);
61
+ });
62
+
63
+ it('should run specific analyzers when specified', async () => {
64
+ const report = await runAnalysis({
65
+ projectPath: '/test/TestApp.xcodeproj',
66
+ analyzers: ['code', 'security'],
67
+ });
68
+
69
+ expect(report.results.length).toBe(2);
70
+ const analyzerNames = report.results.map((r) => r.analyzer);
71
+ expect(analyzerNames).toContain('Code Scanner');
72
+ expect(analyzerNames).toContain('Security Analyzer');
73
+ });
74
+
75
+ it('should calculate summary correctly', async () => {
76
+ const report = await runAnalysis({
77
+ projectPath: '/test/TestApp.xcodeproj',
78
+ analyzers: ['code'],
79
+ });
80
+
81
+ expect(report.summary).toBeDefined();
82
+ expect(typeof report.summary.totalIssues).toBe('number');
83
+ expect(typeof report.summary.errors).toBe('number');
84
+ expect(typeof report.summary.warnings).toBe('number');
85
+ expect(typeof report.summary.info).toBe('number');
86
+ expect(typeof report.summary.passed).toBe('boolean');
87
+ expect(typeof report.summary.duration).toBe('number');
88
+ });
89
+
90
+ it('should report progress when callback provided', async () => {
91
+ const events: ProgressEvent[] = [];
92
+ await runAnalysis(
93
+ { projectPath: '/test/TestApp.xcodeproj', analyzers: ['code'] },
94
+ { onProgress: (e: ProgressEvent) => events.push(e) }
95
+ );
96
+
97
+ const types = events.map((e) => e.type);
98
+ expect(types).toContain('scan:start');
99
+ expect(types).toContain('analyzer:start');
100
+ expect(types).toContain('analyzer:complete');
101
+ expect(types).toContain('scan:complete');
102
+ });
103
+
104
+ it('should handle analyzer failures gracefully', async () => {
105
+ const report = await runAnalysis({
106
+ projectPath: '/test/TestApp.xcodeproj',
107
+ });
108
+
109
+ expect(report).toBeDefined();
110
+ expect(report.summary).toBeDefined();
111
+ });
112
+
113
+ it('should enrich report with score', async () => {
114
+ const report = await runAnalysis({
115
+ projectPath: '/test/TestApp.xcodeproj',
116
+ analyzers: ['code'],
117
+ });
118
+
119
+ expect(report.score).toBeDefined();
120
+ expect(typeof report.score).toBe('number');
121
+ });
122
+
123
+ it('should set passed to true when no errors', async () => {
124
+ const report = await runAnalysis({
125
+ projectPath: '/test/TestApp.xcodeproj',
126
+ analyzers: ['code'],
127
+ });
128
+
129
+ // With mocked empty project, code scanner should find no issues
130
+ expect(report.summary.passed).toBe(true);
131
+ });
132
+
133
+ it('should include timestamp', async () => {
134
+ const report = await runAnalysis({
135
+ projectPath: '/test/TestApp.xcodeproj',
136
+ analyzers: ['code'],
137
+ });
138
+
139
+ expect(report.timestamp).toBeDefined();
140
+ expect(typeof report.timestamp).toBe('string');
141
+ });
142
+ });