pumuki 6.3.306 → 6.3.308

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.
@@ -204,6 +204,25 @@ export const hasSwiftNonPascalCaseTypeDeclarationUsage = (source: string): boole
204
204
  return collectSwiftNonPascalCaseTypeDeclarationLines(source).length > 0;
205
205
  };
206
206
 
207
+ export const collectSwiftEndpointEnumLines = (source: string): readonly number[] => {
208
+ const matches: number[] = [];
209
+ const endpointEnumPattern =
210
+ /\benum\s+(?:APIEndpoint|[A-Za-z_][A-Za-z0-9_]*(?:Endpoint|Endpoints))\b/;
211
+
212
+ source.split(/\r?\n/).forEach((rawLine, index) => {
213
+ const line = stripSwiftLineForSemanticScan(rawLine);
214
+ if (endpointEnumPattern.test(line)) {
215
+ matches.push(index + 1);
216
+ }
217
+ });
218
+
219
+ return sortedUniqueLines(matches);
220
+ };
221
+
222
+ export const hasSwiftEndpointEnumUsage = (source: string): boolean => {
223
+ return collectSwiftEndpointEnumLines(source).length > 0;
224
+ };
225
+
207
226
  const collectSwiftMethodBodyLines = (
208
227
  source: string,
209
228
  methodPattern: RegExp,
@@ -857,6 +876,62 @@ export const hasSwiftTaskDetachedUsage = (source: string): boolean => {
857
876
  });
858
877
  };
859
878
 
