gtx-cli 2.5.0-alpha.3 → 2.5.0

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/config/generateSettings.js +8 -1
  3. package/dist/console/colors.d.ts +1 -0
  4. package/dist/console/colors.js +3 -0
  5. package/dist/console/index.d.ts +8 -0
  6. package/dist/console/index.js +15 -2
  7. package/dist/react/jsx/evaluateJsx.d.ts +9 -6
  8. package/dist/react/jsx/evaluateJsx.js +33 -5
  9. package/dist/react/jsx/utils/buildImportMap.d.ts +9 -0
  10. package/dist/react/jsx/utils/buildImportMap.js +30 -0
  11. package/dist/react/jsx/utils/constants.d.ts +2 -0
  12. package/dist/react/jsx/utils/constants.js +11 -2
  13. package/dist/react/jsx/utils/getPathsAndAliases.d.ts +17 -0
  14. package/dist/react/jsx/utils/getPathsAndAliases.js +89 -0
  15. package/dist/react/{data-_gt → jsx/utils/jsxParsing}/addGTIdentifierToSyntaxTree.d.ts +2 -1
  16. package/dist/react/{data-_gt → jsx/utils/jsxParsing}/addGTIdentifierToSyntaxTree.js +30 -6
  17. package/dist/react/jsx/utils/jsxParsing/handleChildrenWhitespace.d.ts +6 -0
  18. package/dist/react/jsx/utils/jsxParsing/handleChildrenWhitespace.js +199 -0
  19. package/dist/react/jsx/utils/jsxParsing/multiplication/findMultiplicationNode.d.ts +13 -0
  20. package/dist/react/jsx/utils/jsxParsing/multiplication/findMultiplicationNode.js +42 -0
  21. package/dist/react/jsx/utils/jsxParsing/multiplication/multiplyJsxTree.d.ts +5 -0
  22. package/dist/react/jsx/utils/jsxParsing/multiplication/multiplyJsxTree.js +69 -0
  23. package/dist/react/jsx/utils/jsxParsing/parseJsx.d.ts +61 -0
  24. package/dist/react/jsx/utils/jsxParsing/parseJsx.js +1005 -0
  25. package/dist/react/jsx/utils/jsxParsing/parseTProps.d.ts +8 -0
  26. package/dist/react/jsx/utils/jsxParsing/parseTProps.js +47 -0
  27. package/dist/react/jsx/utils/jsxParsing/types.d.ts +48 -0
  28. package/dist/react/jsx/utils/jsxParsing/types.js +34 -0
  29. package/dist/react/jsx/utils/parseStringFunction.js +4 -141
  30. package/dist/react/jsx/utils/resolveImportPath.d.ts +11 -0
  31. package/dist/react/jsx/utils/resolveImportPath.js +111 -0
  32. package/dist/react/parse/createInlineUpdates.js +19 -70
  33. package/package.json +2 -2
  34. package/dist/react/jsx/trimJsxStringChildren.d.ts +0 -7
  35. package/dist/react/jsx/trimJsxStringChildren.js +0 -122
  36. package/dist/react/jsx/utils/parseJsx.d.ts +0 -21
  37. package/dist/react/jsx/utils/parseJsx.js +0 -259
