pumuki 6.3.122 → 6.3.124
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 +8 -0
- package/VERSION +1 -1
- package/core/facts/detectors/astRuleThresholdAudit.test.ts +33 -0
- package/core/facts/detectors/security/index.test.ts +8 -2
- package/core/facts/detectors/security/securityCredentials.test.ts +10 -4
- package/core/facts/detectors/security/securityCredentials.ts +11 -4
- package/core/facts/detectors/text/android.test.ts +40 -0
- package/core/facts/detectors/text/android.ts +68 -25
- package/core/facts/detectors/text/ios.test.ts +48 -0
- package/core/facts/detectors/text/ios.ts +134 -60
- package/core/facts/detectors/typescript/index.ts +10 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,14 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [6.3.123] - 2026-04-28
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Detectores SOLID sin contadores arbitrarios:** `SRP`, `DIP`, `OCP` e `ISP` en iOS/Android/TypeScript dejan de depender de mínimos tipo `relatedNodes.length < N`, `typedCaseCount >= N` o cortes `slice(0, N)` y pasan a usar categorías semánticas explícitas.
|
|
14
|
+
- **ISP iOS/Android por familias de contrato:** los protocolos/interfaces anchos se detectan por mezcla real de `query`/`command` y uso de una sola familia por consumidor, no por número de miembros.
|
|
15
|
+
- **Cierre de `PUMUKI-INC-116`:** se añade regresión negativa para clases/protocolos grandes pero cohesionados y auditoría textual para impedir que los detectores estructurales vuelvan a degradarse a `N señales => bloqueo`.
|
|
16
|
+
|
|
9
17
|
## [6.3.122] - 2026-04-28
|
|
10
18
|
|
|
11
19
|
### Fixed
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
1
|
+
v6.3.123
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
|
|
6
|
+
const detectorFiles = [
|
|
7
|
+
'core/facts/detectors/text/ios.ts',
|
|
8
|
+
'core/facts/detectors/text/android.ts',
|
|
9
|
+
'core/facts/detectors/typescript/index.ts',
|
|
10
|
+
'core/facts/detectors/security/securityCredentials.ts',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const forbiddenDecisionThresholds = [
|
|
14
|
+
/typeDeclarations\.length\s*<\s*\d+/,
|
|
15
|
+
/conformingTypes\.length\s*<\s*\d+/,
|
|
16
|
+
/trim\(\)\.length\s*>=\s*\d+/,
|
|
17
|
+
/relatedNodes\.length\s*<\s*\d+/,
|
|
18
|
+
/typedCaseCount\s*>=\s*\d+/,
|
|
19
|
+
/caseNodes\.length\s*<\s*\d+/,
|
|
20
|
+
/branchNodes\.length\s*<\s*\d+/,
|
|
21
|
+
/slice\(0,\s*\d+\)/,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
test('detectores AST estructurales no usan umbrales internos para decidir skills', () => {
|
|
25
|
+
const violations = detectorFiles.flatMap((filePath) => {
|
|
26
|
+
const content = readFileSync(join(process.cwd(), filePath), 'utf8');
|
|
27
|
+
return forbiddenDecisionThresholds
|
|
28
|
+
.filter((pattern) => pattern.test(content))
|
|
29
|
+
.map((pattern) => `${filePath}: ${pattern.source}`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
assert.deepEqual(violations, []);
|
|
33
|
+
});
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
hasWeakTokenGenerationWithCryptoRandomUuid,
|
|
16
16
|
} from './index';
|
|
17
17
|
|
|
18
|
-
test('hasHardcodedSecretTokenLiteral detecta literales
|
|
18
|
+
test('hasHardcodedSecretTokenLiteral detecta literales reales en identificadores sensibles', () => {
|
|
19
19
|
const ast = {
|
|
20
20
|
type: 'VariableDeclarator',
|
|
21
21
|
id: { type: 'Identifier', name: 'apiToken' },
|
|
@@ -24,11 +24,17 @@ test('hasHardcodedSecretTokenLiteral detecta literales fuertes en identificadore
|
|
|
24
24
|
const safeAst = {
|
|
25
25
|
type: 'VariableDeclarator',
|
|
26
26
|
id: { type: 'Identifier', name: 'apiToken' },
|
|
27
|
-
init: { type: 'StringLiteral', value: '
|
|
27
|
+
init: { type: 'StringLiteral', value: 'example' },
|
|
28
|
+
};
|
|
29
|
+
const shortRealSecretAst = {
|
|
30
|
+
type: 'VariableDeclarator',
|
|
31
|
+
id: { type: 'Identifier', name: 'apiToken' },
|
|
32
|
+
init: { type: 'StringLiteral', value: 'prod' },
|
|
28
33
|
};
|
|
29
34
|
|
|
30
35
|
assert.equal(hasHardcodedSecretTokenLiteral(ast), true);
|
|
31
36
|
assert.equal(hasHardcodedSecretTokenLiteral(safeAst), false);
|
|
37
|
+
assert.equal(hasHardcodedSecretTokenLiteral(shortRealSecretAst), true);
|
|
32
38
|
});
|
|
33
39
|
|
|
34
40
|
test('hasInsecureTokenGenerationWithMathRandom detecta Math.random en asignacion sensible', () => {
|
|
@@ -11,16 +11,21 @@ import {
|
|
|
11
11
|
hasWeakTokenGenerationWithCryptoRandomUuid,
|
|
12
12
|
} from './securityCredentials';
|
|
13
13
|
|
|
14
|
-
test('hasHardcodedSecretTokenLiteral detecta literal
|
|
14
|
+
test('hasHardcodedSecretTokenLiteral detecta literal real en identificador sensible', () => {
|
|
15
15
|
const hardcodedSecretAst = {
|
|
16
16
|
type: 'VariableDeclarator',
|
|
17
17
|
id: { type: 'Identifier', name: 'apiKey' },
|
|
18
18
|
init: { type: 'StringLiteral', value: 'super-secret-key-123' },
|
|
19
19
|
};
|
|
20
|
-
const
|
|
20
|
+
const placeholderAst = {
|
|
21
21
|
type: 'VariableDeclarator',
|
|
22
22
|
id: { type: 'Identifier', name: 'apiKey' },
|
|
23
|
-
init: { type: 'StringLiteral', value: '
|
|
23
|
+
init: { type: 'StringLiteral', value: 'replace-me' },
|
|
24
|
+
};
|
|
25
|
+
const shortRealSecretAst = {
|
|
26
|
+
type: 'VariableDeclarator',
|
|
27
|
+
id: { type: 'Identifier', name: 'apiKey' },
|
|
28
|
+
init: { type: 'StringLiteral', value: 'prod' },
|
|
24
29
|
};
|
|
25
30
|
const nonSensitiveIdentifierAst = {
|
|
26
31
|
type: 'VariableDeclarator',
|
|
@@ -29,7 +34,8 @@ test('hasHardcodedSecretTokenLiteral detecta literal largo en identificador sens
|
|
|
29
34
|
};
|
|
30
35
|
|
|
31
36
|
assert.equal(hasHardcodedSecretTokenLiteral(hardcodedSecretAst), true);
|
|
32
|
-
assert.equal(hasHardcodedSecretTokenLiteral(
|
|
37
|
+
assert.equal(hasHardcodedSecretTokenLiteral(placeholderAst), false);
|
|
38
|
+
assert.equal(hasHardcodedSecretTokenLiteral(shortRealSecretAst), true);
|
|
33
39
|
assert.equal(hasHardcodedSecretTokenLiteral(nonSensitiveIdentifierAst), false);
|
|
34
40
|
});
|
|
35
41
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { collectNodeLineMatches, hasNode, isObject } from '../utils/astHelpers';
|
|
2
2
|
|
|
3
3
|
const sensitiveIdentifierPattern = /(secret|token|password|api[_-]?key)/i;
|
|
4
|
+
const placeholderSecretLiteralPattern =
|
|
5
|
+
/^(?:changeme|change-me|change_me|replace-me|replace_me|todo|tbd|example|sample|dummy|test|testing|placeholder|your[_-]?(?:secret|token|password|api[_-]?key)|xxx+)$/i;
|
|
4
6
|
|
|
5
|
-
const
|
|
7
|
+
const hasCredentialLiteralValue = (value: unknown): boolean => {
|
|
6
8
|
if (!isObject(value)) {
|
|
7
9
|
return false;
|
|
8
10
|
}
|
|
9
11
|
if (value.type === 'StringLiteral') {
|
|
10
|
-
return typeof value.value === 'string' && value.value
|
|
12
|
+
return typeof value.value === 'string' && isCredentialLiteral(value.value);
|
|
11
13
|
}
|
|
12
14
|
if (
|
|
13
15
|
value.type === 'TemplateLiteral' &&
|
|
@@ -17,11 +19,16 @@ const hasStrongLiteralValue = (value: unknown): boolean => {
|
|
|
17
19
|
value.quasis.length === 1
|
|
18
20
|
) {
|
|
19
21
|
const cooked = value.quasis[0]?.value?.cooked;
|
|
20
|
-
return typeof cooked === 'string' && cooked
|
|
22
|
+
return typeof cooked === 'string' && isCredentialLiteral(cooked);
|
|
21
23
|
}
|
|
22
24
|
return false;
|
|
23
25
|
};
|
|
24
26
|
|
|
27
|
+
const isCredentialLiteral = (value: string): boolean => {
|
|
28
|
+
const normalized = value.trim();
|
|
29
|
+
return normalized.length > 0 && !placeholderSecretLiteralPattern.test(normalized);
|
|
30
|
+
};
|
|
31
|
+
|
|
25
32
|
const containsMathRandomCall = (candidate: unknown): boolean => {
|
|
26
33
|
return hasNode(candidate, (value) => {
|
|
27
34
|
if (value.type !== 'CallExpression') {
|
|
@@ -109,7 +116,7 @@ const isHardcodedSecretTokenLiteralNode = (value: Record<string, string | number
|
|
|
109
116
|
if (!sensitiveIdentifierPattern.test(idNode.name as string)) {
|
|
110
117
|
return false;
|
|
111
118
|
}
|
|
112
|
-
return
|
|
119
|
+
return hasCredentialLiteralValue(value.init);
|
|
113
120
|
};
|
|
114
121
|
|
|
115
122
|
const isInsecureTokenGenerationWithMathRandomNode = (value: Record<string, string | number | boolean | bigint | symbol | null | Date | object>): boolean => {
|
|
@@ -254,6 +254,46 @@ class PumukiIspAndroidCanaryUseCase(
|
|
|
254
254
|
assert.match(match.expected_fix, /interfaces pequeñas|puerto mínimo/i);
|
|
255
255
|
});
|
|
256
256
|
|
|
257
|
+
test('detectores SOLID Android no convierten tamaño o cardinalidad en violacion', () => {
|
|
258
|
+
const presentationWithoutMixedResponsibilities = `
|
|
259
|
+
class CatalogViewModel {
|
|
260
|
+
fun restoreSessionSnapshot() {}
|
|
261
|
+
fun refreshSessionToken() {}
|
|
262
|
+
fun resumeSessionIfNeeded() {}
|
|
263
|
+
fun signOut() {}
|
|
264
|
+
}
|
|
265
|
+
`;
|
|
266
|
+
const dipWithPortOnly = `
|
|
267
|
+
interface CatalogFetching {
|
|
268
|
+
suspend fun fetchCatalog(): List<String>
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
class CatalogUseCase(
|
|
272
|
+
private val catalog: CatalogFetching,
|
|
273
|
+
)
|
|
274
|
+
`;
|
|
275
|
+
const cohesiveInterface = `
|
|
276
|
+
interface CatalogReading {
|
|
277
|
+
suspend fun fetchCatalog(): List<String>
|
|
278
|
+
suspend fun loadCachedCatalog(): List<String>
|
|
279
|
+
suspend fun readCatalogVersion(): String
|
|
280
|
+
suspend fun getFeaturedCatalog(): List<String>
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
class CatalogUseCase(
|
|
284
|
+
private val catalog: CatalogReading,
|
|
285
|
+
) {
|
|
286
|
+
suspend fun execute() {
|
|
287
|
+
catalog.fetchCatalog()
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
`;
|
|
291
|
+
|
|
292
|
+
assert.equal(findKotlinPresentationSrpMatch(presentationWithoutMixedResponsibilities), undefined);
|
|
293
|
+
assert.equal(findKotlinConcreteDependencyDipMatch(dipWithPortOnly), undefined);
|
|
294
|
+
assert.equal(findKotlinInterfaceSegregationMatch(cohesiveInterface), undefined);
|
|
295
|
+
});
|
|
296
|
+
|
|
257
297
|
test('findKotlinLiskovSubstitutionMatch devuelve payload semantico para LSP-Android en application', () => {
|
|
258
298
|
const source = `
|
|
259
299
|
interface PumukiLspAndroidCanaryDiscountPolicy {
|
|
@@ -70,6 +70,40 @@ type KotlinTypeDeclaration = {
|
|
|
70
70
|
bodyEndLine: number;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
type KotlinResponsibilityMatch = {
|
|
74
|
+
key: string;
|
|
75
|
+
node: KotlinSemanticNodeMatch;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const kotlinQueryMemberNamePattern = /^(get|find|list|fetch|read|load|restore|refresh|is|has|can)/i;
|
|
79
|
+
const kotlinCommandMemberNamePattern =
|
|
80
|
+
/^(create|update|delete|remove|save|insert|upsert|set|write|persist|clear|reset|sync|store)/i;
|
|
81
|
+
|
|
82
|
+
const registerKotlinResponsibility = (
|
|
83
|
+
nodes: KotlinResponsibilityMatch[],
|
|
84
|
+
key: string,
|
|
85
|
+
kind: KotlinSemanticNodeMatch['kind'],
|
|
86
|
+
name: string,
|
|
87
|
+
lines: readonly number[]
|
|
88
|
+
): void => {
|
|
89
|
+
if (lines.length === 0) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
nodes.push({ key, node: { kind, name, lines } });
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const hasKotlinResponsibilityKeys = (
|
|
96
|
+
nodes: readonly KotlinResponsibilityMatch[],
|
|
97
|
+
keys: readonly string[]
|
|
98
|
+
): boolean => {
|
|
99
|
+
const observedKeys = new Set(nodes.map((node) => node.key));
|
|
100
|
+
return keys.every((key) => observedKeys.has(key));
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const isKotlinQueryMemberName = (name: string): boolean => kotlinQueryMemberNamePattern.test(name);
|
|
104
|
+
const isKotlinCommandMemberName = (name: string): boolean =>
|
|
105
|
+
kotlinCommandMemberNamePattern.test(name);
|
|
106
|
+
|
|
73
107
|
const stripKotlinLineForSemanticScan = (line: string): string => {
|
|
74
108
|
return line
|
|
75
109
|
.replace(/\/\/.*$/, '')
|
|
@@ -274,44 +308,46 @@ export const findKotlinPresentationSrpMatch = (
|
|
|
274
308
|
return undefined;
|
|
275
309
|
}
|
|
276
310
|
|
|
277
|
-
const
|
|
311
|
+
const responsibilities: KotlinResponsibilityMatch[] = [];
|
|
278
312
|
const registerNode = (
|
|
313
|
+
key: string,
|
|
279
314
|
kind: KotlinSemanticNodeMatch['kind'],
|
|
280
315
|
name: string,
|
|
281
316
|
regex: RegExp
|
|
282
317
|
): void => {
|
|
283
|
-
|
|
284
|
-
if (lines.length === 0) {
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
relatedNodes.push({ kind, name, lines });
|
|
318
|
+
registerKotlinResponsibility(responsibilities, key, kind, name, collectKotlinRegexLines(source, regex));
|
|
288
319
|
};
|
|
289
320
|
|
|
290
321
|
registerNode(
|
|
322
|
+
'session',
|
|
291
323
|
'member',
|
|
292
324
|
'session/auth flow',
|
|
293
325
|
/\b(?:restore|bootstrap|refresh|resume|signIn|signOut|authenticate|session)\w*\s*\(/
|
|
294
326
|
);
|
|
295
327
|
registerNode(
|
|
328
|
+
'networking',
|
|
296
329
|
'call',
|
|
297
330
|
'remote networking',
|
|
298
331
|
/\b(?:OkHttpClient\s*\(|Retrofit\.Builder\s*\(|HttpURLConnection\b)/
|
|
299
332
|
);
|
|
300
333
|
registerNode(
|
|
334
|
+
'persistence',
|
|
301
335
|
'call',
|
|
302
336
|
'local persistence',
|
|
303
337
|
/\b(?:SharedPreferences\b.*\)|PreferenceDataStoreFactory\.create|preferencesDataStore|DataStore<|RoomDatabase\b)/
|
|
304
338
|
);
|
|
305
339
|
registerNode(
|
|
340
|
+
'navigation',
|
|
306
341
|
'member',
|
|
307
342
|
'navigation flow',
|
|
308
343
|
/\b(?:findNavController\s*\(|\.\s*navigate\s*\()/
|
|
309
344
|
);
|
|
310
345
|
|
|
311
|
-
if (
|
|
346
|
+
if (!hasKotlinResponsibilityKeys(responsibilities, ['session', 'networking', 'persistence', 'navigation'])) {
|
|
312
347
|
return undefined;
|
|
313
348
|
}
|
|
314
349
|
|
|
350
|
+
const relatedNodes = responsibilities.map((entry) => entry.node);
|
|
315
351
|
const allLines = sortedUniqueLines([
|
|
316
352
|
...classLines,
|
|
317
353
|
...relatedNodes.flatMap((node) => [...node.lines]),
|
|
@@ -396,7 +432,7 @@ export const findKotlinConcreteDependencyDipMatch = (
|
|
|
396
432
|
);
|
|
397
433
|
registerNode('call', 'Room.databaseBuilder', /\bRoom\.databaseBuilder\s*\(/);
|
|
398
434
|
|
|
399
|
-
if (relatedNodes.length
|
|
435
|
+
if (relatedNodes.length === 0) {
|
|
400
436
|
return undefined;
|
|
401
437
|
}
|
|
402
438
|
|
|
@@ -473,7 +509,8 @@ export const findKotlinOpenClosedWhenMatch = (
|
|
|
473
509
|
}
|
|
474
510
|
}
|
|
475
511
|
|
|
476
|
-
|
|
512
|
+
const [firstBranchNode, secondBranchNode] = branchNodes;
|
|
513
|
+
if (!firstBranchNode || !secondBranchNode) {
|
|
477
514
|
continue;
|
|
478
515
|
}
|
|
479
516
|
|
|
@@ -483,7 +520,7 @@ export const findKotlinOpenClosedWhenMatch = (
|
|
|
483
520
|
name: `discriminator switch: ${discriminatorName}`,
|
|
484
521
|
lines: [index + 1],
|
|
485
522
|
},
|
|
486
|
-
...branchNodes
|
|
523
|
+
...branchNodes,
|
|
487
524
|
];
|
|
488
525
|
|
|
489
526
|
const allLines = sortedUniqueLines([
|
|
@@ -492,7 +529,6 @@ export const findKotlinOpenClosedWhenMatch = (
|
|
|
492
529
|
...branchNodes.flatMap((node) => [...node.lines]),
|
|
493
530
|
]);
|
|
494
531
|
const branchSummary = branchNodes
|
|
495
|
-
.slice(0, 3)
|
|
496
532
|
.map((node) => node.name.replace(/^branch /, ''))
|
|
497
533
|
.join(', ');
|
|
498
534
|
|
|
@@ -541,7 +577,13 @@ export const findKotlinInterfaceSegregationMatch = (
|
|
|
541
577
|
const sourceLines = source.split(/\r?\n/);
|
|
542
578
|
|
|
543
579
|
for (const interfaceDeclaration of interfaceDeclarations) {
|
|
544
|
-
|
|
580
|
+
const queryMembers = interfaceDeclaration.members.filter((member) =>
|
|
581
|
+
isKotlinQueryMemberName(member.name)
|
|
582
|
+
);
|
|
583
|
+
const commandMembers = interfaceDeclaration.members.filter((member) =>
|
|
584
|
+
isKotlinCommandMemberName(member.name)
|
|
585
|
+
);
|
|
586
|
+
if (queryMembers.length === 0 || commandMembers.length === 0) {
|
|
545
587
|
continue;
|
|
546
588
|
}
|
|
547
589
|
|
|
@@ -579,14 +621,21 @@ export const findKotlinInterfaceSegregationMatch = (
|
|
|
579
621
|
}
|
|
580
622
|
});
|
|
581
623
|
|
|
582
|
-
|
|
624
|
+
const usedMemberNames = [...usedMembers.keys()];
|
|
625
|
+
if (usedMemberNames.length === 0) {
|
|
583
626
|
continue;
|
|
584
627
|
}
|
|
585
628
|
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
)
|
|
589
|
-
|
|
629
|
+
const usesQueryContract = usedMemberNames.some(isKotlinQueryMemberName);
|
|
630
|
+
const usesCommandContract = usedMemberNames.some(isKotlinCommandMemberName);
|
|
631
|
+
if (usesQueryContract === usesCommandContract) {
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const oppositeFamilyMembers = usesQueryContract ? commandMembers : queryMembers;
|
|
636
|
+
const unusedMembers = oppositeFamilyMembers.filter((member) => !usedMembers.has(member.name));
|
|
637
|
+
const firstUnusedMember = unusedMembers[0];
|
|
638
|
+
if (!firstUnusedMember) {
|
|
590
639
|
continue;
|
|
591
640
|
}
|
|
592
641
|
|
|
@@ -603,7 +652,7 @@ export const findKotlinInterfaceSegregationMatch = (
|
|
|
603
652
|
lines: [interfaceDeclaration.line],
|
|
604
653
|
},
|
|
605
654
|
...usedDescriptors,
|
|
606
|
-
...unusedMembers.
|
|
655
|
+
...unusedMembers.map((member) => ({
|
|
607
656
|
kind: 'member' as const,
|
|
608
657
|
name: `unused contract member: ${member.name}`,
|
|
609
658
|
lines: [member.line],
|
|
@@ -611,7 +660,7 @@ export const findKotlinInterfaceSegregationMatch = (
|
|
|
611
660
|
];
|
|
612
661
|
|
|
613
662
|
const usedMemberSummary = usedDescriptors.map((member) => member.name.replace('used member: ', ''));
|
|
614
|
-
const unusedSummary = unusedMembers.
|
|
663
|
+
const unusedSummary = unusedMembers.map((member) => member.name);
|
|
615
664
|
const allLines = sortedUniqueLines([
|
|
616
665
|
...typeLines,
|
|
617
666
|
interfaceDeclaration.line,
|
|
@@ -650,9 +699,6 @@ export const findKotlinLiskovSubstitutionMatch = (
|
|
|
650
699
|
}
|
|
651
700
|
|
|
652
701
|
const typeDeclarations = parseKotlinTypeDeclarations(source);
|
|
653
|
-
if (typeDeclarations.length < 2) {
|
|
654
|
-
return undefined;
|
|
655
|
-
}
|
|
656
702
|
|
|
657
703
|
const sourceLines = source.split(/\r?\n/);
|
|
658
704
|
|
|
@@ -665,9 +711,6 @@ export const findKotlinLiskovSubstitutionMatch = (
|
|
|
665
711
|
const conformingTypes = typeDeclarations.filter((typeDeclaration) =>
|
|
666
712
|
typeDeclaration.conformances.includes(interfaceDeclaration.name)
|
|
667
713
|
);
|
|
668
|
-
if (conformingTypes.length < 2) {
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
714
|
|
|
672
715
|
for (const memberName of memberNames) {
|
|
673
716
|
let safeType: KotlinTypeDeclaration | undefined;
|
|
@@ -807,6 +807,54 @@ final class PumukiIspIosCanaryUseCase {
|
|
|
807
807
|
assert.match(match.expected_fix, /protocolos pequeños|puerto mínimo/i);
|
|
808
808
|
});
|
|
809
809
|
|
|
810
|
+
test('detectores SOLID iOS no convierten tamaño o cardinalidad en violacion', () => {
|
|
811
|
+
const presentationWithoutMixedResponsibilities = `
|
|
812
|
+
final class CatalogViewModel {
|
|
813
|
+
func restoreSessionSnapshot() async {}
|
|
814
|
+
func refreshSessionToken() async {}
|
|
815
|
+
func resumeSessionIfNeeded() async {}
|
|
816
|
+
func signOut() async {}
|
|
817
|
+
}
|
|
818
|
+
`;
|
|
819
|
+
const dipWithPortOnly = `
|
|
820
|
+
protocol CatalogFetching {
|
|
821
|
+
func fetchCatalog() async throws -> [String]
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
final class CatalogUseCase {
|
|
825
|
+
private let catalog: CatalogFetching
|
|
826
|
+
|
|
827
|
+
init(catalog: CatalogFetching) {
|
|
828
|
+
self.catalog = catalog
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
`;
|
|
832
|
+
const cohesiveProtocol = `
|
|
833
|
+
protocol CatalogReading {
|
|
834
|
+
func fetchCatalog() async throws -> [String]
|
|
835
|
+
func loadCachedCatalog() async throws -> [String]
|
|
836
|
+
func readCatalogVersion() async throws -> String
|
|
837
|
+
func getFeaturedCatalog() async throws -> [String]
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
final class CatalogUseCase {
|
|
841
|
+
private let catalog: CatalogReading
|
|
842
|
+
|
|
843
|
+
init(catalog: CatalogReading) {
|
|
844
|
+
self.catalog = catalog
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
func execute() async throws {
|
|
848
|
+
_ = try await catalog.fetchCatalog()
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
`;
|
|
852
|
+
|
|
853
|
+
assert.equal(findSwiftPresentationSrpMatch(presentationWithoutMixedResponsibilities), undefined);
|
|
854
|
+
assert.equal(findSwiftConcreteDependencyDipMatch(dipWithPortOnly), undefined);
|
|
855
|
+
assert.equal(findSwiftInterfaceSegregationMatch(cohesiveProtocol), undefined);
|
|
856
|
+
});
|
|
857
|
+
|
|
810
858
|
test('findSwiftLiskovSubstitutionMatch devuelve payload semantico para LSP-iOS en application', () => {
|
|
811
859
|
const source = `
|
|
812
860
|
protocol PumukiLspIosCanaryDiscountApplying {
|
|
@@ -144,6 +144,39 @@ type SwiftTypeDeclaration = {
|
|
|
144
144
|
bodyEndLine: number;
|
|
145
145
|
};
|
|
146
146
|
|
|
147
|
+
type SwiftResponsibilityMatch = {
|
|
148
|
+
key: string;
|
|
149
|
+
node: SwiftSemanticNodeMatch;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const swiftQueryMemberNamePattern = /^(get|find|list|fetch|read|load|restore|refresh|is|has|can)/i;
|
|
153
|
+
const swiftCommandMemberNamePattern =
|
|
154
|
+
/^(create|update|delete|remove|save|insert|upsert|set|write|persist|clear|reset|sync|store)/i;
|
|
155
|
+
|
|
156
|
+
const registerSwiftResponsibility = (
|
|
157
|
+
nodes: SwiftResponsibilityMatch[],
|
|
158
|
+
key: string,
|
|
159
|
+
kind: SwiftSemanticNodeMatch['kind'],
|
|
160
|
+
name: string,
|
|
161
|
+
lines: readonly number[]
|
|
162
|
+
): void => {
|
|
163
|
+
if (lines.length === 0) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
nodes.push({ key, node: { kind, name, lines } });
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const hasSwiftResponsibilityKeys = (
|
|
170
|
+
nodes: readonly SwiftResponsibilityMatch[],
|
|
171
|
+
keys: readonly string[]
|
|
172
|
+
): boolean => {
|
|
173
|
+
const observedKeys = new Set(nodes.map((node) => node.key));
|
|
174
|
+
return keys.every((key) => observedKeys.has(key));
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const isSwiftQueryMemberName = (name: string): boolean => swiftQueryMemberNamePattern.test(name);
|
|
178
|
+
const isSwiftCommandMemberName = (name: string): boolean => swiftCommandMemberNamePattern.test(name);
|
|
179
|
+
|
|
147
180
|
const parseSwiftProtocolDeclarations = (source: string): readonly SwiftProtocolDeclaration[] => {
|
|
148
181
|
const lines = source.split(/\r?\n/);
|
|
149
182
|
const declarations: SwiftProtocolDeclaration[] = [];
|
|
@@ -874,31 +907,41 @@ export const findSwiftIOSCanary001Match = (source: string): SwiftIOSCanary001Mat
|
|
|
874
907
|
return undefined;
|
|
875
908
|
}
|
|
876
909
|
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
910
|
+
const explicitInfraResponsibilities: SwiftResponsibilityMatch[] = [];
|
|
911
|
+
registerSwiftResponsibility(
|
|
912
|
+
explicitInfraResponsibilities,
|
|
913
|
+
'shared-state',
|
|
914
|
+
'property',
|
|
915
|
+
'shared singleton',
|
|
916
|
+
collectSwiftRegexLines(source, /\bstatic\s+let\s+shared\b/)
|
|
917
|
+
);
|
|
918
|
+
registerSwiftResponsibility(
|
|
919
|
+
explicitInfraResponsibilities,
|
|
920
|
+
'networking',
|
|
921
|
+
'call',
|
|
922
|
+
'URLSession.shared',
|
|
923
|
+
collectSwiftRegexLines(source, /\bURLSession\.shared\b/)
|
|
924
|
+
);
|
|
925
|
+
registerSwiftResponsibility(
|
|
926
|
+
explicitInfraResponsibilities,
|
|
927
|
+
'persistence',
|
|
928
|
+
'call',
|
|
929
|
+
'FileManager.default',
|
|
930
|
+
collectSwiftRegexLines(source, /\bFileManager\.default\b/)
|
|
931
|
+
);
|
|
932
|
+
registerSwiftResponsibility(
|
|
933
|
+
explicitInfraResponsibilities,
|
|
934
|
+
'navigation',
|
|
896
935
|
'member',
|
|
897
936
|
'navigation flow',
|
|
898
|
-
|
|
937
|
+
collectSwiftRegexLines(
|
|
938
|
+
source,
|
|
939
|
+
/\b(?:router|route|coordinator|navigationPath|navigationDestination)\b|\b(?:navigate|dismiss|present)\s*\(/i
|
|
940
|
+
)
|
|
899
941
|
);
|
|
900
942
|
|
|
901
|
-
if (
|
|
943
|
+
if (hasSwiftResponsibilityKeys(explicitInfraResponsibilities, ['networking', 'persistence', 'navigation'])) {
|
|
944
|
+
const explicitInfraNodes = explicitInfraResponsibilities.map((entry) => entry.node);
|
|
902
945
|
const relatedNodeNames = explicitInfraNodes.map((node) => node.name).join(', ');
|
|
903
946
|
const allLines = sortedUniqueLines([
|
|
904
947
|
...classLines,
|
|
@@ -921,33 +964,55 @@ export const findSwiftIOSCanary001Match = (source: string): SwiftIOSCanary001Mat
|
|
|
921
964
|
};
|
|
922
965
|
}
|
|
923
966
|
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
967
|
+
const appShellResponsibilities: SwiftResponsibilityMatch[] = [];
|
|
968
|
+
registerSwiftResponsibility(
|
|
969
|
+
appShellResponsibilities,
|
|
970
|
+
'session',
|
|
927
971
|
'member',
|
|
928
972
|
'session bootstrap/restoration',
|
|
929
|
-
/\b(?:restorePersistedSessionIfNeeded|continueAsGuest|bootstrapAuthenticatedSession)\s*\(/
|
|
973
|
+
collectSwiftRegexLines(source, /\b(?:restorePersistedSessionIfNeeded|continueAsGuest|bootstrapAuthenticatedSession)\s*\(/)
|
|
930
974
|
);
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
975
|
+
registerSwiftResponsibility(
|
|
976
|
+
appShellResponsibilities,
|
|
977
|
+
'store',
|
|
978
|
+
'member',
|
|
979
|
+
'store selection orchestration',
|
|
980
|
+
collectSwiftRegexLines(source, /\bselectStore\s*\(/)
|
|
981
|
+
);
|
|
982
|
+
registerSwiftResponsibility(
|
|
983
|
+
appShellResponsibilities,
|
|
984
|
+
'shopping-list',
|
|
985
|
+
'member',
|
|
986
|
+
'shopping list synchronization',
|
|
987
|
+
collectSwiftRegexLines(source, /\bsyncShoppingList\s*\(/)
|
|
988
|
+
);
|
|
989
|
+
registerSwiftResponsibility(
|
|
990
|
+
appShellResponsibilities,
|
|
991
|
+
'route',
|
|
935
992
|
'member',
|
|
936
993
|
'route progression',
|
|
937
|
-
/\b(?:markNextStopCompleted|scanCheckpoint|rebuildRouteStatus)\s*\(/
|
|
994
|
+
collectSwiftRegexLines(source, /\b(?:markNextStopCompleted|scanCheckpoint|rebuildRouteStatus)\s*\(/)
|
|
938
995
|
);
|
|
939
|
-
|
|
940
|
-
|
|
996
|
+
registerSwiftResponsibility(
|
|
997
|
+
appShellResponsibilities,
|
|
998
|
+
'offline-queue',
|
|
941
999
|
'member',
|
|
942
1000
|
'offline queue coordination',
|
|
943
|
-
/\b(?:flushOfflineQueue|enqueueOfflineCheckpoint)\s*\(/
|
|
1001
|
+
collectSwiftRegexLines(source, /\b(?:flushOfflineQueue|enqueueOfflineCheckpoint)\s*\(/)
|
|
1002
|
+
);
|
|
1003
|
+
registerSwiftResponsibility(
|
|
1004
|
+
appShellResponsibilities,
|
|
1005
|
+
'navigation',
|
|
1006
|
+
'member',
|
|
1007
|
+
'deep link/navigation flow',
|
|
1008
|
+
collectSwiftRegexLines(source, /\bopenDeepLink\s*\(/)
|
|
944
1009
|
);
|
|
945
|
-
registerNode(appShellNodes, 'member', 'deep link/navigation flow', /\bopenDeepLink\s*\(/);
|
|
946
1010
|
|
|
947
|
-
if (
|
|
1011
|
+
if (!hasSwiftResponsibilityKeys(appShellResponsibilities, ['session', 'store', 'route', 'navigation'])) {
|
|
948
1012
|
return undefined;
|
|
949
1013
|
}
|
|
950
1014
|
|
|
1015
|
+
const appShellNodes = appShellResponsibilities.map((entry) => entry.node);
|
|
951
1016
|
const relatedNodeNames = appShellNodes.map((node) => node.name).join(', ');
|
|
952
1017
|
const allLines = sortedUniqueLines([
|
|
953
1018
|
...classLines,
|
|
@@ -985,44 +1050,46 @@ export const findSwiftPresentationSrpMatch = (
|
|
|
985
1050
|
return undefined;
|
|
986
1051
|
}
|
|
987
1052
|
|
|
988
|
-
const
|
|
1053
|
+
const responsibilities: SwiftResponsibilityMatch[] = [];
|
|
989
1054
|
const registerNode = (
|
|
1055
|
+
key: string,
|
|
990
1056
|
kind: SwiftSemanticNodeMatch['kind'],
|
|
991
1057
|
name: string,
|
|
992
1058
|
regex: RegExp
|
|
993
1059
|
): void => {
|
|
994
|
-
|
|
995
|
-
if (lines.length === 0) {
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
relatedNodes.push({ kind, name, lines });
|
|
1060
|
+
registerSwiftResponsibility(responsibilities, key, kind, name, collectSwiftRegexLines(source, regex));
|
|
999
1061
|
};
|
|
1000
1062
|
|
|
1001
1063
|
registerNode(
|
|
1064
|
+
'session',
|
|
1002
1065
|
'member',
|
|
1003
1066
|
'session/auth flow',
|
|
1004
1067
|
/\b(?:restore|bootstrap|refresh|resume|signIn|signOut|authenticate|session)\w*\s*\(/
|
|
1005
1068
|
);
|
|
1006
1069
|
registerNode(
|
|
1070
|
+
'networking',
|
|
1007
1071
|
'call',
|
|
1008
1072
|
'remote networking',
|
|
1009
1073
|
/\b(?:URLSession\.shared|URLRequest\b|dataTask\s*\(|uploadTask\s*\(|downloadTask\s*\()/
|
|
1010
1074
|
);
|
|
1011
1075
|
registerNode(
|
|
1076
|
+
'persistence',
|
|
1012
1077
|
'call',
|
|
1013
1078
|
'local persistence',
|
|
1014
1079
|
/\b(?:UserDefaults\.standard|FileManager\.default|Keychain|NSPersistentContainer|CoreData)\b/
|
|
1015
1080
|
);
|
|
1016
1081
|
registerNode(
|
|
1082
|
+
'navigation',
|
|
1017
1083
|
'member',
|
|
1018
1084
|
'navigation flow',
|
|
1019
1085
|
/\b(?:navigationPath|navigationDestination)\b|(?:\.\s*(?:navigate|present|dismiss|push|open)\s*\()/
|
|
1020
1086
|
);
|
|
1021
1087
|
|
|
1022
|
-
if (
|
|
1088
|
+
if (!hasSwiftResponsibilityKeys(responsibilities, ['session', 'networking', 'persistence', 'navigation'])) {
|
|
1023
1089
|
return undefined;
|
|
1024
1090
|
}
|
|
1025
1091
|
|
|
1092
|
+
const relatedNodes = responsibilities.map((entry) => entry.node);
|
|
1026
1093
|
const allLines = sortedUniqueLines([
|
|
1027
1094
|
...classLines,
|
|
1028
1095
|
...relatedNodes.flatMap((node) => [...node.lines]),
|
|
@@ -1092,7 +1159,7 @@ export const findSwiftConcreteDependencyDipMatch = (
|
|
|
1092
1159
|
);
|
|
1093
1160
|
registerNode('call', 'FileManager.default', /\bFileManager\.default\b/);
|
|
1094
1161
|
|
|
1095
|
-
if (relatedNodes.length
|
|
1162
|
+
if (relatedNodes.length === 0) {
|
|
1096
1163
|
return undefined;
|
|
1097
1164
|
}
|
|
1098
1165
|
|
|
@@ -1178,7 +1245,8 @@ export const findSwiftOpenClosedSwitchMatch = (
|
|
|
1178
1245
|
}
|
|
1179
1246
|
}
|
|
1180
1247
|
|
|
1181
|
-
|
|
1248
|
+
const [firstCaseNode, secondCaseNode] = caseNodes;
|
|
1249
|
+
if (!firstCaseNode || !secondCaseNode) {
|
|
1182
1250
|
continue;
|
|
1183
1251
|
}
|
|
1184
1252
|
|
|
@@ -1188,7 +1256,7 @@ export const findSwiftOpenClosedSwitchMatch = (
|
|
|
1188
1256
|
name: `discriminator switch: ${discriminatorName}`,
|
|
1189
1257
|
lines: [index + 1],
|
|
1190
1258
|
},
|
|
1191
|
-
...caseNodes
|
|
1259
|
+
...caseNodes,
|
|
1192
1260
|
];
|
|
1193
1261
|
|
|
1194
1262
|
const allLines = sortedUniqueLines([
|
|
@@ -1197,7 +1265,6 @@ export const findSwiftOpenClosedSwitchMatch = (
|
|
|
1197
1265
|
...caseNodes.flatMap((node) => [...node.lines]),
|
|
1198
1266
|
]);
|
|
1199
1267
|
const caseSummary = caseNodes
|
|
1200
|
-
.slice(0, 3)
|
|
1201
1268
|
.map((node) => node.name.replace(/^case\s+/, ''))
|
|
1202
1269
|
.join(', ');
|
|
1203
1270
|
|
|
@@ -1246,7 +1313,13 @@ export const findSwiftInterfaceSegregationMatch = (
|
|
|
1246
1313
|
const sourceLines = source.split(/\r?\n/);
|
|
1247
1314
|
|
|
1248
1315
|
for (const protocolDeclaration of protocolDeclarations) {
|
|
1249
|
-
|
|
1316
|
+
const queryMembers = protocolDeclaration.members.filter((member) =>
|
|
1317
|
+
isSwiftQueryMemberName(member.name)
|
|
1318
|
+
);
|
|
1319
|
+
const commandMembers = protocolDeclaration.members.filter((member) =>
|
|
1320
|
+
isSwiftCommandMemberName(member.name)
|
|
1321
|
+
);
|
|
1322
|
+
if (queryMembers.length === 0 || commandMembers.length === 0) {
|
|
1250
1323
|
continue;
|
|
1251
1324
|
}
|
|
1252
1325
|
|
|
@@ -1283,14 +1356,21 @@ export const findSwiftInterfaceSegregationMatch = (
|
|
|
1283
1356
|
}
|
|
1284
1357
|
});
|
|
1285
1358
|
|
|
1286
|
-
|
|
1359
|
+
const usedMemberNames = [...usedMembers.keys()];
|
|
1360
|
+
if (usedMemberNames.length === 0) {
|
|
1287
1361
|
continue;
|
|
1288
1362
|
}
|
|
1289
1363
|
|
|
1290
|
-
const
|
|
1291
|
-
|
|
1292
|
-
)
|
|
1293
|
-
|
|
1364
|
+
const usesQueryContract = usedMemberNames.some(isSwiftQueryMemberName);
|
|
1365
|
+
const usesCommandContract = usedMemberNames.some(isSwiftCommandMemberName);
|
|
1366
|
+
if (usesQueryContract === usesCommandContract) {
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
const oppositeFamilyMembers = usesQueryContract ? commandMembers : queryMembers;
|
|
1371
|
+
const unusedMembers = oppositeFamilyMembers.filter((member) => !usedMembers.has(member.name));
|
|
1372
|
+
const firstUnusedMember = unusedMembers[0];
|
|
1373
|
+
if (!firstUnusedMember) {
|
|
1294
1374
|
continue;
|
|
1295
1375
|
}
|
|
1296
1376
|
|
|
@@ -1311,7 +1391,7 @@ export const findSwiftInterfaceSegregationMatch = (
|
|
|
1311
1391
|
name: `used member: ${usedMemberName}`,
|
|
1312
1392
|
lines: sortedUniqueLines(usedMemberLines),
|
|
1313
1393
|
},
|
|
1314
|
-
...unusedMembers.
|
|
1394
|
+
...unusedMembers.map((member) => ({
|
|
1315
1395
|
kind: 'member' as const,
|
|
1316
1396
|
name: `unused contract member: ${member.name}`,
|
|
1317
1397
|
lines: [member.line],
|
|
@@ -1322,7 +1402,7 @@ export const findSwiftInterfaceSegregationMatch = (
|
|
|
1322
1402
|
...typeLines,
|
|
1323
1403
|
protocolDeclaration.line,
|
|
1324
1404
|
...usedMemberLines,
|
|
1325
|
-
...unusedMembers.
|
|
1405
|
+
...unusedMembers.map((member) => member.line),
|
|
1326
1406
|
]);
|
|
1327
1407
|
|
|
1328
1408
|
return {
|
|
@@ -1355,9 +1435,6 @@ export const findSwiftLiskovSubstitutionMatch = (
|
|
|
1355
1435
|
}
|
|
1356
1436
|
|
|
1357
1437
|
const typeDeclarations = parseSwiftTypeDeclarations(source);
|
|
1358
|
-
if (typeDeclarations.length < 2) {
|
|
1359
|
-
return undefined;
|
|
1360
|
-
}
|
|
1361
1438
|
|
|
1362
1439
|
const sourceLines = source.split(/\r?\n/);
|
|
1363
1440
|
|
|
@@ -1370,9 +1447,6 @@ export const findSwiftLiskovSubstitutionMatch = (
|
|
|
1370
1447
|
const conformingTypes = typeDeclarations.filter((typeDeclaration) =>
|
|
1371
1448
|
typeDeclaration.conformances.includes(protocolDeclaration.name)
|
|
1372
1449
|
);
|
|
1373
|
-
if (conformingTypes.length < 2) {
|
|
1374
|
-
continue;
|
|
1375
|
-
}
|
|
1376
1450
|
|
|
1377
1451
|
for (const memberName of memberNames) {
|
|
1378
1452
|
let safeType: SwiftTypeDeclaration | undefined;
|
|
@@ -893,14 +893,15 @@ const findTypeDiscriminatorSwitchInfo = (
|
|
|
893
893
|
return false;
|
|
894
894
|
}
|
|
895
895
|
|
|
896
|
-
const
|
|
896
|
+
const typedCases = value.cases.filter((entry) => {
|
|
897
897
|
if (!isObject(entry) || entry.type !== 'SwitchCase' || !isObject(entry.test)) {
|
|
898
898
|
return false;
|
|
899
899
|
}
|
|
900
900
|
return typeof switchCaseLabelFromNode(entry.test) === 'string';
|
|
901
|
-
})
|
|
901
|
+
});
|
|
902
902
|
|
|
903
|
-
|
|
903
|
+
const [firstTypedCase, secondTypedCase] = typedCases;
|
|
904
|
+
return firstTypedCase !== undefined && secondTypedCase !== undefined;
|
|
904
905
|
});
|
|
905
906
|
|
|
906
907
|
if (!match) {
|
|
@@ -934,7 +935,8 @@ const findTypeDiscriminatorSwitchInfo = (
|
|
|
934
935
|
})
|
|
935
936
|
: [];
|
|
936
937
|
|
|
937
|
-
|
|
938
|
+
const [firstCaseNode, secondCaseNode] = caseNodes;
|
|
939
|
+
if (!firstCaseNode || !secondCaseNode) {
|
|
938
940
|
return undefined;
|
|
939
941
|
}
|
|
940
942
|
|
|
@@ -1657,7 +1659,7 @@ export const hasTypeDiscriminatorSwitch = (node: unknown): boolean => {
|
|
|
1657
1659
|
return false;
|
|
1658
1660
|
}
|
|
1659
1661
|
|
|
1660
|
-
const
|
|
1662
|
+
const typedCases = value.cases.filter((entry) => {
|
|
1661
1663
|
if (!isObject(entry) || entry.type !== 'SwitchCase' || !isObject(entry.test)) {
|
|
1662
1664
|
return false;
|
|
1663
1665
|
}
|
|
@@ -1667,9 +1669,10 @@ export const hasTypeDiscriminatorSwitch = (node: unknown): boolean => {
|
|
|
1667
1669
|
testNode.type === 'NumericLiteral' ||
|
|
1668
1670
|
testNode.type === 'BooleanLiteral'
|
|
1669
1671
|
);
|
|
1670
|
-
})
|
|
1672
|
+
});
|
|
1671
1673
|
|
|
1672
|
-
|
|
1674
|
+
const [firstTypedCase, secondTypedCase] = typedCases;
|
|
1675
|
+
return firstTypedCase !== undefined && secondTypedCase !== undefined;
|
|
1673
1676
|
});
|
|
1674
1677
|
};
|
|
1675
1678
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.124",
|
|
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": {
|