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.
- package/CHANGELOG.md +6 -0
- package/core/facts/detectors/text/ios.test.ts +621 -0
- package/core/facts/detectors/text/ios.ts +722 -0
- package/core/facts/extractHeuristicFacts.ts +22 -4
- package/core/rules/presets/heuristics/ios.test.ts +83 -1
- package/core/rules/presets/heuristics/ios.ts +329 -0
- package/integrations/config/skillsDetectorRegistry.ts +111 -1
- package/integrations/evidence/buildEvidence.ts +72 -0
- package/integrations/git/aiGateRepoPolicyFindings.ts +2 -1
- package/integrations/lifecycle/adapter.templates.json +50 -1
- package/integrations/lifecycle/doctor.ts +41 -5
- package/integrations/lifecycle/preWriteLease.ts +5 -1
- package/package.json +1 -1
- package/scripts/framework-menu-matrix-canary-evidence.ts +42 -1
- package/scripts/framework-menu-matrix-canary-lib.ts +25 -2
- package/scripts/framework-menu-matrix-canary-scenario.ts +3 -1
- package/scripts/framework-menu-matrix-canary-types.ts +6 -1
- package/scripts/framework-menu-system-notifications-macos-dialog-payload.ts +31 -3
- package/scripts/framework-menu-system-notifications-payloads-blocked.ts +76 -2
- package/scripts/framework-menu-system-notifications-payloads.ts +1 -0
- package/scripts/framework-menu-system-notifications-remediation.ts +97 -2
- package/skills.lock.json +1 -1
|
@@ -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;
|