core/analyzers/closure.analyzer.ts
core/analyzers/closure.analyzer.ts is a file in Project Scripts. 160 lines of code and 33 definitions.
import type { Atom, FieldEntry, FileFlags, GraphDelta } from "#types/closure.types";
import { ICONS_SUFFIX, IDS_SUFFIX, ID_PROPERTIES, STRINGS_SUFFIX } from "#configuration/constants/closure.constants";
import { callDelta, exportDelta } from "#core/converters/closure.converter";
import { normalizePath } from "@ssot/govlab/shared/resolvers/anchor.resolver.ts";
import { readFileSync } from "node:fs";
import { relative } from "node:path";
import { resolveSelfImport } from "#core/resolvers/specifier.resolver";
import ts from "typescript";
interface FileScope {
readonly flags: FileFlags;
readonly relPath: string;
readonly sourceFile: ts.SourceFile;
readonly targets: ReadonlyMap<string, string>;
}
const atomOf = function atomOf(expr: ts.Expression | undefined, sourceFile: ts.SourceFile): Atom | null {
if (expr === undefined) {
return null;
}
if (ts.isIdentifier(expr)) {
return { kind: "ident", text: expr.text };
}
if (ts.isStringLiteral(expr)) {
return { kind: "literal", text: expr.text };
}
return ts.isPropertyAccessExpression(expr) ? { kind: "ident", text: expr.getText(sourceFile) } : null;
};
const idArgOf = function idArgOf(expr: ts.Expression | undefined, sourceFile: ts.SourceFile): Atom | null {
const atom = atomOf(expr, sourceFile);
if (atom !== null || expr === undefined || !ts.isObjectLiteralExpression(expr)) {
return atom;
}
const found = expr.properties
.filter((prop): prop is ts.PropertyAssignment => ts.isPropertyAssignment(prop))
.filter((prop) => ts.isIdentifier(prop.name) && ID_PROPERTIES.has(prop.name.text))
.map((prop) => atomOf(prop.initializer, sourceFile))
.find((candidate) => candidate !== null);
return found ?? null;
};
const consumerOf = function consumerOf(node: ts.Node): ts.Node {
let current = node.parent;
while (ts.isAwaitExpression(current) || ts.isParenthesizedExpression(current)) {
current = current.parent;
}
return current;
};
const importDelta = function importDelta(file: string, from: string, names: readonly string[]): GraphDelta {
return names.length === 0 ? { sideEffectImports: [{ file, from }] } : { imports: [{ file, from, names }] };
};
const dynamicImportDelta = function dynamicImportDelta(node: ts.CallExpression, scope: FileScope): GraphDelta {
const specifier = node.arguments.at(0);
if (specifier === undefined || !ts.isStringLiteral(specifier)) {
return {};
}
const consumer = consumerOf(node);
const names = ts.isPropertyAccessExpression(consumer) ? [consumer.name.text] : [];
return importDelta(scope.relPath, resolveSelfImport(scope.targets, specifier.text, scope.relPath), names);
};
const callExpressionDelta = function callExpressionDelta(node: ts.CallExpression, scope: FileScope): GraphDelta {
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
return dynamicImportDelta(node, scope);
}
if (!ts.isIdentifier(node.expression)) {
return {};
}
const idArg = idArgOf(node.arguments[0], scope.sourceFile);
return callDelta({ file: scope.relPath, fn: node.expression.text, idArg }, idArg);
};
const fieldOf = function fieldOf(member: ts.TypeElement): FieldEntry[] {
return ts.isPropertySignature(member) && ts.isIdentifier(member.name)
? [{ name: member.name.text, optional: member.questionToken !== undefined }]
: [];
};
const isExported = function isExported(modifiers: ts.NodeArray<ts.ModifierLike> | undefined): boolean {
return modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true;
};
const variableDelta = function variableDelta(node: ts.VariableStatement, scope: FileScope): GraphDelta {
if (!isExported(node.modifiers)) {
return {};
}
const names = node.declarationList.declarations.flatMap((declaration) =>
ts.isIdentifier(declaration.name) ? [declaration.name.text] : [],
);
return exportDelta(names, scope.relPath, scope.flags);
};
const importedNames = function importedNames(clause: ts.ImportClause | undefined): string[] {
if (clause === undefined) {
return [];
}
const bindings = clause.namedBindings;
const named = bindings && ts.isNamedImports(bindings) ? bindings.elements : [];
return [
...named.map((element) => element.propertyName?.text ?? element.name.text),
...(clause.name ? [clause.name.text] : []),
];
};
const exportDeclarationDelta = function exportDeclarationDelta(
node: ts.ExportDeclaration,
scope: FileScope,
): GraphDelta {
if (!node.exportClause || !ts.isNamedExports(node.exportClause)) {
return {};
}
const { elements } = node.exportClause;
const exported = exportDelta(
elements.map((element) => element.name.text),
scope.relPath,
scope.flags,
);
const spec = node.moduleSpecifier;
if (spec === undefined || !ts.isStringLiteral(spec)) {
return exported;
}
const from = resolveSelfImport(scope.targets, spec.text, scope.relPath);
const names = elements.map((element) => element.propertyName?.text ?? element.name.text);
return { ...exported, imports: [{ file: scope.relPath, from, names }] };
};
const ownDelta = function ownDelta(node: ts.Node, scope: FileScope): GraphDelta {
if (ts.isCallExpression(node)) {
return callExpressionDelta(node, scope);
}
if (ts.isInterfaceDeclaration(node)) {
return { interfaces: [{ fields: node.members.flatMap(fieldOf), file: scope.relPath, name: node.name.text }] };
}
if (ts.isVariableStatement(node)) {
return variableDelta(node, scope);
}
if (ts.isFunctionDeclaration(node)) {
return isExported(node.modifiers) && node.name
? { exports: [{ file: scope.relPath, name: node.name.text }] }
: {};
}
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
const from = resolveSelfImport(scope.targets, node.moduleSpecifier.text, scope.relPath);
return importDelta(scope.relPath, from, importedNames(node.importClause));
}
return ts.isExportDeclaration(node) ? exportDeclarationDelta(node, scope) : {};
};
export const flagsOf = function flagsOf(relPath: string): FileFlags {
return {
isIcons: relPath.endsWith(ICONS_SUFFIX),
isIds: relPath.endsWith(IDS_SUFFIX),
isStrings: relPath.endsWith(STRINGS_SUFFIX),
};
};
export const fileDeltas = function fileDeltas(
filePath: string,
root: string,
targets: ReadonlyMap<string, string>,
): GraphDelta[] {
const sourceFile = ts.createSourceFile(filePath, readFileSync(filePath, "utf8"), ts.ScriptTarget.Latest, true);
const relPath = normalizePath(relative(root, filePath));
const scope: FileScope = { flags: flagsOf(relPath), relPath, sourceFile, targets };
const deltas: GraphDelta[] = [];
const visit = function visit(node: ts.Node): void {
deltas.push(ownDelta(node, scope));
ts.forEachChild(node, visit);
};
visit(sourceFile);
return deltas;
};