UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

283 lines (282 loc) 12.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.findReactI18nextHooksInFile = findReactI18nextHooksInFile; exports.findReactI18nextCalls = findReactI18nextCalls; exports.processReactI18nextKey = processReactI18nextKey; exports.analyzeReactI18nextUsageContext = analyzeReactI18nextUsageContext; exports.getContextCode = getContextCode; exports.createSourceFile = createSourceFile; exports.isTypeScriptFile = isTypeScriptFile; const typescript_1 = __importDefault(require("typescript")); const path_1 = __importDefault(require("path")); const ASTUtils_1 = require("../../../utils/ast/ASTUtils"); /** * Finds all react-i18next translation hooks in a source file * @param sourceFile TypeScript source file * @param filePath Path to the source file * @returns Array of translation hooks found in the file */ function findReactI18nextHooksInFile(sourceFile, filePath) { const result = []; // Use CommonASTUtils to find all variable declarations const variableDeclarations = ASTUtils_1.ASTUtils.findNodes(sourceFile, (node) => typescript_1.default.isVariableDeclaration(node)); for (const node of variableDeclarations) { if (node.initializer && typescript_1.default.isCallExpression(node.initializer)) { const call = node.initializer; const expression = call.expression; // Check if it's a useTranslation call (singular) if (typescript_1.default.isIdentifier(expression) && expression.text === "useTranslation") { let varName; let namespace; // Get the variable name from destructuring: const { t } = useTranslation() if (typescript_1.default.isObjectBindingPattern(node.name)) { for (const element of node.name.elements) { if (typescript_1.default.isBindingElement(element) && element.name && typescript_1.default.isIdentifier(element.name) && element.name.text === "t") { varName = element.name.text; break; } } } // Direct assignment: const t = useTranslation() (less common but possible) else if (typescript_1.default.isIdentifier(node.name)) { varName = node.name.text; } // Get the namespace from the first argument if (call.arguments.length > 0) { const arg = call.arguments[0]; if (typescript_1.default.isStringLiteral(arg)) { namespace = arg.text; } // Handle array of namespaces: useTranslation(['ns1', 'ns2']) else if (typescript_1.default.isArrayLiteralExpression(arg) && arg.elements.length > 0) { const firstElement = arg.elements[0]; if (typescript_1.default.isStringLiteral(firstElement)) { namespace = firstElement.text; } } } if (varName) { const componentName = ASTUtils_1.ASTUtils.getFunctionNameFromNode(ASTUtils_1.ASTUtils.getContainingFunction(node) || node); result.push({ varName, namespace, node, componentName, }); } } } } return result; } /** * Finds all react-i18next translation calls for a specific hook * @param sourceFile TypeScript source file * @param hookName The variable name used for translations (usually 't') * @returns Array of react-i18next translation calls */ function findReactI18nextCalls(sourceFile, hookName) { const translations = []; // Use CommonASTUtils to find all call expressions const callExpressions = ASTUtils_1.ASTUtils.findNodes(sourceFile, (node) => typescript_1.default.isCallExpression(node)); for (const node of callExpressions) { const expression = node.expression; // Direct call: t("key") or t("namespace:key") if (typescript_1.default.isIdentifier(expression) && expression.text === hookName) { if (node.arguments.length > 0) { const firstArg = node.arguments[0]; if (typescript_1.default.isStringLiteral(firstArg)) { const key = firstArg.text; let namespaceFromOptions; let defaultValue; const hasNamespacePrefix = key.includes(":"); // Check for options object as second parameter if (node.arguments.length > 1) { const secondArg = node.arguments[1]; if (typescript_1.default.isObjectLiteralExpression(secondArg)) { // Look for 'ns' property for namespace const nsProperty = secondArg.properties.find((prop) => typescript_1.default.isPropertyAssignment(prop) && typescript_1.default.isIdentifier(prop.name) && prop.name.text === "ns" && typescript_1.default.isStringLiteral(prop.initializer)); if (nsProperty && typescript_1.default.isPropertyAssignment(nsProperty) && typescript_1.default.isStringLiteral(nsProperty.initializer)) { namespaceFromOptions = nsProperty.initializer.text; } // Look for 'defaultValue' property const defaultValueProperty = secondArg.properties.find((prop) => typescript_1.default.isPropertyAssignment(prop) && typescript_1.default.isIdentifier(prop.name) && prop.name.text === "defaultValue" && typescript_1.default.isStringLiteral(prop.initializer)); if (defaultValueProperty && typescript_1.default.isPropertyAssignment(defaultValueProperty) && typescript_1.default.isStringLiteral(defaultValueProperty.initializer)) { defaultValue = defaultValueProperty.initializer.text; } } } translations.push({ key, node, namespaceFromOptions, defaultValue, hasNamespacePrefix, }); } } } } return translations; } /** * Processes a react-i18next translation key and creates a TranslationKey object * @param call React-i18next call information * @param hookNamespace Namespace from the hook declaration * @param componentName The component name * @param sourceFile The source file * @param filePath The file path * @returns TranslationKey object */ function processReactI18nextKey(call, hookNamespace, componentName, sourceFile, filePath) { const location = ASTUtils_1.ASTUtils.getNodeLocation(call.node, sourceFile); let finalNamespace; let keyName; let fullKey; // Determine namespace priority: options.ns > namespace:key > hook namespace if (call.namespaceFromOptions) { finalNamespace = call.namespaceFromOptions; keyName = call.key; fullKey = `${finalNamespace}.${keyName}`; } else if (call.hasNamespacePrefix) { const parts = call.key.split(":"); finalNamespace = parts[0]; keyName = parts.slice(1).join(":"); fullKey = `${finalNamespace}.${keyName}`; } else if (hookNamespace) { finalNamespace = hookNamespace; keyName = call.key; fullKey = `${finalNamespace}.${keyName}`; } else { // No namespace, use default 'translation' namespace (react-i18next default) finalNamespace = "translation"; keyName = call.key; fullKey = `${finalNamespace}.${keyName}`; } // Get context code using common utilities const contextCode = getContextCode(call.node, sourceFile); // Analyze usage context const usageContext = analyzeReactI18nextUsageContext(call.node, componentName); return { key: keyName, namespace: finalNamespace, fullKey, location, componentName, filePath, contextCode, usageContext, }; } /** * Analyzes the usage context of a react-i18next translation call * @param node The call expression node * @param componentName The component name * @returns The usage context object */ function analyzeReactI18nextUsageContext(node, componentName) { let isInJSX = false; let isInConditional = false; let parentComponent = undefined; let isInEventHandler = false; let renderCount = 0; // Use CommonASTUtils to get the node path const nodePath = ASTUtils_1.ASTUtils.getNodePath(node); for (const ancestor of nodePath) { // Check if in JSX if (typescript_1.default.isJsxElement(ancestor) || typescript_1.default.isJsxAttribute(ancestor) || typescript_1.default.isJsxExpression(ancestor)) { isInJSX = true; } // Check conditional expressions using CommonASTUtils const conditions = ASTUtils_1.ASTUtils.findContainingConditions(node); if (conditions.length > 0) { isInConditional = true; } // Check if in event handler (JSX attributes starting with 'on') if (typescript_1.default.isJsxAttribute(ancestor) && ancestor.name.getText().startsWith("on")) { isInEventHandler = true; } // Check if in a different component than the current one const containingFunction = ASTUtils_1.ASTUtils.getContainingFunction(ancestor); if (containingFunction) { const functionName = ASTUtils_1.ASTUtils.getFunctionNameFromNode(containingFunction); if (functionName !== componentName && functionName !== "anonymous") { parentComponent = functionName; } } // Check for render-related method if (typescript_1.default.isMethodDeclaration(ancestor) && ancestor.name && typescript_1.default.isIdentifier(ancestor.name) && ancestor.name.text === "render") { renderCount++; } } return { isInJSX, isInConditional, parentComponent, isInEventHandler, renderCount, }; } /** * Gets context code for a node (line before, current line, line after) * @param node The AST node * @param sourceFile The source file * @returns Object with before, line, and after text */ function getContextCode(node, sourceFile) { const { line } = ASTUtils_1.ASTUtils.getNodeLocation(node, sourceFile); const lineIndex = line; const fileLines = sourceFile.text.split("\n"); const beforeLine = lineIndex > 0 ? fileLines[lineIndex - 1].trim() : ""; const currentLine = fileLines[lineIndex].trim(); const afterLine = lineIndex + 1 < fileLines.length ? fileLines[lineIndex + 1].trim() : ""; return { before: beforeLine, line: currentLine, after: afterLine, }; } /** * Creates a source file from content * @param filePath File path * @param content File content * @returns TypeScript source file */ function createSourceFile(filePath, content) { return typescript_1.default.createSourceFile(filePath, content, typescript_1.default.ScriptTarget.Latest, true); } /** * Determines if a file is a TypeScript/JavaScript file * @param filePath File path * @returns Boolean indicating if it's a TS/JS file */ function isTypeScriptFile(filePath) { const ext = path_1.default.extname(filePath).toLowerCase(); return [".js", ".jsx", ".ts", ".tsx"].includes(ext); }