pumuki 6.3.296 → 6.3.298
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 +407 -0
- package/core/facts/detectors/text/ios.ts +328 -0
- package/core/facts/extractHeuristicFacts.ts +17 -0
- package/core/rules/presets/heuristics/ios.test.ts +61 -1
- package/core/rules/presets/heuristics/ios.ts +223 -0
- package/integrations/config/skillsDetectorRegistry.ts +55 -2
- package/integrations/gate/evaluateAiGate.ts +37 -7
- 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
|
+
- 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.
|
|
6
|
+
|
|
7
|
+
## [6.3.297] - 2026-05-19
|
|
8
|
+
|
|
9
|
+
- `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.
|
|
10
|
+
|
|
3
11
|
## [6.3.296] - 2026-05-19
|
|
4
12
|
|
|
5
13
|
- `PUMUKI-INC-156`: follow-up after RuralGo replay. Broad SwiftUI findings are now considered non-actionable even when they include a `primary_node` if that node spans many lines. Only a bounded AST/node range can hard-block a staged diff; file-level brownfield SwiftUI debt remains advisory for the atomic slice.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.3.
|
|
1
|
+
6.3.298
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
findSwiftPresentationSrpMatch,
|
|
9
9
|
collectSwiftModernizableXCTestSuiteLines,
|
|
10
10
|
collectSwiftLegacyExpectationDescriptionLines,
|
|
11
|
+
collectSwiftDirectSUTInstantiationWithoutMakeSUTLines,
|
|
12
|
+
collectSwiftMakeSUTWithoutMemoryTrackingLines,
|
|
11
13
|
collectSwiftMixedTestingFrameworkLines,
|
|
12
14
|
collectSwiftQuickNimbleLines,
|
|
13
15
|
collectSwiftWaitForExpectationsLines,
|
|
@@ -35,6 +37,10 @@ import {
|
|
|
35
37
|
hasSwiftNSErrorThrowUsage,
|
|
36
38
|
collectSwiftPackageBranchDependencyLines,
|
|
37
39
|
hasSwiftPackageBranchDependencyUsage,
|
|
40
|
+
collectSwiftPackageToolsVersionBelow62Lines,
|
|
41
|
+
hasSwiftPackageToolsVersionBelow62Usage,
|
|
42
|
+
collectSwiftStrictConcurrencyBelowCompleteLines,
|
|
43
|
+
hasSwiftStrictConcurrencyBelowCompleteUsage,
|
|
38
44
|
hasSwiftForEachIndicesUsage,
|
|
39
45
|
hasSwiftForEachSelfIdentityUsage,
|
|
40
46
|
collectSwiftForceCastLines,
|
|
@@ -60,7 +66,9 @@ import {
|
|
|
60
66
|
collectSwiftHardcodedUiStringLines,
|
|
61
67
|
collectSwiftNonLazyScrollForEachLines,
|
|
62
68
|
collectSwiftUiForEachConditionalViewCountLines,
|
|
69
|
+
collectSwiftAnimationWithoutReduceMotionLines,
|
|
63
70
|
collectSwiftUiInlineActionLogicLines,
|
|
71
|
+
collectSwiftOnTapGestureWithoutButtonTraitLines,
|
|
64
72
|
collectSwiftForEachIndicesLines,
|
|
65
73
|
collectSwiftForEachSelfIdentityLines,
|
|
66
74
|
collectSwiftInlineForEachTransformLines,
|
|
@@ -78,6 +86,8 @@ import {
|
|
|
78
86
|
hasSwiftEnvironmentObjectUsage,
|
|
79
87
|
hasSwiftLowContrastStaticColorPairUsage,
|
|
80
88
|
hasSwiftMainThreadBlockingSleepUsage,
|
|
89
|
+
hasSwiftDirectSUTInstantiationWithoutMakeSUTUsage,
|
|
90
|
+
hasSwiftMakeSUTWithoutMemoryTrackingUsage,
|
|
81
91
|
hasSwiftMassiveViewControllerResponsibilityUsage,
|
|
82
92
|
hasSwiftMagicNumberLayoutUsage,
|
|
83
93
|
hasSwiftMixedTestingFrameworksUsage,
|
|
@@ -86,8 +96,13 @@ import {
|
|
|
86
96
|
hasSwiftNestedIfPyramidUsage,
|
|
87
97
|
hasSwiftNonLazyScrollForEachUsage,
|
|
88
98
|
hasSwiftUiForEachConditionalViewCountUsage,
|
|
99
|
+
hasSwiftAnimationWithoutReduceMotionUsage,
|
|
89
100
|
hasSwiftViewBodyObjectCreationUsage,
|
|
90
101
|
hasSwiftUiImageDataDecodingUsage,
|
|
102
|
+
collectSwiftUiManualRenderingWithoutImageRendererLines,
|
|
103
|
+
hasSwiftUiManualRenderingWithoutImageRendererUsage,
|
|
104
|
+
collectSwiftLargeViewBuilderFunctionLines,
|
|
105
|
+
hasSwiftLargeViewBuilderFunctionUsage,
|
|
91
106
|
hasSwiftUiInlineActionLogicUsage,
|
|
92
107
|
hasSwiftAssumeIsolatedUsage,
|
|
93
108
|
hasSwiftCoreDataLayerLeakUsage,
|
|
@@ -108,6 +123,7 @@ import {
|
|
|
108
123
|
hasSwiftOnAppearTaskUsage,
|
|
109
124
|
collectSwiftOnAppearTaskLines,
|
|
110
125
|
hasSwiftOnTapGestureUsage,
|
|
126
|
+
hasSwiftOnTapGestureWithoutButtonTraitUsage,
|
|
111
127
|
hasSwiftOperationQueueUsage,
|
|
112
128
|
hasSwiftContainsUserFilterUsage,
|
|
113
129
|
hasSwiftCustomSingletonUsage,
|
|
@@ -128,6 +144,7 @@ import {
|
|
|
128
144
|
hasSwiftExplicitColorStaticMemberUsage,
|
|
129
145
|
hasSwiftClosureBasedViewBuilderContentUsage,
|
|
130
146
|
collectSwiftForceUnwrapLines,
|
|
147
|
+
collectSwiftUIKitManualFrameLayoutLines,
|
|
131
148
|
collectSwiftWarningSuppressionLines,
|
|
132
149
|
hasSwiftLargeConfigContextViewPropertyUsage,
|
|
133
150
|
hasSwiftUiConditionalSameViewIdentityUsage,
|
|
@@ -141,12 +158,19 @@ import {
|
|
|
141
158
|
hasSwiftTabItemUsage,
|
|
142
159
|
hasSwiftTaskDetachedUsage,
|
|
143
160
|
hasSwiftUnownedSelfCaptureUsage,
|
|
161
|
+
collectSwiftManualMemoryManagementLines,
|
|
162
|
+
hasSwiftManualMemoryManagementUsage,
|
|
163
|
+
collectSwiftNonPascalCaseTypeDeclarationLines,
|
|
164
|
+
hasSwiftNonPascalCaseTypeDeclarationUsage,
|
|
165
|
+
collectSwiftCellCreationWithoutReuseLines,
|
|
166
|
+
hasSwiftCellCreationWithoutReuseUsage,
|
|
144
167
|
hasSwiftWaitForExpectationsUsage,
|
|
145
168
|
hasSwiftWarningSuppressionUsage,
|
|
146
169
|
hasSwiftUIScreenMainBoundsUsage,
|
|
147
170
|
hasSwiftXCTestAssertionUsage,
|
|
148
171
|
hasSwiftXCTUnwrapUsage,
|
|
149
172
|
hasSwiftUncheckedSendableUsage,
|
|
173
|
+
hasSwiftUIKitManualFrameLayoutUsage,
|
|
150
174
|
} from './ios';
|
|
151
175
|
|
|
152
176
|
test('hasSwiftForceUnwrap detecta force unwrap postfix en expresiones', () => {
|
|
@@ -230,6 +254,87 @@ let ignored = "[unowned self]"
|
|
|
230
254
|
assert.equal(hasSwiftUnownedSelfCaptureUsage(safe), false);
|
|
231
255
|
});
|
|
232
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
|
+
|
|
233
338
|
test('hasSwiftNestedIfPyramidUsage detecta pyramid of doom y preserva guard clauses', () => {
|
|
234
339
|
const source = `
|
|
235
340
|
func submit() {
|
|
@@ -443,6 +548,52 @@ let sample = ".package(url: branch:)"
|
|
|
443
548
|
assert.deepEqual(collectSwiftPackageBranchDependencyLines(safe), []);
|
|
444
549
|
});
|
|
445
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
|
+
|
|
446
597
|
test('hasSwiftNonLazyScrollForEachUsage detecta ScrollView con stack no lazy y preserva LazyVStack', () => {
|
|
447
598
|
const source = `
|
|
448
599
|
struct FeedView: View {
|
|
@@ -571,6 +722,74 @@ struct AvatarView: View {
|
|
|
571
722
|
assert.equal(hasSwiftUiImageDataDecodingUsage(safe), false);
|
|
572
723
|
});
|
|
573
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
|
+
|
|
574
793
|
test('hasSwiftUiInlineActionLogicUsage detecta lógica inline en Button y preserva método referenciado', () => {
|
|
575
794
|
const source = `
|
|
576
795
|
struct CheckoutView: View {
|
|
@@ -608,6 +827,42 @@ struct CheckoutView: View {
|
|
|
608
827
|
assert.deepEqual(collectSwiftUiInlineActionLogicLines(safe), []);
|
|
609
828
|
});
|
|
610
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
|
+
|
|
611
866
|
test('hasSwiftForEachSelfIdentityUsage detecta id self y preserva ids estables', () => {
|
|
612
867
|
const source = `
|
|
613
868
|
struct FeedView: View {
|
|
@@ -1129,6 +1384,44 @@ struct ProfileView: View {
|
|
|
1129
1384
|
assert.deepEqual(collectSwiftMagicNumberLayoutLines(constants), []);
|
|
1130
1385
|
});
|
|
1131
1386
|
|
|
1387
|
+
test('hasSwiftUIKitManualFrameLayoutUsage detecta layout manual UIKit y preserva Auto Layout y SwiftUI frame', () => {
|
|
1388
|
+
const source = `
|
|
1389
|
+
final class CheckoutView: UIView {
|
|
1390
|
+
private let titleLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 320, height: 44))
|
|
1391
|
+
|
|
1392
|
+
func layoutBadge() {
|
|
1393
|
+
badgeView.frame = CGRect(x: 16, y: 16, width: 80, height: 32)
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
`;
|
|
1397
|
+
const safe = `
|
|
1398
|
+
final class CheckoutView: UIView {
|
|
1399
|
+
private let titleLabel = UILabel()
|
|
1400
|
+
|
|
1401
|
+
func installConstraints() {
|
|
1402
|
+
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
1403
|
+
NSLayoutConstraint.activate([
|
|
1404
|
+
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor)
|
|
1405
|
+
])
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
struct CheckoutSwiftUIView: View {
|
|
1410
|
+
var body: some View {
|
|
1411
|
+
Text("Checkout").frame(maxWidth: .infinity)
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
let sample = "UILabel(frame: CGRect(x: 0, y: 0, width: 10, height: 10))"
|
|
1416
|
+
// badgeView.frame = CGRect(x: 0, y: 0, width: 10, height: 10)
|
|
1417
|
+
`;
|
|
1418
|
+
|
|
1419
|
+
assert.equal(hasSwiftUIKitManualFrameLayoutUsage(source), true);
|
|
1420
|
+
assert.deepEqual(collectSwiftUIKitManualFrameLayoutLines(source), [3, 6]);
|
|
1421
|
+
assert.equal(hasSwiftUIKitManualFrameLayoutUsage(safe), false);
|
|
1422
|
+
assert.deepEqual(collectSwiftUIKitManualFrameLayoutLines(safe), []);
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1132
1425
|
test('detectores de logging iOS detectan logs ad-hoc y PII en produccion', () => {
|
|
1133
1426
|
const adHoc = `
|
|
1134
1427
|
print(user.id)
|
|
@@ -1629,6 +1922,38 @@ MainActor.assumeIsolated { reload() }
|
|
|
1629
1922
|
assert.equal(hasSwiftPassedValueStateWrapperUsage(source), true);
|
|
1630
1923
|
});
|
|
1631
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
|
+
|
|
1632
1957
|
test('detectores legacy ignoran strings y comentarios', () => {
|
|
1633
1958
|
const source = `
|
|
1634
1959
|
// Task.detached { }
|
|
@@ -2217,6 +2542,88 @@ final class BuyerOnboardingStringsTests: XCTestCase {
|
|
|
2217
2542
|
assert.deepEqual(collectSwiftXCTUnwrapLines(brownfieldSpec), []);
|
|
2218
2543
|
});
|
|
2219
2544
|
|
|
2545
|
+
test('hasSwiftMakeSUTWithoutMemoryTrackingUsage detecta specs con makeSUT sin trackForMemoryLeaks', () => {
|
|
2546
|
+
const source = `
|
|
2547
|
+
import XCTest
|
|
2548
|
+
|
|
2549
|
+
final class BuyerAuthScreenTests: XCTestCase {
|
|
2550
|
+
func testRendersTitle() {
|
|
2551
|
+
let sut = makeSUT()
|
|
2552
|
+
XCTAssertNotNil(sut)
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
private func makeSUT() -> BuyerAuthScreen {
|
|
2556
|
+
BuyerAuthScreen()
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
`;
|
|
2560
|
+
const safe = `
|
|
2561
|
+
import XCTest
|
|
2562
|
+
|
|
2563
|
+
final class BuyerAuthScreenTests: XCTestCase {
|
|
2564
|
+
func testRendersTitle() {
|
|
2565
|
+
let sut = makeSUT()
|
|
2566
|
+
XCTAssertNotNil(sut)
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
private func makeSUT() -> BuyerAuthScreen {
|
|
2570
|
+
let sut = BuyerAuthScreen()
|
|
2571
|
+
trackForMemoryLeaks(sut)
|
|
2572
|
+
return sut
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
let sample = "makeSUT() without trackForMemoryLeaks"
|
|
2577
|
+
// private func makeSUT() -> Sample { Sample() }
|
|
2578
|
+
`;
|
|
2579
|
+
|
|
2580
|
+
assert.equal(hasSwiftMakeSUTWithoutMemoryTrackingUsage(source), true);
|
|
2581
|
+
assert.deepEqual(collectSwiftMakeSUTWithoutMemoryTrackingLines(source), [6, 10]);
|
|
2582
|
+
assert.equal(hasSwiftMakeSUTWithoutMemoryTrackingUsage(safe), false);
|
|
2583
|
+
assert.deepEqual(collectSwiftMakeSUTWithoutMemoryTrackingLines(safe), []);
|
|
2584
|
+
});
|
|
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
|
+
|
|
2220
2627
|
test('hasSwiftMixedTestingFrameworksUsage detecta mezcla XCTestCase y Testing/@Test', () => {
|
|
2221
2628
|
const mixedSuite = `
|
|
2222
2629
|
import XCTest
|