pumuki 6.3.269 → 6.3.271

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/VERSION +1 -1
  3. package/core/facts/detectors/text/android.test.ts +538 -0
  4. package/core/facts/detectors/text/android.ts +436 -0
  5. package/core/facts/detectors/text/ios.test.ts +328 -1
  6. package/core/facts/detectors/text/ios.ts +241 -0
  7. package/core/facts/detectors/typescript/index.test.ts +393 -0
  8. package/core/facts/detectors/typescript/index.ts +316 -0
  9. package/core/facts/extractHeuristicFacts.ts +70 -1
  10. package/core/rules/presets/heuristics/android.test.ts +91 -1
  11. package/core/rules/presets/heuristics/android.ts +360 -0
  12. package/core/rules/presets/heuristics/ios.test.ts +54 -1
  13. package/core/rules/presets/heuristics/ios.ts +243 -2
  14. package/core/rules/presets/heuristics/typescript.test.ts +50 -2
  15. package/core/rules/presets/heuristics/typescript.ts +162 -0
  16. package/docs/operations/RELEASE_NOTES.md +4 -0
  17. package/integrations/config/skillsDetectorRegistry.ts +501 -0
  18. package/integrations/config/skillsRuleClassification.ts +127 -3
  19. package/integrations/context/contextGate.ts +192 -0
  20. package/integrations/git/runPlatformGate.ts +4 -1
  21. package/integrations/lifecycle/preWriteAutomation.ts +1 -0
  22. package/integrations/lifecycle/preWriteLease.ts +41 -4
  23. package/package.json +2 -1
  24. package/scripts/classify-skills-rules.ts +2 -2
  25. package/scripts/framework-menu-consumer-actions-lib.ts +9 -9
  26. package/scripts/framework-menu-consumer-runtime-actions.ts +53 -117
  27. package/scripts/framework-menu-consumer-runtime-audit.ts +66 -0
  28. package/scripts/framework-menu-consumer-runtime-menu.ts +4 -4
  29. package/scripts/framework-menu-gate-lib.ts +86 -1
  30. package/scripts/framework-menu-layout-data.ts +3 -3
  31. package/scripts/framework-menu-legacy-audit-render-sections.ts +6 -0
  32. package/scripts/framework-menu.ts +10 -6
  33. package/scripts/package-install-smoke-consumer-npm-lib.ts +10 -4
  34. package/scripts/package-install-smoke-lifecycle-lib.ts +19 -0
  35. package/scripts/package-manifest-lib.ts +1 -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