ios-agent-mcp 2.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/LICENSE +21 -0
- package/README.md +183 -0
- package/dist/analyzers/appstore.d.ts +17 -0
- package/dist/analyzers/appstore.js +78 -0
- package/dist/analyzers/appstore.js.map +1 -0
- package/dist/analyzers/architecture.d.ts +2 -0
- package/dist/analyzers/architecture.js +63 -0
- package/dist/analyzers/architecture.js.map +1 -0
- package/dist/analyzers/availability.d.ts +2 -0
- package/dist/analyzers/availability.js +96 -0
- package/dist/analyzers/availability.js.map +1 -0
- package/dist/analyzers/concurrency.d.ts +8 -0
- package/dist/analyzers/concurrency.js +75 -0
- package/dist/analyzers/concurrency.js.map +1 -0
- package/dist/analyzers/swiftui.d.ts +2 -0
- package/dist/analyzers/swiftui.js +67 -0
- package/dist/analyzers/swiftui.js.map +1 -0
- package/dist/analyzers/types.d.ts +41 -0
- package/dist/analyzers/types.js +47 -0
- package/dist/analyzers/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +238 -0
- package/dist/index.js.map +1 -0
- package/dist/report.d.ts +11 -0
- package/dist/report.js +41 -0
- package/dist/report.js.map +1 -0
- package/dist/scan.d.ts +25 -0
- package/dist/scan.js +153 -0
- package/dist/scan.js.map +1 -0
- package/mcp.json +35 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nagarjuna Reddy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# ios-agent-mcp
|
|
2
|
+
|
|
3
|
+
An MCP server that reviews Swift projects against the rules in
|
|
4
|
+
[ios-agent-skill](https://github.com/Nagarjuna2997/ios-agent-skill).
|
|
5
|
+
|
|
6
|
+
The skill teaches an agent how to *write* iOS code. This server lets an agent
|
|
7
|
+
*check* it β six tools that read a Swift project and report defects with a file,
|
|
8
|
+
a line, the consequence, and the fix.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
You: Review my Swift project for concurrency problems.
|
|
12
|
+
|
|
13
|
+
Claude β review_swift_concurrency
|
|
14
|
+
|
|
15
|
+
π΄ Sources/FeedModel.swift:3 β @Observable type is not @MainActor-isolated.
|
|
16
|
+
Why: @Observable grants no isolation. SwiftUI reads this state during layout
|
|
17
|
+
while any task may write it β a data race under Swift 5 mode, a compile
|
|
18
|
+
error under Swift 6.
|
|
19
|
+
Fix: Annotate the type: `@MainActor @Observable final class β¦`
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
### Claude Code
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
claude mcp add ios-agent -- npx -y ios-agent-mcp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Claude Desktop
|
|
33
|
+
|
|
34
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"ios-agent": {
|
|
40
|
+
"command": "npx",
|
|
41
|
+
"args": ["-y", "ios-agent-mcp"]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Cursor
|
|
48
|
+
|
|
49
|
+
`.cursor/mcp.json` in your project, or `~/.cursor/mcp.json` globally:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"ios-agent": {
|
|
55
|
+
"command": "npx",
|
|
56
|
+
"args": ["-y", "ios-agent-mcp"]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### From source
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/Nagarjuna2997/ios-agent-skill.git
|
|
66
|
+
cd ios-agent-skill/mcp-server
|
|
67
|
+
npm install && npm run build
|
|
68
|
+
# then point your client at: node /absolute/path/to/mcp-server/dist/index.js
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Tools
|
|
74
|
+
|
|
75
|
+
| Tool | Finds |
|
|
76
|
+
|------|-------|
|
|
77
|
+
| `analyze_swift_project` | Structure β file counts, deployment target, frameworks, tests β plus finding counts per category. **Start here.** |
|
|
78
|
+
| `review_swift_concurrency` | `@Observable` without `@MainActor`, `Task.detached`, `DispatchQueue.main.async`, `@unchecked Sendable`, `nonisolated(unsafe)`, unstructured `Task` in `onAppear`, empty `catch`, a type named `Task` |
|
|
79
|
+
| `review_swift_architecture` | Live-implementation default arguments, presentation naming `URLSession`/`APIClient`/`ModelContext`, singletons in view models, domain importing SwiftUI, nested `NavigationStack`, `NavigationView` |
|
|
80
|
+
| `review_swiftui` | Fixed font sizes and heights, `AnyView`, `.cornerRadius`, literal spacing, materials over solid backgrounds, view state on models, `ObservableObject`, `@EnvironmentObject`, `try!` |
|
|
81
|
+
| `check_availability_guards` | Missing guards, **over-restrictive guards** (an iOS 26 API guarded at iOS 27 silently drops every iOS 26 device), Foundation Models without a runtime availability check |
|
|
82
|
+
| `audit_app_store_readiness` | Permission frameworks with no Info.plist purpose string, missing `PrivacyInfo.xcprivacy`, unlocalized strings, unlabeled icon buttons, `print()` |
|
|
83
|
+
|
|
84
|
+
Every tool takes one argument:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{ "path": "/absolute/path/to/your/project" }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## What it does and does not do
|
|
93
|
+
|
|
94
|
+
**Does:** static analysis. Reads `.swift`, `Info.plist`, `Package.swift`, and
|
|
95
|
+
`project.pbxproj` under the path you give it. No network, no writes.
|
|
96
|
+
|
|
97
|
+
**Does not:** prove your app builds or behaves correctly. Run `swift build` and
|
|
98
|
+
`swift test` for that β the tools say so in their own output.
|
|
99
|
+
|
|
100
|
+
Findings are graded so you can triage:
|
|
101
|
+
|
|
102
|
+
| | Meaning |
|
|
103
|
+
|---|---|
|
|
104
|
+
| π΄ **blocker** | Crashes, data races, or App Review rejection |
|
|
105
|
+
| π **serious** | Real defect β untestable code, accessibility failure, deprecated API |
|
|
106
|
+
| π‘ **minor** | Maintainability and consistency |
|
|
107
|
+
|
|
108
|
+
Test, mock, stub, and preview files are exempt from the app-code-only rules, and
|
|
109
|
+
`Package.swift` is skipped β they legitimately do things app code must not.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npm install
|
|
117
|
+
npm run build # tsc
|
|
118
|
+
npm test # 38 tests: unit + end-to-end over real MCP stdio
|
|
119
|
+
npm run typecheck
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Analyzers are pure functions of `(path, content) β Finding[]`, so they are
|
|
123
|
+
tested without the MCP transport. `test/server.smoke.test.js` launches the real
|
|
124
|
+
server and speaks the real protocol, because unit tests cannot tell you whether
|
|
125
|
+
the server actually starts.
|
|
126
|
+
|
|
127
|
+
To add a rule: write the analyzer, then a test that **fails without the rule**.
|
|
128
|
+
A test that passes either way is not a test.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Publishing (maintainers)
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
cd mcp-server
|
|
136
|
+
npm install # REQUIRED FIRST β see below
|
|
137
|
+
npm login
|
|
138
|
+
npm publish
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**`npm install` is not optional.** `prepublishOnly` runs `npm run build && npm test`, and `build` is `tsc`. On a fresh clone there is no `node_modules`, so the compiler is not present and publish fails with:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
error TS2591: Cannot find name 'node:fs/promises'. Do you need to install
|
|
145
|
+
type definitions for node? Try `npm i --save-dev @types/node`
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
That is the guard working as intended β it refuses to publish an unbuilt package β but the fix is `npm install`, not disabling the hook.
|
|
149
|
+
|
|
150
|
+
After publishing, verify:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
npm view ios-agent-mcp version # registry has it
|
|
154
|
+
npx -y ios-agent-mcp --version # 1.0.0
|
|
155
|
+
npx -y ios-agent-mcp --help # usage, tool list, setup commands
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`--help` and `--version` print and exit. Every other invocation starts the
|
|
159
|
+
stdio server and blocks waiting for a client, which is correct but looks like a
|
|
160
|
+
hang if you run it by hand.
|
|
161
|
+
|
|
162
|
+
To see exactly what would ship before committing to it:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
npm pack --dry-run
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Expect ~31 files: `dist/`, `mcp.json`, `README.md`, `LICENSE`, `package.json`. If `dist/` is missing, the build did not run.
|
|
169
|
+
|
|
170
|
+
### Version numbering
|
|
171
|
+
|
|
172
|
+
The npm package version and the repository version are **independent**:
|
|
173
|
+
|
|
174
|
+
| | Version | Why |
|
|
175
|
+
|---|---|---|
|
|
176
|
+
| `ios-agent-mcp` on npm | `1.0.0` | First release of this package |
|
|
177
|
+
| `ios-agent-skill` repo / `SKILL.md` | `2.0.0` | Its own release history |
|
|
178
|
+
|
|
179
|
+
This is not a mismatch. Bump the npm version only when the server changes.
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT β see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Finding, SourceFile } from "./types.js";
|
|
2
|
+
export interface ProjectContext {
|
|
3
|
+
/** Contents of Info.plist files found in the project, concatenated. */
|
|
4
|
+
infoPlist: string;
|
|
5
|
+
/** Whether a PrivacyInfo.xcprivacy exists anywhere. */
|
|
6
|
+
hasPrivacyManifest: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Whether this looks like a shippable app rather than a library.
|
|
9
|
+
*
|
|
10
|
+
* An SPM library has no Info.plist and is never submitted to App Review, so
|
|
11
|
+
* App-Store-only rules must not fire on one.
|
|
12
|
+
*/
|
|
13
|
+
isApp: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function analyzeAppStore(file: SourceFile, context: ProjectContext): Finding[];
|
|
16
|
+
/** Project-level checks that are not tied to a single source file. */
|
|
17
|
+
export declare function analyzeProjectLevelAppStore(context: ProjectContext): Finding[];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { eachLine, isSupportFile } from "./types.js";
|
|
2
|
+
const DOC = "checklists/app-store-submission.md";
|
|
3
|
+
/** Frameworks whose use requires a purpose string in Info.plist. */
|
|
4
|
+
const PERMISSION_FRAMEWORKS = [
|
|
5
|
+
{ pattern: /\bCLLocationManager\b|import\s+CoreLocation/, key: "NSLocationWhenInUseUsageDescription", what: "location" },
|
|
6
|
+
{ pattern: /\bAVCaptureDevice\b|import\s+AVFoundation/, key: "NSCameraUsageDescription", what: "camera" },
|
|
7
|
+
{ pattern: /\bPHPhotoLibrary\b|import\s+Photos\b/, key: "NSPhotoLibraryUsageDescription", what: "the photo library" },
|
|
8
|
+
{ pattern: /\bHKHealthStore\b|import\s+HealthKit/, key: "NSHealthShareUsageDescription", what: "health data" },
|
|
9
|
+
{ pattern: /\bCNContactStore\b|import\s+Contacts\b/, key: "NSContactsUsageDescription", what: "contacts" },
|
|
10
|
+
{ pattern: /\bCBCentralManager\b|import\s+CoreBluetooth/, key: "NSBluetoothAlwaysUsageDescription", what: "Bluetooth" },
|
|
11
|
+
{ pattern: /\bSFSpeechRecognizer\b|import\s+Speech\b/, key: "NSSpeechRecognitionUsageDescription", what: "speech recognition" },
|
|
12
|
+
{ pattern: /\bEKEventStore\b|import\s+EventKit/, key: "NSCalendarsUsageDescription", what: "calendars" },
|
|
13
|
+
];
|
|
14
|
+
export function analyzeAppStore(file, context) {
|
|
15
|
+
const findings = [];
|
|
16
|
+
if (isSupportFile(file.path))
|
|
17
|
+
return findings;
|
|
18
|
+
const push = (line, excerpt, rule, severity, message, consequence, fix, doc = DOC) => findings.push({
|
|
19
|
+
file: file.path,
|
|
20
|
+
line,
|
|
21
|
+
severity,
|
|
22
|
+
rule,
|
|
23
|
+
message,
|
|
24
|
+
consequence,
|
|
25
|
+
fix,
|
|
26
|
+
doc,
|
|
27
|
+
excerpt: excerpt.trim(),
|
|
28
|
+
});
|
|
29
|
+
// Permission-gated framework used with no purpose string.
|
|
30
|
+
for (const framework of PERMISSION_FRAMEWORKS) {
|
|
31
|
+
if (!framework.pattern.test(file.content))
|
|
32
|
+
continue;
|
|
33
|
+
if (context.infoPlist.includes(framework.key))
|
|
34
|
+
continue;
|
|
35
|
+
const line = file.content.split("\n").findIndex((l) => framework.pattern.test(l)) + 1;
|
|
36
|
+
push(Math.max(line, 1), framework.key, "missing-purpose-string", "blocker", `Uses ${framework.what} but Info.plist has no ${framework.key}.`, "iOS terminates the app the moment the permission is requested, and App Review rejects the submission.", `Add ${framework.key} to Info.plist with a specific sentence explaining why the app needs ${framework.what}.`);
|
|
37
|
+
}
|
|
38
|
+
eachLine(file, (line, number) => {
|
|
39
|
+
// Hardcoded user-facing strings.
|
|
40
|
+
const text = /\bText\(\s*"([^"]{4,})"\s*\)/.exec(line);
|
|
41
|
+
if (text && !/String\(localized:/.test(line) && !/LocalizedStringKey/.test(line)) {
|
|
42
|
+
push(number, line, "hardcoded-string", "minor", "User-facing string is not localized.", "The string cannot be translated, and a screen reader announces it in the wrong language.", 'Use `String(localized: "β¦", comment: "β¦")` or a String Catalog key.', "docs/design/interaction-standards.md");
|
|
43
|
+
}
|
|
44
|
+
// Icon-only button with no accessibility label.
|
|
45
|
+
if (/Button\s*\{/.test(line) &&
|
|
46
|
+
/Image\(\s*systemName:/.test(line) &&
|
|
47
|
+
!/accessibilityLabel|Label\(/.test(line)) {
|
|
48
|
+
push(number, line, "unlabeled-icon-button", "serious", "Icon-only button has no accessibility label.", 'VoiceOver announces it as just "button", making the control unusable without sight.', "Use `Label(\"β¦\", systemImage:)` with `.labelStyle(.iconOnly)`, or add `.accessibilityLabel(β¦)`.", "docs/frameworks/accessibility.md");
|
|
49
|
+
}
|
|
50
|
+
// print() is not structured logging and ships in release.
|
|
51
|
+
if (/^\s*print\s*\(/.test(line)) {
|
|
52
|
+
push(number, line, "print-logging", "minor", "print() used for diagnostics.", "Not structured, not filterable, not redacted, and not stripped from release builds.", "Use `Logger` from OSLog.", "docs/frameworks/oslog.md");
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return findings;
|
|
56
|
+
}
|
|
57
|
+
/** Project-level checks that are not tied to a single source file. */
|
|
58
|
+
export function analyzeProjectLevelAppStore(context) {
|
|
59
|
+
// A library is not submitted to App Review β this rule does not apply.
|
|
60
|
+
if (!context.isApp)
|
|
61
|
+
return [];
|
|
62
|
+
if (context.hasPrivacyManifest)
|
|
63
|
+
return [];
|
|
64
|
+
return [
|
|
65
|
+
{
|
|
66
|
+
file: "PrivacyInfo.xcprivacy",
|
|
67
|
+
line: 1,
|
|
68
|
+
severity: "blocker",
|
|
69
|
+
rule: "missing-privacy-manifest",
|
|
70
|
+
message: "No PrivacyInfo.xcprivacy found in the project.",
|
|
71
|
+
consequence: "App Store Connect rejects submissions that use required-reason APIs without a privacy manifest.",
|
|
72
|
+
fix: "Add a PrivacyInfo.xcprivacy declaring collected data types and required-reason API usage.",
|
|
73
|
+
doc: "docs/design/interaction-standards.md",
|
|
74
|
+
excerpt: "(project-level)",
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=appstore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appstore.js","sourceRoot":"","sources":["../../src/analyzers/appstore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { eachLine, isSupportFile } from "./types.js";
|
|
2
|
+
const DOC = "patterns/clean-architecture.md";
|
|
3
|
+
/** Concrete data-layer types that must not appear in the presentation layer. */
|
|
4
|
+
const DATA_LAYER_TYPES = /\b(URLSession|APIClient|ModelContext|NSManagedObjectContext|NSPersistentContainer)\b/;
|
|
5
|
+
/** Heuristic: does this path look like presentation-layer code? */
|
|
6
|
+
function isPresentationLayer(path) {
|
|
7
|
+
return /(Views?|Presentation|Screens?|UI)\//i.test(path) ||
|
|
8
|
+
/(View|ViewModel|Screen)\.swift$/.test(path);
|
|
9
|
+
}
|
|
10
|
+
/** Heuristic: does this path look like domain-layer code? */
|
|
11
|
+
function isDomainLayer(path) {
|
|
12
|
+
return /(Domain|Entities|UseCases)\//i.test(path);
|
|
13
|
+
}
|
|
14
|
+
export function analyzeArchitecture(file) {
|
|
15
|
+
const findings = [];
|
|
16
|
+
const support = isSupportFile(file.path);
|
|
17
|
+
if (support)
|
|
18
|
+
return findings;
|
|
19
|
+
const push = (line, excerpt, rule, severity, message, consequence, fix, doc = DOC) => findings.push({
|
|
20
|
+
file: file.path,
|
|
21
|
+
line,
|
|
22
|
+
severity,
|
|
23
|
+
rule,
|
|
24
|
+
message,
|
|
25
|
+
consequence,
|
|
26
|
+
fix,
|
|
27
|
+
doc,
|
|
28
|
+
excerpt: excerpt.trim(),
|
|
29
|
+
});
|
|
30
|
+
eachLine(file, (line, number) => {
|
|
31
|
+
// A dependency defaulting to a live implementation.
|
|
32
|
+
if (/\binit\s*\(/.test(line) &&
|
|
33
|
+
/:\s*(any\s+)?\w+\s*=\s*(Live|Remote|Default|URLSession|\w*APIClient)\w*\s*[.(]/.test(line)) {
|
|
34
|
+
push(number, line, "live-default-dependency", "blocker", "Initializer parameter defaults to a live implementation.", "Any call site that forgets to inject silently hits the real network or disk, including tests and previews, with no signal that it happened.", "Make the parameter required. The composition root supplies it.");
|
|
35
|
+
}
|
|
36
|
+
// Presentation naming concrete data-layer types.
|
|
37
|
+
if (isPresentationLayer(file.path) && DATA_LAYER_TYPES.test(line) && !/^\s*import\b/.test(line)) {
|
|
38
|
+
push(number, line, "presentation-names-data-type", "serious", "Presentation layer names a concrete data-layer type.", "The screen cannot be unit-tested or previewed without a real client, so previews need a network and tests become integration tests.", "Depend on a protocol declared in the domain layer, injected through `init`.");
|
|
39
|
+
}
|
|
40
|
+
// Singleton resolution inside a view model.
|
|
41
|
+
if (/\bViewModel\b/.test(file.path) || /class\s+\w*(ViewModel|Model)\b/.test(file.content)) {
|
|
42
|
+
if (/=\s*\w+\s*\.\s*shared\b/.test(line)) {
|
|
43
|
+
push(number, line, "singleton-in-viewmodel", "serious", "View model resolves a dependency from a global singleton.", "Not injectable, not overridable in previews, and shared mutable state leaks between parallel tests.", "Inject the dependency through `init`, from a composition root.");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Domain layer importing UI frameworks.
|
|
47
|
+
if (isDomainLayer(file.path) && /^\s*import\s+(SwiftUI|UIKit|AppKit)\b/.test(line)) {
|
|
48
|
+
push(number, line, "domain-imports-ui", "blocker", "Domain layer imports a UI framework.", "The dependency rule is inverted: business logic can no longer be tested, reused on another platform, or extracted into a package.", "Remove the import. Move anything genuinely UI-shaped into the presentation layer.");
|
|
49
|
+
}
|
|
50
|
+
// A screen owning its own NavigationStack.
|
|
51
|
+
if (/NavigationStack\s*[({]/.test(line) && !/^\s*(\/\/|\*)/.test(line)) {
|
|
52
|
+
if (/Detail|Row|Cell|Section/.test(file.path)) {
|
|
53
|
+
push(number, line, "nested-navigation-stack", "serious", "A pushed screen appears to own its own NavigationStack.", "A nested stack renders a second navigation bar and breaks programmatic navigation.", "Only the root of each tab owns a NavigationStack.", "docs/swiftui/deep-linking-and-routing.md");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Deprecated navigation API.
|
|
57
|
+
if (/\bNavigationView\s*\{/.test(line)) {
|
|
58
|
+
push(number, line, "deprecated-navigationview", "serious", "NavigationView is deprecated.", "Programmatic navigation and deep linking do not work reliably, and behavior differs across OS versions.", "Use `NavigationStack` with `NavigationPath` and `.navigationDestination(for:)`.", "docs/swiftui/navigation.md");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return findings;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=architecture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"architecture.js","sourceRoot":"","sources":["../../src/analyzers/architecture.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { eachLine } from "./types.js";
|
|
2
|
+
const DOC = "docs/compatibility-matrix.md";
|
|
3
|
+
/**
|
|
4
|
+
* Symbols and the iOS version in which they were INTRODUCED.
|
|
5
|
+
*
|
|
6
|
+
* The rule this enforces: guard on the version where a symbol became available,
|
|
7
|
+
* never on the newest SDK you happen to be building with. Writing
|
|
8
|
+
* `#available(iOS 27, *)` around an iOS 26 API silently drops every iOS 26
|
|
9
|
+
* device to the fallback path β invisible when testing on a current device.
|
|
10
|
+
*/
|
|
11
|
+
const INTRODUCED = [
|
|
12
|
+
// iOS 26
|
|
13
|
+
{ pattern: /\.glassEffect\s*\(/, version: 26, name: "glassEffect" },
|
|
14
|
+
{ pattern: /\bGlassEffectContainer\b/, version: 26, name: "GlassEffectContainer" },
|
|
15
|
+
{ pattern: /\.glassEffectID\s*\(/, version: 26, name: "glassEffectID" },
|
|
16
|
+
{ pattern: /\.buttonStyle\(\s*\.glass(Prominent)?\s*\)/, version: 26, name: "glass button style" },
|
|
17
|
+
{ pattern: /\bSystemLanguageModel\b/, version: 26, name: "SystemLanguageModel" },
|
|
18
|
+
{ pattern: /\bLanguageModelSession\b/, version: 26, name: "LanguageModelSession" },
|
|
19
|
+
{ pattern: /@Generable\b/, version: 26, name: "@Generable" },
|
|
20
|
+
// iOS 27
|
|
21
|
+
{ pattern: /\bPrivateCloudComputeLanguageModel\b/, version: 27, name: "PrivateCloudComputeLanguageModel" },
|
|
22
|
+
{ pattern: /\bDynamicProfile\b/, version: 27, name: "DynamicProfile" },
|
|
23
|
+
{ pattern: /\bOCRTool\b|\bBarcodeReaderTool\b/, version: 27, name: "built-in system tools" },
|
|
24
|
+
// iOS 18
|
|
25
|
+
{ pattern: /\bMeshGradient\b/, version: 18, name: "MeshGradient" },
|
|
26
|
+
{ pattern: /\bTextRenderer\b/, version: 18, name: "TextRenderer" },
|
|
27
|
+
];
|
|
28
|
+
/** Highest iOS version mentioned in an availability guard on or above a line. */
|
|
29
|
+
function guardedVersions(content) {
|
|
30
|
+
const versions = [];
|
|
31
|
+
const guard = /#available\(\s*iOS\s+(\d+)|@available\([^)]*iOS\s+(\d+)/g;
|
|
32
|
+
let match;
|
|
33
|
+
while ((match = guard.exec(content)) !== null) {
|
|
34
|
+
versions.push(Number(match[1] ?? match[2]));
|
|
35
|
+
}
|
|
36
|
+
return versions;
|
|
37
|
+
}
|
|
38
|
+
export function analyzeAvailability(file) {
|
|
39
|
+
const findings = [];
|
|
40
|
+
const guards = guardedVersions(file.content);
|
|
41
|
+
eachLine(file, (text, number) => {
|
|
42
|
+
for (const entry of INTRODUCED) {
|
|
43
|
+
if (!entry.pattern.test(text))
|
|
44
|
+
continue;
|
|
45
|
+
// Unguarded use of a version-gated symbol.
|
|
46
|
+
if (guards.length === 0) {
|
|
47
|
+
findings.push({
|
|
48
|
+
file: file.path,
|
|
49
|
+
line: number,
|
|
50
|
+
severity: "blocker",
|
|
51
|
+
rule: "missing-availability-guard",
|
|
52
|
+
message: `${entry.name} requires iOS ${entry.version} but the file has no availability guard.`,
|
|
53
|
+
consequence: "The app fails to compile against an older deployment target, or crashes on launch if the symbol is weakly linked.",
|
|
54
|
+
fix: `Wrap in \`if #available(iOS ${entry.version}.0, *)\` or annotate with \`@available(iOS ${entry.version}.0, *)\`, and provide a fallback.`,
|
|
55
|
+
doc: DOC,
|
|
56
|
+
excerpt: text.trim(),
|
|
57
|
+
});
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// Over-guarded: guarded at a HIGHER version than the symbol needs.
|
|
61
|
+
const tightest = Math.min(...guards);
|
|
62
|
+
if (tightest > entry.version) {
|
|
63
|
+
findings.push({
|
|
64
|
+
file: file.path,
|
|
65
|
+
line: number,
|
|
66
|
+
severity: "serious",
|
|
67
|
+
rule: "over-restrictive-guard",
|
|
68
|
+
message: `${entry.name} was introduced in iOS ${entry.version} but is guarded at iOS ${tightest}.`,
|
|
69
|
+
consequence: `Every device on iOS ${entry.version}β${tightest - 1} falls back unnecessarily, losing the feature for a large installed base. This is invisible when testing on a current device.`,
|
|
70
|
+
fix: `Guard on iOS ${entry.version}, the version where the symbol was introduced β not the newest SDK.`,
|
|
71
|
+
doc: DOC,
|
|
72
|
+
excerpt: text.trim(),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
// Foundation Models used without the RUNTIME availability check.
|
|
78
|
+
const usesFoundationModels = /\bLanguageModelSession\s*\(/.test(file.content);
|
|
79
|
+
const hasRuntimeCheck = /SystemLanguageModel[^\n]*\.availability|case\s+\.available/.test(file.content);
|
|
80
|
+
if (usesFoundationModels && !hasRuntimeCheck) {
|
|
81
|
+
const line = file.content.split("\n").findIndex((l) => /\bLanguageModelSession\s*\(/.test(l)) + 1;
|
|
82
|
+
findings.push({
|
|
83
|
+
file: file.path,
|
|
84
|
+
line,
|
|
85
|
+
severity: "blocker",
|
|
86
|
+
rule: "missing-runtime-model-check",
|
|
87
|
+
message: "Foundation Models used without a runtime availability check.",
|
|
88
|
+
consequence: "An @available guard proves the symbol exists; it does not prove the model is usable on this device, in this region, with Apple Intelligence enabled. The feature fails at tap time.",
|
|
89
|
+
fix: "Check `SystemLanguageModel.default.availability` and show a real unavailable state before the entry point renders.",
|
|
90
|
+
doc: "docs/frameworks/foundation-models.md",
|
|
91
|
+
excerpt: "LanguageModelSession(...)",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return findings;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=availability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"availability.js","sourceRoot":"","sources":["../../src/analyzers/availability.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Finding, SourceFile } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Swift 6 isolation and concurrency rules.
|
|
4
|
+
*
|
|
5
|
+
* These are the same rules enforced by templates/hooks/forbid-antipatterns.sh β
|
|
6
|
+
* this is that engine made addressable by an agent.
|
|
7
|
+
*/
|
|
8
|
+
export declare function analyzeConcurrency(file: SourceFile): Finding[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { eachLine, isSupportFile } from "./types.js";
|
|
2
|
+
const DOC = "docs/swift/swift-concurrency.md";
|
|
3
|
+
/**
|
|
4
|
+
* Swift 6 isolation and concurrency rules.
|
|
5
|
+
*
|
|
6
|
+
* These are the same rules enforced by templates/hooks/forbid-antipatterns.sh β
|
|
7
|
+
* this is that engine made addressable by an agent.
|
|
8
|
+
*/
|
|
9
|
+
export function analyzeConcurrency(file) {
|
|
10
|
+
const findings = [];
|
|
11
|
+
const support = isSupportFile(file.path);
|
|
12
|
+
const push = (line, excerpt, rule, severity, message, consequence, fix, doc = DOC) => findings.push({
|
|
13
|
+
file: file.path,
|
|
14
|
+
line,
|
|
15
|
+
severity,
|
|
16
|
+
rule,
|
|
17
|
+
message,
|
|
18
|
+
consequence,
|
|
19
|
+
fix,
|
|
20
|
+
doc,
|
|
21
|
+
excerpt: excerpt.trim(),
|
|
22
|
+
});
|
|
23
|
+
// File-level: @Observable without @MainActor anywhere in the file.
|
|
24
|
+
// Checked at file scope because the annotations may be on separate lines.
|
|
25
|
+
const hasObservable = /^\s*@Observable\b/m.test(file.content);
|
|
26
|
+
const hasMainActor = /@MainActor\b/.test(file.content);
|
|
27
|
+
if (hasObservable && !hasMainActor && !support) {
|
|
28
|
+
const line = file.content.split("\n").findIndex((l) => /^\s*@Observable\b/.test(l)) + 1;
|
|
29
|
+
push(line, "@Observable", "observable-without-mainactor", "blocker", "@Observable type is not @MainActor-isolated.", "@Observable grants no isolation. SwiftUI reads this state during layout while any task may write it β a data race under Swift 5 mode, a compile error under Swift 6.", "Annotate the type: `@MainActor @Observable final class β¦`. Annotate the type, not individual members β per-member isolation leaves gaps.");
|
|
30
|
+
}
|
|
31
|
+
// Non-final observable classes.
|
|
32
|
+
const nonFinal = file.content
|
|
33
|
+
.split("\n")
|
|
34
|
+
.findIndex((l) => /^\s*(public\s+)?class\s+\w+/.test(l) && !/final/.test(l));
|
|
35
|
+
if (hasObservable && nonFinal >= 0) {
|
|
36
|
+
push(nonFinal + 1, file.content.split("\n")[nonFinal], "observable-not-final", "minor", "@Observable class is not `final`.", "A subclass can add unobserved stored properties, and every access costs a dynamic dispatch.", "Mark the class `final`.");
|
|
37
|
+
}
|
|
38
|
+
eachLine(file, (line, number) => {
|
|
39
|
+
if (/\bDispatchQueue\s*\.\s*main\s*\.\s*async\b/.test(line)) {
|
|
40
|
+
push(number, line, "dispatchqueue-main-async", "serious", "DispatchQueue.main.async in Swift Concurrency code.", "Hand-rolled thread hopping bypasses the isolation the compiler can check, and defers the write by a run-loop turn.", "Isolate the enclosing type with @MainActor and assign directly.");
|
|
41
|
+
}
|
|
42
|
+
if (/\bTask\s*\.\s*detached\b/.test(line)) {
|
|
43
|
+
push(number, line, "task-detached", "serious", "Task.detached drops actor isolation, priority, and task-locals.", "Writes to isolated state from the detached task are cross-actor: a data race under Swift 5, a compile error under Swift 6.", "Use `Task { }` (which inherits the enclosing actor), a `nonisolated async` function, or an actor.");
|
|
44
|
+
}
|
|
45
|
+
if (/\bawait\s+MainActor\s*\.\s*run\b/.test(line)) {
|
|
46
|
+
push(number, line, "redundant-mainactor-run", "minor", "await MainActor.run inside an already-isolated type is redundant.", "Adds a suspension point and obscures that the type is already main-actor isolated.", "Assign directly if the enclosing type is @MainActor; otherwise isolate the type.");
|
|
47
|
+
}
|
|
48
|
+
if (/@unchecked\s+Sendable/.test(line)) {
|
|
49
|
+
push(number, line, "unchecked-sendable", "serious", "@unchecked Sendable asserts thread safety the compiler cannot verify.", "If there is no lock, actor, or documented single-threaded contract behind it, this is a race the type system was trying to prevent.", "Prefer a value type, an immutable `final class`, an `actor`, or Swift 6.4's `weak let` / `~Sendable`. If genuinely needed, add a comment naming the mechanism that protects it.");
|
|
50
|
+
}
|
|
51
|
+
if (/\bnonisolated\(unsafe\)/.test(line)) {
|
|
52
|
+
push(number, line, "nonisolated-unsafe", "minor", "nonisolated(unsafe) opts storage out of isolation checking.", "Nothing prevents concurrent access; correctness rests entirely on an unstated invariant.", "Add a comment naming what protects it, or move the state into an actor.");
|
|
53
|
+
}
|
|
54
|
+
// Unstructured Task in onAppear β outlives the view.
|
|
55
|
+
if (/\.onAppear\s*\{/.test(line) && /\bTask\s*\{/.test(line)) {
|
|
56
|
+
push(number, line, "task-in-onappear", "serious", "Unstructured Task in onAppear is never cancelled.", "The task keeps running after the view is dismissed and can write to a model whose screen is gone.", "Use `.task { }` or `.task(id:) { }`, which SwiftUI cancels on disappear.");
|
|
57
|
+
}
|
|
58
|
+
// A type named Task shadows _Concurrency.Task.
|
|
59
|
+
if (/^\s*(public\s+|internal\s+)?(struct|class|enum|actor)\s+Task\b/.test(line)) {
|
|
60
|
+
push(number, line, "type-named-task", "blocker", "A type named `Task` shadows _Concurrency.Task.", "`Task { β¦ }` in the same file fails to compile with a confusing 'extra trailing closure' error.", "Rename it (TodoItem, WorkItem, JobRecordβ¦).");
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
// Empty catch β swallows failures silently.
|
|
64
|
+
//
|
|
65
|
+
// Uses eachLine (which strips comments) rather than a raw regex over the whole
|
|
66
|
+
// file: a doc comment mentioning `catch { }` as an anti-pattern is not itself
|
|
67
|
+
// an anti-pattern.
|
|
68
|
+
eachLine(file, (line, number) => {
|
|
69
|
+
if (/catch\s*\{\s*\}/.test(line)) {
|
|
70
|
+
push(number, line, "empty-catch", "serious", "Empty catch block discards a failure silently.", "The operation fails with no log, no user feedback, and no way to diagnose it in production.", "Surface the error, or comment why the no-op is deliberate (e.g. `catch is CancellationError`).", "patterns/error-handling.md");
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return findings;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=concurrency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"concurrency.js","sourceRoot":"","sources":["../../src/analyzers/concurrency.ts"],"names":[],"mappings":"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"}
|