pumuki 6.3.148 → 6.3.149
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.
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
findSwiftOpenClosedSwitchMatch,
|
|
7
7
|
findSwiftConcreteDependencyDipMatch,
|
|
8
8
|
findSwiftPresentationSrpMatch,
|
|
9
|
+
findSwiftXCTestSrpMatch,
|
|
9
10
|
hasSwiftAnyViewUsage,
|
|
10
11
|
hasSwiftCallbackStyleSignature,
|
|
11
12
|
hasSwiftCornerRadiusUsage,
|
|
@@ -888,6 +889,69 @@ final class PumukiOcpIosCanaryUseCase {
|
|
|
888
889
|
assert.match(match.expected_fix, /estrategia|protocolo|registry/i);
|
|
889
890
|
});
|
|
890
891
|
|
|
892
|
+
test('findSwiftOpenClosedSwitchMatch detecta switch sobre outcome en Coordinator iOS', () => {
|
|
893
|
+
const source = `public final class LaunchFlowCoordinator {
|
|
894
|
+
public func bootstrap() async {
|
|
895
|
+
let outcome = await appConfigurationUseCase.execute()
|
|
896
|
+
switch outcome {
|
|
897
|
+
case .mandatoryUpdate:
|
|
898
|
+
route = .updateRequired
|
|
899
|
+
case .maintenance:
|
|
900
|
+
route = .maintenance
|
|
901
|
+
case .proceed:
|
|
902
|
+
route = .home
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
`;
|
|
907
|
+
|
|
908
|
+
const match = findSwiftOpenClosedSwitchMatch(source);
|
|
909
|
+
|
|
910
|
+
assert.ok(match);
|
|
911
|
+
assert.equal(match.primary_node.name, 'LaunchFlowCoordinator');
|
|
912
|
+
assert.deepEqual(match.related_nodes, [
|
|
913
|
+
{ kind: 'member', name: 'discriminator switch: outcome', lines: [4] },
|
|
914
|
+
{ kind: 'member', name: 'case .mandatoryUpdate', lines: [5] },
|
|
915
|
+
{ kind: 'member', name: 'case .maintenance', lines: [7] },
|
|
916
|
+
{ kind: 'member', name: 'case .proceed', lines: [9] },
|
|
917
|
+
]);
|
|
918
|
+
assert.match(match.why, /OCP/);
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
test('findSwiftXCTestSrpMatch detecta XCTestCase con responsabilidades mezcladas', () => {
|
|
922
|
+
const source = `import XCTest
|
|
923
|
+
|
|
924
|
+
final class LaunchFlowCoordinatorConfigTests: XCTestCase {
|
|
925
|
+
func test_bootstrap_whenMandatoryUpdate_routesToUpdateRequired() async {}
|
|
926
|
+
func test_bootstrap_whenSessionIsValid_routesHome() async {}
|
|
927
|
+
func test_completeOnboarding_marksProgressAndRoutesToLogin() async {}
|
|
928
|
+
}
|
|
929
|
+
`;
|
|
930
|
+
|
|
931
|
+
const match = findSwiftXCTestSrpMatch(source);
|
|
932
|
+
|
|
933
|
+
assert.ok(match);
|
|
934
|
+
assert.equal(match.primary_node.name, 'LaunchFlowCoordinatorConfigTests');
|
|
935
|
+
assert.deepEqual(match.related_nodes, [
|
|
936
|
+
{ kind: 'member', name: 'session routing tests', lines: [5] },
|
|
937
|
+
{ kind: 'member', name: 'onboarding progress tests', lines: [6] },
|
|
938
|
+
]);
|
|
939
|
+
assert.match(match.why, /XCTestCase|SRP/);
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
test('findSwiftXCTestSrpMatch permite XCTestCase enfocado en una responsabilidad', () => {
|
|
943
|
+
const source = `import XCTest
|
|
944
|
+
|
|
945
|
+
final class LaunchFlowCoordinatorNonBlockingConfigTests: XCTestCase {
|
|
946
|
+
func test_bootstrap_whenConfigFetchFailsWithCache_usesCachedConfigAndContinues() async {}
|
|
947
|
+
func test_bootstrap_whenOptionalUpdate_allowsAccessAndContinues() async {}
|
|
948
|
+
func test_bootstrap_whenProceed_continuesToSessionValidation() async {}
|
|
949
|
+
}
|
|
950
|
+
`;
|
|
951
|
+
|
|
952
|
+
assert.equal(findSwiftXCTestSrpMatch(source), undefined);
|
|
953
|
+
});
|
|
954
|
+
|
|
891
955
|
test('findSwiftInterfaceSegregationMatch devuelve payload semantico para ISP-iOS en application', () => {
|
|
892
956
|
const source = `
|
|
893
957
|
protocol PumukiIspIosCanarySessionManaging {
|
|
@@ -32,6 +32,8 @@ export type SwiftPresentationSrpMatch = {
|
|
|
32
32
|
lines: readonly number[];
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
export type SwiftXCTestSrpMatch = SwiftPresentationSrpMatch;
|
|
36
|
+
|
|
35
37
|
export type SwiftConcreteDependencyDipMatch = {
|
|
36
38
|
primary_node: SwiftSemanticNodeMatch;
|
|
37
39
|
related_nodes: readonly SwiftSemanticNodeMatch[];
|
|
@@ -1185,6 +1187,100 @@ export const findSwiftPresentationSrpMatch = (
|
|
|
1185
1187
|
};
|
|
1186
1188
|
};
|
|
1187
1189
|
|
|
1190
|
+
export const findSwiftXCTestSrpMatch = (source: string): SwiftXCTestSrpMatch | undefined => {
|
|
1191
|
+
if (!hasSwiftXCTestCaseSubclassUsage(source)) {
|
|
1192
|
+
return undefined;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const classPattern = /\b(?:final\s+)?class\s+([A-Za-z0-9_]*Tests?)\s*:\s*XCTestCase\b/;
|
|
1196
|
+
const classLines = collectSwiftRegexLines(source, classPattern);
|
|
1197
|
+
if (classLines.length === 0) {
|
|
1198
|
+
return undefined;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const classLine = source.split(/\r?\n/)[classLines[0] - 1] ?? '';
|
|
1202
|
+
const className = classLine.match(classPattern)?.[1];
|
|
1203
|
+
if (!className) {
|
|
1204
|
+
return undefined;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
const sourceLines = source.split(/\r?\n/);
|
|
1208
|
+
const familyPatterns: ReadonlyArray<{
|
|
1209
|
+
key: string;
|
|
1210
|
+
name: string;
|
|
1211
|
+
tokens: readonly string[];
|
|
1212
|
+
}> = [
|
|
1213
|
+
{ key: 'configuration', name: 'configuration outcome tests', tokens: ['config', 'configuration', 'update', 'cache'] },
|
|
1214
|
+
{ key: 'session', name: 'session routing tests', tokens: ['session', 'login', 'auth', 'valid', 'invalid'] },
|
|
1215
|
+
{ key: 'onboarding', name: 'onboarding progress tests', tokens: ['onboarding', 'progress', 'completeonboarding'] },
|
|
1216
|
+
{ key: 'permissions', name: 'permissions routing tests', tokens: ['permission', 'camera', 'location', 'notification'] },
|
|
1217
|
+
{ key: 'tutorial', name: 'tutorial or feature discovery tests', tokens: ['tutorial', 'featurediscovery'] },
|
|
1218
|
+
{ key: 'splash', name: 'splash delay tests', tokens: ['splash', 'delay', 'start'] },
|
|
1219
|
+
];
|
|
1220
|
+
const matchesFamily = (value: string, tokens: readonly string[]): boolean => {
|
|
1221
|
+
const normalizedValue = value.toLowerCase();
|
|
1222
|
+
return tokens.some((token) => normalizedValue.includes(token));
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
const classFamilyKeys = new Set(
|
|
1226
|
+
familyPatterns
|
|
1227
|
+
.filter((entry) => matchesFamily(className, entry.tokens))
|
|
1228
|
+
.map((entry) => entry.key)
|
|
1229
|
+
);
|
|
1230
|
+
const matchedFamilies = new Map<string, SwiftSemanticNodeMatch>();
|
|
1231
|
+
|
|
1232
|
+
sourceLines.forEach((line, index) => {
|
|
1233
|
+
const sanitizedLine = stripSwiftLineForSemanticScan(line);
|
|
1234
|
+
const methodMatch = sanitizedLine.match(/\bfunc\s+(test[A-Za-z0-9_]+)\s*\(/);
|
|
1235
|
+
const methodName = methodMatch?.[1];
|
|
1236
|
+
if (!methodName) {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
for (const family of familyPatterns) {
|
|
1241
|
+
if (!matchesFamily(methodName, family.tokens)) {
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
if (classFamilyKeys.has(family.key)) {
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
if (!matchedFamilies.has(family.key)) {
|
|
1248
|
+
matchedFamilies.set(family.key, {
|
|
1249
|
+
kind: 'member',
|
|
1250
|
+
name: family.name,
|
|
1251
|
+
lines: [index + 1],
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
const relatedNodes = [...matchedFamilies.values()];
|
|
1258
|
+
if (relatedNodes.length < 2) {
|
|
1259
|
+
return undefined;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const relatedNodeNames = relatedNodes.map((node) => node.name).join(', ');
|
|
1263
|
+
const allLines = sortedUniqueLines([
|
|
1264
|
+
...classLines,
|
|
1265
|
+
...relatedNodes.flatMap((node) => [...node.lines]),
|
|
1266
|
+
]);
|
|
1267
|
+
|
|
1268
|
+
return {
|
|
1269
|
+
primary_node: {
|
|
1270
|
+
kind: 'class',
|
|
1271
|
+
name: className,
|
|
1272
|
+
lines: classLines,
|
|
1273
|
+
},
|
|
1274
|
+
related_nodes: relatedNodes,
|
|
1275
|
+
why: `${className} mezcla ${relatedNodeNames} dentro del mismo XCTestCase, rompiendo SRP en la suite de tests.`,
|
|
1276
|
+
impact:
|
|
1277
|
+
'La suite acumula múltiples razones de cambio, oculta regresiones por responsabilidad y hace más difícil aislar el baseline afectado antes de PRE_WRITE.',
|
|
1278
|
+
expected_fix:
|
|
1279
|
+
'Divide la suite por responsabilidad observable: configuración, sesión, onboarding, permisos, tutorial o splash deben vivir en XCTestCase separados.',
|
|
1280
|
+
lines: allLines,
|
|
1281
|
+
};
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1188
1284
|
export const findSwiftConcreteDependencyDipMatch = (
|
|
1189
1285
|
source: string
|
|
1190
1286
|
): SwiftConcreteDependencyDipMatch | undefined => {
|
|
@@ -1275,7 +1371,7 @@ export const findSwiftOpenClosedSwitchMatch = (
|
|
|
1275
1371
|
}
|
|
1276
1372
|
|
|
1277
1373
|
const lines = source.split(/\r?\n/);
|
|
1278
|
-
const discriminatorPattern = /\b(?:kind|type|mode|channel|variant|provider|route|flow|source|experience)\b/i;
|
|
1374
|
+
const discriminatorPattern = /\b(?:kind|type|mode|channel|variant|provider|route|flow|source|experience|outcome|state|status|configuration|config)\b/i;
|
|
1279
1375
|
const switchPattern = /\bswitch\s+([A-Za-z_][A-Za-z0-9_\.]*)\s*\{/;
|
|
1280
1376
|
|
|
1281
1377
|
for (let index = 0; index < lines.length; index += 1) {
|
|
@@ -901,11 +901,30 @@ export const extractHeuristicFacts = (
|
|
|
901
901
|
}
|
|
902
902
|
}
|
|
903
903
|
|
|
904
|
-
if (
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
904
|
+
if (params.detectedPlatforms.ios?.detected && isIOSSwiftPath(fileFact.path)) {
|
|
905
|
+
if (isSwiftTestPath(fileFact.path)) {
|
|
906
|
+
const semanticTestSrpMatch = TextIOS.findSwiftXCTestSrpMatch(fileFact.content);
|
|
907
|
+
if (semanticTestSrpMatch) {
|
|
908
|
+
heuristicFacts.push(
|
|
909
|
+
createHeuristicFact({
|
|
910
|
+
ruleId: 'heuristics.ios.solid.srp.presentation-mixed-responsibilities.ast',
|
|
911
|
+
code: 'HEURISTICS_IOS_SOLID_SRP_XCTEST_MIXED_RESPONSIBILITIES_AST',
|
|
912
|
+
message:
|
|
913
|
+
'Semantic iOS SRP heuristic detected an XCTestCase suite mixing multiple responsibilities.',
|
|
914
|
+
filePath: fileFact.path,
|
|
915
|
+
lines: semanticTestSrpMatch.lines,
|
|
916
|
+
severity: 'CRITICAL',
|
|
917
|
+
primary_node: semanticTestSrpMatch.primary_node,
|
|
918
|
+
related_nodes: semanticTestSrpMatch.related_nodes,
|
|
919
|
+
why: semanticTestSrpMatch.why,
|
|
920
|
+
impact: semanticTestSrpMatch.impact,
|
|
921
|
+
expected_fix: semanticTestSrpMatch.expected_fix,
|
|
922
|
+
})
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (!isSwiftTestPath(fileFact.path)) {
|
|
909
928
|
if (isIOSApplicationOrPresentationPath(fileFact.path)) {
|
|
910
929
|
const semanticOcpMatch = TextIOS.findSwiftOpenClosedSwitchMatch(fileFact.content);
|
|
911
930
|
if (semanticOcpMatch) {
|
|
@@ -1027,6 +1046,7 @@ export const extractHeuristicFacts = (
|
|
|
1027
1046
|
})
|
|
1028
1047
|
);
|
|
1029
1048
|
}
|
|
1049
|
+
}
|
|
1030
1050
|
}
|
|
1031
1051
|
|
|
1032
1052
|
if (
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.149",
|
|
4
4
|
"description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|