pumuki 6.3.270 → 6.3.272
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 +8 -0
- package/VERSION +1 -1
- package/core/facts/detectors/text/android.test.ts +538 -0
- package/core/facts/detectors/text/android.ts +436 -0
- package/core/facts/detectors/text/ios.test.ts +328 -1
- package/core/facts/detectors/text/ios.ts +241 -0
- package/core/facts/detectors/typescript/index.test.ts +393 -0
- package/core/facts/detectors/typescript/index.ts +316 -0
- package/core/facts/extractHeuristicFacts.ts +70 -1
- package/core/rules/presets/heuristics/android.test.ts +91 -1
- package/core/rules/presets/heuristics/android.ts +360 -0
- package/core/rules/presets/heuristics/ios.test.ts +54 -1
- package/core/rules/presets/heuristics/ios.ts +243 -2
- package/core/rules/presets/heuristics/typescript.test.ts +50 -2
- package/core/rules/presets/heuristics/typescript.ts +162 -0
- package/docs/operations/RELEASE_NOTES.md +8 -0
- package/integrations/config/skillsDetectorRegistry.ts +501 -0
- package/integrations/config/skillsRuleClassification.ts +127 -3
- package/integrations/git/runPlatformGate.ts +4 -1
- package/integrations/lifecycle/preWriteAutomation.ts +5 -4
- package/integrations/lifecycle/preWriteLease.ts +41 -4
- package/package.json +1 -1
- package/scripts/classify-skills-rules.ts +2 -2
- package/scripts/framework-menu-consumer-actions-lib.ts +9 -9
- package/scripts/framework-menu-consumer-runtime-actions.ts +53 -117
- package/scripts/framework-menu-consumer-runtime-audit.ts +66 -0
- package/scripts/framework-menu-consumer-runtime-menu.ts +4 -4
- package/scripts/framework-menu-gate-lib.ts +86 -1
- package/scripts/framework-menu-layout-data.ts +3 -3
- package/scripts/framework-menu-legacy-audit-render-sections.ts +6 -0
- package/scripts/framework-menu.ts +10 -6
- package/scripts/package-install-smoke-consumer-npm-lib.ts +10 -4
- package/scripts/package-install-smoke-lifecycle-lib.ts +19 -0
|
@@ -14,12 +14,15 @@ import {
|
|
|
14
14
|
collectSwiftXCTestAssertionLines,
|
|
15
15
|
collectSwiftXCTUnwrapLines,
|
|
16
16
|
collectSwiftAnyViewLines,
|
|
17
|
+
collectSwiftAnyTypeErasureLines,
|
|
17
18
|
collectSwiftCallbackStyleSignatureLines,
|
|
18
19
|
collectSwiftDispatchGroupLines,
|
|
19
20
|
collectSwiftDispatchSemaphoreLines,
|
|
20
21
|
hasSwiftAnyViewUsage,
|
|
22
|
+
hasSwiftAnyTypeErasureUsage,
|
|
21
23
|
hasSwiftAsyncWithoutAwaitUsage,
|
|
22
24
|
hasSwiftCallbackStyleSignature,
|
|
25
|
+
hasSwiftCombineSinkWithoutStoreUsage,
|
|
23
26
|
hasSwiftCornerRadiusUsage,
|
|
24
27
|
hasSwiftDispatchGroupUsage,
|
|
25
28
|
collectSwiftDispatchQueueLines,
|
|
@@ -43,16 +46,23 @@ import {
|
|
|
43
46
|
hasSwiftHardcodedSensitiveStringUsage,
|
|
44
47
|
hasSwiftUnlocalizedDateFormatterUsage,
|
|
45
48
|
hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage,
|
|
49
|
+
collectSwiftInteractiveControlWithoutAccessibilityIdentifierLines,
|
|
50
|
+
hasSwiftInteractiveControlWithoutAccessibilityIdentifierUsage,
|
|
51
|
+
collectSwiftBindableMissingForObservableBindingUsageLines,
|
|
52
|
+
hasSwiftBindableMissingForObservableBindingUsage,
|
|
46
53
|
hasSwiftLooseAssetResourceUsage,
|
|
47
54
|
hasSwiftLegacyOnChangeUsage,
|
|
48
55
|
hasSwiftLegacyExpectationDescriptionUsage,
|
|
56
|
+
hasSwiftLegacyPreviewProviderUsage,
|
|
49
57
|
hasSwiftLegacySwiftUiObservableWrapperUsage,
|
|
58
|
+
hasSwiftLowContrastStaticColorPairUsage,
|
|
50
59
|
hasSwiftMainThreadBlockingSleepUsage,
|
|
51
60
|
hasSwiftMassiveViewControllerResponsibilityUsage,
|
|
52
61
|
hasSwiftMagicNumberLayoutUsage,
|
|
53
62
|
hasSwiftMixedTestingFrameworksUsage,
|
|
54
63
|
hasSwiftLegacyXCTestImportUsage,
|
|
55
64
|
hasSwiftModernizableXCTestSuiteUsage,
|
|
65
|
+
hasSwiftNestedIfPyramidUsage,
|
|
56
66
|
hasSwiftNonLazyScrollForEachUsage,
|
|
57
67
|
hasSwiftUiForEachConditionalViewCountUsage,
|
|
58
68
|
hasSwiftViewBodyObjectCreationUsage,
|
|
@@ -71,6 +81,8 @@ import {
|
|
|
71
81
|
hasSwiftNonIBOutletImplicitlyUnwrappedOptionalUsage,
|
|
72
82
|
hasSwiftObservableObjectUsage,
|
|
73
83
|
hasSwiftOnChangeTaskUsage,
|
|
84
|
+
collectSwiftOnChangeReadonlyVarLines,
|
|
85
|
+
hasSwiftOnChangeReadonlyVarUsage,
|
|
74
86
|
hasSwiftOnAppearTaskUsage,
|
|
75
87
|
hasSwiftOnTapGestureUsage,
|
|
76
88
|
hasSwiftOperationQueueUsage,
|
|
@@ -79,7 +91,10 @@ import {
|
|
|
79
91
|
hasSwiftPassedValueStateWrapperUsage,
|
|
80
92
|
hasSwiftPhysicalTextAlignmentUsage,
|
|
81
93
|
hasSwiftPreconcurrencyUsage,
|
|
94
|
+
hasSwiftProductionCommentUsage,
|
|
95
|
+
hasSwiftProductionTestDoubleUsage,
|
|
82
96
|
hasSwiftQuickNimbleUsage,
|
|
97
|
+
hasSwiftTestDoubleWithoutProtocolConformanceUsage,
|
|
83
98
|
hasSwiftSheetIsPresentedUsage,
|
|
84
99
|
hasSwiftScrollViewShowsIndicatorsUsage,
|
|
85
100
|
hasSwiftSensitiveLoggingUsage,
|
|
@@ -101,6 +116,7 @@ import {
|
|
|
101
116
|
hasSwiftSwinjectUsage,
|
|
102
117
|
hasSwiftTabItemUsage,
|
|
103
118
|
hasSwiftTaskDetachedUsage,
|
|
119
|
+
hasSwiftUnownedSelfCaptureUsage,
|
|
104
120
|
hasSwiftWaitForExpectationsUsage,
|
|
105
121
|
hasSwiftUIScreenMainBoundsUsage,
|
|
106
122
|
hasSwiftXCTestAssertionUsage,
|
|
@@ -142,6 +158,121 @@ if waitersByKey[key] != nil {
|
|
|
142
158
|
assert.deepEqual(collectSwiftForceUnwrapLines(source), []);
|
|
143
159
|
});
|
|
144
160
|
|
|
161
|
+
test('hasSwiftUnownedSelfCaptureUsage detecta captures unowned y preserva weak', () => {
|
|
162
|
+
const source = `
|
|
163
|
+
final class ProfileViewModel {
|
|
164
|
+
func bind() {
|
|
165
|
+
loader.load { [unowned self] value in
|
|
166
|
+
self.value = value
|
|
167
|
+
}
|
|
168
|
+
service.run { [unowned owner] in
|
|
169
|
+
owner.finish()
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
`;
|
|
174
|
+
const safe = `
|
|
175
|
+
loader.load { [weak self] value in
|
|
176
|
+
self?.value = value
|
|
177
|
+
}
|
|
178
|
+
let ignored = "[unowned self]"
|
|
179
|
+
// loader.load { [unowned self] in }
|
|
180
|
+
`;
|
|
181
|
+
|
|
182
|
+
assert.equal(hasSwiftUnownedSelfCaptureUsage(source), true);
|
|
183
|
+
assert.equal(hasSwiftUnownedSelfCaptureUsage(safe), false);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('hasSwiftNestedIfPyramidUsage detecta pyramid of doom y preserva guard clauses', () => {
|
|
187
|
+
const source = `
|
|
188
|
+
func submit() {
|
|
189
|
+
if session.isValid {
|
|
190
|
+
if form.isValid {
|
|
191
|
+
if network.isReachable {
|
|
192
|
+
send()
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
`;
|
|
198
|
+
const safe = `
|
|
199
|
+
func submit() {
|
|
200
|
+
guard session.isValid else { return }
|
|
201
|
+
guard form.isValid else { return }
|
|
202
|
+
if network.isReachable {
|
|
203
|
+
send()
|
|
204
|
+
}
|
|
205
|
+
let ignored = "if a { if b { if c { } } }"
|
|
206
|
+
// if a { if b { if c { } } }
|
|
207
|
+
}
|
|
208
|
+
`;
|
|
209
|
+
|
|
210
|
+
assert.equal(hasSwiftNestedIfPyramidUsage(source), true);
|
|
211
|
+
assert.equal(hasSwiftNestedIfPyramidUsage(safe), false);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('hasSwiftProductionCommentUsage detecta comentarios Swift y preserva strings con URLs', () => {
|
|
215
|
+
const source = `
|
|
216
|
+
func submit() {
|
|
217
|
+
// TODO: remove workaround
|
|
218
|
+
send()
|
|
219
|
+
}
|
|
220
|
+
`;
|
|
221
|
+
const blockSource = `
|
|
222
|
+
func submit() {
|
|
223
|
+
/* legacy workaround */
|
|
224
|
+
send()
|
|
225
|
+
}
|
|
226
|
+
`;
|
|
227
|
+
const safe = `
|
|
228
|
+
let endpoint = "https://api.example.com/v1/orders"
|
|
229
|
+
let sample = "// not a comment"
|
|
230
|
+
send()
|
|
231
|
+
`;
|
|
232
|
+
|
|
233
|
+
assert.equal(hasSwiftProductionCommentUsage(source), true);
|
|
234
|
+
assert.equal(hasSwiftProductionCommentUsage(blockSource), true);
|
|
235
|
+
assert.equal(hasSwiftProductionCommentUsage(safe), false);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('hasSwiftCombineSinkWithoutStoreUsage detecta sink sin store y preserva cancellables', () => {
|
|
239
|
+
const source = `
|
|
240
|
+
publisher
|
|
241
|
+
.sink { value in
|
|
242
|
+
render(value)
|
|
243
|
+
}
|
|
244
|
+
`;
|
|
245
|
+
const safe = `
|
|
246
|
+
publisher
|
|
247
|
+
.sink { value in
|
|
248
|
+
render(value)
|
|
249
|
+
}
|
|
250
|
+
.store(in: &cancellables)
|
|
251
|
+
let ignored = ".sink { value in }"
|
|
252
|
+
// publisher.sink { value in }
|
|
253
|
+
`;
|
|
254
|
+
|
|
255
|
+
assert.equal(hasSwiftCombineSinkWithoutStoreUsage(source), true);
|
|
256
|
+
assert.equal(hasSwiftCombineSinkWithoutStoreUsage(safe), false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('hasSwiftProductionTestDoubleUsage detecta mocks/spies productivos y preserva strings', () => {
|
|
260
|
+
const source = `
|
|
261
|
+
final class MockOrdersRepository: OrdersRepository {
|
|
262
|
+
func fetch() async throws -> [Order] { [] }
|
|
263
|
+
}
|
|
264
|
+
let spy = SpyAnalytics()
|
|
265
|
+
`;
|
|
266
|
+
const safe = `
|
|
267
|
+
final class OrdersRepositoryImpl: OrdersRepository {}
|
|
268
|
+
let ignored = "MockOrdersRepository()"
|
|
269
|
+
// let spy = SpyAnalytics()
|
|
270
|
+
`;
|
|
271
|
+
|
|
272
|
+
assert.equal(hasSwiftProductionTestDoubleUsage(source), true);
|
|
273
|
+
assert.equal(hasSwiftProductionTestDoubleUsage(safe), false);
|
|
274
|
+
});
|
|
275
|
+
|
|
145
276
|
test('hasSwiftAnyViewUsage detecta AnyView en codigo real', () => {
|
|
146
277
|
const source = `
|
|
147
278
|
func render() -> some View {
|
|
@@ -158,6 +289,30 @@ test('hasSwiftAnyViewUsage ignora comentarios, strings y coincidencias parciales
|
|
|
158
289
|
assert.deepEqual(collectSwiftAnyViewLines(source), []);
|
|
159
290
|
});
|
|
160
291
|
|
|
292
|
+
test('hasSwiftAnyTypeErasureUsage detecta Any, AnyObject, AnyHashable y casts en codigo Swift', () => {
|
|
293
|
+
const source = `
|
|
294
|
+
let payload: Any = value
|
|
295
|
+
let cache: [String: Any] = [:]
|
|
296
|
+
let items: [Any] = []
|
|
297
|
+
let object: AnyObject = controller
|
|
298
|
+
let key: AnyHashable = route
|
|
299
|
+
let erased = value as Any
|
|
300
|
+
`;
|
|
301
|
+
assert.equal(hasSwiftAnyTypeErasureUsage(source), true);
|
|
302
|
+
assert.deepEqual(collectSwiftAnyTypeErasureLines(source), [2, 3, 4, 5, 6, 7]);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('hasSwiftAnyTypeErasureUsage ignora AnyView, comentarios y strings', () => {
|
|
306
|
+
const source = `
|
|
307
|
+
let view = AnyView(Text("Title"))
|
|
308
|
+
// let payload: Any = value
|
|
309
|
+
let message = "let object: AnyObject = value"
|
|
310
|
+
let title: String = "Any"
|
|
311
|
+
`;
|
|
312
|
+
assert.equal(hasSwiftAnyTypeErasureUsage(source), false);
|
|
313
|
+
assert.deepEqual(collectSwiftAnyTypeErasureLines(source), []);
|
|
314
|
+
});
|
|
315
|
+
|
|
161
316
|
test('hasSwiftEmptyCatchUsage detecta catch vacio e ignora comentarios y strings', () => {
|
|
162
317
|
const source = `
|
|
163
318
|
do {
|
|
@@ -601,6 +756,45 @@ struct SearchView: View {
|
|
|
601
756
|
assert.equal(hasSwiftOnChangeTaskUsage(safe), false);
|
|
602
757
|
});
|
|
603
758
|
|
|
759
|
+
test('hasSwiftOnChangeReadonlyVarUsage detecta var local dentro de onChange y preserva let', () => {
|
|
760
|
+
const source = `
|
|
761
|
+
struct SearchView: View {
|
|
762
|
+
@State private var query = ""
|
|
763
|
+
|
|
764
|
+
var body: some View {
|
|
765
|
+
Text(query)
|
|
766
|
+
.onChange(of: query) { _, newValue in
|
|
767
|
+
var normalized = newValue.trimmingCharacters(in: .whitespaces)
|
|
768
|
+
search(normalized)
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
`;
|
|
773
|
+
const safe = `
|
|
774
|
+
struct SearchView: View {
|
|
775
|
+
@State private var query = ""
|
|
776
|
+
|
|
777
|
+
var body: some View {
|
|
778
|
+
Text(query)
|
|
779
|
+
.onChange(of: query) { _, newValue in
|
|
780
|
+
let normalized = newValue.trimmingCharacters(in: .whitespaces)
|
|
781
|
+
search(normalized)
|
|
782
|
+
}
|
|
783
|
+
.onChange(of: query) { _, newValue in
|
|
784
|
+
mutate(newValue)
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
let ignored = ".onChange(of: query) { var normalized = query }"
|
|
789
|
+
// .onChange(of: query) { var normalized = query }
|
|
790
|
+
`;
|
|
791
|
+
|
|
792
|
+
assert.equal(hasSwiftOnChangeReadonlyVarUsage(source), true);
|
|
793
|
+
assert.deepEqual(collectSwiftOnChangeReadonlyVarLines(source), [8]);
|
|
794
|
+
assert.equal(hasSwiftOnChangeReadonlyVarUsage(safe), false);
|
|
795
|
+
assert.deepEqual(collectSwiftOnChangeReadonlyVarLines(safe), []);
|
|
796
|
+
});
|
|
797
|
+
|
|
604
798
|
test('hasSwiftStrongDelegateReferenceUsage detecta delegates fuertes y preserva weak delegates', () => {
|
|
605
799
|
const positive = `
|
|
606
800
|
final class CheckoutCoordinator {
|
|
@@ -1132,6 +1326,77 @@ struct ToolbarView: View {
|
|
|
1132
1326
|
assert.equal(hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage(ignored), false);
|
|
1133
1327
|
});
|
|
1134
1328
|
|
|
1329
|
+
test('detector iOS de accesibilidad detecta controles interactivos sin accessibilityIdentifier', () => {
|
|
1330
|
+
const source = `
|
|
1331
|
+
struct CheckoutView: View {
|
|
1332
|
+
var body: some View {
|
|
1333
|
+
VStack {
|
|
1334
|
+
Button("Pay") { pay() }
|
|
1335
|
+
TextField("Email", text: $email)
|
|
1336
|
+
NavigationLink("Details", value: route)
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
`;
|
|
1341
|
+
const safe = `
|
|
1342
|
+
struct CheckoutView: View {
|
|
1343
|
+
var body: some View {
|
|
1344
|
+
VStack {
|
|
1345
|
+
Button("Pay") { pay() }
|
|
1346
|
+
.accessibilityIdentifier("checkout.pay")
|
|
1347
|
+
TextField("Email", text: $email)
|
|
1348
|
+
.accessibilityIdentifier("checkout.email")
|
|
1349
|
+
Toggle("Enabled", isOn: $enabled)
|
|
1350
|
+
.accessibilityIdentifier("checkout.enabled")
|
|
1351
|
+
let sample = "Button(\\"Pay\\")"
|
|
1352
|
+
// Button("Ignored") { pay() }
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
`;
|
|
1357
|
+
|
|
1358
|
+
assert.equal(hasSwiftInteractiveControlWithoutAccessibilityIdentifierUsage(source), true);
|
|
1359
|
+
assert.deepEqual(collectSwiftInteractiveControlWithoutAccessibilityIdentifierLines(source), [5, 6, 7]);
|
|
1360
|
+
assert.equal(hasSwiftInteractiveControlWithoutAccessibilityIdentifierUsage(safe), false);
|
|
1361
|
+
assert.deepEqual(collectSwiftInteractiveControlWithoutAccessibilityIdentifierLines(safe), []);
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
test('detector iOS de Bindable detecta Observable inyectado usado como binding sin @Bindable', () => {
|
|
1365
|
+
const source = `
|
|
1366
|
+
@Observable
|
|
1367
|
+
final class CheckoutForm {
|
|
1368
|
+
var email = ""
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
struct CheckoutView: View {
|
|
1372
|
+
let form: CheckoutForm
|
|
1373
|
+
|
|
1374
|
+
var body: some View {
|
|
1375
|
+
TextField("Email", text: $form.email)
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
`;
|
|
1379
|
+
const safe = `
|
|
1380
|
+
@Observable
|
|
1381
|
+
final class CheckoutForm {
|
|
1382
|
+
var email = ""
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
struct CheckoutView: View {
|
|
1386
|
+
@Bindable var form: CheckoutForm
|
|
1387
|
+
|
|
1388
|
+
var body: some View {
|
|
1389
|
+
TextField("Email", text: $form.email)
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
`;
|
|
1393
|
+
|
|
1394
|
+
assert.equal(hasSwiftBindableMissingForObservableBindingUsage(source), true);
|
|
1395
|
+
assert.deepEqual(collectSwiftBindableMissingForObservableBindingUsageLines(source), [8]);
|
|
1396
|
+
assert.equal(hasSwiftBindableMissingForObservableBindingUsage(safe), false);
|
|
1397
|
+
assert.deepEqual(collectSwiftBindableMissingForObservableBindingUsageLines(safe), []);
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1135
1400
|
test('hasSwiftUncheckedSendableUsage detecta @unchecked Sendable', () => {
|
|
1136
1401
|
const source = `
|
|
1137
1402
|
final class LegacyBox: @unchecked Sendable {}
|
|
@@ -1526,6 +1791,66 @@ struct ContentView: View {
|
|
|
1526
1791
|
assert.equal(hasSwiftLegacySwiftUiObservableWrapperUsage(modernWrapper), false);
|
|
1527
1792
|
});
|
|
1528
1793
|
|
|
1794
|
+
test('hasSwiftLegacyPreviewProviderUsage detecta PreviewProvider legacy y preserva #Preview', () => {
|
|
1795
|
+
const legacyPreview = `
|
|
1796
|
+
struct CheckoutView_Previews: PreviewProvider {
|
|
1797
|
+
static var previews: some View {
|
|
1798
|
+
CheckoutView()
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
`;
|
|
1802
|
+
const modernPreview = `
|
|
1803
|
+
#Preview {
|
|
1804
|
+
CheckoutView()
|
|
1805
|
+
}
|
|
1806
|
+
`;
|
|
1807
|
+
|
|
1808
|
+
assert.equal(hasSwiftLegacyPreviewProviderUsage(legacyPreview), true);
|
|
1809
|
+
assert.equal(hasSwiftLegacyPreviewProviderUsage(modernPreview), false);
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
test('hasSwiftTestDoubleWithoutProtocolConformanceUsage detecta mocks sin protocolo', () => {
|
|
1813
|
+
const invalidMock = `
|
|
1814
|
+
final class MockOrdersRepository {
|
|
1815
|
+
func fetch() async throws -> [Order] { [] }
|
|
1816
|
+
}
|
|
1817
|
+
`;
|
|
1818
|
+
const validMock = `
|
|
1819
|
+
final class MockOrdersRepository: OrdersRepository {
|
|
1820
|
+
func fetch() async throws -> [Order] { [] }
|
|
1821
|
+
}
|
|
1822
|
+
`;
|
|
1823
|
+
|
|
1824
|
+
assert.equal(hasSwiftTestDoubleWithoutProtocolConformanceUsage(invalidMock), true);
|
|
1825
|
+
assert.equal(hasSwiftTestDoubleWithoutProtocolConformanceUsage(validMock), false);
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
test('hasSwiftLowContrastStaticColorPairUsage detecta pares de color estáticos de bajo contraste', () => {
|
|
1829
|
+
const lowContrast = `
|
|
1830
|
+
struct WarningBadge: View {
|
|
1831
|
+
var body: some View {
|
|
1832
|
+
Text("Warning")
|
|
1833
|
+
.foregroundStyle(.white)
|
|
1834
|
+
.background(.yellow)
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
`;
|
|
1838
|
+
const sameColor = `
|
|
1839
|
+
Text("Error")
|
|
1840
|
+
.foregroundColor(Color.red)
|
|
1841
|
+
.background(Color.red)
|
|
1842
|
+
`;
|
|
1843
|
+
const acceptableContrast = `
|
|
1844
|
+
Text("Confirmed")
|
|
1845
|
+
.foregroundStyle(.white)
|
|
1846
|
+
.background(.black)
|
|
1847
|
+
`;
|
|
1848
|
+
|
|
1849
|
+
assert.equal(hasSwiftLowContrastStaticColorPairUsage(lowContrast), true);
|
|
1850
|
+
assert.equal(hasSwiftLowContrastStaticColorPairUsage(sameColor), true);
|
|
1851
|
+
assert.equal(hasSwiftLowContrastStaticColorPairUsage(acceptableContrast), false);
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1529
1854
|
test('hasSwiftNonPrivateStateOwnershipUsage detecta @State y @StateObject no privados', () => {
|
|
1530
1855
|
const source = `
|
|
1531
1856
|
struct DashboardView: View {
|
|
@@ -1793,17 +2118,19 @@ wait(for: [expectation], timeout: 1)
|
|
|
1793
2118
|
waitForExpectations(timeout: 1)
|
|
1794
2119
|
self.wait(for: [expectation], timeout: 1)
|
|
1795
2120
|
XCTWaiter.wait(for: [expectation], timeout: 1)
|
|
2121
|
+
app.buttons["Continue"].waitForExistence(timeout: 1)
|
|
1796
2122
|
`;
|
|
1797
2123
|
const modernWait = `
|
|
1798
2124
|
let expectation = expectation(description: "Done")
|
|
1799
2125
|
await fulfillment(of: [expectation], timeout: 1)
|
|
1800
2126
|
let sample = "waitForExpectations(timeout: 1)"
|
|
1801
2127
|
// wait(for: [expectation], timeout: 1)
|
|
2128
|
+
// app.buttons["Continue"].waitForExistence(timeout: 1)
|
|
1802
2129
|
`;
|
|
1803
2130
|
|
|
1804
2131
|
assert.equal(hasSwiftWaitForExpectationsUsage(legacyWait), true);
|
|
1805
2132
|
assert.equal(hasSwiftWaitForExpectationsUsage(modernWait), false);
|
|
1806
|
-
assert.deepEqual(collectSwiftWaitForExpectationsLines(legacyWait), [3, 4, 5, 6]);
|
|
2133
|
+
assert.deepEqual(collectSwiftWaitForExpectationsLines(legacyWait), [3, 4, 5, 6, 7]);
|
|
1807
2134
|
assert.deepEqual(collectSwiftWaitForExpectationsLines(modernWait), []);
|
|
1808
2135
|
});
|
|
1809
2136
|
|
|
@@ -98,6 +98,79 @@ const hasSwiftSanitizedRegexMatch = (source: string, regex: RegExp): boolean =>
|
|
|
98
98
|
return regex.test(sanitizeSwiftSourceForMultilineRegex(source));
|
|
99
99
|
};
|
|
100
100
|
|
|
101
|
+
const stripSwiftStringLiterals = (line: string): string => {
|
|
102
|
+
return line.replace(/"(?:\\.|[^"\\])*"/g, '""');
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const hasSwiftProductionCommentUsage = (source: string): boolean => {
|
|
106
|
+
return source.split(/\r?\n/).some((rawLine) => {
|
|
107
|
+
const line = stripSwiftStringLiterals(rawLine);
|
|
108
|
+
return /(^|[^:])\/\/|\/\*/.test(line);
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const hasSwiftCombineSinkWithoutStoreUsage = (source: string): boolean => {
|
|
113
|
+
const lines = source.split(/\r?\n/);
|
|
114
|
+
|
|
115
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
116
|
+
const line = stripSwiftLineForSemanticScan(lines[index] ?? '');
|
|
117
|
+
if (!/\.sink\s*(?:\(|\{)/.test(line)) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const chain = lines
|
|
122
|
+
.slice(index, Math.min(lines.length, index + 8))
|
|
123
|
+
.map((candidate) => stripSwiftLineForSemanticScan(candidate))
|
|
124
|
+
.join('\n');
|
|
125
|
+
|
|
126
|
+
if (!/\.store\s*\(\s*in\s*:\s*&[A-Za-z_][A-Za-z0-9_]*\s*\)/.test(chain)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return false;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const hasSwiftProductionTestDoubleUsage = (source: string): boolean => {
|
|
135
|
+
return hasSwiftSanitizedRegexMatch(
|
|
136
|
+
source,
|
|
137
|
+
/\b(?:class|struct|enum|actor)\s+(?:Mock|Fake|Spy|Stub)[A-Za-z0-9_]*\b|\b(?:Mock|Fake|Spy|Stub)[A-Za-z0-9_]*\s*\(/g
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const hasSwiftUnownedSelfCaptureUsage = (source: string): boolean => {
|
|
142
|
+
return hasSwiftSanitizedRegexMatch(
|
|
143
|
+
source,
|
|
144
|
+
/\[\s*unowned\s+(?:self|[A-Za-z_][A-Za-z0-9_]*)\s*\]/g
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const hasSwiftNestedIfPyramidUsage = (source: string): boolean => {
|
|
149
|
+
const lines = source.split(/\r?\n/);
|
|
150
|
+
const ifBraceDepths: number[] = [];
|
|
151
|
+
let braceDepth = 0;
|
|
152
|
+
|
|
153
|
+
for (const rawLine of lines) {
|
|
154
|
+
const line = stripSwiftLineForSemanticScan(rawLine);
|
|
155
|
+
const ifMatches = Array.from(line.matchAll(/\bif\b[^{]*\{/g));
|
|
156
|
+
for (const _match of ifMatches) {
|
|
157
|
+
if (ifBraceDepths.length >= 2) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
ifBraceDepths.push(braceDepth + 1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
braceDepth += countTokenOccurrences(line, '{');
|
|
164
|
+
braceDepth -= countTokenOccurrences(line, '}');
|
|
165
|
+
|
|
166
|
+
while (ifBraceDepths.length > 0 && braceDepth < (ifBraceDepths[ifBraceDepths.length - 1] ?? 0)) {
|
|
167
|
+
ifBraceDepths.pop();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return false;
|
|
172
|
+
};
|
|
173
|
+
|
|
101
174
|
const hasSwiftUiModernizationSnapshotMatch = (source: string, entryId: string): boolean => {
|
|
102
175
|
const entry = getIosSwiftUiModernizationEntry(entryId);
|
|
103
176
|
if (!entry) {
|
|
@@ -398,6 +471,20 @@ export const collectSwiftAnyViewLines = (source: string): readonly number[] => {
|
|
|
398
471
|
return sortedUniqueLines(collectSwiftRegexLines(source, /\bAnyView\b/));
|
|
399
472
|
};
|
|
400
473
|
|
|
474
|
+
export const hasSwiftAnyTypeErasureUsage = (source: string): boolean => {
|
|
475
|
+
return collectSwiftAnyTypeErasureLines(source).length > 0;
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
export const collectSwiftAnyTypeErasureLines = (source: string): readonly number[] => {
|
|
479
|
+
return sortedUniqueLines([
|
|
480
|
+
...collectSwiftRegexLines(
|
|
481
|
+
source,
|
|
482
|
+
/:\s*(?:Any\b|AnyObject\b|AnyHashable\b|\[\s*Any\s*\]|\[[^\]\n]+:\s*Any\s*\]|Dictionary\s*<\s*[^,\n>]+,\s*Any\s*>)/
|
|
483
|
+
),
|
|
484
|
+
...collectSwiftRegexLines(source, /\bas\s+(?:Any|AnyObject|AnyHashable)\b/),
|
|
485
|
+
]);
|
|
486
|
+
};
|
|
487
|
+
|
|
401
488
|
export const collectSwiftCallbackStyleSignatureLines = (source: string): readonly number[] => {
|
|
402
489
|
return sortedUniqueLines(
|
|
403
490
|
collectSwiftRegexLines(
|
|
@@ -574,6 +661,40 @@ export const hasSwiftOnChangeTaskUsage = (source: string): boolean => {
|
|
|
574
661
|
);
|
|
575
662
|
};
|
|
576
663
|
|
|
664
|
+
export const collectSwiftOnChangeReadonlyVarLines = (source: string): readonly number[] => {
|
|
665
|
+
const lines: number[] = [];
|
|
666
|
+
let insideOnChange = false;
|
|
667
|
+
let braceDepth = 0;
|
|
668
|
+
|
|
669
|
+
source.split(/\r?\n/).forEach((line, index) => {
|
|
670
|
+
const sanitizedLine = stripSwiftLineForSemanticScan(line);
|
|
671
|
+
|
|
672
|
+
if (!insideOnChange && /\.onChange\s*\([^)]*\)\s*\{/.test(sanitizedLine)) {
|
|
673
|
+
insideOnChange = true;
|
|
674
|
+
braceDepth = 0;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (insideOnChange && /\bvar\s+[A-Za-z_][A-Za-z0-9_]*\s*=/.test(sanitizedLine)) {
|
|
678
|
+
lines.push(index + 1);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (insideOnChange) {
|
|
682
|
+
const opened = (sanitizedLine.match(/\{/g) ?? []).length;
|
|
683
|
+
const closed = (sanitizedLine.match(/\}/g) ?? []).length;
|
|
684
|
+
braceDepth += opened - closed;
|
|
685
|
+
if (braceDepth <= 0) {
|
|
686
|
+
insideOnChange = false;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
return sortedUniqueLines(lines);
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
export const hasSwiftOnChangeReadonlyVarUsage = (source: string): boolean => {
|
|
695
|
+
return collectSwiftOnChangeReadonlyVarLines(source).length > 0;
|
|
696
|
+
};
|
|
697
|
+
|
|
577
698
|
export const hasSwiftStrongDelegateReferenceUsage = (source: string): boolean => {
|
|
578
699
|
const delegatePropertyPattern =
|
|
579
700
|
/\b(?:var|let)\s+(?:[A-Za-z_][A-Za-z0-9_]*(?:Delegate|DataSource)|delegate|dataSource)\s*:\s*(?:any\s+)?[A-Za-z_][A-Za-z0-9_]*(?:Delegate|DataSource)\b/;
|
|
@@ -933,6 +1054,78 @@ export const hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage = (source: st
|
|
|
933
1054
|
return false;
|
|
934
1055
|
};
|
|
935
1056
|
|
|
1057
|
+
export const collectSwiftInteractiveControlWithoutAccessibilityIdentifierLines = (
|
|
1058
|
+
source: string
|
|
1059
|
+
): readonly number[] => {
|
|
1060
|
+
const lines = sanitizeSwiftSourceForMultilineRegex(source).split(/\r?\n/);
|
|
1061
|
+
const matches: number[] = [];
|
|
1062
|
+
const interactiveControlPattern =
|
|
1063
|
+
/\b(?:Button|TextField|SecureField|Toggle|NavigationLink)\s*(?:<[^>]+>)?\s*\(/;
|
|
1064
|
+
|
|
1065
|
+
lines.forEach((line, index) => {
|
|
1066
|
+
if (!interactiveControlPattern.test(line)) {
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
const window = lines.slice(index, Math.min(lines.length, index + 7)).join('\n');
|
|
1070
|
+
if (!/\.\s*accessibilityIdentifier\s*\(/.test(window)) {
|
|
1071
|
+
matches.push(index + 1);
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
return sortedUniqueLines(matches);
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
export const hasSwiftInteractiveControlWithoutAccessibilityIdentifierUsage = (
|
|
1079
|
+
source: string
|
|
1080
|
+
): boolean => {
|
|
1081
|
+
return collectSwiftInteractiveControlWithoutAccessibilityIdentifierLines(source).length > 0;
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
const collectSwiftBindableMissingForObservableBindingLines = (
|
|
1085
|
+
source: string
|
|
1086
|
+
): readonly number[] => {
|
|
1087
|
+
const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
|
|
1088
|
+
if (!/@\s*Observable\b/.test(sanitized)) {
|
|
1089
|
+
return [];
|
|
1090
|
+
}
|
|
1091
|
+
const lines = sanitized.split(/\r?\n/);
|
|
1092
|
+
const bindableDeclarations = new Set<string>();
|
|
1093
|
+
const plainObservableCandidates = new Map<string, number>();
|
|
1094
|
+
|
|
1095
|
+
lines.forEach((line, index) => {
|
|
1096
|
+
const bindableMatch = line.match(/@\s*Bindable\s+(?:private\s+)?var\s+([A-Za-z_][A-Za-z0-9_]*)\s*:/);
|
|
1097
|
+
if (bindableMatch?.[1]) {
|
|
1098
|
+
bindableDeclarations.add(bindableMatch[1]);
|
|
1099
|
+
}
|
|
1100
|
+
const plainMatch = line.match(/\b(?:let|var)\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*[A-Za-z_][A-Za-z0-9_]*/);
|
|
1101
|
+
if (plainMatch?.[1] && !/@\s*Bindable\b/.test(line)) {
|
|
1102
|
+
plainObservableCandidates.set(plainMatch[1], index + 1);
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
const matches: number[] = [];
|
|
1107
|
+
for (const [name, line] of plainObservableCandidates) {
|
|
1108
|
+
if (bindableDeclarations.has(name)) {
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
if (sanitized.includes(`$${name}.`)) {
|
|
1112
|
+
matches.push(line);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
return sortedUniqueLines(matches);
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
export const collectSwiftBindableMissingForObservableBindingUsageLines = (
|
|
1120
|
+
source: string
|
|
1121
|
+
): readonly number[] => {
|
|
1122
|
+
return collectSwiftBindableMissingForObservableBindingLines(source);
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
export const hasSwiftBindableMissingForObservableBindingUsage = (source: string): boolean => {
|
|
1126
|
+
return collectSwiftBindableMissingForObservableBindingLines(source).length > 0;
|
|
1127
|
+
};
|
|
1128
|
+
|
|
936
1129
|
export const hasSwiftUncheckedSendableUsage = (source: string): boolean => {
|
|
937
1130
|
return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
|
|
938
1131
|
if (current !== '@' || !swiftSource.startsWith('@unchecked', index)) {
|
|
@@ -966,6 +1159,53 @@ export const hasSwiftObservableObjectUsage = (source: string): boolean => {
|
|
|
966
1159
|
});
|
|
967
1160
|
};
|
|
968
1161
|
|
|
1162
|
+
export const hasSwiftLegacyPreviewProviderUsage = (source: string): boolean => {
|
|
1163
|
+
return hasSwiftSanitizedRegexMatch(
|
|
1164
|
+
source,
|
|
1165
|
+
/\b(?:struct|enum|final\s+class|class)\s+[A-Za-z_][A-Za-z0-9_]*\s*:\s*PreviewProvider\b/g
|
|
1166
|
+
);
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
export const hasSwiftTestDoubleWithoutProtocolConformanceUsage = (source: string): boolean => {
|
|
1170
|
+
return source.split(/\r?\n/).some((line) => {
|
|
1171
|
+
const sanitizedLine = stripSwiftLineForSemanticScan(line);
|
|
1172
|
+
return /\b(?:final\s+)?class\s+(?:Mock|Fake|Spy|Stub)[A-Za-z0-9_]*\s*\{/.test(
|
|
1173
|
+
sanitizedLine
|
|
1174
|
+
);
|
|
1175
|
+
});
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
const extractSwiftColorToken = (value: string): string | null => {
|
|
1179
|
+
const match = value.match(/\b(?:Color\s*\.\s*)?([A-Za-z][A-Za-z0-9_]*)\b/);
|
|
1180
|
+
return match?.[1]?.toLowerCase() ?? null;
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
const isLowContrastSwiftColorPair = (foreground: string, background: string): boolean => {
|
|
1184
|
+
if (foreground === background) {
|
|
1185
|
+
return true;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const pair = `${foreground}:${background}`;
|
|
1189
|
+
return new Set(['white:yellow', 'yellow:white', 'white:cyan', 'cyan:white']).has(pair);
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
export const hasSwiftLowContrastStaticColorPairUsage = (source: string): boolean => {
|
|
1193
|
+
const sanitizedSource = sanitizeSwiftSourceForMultilineRegex(source);
|
|
1194
|
+
const chainPattern =
|
|
1195
|
+
/\.(?:foregroundStyle|foregroundColor)\s*\(([^)]{1,80})\)([\s\S]{0,300}?)(?:\.(?:background|fill)\s*\(([^)]{1,80})\))/g;
|
|
1196
|
+
let match: RegExpExecArray | null;
|
|
1197
|
+
|
|
1198
|
+
while ((match = chainPattern.exec(sanitizedSource)) !== null) {
|
|
1199
|
+
const foreground = extractSwiftColorToken(match[1] ?? '');
|
|
1200
|
+
const background = extractSwiftColorToken(match[3] ?? '');
|
|
1201
|
+
if (foreground && background && isLowContrastSwiftColorPair(foreground, background)) {
|
|
1202
|
+
return true;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
return false;
|
|
1207
|
+
};
|
|
1208
|
+
|
|
969
1209
|
export const hasSwiftForEachIndicesUsage = (source: string): boolean => {
|
|
970
1210
|
return hasSwiftSanitizedRegexMatch(
|
|
971
1211
|
source,
|
|
@@ -1459,6 +1699,7 @@ export const collectSwiftWaitForExpectationsLines = (source: string): readonly n
|
|
|
1459
1699
|
...collectSwiftRegexLines(source, /\b(?:self\s*\.\s*)?wait\s*\(\s*for\s*:/),
|
|
1460
1700
|
...collectSwiftRegexLines(source, /\bwaitForExpectations\s*\(/),
|
|
1461
1701
|
...collectSwiftRegexLines(source, /\bXCTWaiter\s*\.\s*wait\s*\(\s*for\s*:/),
|
|
1702
|
+
...collectSwiftRegexLines(source, /\bwaitForExistence\s*\(/),
|
|
1462
1703
|
]);
|
|
1463
1704
|
};
|
|
1464
1705
|
|