UNPKG

@ui5/linter

Version:

A static code analysis tool for UI5

128 lines 7.26 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, classNamespace, body] = extractInfoFromArguments(nodeFactory, callExp); className ??= nodeFactory.createUniqueName(extractedClassName); const classDecl = nodeFactory.createClassDeclaration(modifiers, className, undefined, [nodeFactory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [nodeFactory.createExpressionWithTypeArguments(callExp.expression.expression, undefined)])], body); if (classNamespace) { const jsDoc = `*\n * @namespace ${classNamespace}\n `; ts.addSyntheticLeadingComment(classDecl, ts.SyntaxKind.MultiLineCommentTrivia, jsDoc, true); } return classDecl; } function extractInfoFromArguments(nodeFactory, callExp) { const args = callExp.arguments; if (args.length === 0) { throw new UnsupportedExtendCall(`Missing arguments at ${toPosStr(callExp)}`); } const [className, classNamespace] = getClassNameFromArgument(args[0]); // Class body is optional const classBody = args.length > 1 ? getClassBodyFromArgument(nodeFactory, args[1]) : []; return [className, classNamespace, classBody]; } function getClassNameFromArgument(classNameNode) { if (!ts.isStringLiteralLike(classNameNode)) { throw new UnsupportedExtendCall(`Unexpected extends argument of type ${ts.SyntaxKind[classNameNode.kind]} at ` + toPosStr(classNameNode)); } // Just like OpenUI5's ObjectPath... const nameSegments = classNameNode.text.split("."); const className = nameSegments.pop(); const classNamespace = nameSegments.join("."); return [className, classNamespace]; } 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