UNPKG

@ui5/linter

Version:

A static code analysis tool for UI5

313 lines 14.9 kB
import ts from "typescript"; import path from "node:path/posix"; import jsonMap from "json-source-map"; import { MESSAGE } from "../messages.js"; import { getPropertyNameText } from "./utils/utils.js"; var AsyncPropertyStatus; (function (AsyncPropertyStatus) { AsyncPropertyStatus[AsyncPropertyStatus["parentPropNotSet"] = 0] = "parentPropNotSet"; AsyncPropertyStatus[AsyncPropertyStatus["propNotSet"] = 1] = "propNotSet"; AsyncPropertyStatus[AsyncPropertyStatus["false"] = 2] = "false"; AsyncPropertyStatus[AsyncPropertyStatus["true"] = 3] = "true"; })(AsyncPropertyStatus || (AsyncPropertyStatus = {})); ; export default function analyzeComponentJson({ classDeclaration, manifestContent, resourcePath, reporter, context, checker, isUiComponent, }) { // FIXME: This does a lot more than needed when we only check a Component (not a UIComponent) // but it requires some refactoring to only perform the "hasManifestDefinition" check in that case const analysisResult = findAsyncInterface({ classDeclaration, manifestContent, checker, isUiComponent, }); if (analysisResult) { reportComponentResults({ analysisResult, reporter, classDeclaration, manifestContent }); if (isUiComponent) { reportUiComponentResults({ analysisResult, context, reporter, resourcePath, classDeclaration, manifestContent }); } } } function getHighestPropertyStatus(aProp, bProp) { return aProp > bProp ? aProp : bProp; } ; function mergeAsyncFlags(a, b) { return { hasManifestDefinition: a.hasManifestDefinition || b.hasManifestDefinition, routingAsyncFlag: getHighestPropertyStatus(a.routingAsyncFlag, b.routingAsyncFlag), rootViewAsyncFlag: getHighestPropertyStatus(a.rootViewAsyncFlag, b.rootViewAsyncFlag), hasAsyncInterface: a.hasAsyncInterface || b.hasAsyncInterface, }; } /** * Search for the async interface in the class hierarchy */ function findAsyncInterface({ classDeclaration, manifestContent, checker, isUiComponent }) { const returnTypeTemplate = { hasAsyncInterface: false, routingAsyncFlag: AsyncPropertyStatus.parentPropNotSet, rootViewAsyncFlag: AsyncPropertyStatus.parentPropNotSet, hasManifestDefinition: false, }; // Checks the interfaces and manifest of the class const curClassAnalysis = classDeclaration.members.reduce((acc, member) => { const checkResult = doPropsCheck(member, manifestContent); return mergeAsyncFlags(acc, checkResult); }, { ...returnTypeTemplate }); const heritageAnalysis = classDeclaration?.heritageClauses?.flatMap((parentClasses) => { return parentClasses.types.flatMap((parentClass) => { const parentClassType = checker.getTypeAtLocation(parentClass); return parentClassType.symbol?.declarations?.flatMap((declaration) => { let result = { ...returnTypeTemplate }; // Continue down the heritage chain to search for // the async interface or manifest flags if (ts.isClassDeclaration(declaration)) { result = findAsyncInterface({ classDeclaration: declaration, // We are unable to dynamically search for a parent-component's manifest.json manifestContent: undefined, checker, isUiComponent, }) ?? result; } else if (ts.isInterfaceDeclaration(declaration)) { result.hasAsyncInterface = doAsyncInterfaceChecks(parentClass) ?? result.hasAsyncInterface; } return result; }); }); }) ?? []; return [...heritageAnalysis, curClassAnalysis].reduce((acc, curAnalysis) => { return mergeAsyncFlags(acc ?? { ...returnTypeTemplate }, curAnalysis ?? { ...returnTypeTemplate }); }); } function isCoreImportDeclaration(statement) { return ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === "sap/ui/core/library"; } function doAsyncInterfaceChecks(importDeclaration) { const sourceFile = importDeclaration.getSourceFile(); let coreLibImports; if (sourceFile.isDeclarationFile) { let moduleDeclaration; while (!moduleDeclaration && importDeclaration.kind !== ts.SyntaxKind.SourceFile) { if (ts.isModuleDeclaration(importDeclaration)) { moduleDeclaration = importDeclaration; } else { importDeclaration = importDeclaration.parent; } } if (moduleDeclaration?.body?.kind === ts.SyntaxKind.ModuleBlock) { coreLibImports = moduleDeclaration.body.statements.filter(isCoreImportDeclaration); } } else { coreLibImports = sourceFile.statements.filter(isCoreImportDeclaration); } if (!coreLibImports) { return false; } const hasAsyncImport = coreLibImports.some((importDecl) => { const importClause = importDecl.importClause; if (!importClause) { return; } if (!importClause.namedBindings) { // Example: import "sap/ui/core/library"; or import library from "sap/ui/core/library"; } else if (ts.isNamedImports(importClause.namedBindings)) { // Example: import { IAsyncContentCreation } from "sap/ui/core/library"; return importClause.namedBindings.elements.some((namedImport) => namedImport.getText() === "IAsyncContentCreation"); } else { // Example: import * as library from "sap/ui/core/library"; // TODO: This requires additional handling } }); return hasAsyncImport; } function doPropsCheck(metadata, manifestContent) { let classInterfaces; let componentManifest; if (metadata && ts.isPropertyDeclaration(metadata) && metadata.initializer && ts.isObjectLiteralExpression(metadata.initializer)) { metadata.initializer.properties.forEach((prop) => { if (!prop.name) { return; } const propText = getPropertyNameText(prop.name); if (propText === "interfaces") { classInterfaces = prop; } else if (propText === "manifest") { componentManifest = prop; } }); } let hasAsyncInterface = false; if (classInterfaces && ts.isPropertyAssignment(classInterfaces) && classInterfaces.initializer && ts.isArrayLiteralExpression(classInterfaces.initializer)) { hasAsyncInterface = classInterfaces.initializer .elements.some((implementedInterface) => { return ts.isStringLiteralLike(implementedInterface) && implementedInterface.text === "sap.ui.core.IAsyncContentCreation"; }); } let rootViewAsyncFlag = AsyncPropertyStatus.parentPropNotSet; let routingAsyncFlag = AsyncPropertyStatus.parentPropNotSet; let hasManifestDefinition = false; if (componentManifest && ts.isPropertyAssignment(componentManifest) && ts.isObjectLiteralExpression(componentManifest.initializer)) { /* eslint-disable @typescript-eslint/no-explicit-any */ const instanceOfPropsRecord = (obj) => { return !!obj && typeof obj === "object"; }; hasManifestDefinition = true; const manifestJson = extractPropsRecursive(componentManifest.initializer) ?? {}; let manifestSapui5Section; if (instanceOfPropsRecord(manifestJson["sap.ui5"])) { manifestSapui5Section = manifestJson["sap.ui5"].value; } if (instanceOfPropsRecord(manifestSapui5Section) && instanceOfPropsRecord(manifestSapui5Section?.rootView?.value)) { rootViewAsyncFlag = AsyncPropertyStatus.propNotSet; if (typeof manifestSapui5Section?.rootView?.value.async?.value === "boolean") { const isRootViewAsync = manifestSapui5Section?.rootView?.value.async?.value; rootViewAsyncFlag = isRootViewAsync ? AsyncPropertyStatus.true : AsyncPropertyStatus.false; } } if (instanceOfPropsRecord(manifestSapui5Section) && instanceOfPropsRecord(manifestSapui5Section?.routing?.value)) { routingAsyncFlag = AsyncPropertyStatus.propNotSet; if (instanceOfPropsRecord(manifestSapui5Section?.routing?.value.config?.value) && typeof manifestSapui5Section?.routing?.value.config?.value.async?.value === "boolean") { const isRoutingAsync = manifestSapui5Section?.routing?.value.config?.value.async?.value; routingAsyncFlag = isRoutingAsync ? AsyncPropertyStatus.true : AsyncPropertyStatus.false; } } } else if (manifestContent) { const parsedManifestContent = JSON.parse(manifestContent); const { rootView, routing } = parsedManifestContent["sap.ui5"] ?? {}; if (rootView) { rootViewAsyncFlag = AsyncPropertyStatus.propNotSet; // @ts-expect-error async is part of RootViewDefFlexEnabled and RootViewDef const isRootViewAsync = rootView.async; if (typeof isRootViewAsync === "boolean") { rootViewAsyncFlag = isRootViewAsync ? AsyncPropertyStatus.true : AsyncPropertyStatus.false; } } if (routing) { routingAsyncFlag = AsyncPropertyStatus.propNotSet; const isRoutingAsync = routing?.config?.async; if (typeof isRoutingAsync === "boolean") { routingAsyncFlag = isRoutingAsync ? AsyncPropertyStatus.true : AsyncPropertyStatus.false; } } hasManifestDefinition = !!(componentManifest && ts.isPropertyAssignment(componentManifest) && ts.isPropertyName(componentManifest.initializer) && getPropertyNameText(componentManifest.initializer) === "json"); } return { routingAsyncFlag, rootViewAsyncFlag, hasAsyncInterface, hasManifestDefinition, }; } function extractPropsRecursive(node) { const properties = Object.create(null); node.properties?.forEach((prop) => { if (!ts.isPropertyAssignment(prop)) { return; } const key = getPropertyNameText(prop.name); if (!key) { return; } if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) { properties[key] = { value: true, node: prop.initializer }; } else if (prop.initializer.kind === ts.SyntaxKind.FalseKeyword) { properties[key] = { value: false, node: prop.initializer }; } else if (prop.initializer.kind === ts.SyntaxKind.NullKeyword) { properties[key] = { value: null, node: prop.initializer }; } else if (ts.isObjectLiteralExpression(prop.initializer) && prop.initializer.properties) { properties[key] = { value: extractPropsRecursive(prop.initializer), node: prop.initializer }; } else if (ts.isArrayLiteralExpression(prop.initializer)) { const resolvedValue = prop.initializer.elements.map((elem) => { if (!ts.isObjectLiteralExpression(elem)) { return; } return extractPropsRecursive(elem); }).filter(($) => $); properties[key] = { value: resolvedValue, node: prop.initializer }; } else if (ts.isStringLiteralLike(prop.initializer) || ts.isNumericLiteral(prop.initializer)) { properties[key] = { value: prop.initializer.text, node: prop.initializer }; } else if (ts.isIdentifier(prop.initializer) || ts.isPrivateIdentifier(prop.initializer)) { properties[key] = { value: prop.initializer.getText(), node: prop.initializer }; } else { // throw new Error("Unhandled property assignment"); } }); return properties; } function reportComponentResults({ analysisResult, reporter, classDeclaration, manifestContent, }) { if (!analysisResult.hasManifestDefinition && !!manifestContent) { reporter.addMessage(MESSAGE.COMPONENT_MISSING_MANIFEST_DECLARATION, null, { node: classDeclaration }); } } function reportUiComponentResults({ analysisResult, reporter, classDeclaration, manifestContent, resourcePath, context, }) { const { hasAsyncInterface, routingAsyncFlag, rootViewAsyncFlag } = analysisResult; const componentFileName = path.basename(resourcePath); if (hasAsyncInterface !== true) { if ([AsyncPropertyStatus.propNotSet, AsyncPropertyStatus.false].includes(rootViewAsyncFlag) || [AsyncPropertyStatus.propNotSet, AsyncPropertyStatus.false].includes(routingAsyncFlag)) { let asyncFlagMissingIn; if (AsyncPropertyStatus.parentPropNotSet === rootViewAsyncFlag) { // sap.ui5/rootView is not set at all, so skip it in the message asyncFlagMissingIn = `"sap.ui5/routing/config"`; } else if (AsyncPropertyStatus.parentPropNotSet === routingAsyncFlag) { // sap.ui5/routing/config is not set at all, so skip it in the message asyncFlagMissingIn = `"sap.ui5/rootView"`; } else { asyncFlagMissingIn = `"sap.ui5/routing/config" and "sap.ui5/rootView"`; } reporter.addMessage(MESSAGE.COMPONENT_MISSING_ASYNC_INTERFACE, { componentFileName, asyncFlagMissingIn, }, { node: classDeclaration }); } } else { const { pointers } = jsonMap.parse(manifestContent ?? "{}"); const report = (pointerKey) => { if (manifestContent) { // If the manifest.json is present, then we need to redirect the message pointers to it const { key: posInfo } = pointers[pointerKey]; context.addLintingMessage(resourcePath.replace(componentFileName, "manifest.json"), MESSAGE.COMPONENT_REDUNDANT_ASYNC_FLAG, { asyncFlagLocation: pointerKey }, posInfo); } else { reporter.addMessage(MESSAGE.COMPONENT_REDUNDANT_ASYNC_FLAG, { asyncFlagLocation: pointerKey, }, { node: classDeclaration }); } }; if (rootViewAsyncFlag === AsyncPropertyStatus.true) { report("/sap.ui5/rootView/async"); } if (routingAsyncFlag === AsyncPropertyStatus.true) { report("/sap.ui5/routing/config/async"); } } } //# sourceMappingURL=asyncComponentFlags.js.map