@ui5/linter
Version:
A static code analysis tool for UI5
299 lines • 14.4 kB
JavaScript
import ts from "typescript";
import { getLogger } from "@ui5/logger";
import rewriteExtendCall, { UnsupportedExtendCall } from "./rewriteExtendCall.js";
import { UnsupportedModuleError, toPosStr } from "./util.js";
import pruneNode from "./pruneNode.js";
import { resolveUniqueName } from "../utils/utils.js";
const { SyntaxKind } = ts;
const log = getLogger("linter:ui5Types:amdTranspiler:moduleDeclarationToDefinition");
export default function (moduleDeclaration, sourceFile, nodeFactory) {
const { imports, identifiers: importIdentifiers } = collectImports(moduleDeclaration, nodeFactory);
const { body, oldFactoryBlock, moveComments } = getModuleBody(moduleDeclaration, sourceFile, nodeFactory, importIdentifiers);
/* Ignore module name and export flag for now */
return {
imports,
body,
oldFactoryBlock,
moveComments,
};
}
/** Convert dependencies declaration to import statements:
* a) If dependencies is an ArrayExpression, extract the values and create import statements.
* If factory is a Function extract the names of the parameters and use them for the imports.
* If no parameter is found, assume it's a side effect import.
* If factory is an Identifier, derive import identifier names from the import module name for later usage
* b) If dependencies is an Identifier, abort with an error.
*/
function collectImports(moduleDeclaration, nodeFactory) {
const imports = [];
const identifiers = [];
if (!moduleDeclaration.dependencies) {
// No dependencies declared
return { imports, identifiers };
}
let factoryParams;
let factoryRequiresCallWrapper = false;
if (moduleDeclaration.factory) {
if (ts.isFunctionExpression(moduleDeclaration.factory) ||
ts.isArrowFunction(moduleDeclaration.factory) ||
ts.isFunctionDeclaration(moduleDeclaration.factory)) {
// Extract parameter names
factoryParams = moduleDeclaration.factory.parameters.map((param) => {
if (!ts.isIdentifier(param.name)) {
// Indicates destructuring in factory signature. This is not (yet?) supported
throw new UnsupportedModuleError(`Unexpected parameter type ${ts.SyntaxKind[param.kind]} ` +
`at ${toPosStr(param)}`);
}
return param.name;
});
}
else if (ts.isIdentifier(moduleDeclaration.factory)) {
factoryRequiresCallWrapper = true;
}
}
if (ts.isIdentifier(moduleDeclaration.dependencies)) {
throw new UnsupportedModuleError(`Unable to determine dependencies for module. Can't parse variable at ` +
`${toPosStr(moduleDeclaration.dependencies)}`);
}
// Create import statements based on factory parameters
moduleDeclaration.dependencies.elements.forEach((dep, i) => {
// Create import statements
let moduleSpecifier;
if (!ts.isStringLiteralLike(dep)) {
log.verbose(`Skipping non-string dependency entry of type ${ts.SyntaxKind[dep.kind]} at ` +
toPosStr(dep));
return;
}
if (ts.isNoSubstitutionTemplateLiteral(dep)) {
moduleSpecifier = nodeFactory.createStringLiteral(dep.text);
// Set range to the original range to preserve source mapping capability
ts.setTextRange(moduleSpecifier, dep);
}
else {
moduleSpecifier = dep;
}
let identifier;
if (factoryRequiresCallWrapper) {
// Generate variable name based on import module
// Later this variable will be used to call the factory function
identifier = nodeFactory.createUniqueName(resolveUniqueName(dep.text));
}
else if (factoryParams?.[i]) {
// Use factory parameter identifier as import identifier
identifier = factoryParams[i];
} // else: Side effect imports. No identifier needed
let importClause;
if (identifier) {
identifiers.push(identifier);
importClause = nodeFactory.createImportClause(false, identifier, undefined);
}
imports.push(nodeFactory.createImportDeclaration(undefined, importClause, moduleSpecifier));
});
pruneNode(moduleDeclaration.dependencies);
return { imports, identifiers };
}
/** Convert factory to module body:
* a) If factory is a FunctionExpression or ArrowFunctionExpression, extract the body and create default
* export for it's return value
* b) If factory is any other kind of _value_, use it directly
* c) If factory is an Identifier, create a function and call it with all dependencies
*
* Then create default export:
* a) If factory body contains a single return statement, use its argument for the default export
* b) If factory body contains multiple return statements, wrap the whole body
* in a function and use that for the default export
*/
function getModuleBody(moduleDeclaration, sourceFile, nodeFactory, importIdentifiers) {
if (!moduleDeclaration.factory) {
return { body: [], moveComments: [] };
}
let oldFactoryBlock;
let body;
const moveComments = [];
if ((ts.isFunctionExpression(moduleDeclaration.factory) ||
ts.isArrowFunction(moduleDeclaration.factory) ||
ts.isFunctionDeclaration(moduleDeclaration.factory))) {
if (!moduleDeclaration.factory.body) {
// Empty function body, no export
body = [];
}
else if (ts.isBlock(moduleDeclaration.factory.body)) {
const factoryBody = moduleDeclaration.factory.body.statements;
/* Convert factory to module body:
a) If body contains a single return statement, add all nodes to body but wrap
the return statement's argument with a default export
b) If body contains multiple return statements, wrap body in an iife
use that for the body, wrapped in a default export declaration
*/
const returnStatements = collectReturnStatementsInScope(moduleDeclaration.factory, 2);
if (returnStatements.length > 1) {
log.verbose(`Multiple return statements found in factory at ${toPosStr(moduleDeclaration.factory)}`);
let factoryCall;
if (ts.isFunctionExpression(moduleDeclaration.factory) ||
ts.isArrowFunction(moduleDeclaration.factory)) {
// Wrap factory in iife
factoryCall = nodeFactory.createCallExpression(moduleDeclaration.factory, undefined, importIdentifiers);
}
else if (ts.isFunctionDeclaration(moduleDeclaration.factory) && moduleDeclaration.factory.name) {
// Call the declaration
factoryCall = nodeFactory.createCallExpression(moduleDeclaration.factory.name, undefined, importIdentifiers);
}
else {
throw new UnsupportedModuleError(`Unable to call factory of type ${ts.SyntaxKind[moduleDeclaration.factory.kind]} at ` +
toPosStr(moduleDeclaration.factory));
}
body = [createDefaultExport(nodeFactory, factoryCall)];
}
else {
oldFactoryBlock = moduleDeclaration.factory.body;
body = [];
// One return statement in scope
for (const node of factoryBody) {
if (ts.isReturnStatement(node) && node.expression) {
if (ts.isCallExpression(node.expression)) {
let classDeclaration;
try {
classDeclaration = rewriteExtendCall(nodeFactory, node.expression, [
nodeFactory.createToken(ts.SyntaxKind.ExportKeyword),
nodeFactory.createToken(ts.SyntaxKind.DefaultKeyword),
]);
}
catch (err) {
if (err instanceof UnsupportedExtendCall) {
log.verbose(`Failed to transform extend call: ${err.message}`);
}
else {
throw err;
}
}
if (classDeclaration) {
body.push(classDeclaration);
moveComments.push([node, classDeclaration]);
}
else {
body.push(createDefaultExport(nodeFactory, node.expression));
}
}
else {
const defaultExport = createDefaultExport(nodeFactory, node.expression);
body.push(defaultExport);
moveComments.push([node, defaultExport]);
// body.push(factory.createExportAssignment(undefined, undefined,
// node.expression));
}
}
else if (ts.isExpressionStatement(node) && ts.isStringLiteral(node.expression) &&
node.expression.text === "use strict") {
// Ignore "use strict" directive
continue;
}
else {
body.push(node);
}
}
pruneNode(moduleDeclaration.factory);
}
}
else if (ts.isCallExpression(moduleDeclaration.factory.body)) {
// Arrow function with expression body
let classDeclaration;
try {
classDeclaration = rewriteExtendCall(nodeFactory, moduleDeclaration.factory.body, [
nodeFactory.createToken(ts.SyntaxKind.ExportKeyword),
nodeFactory.createToken(ts.SyntaxKind.DefaultKeyword),
]);
}
catch (err) {
if (err instanceof UnsupportedExtendCall) {
log.verbose(`Failed to transform extend call: ${err.message}`);
}
else {
throw err;
}
}
if (classDeclaration) {
body = [classDeclaration];
}
else {
// Export expression directly
body = [createDefaultExport(nodeFactory, moduleDeclaration.factory.body)];
}
}
else {
// Export expression directly
body = [createDefaultExport(nodeFactory, moduleDeclaration.factory.body)];
}
}
else if (ts.isClassDeclaration(moduleDeclaration.factory) ||
ts.isLiteralExpression(moduleDeclaration.factory) ||
ts.isArrayLiteralExpression(moduleDeclaration.factory) ||
ts.isObjectLiteralExpression(moduleDeclaration.factory) ||
ts.isPropertyAccessExpression(moduleDeclaration.factory)) {
// Use factory directly
body = [createDefaultExport(nodeFactory, moduleDeclaration.factory)];
}
else { // Identifier
throw new Error(`FIXME: Unsupported factory type ${ts.SyntaxKind[moduleDeclaration.factory.kind]} at ` +
toPosStr(moduleDeclaration.factory));
}
return { body, oldFactoryBlock, moveComments };
}
/**
* Collect all return statements in the provided node's function scope
*/
function collectReturnStatementsInScope(node, maxCount) {
const returnStatements = [];
function visitNode(node) {
switch (node.kind) {
case SyntaxKind.ReturnStatement:
returnStatements.push(node);
return;
// Do not traverse into nodes that declare a new function scope
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.FunctionExpression:
case SyntaxKind.ArrowFunction:
case SyntaxKind.MethodDeclaration:
case SyntaxKind.ModuleDeclaration:
case SyntaxKind.Constructor:
case SyntaxKind.SetAccessor:
case SyntaxKind.GetAccessor:
return;
}
if (maxCount && returnStatements.length >= maxCount) {
return;
}
ts.forEachChild(node, visitNode);
}
ts.forEachChild(node, visitNode);
return returnStatements;
}
function createDefaultExport(factory, node) {
if (ts.isLiteralExpression(node)) {
// Use factory directly
return factory.createExportAssignment(undefined, undefined, node);
}
const exportModifiers = [
factory.createToken(ts.SyntaxKind.ExportKeyword),
factory.createToken(ts.SyntaxKind.DefaultKeyword),
];
switch (node.kind) {
case SyntaxKind.CallExpression:
case SyntaxKind.ArrayLiteralExpression:
case SyntaxKind.ObjectLiteralExpression:
case SyntaxKind.ArrowFunction:
case SyntaxKind.Identifier:
case SyntaxKind.PropertyAccessExpression:
case SyntaxKind.NewExpression:
return factory.createExportAssignment(undefined, undefined, node);
case SyntaxKind.ClassDeclaration:
return factory.updateClassDeclaration(node, exportModifiers, node.name, node.typeParameters, node.heritageClauses, node.members);
case SyntaxKind.FunctionDeclaration:
return factory.updateFunctionDeclaration(node, exportModifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, node.body);
case SyntaxKind.FunctionExpression:
return factory.createFunctionDeclaration(exportModifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, node.body);
default:
throw new UnsupportedModuleError(`Unable to create default export assignment for node of type ${SyntaxKind[node.kind]} at ` +
toPosStr(node));
}
}
//# sourceMappingURL=moduleDeclarationToDefinition.js.map