879
+ const swiftCancellationCheckPattern =
880
+ /\bTask\s*\.\s*isCancelled\b|\bTask\s*\.\s*checkCancellation\s*\(/;
881
+
882
+ const swiftLoopPattern = /\b(?:for\s+[A-Za-z_][A-Za-z0-9_]*\s+in|while\s+|repeat\b)/;
883
+
884
+ export const collectSwiftLongAsyncOperationWithoutCancellationCheckLines = (
885
+ source: string
886
+ ): readonly number[] => {
887
+ const lines = source.split(/\r?\n/);
888
+ const matches: number[] = [];
889
+
890
+ for (let index = 0; index < lines.length; index += 1) {
891
+ const declarationLine = stripSwiftLineForSemanticScan(lines[index] ?? '');
892
+ if (!/\bfunc\s+[A-Za-z_][A-Za-z0-9_]*[\s\S]*\basync\b/.test(declarationLine)) {
893
+ continue;
894
+ }
895
+
896
+ let braceDepth =
897
+ countTokenOccurrences(declarationLine, '{') - countTokenOccurrences(declarationLine, '}');
898
+ if (braceDepth <= 0 && !declarationLine.includes('{')) {
899
+ continue;
900
+ }
901
+
902
+ const localLoopLines: number[] = [];
903
+ let hasCancellationCheck = swiftCancellationCheckPattern.test(declarationLine);
904
+
905
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
906
+ const line = stripSwiftLineForSemanticScan(lines[cursor] ?? '');
907
+ if (swiftCancellationCheckPattern.test(line)) {
908
+ hasCancellationCheck = true;
909
+ }
910
+ if (swiftLoopPattern.test(line)) {
911
+ localLoopLines.push(cursor + 1);
912
+ }
913
+
914
+ braceDepth += countTokenOccurrences(line, '{');
915
+ braceDepth -= countTokenOccurrences(line, '}');
916
+ if (braceDepth <= 0) {
917
+ break;
918
+ }
919
+ }
920
+
921
+ if (!hasCancellationCheck && localLoopLines.length > 0) {
922
+ matches.push(...localLoopLines);
923
+ }
924
+ }
925
+
926
+ return sortedUniqueLines(matches);
927
+ };
928
+
929
+ export const hasSwiftLongAsyncOperationWithoutCancellationCheckUsage = (
930
+ source: string
931
+ ): boolean => {
932
+ return collectSwiftLongAsyncOperationWithoutCancellationCheckLines(source).length > 0;
933
+ };
934
+
860
935
  const findMatchingSwiftBrace = (source: string, openBraceIndex: number): number => {
861
936
  let depth = 0;
862
937
  for (let index = openBraceIndex; index < source.length; index += 1) {
@@ -899,6 +974,20 @@ export const hasSwiftAsyncWithoutAwaitUsage = (source: string): boolean => {
899
974
  return false;
900
975
  };
901
976
 
977
+ export const collectSwiftDummyAwaitLines = (source: string): readonly number[] => {
978
+ return sortedUniqueLines([
979
+ ...collectSwiftRegexLines(source, /\bawait\s+Task\s*\.\s*yield\s*\(\s*\)/),
980
+ ...collectSwiftRegexLines(
981
+ source,
982
+ /\bawait\s+Task\s*\.\s*sleep\s*\([^)]*(?:nanoseconds\s*:\s*0\b|for\s*:\s*\.(?:zero|seconds\s*\(\s*0\s*\)))/,
983
+ ),
984
+ ]);
985
+ };
986
+
987
+ export const hasSwiftDummyAwaitUsage = (source: string): boolean => {
988
+ return collectSwiftDummyAwaitLines(source).length > 0;
989
+ };
990
+
902
991
  export const hasSwiftEmptyCatchUsage = (source: string): boolean => {
903
992
  const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
904
993
  return /\bcatch(?:\s+(?:let|var)\s+[A-Za-z_][A-Za-z0-9_]*)?\s*\{\s*\}/.test(sanitized);
@@ -952,6 +1041,57 @@ export const hasSwiftPackageToolsVersionBelow62Usage = (source: string): boolean
952
1041
  return collectSwiftPackageToolsVersionBelow62Lines(source).length > 0;
953
1042
  };
954
1043
 
1044
+ export const collectSwiftPackageDefaultIsolationNotMainActorLines = (
1045
+ source: string
1046
+ ): readonly number[] => {
1047
+ const lines: number[] = [];
1048
+
1049
+ source.split(/\r?\n/).forEach((rawLine, index) => {
1050
+ const line = stripSwiftLineForSemanticScan(rawLine);
1051
+ const match = /\.defaultIsolation\s*\(\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\)/.exec(line);
1052
+ if (!match) {
1053
+ return;
1054
+ }
1055
+
1056
+ const value = (match[1] ?? '').trim().toLowerCase();
1057
+ if (value !== 'mainactor' && value !== 'mainactor.self') {
1058
+ lines.push(index + 1);
1059
+ }
1060
+ });
1061
+
1062
+ return sortedUniqueLines(lines);
1063
+ };
1064
+
1065
+ export const hasSwiftPackageDefaultIsolationNotMainActorUsage = (source: string): boolean => {
1066
+ return collectSwiftPackageDefaultIsolationNotMainActorLines(source).length > 0;
1067
+ };
1068
+
1069
+ export const collectSwiftPackageStrictConcurrencyBelowCompleteLines = (
1070
+ source: string
1071
+ ): readonly number[] => {
1072
+ const lines: number[] = [];
1073
+
1074
+ source.split(/\r?\n/).forEach((rawLine, index) => {
1075
+ const line = rawLine.trim();
1076
+ if (line.startsWith('//')) {
1077
+ return;
1078
+ }
1079
+ const match =
1080
+ /\.enable(?:Experimental|Upcoming)Feature\s*\(\s*"StrictConcurrency\s*=\s*(minimal|targeted)"\s*\)/i.exec(
1081
+ line
1082
+ );
1083
+ if (match) {
1084
+ lines.push(index + 1);
1085
+ }
1086
+ });
1087
+
1088
+ return sortedUniqueLines(lines);
1089
+ };
1090
+
1091
+ export const hasSwiftPackageStrictConcurrencyBelowCompleteUsage = (source: string): boolean => {
1092
+ return collectSwiftPackageStrictConcurrencyBelowCompleteLines(source).length > 0;
1093
+ };
1094
+
955
1095
  export const collectSwiftStrictConcurrencyBelowCompleteLines = (source: string): readonly number[] => {
956
1096
  const lines: number[] = [];
957
1097
 
@@ -975,6 +1115,33 @@ export const hasSwiftStrictConcurrencyBelowCompleteUsage = (source: string): boo
975
1115
  return collectSwiftStrictConcurrencyBelowCompleteLines(source).length > 0;
976
1116
  };
977
1117
 
1118
+ export const collectSwiftDefaultActorIsolationNotMainActorLines = (
1119
+ source: string
1120
+ ): readonly number[] => {
1121
+ const lines: number[] = [];
1122
+
1123
+ source.split(/\r?\n/).forEach((rawLine, index) => {
1124
+ const line = stripSwiftLineForSemanticScan(rawLine);
1125
+ const match = /\bSWIFT_DEFAULT_ACTOR_ISOLATION\s*=\s*([A-Za-z_][A-Za-z0-9_.]*)\s*;?/.exec(
1126
+ line
1127
+ );
1128
+ if (!match) {
1129
+ return;
1130
+ }
1131
+
1132
+ const value = (match[1] ?? '').trim().toLowerCase();
1133
+ if (value !== 'mainactor' && value !== 'mainactor.self') {
1134
+ lines.push(index + 1);
1135
+ }
1136
+ });
1137
+
1138
+ return sortedUniqueLines(lines);
1139
+ };
1140
+
1141
+ export const hasSwiftDefaultActorIsolationNotMainActorUsage = (source: string): boolean => {
1142
+ return collectSwiftDefaultActorIsolationNotMainActorLines(source).length > 0;
1143
+ };
1144
+
978
1145
  export const hasSwiftOnAppearTaskUsage = (source: string): boolean => {
979
1146
  return collectSwiftOnAppearTaskLines(source).length > 0;
980
1147
  };
@@ -1474,6 +1641,18 @@ export const hasSwiftMainThreadBlockingSleepUsage = (source: string): boolean =>
1474
1641
  });
