UNPKG

@ui5/linter

Version:

A static code analysis tool for UI5

121 lines 6.88 kB
import ts from "typescript"; import { toPosStr } from "./util.js"; import { getPropertyNameText } from "../utils/utils.js"; export class UnsupportedExtendCall extends Error { constructor(message) { super(message); this.name = this.constructor.name; } } /** * Rewrite a UI5-typical `Class.extend("MyClass", {})` CallExpression to a ClassDeclaration */ export default function rewriteExtendCall(nodeFactory, callExp, modifiers, className) { if (!(ts.isPropertyAccessExpression(callExp.expression) && ts.isIdentifier(callExp.expression.name) && callExp.expression.name.text === "extend")) { return undefined; } const [extractedClassName, body] = extractInfoFromArguments(nodeFactory, callExp); className ??= nodeFactory.createUniqueName(extractedClassName); return nodeFactory.createClassDeclaration(modifiers, className, undefined, [nodeFactory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [nodeFactory.createExpressionWithTypeArguments(callExp.expression.expression, undefined)])], body); } function extractInfoFromArguments(nodeFactory, callExp) { const args = callExp.arguments; if (args.length === 0) { throw new UnsupportedExtendCall(`Missing arguments at ${toPosStr(callExp)}`); } const className = getClassNameFromArgument(args[0]); // Class body is optional const classBody = args.length > 1 ? getClassBodyFromArgument(nodeFactory, args[1]) : []; return [className, classBody]; } function getClassNameFromArgument(className) { if (!ts.isStringLiteralLike(className)) { throw new UnsupportedExtendCall(`Unexpected extends argument of type ${ts.SyntaxKind[className.kind]} at ` + toPosStr(className)); } // Just like OpenUI5's ObjectPath... const nameSegments = className.text.split("."); return nameSegments[nameSegments.length - 1]; } function getClassBodyFromArgument(nodeFactory, classBody) { if (!ts.isObjectLiteralExpression(classBody)) { throw new UnsupportedExtendCall(`Unexpected extends argument of type ${ts.SyntaxKind[classBody.kind]} at ` + toPosStr(classBody)); } if (classBody.properties.find((prop) => ts.isSpreadAssignment(prop))) { // TODO: Support spread elements(?) throw new UnsupportedExtendCall(`Unsupported spread element in extends call body`); } return classBody.properties.map((prop) => { if (ts.isShorthandPropertyAssignment(prop)) { // When a property is declared as a shorthand, create a property declaration to itself. const staticModifier = (ts.isIdentifier(prop.name) && ["renderer", "metadata"].includes(prop.name.text)) ? [nodeFactory.createToken(ts.SyntaxKind.StaticKeyword)] : []; return nodeFactory.createPropertyDeclaration(staticModifier, prop.name, undefined, undefined, prop.name); } else if (ts.isMethodDeclaration(prop)) { // Use method declarations as-is // e.g. "method() {}" // Special handling: // - renderer: *static* // This aligns it with how UI5 projects should declare those properties in TypeScript if (ts.isIdentifier(prop.name) && prop.name.text === "renderer" && !prop.modifiers?.find((mod) => mod.kind === ts.SyntaxKind.StaticKeyword)) { // Add static modifier to renderer method const staticModifier = nodeFactory.createToken(ts.SyntaxKind.StaticKeyword); return nodeFactory.updateMethodDeclaration(prop, prop.modifiers ? [...prop.modifiers, staticModifier] : [staticModifier], prop.asteriskToken, prop.name, prop.questionToken, prop.typeParameters, prop.parameters, prop.type, prop.body); } return prop; } else if (ts.isPropertyAssignment(prop)) { if (ts.isFunctionExpression(prop.initializer)) { let modifiers; // Special handling: // - renderer: *static* // This aligns it with how UI5 projects should declare those properties in TypeScript if (ts.isIdentifier(prop.name) && prop.name.text === "renderer" && !prop.initializer.modifiers?.find((mod) => mod.kind === ts.SyntaxKind.StaticKeyword)) { modifiers = [nodeFactory.createToken(ts.SyntaxKind.StaticKeyword)]; } return nodeFactory.createMethodDeclaration(modifiers, undefined, prop.name, undefined, undefined, prop.initializer.parameters, undefined, prop.initializer.body); } else { const modifiers = []; const propertyName = getPropertyNameText(prop.name); // Special handling: // - metadata: *readonly static* // - renderer: *static* // This aligns it with how UI5 projects should declare those properties in TypeScript if (ts.isObjectLiteralExpression(prop.initializer) && propertyName === "metadata") { modifiers.push(nodeFactory.createToken(ts.SyntaxKind.ReadonlyKeyword)); modifiers.push(nodeFactory.createToken(ts.SyntaxKind.StaticKeyword)); } else if (propertyName === "renderer") { modifiers.push(nodeFactory.createToken(ts.SyntaxKind.StaticKeyword)); } else if (prop.initializer.kind === ts.SyntaxKind.NullKeyword || prop.initializer.kind === ts.SyntaxKind.UndefinedKeyword) { // Skip property assignments that declare a null value, in the hope // that tsc can infer a more useful type based on other assignment // in one of the methods. If we would define a class property, tsc // would not attempt to infer more type information. return; } // Assign all properties (including arrow functions) to the class prototype // This transformation does not reflect the runtime behavior where // properties are set on the Class's prototype. However, tsc won't derive *any* // type information from Object.defineProperty(Class.prototype, "prob", ...) // So the current approach works better for the static analysis return nodeFactory.createPropertyDeclaration(modifiers, prop.name, undefined, undefined, prop.initializer); } } else { throw new UnsupportedExtendCall(`While generating class body: Unexpected property type ${ts.SyntaxKind[prop.kind]} at ` + toPosStr(prop)); } }).filter((_) => _); } //# sourceMappingURL=rewriteExtendCall.js.map