pumuki 6.3.61 → 6.3.63
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/README.md +2 -0
- package/assets/rule-packs/ios-swiftui-modernization-v2.json +110 -0
- package/core/facts/detectors/text/ios.test.ts +333 -12
- package/core/facts/detectors/text/ios.ts +322 -3
- package/core/facts/detectors/text/iosSwiftUiModernizationSnapshot.test.ts +16 -3
- package/core/facts/detectors/text/iosSwiftUiModernizationSnapshot.ts +12 -3
- package/core/facts/extractHeuristicFacts.ts +21 -0
- package/core/rules/presets/heuristics/ios.test.ts +85 -3
- package/core/rules/presets/heuristics/ios.ts +310 -0
- package/docs/README.md +6 -5
- package/docs/codex-skills/core-data-expert.md +2 -1
- package/docs/codex-skills/swift-concurrency.md +3 -0
- package/docs/codex-skills/swift-testing-expert.md +6 -0
- package/docs/codex-skills/swiftui-expert-skill.md +1 -0
- package/docs/operations/RELEASE_NOTES.md +15 -1
- package/docs/rule-packs/ios.md +18 -1
- package/docs/validation/README.md +3 -32
- package/docs/validation/ios-avdlee-parity-matrix.md +48 -0
- package/integrations/config/skillsCompilerTemplates.test.ts +2 -0
- package/integrations/config/skillsCompilerTemplates.ts +157 -1
- package/integrations/config/skillsDetectorRegistry.ts +52 -0
- package/integrations/config/skillsLock.test.ts +3 -3
- package/integrations/config/skillsLock.ts +1 -1
- package/integrations/config/skillsMarkdownRules.ts +133 -2
- package/integrations/config/skillsPolicy.ts +2 -1
- package/integrations/config/skillsSources.test.ts +4 -4
- package/integrations/evidence/buildEvidence.ts +32 -0
- package/integrations/gate/stagePolicies.ts +591 -50
- package/integrations/lifecycle/cli.ts +37 -537
- package/integrations/lifecycle/cliOutputs.ts +14 -0
- package/integrations/lifecycle/cliSdd.ts +505 -0
- package/integrations/lifecycle/doctor.ts +13 -63
- package/integrations/lifecycle/install.ts +43 -8
- package/integrations/lifecycle/policyReconcile.ts +227 -217
- package/integrations/lifecycle/policyValidationSnapshot.ts +1 -7
- package/integrations/telemetry/gateTelemetry.ts +0 -4
- package/package.json +3 -1
- package/scripts/consumer-postinstall.cjs +59 -0
- package/scripts/framework-menu-system-notifications-config-choice.ts +2 -2
- package/scripts/framework-menu-system-notifications-config-state.ts +2 -2
- package/scripts/framework-menu-system-notifications-macos-dialog-enabled.ts +1 -1
- package/scripts/framework-menu-system-notifications-macos-swift-source.ts +8 -1
- package/skills.lock.json +308 -172
package/README.md
CHANGED
|
@@ -37,6 +37,8 @@ npx --yes pumuki status
|
|
|
37
37
|
npx --yes pumuki doctor --json
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
Desde **6.3.63**, `npm install` en la raíz de un repo **Git** dispara un `postinstall` que ejecuta `pumuki install` automáticamente (hooks `pre-commit` / `pre-push`). No configura MCP del IDE por sí solo: usa `pumuki install --with-mcp` o el adaptador. Desactivar el postinstall: `PUMUKI_SKIP_POSTINSTALL=1`. En CI suele saltarse solo (`CI=true`).
|
|
41
|
+
|
|
40
42
|
Fallback (equivalent in pasos separados):
|
|
41
43
|
|
|
42
44
|
```bash
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{
|
|
2
|
+
"snapshotId": "ios-swiftui-modernization-v2",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"generatedAt": "2026-04-04T00:00:00.000Z",
|
|
5
|
+
"sourceSkill": "swiftui-expert-skill",
|
|
6
|
+
"sourceReferences": [
|
|
7
|
+
"vendor/skills/swiftui-expert-skill/references/modern-apis.md",
|
|
8
|
+
"vendor/skills/swiftui-expert-skill/references/text-formatting.md",
|
|
9
|
+
"vendor/skills/swiftui-expert-skill/references/scroll-patterns.md",
|
|
10
|
+
"vendor/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md"
|
|
11
|
+
],
|
|
12
|
+
"entries": [
|
|
13
|
+
{
|
|
14
|
+
"id": "foreground-color",
|
|
15
|
+
"ruleId": "skills.ios.no-foreground-color",
|
|
16
|
+
"heuristicRuleId": "heuristics.ios.foreground-color.ast",
|
|
17
|
+
"category": "styling",
|
|
18
|
+
"legacyApi": ".foregroundColor(...)",
|
|
19
|
+
"modernApi": ".foregroundStyle(...)",
|
|
20
|
+
"rationale": "foregroundStyle supports hierarchical styles, gradients and materials.",
|
|
21
|
+
"confidence": "HIGH",
|
|
22
|
+
"minimumStage": "PRE_PUSH",
|
|
23
|
+
"minimumIos": "13.0",
|
|
24
|
+
"match": {
|
|
25
|
+
"kind": "regex",
|
|
26
|
+
"pattern": "\\.\\s*foregroundColor\\s*\\("
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "corner-radius",
|
|
31
|
+
"ruleId": "skills.ios.no-corner-radius",
|
|
32
|
+
"heuristicRuleId": "heuristics.ios.corner-radius.ast",
|
|
33
|
+
"category": "styling",
|
|
34
|
+
"legacyApi": ".cornerRadius(...)",
|
|
35
|
+
"modernApi": ".clipShape(.rect(cornerRadius: ...))",
|
|
36
|
+
"rationale": "cornerRadius is deprecated in modern SwiftUI and clipShape is explicit about the rendered shape.",
|
|
37
|
+
"confidence": "HIGH",
|
|
38
|
+
"minimumStage": "PRE_PUSH",
|
|
39
|
+
"minimumIos": "17.0",
|
|
40
|
+
"match": {
|
|
41
|
+
"kind": "regex",
|
|
42
|
+
"pattern": "\\.\\s*cornerRadius\\s*\\("
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "tab-item",
|
|
47
|
+
"ruleId": "skills.ios.no-tab-item",
|
|
48
|
+
"heuristicRuleId": "heuristics.ios.tab-item.ast",
|
|
49
|
+
"category": "tabs",
|
|
50
|
+
"legacyApi": ".tabItem { ... }",
|
|
51
|
+
"modernApi": "Tab { ... } label: { ... }",
|
|
52
|
+
"rationale": "The Tab API unlocks modern tab roles and avoids mixed syntax issues on iOS 18+.",
|
|
53
|
+
"confidence": "MEDIUM",
|
|
54
|
+
"minimumStage": "PRE_PUSH",
|
|
55
|
+
"minimumIos": "18.0",
|
|
56
|
+
"match": {
|
|
57
|
+
"kind": "regex",
|
|
58
|
+
"pattern": "\\.\\s*tabItem\\s*\\{"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "scrollview-shows-indicators",
|
|
63
|
+
"ruleId": "skills.ios.no-scrollview-shows-indicators",
|
|
64
|
+
"heuristicRuleId": "heuristics.ios.scrollview-shows-indicators.ast",
|
|
65
|
+
"category": "scrolling",
|
|
66
|
+
"legacyApi": "ScrollView(..., showsIndicators: false)",
|
|
67
|
+
"modernApi": ".scrollIndicators(.hidden)",
|
|
68
|
+
"rationale": "The modifier-based API keeps the initializer clean and matches modern SwiftUI scrolling patterns.",
|
|
69
|
+
"confidence": "HIGH",
|
|
70
|
+
"minimumStage": "PRE_PUSH",
|
|
71
|
+
"minimumIos": "16.0",
|
|
72
|
+
"match": {
|
|
73
|
+
"kind": "regex",
|
|
74
|
+
"pattern": "\\bScrollView\\s*\\([\\s\\S]{0,80}showsIndicators\\s*:\\s*false\\b"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "sheet-is-presented",
|
|
79
|
+
"ruleId": "skills.ios.no-sheet-is-presented",
|
|
80
|
+
"heuristicRuleId": "heuristics.ios.sheet-is-presented.ast",
|
|
81
|
+
"category": "presentation",
|
|
82
|
+
"legacyApi": ".sheet(isPresented: ...)",
|
|
83
|
+
"modernApi": ".sheet(item: ...)",
|
|
84
|
+
"rationale": "Item-based sheet presentation keeps model and presentation state aligned and avoids boolean drift in SwiftUI flows.",
|
|
85
|
+
"confidence": "MEDIUM",
|
|
86
|
+
"minimumStage": "PRE_PUSH",
|
|
87
|
+
"minimumIos": "13.0",
|
|
88
|
+
"match": {
|
|
89
|
+
"kind": "regex",
|
|
90
|
+
"pattern": "\\.\\s*sheet\\s*\\(\\s*isPresented\\s*:"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "legacy-onchange",
|
|
95
|
+
"ruleId": "skills.ios.no-legacy-onchange",
|
|
96
|
+
"heuristicRuleId": "heuristics.ios.legacy-onchange.ast",
|
|
97
|
+
"category": "state",
|
|
98
|
+
"legacyApi": ".onChange(of: ...) { value in ... }",
|
|
99
|
+
"modernApi": ".onChange(of: ...) { oldValue, newValue in ... } or .onChange(of: ...) { ... }",
|
|
100
|
+
"rationale": "Modern onChange variants make value transitions explicit and remove the need for legacy single-value closure patterns.",
|
|
101
|
+
"confidence": "HIGH",
|
|
102
|
+
"minimumStage": "PRE_PUSH",
|
|
103
|
+
"minimumIos": "17.0",
|
|
104
|
+
"match": {
|
|
105
|
+
"kind": "regex",
|
|
106
|
+
"pattern": "\\.\\s*onChange\\s*\\(\\s*of\\s*:[\\s\\S]{0,160}?(?:,\\s*perform\\s*:\\s*\\{\\s*(?:\\[[^\\]]+\\]\\s*)?(?:\\(\\s*[A-Za-z_][A-Za-z0-9_]*(?:\\s*:\\s*[^,)]+)?\\s*\\)|[A-Za-z_][A-Za-z0-9_]*)\\s+in\\b|\\)\\s*\\{\\s*(?:\\[[^\\]]+\\]\\s*)?(?:\\(\\s*[A-Za-z_][A-Za-z0-9_]*(?:\\s*:\\s*[^,)]+)?\\s*\\)|[A-Za-z_][A-Za-z0-9_]*)\\s+in\\b)"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
@@ -12,21 +12,38 @@ import {
|
|
|
12
12
|
hasSwiftDispatchGroupUsage,
|
|
13
13
|
hasSwiftDispatchQueueUsage,
|
|
14
14
|
hasSwiftDispatchSemaphoreUsage,
|
|
15
|
+
hasSwiftForEachIndicesUsage,
|
|
15
16
|
hasSwiftForceCastUsage,
|
|
17
|
+
hasSwiftFontWeightBoldUsage,
|
|
16
18
|
hasSwiftForegroundColorUsage,
|
|
17
19
|
hasSwiftForceTryUsage,
|
|
18
20
|
hasSwiftForceUnwrap,
|
|
21
|
+
hasSwiftGeometryReaderUsage,
|
|
22
|
+
hasSwiftLegacyOnChangeUsage,
|
|
23
|
+
hasSwiftLegacyExpectationDescriptionUsage,
|
|
24
|
+
hasSwiftLegacySwiftUiObservableWrapperUsage,
|
|
25
|
+
hasSwiftMixedTestingFrameworksUsage,
|
|
19
26
|
hasSwiftLegacyXCTestImportUsage,
|
|
27
|
+
hasSwiftModernizableXCTestSuiteUsage,
|
|
28
|
+
hasSwiftAssumeIsolatedUsage,
|
|
29
|
+
hasSwiftCoreDataLayerLeakUsage,
|
|
30
|
+
hasSwiftNonisolatedUnsafeUsage,
|
|
20
31
|
hasSwiftNSManagedObjectAsyncBoundaryUsage,
|
|
21
32
|
hasSwiftNSManagedObjectBoundaryUsage,
|
|
33
|
+
hasSwiftNSManagedObjectStateLeakUsage,
|
|
22
34
|
hasSwiftNavigationViewUsage,
|
|
23
35
|
hasSwiftObservableObjectUsage,
|
|
24
36
|
hasSwiftOnTapGestureUsage,
|
|
25
37
|
hasSwiftOperationQueueUsage,
|
|
38
|
+
hasSwiftContainsUserFilterUsage,
|
|
39
|
+
hasSwiftPassedValueStateWrapperUsage,
|
|
40
|
+
hasSwiftPreconcurrencyUsage,
|
|
41
|
+
hasSwiftSheetIsPresentedUsage,
|
|
26
42
|
hasSwiftScrollViewShowsIndicatorsUsage,
|
|
27
43
|
hasSwiftStringFormatUsage,
|
|
28
44
|
hasSwiftTabItemUsage,
|
|
29
45
|
hasSwiftTaskDetachedUsage,
|
|
46
|
+
hasSwiftWaitForExpectationsUsage,
|
|
30
47
|
hasSwiftUIScreenMainBoundsUsage,
|
|
31
48
|
hasSwiftXCTestAssertionUsage,
|
|
32
49
|
hasSwiftXCTUnwrapUsage,
|
|
@@ -120,16 +137,6 @@ test('hasSwiftCallbackStyleSignature ignora usos fuera de firmas callback', () =
|
|
|
120
137
|
assert.equal(hasSwiftCallbackStyleSignature(source), false);
|
|
121
138
|
});
|
|
122
139
|
|
|
123
|
-
test('hasSwiftCallbackStyleSignature ignora closures async modernos con @Sendable', () => {
|
|
124
|
-
const source = `
|
|
125
|
-
public init(publish: @escaping @Sendable ([AppRoute]) async -> Void) {
|
|
126
|
-
self.publish = publish
|
|
127
|
-
}
|
|
128
|
-
`;
|
|
129
|
-
|
|
130
|
-
assert.equal(hasSwiftCallbackStyleSignature(source), false);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
140
|
test('detecta primitivas GCD y OperationQueue en codigo ejecutable', () => {
|
|
134
141
|
const source = `
|
|
135
142
|
DispatchQueue.main.async { }
|
|
@@ -164,10 +171,41 @@ final class LegacyBox: @unchecked Sendable {}
|
|
|
164
171
|
assert.equal(hasSwiftUncheckedSendableUsage(source), true);
|
|
165
172
|
});
|
|
166
173
|
|
|
174
|
+
test('detectores de hardening de concurrencia detectan escapes inseguros', () => {
|
|
175
|
+
const source = `
|
|
176
|
+
@preconcurrency import LegacyFramework
|
|
177
|
+
|
|
178
|
+
struct APIProvider: Sendable {
|
|
179
|
+
nonisolated(unsafe) static private(set) var shared: APIProvider!
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
func renderFromLegacyCallback() {
|
|
183
|
+
MainActor.assumeIsolated {
|
|
184
|
+
updateUI()
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
`;
|
|
188
|
+
|
|
189
|
+
assert.equal(hasSwiftPreconcurrencyUsage(source), true);
|
|
190
|
+
assert.equal(hasSwiftNonisolatedUnsafeUsage(source), true);
|
|
191
|
+
assert.equal(hasSwiftAssumeIsolatedUsage(source), true);
|
|
192
|
+
});
|
|
193
|
+
|
|
167
194
|
test('detectores SwiftUI modernos detectan patrones legacy relevantes', () => {
|
|
168
195
|
const source = `
|
|
196
|
+
@preconcurrency import LegacyFramework
|
|
169
197
|
final class LegacyViewModel: ObservableObject {}
|
|
198
|
+
@StateObject private var ownedViewModel = LegacyViewModel()
|
|
199
|
+
@ObservedObject var injectedViewModel: LegacyViewModel
|
|
170
200
|
NavigationView { Text("x") }
|
|
201
|
+
GeometryReader { proxy in
|
|
202
|
+
Text("x").frame(width: proxy.size.width)
|
|
203
|
+
}
|
|
204
|
+
Text("Headline").fontWeight(.bold)
|
|
205
|
+
let filtered = items.filter { $0.title.contains(searchText) }
|
|
206
|
+
ForEach(items.indices, id: \\.self) { index in
|
|
207
|
+
Text(items[index].title)
|
|
208
|
+
}
|
|
171
209
|
Text("Primary").foregroundColor(.blue)
|
|
172
210
|
Image("hero").cornerRadius(12)
|
|
173
211
|
TabView {
|
|
@@ -181,8 +219,33 @@ let width = UIScreen.main.bounds.width
|
|
|
181
219
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
182
220
|
Text("feed")
|
|
183
221
|
}
|
|
184
|
-
|
|
222
|
+
.sheet(isPresented: $showDetails) {
|
|
223
|
+
DetailView()
|
|
224
|
+
}
|
|
225
|
+
.onChange(of: query) { newValue in
|
|
226
|
+
print(newValue)
|
|
227
|
+
}
|
|
228
|
+
struct DetailView: View {
|
|
229
|
+
@State private var filter: String
|
|
230
|
+
@StateObject private var detailViewModel: LegacyViewModel
|
|
231
|
+
|
|
232
|
+
init(filter: String, detailViewModel: LegacyViewModel) {
|
|
233
|
+
_filter = State(initialValue: filter)
|
|
234
|
+
_detailViewModel = StateObject(wrappedValue: detailViewModel)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
nonisolated(unsafe) static var sharedBridge: LegacyViewModel?
|
|
238
|
+
MainActor.assumeIsolated { reload() }
|
|
239
|
+
`;
|
|
240
|
+
assert.equal(hasSwiftPreconcurrencyUsage(source), true);
|
|
241
|
+
assert.equal(hasSwiftNonisolatedUnsafeUsage(source), true);
|
|
242
|
+
assert.equal(hasSwiftAssumeIsolatedUsage(source), true);
|
|
243
|
+
assert.equal(hasSwiftForEachIndicesUsage(source), true);
|
|
244
|
+
assert.equal(hasSwiftContainsUserFilterUsage(source), true);
|
|
245
|
+
assert.equal(hasSwiftGeometryReaderUsage(source), true);
|
|
246
|
+
assert.equal(hasSwiftFontWeightBoldUsage(source), true);
|
|
185
247
|
assert.equal(hasSwiftObservableObjectUsage(source), true);
|
|
248
|
+
assert.equal(hasSwiftLegacySwiftUiObservableWrapperUsage(source), true);
|
|
186
249
|
assert.equal(hasSwiftNavigationViewUsage(source), true);
|
|
187
250
|
assert.equal(hasSwiftForegroundColorUsage(source), true);
|
|
188
251
|
assert.equal(hasSwiftCornerRadiusUsage(source), true);
|
|
@@ -191,6 +254,9 @@ ScrollView(.horizontal, showsIndicators: false) {
|
|
|
191
254
|
assert.equal(hasSwiftStringFormatUsage(source), true);
|
|
192
255
|
assert.equal(hasSwiftUIScreenMainBoundsUsage(source), true);
|
|
193
256
|
assert.equal(hasSwiftScrollViewShowsIndicatorsUsage(source), true);
|
|
257
|
+
assert.equal(hasSwiftSheetIsPresentedUsage(source), true);
|
|
258
|
+
assert.equal(hasSwiftLegacyOnChangeUsage(source), true);
|
|
259
|
+
assert.equal(hasSwiftPassedValueStateWrapperUsage(source), true);
|
|
194
260
|
});
|
|
195
261
|
|
|
196
262
|
test('detectores legacy ignoran strings y comentarios', () => {
|
|
@@ -204,7 +270,26 @@ let e = ".foregroundColor(.blue)"
|
|
|
204
270
|
let f = ".cornerRadius(12)"
|
|
205
271
|
let g = ".tabItem { Label(\\\"Home\\\", systemImage: \\\"house\\\") }"
|
|
206
272
|
let h = "ScrollView(showsIndicators: false) { }"
|
|
207
|
-
|
|
273
|
+
let i = ".sheet(isPresented: $showDetails) { DetailView() }"
|
|
274
|
+
let j = ".onChange(of: query) { newValue in }"
|
|
275
|
+
let k = "@StateObject private var ownedViewModel = LegacyViewModel()"
|
|
276
|
+
let l = "@ObservedObject var injectedViewModel: LegacyViewModel"
|
|
277
|
+
let m = "_filter = State(initialValue: filter)"
|
|
278
|
+
let n = "ForEach(items.indices, id: \\.self) { index in }"
|
|
279
|
+
let o = "items.filter { $0.title.contains(searchText) }"
|
|
280
|
+
let p = "GeometryReader { proxy in }"
|
|
281
|
+
let q = ".fontWeight(.bold)"
|
|
282
|
+
let r = "@preconcurrency import LegacyFramework"
|
|
283
|
+
let s = "nonisolated(unsafe) static var sharedBridge: Model?"
|
|
284
|
+
let t = "MainActor.assumeIsolated { reload() }"
|
|
285
|
+
`;
|
|
286
|
+
assert.equal(hasSwiftPreconcurrencyUsage(source), false);
|
|
287
|
+
assert.equal(hasSwiftNonisolatedUnsafeUsage(source), false);
|
|
288
|
+
assert.equal(hasSwiftAssumeIsolatedUsage(source), false);
|
|
289
|
+
assert.equal(hasSwiftForEachIndicesUsage(source), false);
|
|
290
|
+
assert.equal(hasSwiftContainsUserFilterUsage(source), false);
|
|
291
|
+
assert.equal(hasSwiftGeometryReaderUsage(source), false);
|
|
292
|
+
assert.equal(hasSwiftFontWeightBoldUsage(source), false);
|
|
208
293
|
assert.equal(hasSwiftTaskDetachedUsage(source), false);
|
|
209
294
|
assert.equal(hasSwiftNavigationViewUsage(source), false);
|
|
210
295
|
assert.equal(hasSwiftForegroundColorUsage(source), false);
|
|
@@ -213,26 +298,54 @@ let h = "ScrollView(showsIndicators: false) { }"
|
|
|
213
298
|
assert.equal(hasSwiftStringFormatUsage(source), false);
|
|
214
299
|
assert.equal(hasSwiftUIScreenMainBoundsUsage(source), false);
|
|
215
300
|
assert.equal(hasSwiftScrollViewShowsIndicatorsUsage(source), false);
|
|
301
|
+
assert.equal(hasSwiftSheetIsPresentedUsage(source), false);
|
|
302
|
+
assert.equal(hasSwiftLegacyOnChangeUsage(source), false);
|
|
303
|
+
assert.equal(hasSwiftLegacySwiftUiObservableWrapperUsage(source), false);
|
|
304
|
+
assert.equal(hasSwiftPassedValueStateWrapperUsage(source), false);
|
|
216
305
|
});
|
|
217
306
|
|
|
218
307
|
test('detectores snapshot SwiftUI ignoran reemplazos modernos', () => {
|
|
219
308
|
const source = `
|
|
220
309
|
Text("Primary").foregroundStyle(.blue)
|
|
221
310
|
Image("hero").clipShape(.rect(cornerRadius: 12))
|
|
311
|
+
Text("Headline").bold()
|
|
222
312
|
TabView {
|
|
223
313
|
Tab("Home", systemImage: "house") {
|
|
224
314
|
HomeView()
|
|
225
315
|
}
|
|
226
316
|
}
|
|
317
|
+
let filtered = items.filter { $0.title.localizedStandardContains(searchText) }
|
|
318
|
+
ForEach(items) { item in
|
|
319
|
+
Text(item.title)
|
|
320
|
+
}
|
|
321
|
+
containerRelativeFrame(.horizontal)
|
|
227
322
|
ScrollView {
|
|
228
323
|
Text("feed")
|
|
229
324
|
}
|
|
230
325
|
.scrollIndicators(.hidden)
|
|
326
|
+
.sheet(item: $selectedItem) { item in
|
|
327
|
+
DetailView(item: item)
|
|
328
|
+
}
|
|
329
|
+
.onChange(of: query) { oldValue, newValue in
|
|
330
|
+
print(oldValue, newValue)
|
|
331
|
+
}
|
|
332
|
+
.onChange(of: selection) {
|
|
333
|
+
reloadSelection()
|
|
334
|
+
}
|
|
231
335
|
`;
|
|
336
|
+
assert.equal(hasSwiftPreconcurrencyUsage(source), false);
|
|
337
|
+
assert.equal(hasSwiftNonisolatedUnsafeUsage(source), false);
|
|
338
|
+
assert.equal(hasSwiftAssumeIsolatedUsage(source), false);
|
|
339
|
+
assert.equal(hasSwiftForEachIndicesUsage(source), false);
|
|
340
|
+
assert.equal(hasSwiftContainsUserFilterUsage(source), false);
|
|
341
|
+
assert.equal(hasSwiftGeometryReaderUsage(source), false);
|
|
342
|
+
assert.equal(hasSwiftFontWeightBoldUsage(source), false);
|
|
232
343
|
assert.equal(hasSwiftForegroundColorUsage(source), false);
|
|
233
344
|
assert.equal(hasSwiftCornerRadiusUsage(source), false);
|
|
234
345
|
assert.equal(hasSwiftTabItemUsage(source), false);
|
|
235
346
|
assert.equal(hasSwiftScrollViewShowsIndicatorsUsage(source), false);
|
|
347
|
+
assert.equal(hasSwiftSheetIsPresentedUsage(source), false);
|
|
348
|
+
assert.equal(hasSwiftLegacyOnChangeUsage(source), false);
|
|
236
349
|
});
|
|
237
350
|
|
|
238
351
|
test('hasSwiftLegacyXCTestImportUsage detecta XCTest unitario y excluye UI/performance', () => {
|
|
@@ -268,6 +381,124 @@ final class SyncTests: XCTestCase {
|
|
|
268
381
|
assert.equal(hasSwiftLegacyXCTestImportUsage(performanceTest), false);
|
|
269
382
|
});
|
|
270
383
|
|
|
384
|
+
test('hasSwiftLegacySwiftUiObservableWrapperUsage detecta @StateObject/@ObservedObject legacy', () => {
|
|
385
|
+
const legacyWrapper = `
|
|
386
|
+
@StateObject private var viewModel = LegacyViewModel()
|
|
387
|
+
@ObservedObject var sessionViewModel: SessionViewModel
|
|
388
|
+
`;
|
|
389
|
+
const modernWrapper = `
|
|
390
|
+
@Observable
|
|
391
|
+
final class SessionViewModel {}
|
|
392
|
+
|
|
393
|
+
struct ContentView: View {
|
|
394
|
+
@State private var viewModel = SessionViewModel()
|
|
395
|
+
}
|
|
396
|
+
`;
|
|
397
|
+
|
|
398
|
+
assert.equal(hasSwiftLegacySwiftUiObservableWrapperUsage(legacyWrapper), true);
|
|
399
|
+
assert.equal(hasSwiftLegacySwiftUiObservableWrapperUsage(modernWrapper), false);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test('hasSwiftPassedValueStateWrapperUsage detecta valores inyectados guardados como @State o @StateObject', () => {
|
|
403
|
+
const invalidOwnership = `
|
|
404
|
+
struct DetailView: View {
|
|
405
|
+
@State private var filter: String
|
|
406
|
+
@StateObject private var viewModel: DetailViewModel
|
|
407
|
+
|
|
408
|
+
init(filter: String, viewModel: DetailViewModel) {
|
|
409
|
+
_filter = State(initialValue: filter)
|
|
410
|
+
_viewModel = StateObject(wrappedValue: viewModel)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
`;
|
|
414
|
+
const validOwnership = `
|
|
415
|
+
@Observable
|
|
416
|
+
final class DetailViewModel {}
|
|
417
|
+
|
|
418
|
+
struct DetailView: View {
|
|
419
|
+
let filter: String
|
|
420
|
+
@State private var viewModel = DetailViewModel()
|
|
421
|
+
}
|
|
422
|
+
`;
|
|
423
|
+
|
|
424
|
+
assert.equal(hasSwiftPassedValueStateWrapperUsage(invalidOwnership), true);
|
|
425
|
+
assert.equal(hasSwiftPassedValueStateWrapperUsage(validOwnership), false);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test('hasSwiftModernizableXCTestSuiteUsage detecta suites legacy y excluye mixed/UI', () => {
|
|
429
|
+
const legacySuite = `
|
|
430
|
+
import XCTest
|
|
431
|
+
|
|
432
|
+
final class LoginTests: XCTestCase {
|
|
433
|
+
func testLogin() async throws {
|
|
434
|
+
XCTAssertEqual(result, expected)
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
`;
|
|
438
|
+
const mixedSuite = `
|
|
439
|
+
import XCTest
|
|
440
|
+
import Testing
|
|
441
|
+
|
|
442
|
+
final class LoginTests: XCTestCase {
|
|
443
|
+
func testLegacyLogin() {}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
@Suite
|
|
447
|
+
struct LoginModernTests {
|
|
448
|
+
@Test func login() async {}
|
|
449
|
+
}
|
|
450
|
+
`;
|
|
451
|
+
const uiSuite = `
|
|
452
|
+
import XCTest
|
|
453
|
+
|
|
454
|
+
final class LoginUITests: XCTestCase {
|
|
455
|
+
func testLoginFlow() {
|
|
456
|
+
let app = XCUIApplication()
|
|
457
|
+
app.launch()
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
`;
|
|
461
|
+
|
|
462
|
+
assert.equal(hasSwiftModernizableXCTestSuiteUsage(legacySuite), true);
|
|
463
|
+
assert.equal(hasSwiftModernizableXCTestSuiteUsage(mixedSuite), false);
|
|
464
|
+
assert.equal(hasSwiftModernizableXCTestSuiteUsage(uiSuite), false);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test('hasSwiftMixedTestingFrameworksUsage detecta mezcla XCTestCase y Testing/@Test', () => {
|
|
468
|
+
const mixedSuite = `
|
|
469
|
+
import XCTest
|
|
470
|
+
import Testing
|
|
471
|
+
|
|
472
|
+
final class LoginTests: XCTestCase {
|
|
473
|
+
func testLegacyLogin() {}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
@Suite
|
|
477
|
+
struct LoginModernTests {
|
|
478
|
+
@Test func login() async {}
|
|
479
|
+
}
|
|
480
|
+
`;
|
|
481
|
+
const legacyOnly = `
|
|
482
|
+
import XCTest
|
|
483
|
+
|
|
484
|
+
final class LoginTests: XCTestCase {
|
|
485
|
+
func testLegacyLogin() {}
|
|
486
|
+
}
|
|
487
|
+
`;
|
|
488
|
+
const modernOnly = `
|
|
489
|
+
import Testing
|
|
490
|
+
|
|
491
|
+
@Suite
|
|
492
|
+
struct LoginModernTests {
|
|
493
|
+
@Test func login() async {}
|
|
494
|
+
}
|
|
495
|
+
`;
|
|
496
|
+
|
|
497
|
+
assert.equal(hasSwiftMixedTestingFrameworksUsage(mixedSuite), true);
|
|
498
|
+
assert.equal(hasSwiftMixedTestingFrameworksUsage(legacyOnly), false);
|
|
499
|
+
assert.equal(hasSwiftMixedTestingFrameworksUsage(modernOnly), false);
|
|
500
|
+
});
|
|
501
|
+
|
|
271
502
|
test('hasSwiftXCTestAssertionUsage detecta XCTAssert y XCTFail reales', () => {
|
|
272
503
|
const source = `
|
|
273
504
|
XCTAssertEqual(value, expected)
|
|
@@ -294,6 +525,43 @@ let text = "XCTUnwrap(optionalValue)"
|
|
|
294
525
|
assert.equal(hasSwiftXCTUnwrapUsage(ignored), false);
|
|
295
526
|
});
|
|
296
527
|
|
|
528
|
+
test('hasSwiftWaitForExpectationsUsage detecta waits legacy y excluye await fulfillment', () => {
|
|
529
|
+
const legacyWait = `
|
|
530
|
+
let expectation = expectation(description: "Done")
|
|
531
|
+
wait(for: [expectation], timeout: 1)
|
|
532
|
+
waitForExpectations(timeout: 1)
|
|
533
|
+
`;
|
|
534
|
+
const modernWait = `
|
|
535
|
+
let expectation = expectation(description: "Done")
|
|
536
|
+
await fulfillment(of: [expectation], timeout: 1)
|
|
537
|
+
`;
|
|
538
|
+
|
|
539
|
+
assert.equal(hasSwiftWaitForExpectationsUsage(legacyWait), true);
|
|
540
|
+
assert.equal(hasSwiftWaitForExpectationsUsage(modernWait), false);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test('hasSwiftLegacyExpectationDescriptionUsage detecta expectation(description:) sin flujo moderno', () => {
|
|
544
|
+
const legacyExpectation = `
|
|
545
|
+
let expectation = expectation(description: "Done")
|
|
546
|
+
doWork { expectation.fulfill() }
|
|
547
|
+
waitForExpectations(timeout: 1)
|
|
548
|
+
`;
|
|
549
|
+
const modernExpectation = `
|
|
550
|
+
let expectation = expectation(description: "Done")
|
|
551
|
+
doWork { expectation.fulfill() }
|
|
552
|
+
await fulfillment(of: [expectation], timeout: 1)
|
|
553
|
+
`;
|
|
554
|
+
const confirmationOnly = `
|
|
555
|
+
await confirmation("Done") { confirm in
|
|
556
|
+
await doWork { confirm() }
|
|
557
|
+
}
|
|
558
|
+
`;
|
|
559
|
+
|
|
560
|
+
assert.equal(hasSwiftLegacyExpectationDescriptionUsage(legacyExpectation), true);
|
|
561
|
+
assert.equal(hasSwiftLegacyExpectationDescriptionUsage(modernExpectation), false);
|
|
562
|
+
assert.equal(hasSwiftLegacyExpectationDescriptionUsage(confirmationOnly), false);
|
|
563
|
+
});
|
|
564
|
+
|
|
297
565
|
test('hasSwiftNSManagedObjectBoundaryUsage detecta boundaries con NSManagedObject y excluye IDs o subclases', () => {
|
|
298
566
|
const source = `
|
|
299
567
|
func persist(_ entity: NSManagedObject) {}
|
|
@@ -325,6 +593,59 @@ func fetchEntityID() async throws -> NSManagedObjectID {
|
|
|
325
593
|
assert.equal(hasSwiftNSManagedObjectAsyncBoundaryUsage(ignored), false);
|
|
326
594
|
});
|
|
327
595
|
|
|
596
|
+
test('hasSwiftCoreDataLayerLeakUsage detecta Core Data fuera de infraestructura', () => {
|
|
597
|
+
const source = `
|
|
598
|
+
import CoreData
|
|
599
|
+
|
|
600
|
+
struct DetailView: View {
|
|
601
|
+
@Environment(\\.managedObjectContext) private var context
|
|
602
|
+
@FetchRequest(sortDescriptors: []) private var items: FetchedResults<TodoEntity>
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
final class DetailUseCase {
|
|
606
|
+
private let container: NSPersistentContainer
|
|
607
|
+
}
|
|
608
|
+
`;
|
|
609
|
+
const ignored = `
|
|
610
|
+
import Foundation
|
|
611
|
+
|
|
612
|
+
struct DetailView: View {
|
|
613
|
+
let selectedID: NSManagedObjectID?
|
|
614
|
+
}
|
|
615
|
+
`;
|
|
616
|
+
|
|
617
|
+
assert.equal(hasSwiftCoreDataLayerLeakUsage(source), true);
|
|
618
|
+
assert.equal(hasSwiftCoreDataLayerLeakUsage(ignored), false);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
test('hasSwiftNSManagedObjectStateLeakUsage detecta fugas a SwiftUI state y ViewModels', () => {
|
|
622
|
+
const source = `
|
|
623
|
+
final class TodoEntity: NSManagedObject {}
|
|
624
|
+
|
|
625
|
+
struct DetailView: View {
|
|
626
|
+
@State private var selectedEntity: TodoEntity?
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
final class DetailViewModel: ObservableObject {
|
|
630
|
+
@Published var entity: TodoEntity?
|
|
631
|
+
}
|
|
632
|
+
`;
|
|
633
|
+
const ignored = `
|
|
634
|
+
final class TodoEntity: NSManagedObject {}
|
|
635
|
+
|
|
636
|
+
struct DetailView: View {
|
|
637
|
+
let selectedID: NSManagedObjectID?
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
final class DetailViewModel: ObservableObject {
|
|
641
|
+
@Published var entityID: NSManagedObjectID?
|
|
642
|
+
}
|
|
643
|
+
`;
|
|
644
|
+
|
|
645
|
+
assert.equal(hasSwiftNSManagedObjectStateLeakUsage(source), true);
|
|
646
|
+
assert.equal(hasSwiftNSManagedObjectStateLeakUsage(ignored), false);
|
|
647
|
+
});
|
|
648
|
+
|
|
328
649
|
test('findSwiftPresentationSrpMatch devuelve payload semantico para SRP-iOS en presentation', () => {
|
|
329
650
|
const source = `
|
|
330
651
|
@MainActor
|