UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

291 lines (290 loc) 11.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.findTranslationHooksInFile = findTranslationHooksInFile; exports.findTranslationCalls = findTranslationCalls; exports.getComponentNameForNode = getComponentNameForNode; exports.analyzeUsageContext = analyzeUsageContext; exports.processTranslationKey = processTranslationKey; 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 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 findTranslationHooksInFile(sourceFile, filePath) { const result = []; // Visit all nodes in the source file const visitNode = (node) => { // Look for variable declarations like: const t = useTranslations("Namespace") if (typescript_1.default.isVariableDeclaration(node) && node.initializer && typescript_1.default.isCallExpression(node.initializer)) { const call = node.initializer; const expression = call.expression; // Check if it's a useTranslations call if (typescript_1.default.isIdentifier(expression) && /^useTranslation[s]?$/.test(expression.text)) { let varName; let namespace; // Get the variable name if (typescript_1.default.isIdentifier(node.name)) { varName = node.name.text; } else if (typescript_1.default.isObjectBindingPattern(node.name)) { // Handle destructuring: const { t } = useTranslations() for (const element of node.name.elements) { if (typescript_1.default.isBindingElement(element) && element.name && typescript_1.default.isIdentifier(element.name)) { varName = element.name.text; break; } } } // Get the namespace if (call.arguments.length > 0) { const arg = call.arguments[0]; if (typescript_1.default.isStringLiteral(arg)) { namespace = arg.text; } } if (varName) { const componentName = getComponentNameForNode(node, sourceFile); result.push({ varName, namespace, node, componentName, }); } } } typescript_1.default.forEachChild(node, visitNode); }; visitNode(sourceFile); return result; } /** * Finds all translation calls for a specific hook * @param sourceFile TypeScript source file * @param hookName The variable name used for translations * @returns Array of translation calls */ function findTranslationCalls(sourceFile, hookName) { const translations = []; // Visit all nodes to find translation function calls const visitNode = (node) => { if (typescript_1.default.isCallExpression(node)) { const expression = node.expression; // Direct call: t("key") if (typescript_1.default.isIdentifier(expression) && expression.text === hookName) { if (node.arguments.length > 0) { const arg = node.arguments[0]; if (typescript_1.default.isStringLiteral(arg)) { translations.push({ key: arg.text, node, }); } } } } typescript_1.default.forEachChild(node, visitNode); }; visitNode(sourceFile); return translations; } /** * Gets the enclosing component name for a node * @param node AST node * @param sourceFile Source file containing the node * @returns The component name */ function getComponentNameForNode(node, sourceFile) { // Try to find the nearest function or class declaration let current = node; while (current) { if (typescript_1.default.isFunctionDeclaration(current) || typescript_1.default.isArrowFunction(current) || typescript_1.default.isFunctionExpression(current)) { // For function declaration, use the name if (typescript_1.default.isFunctionDeclaration(current) && current.name) { return current.name.text; } // For variable declaration with arrow function, use variable name if (current.parent && typescript_1.default.isVariableDeclaration(current.parent) && current.parent.name) { if (typescript_1.default.isIdentifier(current.parent.name)) { return current.parent.name.text; } } } // For class component if (typescript_1.default.isClassDeclaration(current) && current.name) { return current.name.text; } current = current.parent; } // If no component name found, use the file name return path_1.default.basename(sourceFile.fileName, path_1.default.extname(sourceFile.fileName)); } /** * Analyzes the usage context of a translation call * @param node The call expression node * @param componentName The component name * @returns The usage context object */ function analyzeUsageContext(node, componentName) { let isInJSX = false; let isInConditional = false; let parentComponent = undefined; let isInEventHandler = false; let renderCount = 0; // Find parent JSX let current = node; while (current) { // Check if in JSX if (typescript_1.default.isJsxElement(current) || typescript_1.default.isJsxAttribute(current) || typescript_1.default.isJsxExpression(current)) { isInJSX = true; } // Check if in conditional if (typescript_1.default.isIfStatement(current) || typescript_1.default.isConditionalExpression(current) || (typescript_1.default.isBinaryExpression(current) && (current.operatorToken.kind === typescript_1.default.SyntaxKind.AmpersandAmpersandToken || current.operatorToken.kind === typescript_1.default.SyntaxKind.BarBarToken))) { isInConditional = true; } // Check if in event handler if (typescript_1.default.isMethodDeclaration(current) && current.name && typescript_1.default.isIdentifier(current.name) && (current.name.text.startsWith("handle") || current.name.text.startsWith("on"))) { isInEventHandler = true; } // Check if in a different component than the current one if ((typescript_1.default.isFunctionDeclaration(current) || typescript_1.default.isArrowFunction(current) || typescript_1.default.isFunctionExpression(current)) && current.parent && typescript_1.default.isVariableDeclaration(current.parent) && current.parent.name && typescript_1.default.isIdentifier(current.parent.name) && current.parent.name.text !== componentName) { parentComponent = current.parent.name.text; } // Check for render-related method if (typescript_1.default.isMethodDeclaration(current) && current.name && typescript_1.default.isIdentifier(current.name) && current.name.text === "render") { renderCount++; } current = current.parent; } return { isInJSX, isInConditional, parentComponent, isInEventHandler, renderCount, }; } /** * Processes a translation key and creates a TranslationKey object * @param keyText The translation key text * @param namespace The namespace if available * @param componentName The component name * @param call The call expression node * @param sourceFile The source file * @param filePath The file path * @returns TranslationKey object */ function processTranslationKey(keyText, namespace, componentName, call, sourceFile, filePath) { const location = ASTUtils_1.ASTUtils.getNodeLocation(call, sourceFile); // Handle both namespace.key and key formats let fullKey; let keyNamespace = namespace; let keyName = keyText; if (keyText.includes(".") && !namespace) { // Format: t("namespace.key") const parts = keyText.split("."); keyNamespace = parts[0]; keyName = parts.slice(1).join("."); fullKey = keyText; } else if (namespace) { // Format: const t = useTranslations("namespace"); t("key") fullKey = `${namespace}.${keyText}`; } else { // No namespace provided fullKey = keyText; } // Get context code const contextCode = getContextCode(call, sourceFile); // Analyze usage context const usageContext = analyzeUsageContext(call, componentName); return { key: keyName, namespace: keyNamespace, fullKey, location, componentName, filePath, contextCode, usageContext, }; } /** * 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) { // Get the line number (0-based from TS API) const { line } = ASTUtils_1.ASTUtils.getNodeLocation(node, sourceFile); // Convert to 1-based for display but access array with 0-based index const lineIndex = line; // line is already 0-based from TypeScript API 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); }