i18next-cli 1.48.1 → 1.49.1
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/dist/cjs/cli.js +1 -1
- package/dist/cjs/extractor/core/ast-visitors.js +165 -13
- package/dist/cjs/extractor/parsers/call-expression-handler.js +5 -3
- package/dist/cjs/extractor/parsers/expression-resolver.js +290 -4
- package/dist/esm/cli.js +1 -1
- package/dist/esm/extractor/core/ast-visitors.js +165 -13
- package/dist/esm/extractor/parsers/call-expression-handler.js +5 -3
- package/dist/esm/extractor/parsers/expression-resolver.js +290 -4
- package/package.json +1 -1
- package/types/extractor/core/ast-visitors.d.ts +6 -0
- package/types/extractor/core/ast-visitors.d.ts.map +1 -1
- package/types/extractor/parsers/call-expression-handler.d.ts +2 -1
- package/types/extractor/parsers/call-expression-handler.d.ts.map +1 -1
- package/types/extractor/parsers/expression-resolver.d.ts +44 -0
- package/types/extractor/parsers/expression-resolver.d.ts.map +1 -1
|
@@ -59,7 +59,7 @@ class ASTVisitors {
|
|
|
59
59
|
this.scopeManager = new ScopeManager(config);
|
|
60
60
|
// use shared resolver when provided so captured enums/objects are visible across files
|
|
61
61
|
this.expressionResolver = expressionResolver ?? new ExpressionResolver(this.hooks);
|
|
62
|
-
this.callExpressionHandler = new CallExpressionHandler(config, pluginContext, logger, this.expressionResolver, () => this.getCurrentFile(), () => this.getCurrentCode());
|
|
62
|
+
this.callExpressionHandler = new CallExpressionHandler(config, pluginContext, logger, this.expressionResolver, () => this.getCurrentFile(), () => this.getCurrentCode(), (name) => this.scopeManager.resolveSimpleStringIdentifier(name));
|
|
63
63
|
this.jsxHandler = new JSXHandler(config, pluginContext, this.expressionResolver, () => this.getCurrentFile(), () => this.getCurrentCode());
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
@@ -271,6 +271,17 @@ class ASTVisitors {
|
|
|
271
271
|
// capture enums into resolver symbol table
|
|
272
272
|
this.expressionResolver.captureEnumDeclaration(node);
|
|
273
273
|
break;
|
|
274
|
+
// pattern 2: capture type aliases so `declare const x: Alias` can be resolved
|
|
275
|
+
case 'TsTypeAliasDeclaration':
|
|
276
|
+
case 'TSTypeAliasDeclaration':
|
|
277
|
+
case 'TsTypeAliasDecl':
|
|
278
|
+
this.expressionResolver.captureTypeAliasDeclaration(node);
|
|
279
|
+
break;
|
|
280
|
+
// pattern 3: capture function return types so `t(fn())` can be resolved
|
|
281
|
+
case 'FunctionDeclaration':
|
|
282
|
+
case 'FnDecl':
|
|
283
|
+
this.expressionResolver.captureFunctionDeclaration(node);
|
|
284
|
+
break;
|
|
274
285
|
case 'CallExpression':
|
|
275
286
|
this.callExpressionHandler.handleCallExpression(node, this.scopeManager.getVarFromScope.bind(this.scopeManager));
|
|
276
287
|
break;
|
|
@@ -288,6 +299,18 @@ class ASTVisitors {
|
|
|
288
299
|
}
|
|
289
300
|
this.hooks.onAfterVisitNode?.(node);
|
|
290
301
|
// --- END VISIT LOGIC ---
|
|
302
|
+
// Detect array iteration calls (.map / .forEach / .flatMap etc.) on a known
|
|
303
|
+
// as-const array so the callback parameter is bound to the array values while
|
|
304
|
+
// the callback body is walked. We inject the binding BEFORE generic recursion
|
|
305
|
+
// and remove it AFTER, so the whole subtree sees the correct value.
|
|
306
|
+
let arrayCallbackCleanup;
|
|
307
|
+
if (node.type === 'CallExpression') {
|
|
308
|
+
const info = this.tryGetArrayIterationCallbackInfo(node);
|
|
309
|
+
if (info) {
|
|
310
|
+
this.expressionResolver.setTemporaryVariable(info.paramName, info.values);
|
|
311
|
+
arrayCallbackCleanup = () => this.expressionResolver.deleteTemporaryVariable(info.paramName);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
291
314
|
// --- RECURSION ---
|
|
292
315
|
// Recurse into the children of the current node
|
|
293
316
|
for (const key in node) {
|
|
@@ -295,32 +318,112 @@ class ASTVisitors {
|
|
|
295
318
|
continue;
|
|
296
319
|
const child = node[key];
|
|
297
320
|
if (Array.isArray(child)) {
|
|
298
|
-
// Pre-scan array children
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
321
|
+
// Pre-scan array children in THREE passes:
|
|
322
|
+
// Pass 1 — variables WITH init (arrays, objects, strings, fns) + enums
|
|
323
|
+
// Pass 2 — type aliases + functions (may depend on pass-1 arrays)
|
|
324
|
+
// Pass 3 — `declare const x: Type` (no init; depends on pass-2 type aliases)
|
|
325
|
+
// This ordering ensures e.g.:
|
|
326
|
+
// const OPTS = ['a','b'] as const → pass 1
|
|
327
|
+
// type T = (typeof OPTS)[number] → pass 2 (resolves OPTS)
|
|
328
|
+
// declare const v: T → pass 3 (resolves T)
|
|
329
|
+
// ── Pass 1: variables with init ──────────────────────────────────────
|
|
303
330
|
for (const item of child) {
|
|
304
331
|
if (!item || typeof item !== 'object')
|
|
305
332
|
continue;
|
|
306
|
-
// Direct declarator
|
|
307
|
-
if (item.type === 'VariableDeclarator') {
|
|
333
|
+
// Direct declarator (rare)
|
|
334
|
+
if (item.type === 'VariableDeclarator' && item.init) {
|
|
308
335
|
this.scopeManager.handleVariableDeclarator(item);
|
|
309
336
|
this.expressionResolver.captureVariableDeclarator(item);
|
|
310
337
|
continue;
|
|
311
338
|
}
|
|
312
|
-
// enum declarations
|
|
313
|
-
if (item
|
|
339
|
+
// enum declarations
|
|
340
|
+
if (item.id && Array.isArray(item.members)) {
|
|
314
341
|
this.expressionResolver.captureEnumDeclaration(item);
|
|
315
|
-
// continue to allow further traversal
|
|
316
342
|
}
|
|
317
|
-
//
|
|
343
|
+
// Bare VariableDeclaration — only declarators that have an init
|
|
344
|
+
if (item.type === 'VariableDeclaration' && Array.isArray(item.declarations)) {
|
|
345
|
+
for (const decl of item.declarations) {
|
|
346
|
+
if (decl?.type === 'VariableDeclarator' && decl.init) {
|
|
347
|
+
this.scopeManager.handleVariableDeclarator(decl);
|
|
348
|
+
this.expressionResolver.captureVariableDeclarator(decl);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// ExportDeclaration wrapping VariableDeclaration — only inited declarators
|
|
353
|
+
if ((item.type === 'ExportDeclaration' || item.type === 'ExportNamedDeclaration') && item.declaration) {
|
|
354
|
+
const inner = item.declaration;
|
|
355
|
+
if (inner.type === 'VariableDeclaration' && Array.isArray(inner.declarations)) {
|
|
356
|
+
for (const vd of inner.declarations) {
|
|
357
|
+
if (vd?.type === 'VariableDeclarator' && vd.init) {
|
|
358
|
+
this.scopeManager.handleVariableDeclarator(vd);
|
|
359
|
+
this.expressionResolver.captureVariableDeclarator(vd);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// ── Pass 2: type aliases + functions ─────────────────────────────────
|
|
366
|
+
for (const item of child) {
|
|
367
|
+
if (!item || typeof item !== 'object')
|
|
368
|
+
continue;
|
|
369
|
+
if (item.type === 'TsTypeAliasDeclaration' || item.type === 'TSTypeAliasDeclaration' || item.type === 'TsTypeAliasDecl') {
|
|
370
|
+
this.expressionResolver.captureTypeAliasDeclaration(item);
|
|
371
|
+
}
|
|
372
|
+
if (item.type === 'FunctionDeclaration' || item.type === 'FnDecl') {
|
|
373
|
+
this.expressionResolver.captureFunctionDeclaration(item);
|
|
374
|
+
}
|
|
375
|
+
if ((item.type === 'ExportDeclaration' || item.type === 'ExportNamedDeclaration') && item.declaration) {
|
|
376
|
+
const inner = item.declaration;
|
|
377
|
+
if (inner.type === 'TsTypeAliasDeclaration' || inner.type === 'TSTypeAliasDeclaration' || inner.type === 'TsTypeAliasDecl') {
|
|
378
|
+
this.expressionResolver.captureTypeAliasDeclaration(inner);
|
|
379
|
+
}
|
|
380
|
+
if (inner.type === 'FunctionDeclaration' || inner.type === 'FnDecl') {
|
|
381
|
+
this.expressionResolver.captureFunctionDeclaration(inner);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// ── Pass 3: `declare const x: Type` — no init, depends on type aliases ─
|
|
386
|
+
// Also re-processes ArrayPattern destructuring (e.g. useState<T>) whose
|
|
387
|
+
// type argument resolution failed in Pass 1 because typeAliasTable was empty.
|
|
388
|
+
for (const item of child) {
|
|
389
|
+
if (!item || typeof item !== 'object')
|
|
390
|
+
continue;
|
|
391
|
+
// Direct declarator with no init
|
|
392
|
+
if (item.type === 'VariableDeclarator' && !item.init) {
|
|
393
|
+
this.scopeManager.handleVariableDeclarator(item);
|
|
394
|
+
this.expressionResolver.captureVariableDeclarator(item);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
// ArrayPattern destructuring with init — re-run now that type aliases are populated
|
|
398
|
+
if (item.type === 'VariableDeclarator' && item.init && item.id?.type === 'ArrayPattern') {
|
|
399
|
+
this.expressionResolver.captureVariableDeclarator(item);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
// VariableDeclaration — process no-init declarators and re-process ArrayPattern ones
|
|
318
403
|
if (item.type === 'VariableDeclaration' && Array.isArray(item.declarations)) {
|
|
319
404
|
for (const decl of item.declarations) {
|
|
320
|
-
if (decl
|
|
405
|
+
if (!decl.init) {
|
|
321
406
|
this.scopeManager.handleVariableDeclarator(decl);
|
|
322
407
|
this.expressionResolver.captureVariableDeclarator(decl);
|
|
323
408
|
}
|
|
409
|
+
else if (decl.id?.type === 'ArrayPattern') {
|
|
410
|
+
this.expressionResolver.captureVariableDeclarator(decl);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// ExportDeclaration wrapping — same logic
|
|
415
|
+
if ((item.type === 'ExportDeclaration' || item.type === 'ExportNamedDeclaration') && item.declaration) {
|
|
416
|
+
const inner = item.declaration;
|
|
417
|
+
if (inner.type === 'VariableDeclaration' && Array.isArray(inner.declarations)) {
|
|
418
|
+
for (const vd of inner.declarations) {
|
|
419
|
+
if (!vd.init) {
|
|
420
|
+
this.scopeManager.handleVariableDeclarator(vd);
|
|
421
|
+
this.expressionResolver.captureVariableDeclarator(vd);
|
|
422
|
+
}
|
|
423
|
+
else if (vd.id?.type === 'ArrayPattern') {
|
|
424
|
+
this.expressionResolver.captureVariableDeclarator(vd);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
324
427
|
}
|
|
325
428
|
}
|
|
326
429
|
}
|
|
@@ -340,11 +443,60 @@ class ASTVisitors {
|
|
|
340
443
|
}
|
|
341
444
|
}
|
|
342
445
|
// --- END RECURSION ---
|
|
446
|
+
// Remove temporary callback param binding if one was injected for this node
|
|
447
|
+
arrayCallbackCleanup?.();
|
|
343
448
|
// LEAVE SCOPE for functions
|
|
344
449
|
if (isNewScope) {
|
|
345
450
|
this.scopeManager.exitScope();
|
|
346
451
|
}
|
|
347
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* If `node` is a call like `ARRAY.map(param => ...)` where ARRAY is a known
|
|
455
|
+
* string-array constant, returns the callback's first parameter name and the
|
|
456
|
+
* array values so the caller can inject a temporary variable binding.
|
|
457
|
+
*/
|
|
458
|
+
tryGetArrayIterationCallbackInfo(node) {
|
|
459
|
+
try {
|
|
460
|
+
const callee = node.callee;
|
|
461
|
+
if (callee?.type !== 'MemberExpression')
|
|
462
|
+
return undefined;
|
|
463
|
+
const prop = callee.property;
|
|
464
|
+
if (prop?.type !== 'Identifier')
|
|
465
|
+
return undefined;
|
|
466
|
+
if (!['map', 'forEach', 'flatMap', 'filter', 'find', 'some', 'every'].includes(prop.value))
|
|
467
|
+
return undefined;
|
|
468
|
+
// The object must be an identifier whose value is a known string array
|
|
469
|
+
const obj = callee.object;
|
|
470
|
+
if (obj?.type !== 'Identifier')
|
|
471
|
+
return undefined;
|
|
472
|
+
const values = this.expressionResolver.getVariableValues(obj.value);
|
|
473
|
+
if (!values || values.length === 0)
|
|
474
|
+
return undefined;
|
|
475
|
+
// First argument must be a callback with at least one parameter
|
|
476
|
+
const callbackArg = node.arguments?.[0]?.expression;
|
|
477
|
+
if (!callbackArg)
|
|
478
|
+
return undefined;
|
|
479
|
+
// Normalise param across SWC shapes: ArrowFunctionExpression / FunctionExpression
|
|
480
|
+
const params = callbackArg.params ?? callbackArg.parameters ?? [];
|
|
481
|
+
const firstParam = params[0];
|
|
482
|
+
if (!firstParam)
|
|
483
|
+
return undefined;
|
|
484
|
+
// SWC wraps params in `Param { pat: Identifier }` or exposes them directly
|
|
485
|
+
const ident = firstParam.type === 'Identifier'
|
|
486
|
+
? firstParam
|
|
487
|
+
: firstParam.type === 'Param' && firstParam.pat?.type === 'Identifier'
|
|
488
|
+
? firstParam.pat
|
|
489
|
+
: firstParam.type === 'AssignmentPattern' && firstParam.left?.type === 'Identifier'
|
|
490
|
+
? firstParam.left
|
|
491
|
+
: null;
|
|
492
|
+
if (!ident)
|
|
493
|
+
return undefined;
|
|
494
|
+
return { paramName: ident.value, values };
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return undefined;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
348
500
|
/**
|
|
349
501
|
* Retrieves variable information from the scope chain.
|
|
350
502
|
* Searches from innermost to outermost scope.
|
|
@@ -10,13 +10,15 @@ class CallExpressionHandler {
|
|
|
10
10
|
objectKeys = new Set();
|
|
11
11
|
getCurrentFile;
|
|
12
12
|
getCurrentCode;
|
|
13
|
-
|
|
13
|
+
resolveIdentifier;
|
|
14
|
+
constructor(config, pluginContext, logger, expressionResolver, getCurrentFile, getCurrentCode, resolveIdentifier = () => undefined) {
|
|
14
15
|
this.config = config;
|
|
15
16
|
this.pluginContext = pluginContext;
|
|
16
17
|
this.logger = logger;
|
|
17
18
|
this.expressionResolver = expressionResolver;
|
|
18
19
|
this.getCurrentFile = getCurrentFile;
|
|
19
20
|
this.getCurrentCode = getCurrentCode;
|
|
21
|
+
this.resolveIdentifier = resolveIdentifier;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Computes line and column from a node's normalised span.
|
|
@@ -151,8 +153,8 @@ class CallExpressionHandler {
|
|
|
151
153
|
// Determine namespace (explicit ns > ns:key > scope ns > default)
|
|
152
154
|
// See https://www.i18next.com/overview/api#getfixedt
|
|
153
155
|
if (options) {
|
|
154
|
-
const nsVal = getObjectPropValue(options, 'ns');
|
|
155
|
-
if (typeof nsVal === 'string')
|
|
156
|
+
const nsVal = getObjectPropValue(options, 'ns', this.resolveIdentifier);
|
|
157
|
+
if (typeof nsVal === 'string' && nsVal !== '')
|
|
156
158
|
ns = nsVal;
|
|
157
159
|
}
|
|
158
160
|
const nsSeparator = this.config.extract.nsSeparator ?? ':';
|
|
@@ -7,6 +7,18 @@ class ExpressionResolver {
|
|
|
7
7
|
variableTable = new Map();
|
|
8
8
|
// Shared (cross-file) table for enums / exported object maps that should persist
|
|
9
9
|
sharedEnumTable = new Map();
|
|
10
|
+
// Per-file table for type aliases: Maps typeName -> string[]
|
|
11
|
+
// e.g. `type ChangeType = 'all' | 'next' | 'this'` -> { ChangeType: ['all', 'next', 'this'] }
|
|
12
|
+
typeAliasTable = new Map();
|
|
13
|
+
// Shared (cross-file) table for string-array constants (e.g. `as const` arrays).
|
|
14
|
+
// Persists across resetFileSymbols() so exported arrays are visible to importers.
|
|
15
|
+
sharedVariableTable = new Map();
|
|
16
|
+
// Shared (cross-file) table for type aliases — populated alongside typeAliasTable.
|
|
17
|
+
// Persists across resetFileSymbols() so exported type aliases are visible to importers.
|
|
18
|
+
sharedTypeAliasTable = new Map();
|
|
19
|
+
// Temporary per-scope variable overrides, used to inject .map() / .forEach()
|
|
20
|
+
// callback parameters while the callback body is being walked.
|
|
21
|
+
temporaryVariables = new Map();
|
|
10
22
|
constructor(hooks) {
|
|
11
23
|
this.hooks = hooks;
|
|
12
24
|
}
|
|
@@ -15,6 +27,8 @@ class ExpressionResolver {
|
|
|
15
27
|
*/
|
|
16
28
|
resetFileSymbols() {
|
|
17
29
|
this.variableTable.clear();
|
|
30
|
+
this.typeAliasTable.clear();
|
|
31
|
+
this.temporaryVariables.clear();
|
|
18
32
|
}
|
|
19
33
|
/**
|
|
20
34
|
* Capture a VariableDeclarator node to record simple statically analyzable
|
|
@@ -28,17 +42,74 @@ class ExpressionResolver {
|
|
|
28
42
|
*/
|
|
29
43
|
captureVariableDeclarator(node) {
|
|
30
44
|
try {
|
|
31
|
-
if (!node || !node.id
|
|
45
|
+
if (!node || !node.id)
|
|
32
46
|
return;
|
|
47
|
+
// ── ArrayPattern id: `const [x, y] = fn<T>(...)` ────────────────────────
|
|
48
|
+
// Handles `const [state] = useState<'a'|'b'>('a')` or similar generic calls
|
|
49
|
+
// where the type argument is a finite string-literal union.
|
|
50
|
+
if (node.id.type === 'ArrayPattern' && node.init) {
|
|
51
|
+
const init = node.init;
|
|
52
|
+
// Unwrap await / as-expressions
|
|
53
|
+
let callExpr = init;
|
|
54
|
+
while (callExpr?.type === 'AwaitExpression')
|
|
55
|
+
callExpr = callExpr.argument;
|
|
56
|
+
while (callExpr?.type === 'TsConstAssertion' ||
|
|
57
|
+
callExpr?.type === 'TsAsExpression' ||
|
|
58
|
+
callExpr?.type === 'TsSatisfiesExpression')
|
|
59
|
+
callExpr = callExpr.expression;
|
|
60
|
+
if (callExpr?.type === 'CallExpression') {
|
|
61
|
+
const typeArgs = callExpr.typeArguments?.params ??
|
|
62
|
+
callExpr.typeParameters?.params ??
|
|
63
|
+
[];
|
|
64
|
+
if (typeArgs.length > 0) {
|
|
65
|
+
const vals = this.resolvePossibleStringValuesFromType(typeArgs[0]);
|
|
66
|
+
if (vals.length > 0) {
|
|
67
|
+
// Bind each array-pattern element: first element is the state variable
|
|
68
|
+
for (const el of node.id.elements) {
|
|
69
|
+
if (!el)
|
|
70
|
+
continue;
|
|
71
|
+
const ident = el.type === 'Identifier' ? el : (el.type === 'AssignmentPattern' && el.left?.type === 'Identifier' ? el.left : null);
|
|
72
|
+
if (ident) {
|
|
73
|
+
this.variableTable.set(ident.value, vals);
|
|
74
|
+
}
|
|
75
|
+
break; // only bind the first element (the state value, not the setter)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
33
82
|
// only handle simple identifier bindings like `const x = ...`
|
|
34
83
|
if (node.id.type !== 'Identifier')
|
|
35
84
|
return;
|
|
36
85
|
const name = node.id.value;
|
|
86
|
+
// pattern 1:
|
|
87
|
+
// Handle `declare const x: 'a' | 'b'` and `declare const x: SomeUnion`
|
|
88
|
+
// where there is no initializer but a TypeScript type annotation.
|
|
89
|
+
if (!node.init) {
|
|
90
|
+
const typeAnnotation = this.extractTypeAnnotation(node.id);
|
|
91
|
+
if (typeAnnotation) {
|
|
92
|
+
const vals = this.resolvePossibleStringValuesFromType(typeAnnotation);
|
|
93
|
+
if (vals.length > 0) {
|
|
94
|
+
this.variableTable.set(name, vals);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
37
99
|
const init = node.init;
|
|
100
|
+
// Unwrap TS type assertion wrappers before inspecting the shape of the initializer.
|
|
101
|
+
// `{ ... } as const` → TsConstAssertion; `x as Type` → TsAsExpression; etc.
|
|
102
|
+
// We need the raw expression to detect ObjectExpression and ArrowFunctionExpression.
|
|
103
|
+
let unwrappedInit = init;
|
|
104
|
+
while (unwrappedInit?.type === 'TsConstAssertion' ||
|
|
105
|
+
unwrappedInit?.type === 'TsAsExpression' ||
|
|
106
|
+
unwrappedInit?.type === 'TsSatisfiesExpression') {
|
|
107
|
+
unwrappedInit = unwrappedInit.expression;
|
|
108
|
+
}
|
|
38
109
|
// ObjectExpression -> map of string props
|
|
39
|
-
if (
|
|
110
|
+
if (unwrappedInit.type === 'ObjectExpression' && Array.isArray(unwrappedInit.properties)) {
|
|
40
111
|
const map = {};
|
|
41
|
-
for (const p of
|
|
112
|
+
for (const p of unwrappedInit.properties) {
|
|
42
113
|
if (!p || p.type !== 'KeyValueProperty')
|
|
43
114
|
continue;
|
|
44
115
|
const keyNode = p.key;
|
|
@@ -58,16 +129,147 @@ class ExpressionResolver {
|
|
|
58
129
|
return;
|
|
59
130
|
}
|
|
60
131
|
}
|
|
132
|
+
// ArrayExpression -> list of string values
|
|
133
|
+
// Handles `const OPTS = ['a', 'b', 'c'] as const`
|
|
134
|
+
if (unwrappedInit.type === 'ArrayExpression' && Array.isArray(unwrappedInit.elements)) {
|
|
135
|
+
const vals = [];
|
|
136
|
+
for (const elem of unwrappedInit.elements) {
|
|
137
|
+
if (!elem || !elem.expression)
|
|
138
|
+
continue;
|
|
139
|
+
const resolved = this.resolvePossibleStringValuesFromExpression(elem.expression);
|
|
140
|
+
if (resolved.length === 1)
|
|
141
|
+
vals.push(resolved[0]);
|
|
142
|
+
}
|
|
143
|
+
if (vals.length > 0) {
|
|
144
|
+
this.variableTable.set(name, vals);
|
|
145
|
+
// Also share so importing files can see this array
|
|
146
|
+
this.sharedVariableTable.set(name, vals);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
61
150
|
// For other initializers, try to resolve to one-or-more strings
|
|
62
151
|
const vals = this.resolvePossibleStringValuesFromExpression(init);
|
|
63
152
|
if (vals.length > 0) {
|
|
64
153
|
this.variableTable.set(name, vals);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// pattern 3 (arrow function variant):
|
|
157
|
+
// `const fn = (): 'a' | 'b' => ...` — capture the explicit return type annotation.
|
|
158
|
+
if (unwrappedInit.type === 'ArrowFunctionExpression' || unwrappedInit.type === 'FunctionExpression') {
|
|
159
|
+
const rawReturnType = unwrappedInit.returnType ?? unwrappedInit.typeAnnotation;
|
|
160
|
+
if (rawReturnType) {
|
|
161
|
+
const tsType = rawReturnType.typeAnnotation ?? rawReturnType;
|
|
162
|
+
const returnVals = this.resolvePossibleStringValuesFromType(tsType);
|
|
163
|
+
if (returnVals.length > 0) {
|
|
164
|
+
this.variableTable.set(name, returnVals);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
65
167
|
}
|
|
66
168
|
}
|
|
67
169
|
catch {
|
|
68
170
|
// be silent - conservative only
|
|
69
171
|
}
|
|
70
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Capture a TypeScript type alias so that `declare const x: AliasName` can
|
|
175
|
+
* be resolved to its string union members later.
|
|
176
|
+
*
|
|
177
|
+
* Handles: `type Foo = 'a' | 'b' | 'c'`
|
|
178
|
+
*
|
|
179
|
+
* SWC node shapes: `TsTypeAliasDeclaration` / `TsTypeAliasDecl`
|
|
180
|
+
*/
|
|
181
|
+
captureTypeAliasDeclaration(node) {
|
|
182
|
+
try {
|
|
183
|
+
const name = node?.id?.type === 'Identifier' ? node.id.value : undefined;
|
|
184
|
+
if (!name)
|
|
185
|
+
return;
|
|
186
|
+
// SWC puts the actual type in `.typeAnnotation`
|
|
187
|
+
const tsType = node.typeAnnotation ?? node.typeAnn;
|
|
188
|
+
if (!tsType)
|
|
189
|
+
return;
|
|
190
|
+
const vals = this.resolvePossibleStringValuesFromType(tsType);
|
|
191
|
+
if (vals.length > 0) {
|
|
192
|
+
this.typeAliasTable.set(name, vals);
|
|
193
|
+
// Also share so importing files can resolve this alias by name
|
|
194
|
+
this.sharedTypeAliasTable.set(name, vals);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// noop
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Capture the return-type annotation of a function declaration so that
|
|
203
|
+
* `t(fn())` calls can be expanded to all union members.
|
|
204
|
+
*
|
|
205
|
+
* Handles both `function f(): 'a' | 'b' { ... }` and
|
|
206
|
+
* `const f = (): 'a' | 'b' => ...` (the arrow-function form is captured
|
|
207
|
+
* via captureVariableDeclarator when the init is an ArrowFunctionExpression).
|
|
208
|
+
*
|
|
209
|
+
* SWC node shapes: `FunctionDeclaration` / `FnDecl`
|
|
210
|
+
*/
|
|
211
|
+
captureFunctionDeclaration(node) {
|
|
212
|
+
try {
|
|
213
|
+
const name = node?.identifier?.value ?? node?.id?.value;
|
|
214
|
+
if (!name)
|
|
215
|
+
return;
|
|
216
|
+
// SWC places the return type annotation in `.function.returnType` (FunctionDeclaration)
|
|
217
|
+
// or directly in `.returnType` (FunctionExpression / ArrowFunctionExpression).
|
|
218
|
+
const fn = node.function ?? node;
|
|
219
|
+
const rawReturnType = fn.returnType ?? fn.typeAnnotation;
|
|
220
|
+
if (!rawReturnType)
|
|
221
|
+
return;
|
|
222
|
+
// Unwrap TsTypeAnnotation wrapper if present
|
|
223
|
+
const tsType = rawReturnType.typeAnnotation ?? rawReturnType;
|
|
224
|
+
const vals = this.resolvePossibleStringValuesFromType(tsType);
|
|
225
|
+
if (vals.length > 0) {
|
|
226
|
+
this.variableTable.set(name, vals);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// noop
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Extract a raw TsType node from an identifier's type annotation.
|
|
235
|
+
* SWC may wrap it in a `TsTypeAnnotation` node — this unwraps it.
|
|
236
|
+
*/
|
|
237
|
+
extractTypeAnnotation(idNode) {
|
|
238
|
+
const raw = idNode?.typeAnnotation;
|
|
239
|
+
if (!raw)
|
|
240
|
+
return undefined;
|
|
241
|
+
// TsTypeAnnotation wrapper -> .typeAnnotation holds the actual TsType
|
|
242
|
+
if (raw.type === 'TsTypeAnnotation')
|
|
243
|
+
return raw.typeAnnotation;
|
|
244
|
+
return raw;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Temporarily bind a variable name to a set of string values.
|
|
248
|
+
* Used by ast-visitors to inject .map()/.forEach() callback parameters.
|
|
249
|
+
* Call deleteTemporaryVariable() after walking the callback body.
|
|
250
|
+
*/
|
|
251
|
+
setTemporaryVariable(name, values) {
|
|
252
|
+
this.temporaryVariables.set(name, values);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Remove a previously-injected temporary variable binding.
|
|
256
|
+
*/
|
|
257
|
+
deleteTemporaryVariable(name) {
|
|
258
|
+
this.temporaryVariables.delete(name);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Return the array values stored for a variable name, checking all tables.
|
|
262
|
+
* Returns undefined if the name is not a known string array.
|
|
263
|
+
*/
|
|
264
|
+
getVariableValues(name) {
|
|
265
|
+
const tmp = this.temporaryVariables.get(name);
|
|
266
|
+
if (tmp)
|
|
267
|
+
return tmp;
|
|
268
|
+
const v = this.variableTable.get(name);
|
|
269
|
+
if (Array.isArray(v))
|
|
270
|
+
return v;
|
|
271
|
+
return this.sharedVariableTable.get(name);
|
|
272
|
+
}
|
|
71
273
|
/**
|
|
72
274
|
* Capture a TypeScript enum declaration so members can be resolved later.
|
|
73
275
|
* Accepts SWC node shapes like `TsEnumDeclaration` / `TSEnumDeclaration`.
|
|
@@ -218,11 +420,36 @@ class ExpressionResolver {
|
|
|
218
420
|
if (propName && base[propName] !== undefined) {
|
|
219
421
|
return [base[propName]];
|
|
220
422
|
}
|
|
423
|
+
// pattern 4:
|
|
424
|
+
// `map[identifierVar]` where identifierVar resolves to a known set of keys.
|
|
425
|
+
// Try to enumerate which map values are reachable.
|
|
426
|
+
if (prop.type === 'Computed' && prop.expression) {
|
|
427
|
+
const keyVals = this.resolvePossibleStringValuesFromExpression(prop.expression, returnEmptyStrings);
|
|
428
|
+
if (keyVals.length > 0) {
|
|
429
|
+
// Return only the map values for the known keys (subset access)
|
|
430
|
+
return keyVals.map(k => base[k]).filter((v) => v !== undefined);
|
|
431
|
+
}
|
|
432
|
+
// Cannot narrow the key at all — return all map values as a conservative fallback
|
|
433
|
+
return Object.values(base);
|
|
434
|
+
}
|
|
221
435
|
}
|
|
222
436
|
}
|
|
223
437
|
}
|
|
224
438
|
catch { }
|
|
225
439
|
}
|
|
440
|
+
// pattern 3:
|
|
441
|
+
// `t(fn())` — resolve to the function's known return-type union when captured.
|
|
442
|
+
if (expression.type === 'CallExpression') {
|
|
443
|
+
try {
|
|
444
|
+
const callee = expression.callee;
|
|
445
|
+
if (callee?.type === 'Identifier') {
|
|
446
|
+
const v = this.variableTable.get(callee.value);
|
|
447
|
+
if (Array.isArray(v) && v.length > 0)
|
|
448
|
+
return v;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch { }
|
|
452
|
+
}
|
|
226
453
|
// Binary concatenation support (e.g., a + '_' + b)
|
|
227
454
|
// SWC binary expr can be represented as `BinExpr` with left/right; be permissive:
|
|
228
455
|
if (expression.left && expression.right) {
|
|
@@ -277,11 +504,25 @@ class ExpressionResolver {
|
|
|
277
504
|
const annotation = expression.typeAnnotation;
|
|
278
505
|
return this.resolvePossibleStringValuesFromType(annotation, returnEmptyStrings);
|
|
279
506
|
}
|
|
507
|
+
// `expr as const` — delegate to the underlying expression (the type annotation is
|
|
508
|
+
// just `const`, which carries no union information, so we want the value side).
|
|
509
|
+
if (expression.type === 'TsConstAssertion') {
|
|
510
|
+
return this.resolvePossibleStringValuesFromExpression(expression.expression, returnEmptyStrings);
|
|
511
|
+
}
|
|
280
512
|
// Identifier resolution via captured per-file variable table only
|
|
281
513
|
if (expression.type === 'Identifier') {
|
|
514
|
+
// Check temporary (callback param) overrides first
|
|
515
|
+
const tmp = this.temporaryVariables.get(expression.value);
|
|
516
|
+
if (tmp)
|
|
517
|
+
return tmp;
|
|
282
518
|
const v = this.variableTable.get(expression.value);
|
|
283
|
-
if (!v)
|
|
519
|
+
if (!v) {
|
|
520
|
+
// Fall back to shared cross-file array table
|
|
521
|
+
const sv = this.sharedVariableTable.get(expression.value);
|
|
522
|
+
if (sv)
|
|
523
|
+
return sv;
|
|
284
524
|
return [];
|
|
525
|
+
}
|
|
285
526
|
if (Array.isArray(v))
|
|
286
527
|
return v;
|
|
287
528
|
// object map - cannot be used directly as key, so return empty
|
|
@@ -291,6 +532,11 @@ class ExpressionResolver {
|
|
|
291
532
|
return [];
|
|
292
533
|
}
|
|
293
534
|
resolvePossibleStringValuesFromType(type, returnEmptyStrings = false) {
|
|
535
|
+
// Unwrap TsParenthesizedType — SWC explicitly emits these for grouped types like
|
|
536
|
+
// `(typeof X)[number]` where `(typeof X)` becomes TsParenthesizedType { typeAnnotation: TsTypeQuery }
|
|
537
|
+
if (type.type === 'TsParenthesizedType') {
|
|
538
|
+
return this.resolvePossibleStringValuesFromType(type.typeAnnotation, returnEmptyStrings);
|
|
539
|
+
}
|
|
294
540
|
if (type.type === 'TsUnionType') {
|
|
295
541
|
return type.types.flatMap((t) => this.resolvePossibleStringValuesFromType(t, returnEmptyStrings));
|
|
296
542
|
}
|
|
@@ -306,6 +552,46 @@ class ExpressionResolver {
|
|
|
306
552
|
return [`${type.literal.value}`]; // Handle literals like 5 or true
|
|
307
553
|
}
|
|
308
554
|
}
|
|
555
|
+
// pattern 2:
|
|
556
|
+
// Resolve a named type alias reference: `declare const x: ChangeType`
|
|
557
|
+
// where `type ChangeType = 'all' | 'next' | 'this'` was captured earlier.
|
|
558
|
+
if (type.type === 'TsTypeReference') {
|
|
559
|
+
const typeName = type.typeName?.type === 'Identifier'
|
|
560
|
+
? type.typeName.value
|
|
561
|
+
: undefined;
|
|
562
|
+
if (typeName) {
|
|
563
|
+
const aliasVals = this.typeAliasTable.get(typeName) ?? this.sharedTypeAliasTable.get(typeName);
|
|
564
|
+
if (aliasVals && aliasVals.length > 0)
|
|
565
|
+
return aliasVals;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// `(typeof ACCESS_OPTIONS)[number]` — resolve through the shared array variable table.
|
|
569
|
+
// SWC emits: TsIndexedAccessType {
|
|
570
|
+
// objectType: TsParenthesizedType { typeAnnotation: TsTypeQuery { exprName: Identifier } }
|
|
571
|
+
// indexType: TsKeywordType
|
|
572
|
+
// }
|
|
573
|
+
// The parens around `typeof X` produce a TsParenthesizedType wrapper that we must unwrap.
|
|
574
|
+
if (type.type === 'TsIndexedAccessType') {
|
|
575
|
+
try {
|
|
576
|
+
let objType = type.objectType;
|
|
577
|
+
// Unwrap TsParenthesizedType wrapper (SWC preserves explicit parens in type positions)
|
|
578
|
+
while (objType?.type === 'TsParenthesizedType') {
|
|
579
|
+
objType = objType.typeAnnotation;
|
|
580
|
+
}
|
|
581
|
+
if (objType?.type === 'TsTypeQuery' || objType?.type === 'TSTypeQuery') {
|
|
582
|
+
// SWC: TsTypeQuery.exprName is TsEntityName (Identifier | TsQualifiedName)
|
|
583
|
+
const exprName = objType.exprName ?? objType.expr ?? objType.entityName;
|
|
584
|
+
// access .value (Identifier) or fall back to .name for alternate SWC builds
|
|
585
|
+
const varName = exprName?.value ?? exprName?.name;
|
|
586
|
+
if (varName) {
|
|
587
|
+
const vals = this.getVariableValues(varName);
|
|
588
|
+
if (vals && vals.length > 0)
|
|
589
|
+
return vals;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
catch { }
|
|
594
|
+
}
|
|
309
595
|
// We can't statically determine the value of other expressions (e.g., variables, function calls)
|
|
310
596
|
return [];
|
|
311
597
|
}
|
package/package.json
CHANGED
|
@@ -65,6 +65,12 @@ export declare class ASTVisitors {
|
|
|
65
65
|
* @private
|
|
66
66
|
*/
|
|
67
67
|
private walk;
|
|
68
|
+
/**
|
|
69
|
+
* If `node` is a call like `ARRAY.map(param => ...)` where ARRAY is a known
|
|
70
|
+
* string-array constant, returns the callback's first parameter name and the
|
|
71
|
+
* array values so the caller can inject a temporary variable binding.
|
|
72
|
+
*/
|
|
73
|
+
private tryGetArrayIterationCallbackInfo;
|
|
68
74
|
/**
|
|
69
75
|
* Retrieves variable information from the scope chain.
|
|
70
76
|
* Searches from innermost to outermost scope.
|