sicua
Version:
A tool for analyzing project structure and dependencies
283 lines (282 loc) • 12.5 kB
JavaScript
;
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);
}