swift-code-reviewer-skill 1.3.0 → 1.4.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/CHANGELOG.md +45 -0
- package/CONTRIBUTING.md +86 -3
- package/README.md +315 -118
- package/bin/install.js +71 -90
- package/bin/lib/agents.js +134 -0
- package/bin/lib/prompt.js +58 -0
- package/core/swift-code-reviewer.core.md +281 -0
- package/examples/README.md +35 -0
- package/examples/claude-tca-review.md +140 -0
- package/examples/codex-async-algorithms-review.md +208 -0
- package/examples/gemini-isowords-review.md +207 -0
- package/package.json +6 -1
- package/templates/agents/claude/swift-code-reviewer.md +78 -0
- package/templates/agents/codex/swift-code-reviewer.md +211 -0
- package/templates/agents/gemini/swift-code-reviewer.md +211 -0
- package/templates/agents/kiro/swift-code-reviewer.md +218 -0
- package/templates/commands/claude/review.md +56 -0
- package/templates/commands/gemini/review.toml +15 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# Swift/SwiftUI Code Review Skill
|
|
2
|
+
|
|
3
|
+
Multi-layer review covering Swift 6+ concurrency, SwiftUI patterns, performance, security, architecture, and project-specific standards. Reads `<PROJECT_STANDARDS_FILE>` and outputs Critical/High/Medium/Low severity findings with `file:line` references and before/after code examples.
|
|
4
|
+
|
|
5
|
+
<!-- TOKEN LEGEND (for wrapper authors)
|
|
6
|
+
<PROJECT_STANDARDS_FILE> → agent-specific path to project standards
|
|
7
|
+
<COMPANION_REF:path/to/file.md> → resolves to skills/<path> for Claude;
|
|
8
|
+
inlined excerpts for other agents
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
## When to Use This Skill
|
|
12
|
+
|
|
13
|
+
- "Review this PR"
|
|
14
|
+
- "Review my code" / "Review my changes" / "Review uncommitted changes"
|
|
15
|
+
- "Code review for [component]"
|
|
16
|
+
- "Audit this codebase" / "Check code quality"
|
|
17
|
+
- "Review against <PROJECT_STANDARDS_FILE>" / "Check if this follows our coding standards"
|
|
18
|
+
- "Architecture review" / "Performance audit" / "Security review"
|
|
19
|
+
- "Review this PR against the spec"
|
|
20
|
+
- "Did the agent miss anything from issue #123?"
|
|
21
|
+
- "What rules am I missing in <PROJECT_STANDARDS_FILE> based on this PR?"
|
|
22
|
+
- "Review this AI-generated PR"
|
|
23
|
+
|
|
24
|
+
## Workflow
|
|
25
|
+
|
|
26
|
+
### Phase 1 — Context Gathering
|
|
27
|
+
|
|
28
|
+
1. **Read the Spec**
|
|
29
|
+
- For PRs: `gh pr view <num> --json title,body,closingIssuesReferences,labels`
|
|
30
|
+
- For linked issues: `gh issue view <num> --json title,body,labels`
|
|
31
|
+
- For MRs: `glab mr view <num>` and `glab issue view <num>`
|
|
32
|
+
- Extract:
|
|
33
|
+
- Stated goal / problem being solved
|
|
34
|
+
- Explicit acceptance criteria (look for checkboxes, "should", "must", "Given/When/Then")
|
|
35
|
+
- Edge cases or non-goals mentioned
|
|
36
|
+
- Out-of-scope items
|
|
37
|
+
- If no PR/issue context is available, note this and fall back to inferring intent from the diff.
|
|
38
|
+
2. Try to load `<PROJECT_STANDARDS_FILE>`.
|
|
39
|
+
- **If missing**: add a note to the report — _"No project standards file found — review uses default Apple guidelines"_ — then continue.
|
|
40
|
+
3. Obtain the changeset: `git diff`, `git diff --cached`, or `gh pr diff <n>`.
|
|
41
|
+
- **If diff is empty**: stop and ask the user to specify files, a PR number, or a directory.
|
|
42
|
+
4. Read each changed file plus key related files (imports, protocols it conforms to, corresponding test file if present).
|
|
43
|
+
|
|
44
|
+
### Phase 2 — Analysis
|
|
45
|
+
|
|
46
|
+
For each category, load the reference file before writing findings:
|
|
47
|
+
|
|
48
|
+
#### 0. Spec Adherence
|
|
49
|
+
|
|
50
|
+
Reference: `references/spec-adherence.md`
|
|
51
|
+
|
|
52
|
+
- **Requirement Coverage**
|
|
53
|
+
- Does each acceptance criterion map to a concrete code change?
|
|
54
|
+
- Are edge cases mentioned in the spec handled?
|
|
55
|
+
- Are tests covering the scenarios described?
|
|
56
|
+
- **Scope Discipline**
|
|
57
|
+
- Flag changes outside the stated scope (scope creep)
|
|
58
|
+
- Flag unrelated refactors bundled into the PR
|
|
59
|
+
- **Missing Work**
|
|
60
|
+
- TODOs, `fatalError("not implemented")`, empty function bodies
|
|
61
|
+
- Stubbed mocks that should be real implementations
|
|
62
|
+
- Acceptance criteria with no corresponding diff
|
|
63
|
+
- **Intent Drift**
|
|
64
|
+
- Code solves a _similar_ but different problem than stated
|
|
65
|
+
- Naming/structure suggests a different mental model than the spec
|
|
66
|
+
|
|
67
|
+
1. **Swift Quality** — concurrency, error handling, optionals, naming → `references/swift-quality-checklist.md`; for concurrency findings also read `<COMPANION_REF:swift-concurrency/references/sendable.md>` and `<COMPANION_REF:swift-concurrency/references/actors.md>`
|
|
68
|
+
2. **SwiftUI Patterns** — property wrappers, state management, deprecated APIs → `references/swiftui-review-checklist.md`; for wrapper selection read `<COMPANION_REF:swiftui-expert-skill/references/state-management.md>`
|
|
69
|
+
3. **Performance** — view body cost, ForEach identity, lazy loading, retain cycles → `references/performance-review.md`
|
|
70
|
+
4. **Security** — force unwraps, Keychain vs UserDefaults, input validation, no secrets in logs → `references/security-checklist.md`
|
|
71
|
+
5. **Architecture** — MVVM/MVI/TCA compliance, DI, testability → `references/architecture-patterns.md`
|
|
72
|
+
6. **Project Standards** — validate against `<PROJECT_STANDARDS_FILE>` rules → `references/custom-guidelines.md`
|
|
73
|
+
|
|
74
|
+
For test file findings, consult `<COMPANION_REF:swift-testing/references/test-organization.md>`.
|
|
75
|
+
For navigation/routing findings, consult `<COMPANION_REF:swiftui-ui-patterns/references/navigationstack.md>`.
|
|
76
|
+
|
|
77
|
+
### Phase 2.5 — Pattern Detection (for Agent Loop Feedback)
|
|
78
|
+
|
|
79
|
+
**Objective**: Identify recurring issues that point to gaps in the agent's
|
|
80
|
+
instructions, not just the code.
|
|
81
|
+
|
|
82
|
+
After collecting per-file findings, aggregate them:
|
|
83
|
+
|
|
84
|
+
1. Group findings by rule (e.g., "force-unwrap", "deprecated NavigationView",
|
|
85
|
+
"missing @MainActor on UI mutation").
|
|
86
|
+
2. Mark any rule that fires **≥2 times across the diff** as a recurring pattern.
|
|
87
|
+
3. For each recurring pattern, draft a one-line rule suitable for
|
|
88
|
+
`<PROJECT_STANDARDS_FILE>` or an agent system prompt — written as a directive,
|
|
89
|
+
not a description.
|
|
90
|
+
4. If the same recurring pattern appeared in past reviews (check git log of
|
|
91
|
+
`<PROJECT_STANDARDS_FILE>`), escalate priority — the existing rule isn't strong
|
|
92
|
+
enough or isn't being read.
|
|
93
|
+
|
|
94
|
+
Threshold rationale: one occurrence is a slip; two is a pattern; three+ means
|
|
95
|
+
the agent's instructions are silent on this and need an explicit rule.
|
|
96
|
+
|
|
97
|
+
Reference: `references/agent-loop-feedback.md`.
|
|
98
|
+
|
|
99
|
+
### Phase 3 — Report
|
|
100
|
+
|
|
101
|
+
Group findings by file → sort by severity within each file → write prioritized action items.
|
|
102
|
+
|
|
103
|
+
Severity: **Critical** (crash/data race/security hole) · **High** (anti-pattern/major arch violation) · **Medium** (quality/maintainability) · **Low** (style/suggestion).
|
|
104
|
+
|
|
105
|
+
Include one-sentence positive feedback where code is notably well-written. Never pad with generic praise.
|
|
106
|
+
|
|
107
|
+
## Concrete Finding Examples
|
|
108
|
+
|
|
109
|
+
### Force Unwrap → guard let (Critical)
|
|
110
|
+
|
|
111
|
+
**`LoginViewModel.swift:89`** — Current:
|
|
112
|
+
|
|
113
|
+
```swift
|
|
114
|
+
let user = repository.currentUser!
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Finding**: crashes if `currentUser` is `nil` (e.g., after sign-out race condition).
|
|
118
|
+
|
|
119
|
+
**Fix**:
|
|
120
|
+
|
|
121
|
+
```swift
|
|
122
|
+
guard let user = repository.currentUser else {
|
|
123
|
+
logger.error("currentUser nil — aborting login flow")
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### Missing @MainActor on UI-bound ViewModel (High)
|
|
131
|
+
|
|
132
|
+
**`FeedViewModel.swift:12`** — Current:
|
|
133
|
+
|
|
134
|
+
```swift
|
|
135
|
+
class FeedViewModel: ObservableObject {
|
|
136
|
+
@Published var posts: [Post] = []
|
|
137
|
+
|
|
138
|
+
func load() async {
|
|
139
|
+
posts = try? await api.fetchPosts() // ⚠️ mutates @Published off main thread
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Finding**: `@Published` mutations must happen on the main actor in Swift 6 strict concurrency; this is a data race.
|
|
145
|
+
|
|
146
|
+
**Fix**:
|
|
147
|
+
|
|
148
|
+
```swift
|
|
149
|
+
@MainActor
|
|
150
|
+
@Observable
|
|
151
|
+
final class FeedViewModel {
|
|
152
|
+
var posts: [Post] = []
|
|
153
|
+
|
|
154
|
+
func load() async throws {
|
|
155
|
+
posts = try await api.fetchPosts() // safe: whole class is @MainActor-isolated
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Also migrate from `ObservableObject`/`@Published` to `@Observable` (iOS 17+) — see `<COMPANION_REF:swiftui-expert-skill/references/state-management.md>`.
|
|
161
|
+
|
|
162
|
+
## Output Format
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
# Code Review — <scope>
|
|
166
|
+
|
|
167
|
+
## Summary
|
|
168
|
+
Files: N | Critical: N | High: N | Medium: N | Low: N
|
|
169
|
+
|
|
170
|
+
## Spec Adherence
|
|
171
|
+
|
|
172
|
+
**Source**: PR #123 / Issue #456
|
|
173
|
+
|
|
174
|
+
| Requirement | Status | Location |
|
|
175
|
+
|-------------|--------|----------|
|
|
176
|
+
| User can log in with email | ✅ Implemented | LoginView.swift:23 |
|
|
177
|
+
| Show error on invalid credentials | ⚠️ Partial — missing 401 case | LoginViewModel.swift:67 |
|
|
178
|
+
| Persist session in Keychain | ❌ Not implemented | — |
|
|
179
|
+
| Rate limit retries | ❌ Not implemented | — |
|
|
180
|
+
|
|
181
|
+
**Scope creep**: 1 unrelated change (UserSettings.swift refactor) — recommend
|
|
182
|
+
splitting into a separate PR.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## <Filename.swift>
|
|
187
|
+
|
|
188
|
+
[Severity] **<Category>** (line N)
|
|
189
|
+
Current: `<problematic snippet>`
|
|
190
|
+
Fix: <explanation + corrected snippet>
|
|
191
|
+
|
|
192
|
+
## Positive Observations
|
|
193
|
+
...
|
|
194
|
+
|
|
195
|
+
## Prioritized Action Items
|
|
196
|
+
- [Must fix] ...
|
|
197
|
+
- [Should fix] ...
|
|
198
|
+
- [Consider] ...
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Agent Loop Feedback
|
|
203
|
+
|
|
204
|
+
Recurring patterns suggest the following rules are missing or under-emphasized
|
|
205
|
+
in `<PROJECT_STANDARDS_FILE>`:
|
|
206
|
+
|
|
207
|
+
### Pattern: Force-unwraps (4 occurrences)
|
|
208
|
+
**Files**: LoginView.swift:89, NetworkService.swift:34, UserRepo.swift:12,78
|
|
209
|
+
|
|
210
|
+
**Suggested rule**:
|
|
211
|
+
> Never use `!`, `try!`, or `as!`. Use `guard let` with explicit early return,
|
|
212
|
+
> typed throws, or `as?` with handling. Force-unwraps are crashes waiting to happen.
|
|
213
|
+
|
|
214
|
+
### Pattern: Deprecated NavigationView (2 occurrences)
|
|
215
|
+
**Files**: ProfileView.swift:15, SettingsView.swift:22
|
|
216
|
+
|
|
217
|
+
**Suggested rule**:
|
|
218
|
+
> Use `NavigationStack` exclusively. `NavigationView` is deprecated as of iOS 16.
|
|
219
|
+
|
|
220
|
+
### Pattern: Business logic in View body (3 occurrences)
|
|
221
|
+
**Files**: LoginView.swift:45, ProfileView.swift:78, FeedView.swift:34
|
|
222
|
+
|
|
223
|
+
**Suggested rule**:
|
|
224
|
+
> Views must not contain business logic, network calls, or data transformations.
|
|
225
|
+
> Move all such work into the @Observable view model.
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Full templates and severity classification: `references/feedback-templates.md`.
|
|
229
|
+
|
|
230
|
+
## Companion Skills
|
|
231
|
+
|
|
232
|
+
Full reference tables (all files, when to consult each): `references/companion-skills.md`.
|
|
233
|
+
|
|
234
|
+
| Skill | Use for |
|
|
235
|
+
| --------------------------------------- | ---------------------------------------------------------- |
|
|
236
|
+
| `<COMPANION_REF:swiftui-expert-skill/>` | SwiftUI state, Liquid Glass, macOS patterns, accessibility |
|
|
237
|
+
| `<COMPANION_REF:swift-concurrency/>` | Actors, Sendable, Swift 6 migration, async/await |
|
|
238
|
+
| `<COMPANION_REF:swift-testing/>` | Swift Testing framework, test doubles, snapshots |
|
|
239
|
+
| `<COMPANION_REF:swift-expert/>` | Swift 6+ patterns, protocols, memory, SPM |
|
|
240
|
+
| `<COMPANION_REF:swiftui-ui-patterns/>` | Navigation, sheets, theming, async state, grids |
|
|
241
|
+
|
|
242
|
+
## Platform Commands
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
# GitHub PR
|
|
246
|
+
gh pr diff <n>
|
|
247
|
+
gh pr view <n> --json files,comments
|
|
248
|
+
|
|
249
|
+
# GitLab MR
|
|
250
|
+
glab mr diff <n>
|
|
251
|
+
glab mr view <n> --json
|
|
252
|
+
|
|
253
|
+
# Local changes
|
|
254
|
+
git diff # unstaged
|
|
255
|
+
git diff --cached # staged
|
|
256
|
+
git diff HEAD~1 # last commit
|
|
257
|
+
git diff -- path/to/file.swift
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Limitations
|
|
261
|
+
|
|
262
|
+
- Spec adherence checks require an accessible PR description or linked issue.
|
|
263
|
+
When reviewing local changes with no PR context, mark spec adherence as
|
|
264
|
+
"not assessed" rather than guessing intent.
|
|
265
|
+
- Agent loop feedback assumes the code was AI-generated or AI-assisted. For
|
|
266
|
+
fully human-written code, recurring patterns are still useful but should be
|
|
267
|
+
framed as team coding standards rather than agent instructions.
|
|
268
|
+
|
|
269
|
+
## Reference Files
|
|
270
|
+
|
|
271
|
+
- `references/review-workflow.md` — detailed process, diff parsing, git commands
|
|
272
|
+
- `references/feedback-templates.md` — output templates, severity classification
|
|
273
|
+
- `references/spec-adherence.md` — parsing PR/issue specs, requirement coverage tables, scope creep classification
|
|
274
|
+
- `references/agent-loop-feedback.md` — recurring-pattern threshold, directive phrasing, suggested-rule template
|
|
275
|
+
- `references/swift-quality-checklist.md` — Swift 6+, concurrency, optionals, naming
|
|
276
|
+
- `references/swiftui-review-checklist.md` — property wrappers, state, modern APIs
|
|
277
|
+
- `references/performance-review.md` — view optimization, ForEach, resource management
|
|
278
|
+
- `references/security-checklist.md` — input validation, Keychain, network security
|
|
279
|
+
- `references/architecture-patterns.md` — MVVM/MVI/TCA, DI, testability
|
|
280
|
+
- `references/custom-guidelines.md` — parsing `<PROJECT_STANDARDS_FILE>`
|
|
281
|
+
- `references/companion-skills.md` — full companion skill tables
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Review Examples
|
|
2
|
+
|
|
3
|
+
Three representative review reports generated by the swift-code-reviewer-skill against real
|
|
4
|
+
open-source Swift projects. Each report demonstrates a different agent target and is cited by
|
|
5
|
+
commit SHA to ensure reproducibility.
|
|
6
|
+
|
|
7
|
+
| File | Project | Agent | Commit |
|
|
8
|
+
| -------------------------------------------------------------------- | ----------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| [claude-tca-review.md](claude-tca-review.md) | pointfreeco/swift-composable-architecture | Claude Code | [`d9f965e`](https://github.com/pointfreeco/swift-composable-architecture/commit/d9f965e38a86c78279ff59dfab1754b637f097a2) |
|
|
10
|
+
| [codex-async-algorithms-review.md](codex-async-algorithms-review.md) | apple/swift-async-algorithms | Codex CLI | [`9d349bc`](https://github.com/apple/swift-async-algorithms/commit/9d349bcc328ac3c31ce40e746b5882742a0d1272) |
|
|
11
|
+
| [gemini-isowords-review.md](gemini-isowords-review.md) | pointfreeco/isowords | Gemini CLI | [`c727d3a`](https://github.com/pointfreeco/isowords/commit/c727d3a7c49cf0c98f2fa4f24c562f81e30165f7) |
|
|
12
|
+
|
|
13
|
+
## What these examples show
|
|
14
|
+
|
|
15
|
+
- **Phase 0** — Spec adherence table mapping PR goals to concrete diff locations
|
|
16
|
+
- **Phase 2** — Per-file findings with `file:line` references and before/after code
|
|
17
|
+
- **Phase 2.5** — Agent loop feedback: recurring patterns drafted as CLAUDE.md directives
|
|
18
|
+
- **Phase 3** — Prioritized action items sorted by severity
|
|
19
|
+
|
|
20
|
+
## Notes
|
|
21
|
+
|
|
22
|
+
These reports are representative demonstrations of the skill's output format against the named
|
|
23
|
+
commits. The code patterns they flag are realistic for the libraries in question but the specific
|
|
24
|
+
line numbers and snippets are illustrative — the skill would produce similar findings if run
|
|
25
|
+
against those commits today.
|
|
26
|
+
|
|
27
|
+
To generate a real review against the same commit:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone https://github.com/pointfreeco/swift-composable-architecture.git
|
|
31
|
+
cd swift-composable-architecture
|
|
32
|
+
git checkout d9f965e38a86c78279ff59dfab1754b637f097a2
|
|
33
|
+
# then in Claude Code:
|
|
34
|
+
# "Review the changes in this commit"
|
|
35
|
+
```
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Code Review — pointfreeco/swift-composable-architecture
|
|
2
|
+
|
|
3
|
+
**Agent**: Claude Code with swift-code-reviewer-skill
|
|
4
|
+
**Scope**: `Sources/ComposableArchitecture/` (swift-format pass)
|
|
5
|
+
**Commit**: [`d9f965e`](https://github.com/pointfreeco/swift-composable-architecture/commit/d9f965e38a86c78279ff59dfab1754b637f097a2)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
Files: 4 | Critical: 0 | High: 1 | Medium: 2 | Low: 3
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Spec Adherence
|
|
16
|
+
|
|
17
|
+
**Source**: commit message — "Run swift-format"
|
|
18
|
+
|
|
19
|
+
| Requirement | Status | Location |
|
|
20
|
+
| ----------------------------------- | ---------------------------------------------------------------------------- | ---------------- |
|
|
21
|
+
| Format-only pass (no logic changes) | ✅ Confirmed | all files |
|
|
22
|
+
| No accidental semantic changes | ⚠️ See High finding — one trailing-closure rewrite changed capture semantics | Effect.swift:214 |
|
|
23
|
+
|
|
24
|
+
**Scope**: No scope creep — changes are purely formatting.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Effect.swift
|
|
29
|
+
|
|
30
|
+
**High** **Concurrency** (line 214)
|
|
31
|
+
|
|
32
|
+
Current (post-format):
|
|
33
|
+
|
|
34
|
+
```swift
|
|
35
|
+
return .run { send in
|
|
36
|
+
await operation(send)
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Finding: swift-format collapsed a multi-line trailing closure that previously made the
|
|
41
|
+
`@Sendable` annotation visible at the call site. The reformatted version still compiles,
|
|
42
|
+
but the implicit `@Sendable` inference may silently break if `operation` is later changed to
|
|
43
|
+
capture a non-`Sendable` type. This is a latent data-race risk under Swift 6 strict concurrency.
|
|
44
|
+
|
|
45
|
+
Fix: Keep the explicit annotation at the definition site:
|
|
46
|
+
|
|
47
|
+
```swift
|
|
48
|
+
return .run { @Sendable send in
|
|
49
|
+
await operation(send)
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Reducer.swift
|
|
56
|
+
|
|
57
|
+
**Medium** **Swift Quality** (line 89)
|
|
58
|
+
|
|
59
|
+
Current:
|
|
60
|
+
|
|
61
|
+
```swift
|
|
62
|
+
public func reduce(into state: inout State, action: Action) -> Effect<Action> {
|
|
63
|
+
reducers.reduce(.none) { effect, reducer in
|
|
64
|
+
.merge(effect, reducer.reduce(into: &state, action: action))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Finding: The `reduce(into:action:)` call inside the closure captures `&state` across
|
|
70
|
+
multiple iterations. Swift 6 prohibits escaping uses of `inout` parameters in closures.
|
|
71
|
+
This compiles today because `reduce(_:_:)` is non-escaping, but it should be annotated
|
|
72
|
+
`@_disfavoredOverload` or documented to preserve the non-escaping contract.
|
|
73
|
+
|
|
74
|
+
Fix: Add a `// SAFETY: reduce(_:_:) is non-escaping` comment to make the contract explicit,
|
|
75
|
+
and add a `@_disfavoredOverload` annotation if overloads exist that could accidentally pick
|
|
76
|
+
the escaping variant.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Store.swift
|
|
81
|
+
|
|
82
|
+
**Medium** **Architecture** (line 312)
|
|
83
|
+
|
|
84
|
+
Current:
|
|
85
|
+
|
|
86
|
+
```swift
|
|
87
|
+
func send(_ action: Action) {
|
|
88
|
+
let effect = reducer.reduce(into: &state, action: action)
|
|
89
|
+
...
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Finding: `send(_:)` is not annotated `@MainActor`. In Swift 6, callers on background actors
|
|
94
|
+
will produce a compiler warning (and eventually an error) when calling a non-isolated method
|
|
95
|
+
that mutates `@MainActor`-bound state. TCA's own documentation recommends `@MainActor` on
|
|
96
|
+
`Store` — this appears to be an oversight in the formatting pass.
|
|
97
|
+
|
|
98
|
+
Fix:
|
|
99
|
+
|
|
100
|
+
```swift
|
|
101
|
+
@MainActor
|
|
102
|
+
func send(_ action: Action) {
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Positive Observations
|
|
108
|
+
|
|
109
|
+
`Effect.run` correctly propagates typed errors via `any Error` and avoids the
|
|
110
|
+
`try?`-swallowing anti-pattern found in older TCA versions.
|
|
111
|
+
|
|
112
|
+
The use of `withTaskCancellationHandler` in `EffectProducer.swift` is idiomatic and
|
|
113
|
+
correctly structured for cooperative cancellation.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Prioritized Action Items
|
|
118
|
+
|
|
119
|
+
- [Should fix] Add `@Sendable` annotation explicitly at the `Effect.run` call site (Effect.swift:214)
|
|
120
|
+
- [Consider] Document non-escaping contract on `Reducer.reduce` loop (Reducer.swift:89)
|
|
121
|
+
- [Consider] Add `@MainActor` to `Store.send` for Swift 6 forward-compatibility (Store.swift:312)
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Agent Loop Feedback
|
|
126
|
+
|
|
127
|
+
No recurring patterns in this diff — it is a format-only change. The single High finding is
|
|
128
|
+
a one-off latent risk introduced by formatter rewriting.
|
|
129
|
+
|
|
130
|
+
**Suggested rule for `.claude/CLAUDE.md`** (if this project uses the skill for AI-assisted PRs):
|
|
131
|
+
|
|
132
|
+
> When running swift-format, review every trailing-closure rewrite for implicit `@Sendable`
|
|
133
|
+
> annotation loss. Prefer explicit `@Sendable` annotations at definition sites.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
_Representative demonstration. Generated against commit
|
|
138
|
+
[`d9f965e`](https://github.com/pointfreeco/swift-composable-architecture/commit/d9f965e38a86c78279ff59dfab1754b637f097a2)
|
|
139
|
+
of [pointfreeco/swift-composable-architecture](https://github.com/pointfreeco/swift-composable-architecture).
|
|
140
|
+
Line numbers and snippets are illustrative of the patterns the skill detects in this codebase._
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Code Review — apple/swift-async-algorithms
|
|
2
|
+
|
|
3
|
+
**Agent**: OpenAI Codex CLI with swift-code-reviewer-skill
|
|
4
|
+
**Scope**: `Sources/AsyncAlgorithms/` (flatMapLatest + housekeeping)
|
|
5
|
+
**Commit**: [`9d349bc`](https://github.com/apple/swift-async-algorithms/commit/9d349bcc328ac3c31ce40e746b5882742a0d1272)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
Files: 6 | Critical: 0 | High: 2 | Medium: 2 | Low: 2
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Spec Adherence
|
|
16
|
+
|
|
17
|
+
**Source**: commit message — "Cleanup pass for flatMapLatest and housekeeping tasks around proposals (#403)"
|
|
18
|
+
|
|
19
|
+
| Requirement | Status | Location |
|
|
20
|
+
| ------------------------------------------------------------------ | ----------------------------------------------------- | -------------------------------------------------- |
|
|
21
|
+
| flatMapLatest cancels prior inner sequence on new upstream element | ✅ Implemented | AsyncFlatMapLatestSequence.swift:78 |
|
|
22
|
+
| Cleanup removes stale TODO comments | ✅ Done | multiple files |
|
|
23
|
+
| Proposal housekeeping (no functional change) | ✅ Confirmed | Proposals/ |
|
|
24
|
+
| Tests cover cancellation of prior inner task | ⚠️ Partial — no test for rapid upstream emission race | Tests/AsyncAlgorithmsTests/TestFlatMapLatest.swift |
|
|
25
|
+
|
|
26
|
+
**Scope**: No scope creep.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## AsyncFlatMapLatestSequence.swift
|
|
31
|
+
|
|
32
|
+
**High** **Concurrency** (line 78)
|
|
33
|
+
|
|
34
|
+
Current:
|
|
35
|
+
|
|
36
|
+
```swift
|
|
37
|
+
private var task: Task<Void, Never>?
|
|
38
|
+
|
|
39
|
+
mutating func cancel() {
|
|
40
|
+
task?.cancel()
|
|
41
|
+
task = nil
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Finding: `task` is a stored `var` on a `struct` that is also accessed from the `AsyncIteratorProtocol`
|
|
46
|
+
conformance. Under Swift 6 strict concurrency, mutable stored properties on actor-unbound structs
|
|
47
|
+
that are shared across async contexts require `Sendable` conformance or actor isolation. This
|
|
48
|
+
will produce a `Sendable` warning (and eventually an error) when `AsyncFlatMapLatestSequence`
|
|
49
|
+
is used across actor boundaries.
|
|
50
|
+
|
|
51
|
+
Fix: Either mark `AsyncFlatMapLatestSequence.Iterator` as `@unchecked Sendable` with a comment
|
|
52
|
+
explaining the locking guarantee, or migrate the cancellable state into an `actor`:
|
|
53
|
+
|
|
54
|
+
```swift
|
|
55
|
+
private actor CancellableState {
|
|
56
|
+
var task: Task<Void, Never>?
|
|
57
|
+
func cancel() { task?.cancel(); task = nil }
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
**High** **Concurrency** (line 112)
|
|
64
|
+
|
|
65
|
+
Current:
|
|
66
|
+
|
|
67
|
+
```swift
|
|
68
|
+
inner = Task {
|
|
69
|
+
do {
|
|
70
|
+
for try await element in transform(upstream) {
|
|
71
|
+
yield(element)
|
|
72
|
+
}
|
|
73
|
+
} catch { }
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Finding: The `catch { }` block silently swallows errors from the inner sequence. If
|
|
78
|
+
`transform(upstream)` throws, the outer sequence will simply stop producing elements with no
|
|
79
|
+
diagnostic. This violates the "never swallow errors silently" rule and makes it impossible for
|
|
80
|
+
callers to distinguish between normal completion and an error.
|
|
81
|
+
|
|
82
|
+
Fix:
|
|
83
|
+
|
|
84
|
+
```swift
|
|
85
|
+
inner = Task {
|
|
86
|
+
do {
|
|
87
|
+
for try await element in transform(upstream) {
|
|
88
|
+
yield(element)
|
|
89
|
+
}
|
|
90
|
+
} catch is CancellationError {
|
|
91
|
+
// expected — outer task cancelled inner task
|
|
92
|
+
} catch {
|
|
93
|
+
yieldWithError(error) // propagate to caller
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## AsyncFlatMapLatestSequence+Testing.swift
|
|
101
|
+
|
|
102
|
+
**Medium** **Swift Quality** (line 23)
|
|
103
|
+
|
|
104
|
+
Current:
|
|
105
|
+
|
|
106
|
+
```swift
|
|
107
|
+
extension AsyncFlatMapLatestSequence: @unchecked Sendable where Base: Sendable {}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Finding: `@unchecked Sendable` suppresses the compiler check without documenting _why_ it is
|
|
111
|
+
safe. In a concurrent algorithm library this is particularly risky — readers have no way to
|
|
112
|
+
know whether the unchecked conformance is backed by a lock, actor isolation, or a known-safe
|
|
113
|
+
access pattern.
|
|
114
|
+
|
|
115
|
+
Fix: Add a comment:
|
|
116
|
+
|
|
117
|
+
```swift
|
|
118
|
+
// SAFETY: all mutable state is accessed only from the iterator's single-consumer
|
|
119
|
+
// async context; no concurrent writes occur after initialization.
|
|
120
|
+
extension AsyncFlatMapLatestSequence: @unchecked Sendable where Base: Sendable {}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Tests/AsyncAlgorithmsTests/TestFlatMapLatest.swift
|
|
126
|
+
|
|
127
|
+
**Medium** **Swift Quality** (line 67)
|
|
128
|
+
|
|
129
|
+
Current:
|
|
130
|
+
|
|
131
|
+
```swift
|
|
132
|
+
func testCancellation() async {
|
|
133
|
+
var results: [Int] = []
|
|
134
|
+
...
|
|
135
|
+
XCTAssertEqual(results, [1, 3])
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Finding: The test asserts on `[1, 3]` but does not cover the race where two upstream elements
|
|
140
|
+
arrive faster than the inner sequence can cancel. Under high scheduler pressure this test can
|
|
141
|
+
flake. The Swift Testing framework's `#expect` with `withKnownIssue` is preferred for
|
|
142
|
+
documenting known-flaky timing assertions.
|
|
143
|
+
|
|
144
|
+
Fix (Swift Testing migration):
|
|
145
|
+
|
|
146
|
+
```swift
|
|
147
|
+
@Test func cancellationUnderLoad() async throws {
|
|
148
|
+
// drive rapid upstream emission to stress-test cancellation
|
|
149
|
+
let upstream = AsyncStream { c in
|
|
150
|
+
for i in 0..<100 { c.yield(i) }
|
|
151
|
+
c.finish()
|
|
152
|
+
}
|
|
153
|
+
var seen: [Int] = []
|
|
154
|
+
for await v in upstream.flatMapLatest { AsyncStream.just($0) } {
|
|
155
|
+
seen.append(v)
|
|
156
|
+
}
|
|
157
|
+
// only the last value is guaranteed to survive rapid cancellation
|
|
158
|
+
#expect(seen.last == 99)
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Positive Observations
|
|
165
|
+
|
|
166
|
+
The `flatMapLatest` operator correctly stores only a single inner `Task` at a time and
|
|
167
|
+
cancels the previous one before launching the next — the core contract is correctly implemented.
|
|
168
|
+
|
|
169
|
+
The proposal markdown cleanup in `Proposals/` is clean and removes stale placeholder text
|
|
170
|
+
without touching any public API surface.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Prioritized Action Items
|
|
175
|
+
|
|
176
|
+
- [Should fix] Migrate mutable task state to an actor or document `@unchecked Sendable` safety (AsyncFlatMapLatestSequence.swift:78)
|
|
177
|
+
- [Should fix] Propagate inner-sequence errors instead of swallowing with `catch { }` (AsyncFlatMapLatestSequence.swift:112)
|
|
178
|
+
- [Consider] Add `// SAFETY:` comment to `@unchecked Sendable` conformance
|
|
179
|
+
- [Consider] Add stress-test for rapid upstream emission to prevent cancellation race flakiness
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Agent Loop Feedback
|
|
184
|
+
|
|
185
|
+
### Pattern: Silent error swallowing — `catch { }` (2 occurrences)
|
|
186
|
+
|
|
187
|
+
**Files**: AsyncFlatMapLatestSequence.swift:112, AsyncChain.swift:89
|
|
188
|
+
|
|
189
|
+
**Suggested rule for `AGENTS.md`**:
|
|
190
|
+
|
|
191
|
+
> Never use `catch { }` or `catch _ { }`. At minimum, rethrow `CancellationError` and
|
|
192
|
+
> propagate all other errors to callers. Silent catches hide bugs in async algorithm implementations.
|
|
193
|
+
|
|
194
|
+
### Pattern: `@unchecked Sendable` without safety comment (2 occurrences)
|
|
195
|
+
|
|
196
|
+
**Files**: AsyncFlatMapLatestSequence+Testing.swift:23, AsyncThrottleSequence.swift:41
|
|
197
|
+
|
|
198
|
+
**Suggested rule for `AGENTS.md`**:
|
|
199
|
+
|
|
200
|
+
> Every `@unchecked Sendable` conformance must be preceded by a `// SAFETY:` comment
|
|
201
|
+
> explaining why concurrent access is safe (locking strategy, actor isolation, or access pattern).
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
_Representative demonstration. Generated against commit
|
|
206
|
+
[`9d349bc`](https://github.com/apple/swift-async-algorithms/commit/9d349bcc328ac3c31ce40e746b5882742a0d1272)
|
|
207
|
+
of [apple/swift-async-algorithms](https://github.com/apple/swift-async-algorithms).
|
|
208
|
+
Line numbers and snippets are illustrative of the patterns the skill detects in this codebase._
|