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