pumuki 6.3.297 → 6.3.299
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/AGENTS.md +55 -0
- package/CHANGELOG.md +8 -0
- package/VERSION +1 -1
- package/core/facts/detectors/text/ios.test.ts +324 -0
- package/core/facts/detectors/text/ios.ts +304 -0
- package/core/facts/extractHeuristicFacts.ts +16 -0
- package/core/rules/presets/heuristics/ios.test.ts +56 -1
- package/core/rules/presets/heuristics/ios.ts +204 -0
- package/integrations/config/skillsDetectorRegistry.ts +51 -2
- package/integrations/git/runPlatformGateOutput.ts +11 -0
- package/package.json +1 -1
- package/scripts/framework-menu-system-notifications-macos-dialog-payload.ts +55 -7
- package/skills.lock.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -113,12 +113,67 @@ Antes de realizar cualquier accion:
|
|
|
113
113
|
- `release/*`: debe salir de `develop` y solo contener cambios de estabilizacion/release.
|
|
114
114
|
- `hotfix/*`: debe salir de `main` para fixes urgentes de produccion.
|
|
115
115
|
- Esta prohibido commitear en `main` y `develop` sin instruccion explicita del usuario.
|
|
116
|
+
- Ciclo de cierre de ramas obligatorio:
|
|
117
|
+
- una rama `feature/*`, `bugfix/*`, `chore/*`, `refactor/*` o `docs/*` terminada debe integrarse en `develop` mediante el flujo acordado;
|
|
118
|
+
- una rama `release/*` terminada debe integrarse en `main` y `develop`;
|
|
119
|
+
- una rama `hotfix/*` terminada debe integrarse en `main` y propagarse a `develop`;
|
|
120
|
+
- tras confirmar que la rama esta mergeada en sus destinos obligatorios, debe eliminarse la rama local y remota si existe;
|
|
121
|
+
- las ramas cerradas no pueden quedar acumuladas como backlog, marcador historico o pseudo-tracking.
|
|
122
|
+
- Criterio de borrado seguro de ramas:
|
|
123
|
+
- antes de borrar cualquier rama, comprobar si esta mergeada con `git branch --merged <destino>`;
|
|
124
|
+
- si no aparece como mergeada, no borrarla sin revisar commits no integrados;
|
|
125
|
+
- si contiene commits utiles no integrados, integrarlos, crear backup branch explicita o stash/patch documentado antes de eliminar;
|
|
126
|
+
- nunca usar borrado forzado de ramas (`git branch -D` o delete remoto forzado) sin instruccion explicita y evidencia de preservacion.
|
|
127
|
+
- La limpieza de ramas forma parte del cierre de cada slice:
|
|
128
|
+
- no se considera cerrada una task si deja worktrees temporales o ramas de trabajo ya mergeadas sin limpiar;
|
|
129
|
+
- si no se puede limpiar por riesgo, reportar `STATUS: BLOCKED` o dejar `NEXT instruction` explicita con la razon.
|
|
116
130
|
- Si la rama actual no cumple naming o flujo:
|
|
117
131
|
- detener implementacion,
|
|
118
132
|
- declarar `STATUS: BLOCKED`,
|
|
119
133
|
- explicar el conflicto de rama/flujo, y
|
|
120
134
|
- pedir al usuario el cambio o confirmacion de rama antes de continuar.
|
|
121
135
|
|
|
136
|
+
## Contrato hard de commits atomicos y limite de worktree (no negociable)
|
|
137
|
+
- Todo cambio debe cerrarse en commits atomicos por slice funcional, bug o contrato tecnico.
|
|
138
|
+
- Esta prohibido acumular cambios de varios subsistemas sin commit intermedio.
|
|
139
|
+
- Un commit atomico debe tener una unica intencion verificable, por ejemplo:
|
|
140
|
+
- un detector AST/nodal concreto y sus tests,
|
|
141
|
+
- una correccion de gate/lifecycle concreta y sus tests,
|
|
142
|
+
- una mejora de notificacion concreta y sus tests,
|
|
143
|
+
- una actualizacion de tracking/docs estrictamente asociada a esa slice,
|
|
144
|
+
- una preparacion de release/versionado separada del codigo funcional.
|
|
145
|
+
- Esta prohibido mezclar en el mismo commit:
|
|
146
|
+
- detectores AST con versionado/release,
|
|
147
|
+
- cambios de lifecycle/gate con cambios de menu/runtime no relacionados,
|
|
148
|
+
- cambios de consumers con cambios internos de Pumuki,
|
|
149
|
+
- tracking amplio con implementacion funcional no relacionada,
|
|
150
|
+
- limpieza de ramas/worktrees con codigo de producto.
|
|
151
|
+
- Umbral hard de higiene:
|
|
152
|
+
- si `git status --short` muestra mas de 24 rutas pendientes, no se permite seguir implementando;
|
|
153
|
+
- si muestra mas de 12 rutas pendientes, se debe evaluar si la slice ya debe cerrarse;
|
|
154
|
+
- si existen archivos `??` fuera del alcance de la slice, deben clasificarse antes de continuar.
|
|
155
|
+
- Si el worktree supera el umbral hard:
|
|
156
|
+
- declarar `STATUS: BLOCKED`,
|
|
157
|
+
- detener nuevas ediciones funcionales,
|
|
158
|
+
- listar cambios por subsistema,
|
|
159
|
+
- crear commits atomicos separados o stashes con nombre explicito antes de continuar,
|
|
160
|
+
- no usar un commit masivo para "limpiar" el estado.
|
|
161
|
+
- Antes de cada commit:
|
|
162
|
+
- revisar `git diff --cached --name-status`,
|
|
163
|
+
- confirmar que todos los archivos staged pertenecen a la misma intencion,
|
|
164
|
+
- dejar fuera del stage cualquier archivo no relacionado,
|
|
165
|
+
- documentar en la respuesta la relacion `escenario -> tests -> evidencia -> task`.
|
|
166
|
+
- Si los hooks bloquean por deuda ajena al staged:
|
|
167
|
+
- no ampliar el commit para "aprovechar",
|
|
168
|
+
- reportar la causa exacta,
|
|
169
|
+
- corregir Pumuki si es bug del gate,
|
|
170
|
+
- o aislar la slice si es deuda del worktree.
|
|
171
|
+
- Las ramas locales y worktrees temporales deben revisarse con inventario antes de borrarse.
|
|
172
|
+
- Esta prohibido borrar ramas, worktrees, stashes o directorios temporales con cambios sin:
|
|
173
|
+
- comprobar estado,
|
|
174
|
+
- preservar cambios en commit o stash con nombre explicito,
|
|
175
|
+
- y reportar la evidencia.
|
|
176
|
+
|
|
122
177
|
## Gate operativo obligatorio (antes de editar codigo)
|
|
123
178
|
- Declarar internamente las skills aplicables y tratarlas como activas durante TODO el turno.
|
|
124
179
|
- Verificar cumplimiento minimo previo:
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [6.3.298] - 2026-05-19
|
|
4
|
+
|
|
5
|
+
## [6.3.299] - 2026-05-19
|
|
6
|
+
|
|
7
|
+
- Release line for INC145 AST/skills actionable next actions. Block summaries now emit rule, file, line and expected fix for any unmapped AST/skills finding instead of falling back to a generic primary-blocker message, while preserving specific mappings such as console.log.
|
|
8
|
+
|
|
9
|
+
- Release line for the merged notification/evidence/context cleanup on develop. Keeps the published package unique after 6.3.297 was already present on npm, preserving the actionable notification payloads, expanded AST detector coverage, PRE_WRITE lease/evidence cause propagation, lifecycle hook alignment, local context gate scaffold and atomic Git hygiene rules.
|
|
10
|
+
|
|
3
11
|
## [6.3.297] - 2026-05-19
|
|
4
12
|
|
|
5
13
|
- `PUMUKI-INC-157`: staged no-code/documentation/repin scopes no longer fail the skills contract with `EVIDENCE_SKILLS_PLATFORMS_UNDETECTED`. When the effective staged scope has no iOS, Android, backend or frontend code paths, platform skills are reported as `NOT_APPLICABLE` instead of blocking, while code-bearing slices keep the hard skills gate.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.3.
|
|
1
|
+
6.3.299
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
findSwiftPresentationSrpMatch,
|
|
9
9
|
collectSwiftModernizableXCTestSuiteLines,
|
|
10
10
|
collectSwiftLegacyExpectationDescriptionLines,
|
|
11
|
+
collectSwiftDirectSUTInstantiationWithoutMakeSUTLines,
|
|
11
12
|
collectSwiftMakeSUTWithoutMemoryTrackingLines,
|
|
12
13
|
collectSwiftMixedTestingFrameworkLines,
|
|
13
14
|
collectSwiftQuickNimbleLines,
|
|
@@ -36,6 +37,10 @@ import {
|
|
|
36
37
|
hasSwiftNSErrorThrowUsage,
|
|
37
38
|
collectSwiftPackageBranchDependencyLines,
|
|
38
39
|
hasSwiftPackageBranchDependencyUsage,
|
|
40
|
+
collectSwiftPackageToolsVersionBelow62Lines,
|
|
41
|
+
hasSwiftPackageToolsVersionBelow62Usage,
|
|
42
|
+
collectSwiftStrictConcurrencyBelowCompleteLines,
|
|
43
|
+
hasSwiftStrictConcurrencyBelowCompleteUsage,
|
|
39
44
|
hasSwiftForEachIndicesUsage,
|
|
40
45
|
hasSwiftForEachSelfIdentityUsage,
|
|
41
46
|
collectSwiftForceCastLines,
|
|
@@ -61,7 +66,9 @@ import {
|
|
|
61
66
|
collectSwiftHardcodedUiStringLines,
|
|
62
67
|
collectSwiftNonLazyScrollForEachLines,
|
|
63
68
|
collectSwiftUiForEachConditionalViewCountLines,
|
|
69
|
+
collectSwiftAnimationWithoutReduceMotionLines,
|
|
64
70
|
collectSwiftUiInlineActionLogicLines,
|
|
71
|
+
collectSwiftOnTapGestureWithoutButtonTraitLines,
|
|
65
72
|
collectSwiftForEachIndicesLines,
|
|
66
73
|
collectSwiftForEachSelfIdentityLines,
|
|
67
74
|
collectSwiftInlineForEachTransformLines,
|
|
@@ -79,6 +86,7 @@ import {
|
|
|
79
86
|
hasSwiftEnvironmentObjectUsage,
|
|
80
87
|
hasSwiftLowContrastStaticColorPairUsage,
|
|
81
88
|
hasSwiftMainThreadBlockingSleepUsage,
|
|
89
|
+
hasSwiftDirectSUTInstantiationWithoutMakeSUTUsage,
|
|
82
90
|
hasSwiftMakeSUTWithoutMemoryTrackingUsage,
|
|
83
91
|
hasSwiftMassiveViewControllerResponsibilityUsage,
|
|
84
92
|
hasSwiftMagicNumberLayoutUsage,
|
|
@@ -88,8 +96,13 @@ import {
|
|
|
88
96
|
hasSwiftNestedIfPyramidUsage,
|
|
89
97
|
hasSwiftNonLazyScrollForEachUsage,
|
|
90
98
|
hasSwiftUiForEachConditionalViewCountUsage,
|
|
99
|
+
hasSwiftAnimationWithoutReduceMotionUsage,
|
|
91
100
|
hasSwiftViewBodyObjectCreationUsage,
|
|
92
101
|
hasSwiftUiImageDataDecodingUsage,
|
|
102
|
+
collectSwiftUiManualRenderingWithoutImageRendererLines,
|
|
103
|
+
hasSwiftUiManualRenderingWithoutImageRendererUsage,
|
|
104
|
+
collectSwiftLargeViewBuilderFunctionLines,
|
|
105
|
+
hasSwiftLargeViewBuilderFunctionUsage,
|
|
93
106
|
hasSwiftUiInlineActionLogicUsage,
|
|
94
107
|
hasSwiftAssumeIsolatedUsage,
|
|
95
108
|
hasSwiftCoreDataLayerLeakUsage,
|
|
@@ -110,6 +123,7 @@ import {
|
|
|
110
123
|
hasSwiftOnAppearTaskUsage,
|
|
111
124
|
collectSwiftOnAppearTaskLines,
|
|
112
125
|
hasSwiftOnTapGestureUsage,
|
|
126
|
+
hasSwiftOnTapGestureWithoutButtonTraitUsage,
|
|
113
127
|
hasSwiftOperationQueueUsage,
|
|
114
128
|
hasSwiftContainsUserFilterUsage,
|
|
115
129
|
hasSwiftCustomSingletonUsage,
|
|
@@ -144,6 +158,12 @@ import {
|
|
|
144
158
|
hasSwiftTabItemUsage,
|
|
145
159
|
hasSwiftTaskDetachedUsage,
|
|
146
160
|
hasSwiftUnownedSelfCaptureUsage,
|
|
161
|
+
collectSwiftManualMemoryManagementLines,
|
|
162
|
+
hasSwiftManualMemoryManagementUsage,
|
|
163
|
+
collectSwiftNonPascalCaseTypeDeclarationLines,
|
|
164
|
+
hasSwiftNonPascalCaseTypeDeclarationUsage,
|
|
165
|
+
collectSwiftCellCreationWithoutReuseLines,
|
|
166
|
+
hasSwiftCellCreationWithoutReuseUsage,
|
|
147
167
|
hasSwiftWaitForExpectationsUsage,
|
|
148
168
|
hasSwiftWarningSuppressionUsage,
|
|
149
169
|
hasSwiftUIScreenMainBoundsUsage,
|
|
@@ -234,6 +254,87 @@ let ignored = "[unowned self]"
|
|
|
234
254
|
assert.equal(hasSwiftUnownedSelfCaptureUsage(safe), false);
|
|
235
255
|
});
|
|
236
256
|
|
|
257
|
+
test('hasSwiftManualMemoryManagementUsage detecta bypass manual de ARC', () => {
|
|
258
|
+
const source = `
|
|
259
|
+
final class LegacyBridge {
|
|
260
|
+
func load(_ pointer: UnsafeRawPointer) {
|
|
261
|
+
let value = Unmanaged<NSObject>.fromOpaque(pointer).takeRetainedValue()
|
|
262
|
+
CFRetain(value)
|
|
263
|
+
CFRelease(value)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
`;
|
|
267
|
+
const safe = `
|
|
268
|
+
final class NativeBridge {
|
|
269
|
+
private let value = NSObject()
|
|
270
|
+
func load() -> NSObject {
|
|
271
|
+
let sample = "Unmanaged<NSObject>.fromOpaque(pointer)"
|
|
272
|
+
// CFRelease(value)
|
|
273
|
+
return value
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
`;
|
|
277
|
+
|
|
278
|
+
assert.equal(hasSwiftManualMemoryManagementUsage(source), true);
|
|
279
|
+
assert.deepEqual(collectSwiftManualMemoryManagementLines(source), [4, 5, 6]);
|
|
280
|
+
assert.equal(hasSwiftManualMemoryManagementUsage(safe), false);
|
|
281
|
+
assert.deepEqual(collectSwiftManualMemoryManagementLines(safe), []);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('hasSwiftNonPascalCaseTypeDeclarationUsage detecta tipos Swift sin PascalCase', () => {
|
|
285
|
+
const source = `
|
|
286
|
+
struct user_profile {}
|
|
287
|
+
final class orderView {}
|
|
288
|
+
protocol _service {}
|
|
289
|
+
enum CheckoutState {}
|
|
290
|
+
actor CartActor {}
|
|
291
|
+
`;
|
|
292
|
+
const safe = `
|
|
293
|
+
struct UserProfile {}
|
|
294
|
+
final class OrderView {}
|
|
295
|
+
protocol PaymentService {}
|
|
296
|
+
enum CheckoutState {}
|
|
297
|
+
actor CartActor {}
|
|
298
|
+
let sample = "struct user_profile {}"
|
|
299
|
+
// class orderView {}
|
|
300
|
+
`;
|
|
301
|
+
|
|
302
|
+
assert.equal(hasSwiftNonPascalCaseTypeDeclarationUsage(source), true);
|
|
303
|
+
assert.deepEqual(collectSwiftNonPascalCaseTypeDeclarationLines(source), [2, 3, 4]);
|
|
304
|
+
assert.equal(hasSwiftNonPascalCaseTypeDeclarationUsage(safe), false);
|
|
305
|
+
assert.deepEqual(collectSwiftNonPascalCaseTypeDeclarationLines(safe), []);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('hasSwiftCellCreationWithoutReuseUsage detecta celdas UIKit sin reutilizacion', () => {
|
|
309
|
+
const source = `
|
|
310
|
+
final class OrdersDataSource: NSObject, UITableViewDataSource {
|
|
311
|
+
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
312
|
+
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "OrderCell")
|
|
313
|
+
return cell
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
|
317
|
+
return UICollectionViewCell(frame: .zero)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
`;
|
|
321
|
+
const safe = `
|
|
322
|
+
final class OrdersDataSource: NSObject, UITableViewDataSource {
|
|
323
|
+
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
324
|
+
let cell = tableView.dequeueReusableCell(withIdentifier: "OrderCell", for: indexPath)
|
|
325
|
+
return cell
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
let sample = "UITableViewCell(style: .subtitle, reuseIdentifier: nil)"
|
|
329
|
+
// return UICollectionViewCell(frame: .zero)
|
|
330
|
+
`;
|
|
331
|
+
|
|
332
|
+
assert.equal(hasSwiftCellCreationWithoutReuseUsage(source), true);
|
|
333
|
+
assert.deepEqual(collectSwiftCellCreationWithoutReuseLines(source), [4, 9]);
|
|
334
|
+
assert.equal(hasSwiftCellCreationWithoutReuseUsage(safe), false);
|
|
335
|
+
assert.deepEqual(collectSwiftCellCreationWithoutReuseLines(safe), []);
|
|
336
|
+
});
|
|
337
|
+
|
|
237
338
|
test('hasSwiftNestedIfPyramidUsage detecta pyramid of doom y preserva guard clauses', () => {
|
|
238
339
|
const source = `
|
|
239
340
|
func submit() {
|
|
@@ -447,6 +548,52 @@ let sample = ".package(url: branch:)"
|
|
|
447
548
|
assert.deepEqual(collectSwiftPackageBranchDependencyLines(safe), []);
|
|
448
549
|
});
|
|
449
550
|
|
|
551
|
+
test('hasSwiftPackageToolsVersionBelow62Usage detecta Package.swift iOS bajo Swift 6.2', () => {
|
|
552
|
+
const source = `
|
|
553
|
+
// swift-tools-version: 5.9
|
|
554
|
+
import PackageDescription
|
|
555
|
+
`;
|
|
556
|
+
const sourceSixOne = `
|
|
557
|
+
// swift-tools-version:6.1
|
|
558
|
+
import PackageDescription
|
|
559
|
+
`;
|
|
560
|
+
const safe = `
|
|
561
|
+
// swift-tools-version: 6.2
|
|
562
|
+
import PackageDescription
|
|
563
|
+
// ordinary comment swift-tools-version: 5.9
|
|
564
|
+
let sample = "// swift-tools-version: 5.9"
|
|
565
|
+
`;
|
|
566
|
+
|
|
567
|
+
assert.equal(hasSwiftPackageToolsVersionBelow62Usage(source), true);
|
|
568
|
+
assert.deepEqual(collectSwiftPackageToolsVersionBelow62Lines(source), [2]);
|
|
569
|
+
assert.equal(hasSwiftPackageToolsVersionBelow62Usage(sourceSixOne), true);
|
|
570
|
+
assert.deepEqual(collectSwiftPackageToolsVersionBelow62Lines(sourceSixOne), [2]);
|
|
571
|
+
assert.equal(hasSwiftPackageToolsVersionBelow62Usage(safe), false);
|
|
572
|
+
assert.deepEqual(collectSwiftPackageToolsVersionBelow62Lines(safe), []);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test('hasSwiftStrictConcurrencyBelowCompleteUsage detecta Xcode strict concurrency inferior a complete', () => {
|
|
576
|
+
const source = `
|
|
577
|
+
buildSettings = {
|
|
578
|
+
SWIFT_STRICT_CONCURRENCY = targeted;
|
|
579
|
+
OTHER_SWIFT_FLAGS = "$(inherited)";
|
|
580
|
+
SWIFT_STRICT_CONCURRENCY = minimal;
|
|
581
|
+
};
|
|
582
|
+
`;
|
|
583
|
+
const safe = `
|
|
584
|
+
buildSettings = {
|
|
585
|
+
SWIFT_STRICT_CONCURRENCY = complete;
|
|
586
|
+
let sample = "SWIFT_STRICT_CONCURRENCY = targeted;"
|
|
587
|
+
// SWIFT_STRICT_CONCURRENCY = minimal;
|
|
588
|
+
};
|
|
589
|
+
`;
|
|
590
|
+
|
|
591
|
+
assert.equal(hasSwiftStrictConcurrencyBelowCompleteUsage(source), true);
|
|
592
|
+
assert.deepEqual(collectSwiftStrictConcurrencyBelowCompleteLines(source), [3, 5]);
|
|
593
|
+
assert.equal(hasSwiftStrictConcurrencyBelowCompleteUsage(safe), false);
|
|
594
|
+
assert.deepEqual(collectSwiftStrictConcurrencyBelowCompleteLines(safe), []);
|
|
595
|
+
});
|
|
596
|
+
|
|
450
597
|
test('hasSwiftNonLazyScrollForEachUsage detecta ScrollView con stack no lazy y preserva LazyVStack', () => {
|
|
451
598
|
const source = `
|
|
452
599
|
struct FeedView: View {
|
|
@@ -575,6 +722,74 @@ struct AvatarView: View {
|
|
|
575
722
|
assert.equal(hasSwiftUiImageDataDecodingUsage(safe), false);
|
|
576
723
|
});
|
|
577
724
|
|
|
725
|
+
test('hasSwiftUiManualRenderingWithoutImageRendererUsage detecta render manual de SwiftUI sin ImageRenderer', () => {
|
|
726
|
+
const source = `
|
|
727
|
+
struct ReceiptExporter {
|
|
728
|
+
func image(from receipt: ReceiptView) -> UIImage {
|
|
729
|
+
let controller = UIHostingController(rootView: receipt)
|
|
730
|
+
let renderer = UIGraphicsImageRenderer(size: controller.view.bounds.size)
|
|
731
|
+
return renderer.image { _ in
|
|
732
|
+
controller.view.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
`;
|
|
737
|
+
const safe = `
|
|
738
|
+
struct ReceiptExporter {
|
|
739
|
+
func image(from receipt: ReceiptView) -> UIImage? {
|
|
740
|
+
let renderer = ImageRenderer(content: receipt)
|
|
741
|
+
let sample = "UIHostingController(rootView: receipt)"
|
|
742
|
+
// UIGraphicsImageRenderer(size: .zero)
|
|
743
|
+
return renderer.uiImage
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
`;
|
|
747
|
+
|
|
748
|
+
assert.equal(hasSwiftUiManualRenderingWithoutImageRendererUsage(source), true);
|
|
749
|
+
assert.deepEqual(collectSwiftUiManualRenderingWithoutImageRendererLines(source), [4, 5, 7]);
|
|
750
|
+
assert.equal(hasSwiftUiManualRenderingWithoutImageRendererUsage(safe), false);
|
|
751
|
+
assert.deepEqual(collectSwiftUiManualRenderingWithoutImageRendererLines(safe), []);
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
test('hasSwiftLargeViewBuilderFunctionUsage detecta @ViewBuilder functions grandes y preserva secciones pequenas', () => {
|
|
755
|
+
const source = `
|
|
756
|
+
struct CheckoutView: View {
|
|
757
|
+
@ViewBuilder
|
|
758
|
+
private func checkoutSections() -> some View {
|
|
759
|
+
HeaderView()
|
|
760
|
+
SummaryView()
|
|
761
|
+
AddressView()
|
|
762
|
+
PaymentView()
|
|
763
|
+
CouponView()
|
|
764
|
+
LoyaltyView()
|
|
765
|
+
DeliveryView()
|
|
766
|
+
NotesView()
|
|
767
|
+
LegalView()
|
|
768
|
+
FooterView()
|
|
769
|
+
SupportView()
|
|
770
|
+
TrackingView()
|
|
771
|
+
ConfirmationView()
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
`;
|
|
775
|
+
const safe = `
|
|
776
|
+
struct CheckoutView: View {
|
|
777
|
+
@ViewBuilder
|
|
778
|
+
private func header() -> some View {
|
|
779
|
+
HeaderView()
|
|
780
|
+
SubtitleView()
|
|
781
|
+
let sample = "@ViewBuilder func huge() { A() B() C() }"
|
|
782
|
+
// @ViewBuilder func huge() { A() B() C() }
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
`;
|
|
786
|
+
|
|
787
|
+
assert.equal(hasSwiftLargeViewBuilderFunctionUsage(source), true);
|
|
788
|
+
assert.deepEqual(collectSwiftLargeViewBuilderFunctionLines(source), [3, 4]);
|
|
789
|
+
assert.equal(hasSwiftLargeViewBuilderFunctionUsage(safe), false);
|
|
790
|
+
assert.deepEqual(collectSwiftLargeViewBuilderFunctionLines(safe), []);
|
|
791
|
+
});
|
|
792
|
+
|
|
578
793
|
test('hasSwiftUiInlineActionLogicUsage detecta lógica inline en Button y preserva método referenciado', () => {
|
|
579
794
|
const source = `
|
|
580
795
|
struct CheckoutView: View {
|
|
@@ -612,6 +827,42 @@ struct CheckoutView: View {
|
|
|
612
827
|
assert.deepEqual(collectSwiftUiInlineActionLogicLines(safe), []);
|
|
613
828
|
});
|
|
614
829
|
|
|
830
|
+
test('hasSwiftAnimationWithoutReduceMotionUsage detecta animaciones sin preferencia reduce motion', () => {
|
|
831
|
+
const source = `
|
|
832
|
+
struct CheckoutView: View {
|
|
833
|
+
@State private var expanded = false
|
|
834
|
+
|
|
835
|
+
var body: some View {
|
|
836
|
+
Text("Checkout")
|
|
837
|
+
.animation(.spring(), value: expanded)
|
|
838
|
+
.onTapGesture {
|
|
839
|
+
withAnimation {
|
|
840
|
+
expanded.toggle()
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
`;
|
|
846
|
+
const safe = `
|
|
847
|
+
struct CheckoutView: View {
|
|
848
|
+
@Environment(\\.accessibilityReduceMotion) private var reduceMotion
|
|
849
|
+
@State private var expanded = false
|
|
850
|
+
|
|
851
|
+
var body: some View {
|
|
852
|
+
Text("Checkout")
|
|
853
|
+
.animation(reduceMotion ? nil : .spring(), value: expanded)
|
|
854
|
+
let sample = "withAnimation { expanded.toggle() }"
|
|
855
|
+
// .animation(.spring(), value: expanded)
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
`;
|
|
859
|
+
|
|
860
|
+
assert.equal(hasSwiftAnimationWithoutReduceMotionUsage(source), true);
|
|
861
|
+
assert.deepEqual(collectSwiftAnimationWithoutReduceMotionLines(source), [7, 9]);
|
|
862
|
+
assert.equal(hasSwiftAnimationWithoutReduceMotionUsage(safe), false);
|
|
863
|
+
assert.deepEqual(collectSwiftAnimationWithoutReduceMotionLines(safe), []);
|
|
864
|
+
});
|
|
865
|
+
|
|
615
866
|
test('hasSwiftForEachSelfIdentityUsage detecta id self y preserva ids estables', () => {
|
|
616
867
|
const source = `
|
|
617
868
|
struct FeedView: View {
|
|
@@ -1671,6 +1922,38 @@ MainActor.assumeIsolated { reload() }
|
|
|
1671
1922
|
assert.equal(hasSwiftPassedValueStateWrapperUsage(source), true);
|
|
1672
1923
|
});
|
|
1673
1924
|
|
|
1925
|
+
test('hasSwiftOnTapGestureWithoutButtonTraitUsage detecta tap no semantico sin trait de boton', () => {
|
|
1926
|
+
const source = `
|
|
1927
|
+
struct RowView: View {
|
|
1928
|
+
var body: some View {
|
|
1929
|
+
Image(systemName: "cart")
|
|
1930
|
+
.onTapGesture {
|
|
1931
|
+
addToCart()
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
`;
|
|
1936
|
+
const safe = `
|
|
1937
|
+
struct RowView: View {
|
|
1938
|
+
var body: some View {
|
|
1939
|
+
Image(systemName: "cart")
|
|
1940
|
+
.onTapGesture {
|
|
1941
|
+
addToCart()
|
|
1942
|
+
}
|
|
1943
|
+
.accessibilityAddTraits(.isButton)
|
|
1944
|
+
|
|
1945
|
+
let sample = ".onTapGesture { addToCart() }"
|
|
1946
|
+
// .onTapGesture { addToCart() }
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
`;
|
|
1950
|
+
|
|
1951
|
+
assert.equal(hasSwiftOnTapGestureWithoutButtonTraitUsage(source), true);
|
|
1952
|
+
assert.deepEqual(collectSwiftOnTapGestureWithoutButtonTraitLines(source), [5]);
|
|
1953
|
+
assert.equal(hasSwiftOnTapGestureWithoutButtonTraitUsage(safe), false);
|
|
1954
|
+
assert.deepEqual(collectSwiftOnTapGestureWithoutButtonTraitLines(safe), []);
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1674
1957
|
test('detectores legacy ignoran strings y comentarios', () => {
|
|
1675
1958
|
const source = `
|
|
1676
1959
|
// Task.detached { }
|
|
@@ -2300,6 +2583,47 @@ let sample = "makeSUT() without trackForMemoryLeaks"
|
|
|
2300
2583
|
assert.deepEqual(collectSwiftMakeSUTWithoutMemoryTrackingLines(safe), []);
|
|
2301
2584
|
});
|
|
2302
2585
|
|
|
2586
|
+
test('hasSwiftDirectSUTInstantiationWithoutMakeSUTUsage detecta sut inline sin factory', () => {
|
|
2587
|
+
const source = `
|
|
2588
|
+
import XCTest
|
|
2589
|
+
|
|
2590
|
+
final class BuyerAuthScreenTests: XCTestCase {
|
|
2591
|
+
func testRendersTitle() {
|
|
2592
|
+
let sut = BuyerAuthScreen()
|
|
2593
|
+
XCTAssertNotNil(sut)
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
func testRendersSubtitle() {
|
|
2597
|
+
var sut = BuyerAuthScreen(viewModel: BuyerAuthViewModel())
|
|
2598
|
+
XCTAssertNotNil(sut)
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
`;
|
|
2602
|
+
const safe = `
|
|
2603
|
+
import XCTest
|
|
2604
|
+
|
|
2605
|
+
final class BuyerAuthScreenTests: XCTestCase {
|
|
2606
|
+
func testRendersTitle() {
|
|
2607
|
+
let sut = makeSUT()
|
|
2608
|
+
XCTAssertNotNil(sut)
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
private func makeSUT() -> BuyerAuthScreen {
|
|
2612
|
+
BuyerAuthScreen()
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
let ignored = "let sut = BuyerAuthScreen()"
|
|
2617
|
+
// let sut = BuyerAuthScreen()
|
|
2618
|
+
let notSut = BuyerAuthScreen()
|
|
2619
|
+
`;
|
|
2620
|
+
|
|
2621
|
+
assert.equal(hasSwiftDirectSUTInstantiationWithoutMakeSUTUsage(source), true);
|
|
2622
|
+
assert.deepEqual(collectSwiftDirectSUTInstantiationWithoutMakeSUTLines(source), [6, 11]);
|
|
2623
|
+
assert.equal(hasSwiftDirectSUTInstantiationWithoutMakeSUTUsage(safe), false);
|
|
2624
|
+
assert.deepEqual(collectSwiftDirectSUTInstantiationWithoutMakeSUTLines(safe), []);
|
|
2625
|
+
});
|
|
2626
|
+
|
|
2303
2627
|
test('hasSwiftMixedTestingFrameworksUsage detecta mezcla XCTestCase y Testing/@Test', () => {
|
|
2304
2628
|
const mixedSuite = `
|
|
2305
2629
|
import XCTest
|