@@ -0,0 +1,1005 @@
1
+ import generateModule from '@babel/generator';
2
+ // Handle CommonJS/ESM interop
3
+ const generate = generateModule.default || generateModule;
4
+ import * as t from '@babel/types';
5
+ import fs from 'node:fs';
6
+ import { parse } from '@babel/parser';
7
+ import addGTIdentifierToSyntaxTree from './addGTIdentifierToSyntaxTree.js';
8
+ import { warnHasUnwrappedExpressionSync, warnNestedTComponent, warnInvalidStaticChildSync, warnInvalidReturnSync as warnInvalidReturnExpressionSync, warnFunctionNotFoundSync, warnMissingReturnSync, warnDuplicateFunctionDefinitionSync, warnInvalidStaticInitSync, warnRecursiveFunctionCallSync, } from '../../../../console/index.js';
9
+ import { isAcceptedPluralForm } from 'generaltranslation/internal';
10
+ import { isStaticExpression } from '../../evaluateJsx.js';
11
+ import { STATIC_COMPONENT, TRANSLATION_COMPONENT, VARIABLE_COMPONENTS, } from '../constants.js';
12
+ import { HTML_CONTENT_PROPS } from 'generaltranslation/types';
13
+ import { resolveImportPath } from '../resolveImportPath.js';
14
+ import traverseModule from '@babel/traverse';
15
+ import { buildImportMap } from '../buildImportMap.js';
16
+ import { getPathsAndAliases } from '../getPathsAndAliases.js';
17
+ import { parseTProps } from './parseTProps.js';
18
+ import { handleChildrenWhitespace } from './handleChildrenWhitespace.js';
19
+ import { isElementNode, } from './types.js';
20
+ import { multiplyJsxTree } from './multiplication/multiplyJsxTree.js';
21
+ // Handle CommonJS/ESM interop
22
+ const traverse = traverseModule.default || traverseModule;
23
+ // TODO: currently we cover VariableDeclaration and FunctionDeclaration nodes, but are there others we should cover as well?
24
+ /**
25
+ * Cache for resolved import paths to avoid redundant I/O operations.
26
+ * Key: `${currentFile}::${importPath}`
27
+ * Value: resolved absolute path or null
28
+ */
29
+ const resolveImportPathCache = new Map();
30
+ /**
31
+ * Cache for processed functions to avoid re-parsing the same files.
32
+ * Key: `${filePath}::${functionName}::${argIndex}`
33
+ * Value: boolean indicating whether the function was found and processed
34
+ */
35
+ const processFunctionCache = new Map();
36
+ /**
37
+ * Entry point for JSX parsing
38
+ */
39
+ export function parseTranslationComponent({ originalName, importAliases, localName, path, updates, errors, warnings, file, parsingOptions, pkg, }) {
40
+ // First, collect all imports in this file to track cross-file function calls
41
+ const importedFunctionsMap = buildImportMap(path.scope.getProgramParent().path);
42
+ const referencePaths = path.scope.bindings[localName]?.referencePaths || [];
43
+ for (const refPath of referencePaths) {
44
+ // Only start at opening tag
45
+ if (!t.isJSXOpeningElement(refPath.parent) ||
46
+ !refPath.parentPath?.parentPath) {
47
+ continue;
48
+ }
49
+ // Get the JSX element NodePath
50
+ const jsxElementPath = refPath.parentPath
51
+ ?.parentPath;
52
+ // Parse <T> component
53
+ parseJSXElement({
54
+ scopeNode: jsxElementPath,
55
+ node: jsxElementPath.node,
56
+ pkg,
57
+ originalName,
58
+ importAliases,
59
+ updates,
60
+ errors,
61
+ warnings,
62
+ file,
63
+ parsingOptions,
64
+ importedFunctionsMap,
65
+ });
66
+ }
67
+ }
68
+ /**
69
+ * Builds a JSX tree from a given node, recursively handling children.
70
+ * @param node - The node to build the tree from
71
+ * @param unwrappedExpressions - An array to store unwrapped expressions
72
+ * @param updates - The updates array
73
+ * @param errors - The errors array
74
+ * @param file - The file name
75
+ * @param insideT - Whether the current node is inside a <T> component
76
+ * @returns The built JSX tree
77
+ */
78
+ export function buildJSXTree({ importAliases, node, unwrappedExpressions, visited, callStack, updates, errors, warnings, file, insideT, parsingOptions, scopeNode, importedFunctionsMap, pkg, }) {
79
+ if (t.isJSXExpressionContainer(node)) {
80
+ // Skip JSX comments
81
+ if (t.isJSXEmptyExpression(node.expression)) {
82
+ return null;
83
+ }
84
+ const expr = node.expression;
85
+ if (t.isJSXElement(expr)) {
86
+ return buildJSXTree({
87
+ importAliases,
88
+ node: expr,
89
+ unwrappedExpressions,
90
+ visited,
91
+ callStack,
92
+ updates,
93
+ errors: errors,
94
+ warnings: warnings,
95
+ file,
96
+ insideT,
97
+ parsingOptions,
98
+ scopeNode,
99
+ importedFunctionsMap,
100
+ pkg,
101
+ });
102
+ }
103
+ const staticAnalysis = isStaticExpression(expr, true);
104
+ if (staticAnalysis.isStatic && staticAnalysis.value !== undefined) {
105
+ // Preserve the exact whitespace for static string expressions
106
+ return {
107
+ nodeType: 'expression',
108
+ result: staticAnalysis.value,
109
+ };
110
+ }
111
+ // Keep existing behavior for non-static expressions
112
+ const code = generate(node).code;
113
+ unwrappedExpressions.push(code); // Keep track of unwrapped expressions for error reporting
114
+ return code;
115
+ }
116
+ else if (t.isJSXText(node)) {
117
+ // Updated JSX Text handling
118
+ // JSX Text handling following React's rules
119
+ const text = node.value;
120
+ return text;
121
+ }
122
+ else if (t.isJSXElement(node)) {
123
+ const element = node;
124
+ const elementName = element.openingElement.name;
125
+ let typeName;
126
+ if (t.isJSXIdentifier(elementName)) {
127
+ typeName = elementName.name;
128
+ }
129
+ else if (t.isJSXMemberExpression(elementName)) {
130
+ typeName = generate(elementName).code;
131
+ }
132
+ else {
133
+ typeName = null;
134
+ }
135
+ // Convert from alias to original name
136
+ const componentType = importAliases[typeName ?? ''];
137
+ if (componentType === TRANSLATION_COMPONENT && insideT) {
138
+ // Add warning: Nested <T> components are allowed, but they are advised against
139
+ warnings.add(warnNestedTComponent(file, `${element.loc?.start?.line}:${element.loc?.start?.column}`));
140
+ }
141
+ // If this JSXElement is one of the recognized variable components,
142
+ const elementIsVariable = VARIABLE_COMPONENTS.includes(componentType);
143
+ const props = {};
144
+ const elementIsPlural = componentType === 'Plural';
145
+ const elementIsBranch = componentType === 'Branch';
146
+ element.openingElement.attributes.forEach((attr) => {
147
+ if (t.isJSXAttribute(attr)) {
148
+ const attrName = attr.name.name;
149
+ let attrValue = null;
150
+ if (attr.value) {
151
+ if (t.isStringLiteral(attr.value)) {
152
+ attrValue = attr.value.value;
153
+ }
154
+ else if (t.isJSXExpressionContainer(attr.value)) {
155
+ // Check if this is an HTML content prop (title, placeholder, alt, etc.)
156
+ const isHtmlContentProp = Object.values(HTML_CONTENT_PROPS).includes(attrName);
157
+ if (isHtmlContentProp) {
158
+ // For HTML content props, only accept static string expressions
159
+ const staticAnalysis = isStaticExpression(attr.value.expression, true);
160
+ if (staticAnalysis.isStatic &&
161
+ staticAnalysis.value !== undefined) {
162
+ attrValue = staticAnalysis.value;
163
+ }
164
+ // Otherwise attrValue stays null and won't be included
165
+ }
166
+ else {
167
+ // For non-HTML-content props, validate plural/branch then build tree
168
+ if ((elementIsPlural && isAcceptedPluralForm(attrName)) ||
169
+ (elementIsBranch && attrName !== 'branch')) {
170
+ // Make sure that variable strings like {`I have ${count} book`} are invalid!
171
+ if (t.isTemplateLiteral(attr.value.expression) &&
172
+ !isStaticExpression(attr.value.expression, true).isStatic) {
173
+ unwrappedExpressions.push(generate(attr.value).code);
174
+ }
175
+ // If it's an array, flag as an unwrapped expression
176
+ if (t.isArrayExpression(attr.value.expression)) {
177
+ unwrappedExpressions.push(generate(attr.value.expression).code);
178
+ }
179
+ }
180
+ attrValue = buildJSXTree({
181
+ importAliases,
182
+ node: attr.value.expression,
183
+ unwrappedExpressions,
184
+ visited,
185
+ callStack,
186
+ updates,
187
+ errors: errors,
188
+ warnings: warnings,
189
+ file: file,
190
+ insideT: true,
191
+ parsingOptions,
192
+ scopeNode,
193
+ importedFunctionsMap,
194
+ pkg,
195
+ });
196
+ }
197
+ }
198
+ }
199
+ props[attrName] = attrValue;
200
+ }
201
+ });
202
+ if (elementIsVariable) {
203
+ if (componentType === STATIC_COMPONENT) {
204
+ return resolveStaticComponentChildren({
205
+ importAliases,
206
+ scopeNode,
207
+ children: element.children,
208
+ unwrappedExpressions,
209
+ visited,
210
+ updates,
211
+ errors,
212
+ warnings,
213
+ file,
214
+ callStack,
215
+ parsingOptions,
216
+ importedFunctionsMap,
217
+ pkg,
218
+ props,
219
+ });
220
+ }
221
+ // I do not see why this is being called, i am disabling this for now:
222
+ // parseJSXElement({
223
+ // importAliases,
224
+ // node: element,
225
+ // updates,
226
+ // errors,
227
+ // warnings,
228
+ // file,
229
+ // parsingOptions,
230
+ // });
231
+ return {
232
+ nodeType: 'element',
233
+ // if componentType is undefined, use typeName
234
+ // Basically, if componentType is not a GT component, use typeName such as <div>
235
+ type: componentType ?? typeName ?? '',
236
+ props,
237
+ };
238
+ }
239
+ const children = element.children
240
+ .map((child) => buildJSXTree({
241
+ importAliases,
242
+ node: child,
243
+ unwrappedExpressions,
244
+ visited,
245
+ callStack,
246
+ updates,
247
+ errors,
248
+ warnings,
249
+ file,
250
+ insideT: true,
251
+ parsingOptions,
252
+ scopeNode,
253
+ importedFunctionsMap,
254
+ pkg,
255
+ }))
256
+ .filter((child) => child !== null && child !== '');
257
+ if (children.length === 1) {
258
+ props.children = children[0];
259
+ }
260
+ else if (children.length > 1) {
261
+ props.children = children;
262
+ }
263
+ return {
264
+ nodeType: 'element',
265
+ // if componentType is undefined, use typeName
266
+ // Basically, if componentType is not a GT component, use typeName such as <div>
267
+ type: componentType ?? typeName,
268
+ props,
269
+ };
270
+ }
271
+ // If it's a JSX fragment
272
+ else if (t.isJSXFragment(node)) {
273
+ const children = node.children
274
+ .map((child) => buildJSXTree({
275
+ importAliases,
276
+ node: child,
277
+ unwrappedExpressions,
278
+ visited,
279
+ callStack,
280
+ updates,
281
+ errors,
282
+ warnings,
283
+ file,
284
+ insideT: true,
285
+ parsingOptions,
286
+ scopeNode,
287
+ importedFunctionsMap,
288
+ pkg,
289
+ }))
290
+ .filter((child) => child !== null && child !== '');
291
+ const props = {};
292
+ if (children.length === 1) {
293
+ props.children = children[0];
294
+ }
295
+ else if (children.length > 1) {
296
+ props.children = children;
297
+ }
298
+ return {
299
+ nodeType: 'element',
300
+ type: '',
301
+ props,
302
+ };
303
+ }
304
+ // If it's a string literal (standalone)
305
+ else if (t.isStringLiteral(node)) {
306
+ return node.value;
307
+ }
308
+ // If it's a template literal
309
+ else if (t.isTemplateLiteral(node)) {
310
+ // We've already checked that it's static, and and added a warning if it's not, this check is just for fallback behavior
311
+ if (!isStaticExpression(node, true).isStatic ||
312
+ node.quasis[0].value.cooked === undefined) {
313
+ return generate(node).code;
314
+ }
315
+ return node.quasis[0].value.cooked;
316
+ }
317
+ else if (t.isNullLiteral(node)) {
318
+ // If it's null, return null
319
+ return null;
320
+ }
321
+ else if (t.isBooleanLiteral(node)) {
322
+ // If it's a boolean, return the boolean
323
+ return node.value;
324
+ }
325
+ else if (t.isNumericLiteral(node)) {
326
+ // If it's a number, return the number
327
+ return node.value.toString();
328
+ }
329
+ // Negative
330
+ else if (t.isUnaryExpression(node)) {
331
+ // If it's a unary expression, return the expression
332
+ const staticAnalysis = isStaticExpression(node, true);
333
+ if (staticAnalysis.isStatic && staticAnalysis.value !== undefined) {
334
+ return staticAnalysis.value;
335
+ }
336
+ return generate(node).code;
337
+ }
338
+ // If it's some other JS expression
339
+ else if (t.isIdentifier(node) ||
340
+ t.isMemberExpression(node) ||
341
+ t.isCallExpression(node) ||
342
+ t.isBinaryExpression(node) ||
343
+ t.isLogicalExpression(node) ||
344
+ t.isConditionalExpression(node)) {
345
+ return generate(node).code;
346
+ }
347
+ else {
348
+ return generate(node).code;
349
+ }
350
+ }
351
+ // end buildJSXTree
352
+ // Parses a JSX element and adds it to the updates array
353
+ export function parseJSXElement({ importAliases, node, originalName, pkg, updates, errors, warnings, file, parsingOptions, scopeNode, importedFunctionsMap, }) {
354
+ const openingElement = node.openingElement;
355
+ const name = openingElement.name;
356
+ // Only proceed if it's <T> ...
357
+ // TODO: i don't think this condition is needed anymore
358
+ if (!(name.type === 'JSXIdentifier' && originalName === TRANSLATION_COMPONENT)) {
359
+ return;
360
+ }
361
+ const componentErrors = [];
362
+ const componentWarnings = new Set();
363
+ const metadata = {};
364
+ // We'll track this flag to know if any unwrapped {variable} is found in children
365
+ const unwrappedExpressions = [];
366
+ // Gather <T>'s props
367
+ parseTProps({
368
+ openingElement,
369
+ metadata,
370
+ componentErrors,
371
+ file,
372
+ });
373
+ // Build the JSX tree for this component
374
+ const treeResult = buildJSXTree({
375
+ importAliases,
376
+ node,
377
+ scopeNode,
378
+ visited: new Set(),
379
+ callStack: [],
380
+ pkg,
381
+ unwrappedExpressions,
382
+ updates,
383
+ errors: componentErrors,
384
+ warnings: componentWarnings,
385
+ file,
386
+ insideT: false,
387
+ parsingOptions,
388
+ importedFunctionsMap,
389
+ });
390
+ // Strip the outer <T> component if necessary
391
+ const jsxTree = isElementNode(treeResult) && treeResult.props?.children
392
+ ? // We know this b/c the direct children of <T> will never be a multiplication node
393
+ treeResult.props.children
394
+ : treeResult;
395
+ // Update warnings
396
+ if (componentWarnings.size > 0) {
397
+ componentWarnings.forEach((warning) => warnings.add(warning));
398
+ }
399
+ // Update errors
400
+ if (componentErrors.length > 0) {
401
+ errors.push(...componentErrors);
402
+ return;
403
+ }
404
+ // Handle whitespace in children
405
+ const whitespaceHandledTree = handleChildrenWhitespace(jsxTree);
406
+ // Multiply the tree
407
+ const multipliedTrees = multiplyJsxTree(whitespaceHandledTree);
408
+ // Add GT identifiers to the tree
409
+ // TODO: do this in parallel
410
+ const minifiedTress = [];
411
+ for (const multipliedTree of multipliedTrees) {
412
+ const minifiedTree = addGTIdentifierToSyntaxTree(multipliedTree);
413
+ minifiedTress.push(Array.isArray(minifiedTree) && minifiedTree.length === 1
414
+ ? minifiedTree[0]
415
+ : minifiedTree);
416
+ }
417
+ // If we found an unwrapped expression, skip
418
+ if (unwrappedExpressions.length > 0) {
419
+ errors.push(warnHasUnwrappedExpressionSync(file, unwrappedExpressions, metadata.id, `${node.loc?.start?.line}:${node.loc?.start?.column}`));
420
+ return;
421
+ }
422
+ // <T> is valid here
423
+ for (const minifiedTree of minifiedTress) {
424
+ updates.push({
425
+ dataFormat: 'JSX',
426
+ source: minifiedTree,
427
+ // eslint-disable-next-line no-undef
428
+ metadata: { ...structuredClone(metadata) },
429
+ });
430
+ }
431
+ }
432
+ /**
433
+ * Resolves an invocation inside of a <Static> component. It will resolve the function, and build
434
+ * a jsx tree for each return inside of the function definition.
435
+ *
436
+ * function getOtherSubject() {
437
+ * return <div>Jane</div>;
438
+ * }
439
+ *
440
+ * function getSubject() {
441
+ * if (condition) return getOtherSubject();
442
+ * return <div>John</div>;
443
+ * }
444
+ * ...
445
+ * <Static>
446
+ * {getSubject()}
447
+ * </Static>
448
+ */
449
+ function resolveStaticComponentChildren({ importAliases, scopeNode, children, unwrappedExpressions, visited, updates, errors, warnings, file, callStack, parsingOptions, importedFunctionsMap, pkg, props, }) {
450
+ const result = {
451
+ nodeType: 'element',
452
+ type: STATIC_COMPONENT,
453
+ props,
454
+ };
455
+ let found = false;
456
+ // Create children array if necessary
457
+ if (children.length) {
458
+ result.props.children = [];
459
+ }
460
+ for (const child of children) {
461
+ // Ignore whitespace outside of jsx container
462
+ if (t.isJSXText(child) && child.value.trim() === '') {
463
+ result.props.children.push(child.value);
464
+ continue;
465
+ }
466
+ // Must be an expression container with a function invocation
467
+ if (!t.isJSXExpressionContainer(child) ||
468
+ !((t.isCallExpression(child.expression) &&
469
+ t.isIdentifier(child.expression.callee)) ||
470
+ (t.isAwaitExpression(child.expression) &&
471
+ t.isCallExpression(child.expression.argument) &&
472
+ t.isIdentifier(child.expression.argument.callee))) ||
473
+ found // There can only be one invocation inside of a <Static> component
474
+ ) {
475
+ errors.push(warnInvalidStaticChildSync(file, `${child.loc?.start?.line}:${child.loc?.start?.column}`));
476
+ continue;
477
+ }
478
+ // Set found to true
479
+ found = true;
480
+ // Get callee and binding from scope
481
+ const callee = (t.isAwaitExpression(child.expression)
482
+ ? child.expression.argument.callee
483
+ : child.expression.callee);
484
+ const calleeBinding = scopeNode.scope.getBinding(callee.name);
485
+ if (!calleeBinding) {
486
+ warnFunctionNotFoundSync(file, callee.name, `${callee.loc?.start?.line}:${callee.loc?.start?.column}`);
487
+ continue;
488
+ }
489
+ // Function is found locally, return wrapped in an expression
490
+ const staticFunctionInvocation = resolveStaticFunctionInvocationFromBinding({
491
+ importAliases,
492
+ calleeBinding,
493
+ callee,
494
+ visited,
495
+ callStack,
496
+ file,
497
+ updates,
498
+ errors,
499
+ warnings,
500
+ unwrappedExpressions,
501
+ pkg,
502
+ parsingOptions,
503
+ importedFunctionsMap,
504
+ });
505
+ result.props.children.push({
506
+ nodeType: 'expression',
507
+ result: staticFunctionInvocation,
508
+ });
509
+ }
510
+ return result;
511
+ }
512
+ function resolveStaticFunctionInvocationFromBinding({ importAliases, calleeBinding, callee, unwrappedExpressions, visited, callStack, file, updates, errors, warnings, parsingOptions, importedFunctionsMap, pkg, }) {
513
+ function withRecusionGuard({ cb, filename, functionName, }) {
514
+ const cacheKey = `${filename}::${functionName}`;
515
+ if (callStack.includes(cacheKey)) {
516
+ errors.push(warnRecursiveFunctionCallSync(file, functionName));
517
+ return null;
518
+ }
519
+ callStack.push(cacheKey);
520
+ const result = cb();
521
+ callStack.pop();
522
+ return result;
523
+ }
524
+ // check for recursive calls
525
+ if (calleeBinding.path.isFunctionDeclaration()) {
526
+ // Handle function declarations: function getSubject() { ... }
527
+ const functionName = callee.name;
528
+ const path = calleeBinding.path;
529
+ return withRecusionGuard({
530
+ filename: file,
531
+ functionName,
532
+ cb: () => processFunctionDeclarationNodePath({
533
+ importAliases,
534
+ functionName,
535
+ path,
536
+ unwrappedExpressions,
537
+ callStack,
538
+ updates,
539
+ errors,
540
+ warnings,
541
+ visited,
542
+ file,
543
+ parsingOptions,
544
+ importedFunctionsMap,
545
+ pkg,
546
+ }),
547
+ });
548
+ }
549
+ else if (calleeBinding.path.isVariableDeclarator() &&
550
+ calleeBinding.path.node.init &&
551
+ (t.isArrowFunctionExpression(calleeBinding.path.node.init) ||
552
+ t.isFunctionExpression(calleeBinding.path.node.init))) {
553
+ // Handle arrow functions assigned to variables: const getData = (t) => {...}
554
+ const functionName = callee.name;
555
+ const path = calleeBinding.path;
556
+ return withRecusionGuard({
557
+ filename: file,
558
+ functionName,
559
+ cb: () => processVariableDeclarationNodePath({
560
+ importAliases,
561
+ functionName,
562
+ path,
563
+ unwrappedExpressions,
564
+ updates,
565
+ callStack,
566
+ pkg,
567
+ errors,
568
+ visited,
569
+ warnings,
570
+ file,
571
+ parsingOptions,
572
+ importedFunctionsMap,
573
+ }),
574
+ });
575
+ }
576
+ else if (importedFunctionsMap.has(callee.name)) {
577
+ // Function is being imported
578
+ const importPath = importedFunctionsMap.get(callee.name);
579
+ const filePath = resolveImportPath(file, importPath, parsingOptions, resolveImportPathCache);
580
+ if (filePath) {
581
+ const functionName = callee.name;
582
+ return withRecusionGuard({
583
+ filename: file,
584
+ functionName,
585
+ cb: () => processFunctionInFile({
586
+ filePath,
587
+ functionName,
588
+ visited,
589
+ callStack,
590
+ unwrappedExpressions,
591
+ updates,
592
+ errors,
593
+ warnings,
594
+ file,
595
+ parsingOptions,
596
+ pkg,
597
+ }),
598
+ });
599
+ }
600
+ }
601
+ warnings.add(warnFunctionNotFoundSync(file, callee.name, `${callee.loc?.start?.line}:${callee.loc?.start?.column}`));
602
+ return null;
603
+ }
604
+ /**
605
+ * Searches for a specific user-defined function in a file.
606
+ * This is the resolution logic
607
+ *
608
+ * Handles multiple function declaration patterns:
609
+ * - function getInfo() { ... }
610
+ * - export function getInfo() { ... }
611
+ * - const getInfo = () => { ... }
612
+ *
613
+ * If the function is not found in the file, follows re-exports (export * from './other')
614
+ */
615
+ function processFunctionInFile({ filePath, functionName, visited, callStack, parsingOptions, updates, errors, warnings, file, unwrappedExpressions, pkg, }) {
616
+ // Create a custom key for the function call
617
+ const cacheKey = `${filePath}::${functionName}`;
618
+ // Check cache first to avoid redundant parsing
619
+ if (processFunctionCache.has(cacheKey)) {
620
+ return processFunctionCache.get(cacheKey) ?? null;
621
+ }
622
+ // Prevent infinite loops from circular re-exports
623
+ if (visited.has(filePath)) {
624
+ return null;
625
+ }
626
+ visited.add(filePath);
627
+ let result = undefined;
628
+ try {
629
+ const code = fs.readFileSync(filePath, 'utf8');
630
+ const ast = parse(code, {
631
+ sourceType: 'module',
632
+ plugins: ['jsx', 'typescript'],
633
+ });
634
+ const { importAliases } = getPathsAndAliases(ast, pkg);
635
+ // Collect all imports in this file to track cross-file function calls
636
+ let importedFunctionsMap;
637
+ traverse(ast, {
638
+ Program(path) {
639
+ importedFunctionsMap = buildImportMap(path);
640
+ },
641
+ });
642
+ const reExports = [];
643
+ const warnDuplicateFuncDef = (path) => {
644
+ warnings.add(warnDuplicateFunctionDefinitionSync(file, functionName, `${path.node.loc?.start?.line}:${path.node.loc?.start?.column}`));
645
+ };
646
+ traverse(ast, {
647
+ // Handle function declarations: function getInfo() { ... }
648
+ FunctionDeclaration(path) {
649
+ if (path.node.id?.name === functionName) {
650
+ if (result !== undefined)
651
+ return warnDuplicateFuncDef(path);
652
+ result = processFunctionDeclarationNodePath({
653
+ importAliases,
654
+ functionName,
655
+ path,
656
+ unwrappedExpressions,
657
+ callStack,
658
+ visited,
659
+ pkg,
660
+ updates,
661
+ errors,
662
+ warnings,
663
+ file,
664
+ parsingOptions,
665
+ importedFunctionsMap,
666
+ });
667
+ }
668
+ },
669
+ // Handle variable declarations: const getInfo = () => { ... }
670
+ VariableDeclarator(path) {
671
+ if (t.isIdentifier(path.node.id) &&
672
+ path.node.id.name === functionName &&
673
+ path.node.init &&
674
+ (t.isArrowFunctionExpression(path.node.init) ||
675
+ t.isFunctionExpression(path.node.init))) {
676
+ if (result !== undefined)
677
+ return warnDuplicateFuncDef(path);
678
+ result = processVariableDeclarationNodePath({
679
+ importAliases,
680
+ functionName,
681
+ path,
682
+ callStack,
683
+ pkg,
684
+ updates,
685
+ errors,
686
+ warnings,
687
+ visited,
688
+ unwrappedExpressions,
689
+ file,
690
+ parsingOptions,
691
+ importedFunctionsMap,
692
+ });
693
+ }
694
+ },
695
+ // Collect re-exports: export * from './other'
696
+ ExportAllDeclaration(path) {
697
+ if (t.isStringLiteral(path.node.source)) {
698
+ reExports.push(path.node.source.value);
699
+ }
700
+ },
701
+ // Collect named re-exports: export { foo } from './other'
702
+ ExportNamedDeclaration(path) {
703
+ if (path.node.source && t.isStringLiteral(path.node.source)) {
704
+ // Check if this export includes our function
705
+ const exportsFunction = path.node.specifiers.some((spec) => {
706
+ if (t.isExportSpecifier(spec)) {
707
+ const exportedName = t.isIdentifier(spec.exported)
708
+ ? spec.exported.name
709
+ : spec.exported.value;
710
+ return exportedName === functionName;
711
+ }
712
+ return false;
713
+ });
714
+ if (exportsFunction) {
715
+ reExports.push(path.node.source.value);
716
+ }
717
+ }
718
+ },
719
+ });
720
+ // If function not found, follow re-exports
721
+ if (result === undefined && reExports.length > 0) {
722
+ for (const reExportPath of reExports) {
723
+ const resolvedPath = resolveImportPath(filePath, reExportPath, parsingOptions, resolveImportPathCache);
724
+ if (resolvedPath) {
725
+ const foundResult = processFunctionInFile({
726
+ filePath: resolvedPath,
727
+ functionName,
728
+ unwrappedExpressions,
729
+ visited,
730
+ callStack,
731
+ parsingOptions,
732
+ updates,
733
+ errors,
734
+ warnings,
735
+ file,
736
+ pkg,
737
+ });
738
+ if (foundResult != null) {
739
+ result = foundResult;
740
+ break;
741
+ }
742
+ }
743
+ }
744
+ }
745
+ // Mark this function search as processed in the cache
746
+ processFunctionCache.set(cacheKey, result !== undefined ? result : null);
747
+ }
748
+ catch {
749
+ // Silently skip files that can't be parsed or accessed
750
+ // Still mark as processed to avoid retrying failed parses
751
+ processFunctionCache.set(cacheKey, null);
752
+ }
753
+ return result !== undefined ? result : null;
754
+ }
755
+ /**
756
+ * Process a function declaration
757
+ * function getInfo() { ... }
758
+ */
759
+ function processFunctionDeclarationNodePath({ functionName, path, importAliases, unwrappedExpressions, visited, callStack, updates, errors, warnings, file, parsingOptions, importedFunctionsMap, pkg, }) {
760
+ const result = {
761
+ nodeType: 'multiplication',
762
+ branches: [],
763
+ };
764
+ path.traverse({
765
+ Function(path) {
766
+ path.skip();
767
+ },
768
+ ReturnStatement(returnPath) {
769
+ const returnNodePath = returnPath.get('argument');
770
+ if (!returnNodePath.isExpression()) {
771
+ return;
772
+ }
773
+ result.branches.push(processReturnExpression({
774
+ unwrappedExpressions,
775
+ functionName,
776
+ pkg,
777
+ callStack,
778
+ scopeNode: returnNodePath,
779
+ expressionNodePath: returnNodePath,
780
+ importAliases,
781
+ visited,
782
+ updates,
783
+ errors,
784
+ warnings,
785
+ file,
786
+ parsingOptions,
787
+ importedFunctionsMap,
788
+ }));
789
+ },
790
+ });
791
+ if (result.branches.length === 0) {
792
+ return null;
793
+ }
794
+ return result;
795
+ }
796
+ /**
797
+ * Process a variable declaration of a function
798
+ * const getInfo = () => { ... }
799
+ *
800
+ * IMPORTANT: the RHand value must be the function definition, or this will fail
801
+ */
802
+ function processVariableDeclarationNodePath({ functionName, path, importAliases, unwrappedExpressions, visited, callStack, updates, errors, warnings, file, parsingOptions, importedFunctionsMap, pkg, }) {
803
+ const result = {
804
+ nodeType: 'multiplication',
805
+ branches: [],
806
+ };
807
+ // Enforce the Rhand is a function definition
808
+ const arrowFunctionPath = path.get('init');
809
+ if (!arrowFunctionPath.isArrowFunctionExpression()) {
810
+ errors.push(warnInvalidStaticInitSync(file, functionName, `${path.node.loc?.start?.line}:${path.node.loc?.start?.column}`));
811
+ return null;
812
+ }
813
+ const bodyNodePath = arrowFunctionPath.get('body');
814
+ if (bodyNodePath.isExpression()) {
815
+ // process expression return
816
+ result.branches.push(processReturnExpression({
817
+ unwrappedExpressions,
818
+ functionName,
819
+ pkg,
820
+ scopeNode: arrowFunctionPath,
821
+ expressionNodePath: bodyNodePath,
822
+ importAliases,
823
+ visited,
824
+ callStack,
825
+ updates,
826
+ errors,
827
+ warnings,
828
+ file,
829
+ parsingOptions,
830
+ importedFunctionsMap,
831
+ }));
832
+ }
833
+ else {
834
+ // search for a return statement
835
+ bodyNodePath.traverse({
836
+ Function(path) {
837
+ path.skip();
838
+ },
839
+ ReturnStatement(returnPath) {
840
+ const returnNodePath = returnPath.get('argument');
841
+ if (!returnNodePath.isExpression()) {
842
+ return;
843
+ }
844
+ result.branches.push(processReturnExpression({
845
+ unwrappedExpressions,
846
+ functionName,
847
+ pkg,
848
+ scopeNode: returnPath,
849
+ expressionNodePath: returnNodePath,
850
+ importAliases,
851
+ visited,
852
+ callStack,
853
+ updates,
854
+ errors,
855
+ warnings,
856
+ file,
857
+ parsingOptions,
858
+ importedFunctionsMap,
859
+ }));
860
+ },
861
+ });
862
+ }
863
+ if (result.branches.length === 0) {
864
+ errors.push(warnMissingReturnSync(file, functionName, `${path.node.loc?.start?.line}:${path.node.loc?.start?.column}`));
865
+ return null;
866
+ }
867
+ return result;
868
+ }
869
+ /**
870
+ * Process a expression being returned from a function
871
+ */
872
+ function processReturnExpression({ unwrappedExpressions, scopeNode, expressionNodePath, importAliases, visited, callStack, updates, errors, warnings, file, parsingOptions, importedFunctionsMap, functionName, pkg, }) {
873
+ // // If the node is null, return
874
+ // if (expressionNodePath == null) return null;
875
+ // Remove parentheses if they exist
876
+ if (t.isParenthesizedExpression(expressionNodePath.node)) {
877
+ // ex: return (value)
878
+ return processReturnExpression({
879
+ unwrappedExpressions,
880
+ importAliases,
881
+ scopeNode,
882
+ expressionNodePath: expressionNodePath.get('expression'),
883
+ visited,
884
+ callStack,
885
+ updates,
886
+ errors,
887
+ warnings,
888
+ file,
889
+ parsingOptions,
890
+ functionName,
891
+ importedFunctionsMap,
892
+ pkg,
893
+ });
894
+ }
895
+ else if (t.isCallExpression(expressionNodePath.node) &&
896
+ t.isIdentifier(expressionNodePath.node.callee)) {
897
+ // ex: return someFunc()
898
+ const callee = expressionNodePath.node.callee;
899
+ const calleeBinding = scopeNode.scope.getBinding(callee.name);
900
+ if (!calleeBinding) {
901
+ warnings.add(warnFunctionNotFoundSync(file, callee.name, `${callee.loc?.start?.line}:${callee.loc?.start?.column}`));
902
+ return null;
903
+ }
904
+ // Function is found locally
905
+ return resolveStaticFunctionInvocationFromBinding({
906
+ importAliases,
907
+ calleeBinding,
908
+ callee,
909
+ unwrappedExpressions,
910
+ callStack,
911
+ visited,
912
+ file,
913
+ updates,
914
+ errors,
915
+ warnings,
916
+ pkg,
917
+ parsingOptions,
918
+ importedFunctionsMap,
919
+ });
920
+ }
921
+ else if (t.isAwaitExpression(expressionNodePath.node) &&
922
+ t.isCallExpression(expressionNodePath.node.argument) &&
923
+ t.isIdentifier(expressionNodePath.node.argument.callee)) {
924
+ // ex: return await someFunc()
925
+ const callee = expressionNodePath.node.argument.callee;
926
+ const calleeBinding = scopeNode.scope.getBinding(callee.name);
927
+ if (!calleeBinding) {
928
+ warnings.add(warnFunctionNotFoundSync(file, callee.name, `${callee.loc?.start?.line}:${callee.loc?.start?.column}`));
929
+ return null;
930
+ }
931
+ // Function is found locally
932
+ return resolveStaticFunctionInvocationFromBinding({
933
+ importAliases,
934
+ calleeBinding,
935
+ callee,
936
+ unwrappedExpressions,
937
+ visited,
938
+ callStack,
939
+ file,
940
+ updates,
941
+ errors,
942
+ warnings,
943
+ pkg,
944
+ parsingOptions,
945
+ importedFunctionsMap,
946
+ });
947
+ }
948
+ else if (t.isJSXElement(expressionNodePath.node) ||
949
+ t.isJSXFragment(expressionNodePath.node)) {
950
+ // ex: return <div>Jsx content</div>
951
+ return buildJSXTree({
952
+ importAliases,
953
+ node: expressionNodePath.node,
954
+ unwrappedExpressions,
955
+ visited,
956
+ callStack,
957
+ updates,
958
+ errors,
959
+ warnings,
960
+ file,
961
+ insideT: true,
962
+ parsingOptions,
963
+ scopeNode,
964
+ importedFunctionsMap,
965
+ pkg,
966
+ });
967
+ }
968
+ else if (t.isConditionalExpression(expressionNodePath.node)) {
969
+ // ex: return condition ? <div>Jsx content</div> : <div>Jsx content</div>
970
+ // since two options here we must construct a new multiplication node
971
+ const consequentNodePath = expressionNodePath.get('consequent');
972
+ const alternateNodePath = expressionNodePath.get('alternate');
973
+ const result = {
974
+ nodeType: 'multiplication',
975
+ branches: [consequentNodePath, alternateNodePath].map((expressionNodePath) => processReturnExpression({
976
+ unwrappedExpressions,
977
+ importAliases,
978
+ scopeNode,
979
+ expressionNodePath,
980
+ visited,
981
+ callStack,
982
+ updates,
983
+ errors,
984
+ warnings,
985
+ file,
986
+ parsingOptions,
987
+ functionName,
988
+ importedFunctionsMap,
989
+ pkg,
990
+ })),
991
+ };
992
+ return result;
993
+ }
994
+ else {
995
+ // Handle static expressions (e.g. return 'static string')
996
+ const staticAnalysis = isStaticExpression(expressionNodePath.node);
997
+ if (staticAnalysis.isStatic && staticAnalysis.value !== undefined) {
998
+ // Preserve the exact whitespace for static string expressions
999
+ return staticAnalysis.value;
1000
+ }
1001
+ // reject
1002
+ errors.push(warnInvalidReturnExpressionSync(file, functionName, generate(expressionNodePath.node).code, `${scopeNode.node.loc?.start?.line}:${scopeNode.node.loc?.start?.column}`));
1003
+ return null;
1004
+ }
1005
+ }