1475
1642
  };
1476
1643
 
1644
+ export const collectSwiftThreadCentricDebuggingLines = (source: string): readonly number[] => {
1645
+ return sortedUniqueLines([
1646
+ ...collectSwiftRegexLines(source, /\bThread\s*\.\s*(?:current|isMainThread)\b/),
1647
+ ...collectSwiftRegexLines(source, /\bpthread_self\s*\(/),
1648
+ ...collectSwiftRegexLines(source, /\bpthread_mach_thread_np\s*\(/),
1649
+ ]);
1650
+ };
1651
+
1652
+ export const hasSwiftThreadCentricDebuggingUsage = (source: string): boolean => {
1653
+ return collectSwiftThreadCentricDebuggingLines(source).length > 0;
1654
+ };
1655
+
1477
1656
  export const hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage = (source: string): boolean => {
1478
1657
  const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
1479
1658
  const iconOnlyButtonPattern =
@@ -1569,6 +1748,227 @@ export const hasSwiftBindableMissingForObservableBindingUsage = (source: string)
1569
1748
  return collectSwiftBindableMissingForObservableBindingLines(source).length > 0;
1570
1749
  };
1571
1750
 
1751
+ const allowedCrossFeatureImportModules = new Set([
1752
+ 'Combine',
1753
+ 'CoreData',
1754
+ 'DesignSystem',
1755
+ 'Foundation',
1756
+ 'LocalAuthentication',
1757
+ 'MapKit',
1758
+ 'Navigation',
1759
+ 'Observation',
1760
+ 'PhotosUI',
1761
+ 'Shared',
1762
+ 'SharedKernel',
1763
+ 'SwiftData',
1764
+ 'SwiftUI',
1765
+ 'Testing',
1766
+ 'UIKit',
1767
+ 'XCTest',
1768
+ ]);
1769
+
1770
+ const featureNameFromSwiftPath = (path: string): string | undefined => {
1771
+ const normalized = path.replace(/\\/g, '/');
1772
+ const match = /\/Features\/([^/]+)\//.exec(normalized);
1773
+ return match?.[1];
1774
+ };
1775
+
1776
+ export const collectSwiftCrossFeatureImportLines = (
1777
+ source: string,
1778
+ path: string
1779
+ ): readonly number[] => {
1780
+ const currentFeature = featureNameFromSwiftPath(path);
1781
+ if (!currentFeature) {
1782
+ return [];
1783
+ }
1784
+
1785
+ const lines: number[] = [];
1786
+ source.split(/\r?\n/).forEach((rawLine, index) => {
1787
+ const line = stripSwiftLineForSemanticScan(rawLine);
1788
+ const match = /^\s*import\s+(?:@\w+\s+)?([A-Za-z_][A-Za-z0-9_]*)\b/.exec(line);
1789
+ if (!match?.[1]) {
1790
+ return;
1791
+ }
1792
+
1793
+ const moduleName = match[1];
1794
+ if (moduleName === currentFeature || allowedCrossFeatureImportModules.has(moduleName)) {
1795
+ return;
1796
+ }
1797
+ if (moduleName.endsWith('Kit') || moduleName.startsWith('Swift')) {
1798
+ return;
1799
+ }
1800
+
1801
+ lines.push(index + 1);
1802
+ });
1803
+
1804
+ return sortedUniqueLines(lines);
1805
+ };
1806
+
1807
+ export const hasSwiftCrossFeatureImportUsage = (source: string, path: string): boolean => {
1808
+ return collectSwiftCrossFeatureImportLines(source, path).length > 0;
1809
+ };
1810
+
1811
+ type SwiftArchitectureLayer = 'domain' | 'application' | 'presentation' | 'infrastructure';
1812
+
1813
+ const forbiddenLayerImports: Record<SwiftArchitectureLayer, ReadonlySet<string>> = {
1814
+ domain: new Set([
1815
+ 'Alamofire',
1816
+ 'AppKit',
1817
+ 'CloudKit',
1818
+ 'Combine',
1819
+ 'ComposableArchitecture',
1820
+ 'CoreData',
1821
+ 'Firebase',
1822
+ 'GRDB',
1823
+ 'Kingfisher',
1824
+ 'RealmSwift',
1825
+ 'Swinject',
1826
+ 'SwiftData',
1827
+ 'SwiftUI',
1828
+ 'UIKit',
1829
+ 'WatchKit',
1830
+ ]),
1831
+ application: new Set([
1832
+ 'Alamofire',
1833
+ 'AppKit',
1834
+ 'CloudKit',
1835
+ 'ComposableArchitecture',
1836
+ 'CoreData',
1837
+ 'Firebase',
1838
+ 'GRDB',
1839
+ 'Kingfisher',
1840
+ 'RealmSwift',
1841
+ 'Swinject',
1842
+ 'SwiftData',
1843
+ 'SwiftUI',
1844
+ 'UIKit',
1845
+ 'WatchKit',
1846
+ ]),
1847
+ presentation: new Set([
1848
+ 'Alamofire',
1849
+ 'CloudKit',
1850
+ 'CoreData',
1851
+ 'Firebase',
1852
+ 'GRDB',
1853
+ 'RealmSwift',
1854
+ 'Swinject',
1855
+ 'SwiftData',
1856
+ ]),
1857
+ infrastructure: new Set([]),
1858
+ };
1859
+
1860
+ const swiftArchitectureLayerFromPath = (path: string): SwiftArchitectureLayer | undefined => {
1861
+ const normalized = path.replace(/\\/g, '/').toLowerCase();
1862
+ if (normalized.includes('/domain/')) {
1863
+ return 'domain';
1864
+ }
1865
+ if (normalized.includes('/application/') || normalized.includes('/usecases/')) {
1866
+ return 'application';
1867
+ }
1868
+ if (normalized.includes('/presentation/')) {
1869
+ return 'presentation';
1870
+ }
1871
+ if (normalized.includes('/infrastructure/')) {
1872
+ return 'infrastructure';
1873
+ }
1874
+ return undefined;
1875
+ };
1876
+
1877
+ const hasForbiddenLayerImportName = (
1878
+ layer: SwiftArchitectureLayer,
1879
+ moduleName: string
1880
+ ): boolean => {
1881
+ if (forbiddenLayerImports[layer].has(moduleName)) {
1882
+ return true;
1883
+ }
1884
+ if (moduleName.startsWith('Firebase')) {
1885
+ return true;
1886
+ }
1887
+ if (layer === 'domain') {
1888
+ return (
1889
+ moduleName.includes('Application') ||
1890
+ moduleName.includes('Presentation') ||
1891
+ moduleName.includes('Infrastructure')
1892
+ );
1893
+ }
1894
+ if (layer === 'application') {
1895
+ return moduleName.includes('Presentation') || moduleName.includes('Infrastructure');
1896
+ }
1897
+ if (layer === 'presentation') {
1898
+ return moduleName.includes('Infrastructure');
1899
+ }
1900
+ return false;
1901
+ };
1902
+
1903
+ export const collectSwiftLayerDirectionViolationLines = (
1904
+ source: string,
1905
+ path: string
1906
+ ): readonly number[] => {
1907
+ const layer = swiftArchitectureLayerFromPath(path);
1908
+ if (!layer) {
1909
+ return [];
1910
+ }
1911
+
1912
+ if (layer === 'infrastructure') {
1913
+ return [];
1914
+ }
1915
+
1916
+ const lines: number[] = [];
1917
+ source.split(/\r?\n/).forEach((rawLine, index) => {
1918
+ const line = stripSwiftLineForSemanticScan(rawLine);
1919
+ const match = /^\s*import\s+(?:@\w+\s+)?([A-Za-z_][A-Za-z0-9_]*)\b/.exec(line);
1920
+ if (match?.[1] && hasForbiddenLayerImportName(layer, match[1])) {
1921
+ lines.push(index + 1);
1922
+ }
1923
+ });
1924
+
1925
+ return sortedUniqueLines(lines);
1926
+ };
1927
+
1928
+ export const hasSwiftLayerDirectionViolationUsage = (source: string, path: string): boolean => {
1929
+ return collectSwiftLayerDirectionViolationLines(source, path).length > 0;
1930
+ };
1931
+
1932
+ const isSwiftAppImplementationPath = (path: string): boolean => {
1933
+ const normalized = path.replace(/\\/g, '/').toLowerCase();
1934
+ if (
1935
+ !normalized.endsWith('.swift') ||
1936
+ /(^|\/)(tests?|uitests?|testsupport|fixtures?|mocks?)(\/|$)/.test(normalized) ||
1937
+ /(?:tests?|spec)\.swift$/.test(normalized)
1938
+ ) {
1939
+ return false;
1940
+ }
1941
+ if (normalized.includes('/sources/') || normalized.includes('/public/') || normalized.includes('/exports/')) {
1942
+ return false;
1943
+ }
1944
+ return normalized.includes('/apps/ios/') || normalized.includes('/ios/');
1945
+ };
1946
+
1947
+ export const collectSwiftExcessivePublicApiLines = (
1948
+ source: string,
1949
+ path: string
1950
+ ): readonly number[] => {
1951
+ if (!isSwiftAppImplementationPath(path)) {
1952
+ return [];
1953
+ }
1954
+
1955
+ const lines: number[] = [];
1956
+ source.split(/\r?\n/).forEach((rawLine, index) => {
1957
+ const line = stripSwiftLineForSemanticScan(rawLine);
1958
+ if (
1959
+ /^\s*(?:@(?:MainActor|Observable|objc|objcMembers|available|Published|ViewBuilder|discardableResult)[^\n]*\s*)*(?:public|open)\s+(?:(?:final|static|class|override|mutating|nonisolated)\s+)*(?:class|struct|enum|actor|protocol|extension|func|var|let|subscript|init)\b/.test(line)
1960
+ ) {
1961
+ lines.push(index + 1);
1962
+ }
1963
+ });
1964
+
1965
+ return lines.length >= 3 ? sortedUniqueLines(lines) : [];
1966
+ };
1967
+
1968
+ export const hasSwiftExcessivePublicApiUsage = (source: string, path: string): boolean => {
1969
+ return collectSwiftExcessivePublicApiLines(source, path).length > 0;
1970
+ };
1971
+
1572
1972
  export const hasSwiftUncheckedSendableUsage = (source: string): boolean => {
1573
1973
  return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
1574
1974
  if (current !== '@' || !swiftSource.startsWith('@unchecked', index)) {
@@ -2051,6 +2451,72 @@ export const hasSwiftOnTapGestureWithoutButtonTraitUsage = (source: string): boo
2051
2451
  return collectSwiftOnTapGestureWithoutButtonTraitLines(source).length > 0;
2052
2452
  };
2053
2453
 
2454
+ export const collectSwiftGlassInteractiveOnStaticElementLines = (source: string): readonly number[] => {
2455
+ const sanitizedLines = sanitizeSwiftSourceForMultilineRegex(source).split(/\r?\n/);
2456
+ const originalLines = source.split(/\r?\n/);
2457
+ const matches: number[] = [];
2458
+
2459
+ for (let index = 0; index < sanitizedLines.length; index += 1) {
2460
+ const line = sanitizedLines[index] ?? '';
2461
+ if (!/\.glassEffect\s*\(/.test(line)) {
2462
+ continue;
2463
+ }
2464
+
2465
+ const previousWindow = sanitizedLines.slice(Math.max(0, index - 4), index).join('\n');
2466
+ const followingWindow = sanitizedLines
2467
+ .slice(index, Math.min(sanitizedLines.length, index + 8))
2468
+ .join('\n');
2469
+ const modifierWindow = `${previousWindow}\n${followingWindow}`;
2470
+ const currentStartsModifierChain = /^\s*\./.test(line);
2471
+
2472
+ if (!/\.interactive\s*\(/.test(modifierWindow)) {
2473
+ continue;
2474
+ }
2475
+
2476
+ if (
2477
+ /\b(?:Button|NavigationLink|Menu|Toggle|Picker|Slider|Stepper|TextField|SecureField)\s*(?:<[^>]+>)?\s*\(/.test(
2478
+ modifierWindow
2479
+ ) ||
2480
+ /^\s*\.onTapGesture\s*(?:\(|\{)/m.test(followingWindow) ||
2481
+ (currentStartsModifierChain && /\.onTapGesture\s*(?:\(|\{)/.test(previousWindow)) ||
2482
+ /^\s*\.accessibilityAction\s*\(/m.test(followingWindow) ||
2483
+ (currentStartsModifierChain && /\.accessibilityAction\s*\(/.test(previousWindow)) ||
2484
+ /^\s*\.accessibilityAddTraits\s*\(\s*(?:AccessibilityTraits\s*\.\s*)?\.isButton\s*\)/m.test(followingWindow) ||
2485
+ (currentStartsModifierChain &&
2486
+ /\.accessibilityAddTraits\s*\(\s*(?:AccessibilityTraits\s*\.\s*)?\.isButton\s*\)/.test(previousWindow)) ||
2487
+ /^\s*\.focusable\s*\(\s*true\s*\)/m.test(followingWindow) ||
2488
+ (currentStartsModifierChain && /\.focusable\s*\(\s*true\s*\)/.test(previousWindow))
2489
+ ) {
2490
+ continue;
2491
+ }
2492
+
2493
+ if (!/\.glassEffect\s*\(/.test(stripSwiftLineForSemanticScan(originalLines[index] ?? ''))) {
2494
+ continue;
2495
+ }
2496
+
2497
+ matches.push(index + 1);
2498
+ }
2499
+
2500
+ return sortedUniqueLines(matches);
2501
+ };
2502
+
2503
+ export const hasSwiftGlassInteractiveOnStaticElementUsage = (source: string): boolean => {
2504
+ return collectSwiftGlassInteractiveOnStaticElementLines(source).length > 0;
2505
+ };
2506
+
2507
+ export const collectSwiftGlassEffectIDWithoutNamespaceLines = (source: string): readonly number[] => {
2508
+ const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
2509
+ if (/@Namespace\b/.test(sanitized) || /\bNamespace\s*\.\s*ID\b/.test(sanitized)) {
2510
+ return [];
2511
+ }
2512
+
2513
+ return collectSwiftRegexLines(source, /\.\s*glassEffectID\s*\(/);
2514
+ };
2515
+
2516
+ export const hasSwiftGlassEffectIDWithoutNamespaceUsage = (source: string): boolean => {
2517
+ return collectSwiftGlassEffectIDWithoutNamespaceLines(source).length > 0;
2518
+ };
2519
+
2054
2520
  export const hasSwiftStringFormatUsage = (source: string): boolean => {
2055
2521
  return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
2056
2522
  if (current !== 'S' || !hasIdentifierAt(swiftSource, index, 'String')) {
@@ -2257,7 +2723,29 @@ export const collectSwiftQuickNimbleLines = (source: string): readonly number[]
2257
2723
  ]);
2258
2724
  };
2259
2725
 
2726
+ export const hasSwiftThirdPartyUiTestFrameworkUsage = (source: string): boolean => {
2727
+ return hasSwiftSanitizedRegexMatch(
2728
+ source,
2729
+ /\bimport\s+(?:KIF|EarlGrey|GREYMatchers|GREYActions|Detox|Appium|Calabash)\b|\b(?:tester|KIFUITestActor|EarlGrey|GREYMatchers|GREYActions|Detox|Appium|Calabash)\b/
2730
+ );
2731
+ };
2732
+
2733
+ export const collectSwiftThirdPartyUiTestFrameworkLines = (source: string): readonly number[] => {
2734
+ if (!hasSwiftThirdPartyUiTestFrameworkUsage(source)) {
2735
+ return [];
2736
+ }
2737
+
2738
+ return sortedUniqueLines([
2739
+ ...collectSwiftRegexLines(source, /\bimport\s+(?:KIF|EarlGrey|GREYMatchers|GREYActions|Detox|Appium|Calabash)\b/),
2740
+ ...collectSwiftRegexLines(source, /\b(?:tester|KIFUITestActor|EarlGrey|GREYMatchers|GREYActions|Detox|Appium|Calabash)\b/),
2741
+ ]);
2742
+ };
2743
+
2260
2744
  export const hasSwiftXCTestAssertionUsage = (source: string): boolean => {
2745
+ if (hasSwiftLegacyXCTestUiOrPerformanceUsage(source)) {
2746
+ return false;
2747
+ }
2748
+
2261
2749
  if (hasSwiftBrownfieldXCTestQualityPattern(source)) {
2262
2750
  return false;
2263
2751
  }
@@ -2304,6 +2792,10 @@ const hasSwiftConfirmationUsage = (source: string): boolean => {
2304
2792
  };
2305
2793
 
2306
2794
  export const collectSwiftWaitForExpectationsLines = (source: string): readonly number[] => {
2795
+ if (hasSwiftLegacyXCTestUiOrPerformanceUsage(source)) {
2796
+ return [];
2797
+ }
2798
+
2307
2799
  return sortedUniqueLines([
2308
2800
  ...collectSwiftRegexLines(source, /\b(?:self\s*\.\s*)?wait\s*\(\s*for\s*:/),
2309
2801
  ...collectSwiftRegexLines(source, /\bwaitForExpectations\s*\(/),