pumuki 6.3.307 → 6.3.309

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,271 @@ 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
+
1145
+ export const collectSwiftUpcomingFeatureDisabledLines = (source: string): readonly number[] => {
1146
+ const lines: number[] = [];
1147
+
1148
+ source.split(/\r?\n/).forEach((rawLine, index) => {
1149
+ const line = stripSwiftLineForSemanticScan(rawLine);
1150
+ const upcomingFeatureMatch =
1151
+ /\bSWIFT_UPCOMING_FEATURE_[A-Za-z0-9_]+\s*=\s*([A-Za-z0-9_]+)\s*;?/.exec(line);
1152
+ if (upcomingFeatureMatch) {
1153
+ const value = (upcomingFeatureMatch[1] ?? '').trim().toLowerCase();
1154
+ if (value === 'no' || value === 'false' || value === '0') {
1155
+ lines.push(index + 1);
1156
+ }
1157
+ return;
1158
+ }
1159
+
1160
+ const experimentalFeaturesMatch =
1161
+ /\bSWIFT_ENABLE_EXPERIMENTAL_FEATURES\s*=\s*(.*?)\s*;?$/.exec(line);
1162
+ if (!experimentalFeaturesMatch) {
1163
+ return;
1164
+ }
1165
+
1166
+ const value = (experimentalFeaturesMatch[1] ?? '').trim().toLowerCase();
1167
+ if (value === '' || value === 'no' || value === 'false' || value === '0') {
1168
+ lines.push(index + 1);
1169
+ }
1170
+ });
1171
+
1172
+ return sortedUniqueLines(lines);
1173
+ };
1174
+
1175
+ export const hasSwiftUpcomingFeatureDisabledUsage = (source: string): boolean => {
1176
+ return collectSwiftUpcomingFeatureDisabledLines(source).length > 0;
1177
+ };
1178
+
1179
+ const swiftUiStateOwnerDeclarationPattern =
1180
+ /\b(?:final\s+)?(?:class|struct)\s+([A-Za-z_][A-Za-z0-9_]*(?:ViewModel|Presenter|Store))\b/;
1181
+
1182
+ const hasSwiftMainActorAnnotationNear = (
1183
+ lines: readonly string[],
1184
+ declarationIndex: number
1185
+ ): boolean => {
1186
+ const start = Math.max(0, declarationIndex - 3);
1187
+ const context = lines
1188
+ .slice(start, declarationIndex + 1)
1189
+ .map((line) => stripSwiftLineForSemanticScan(line))
1190
+ .join('\n');
1191
+ return /@MainActor\b/.test(context);
1192
+ };
1193
+
1194
+ const hasSwiftObservableUiStateEvidence = (
1195
+ lines: readonly string[],
1196
+ declarationIndex: number
1197
+ ): boolean => {
1198
+ const annotationStart = Math.max(0, declarationIndex - 2);
1199
+ const end = Math.min(lines.length, declarationIndex + 60);
1200
+ const annotationContext = lines
1201
+ .slice(annotationStart, declarationIndex + 1)
1202
+ .map((line) => stripSwiftLineForSemanticScan(line))
1203
+ .join('\n');
1204
+ const bodyContext = lines
1205
+ .slice(declarationIndex, end)
1206
+ .map((line) => stripSwiftLineForSemanticScan(line))
1207
+ .join('\n');
1208
+
1209
+ return (
1210
+ /@Observable\b/.test(annotationContext) ||
1211
+ /\bObservableObject\b/.test(bodyContext) ||
1212
+ /@Published\b/.test(bodyContext)
1213
+ );
1214
+ };
1215
+
1216
+ export const collectSwiftUiStateWithoutMainActorLines = (source: string): readonly number[] => {
1217
+ const matches: number[] = [];
1218
+ const lines = source.split(/\r?\n/);
1219
+
1220
+ lines.forEach((rawLine, index) => {
1221
+ const line = stripSwiftLineForSemanticScan(rawLine);
1222
+ if (!swiftUiStateOwnerDeclarationPattern.test(line)) {
1223
+ return;
1224
+ }
1225
+ if (hasSwiftMainActorAnnotationNear(lines, index)) {
1226
+ return;
1227
+ }
1228
+ if (!hasSwiftObservableUiStateEvidence(lines, index)) {
1229
+ return;
1230
+ }
1231
+ matches.push(index + 1);
1232
+ });
1233
+
1234
+ return sortedUniqueLines(matches);
1235
+ };
1236
+
1237
+ export const hasSwiftUiStateWithoutMainActorUsage = (source: string): boolean => {
1238
+ return collectSwiftUiStateWithoutMainActorLines(source).length > 0;
1239
+ };
1240
+
1241
+ const swiftSharedStateOwnerDeclarationPattern =
1242
+ /\b(?:final\s+)?class\s+([A-Za-z_][A-Za-z0-9_]*(?:Cache|Manager|Session|Store|Repository))\b/;
1243
+
1244
+ const findSwiftDeclarationBlockEnd = (lines: readonly string[], declarationIndex: number): number => {
1245
+ let depth = 0;
1246
+ let started = false;
1247
+
1248
+ for (let index = declarationIndex; index < lines.length; index += 1) {
1249
+ const line = stripSwiftLineForSemanticScan(lines[index] ?? '');
1250
+ for (const char of line) {
1251
+ if (char === '{') {
1252
+ depth += 1;
1253
+ started = true;
1254
+ } else if (char === '}') {
1255
+ depth -= 1;
1256
+ if (started && depth <= 0) {
1257
+ return index + 1;
1258
+ }
1259
+ }
1260
+ }
1261
+ }
1262
+
1263
+ return Math.min(lines.length, declarationIndex + 80);
1264
+ };
1265
+
1266
+ const hasSwiftSharedMutableStateBody = (body: string): boolean => {
1267
+ return (
1268
+ /\b(?:private\s+|fileprivate\s+|internal\s+|public\s+|open\s+)?var\s+[A-Za-z_][A-Za-z0-9_]*\b/.test(
1269
+ body
1270
+ ) &&
1271
+ /\bfunc\s+[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(body) &&
1272
+ !/@(?:Observable|Published)\b/.test(body) &&
1273
+ !/\bObservableObject\b/.test(body)
1274
+ );
1275
+ };
1276
+
1277
+ export const collectSwiftSharedMutableStateWithoutActorLines = (
1278
+ source: string
1279
+ ): readonly number[] => {
1280
+ const matches: number[] = [];
1281
+ const lines = source.split(/\r?\n/);
1282
+
1283
+ lines.forEach((rawLine, index) => {
1284
+ const line = stripSwiftLineForSemanticScan(rawLine);
1285
+ if (!swiftSharedStateOwnerDeclarationPattern.test(line)) {
1286
+ return;
1287
+ }
1288
+ if (hasSwiftMainActorAnnotationNear(lines, index)) {
1289
+ return;
1290
+ }
1291
+
1292
+ const blockEnd = findSwiftDeclarationBlockEnd(lines, index);
1293
+ const body = lines
1294
+ .slice(index, blockEnd)
1295
+ .map((candidate) => stripSwiftLineForSemanticScan(candidate))
1296
+ .join('\n');
1297
+
1298
+ if (!hasSwiftSharedMutableStateBody(body)) {
1299
+ return;
1300
+ }
1301
+
1302
+ matches.push(index + 1);
1303
+ });
1304
+
1305
+ return sortedUniqueLines(matches);
1306
+ };
1307
+
1308
+ export const hasSwiftSharedMutableStateWithoutActorUsage = (source: string): boolean => {
1309
+ return collectSwiftSharedMutableStateWithoutActorLines(source).length > 0;
1310
+ };
1311
+
1312
+ const swiftActorPatchOwnerDeclarationPattern =
1313
+ /\b(?:final\s+)?(?:class|struct)\s+([A-Za-z_][A-Za-z0-9_]*(?:ViewModel|Presenter|Store|Manager))\b/;
1314
+
1315
+ const findNearestSwiftActorPatchOwnerIndex = (
1316
+ lines: readonly string[],
1317
+ usageIndex: number
1318
+ ): number | undefined => {
1319
+ const start = Math.max(0, usageIndex - 80);
1320
+ for (let index = usageIndex; index >= start; index -= 1) {
1321
+ const line = stripSwiftLineForSemanticScan(lines[index] ?? '');
1322
+ if (swiftActorPatchOwnerDeclarationPattern.test(line)) {
1323
+ return index;
1324
+ }
1325
+ if (/^\s*(?:actor|enum|protocol)\s+[A-Za-z_][A-Za-z0-9_]*\b/.test(line)) {
1326
+ return undefined;
1327
+ }
1328
+ }
1329
+ return undefined;
1330
+ };
1331
+
1332
+ export const collectSwiftMainActorRunPatchLines = (source: string): readonly number[] => {
1333
+ const matches: number[] = [];
1334
+ const lines = source.split(/\r?\n/);
1335
+
1336
+ lines.forEach((rawLine, index) => {
1337
+ const line = stripSwiftLineForSemanticScan(rawLine);
1338
+ if (!/\bMainActor\s*\.\s*run\s*(?:\(|\{)/.test(line)) {
1339
+ return;
1340
+ }
1341
+
1342
+ const ownerIndex = findNearestSwiftActorPatchOwnerIndex(lines, index);
1343
+ if (ownerIndex === undefined) {
1344
+ return;
1345
+ }
1346
+ if (hasSwiftMainActorAnnotationNear(lines, ownerIndex)) {
1347
+ return;
1348
+ }
1349
+
1350
+ matches.push(index + 1);
1351
+ });
1352
+
1353
+ return sortedUniqueLines(matches);
1354
+ };
1355
+
1356
+ export const hasSwiftMainActorRunPatchUsage = (source: string): boolean => {
1357
+ return collectSwiftMainActorRunPatchLines(source).length > 0;
1358
+ };
1359
+
1360
+ const swiftNavigationPathRestorationPattern =
1361
+ /@(?:SceneStorage|AppStorage)\b|\bCodableRepresentation\b|\.codable\b|\b(?:restore|rehydrate|persist|save|load)[A-Za-z0-9_]*(?:Navigation)?Path\b/i;
1362
+
1363
+ export const collectSwiftNavigationPathWithoutRestorationLines = (
1364
+ source: string
1365
+ ): readonly number[] => {
1366
+ const lines = collectSwiftRegexLines(source, /\bNavigationPath\s*\(/);
1367
+ if (lines.length === 0) {
1368
+ return [];
1369
+ }
1370
+
1371
+ const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
1372
+ if (swiftNavigationPathRestorationPattern.test(sanitized)) {
1373
+ return [];
1374
+ }
1375
+
1376
+ return sortedUniqueLines(lines);
1377
+ };
1378
+
1379
+ export const hasSwiftNavigationPathWithoutRestorationUsage = (source: string): boolean => {
1380
+ return collectSwiftNavigationPathWithoutRestorationLines(source).length > 0;
1381
+ };
1382
+
978
1383
  export const hasSwiftOnAppearTaskUsage = (source: string): boolean => {
979
1384
  return collectSwiftOnAppearTaskLines(source).length > 0;
980
1385
  };
@@ -1474,6 +1879,18 @@ export const hasSwiftMainThreadBlockingSleepUsage = (source: string): boolean =>
1474
1879
  });
1475
1880
  };
1476
1881
 
1882
+ export const collectSwiftThreadCentricDebuggingLines = (source: string): readonly number[] => {
1883
+ return sortedUniqueLines([
1884
+ ...collectSwiftRegexLines(source, /\bThread\s*\.\s*(?:current|isMainThread)\b/),
1885
+ ...collectSwiftRegexLines(source, /\bpthread_self\s*\(/),
1886
+ ...collectSwiftRegexLines(source, /\bpthread_mach_thread_np\s*\(/),
1887
+ ]);
1888
+ };
1889
+
1890
+ export const hasSwiftThreadCentricDebuggingUsage = (source: string): boolean => {
1891
+ return collectSwiftThreadCentricDebuggingLines(source).length > 0;
1892
+ };
1893
+
1477
1894
  export const hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage = (source: string): boolean => {
1478
1895
  const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
1479
1896
  const iconOnlyButtonPattern =
@@ -1569,6 +1986,227 @@ export const hasSwiftBindableMissingForObservableBindingUsage = (source: string)
1569
1986
  return collectSwiftBindableMissingForObservableBindingLines(source).length > 0;
1570
1987
  };
1571
1988
 
1989
+ const allowedCrossFeatureImportModules = new Set([
1990
+ 'Combine',
1991
+ 'CoreData',
1992
+ 'DesignSystem',
1993
+ 'Foundation',
1994
+ 'LocalAuthentication',
1995
+ 'MapKit',
1996
+ 'Navigation',
1997
+ 'Observation',
1998
+ 'PhotosUI',
1999
+ 'Shared',
2000
+ 'SharedKernel',
2001
+ 'SwiftData',
2002
+ 'SwiftUI',
2003
+ 'Testing',
2004
+ 'UIKit',
2005
+ 'XCTest',
2006
+ ]);
2007
+
2008
+ const featureNameFromSwiftPath = (path: string): string | undefined => {
2009
+ const normalized = path.replace(/\\/g, '/');
2010
+ const match = /\/Features\/([^/]+)\//.exec(normalized);
2011
+ return match?.[1];
2012
+ };
2013
+
2014
+ export const collectSwiftCrossFeatureImportLines = (
2015
+ source: string,
2016
+ path: string
2017
+ ): readonly number[] => {
2018
+ const currentFeature = featureNameFromSwiftPath(path);
2019
+ if (!currentFeature) {
2020
+ return [];
2021
+ }
2022
+
2023
+ const lines: number[] = [];
2024
+ source.split(/\r?\n/).forEach((rawLine, index) => {
2025
+ const line = stripSwiftLineForSemanticScan(rawLine);
2026
+ const match = /^\s*import\s+(?:@\w+\s+)?([A-Za-z_][A-Za-z0-9_]*)\b/.exec(line);
2027
+ if (!match?.[1]) {
2028
+ return;
2029
+ }
2030
+
2031
+ const moduleName = match[1];
2032
+ if (moduleName === currentFeature || allowedCrossFeatureImportModules.has(moduleName)) {
2033
+ return;
2034
+ }
2035
+ if (moduleName.endsWith('Kit') || moduleName.startsWith('Swift')) {
2036
+ return;
2037
+ }
2038
+
2039
+ lines.push(index + 1);
2040
+ });
2041
+
2042
+ return sortedUniqueLines(lines);
2043
+ };
2044
+
2045
+ export const hasSwiftCrossFeatureImportUsage = (source: string, path: string): boolean => {
2046
+ return collectSwiftCrossFeatureImportLines(source, path).length > 0;
2047
+ };
2048
+
2049
+ type SwiftArchitectureLayer = 'domain' | 'application' | 'presentation' | 'infrastructure';
2050
+
2051
+ const forbiddenLayerImports: Record<SwiftArchitectureLayer, ReadonlySet<string>> = {
2052
+ domain: new Set([
2053
+ 'Alamofire',
2054
+ 'AppKit',
2055
+ 'CloudKit',
2056
+ 'Combine',
2057
+ 'ComposableArchitecture',
2058
+ 'CoreData',
2059
+ 'Firebase',
2060
+ 'GRDB',
2061
+ 'Kingfisher',
2062
+ 'RealmSwift',
2063
+ 'Swinject',
2064
+ 'SwiftData',
2065
+ 'SwiftUI',
2066
+ 'UIKit',
2067
+ 'WatchKit',
2068
+ ]),
2069
+ application: new Set([
2070
+ 'Alamofire',
2071
+ 'AppKit',
2072
+ 'CloudKit',
2073
+ 'ComposableArchitecture',
2074
+ 'CoreData',
2075
+ 'Firebase',
2076
+ 'GRDB',
2077
+ 'Kingfisher',
2078
+ 'RealmSwift',
2079
+ 'Swinject',
2080
+ 'SwiftData',
2081
+ 'SwiftUI',
2082
+ 'UIKit',
2083
+ 'WatchKit',
2084
+ ]),
2085
+ presentation: new Set([
2086
+ 'Alamofire',
2087
+ 'CloudKit',
2088
+ 'CoreData',
2089
+ 'Firebase',
2090
+ 'GRDB',
2091
+ 'RealmSwift',
2092
+ 'Swinject',
2093
+ 'SwiftData',
2094
+ ]),
2095
+ infrastructure: new Set([]),
2096
+ };
2097
+
2098
+ const swiftArchitectureLayerFromPath = (path: string): SwiftArchitectureLayer | undefined => {
2099
+ const normalized = path.replace(/\\/g, '/').toLowerCase();
2100
+ if (normalized.includes('/domain/')) {
2101
+ return 'domain';
2102
+ }
2103
+ if (normalized.includes('/application/') || normalized.includes('/usecases/')) {
2104
+ return 'application';
2105
+ }
2106
+ if (normalized.includes('/presentation/')) {
2107
+ return 'presentation';
2108
+ }
2109
+ if (normalized.includes('/infrastructure/')) {
2110
+ return 'infrastructure';
2111
+ }
2112
+ return undefined;
2113
+ };
2114
+
2115
+ const hasForbiddenLayerImportName = (
2116
+ layer: SwiftArchitectureLayer,
2117
+ moduleName: string
2118
+ ): boolean => {
2119
+ if (forbiddenLayerImports[layer].has(moduleName)) {
2120
+ return true;
2121
+ }
2122
+ if (moduleName.startsWith('Firebase')) {
2123
+ return true;
2124
+ }
2125
+ if (layer === 'domain') {
2126
+ return (
2127
+ moduleName.includes('Application') ||
2128
+ moduleName.includes('Presentation') ||
2129
+ moduleName.includes('Infrastructure')
2130
+ );
2131
+ }
2132
+ if (layer === 'application') {
2133
+ return moduleName.includes('Presentation') || moduleName.includes('Infrastructure');
2134
+ }
2135
+ if (layer === 'presentation') {
2136
+ return moduleName.includes('Infrastructure');
2137
+ }
2138
+ return false;
2139
+ };
2140
+
2141
+ export const collectSwiftLayerDirectionViolationLines = (
2142
+ source: string,
2143
+ path: string
2144
+ ): readonly number[] => {
2145
+ const layer = swiftArchitectureLayerFromPath(path);
2146
+ if (!layer) {
2147
+ return [];
2148
+ }
2149
+
2150
+ if (layer === 'infrastructure') {
2151
+ return [];
2152
+ }
2153
+
2154
+ const lines: number[] = [];
2155
+ source.split(/\r?\n/).forEach((rawLine, index) => {
2156
+ const line = stripSwiftLineForSemanticScan(rawLine);
2157
+ const match = /^\s*import\s+(?:@\w+\s+)?([A-Za-z_][A-Za-z0-9_]*)\b/.exec(line);
2158
+ if (match?.[1] && hasForbiddenLayerImportName(layer, match[1])) {
2159
+ lines.push(index + 1);
2160
+ }
2161
+ });
2162
+
2163
+ return sortedUniqueLines(lines);
2164
+ };
2165
+
2166
+ export const hasSwiftLayerDirectionViolationUsage = (source: string, path: string): boolean => {
2167
+ return collectSwiftLayerDirectionViolationLines(source, path).length > 0;
2168
+ };
2169
+
2170
+ const isSwiftAppImplementationPath = (path: string): boolean => {
2171
+ const normalized = path.replace(/\\/g, '/').toLowerCase();
2172
+ if (
2173
+ !normalized.endsWith('.swift') ||
2174
+ /(^|\/)(tests?|uitests?|testsupport|fixtures?|mocks?)(\/|$)/.test(normalized) ||
2175
+ /(?:tests?|spec)\.swift$/.test(normalized)
2176
+ ) {
2177
+ return false;
2178
+ }
2179
+ if (normalized.includes('/sources/') || normalized.includes('/public/') || normalized.includes('/exports/')) {
2180
+ return false;
2181
+ }
2182
+ return normalized.includes('/apps/ios/') || normalized.includes('/ios/');
2183
+ };
2184
+
2185
+ export const collectSwiftExcessivePublicApiLines = (
2186
+ source: string,
2187
+ path: string
2188
+ ): readonly number[] => {
2189
+ if (!isSwiftAppImplementationPath(path)) {
2190
+ return [];
2191
+ }
2192
+
2193
+ const lines: number[] = [];
2194
+ source.split(/\r?\n/).forEach((rawLine, index) => {
2195
+ const line = stripSwiftLineForSemanticScan(rawLine);
2196
+ if (
2197
+ /^\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)
2198
+ ) {
2199
+ lines.push(index + 1);
2200
+ }
2201
+ });
2202
+
2203
+ return lines.length >= 3 ? sortedUniqueLines(lines) : [];
2204
+ };
2205
+
2206
+ export const hasSwiftExcessivePublicApiUsage = (source: string, path: string): boolean => {
2207
+ return collectSwiftExcessivePublicApiLines(source, path).length > 0;
2208
+ };
2209
+
1572
2210
  export const hasSwiftUncheckedSendableUsage = (source: string): boolean => {
1573
2211
  return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
1574
2212
  if (current !== '@' || !swiftSource.startsWith('@unchecked', index)) {
@@ -2051,6 +2689,72 @@ export const hasSwiftOnTapGestureWithoutButtonTraitUsage = (source: string): boo
2051
2689
  return collectSwiftOnTapGestureWithoutButtonTraitLines(source).length > 0;
2052
2690
  };
2053
2691
 
2692
+ export const collectSwiftGlassInteractiveOnStaticElementLines = (source: string): readonly number[] => {
2693
+ const sanitizedLines = sanitizeSwiftSourceForMultilineRegex(source).split(/\r?\n/);
2694
+ const originalLines = source.split(/\r?\n/);
2695
+ const matches: number[] = [];
2696
+
2697
+ for (let index = 0; index < sanitizedLines.length; index += 1) {
2698
+ const line = sanitizedLines[index] ?? '';
2699
+ if (!/\.glassEffect\s*\(/.test(line)) {
2700
+ continue;
2701
+ }
2702
+
2703
+ const previousWindow = sanitizedLines.slice(Math.max(0, index - 4), index).join('\n');
2704
+ const followingWindow = sanitizedLines
2705
+ .slice(index, Math.min(sanitizedLines.length, index + 8))
2706
+ .join('\n');
2707
+ const modifierWindow = `${previousWindow}\n${followingWindow}`;
2708
+ const currentStartsModifierChain = /^\s*\./.test(line);
2709
+
2710
+ if (!/\.interactive\s*\(/.test(modifierWindow)) {
2711
+ continue;
2712
+ }
2713
+
2714
+ if (
2715
+ /\b(?:Button|NavigationLink|Menu|Toggle|Picker|Slider|Stepper|TextField|SecureField)\s*(?:<[^>]+>)?\s*\(/.test(
2716
+ modifierWindow
2717
+ ) ||
2718
+ /^\s*\.onTapGesture\s*(?:\(|\{)/m.test(followingWindow) ||
2719
+ (currentStartsModifierChain && /\.onTapGesture\s*(?:\(|\{)/.test(previousWindow)) ||
2720
+ /^\s*\.accessibilityAction\s*\(/m.test(followingWindow) ||
2721
+ (currentStartsModifierChain && /\.accessibilityAction\s*\(/.test(previousWindow)) ||
2722
+ /^\s*\.accessibilityAddTraits\s*\(\s*(?:AccessibilityTraits\s*\.\s*)?\.isButton\s*\)/m.test(followingWindow) ||
2723
+ (currentStartsModifierChain &&
2724
+ /\.accessibilityAddTraits\s*\(\s*(?:AccessibilityTraits\s*\.\s*)?\.isButton\s*\)/.test(previousWindow)) ||
2725
+ /^\s*\.focusable\s*\(\s*true\s*\)/m.test(followingWindow) ||
2726
+ (currentStartsModifierChain && /\.focusable\s*\(\s*true\s*\)/.test(previousWindow))
2727
+ ) {
2728
+ continue;
2729
+ }
2730
+
2731
+ if (!/\.glassEffect\s*\(/.test(stripSwiftLineForSemanticScan(originalLines[index] ?? ''))) {
2732
+ continue;
2733
+ }
2734
+
2735
+ matches.push(index + 1);
2736
+ }
2737
+
2738
+ return sortedUniqueLines(matches);
2739
+ };
2740
+
2741
+ export const hasSwiftGlassInteractiveOnStaticElementUsage = (source: string): boolean => {
2742
+ return collectSwiftGlassInteractiveOnStaticElementLines(source).length > 0;
2743
+ };
2744
+
2745
+ export const collectSwiftGlassEffectIDWithoutNamespaceLines = (source: string): readonly number[] => {
2746
+ const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
2747
+ if (/@Namespace\b/.test(sanitized) || /\bNamespace\s*\.\s*ID\b/.test(sanitized)) {
2748
+ return [];
2749
+ }
2750
+
2751
+ return collectSwiftRegexLines(source, /\.\s*glassEffectID\s*\(/);
2752
+ };
2753
+
2754
+ export const hasSwiftGlassEffectIDWithoutNamespaceUsage = (source: string): boolean => {
2755
+ return collectSwiftGlassEffectIDWithoutNamespaceLines(source).length > 0;
2756
+ };
2757
+
2054
2758
  export const hasSwiftStringFormatUsage = (source: string): boolean => {
2055
2759
  return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
2056
2760
  if (current !== 'S' || !hasIdentifierAt(swiftSource, index, 'String')) {
@@ -2257,6 +2961,24 @@ export const collectSwiftQuickNimbleLines = (source: string): readonly number[]
2257
2961
  ]);
2258
2962
  };
2259
2963
 
2964
+ export const hasSwiftThirdPartyUiTestFrameworkUsage = (source: string): boolean => {
2965
+ return hasSwiftSanitizedRegexMatch(
2966
+ source,
2967
+ /\bimport\s+(?:KIF|EarlGrey|GREYMatchers|GREYActions|Detox|Appium|Calabash)\b|\b(?:tester|KIFUITestActor|EarlGrey|GREYMatchers|GREYActions|Detox|Appium|Calabash)\b/
2968
+ );
2969
+ };
2970
+
2971
+ export const collectSwiftThirdPartyUiTestFrameworkLines = (source: string): readonly number[] => {
2972
+ if (!hasSwiftThirdPartyUiTestFrameworkUsage(source)) {
2973
+ return [];
2974
+ }
2975
+
2976
+ return sortedUniqueLines([
2977
+ ...collectSwiftRegexLines(source, /\bimport\s+(?:KIF|EarlGrey|GREYMatchers|GREYActions|Detox|Appium|Calabash)\b/),
2978
+ ...collectSwiftRegexLines(source, /\b(?:tester|KIFUITestActor|EarlGrey|GREYMatchers|GREYActions|Detox|Appium|Calabash)\b/),
2979
+ ]);
2980
+ };
2981
+
2260
2982
  export const hasSwiftXCTestAssertionUsage = (source: string): boolean => {
2261
2983
  if (hasSwiftLegacyXCTestUiOrPerformanceUsage(source)) {
2262
2984
  return false;