From 1c5810593abf227a88a908c9d17aa9eacf113f10 Mon Sep 17 00:00:00 2001 From: Yavor Ivanov Date: Wed, 29 May 2024 10:31:57 +0300 Subject: [PATCH] feat: Component best practices- async flags check (#73) JIRA: CPOUI5FOUNDATION-792 --------- Co-authored-by: Merlin Beutlberger --- README.md | 4 +- package.json | 2 +- resources/api-extract.json | 2 +- src/linter/LinterContext.ts | 5 + src/linter/linter.ts | 2 +- src/linter/ui5Types/SourceFileLinter.ts | 22 +- src/linter/ui5Types/TypeLinter.ts | 12 +- src/linter/ui5Types/asyncComponentFlags.ts | 414 +++++++++++++++ .../Negative_01/Component.js | 12 + .../Negative_01/manifest.json | 45 ++ .../Negative_02/Component.js | 11 + .../Negative_02/manifest.json | 47 ++ .../Negative_03/Component.js | 56 ++ .../Negative_04/Component.js | 57 +++ .../Negative_05/Component.js | 11 + .../Negative_05/manifest.json | 47 ++ .../Negative_05/subdir/ParentComponent.js | 6 + .../Negative_06/Component.js | 10 + .../Negative_06/subdir/ParentComponent.js | 53 ++ .../Negative_07/Component.js | 10 + .../Negative_07/manifest.json | 45 ++ .../Negative_08/Component.js | 10 + .../Negative_08/manifest.json | 14 + .../Negative_09/Component.ts | 9 + .../Negative_09/manifest.json | 45 ++ .../Negative_09/subdir/ParentComponent.ts | 6 + .../Negative_10/Component.ts | 10 + .../Negative_10/manifest.json | 45 ++ .../Negative_10/subdir/ParentComponent.ts | 6 + .../Positive_01/Component.js | 11 + .../Positive_01/manifest.json | 45 ++ .../Positive_02/Component.js | 12 + .../Positive_02/manifest.json | 47 ++ .../Positive_03/Component.js | 55 ++ .../Positive_04/Component.js | 58 +++ .../Positive_05/Component.js | 11 + .../Positive_05/subdir/ParentComponent.js | 18 + .../Positive_06/Component.js | 12 + .../Positive_06/manifest.json | 46 ++ .../Positive_07/Component.js | 11 + .../Positive_07/manifest.json | 22 + .../Positive_08/Component.js | 11 + .../Positive_08/manifest.json | 39 ++ .../Positive_09/Component.js | 12 + .../Positive_09/manifest.json | 45 ++ .../Positive_10/Component.js | 10 + .../Positive_10/manifest.json | 45 ++ .../Positive_11/Component.ts | 9 + .../Positive_11/manifest.json | 45 ++ .../AsyncComponentFlags/test-overview.md | 20 + test/lib/linter/_linterHelper.ts | 148 ++++-- test/lib/linter/rules/AsyncComponentFlags.ts | 18 + test/lib/linter/rules/CSPCompliance.ts | 9 +- test/lib/linter/rules/NoGlobals.ts | 9 +- test/lib/linter/rules/NoPseudoModules.ts | 9 +- .../rules/snapshots/AsyncComponentFlags.ts.md | 483 ++++++++++++++++++ .../snapshots/AsyncComponentFlags.ts.snap | Bin 0 -> 1902 bytes .../rules/snapshots/NoDeprecatedApi.ts.md | 2 +- .../rules/snapshots/NoDeprecatedApi.ts.snap | Bin 6736 -> 6729 bytes .../xmlTemplate/snapshots/transpiler.ts.md | 2 +- .../xmlTemplate/snapshots/transpiler.ts.snap | Bin 5366 -> 5388 bytes 61 files changed, 2201 insertions(+), 81 deletions(-) create mode 100644 src/linter/ui5Types/asyncComponentFlags.ts create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_01/Component.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_01/manifest.json create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_02/Component.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_02/manifest.json create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_03/Component.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_04/Component.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_05/Component.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_05/manifest.json create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_05/subdir/ParentComponent.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_06/Component.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_06/subdir/ParentComponent.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_07/Component.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_07/manifest.json create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_08/Component.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_08/manifest.json create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_09/Component.ts create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_09/manifest.json create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_09/subdir/ParentComponent.ts create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_10/Component.ts create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_10/manifest.json create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Negative_10/subdir/ParentComponent.ts create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_01/Component.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_01/manifest.json create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_02/Component.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_02/manifest.json create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_03/Component.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_04/Component.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_05/Component.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_05/subdir/ParentComponent.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_06/Component.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_06/manifest.json create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_07/Component.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_07/manifest.json create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_08/Component.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_08/manifest.json create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_09/Component.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_09/manifest.json create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_10/Component.js create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_10/manifest.json create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_11/Component.ts create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/Positive_11/manifest.json create mode 100644 test/fixtures/linter/rules/AsyncComponentFlags/test-overview.md create mode 100644 test/lib/linter/rules/AsyncComponentFlags.ts create mode 100644 test/lib/linter/rules/snapshots/AsyncComponentFlags.ts.md create mode 100644 test/lib/linter/rules/snapshots/AsyncComponentFlags.ts.snap diff --git a/README.md b/README.md index 901529a7..ddc35017 100644 --- a/README.md +++ b/README.md @@ -106,11 +106,11 @@ The extracted and generated metadata is stored within the repository under the ` Regular updates to the metadata are necessary to ensure that the data is compatible with the corresponding UI5 type definitions. ```sh -npm run update-pseudo-modules-info -- $DOMAIN_NAME/com/sap/ui5/dist/sapui5-sdk-dist/1.120.12/sapui5-sdk-dist-1.120.12-api-jsons.zip 1.120.12 +npm run update-pseudo-modules-info -- $DOMAIN_NAME/com/sap/ui5/dist/sapui5-sdk-dist/1.120.13/sapui5-sdk-dist-1.120.13-api-jsons.zip 1.120.13 ``` ```sh -npm run update-semantic-model-info -- $DOMAIN_NAME/com/sap/ui5/dist/sapui5-sdk-dist/1.120.12/sapui5-sdk-dist-1.120.12-api-jsons.zip 1.120.12 +npm run update-semantic-model-info -- $DOMAIN_NAME/com/sap/ui5/dist/sapui5-sdk-dist/1.120.13/sapui5-sdk-dist-1.120.13-api-jsons.zip 1.120.13 ``` diff --git a/package.json b/package.json index 293c1bb8..59c9112b 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "check-licenses": "licensee --errors-only", "cleanup": "rimraf lib coverage", "coverage": "nyc ava --node-arguments=\"--experimental-loader=@istanbuljs/esm-loader-hook\"", - "depcheck": "depcheck --ignores @commitlint/config-conventional,@istanbuljs/esm-loader-hook,@sapui5/types,@ui5/logger,ava,rimraf,sap,tsx,json-source-map,he,@types/he", + "depcheck": "depcheck --ignores @commitlint/config-conventional,@istanbuljs/esm-loader-hook,@sapui5/types,@ui5/logger,ava,rimraf,sap,tsx,json-source-map,he,@types/he,mycomp", "hooks:pre-push": "npm run lint:commit", "lint": "eslint .", "lint:commit": "commitlint -e", diff --git a/resources/api-extract.json b/resources/api-extract.json index 236c5ee0..dd0177b2 100644 --- a/resources/api-extract.json +++ b/resources/api-extract.json @@ -1,7 +1,7 @@ { "framework": { "name": "SAPUI5", - "version": "1.120.12" + "version": "1.120.13" }, "defaultAggregations": { "sap.ca.ui.CustomerControlListItem": "content", diff --git a/src/linter/LinterContext.ts b/src/linter/LinterContext.ts index dbdcdc68..afef8f21 100644 --- a/src/linter/LinterContext.ts +++ b/src/linter/LinterContext.ts @@ -1,5 +1,6 @@ import {AbstractAdapter, AbstractReader} from "@ui5/fs"; import {createReader} from "@ui5/fs/resourceFactory"; +import {resolveLinks} from "../formatter/lib/resolveLinks.js"; export type FilePath = string; // Platform-dependent path export type ResourcePath = string; // Always POSIX @@ -156,6 +157,10 @@ export default class LinterContext { } addLintingMessage(resourcePath: ResourcePath, message: LintMessage) { + if (message.messageDetails) { + message.messageDetails = resolveLinks(message.messageDetails); + } + this.getLintingMessages(resourcePath).push(message); } diff --git a/src/linter/linter.ts b/src/linter/linter.ts index 87692b5d..13c51523 100644 --- a/src/linter/linter.ts +++ b/src/linter/linter.ts @@ -89,7 +89,7 @@ export async function lintFile({ }: LinterOptions): Promise { const reader = createReader({ fsBasePath: rootDir, - virBasePath: "/", + virBasePath: namespace ? `/resources/${namespace}/` : "/", }); let resolvedFilePaths; if (pathsToLint?.length) { diff --git a/src/linter/ui5Types/SourceFileLinter.ts b/src/linter/ui5Types/SourceFileLinter.ts index a638163a..a702f4e0 100644 --- a/src/linter/ui5Types/SourceFileLinter.ts +++ b/src/linter/ui5Types/SourceFileLinter.ts @@ -1,6 +1,8 @@ import ts, {Identifier} from "typescript"; +import path from "node:path/posix"; import SourceFileReporter from "./SourceFileReporter.js"; import LinterContext, {ResourcePath, CoverageCategory, LintMessageSeverity} from "../LinterContext.js"; +import analyzeComponentJson from "./asyncComponentFlags.js"; interface DeprecationInfo { symbol: ts.Symbol; @@ -16,11 +18,14 @@ export default class SourceFileLinter { #boundVisitNode: (node: ts.Node) => void; #reportCoverage: boolean; #messageDetails: boolean; + #manifestContent: string | undefined; + #fileName: string; + #isComponent: boolean; constructor( context: LinterContext, resourcePath: ResourcePath, sourceFile: ts.SourceFile, sourceMap: string | undefined, checker: ts.TypeChecker, reportCoverage: boolean | undefined = false, - messageDetails: boolean | undefined = false + messageDetails: boolean | undefined = false, manifestContent?: string | undefined ) { this.#resourcePath = resourcePath; this.#sourceFile = sourceFile; @@ -30,6 +35,9 @@ export default class SourceFileLinter { this.#boundVisitNode = this.visitNode.bind(this); this.#reportCoverage = reportCoverage; this.#messageDetails = messageDetails; + this.#manifestContent = manifestContent; + this.#fileName = path.basename(resourcePath); + this.#isComponent = this.#fileName === "Component.js" || this.#fileName === "Component.ts"; } // eslint-disable-next-line @typescript-eslint/require-await @@ -67,6 +75,15 @@ export default class SourceFileLinter { node as (ts.PropertyAccessExpression | ts.ElementAccessExpression)); // Check for deprecation } else if (node.kind === ts.SyntaxKind.ImportDeclaration) { this.analyzeImportDeclaration(node as ts.ImportDeclaration); // Check for deprecation + } else if (node.kind === ts.SyntaxKind.ExpressionWithTypeArguments && this.#isComponent) { + analyzeComponentJson({ + node: node as ts.ExpressionWithTypeArguments, + manifestContent: this.#manifestContent, + resourcePath: this.#resourcePath, + reporter: this.#reporter, + context: this.#context, + checker: this.#checker, + }); } // Traverse the whole AST from top to bottom @@ -169,6 +186,9 @@ export default class SourceFileLinter { // returned by a class constructor. // However, the OPA Matchers are a known exception where constructors do return a function. return; + } else if (exprNode.kind === ts.SyntaxKind.SuperKeyword) { + // Ignore super calls + return; } if (!ts.isPropertyAccessExpression(exprNode) && diff --git a/src/linter/ui5Types/TypeLinter.ts b/src/linter/ui5Types/TypeLinter.ts index 12cc80c8..d2779d6d 100644 --- a/src/linter/ui5Types/TypeLinter.ts +++ b/src/linter/ui5Types/TypeLinter.ts @@ -4,7 +4,7 @@ import SourceFileLinter from "./SourceFileLinter.js"; import {taskStart} from "../../util/perf.js"; import {getLogger} from "@ui5/logger"; import LinterContext, {LinterParameters} from "../LinterContext.js"; -// import {Project} from "@ui5/project"; +import path from "node:path/posix"; import {AbstractAdapter} from "@ui5/fs"; import {createAdapter, createResource} from "@ui5/fs/resourceFactory"; @@ -103,12 +103,20 @@ export default class TypeChecker { if (!sourceMap) { log.verbose(`Failed to get source map for ${sourceFile.fileName}`); } + let manifestContent; + if (sourceFile.fileName.endsWith("/Component.js") || sourceFile.fileName.endsWith("/Component.ts")) { + const res = await this.#workspace.byPath(path.dirname(sourceFile.fileName) + "/manifest.json"); + if (res) { + manifestContent = await res.getString(); + } + } const linterDone = taskStart("Type-check resource", sourceFile.fileName, true); const linter = new SourceFileLinter( this.#context, sourceFile.fileName, sourceFile, sourceMap, - checker, reportCoverage, messageDetails + checker, reportCoverage, messageDetails, + manifestContent ); await linter.lint(); linterDone(); diff --git a/src/linter/ui5Types/asyncComponentFlags.ts b/src/linter/ui5Types/asyncComponentFlags.ts new file mode 100644 index 00000000..5967e91d --- /dev/null +++ b/src/linter/ui5Types/asyncComponentFlags.ts @@ -0,0 +1,414 @@ +import ts from "typescript"; +import path from "node:path/posix"; +import SourceFileReporter from "./SourceFileReporter.js"; +import type {JSONSchemaForSAPUI5Namespace, SAPJSONSchemaForWebApplicationManifestFile} from "../../manifest.js"; +import LinterContext, {LintMessage, LintMessageSeverity} from "../LinterContext.js"; +import jsonMap from "json-source-map"; +import type {jsonSourceMapType} from "../manifestJson/ManifestLinter.js"; + +type propsRecordValueType = string | boolean | undefined | null | number | propsRecord; +type propsRecord = Record; + +enum AsyncPropertyStatus { + parentPropNotSet, // In the manifest, the parent object of the property is not set + propNotSet, // Property is not set + false, // Property is set to false + true, // Property is set to true +}; + +interface AsyncFlags { + hasAsyncInterface: boolean; + hasManifestDefinition: boolean; + routingAsyncFlag: AsyncPropertyStatus; + rootViewAsyncFlag: AsyncPropertyStatus; +} + +export default function analyzeComponentJson({ + node, + manifestContent, + resourcePath, + reporter, + context, + checker, +}: { + node: ts.ExpressionWithTypeArguments; + manifestContent: string | undefined; + resourcePath: string; + reporter: SourceFileReporter; + context: LinterContext; + checker: ts.TypeChecker; +}) { + let classDesc = node.parent; + while (classDesc && classDesc.kind !== ts.SyntaxKind.ClassDeclaration) { + classDesc = classDesc.parent; + } + + if (!classDesc || !ts.isClassDeclaration(classDesc)) { + return; + } + + const analysisResult = findAsyncInterface({ + classDefinition: classDesc, manifestContent, checker, + }); + + if (analysisResult) { + reportResults({analysisResult, context, reporter, resourcePath, classDesc, manifestContent}); + } +} +function getHighestPropertyStatus(aProp: AsyncPropertyStatus, bProp: AsyncPropertyStatus): AsyncPropertyStatus { + return aProp > bProp ? aProp : bProp; +}; + +function mergeAsyncFlags(a: AsyncFlags, b: AsyncFlags): AsyncFlags { + 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({classDefinition, manifestContent, checker}: { + classDefinition: ts.ClassDeclaration; + manifestContent: string | undefined; + checker: ts.TypeChecker; +}): AsyncFlags | undefined { + const returnTypeTemplate = { + hasAsyncInterface: false, + routingAsyncFlag: AsyncPropertyStatus.parentPropNotSet, + rootViewAsyncFlag: AsyncPropertyStatus.parentPropNotSet, + hasManifestDefinition: false, + } as AsyncFlags; + + // Checks the interfaces and manifest of the class + const curClassAnalysis = classDefinition.members.reduce((acc, member) => { + const checkResult = doPropsCheck(member as ts.PropertyDeclaration, manifestContent); + return mergeAsyncFlags(acc, checkResult); + }, {...returnTypeTemplate}); + + const heritageAnalysis = + classDefinition?.heritageClauses?.flatMap((parentClasses: ts.HeritageClause) => { + return parentClasses.types.flatMap((parentClass) => { + const parentClassType = checker.getTypeAtLocation(parentClass); + + return parentClassType.symbol?.declarations?.flatMap((declaration) => { + let result = {...returnTypeTemplate} as AsyncFlags; + // Continue down the heritage chain to search for + // the async interface or manifest flags + if (ts.isClassDeclaration(declaration)) { + result = findAsyncInterface({ + classDefinition: declaration, + // We are unable to dynamically search for a parent-component's manifest.json + manifestContent: undefined, + checker, + }) ?? 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: ts.Node): statement is ts.ImportDeclaration { + return ts.isImportDeclaration(statement) && + ts.isStringLiteral(statement.moduleSpecifier) && + statement.moduleSpecifier.text === "sap/ui/core/library"; +} + +function doAsyncInterfaceChecks(importDeclaration: ts.Node): boolean { + const sourceFile = importDeclaration.getSourceFile(); + + let coreLibImports: ts.ImportDeclaration[] | undefined; + if (sourceFile.isDeclarationFile) { + let moduleDeclaration: ts.ModuleDeclaration | undefined; + 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: ts.PropertyDeclaration, manifestContent: string | undefined) { + let classInterfaces: ts.ObjectLiteralElementLike | undefined; + let componentManifest: ts.ObjectLiteralElementLike | undefined; + + if (metadata && ts.isPropertyDeclaration(metadata) && + metadata.initializer && ts.isObjectLiteralExpression(metadata.initializer)) { + metadata.initializer.properties.forEach((prop) => { + if (!prop.name) { + return; + } + const propText = getPropertyName(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 = AsyncPropertyStatus.parentPropNotSet; + let routingAsyncFlag: AsyncPropertyStatus = AsyncPropertyStatus.parentPropNotSet; + let hasManifestDefinition = false; + + if (componentManifest && + ts.isPropertyAssignment(componentManifest) && + ts.isObjectLiteralExpression(componentManifest.initializer)) { + /* eslint-disable @typescript-eslint/no-explicit-any */ + const instanceOfPropsRecord = (obj: any): obj is propsRecord => { + return !!obj && typeof obj === "object"; + }; + + hasManifestDefinition = true; + + const manifestJson = extractPropsRecursive(componentManifest.initializer) ?? {}; + let manifestSapui5Section: propsRecordValueType | propsRecordValueType[] | undefined; + 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) as SAPJSONSchemaForWebApplicationManifestFile; + + const {rootView, routing} = parsedManifestContent["sap.ui5"] ?? {} as JSONSchemaForSAPUI5Namespace; + + if (rootView) { + rootViewAsyncFlag = AsyncPropertyStatus.propNotSet; + // @ts-expect-error async is part of RootViewDefFlexEnabled and RootViewDef + const isRootViewAsync = rootView.async as boolean | undefined; + 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) && + componentManifest.initializer.getText() === "\"json\""); + } + + return { + routingAsyncFlag, + rootViewAsyncFlag, + hasAsyncInterface, + hasManifestDefinition, + }; +} + +function getPropertyName(node: ts.PropertyName): string { + if (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node)) { + return node.text; + } else { + return node.getText(); + } +} + +function extractPropsRecursive(node: ts.ObjectLiteralExpression) { + const properties = Object.create(null) as propsRecord; + + node.properties?.forEach((prop) => { + if (!ts.isPropertyAssignment(prop) || !prop.name) { + return; + } + + const key = getPropertyName(prop.name); + 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(($) => $) as propsRecordValueType[]; + + 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 reportResults({ + analysisResult, reporter, classDesc, manifestContent, resourcePath, context, +}: { + analysisResult: AsyncFlags; + reporter: SourceFileReporter; + context: LinterContext; + classDesc: ts.ClassDeclaration; + manifestContent: string | undefined; + resourcePath: string; +}) { + const {hasAsyncInterface, routingAsyncFlag, rootViewAsyncFlag, hasManifestDefinition} = analysisResult; + const fileName = path.basename(resourcePath); + + if (!hasManifestDefinition && !!manifestContent) { + reporter.addMessage({ + node: classDesc, + severity: LintMessageSeverity.Warning, + ruleId: "ui5-linter-async-component-flags", + message: `Component does not specify that it uses the descriptor via the manifest.json file`, + messageDetails: + `A manifest.json has been found in the same directory as the component. Although it will be used at ` + + `runtime automatically, this should still be expressed in the ` + + `{@link topic:0187ea5e2eff4166b0453b9dcc8fc64f metadata of the component class}.`, + }); + } + + if (hasAsyncInterface !== true) { + if ([AsyncPropertyStatus.propNotSet, AsyncPropertyStatus.false].includes(rootViewAsyncFlag) || + [AsyncPropertyStatus.propNotSet, AsyncPropertyStatus.false].includes(routingAsyncFlag)) { + let message = `Component Root View and Routing are not configured to load their modules asynchronously.`; + let messageDetails = `{@link topic:676b636446c94eada183b1218a824717 Use Asynchronous Loading}. ` + + `Implement sap.ui.core.IAsyncContentCreation interface in ${fileName} or set the "async" flags for ` + + `"sap.ui5/routing/config" and "sap.ui5/rootView" in the component manifest.`; + + if (AsyncPropertyStatus.parentPropNotSet === rootViewAsyncFlag) { + // sap.ui5/rootView is not set at all, so skip it in the message + message = `Component Routing is not configured to load its targets asynchronously.`; + messageDetails = `{@link topic:676b636446c94eada183b1218a824717 Use Asynchronous Loading}. ` + + `Implement sap.ui.core.IAsyncContentCreation interface in ${fileName} or set the "async" flag for ` + + `"sap.ui5/routing/config" in the component manifest.`; + } else if (AsyncPropertyStatus.parentPropNotSet === routingAsyncFlag) { + // sap.ui5/routing/config is not set at all, so skip it in the message + message = `Component Root View is not configured to load its views asynchronously.`; + messageDetails = `{@link topic:676b636446c94eada183b1218a824717 Use Asynchronous Loading}. ` + + `Implement sap.ui.core.IAsyncContentCreation interface in ${fileName} or set the "async" flag for ` + + `"sap.ui5/rootView" in the component manifest.`; + } + + reporter.addMessage({ + node: classDesc, + severity: LintMessageSeverity.Error, + ruleId: "ui5-linter-async-component-flags", + message, + messageDetails, + }); + } + } else { + const {pointers} = jsonMap.parse(manifestContent ?? "{}"); + const report = (pointerKey: string, message: LintMessage) => { + 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(fileName, "manifest.json"), {...message, ...posInfo}); + } else { + reporter.addMessage({...message, ...{node: classDesc}}); + } + }; + + if (rootViewAsyncFlag === AsyncPropertyStatus.true) { + report("/sap.ui5/rootView/async", { + severity: LintMessageSeverity.Warning, + ruleId: "ui5-linter-async-component-flags", + message: `Component implements the sap.ui.core.IAsyncContentCreation interface. ` + + `The redundant "async" flag for "sap.ui5/rootView" should be removed from the component manifest`, + messageDetails: `{@link sap.ui.core.IAsyncContentCreation sap.ui.core.IAsyncContentCreation}`, + }); + } + if (routingAsyncFlag === AsyncPropertyStatus.true) { + report("/sap.ui5/routing/config/async", { + severity: LintMessageSeverity.Warning, + ruleId: "ui5-linter-async-component-flags", + message: `Component implements the sap.ui.core.IAsyncContentCreation interface. ` + + `The redundant "async" flag for "sap.ui5/routing/config" should be removed from the component manifest`, + messageDetails: `{@link sap.ui.core.IAsyncContentCreation sap.ui.core.IAsyncContentCreation}`, + }); + } + } +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_01/Component.js b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_01/Component.js new file mode 100644 index 00000000..922ef9db --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_01/Component.js @@ -0,0 +1,12 @@ +// Fixture description: +// IAsyncContentCreation interface is implemented, no redundant async flags in manifest.json +sap.ui.define(["sap/ui/core/UIComponent"], function (UIComponent) { + "use strict"; + + return UIComponent.extend("mycomp.Component", { + metadata: { + "interfaces": ["sap.ui.core.IAsyncContentCreation"], + "manifest": "json", + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_01/manifest.json b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_01/manifest.json new file mode 100644 index 00000000..63a4fffc --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_01/manifest.json @@ -0,0 +1,45 @@ +{ + "_version": "1.12.0", + + "sap.app": { + "id": "mycomp", + "type": "application", + "i18n": "i18n/i18n.properties", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "applicationVersion": { + "version": "1.0.0" + } + }, + + "sap.ui5": { + "rootView": { + "viewName": "mycomp.view.App", + "type": "XML", + "id": "app" + }, + + "routing": { + "config": { + "routerClass": "sap.m.routing.Router", + "viewType": "XML", + "viewPath": "mycomp.view", + "controlId": "app", + "controlAggregation": "pages" + }, + "routes": [ + { + "pattern": "", + "name": "main", + "target": "main" + } + ], + "targets": { + "main": { + "viewId": "main", + "viewName": "Main" + } + } + } + } +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_02/Component.js b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_02/Component.js new file mode 100644 index 00000000..e847e680 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_02/Component.js @@ -0,0 +1,11 @@ +// Fixture description: +// Async flags are maintained in manifest.json +sap.ui.define(["sap/ui/core/UIComponent"], function (UIComponent) { + "use strict"; + + return UIComponent.extend("mycomp.Component", { + metadata: { + manifest: "json", + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_02/manifest.json b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_02/manifest.json new file mode 100644 index 00000000..31637c67 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_02/manifest.json @@ -0,0 +1,47 @@ +{ + "_version": "1.12.0", + + "sap.app": { + "id": "mycomp", + "type": "application", + "i18n": "i18n/i18n.properties", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "applicationVersion": { + "version": "1.0.0" + } + }, + + "sap.ui5": { + "rootView": { + "viewName": "mycomp.view.App", + "type": "XML", + "async": true, + "id": "app" + }, + + "routing": { + "config": { + "routerClass": "sap.m.routing.Router", + "viewType": "XML", + "viewPath": "mycomp.view", + "controlId": "app", + "controlAggregation": "pages", + "async": true + }, + "routes": [ + { + "pattern": "", + "name": "main", + "target": "main" + } + ], + "targets": { + "main": { + "viewId": "main", + "viewName": "Main" + } + } + } + } +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_03/Component.js b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_03/Component.js new file mode 100644 index 00000000..4df637e9 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_03/Component.js @@ -0,0 +1,56 @@ +// Fixture description: +// IAsyncContentCreation interface is implemented, no redundant async flags in manifest +sap.ui.define(["sap/ui/core/UIComponent"], function (UIComponent) { + "use strict"; + + return UIComponent.extend("mycomp.Component", { + metadata: { + interfaces: ["sap.ui.core.IAsyncContentCreation"], + manifest: { + _version: "1.12.0", + + "sap.app": { + id: "mycomp", + type: "application", + i18n: "i18n/i18n.properties", + title: "{{appTitle}}", + description: "{{appDescription}}", + applicationVersion: { + version: "1.0.0", + }, + }, + + "sap.ui5": { + rootView: { + viewName: "mycomp.view.App", + type: "XML", + id: "app", + }, + + routing: { + config: { + routerClass: "sap.m.routing.Router", + viewType: "XML", + viewPath: "mycomp.view", + controlId: "app", + controlAggregation: "pages", + }, + routes: [ + { + pattern: "", + name: "main", + target: "main", + }, + ], + targets: { + main: { + viewId: "main", + viewName: "Main", + }, + }, + }, + }, + }, + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_04/Component.js b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_04/Component.js new file mode 100644 index 00000000..403ec186 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_04/Component.js @@ -0,0 +1,57 @@ +// Fixture description: +// Async flags are maintained, no IAsyncContentCreation interface implemented +sap.ui.define(["sap/ui/core/UIComponent"], function (UIComponent) { + "use strict"; + + return UIComponent.extend("mycomp.Component", { + metadata: { + manifest: { + _version: "1.12.0", + + "sap.app": { + id: "mycomp", + type: "application", + i18n: "i18n/i18n.properties", + title: "{{appTitle}}", + description: "{{appDescription}}", + applicationVersion: { + version: "1.0.0", + }, + }, + + "sap.ui5": { + rootView: { + viewName: "mycomp.view.App", + type: "XML", + async: true, + id: "app", + }, + + routing: { + config: { + routerClass: "sap.m.routing.Router", + viewType: "XML", + viewPath: "mycomp.view", + controlId: "app", + controlAggregation: "pages", + async: true, + }, + routes: [ + { + pattern: "", + name: "main", + target: "main", + }, + ], + targets: { + main: { + viewId: "main", + viewName: "Main", + }, + }, + }, + }, + }, + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_05/Component.js b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_05/Component.js new file mode 100644 index 00000000..079eb560 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_05/Component.js @@ -0,0 +1,11 @@ +// Fixture description: +// Async flags are maintained in manifest.json. Inheriting from parent component. +sap.ui.define(["mycomp/subdir/ParentComponent"], function (ParentComponent) { + "use strict"; + + return ParentComponent.extend("mycomp.Component", { + metadata: { + manifest: "json", + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_05/manifest.json b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_05/manifest.json new file mode 100644 index 00000000..31637c67 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_05/manifest.json @@ -0,0 +1,47 @@ +{ + "_version": "1.12.0", + + "sap.app": { + "id": "mycomp", + "type": "application", + "i18n": "i18n/i18n.properties", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "applicationVersion": { + "version": "1.0.0" + } + }, + + "sap.ui5": { + "rootView": { + "viewName": "mycomp.view.App", + "type": "XML", + "async": true, + "id": "app" + }, + + "routing": { + "config": { + "routerClass": "sap.m.routing.Router", + "viewType": "XML", + "viewPath": "mycomp.view", + "controlId": "app", + "controlAggregation": "pages", + "async": true + }, + "routes": [ + { + "pattern": "", + "name": "main", + "target": "main" + } + ], + "targets": { + "main": { + "viewId": "main", + "viewName": "Main" + } + } + } + } +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_05/subdir/ParentComponent.js b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_05/subdir/ParentComponent.js new file mode 100644 index 00000000..01a5e3b1 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_05/subdir/ParentComponent.js @@ -0,0 +1,6 @@ +sap.ui.define(["sap/ui/core/UIComponent"], function (UIComponent) { + "use strict"; + + return UIComponent.extend("mycomp.subdir.ParentComponent", { + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_06/Component.js b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_06/Component.js new file mode 100644 index 00000000..2b70a5fa --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_06/Component.js @@ -0,0 +1,10 @@ +// IAsyncContentCreation interface is implemented, no redundant async flags in inline manifest of parent component +sap.ui.define(["mycomp/subdir/ParentComponent"], function (ParentComponent) { + "use strict"; + + return ParentComponent.extend("mycomp.Component", { + metadata: { + interfaces: ["sap.ui.core.IAsyncContentCreation"], + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_06/subdir/ParentComponent.js b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_06/subdir/ParentComponent.js new file mode 100644 index 00000000..37dd42ae --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_06/subdir/ParentComponent.js @@ -0,0 +1,53 @@ +sap.ui.define(["sap/ui/core/UIComponent"], function (UIComponent) { + "use strict"; + + return UIComponent.extend("mycomp.subdir.ParentComponent", { + metadata: { + manifest: { + _version: "1.12.0", + + "sap.app": { + id: "mycomp", + type: "application", + i18n: "i18n/i18n.properties", + title: "{{appTitle}}", + description: "{{appDescription}}", + applicationVersion: { + version: "1.0.0", + }, + }, + + "sap.ui5": { + rootView: { + viewName: "mycomp.view.App", + type: "XML", + id: "app", + }, + + routing: { + config: { + routerClass: "sap.m.routing.Router", + viewType: "XML", + viewPath: "mycomp.view", + controlId: "app", + controlAggregation: "pages", + }, + routes: [ + { + pattern: "", + name: "main", + target: "main", + }, + ], + targets: { + main: { + viewId: "main", + viewName: "Main", + }, + }, + }, + }, + }, + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_07/Component.js b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_07/Component.js new file mode 100644 index 00000000..596cd3fd --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_07/Component.js @@ -0,0 +1,10 @@ +// Inheriting from sap/fe/core/AppComponent (implements IAsyncContentCreation interface), no redundant async flags in manifest +sap.ui.define(["sap/fe/core/AppComponent"], function (AppComponent) { + "use strict"; + + return AppComponent.extend("mycomp.Component", { + metadata: { + manifest: "json", + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_07/manifest.json b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_07/manifest.json new file mode 100644 index 00000000..63a4fffc --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_07/manifest.json @@ -0,0 +1,45 @@ +{ + "_version": "1.12.0", + + "sap.app": { + "id": "mycomp", + "type": "application", + "i18n": "i18n/i18n.properties", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "applicationVersion": { + "version": "1.0.0" + } + }, + + "sap.ui5": { + "rootView": { + "viewName": "mycomp.view.App", + "type": "XML", + "id": "app" + }, + + "routing": { + "config": { + "routerClass": "sap.m.routing.Router", + "viewType": "XML", + "viewPath": "mycomp.view", + "controlId": "app", + "controlAggregation": "pages" + }, + "routes": [ + { + "pattern": "", + "name": "main", + "target": "main" + } + ], + "targets": { + "main": { + "viewId": "main", + "viewName": "Main" + } + } + } + } +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_08/Component.js b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_08/Component.js new file mode 100644 index 00000000..2cdad8a9 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_08/Component.js @@ -0,0 +1,10 @@ +// No rootView or router config and no IAsyncContentCreation interface implemented +sap.ui.define(["sap/ui/core/UIComponent"], function (UIComponent) { + "use strict"; + + return UIComponent.extend("mycomp.Component", { + "metadata": { + "manifest": "json", + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_08/manifest.json b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_08/manifest.json new file mode 100644 index 00000000..24562d88 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_08/manifest.json @@ -0,0 +1,14 @@ +{ + "_version": "1.12.0", + + "sap.app": { + "id": "mycomp", + "type": "application", + "i18n": "i18n/i18n.properties", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "applicationVersion": { + "version": "1.0.0" + } + } +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_09/Component.ts b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_09/Component.ts new file mode 100644 index 00000000..c37a7c87 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_09/Component.ts @@ -0,0 +1,9 @@ +// Fixture description: +// TypeScript component which inherits from ParentComponent which implements IAsyncContentCreation interface +import ParentComponent from "mycomp/subdir/ParentComponent"; + +export default class Component extends ParentComponent { + static metadata = { + manifest: "json", + }; +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_09/manifest.json b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_09/manifest.json new file mode 100644 index 00000000..63a4fffc --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_09/manifest.json @@ -0,0 +1,45 @@ +{ + "_version": "1.12.0", + + "sap.app": { + "id": "mycomp", + "type": "application", + "i18n": "i18n/i18n.properties", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "applicationVersion": { + "version": "1.0.0" + } + }, + + "sap.ui5": { + "rootView": { + "viewName": "mycomp.view.App", + "type": "XML", + "id": "app" + }, + + "routing": { + "config": { + "routerClass": "sap.m.routing.Router", + "viewType": "XML", + "viewPath": "mycomp.view", + "controlId": "app", + "controlAggregation": "pages" + }, + "routes": [ + { + "pattern": "", + "name": "main", + "target": "main" + } + ], + "targets": { + "main": { + "viewId": "main", + "viewName": "Main" + } + } + } + } +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_09/subdir/ParentComponent.ts b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_09/subdir/ParentComponent.ts new file mode 100644 index 00000000..c6825bbc --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_09/subdir/ParentComponent.ts @@ -0,0 +1,6 @@ +import UIComponent from "sap/ui/core/UIComponent"; +import { IAsyncContentCreation } from "sap/ui/core/library"; + +export default class Component extends UIComponent implements IAsyncContentCreation { + __implements__sap_ui_core_IAsyncContentCreation: boolean; +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_10/Component.ts b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_10/Component.ts new file mode 100644 index 00000000..0a56f6c3 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_10/Component.ts @@ -0,0 +1,10 @@ +// Fixture description: +// TypeScript component which inherits from ParentComponent which implements IAsyncContentCreation interface through metadata +import ParentComponent from "mycomp/subdir/ParentComponent"; +import * as library from "sap/ui/core/library"; // Unused core library import for code coverage purposes + +export default class Component extends ParentComponent { + static metadata = { + manifest: "json" + }; +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_10/manifest.json b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_10/manifest.json new file mode 100644 index 00000000..63a4fffc --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_10/manifest.json @@ -0,0 +1,45 @@ +{ + "_version": "1.12.0", + + "sap.app": { + "id": "mycomp", + "type": "application", + "i18n": "i18n/i18n.properties", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "applicationVersion": { + "version": "1.0.0" + } + }, + + "sap.ui5": { + "rootView": { + "viewName": "mycomp.view.App", + "type": "XML", + "id": "app" + }, + + "routing": { + "config": { + "routerClass": "sap.m.routing.Router", + "viewType": "XML", + "viewPath": "mycomp.view", + "controlId": "app", + "controlAggregation": "pages" + }, + "routes": [ + { + "pattern": "", + "name": "main", + "target": "main" + } + ], + "targets": { + "main": { + "viewId": "main", + "viewName": "Main" + } + } + } + } +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Negative_10/subdir/ParentComponent.ts b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_10/subdir/ParentComponent.ts new file mode 100644 index 00000000..991baefb --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Negative_10/subdir/ParentComponent.ts @@ -0,0 +1,6 @@ +import UIComponent from "sap/ui/core/UIComponent"; +export default class Component extends UIComponent { + static metadata = { + interfaces: ["sap.ui.core.IAsyncContentCreation"], + } +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_01/Component.js b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_01/Component.js new file mode 100644 index 00000000..d38b6a97 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_01/Component.js @@ -0,0 +1,11 @@ +// Fixture description: +// No IAsyncContentCreation interface is implemented, no async flags in manifest.json +sap.ui.define(["sap/ui/core/UIComponent"], function (UIComponent) { + "use strict"; + + return UIComponent.extend("mycomp.Component", { + metadata: { + manifest: "json", + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_01/manifest.json b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_01/manifest.json new file mode 100644 index 00000000..63a4fffc --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_01/manifest.json @@ -0,0 +1,45 @@ +{ + "_version": "1.12.0", + + "sap.app": { + "id": "mycomp", + "type": "application", + "i18n": "i18n/i18n.properties", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "applicationVersion": { + "version": "1.0.0" + } + }, + + "sap.ui5": { + "rootView": { + "viewName": "mycomp.view.App", + "type": "XML", + "id": "app" + }, + + "routing": { + "config": { + "routerClass": "sap.m.routing.Router", + "viewType": "XML", + "viewPath": "mycomp.view", + "controlId": "app", + "controlAggregation": "pages" + }, + "routes": [ + { + "pattern": "", + "name": "main", + "target": "main" + } + ], + "targets": { + "main": { + "viewId": "main", + "viewName": "Main" + } + } + } + } +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_02/Component.js b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_02/Component.js new file mode 100644 index 00000000..daef7eed --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_02/Component.js @@ -0,0 +1,12 @@ +// Fixture description: +// IAsyncContentCreation interface is implemented, redundant async flags in manifest.json +sap.ui.define(["sap/ui/core/UIComponent"], function (UIComponent) { + "use strict"; + + return UIComponent.extend("mycomp.Component", { + metadata: { + interfaces: ["sap.ui.core.IAsyncContentCreation"], + manifest: "json", + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_02/manifest.json b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_02/manifest.json new file mode 100644 index 00000000..31637c67 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_02/manifest.json @@ -0,0 +1,47 @@ +{ + "_version": "1.12.0", + + "sap.app": { + "id": "mycomp", + "type": "application", + "i18n": "i18n/i18n.properties", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "applicationVersion": { + "version": "1.0.0" + } + }, + + "sap.ui5": { + "rootView": { + "viewName": "mycomp.view.App", + "type": "XML", + "async": true, + "id": "app" + }, + + "routing": { + "config": { + "routerClass": "sap.m.routing.Router", + "viewType": "XML", + "viewPath": "mycomp.view", + "controlId": "app", + "controlAggregation": "pages", + "async": true + }, + "routes": [ + { + "pattern": "", + "name": "main", + "target": "main" + } + ], + "targets": { + "main": { + "viewId": "main", + "viewName": "Main" + } + } + } + } +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_03/Component.js b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_03/Component.js new file mode 100644 index 00000000..411d0c4e --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_03/Component.js @@ -0,0 +1,55 @@ +// Fixture description: +// No IAsyncContentCreation interface, missing async flags in manifest.json +sap.ui.define(["sap/ui/core/UIComponent"], function (UIComponent) { + "use strict"; + + return UIComponent.extend("mycomp.Component", { + metadata: { + manifest: { + _version: "1.12.0", + + "sap.app": { + id: "mycomp", + type: "application", + i18n: "i18n/i18n.properties", + title: "{{appTitle}}", + description: "{{appDescription}}", + applicationVersion: { + version: "1.0.0", + }, + }, + + "sap.ui5": { + rootView: { + viewName: "mycomp.view.App", + type: "XML", + id: "app", + }, + + routing: { + config: { + routerClass: "sap.m.routing.Router", + viewType: "XML", + viewPath: "mycomp.view", + controlId: "app", + controlAggregation: "pages", + }, + routes: [ + { + pattern: "", + name: "main", + target: "main", + }, + ], + targets: { + main: { + viewId: "main", + viewName: "Main", + }, + }, + }, + }, + }, + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_04/Component.js b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_04/Component.js new file mode 100644 index 00000000..eff7b1af --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_04/Component.js @@ -0,0 +1,58 @@ +// Fixture description: +// IAsyncContentCreation interface is implemented, redundant async flags in manifest.json +sap.ui.define(["sap/ui/core/UIComponent"], function (UIComponent) { + "use strict"; + + return UIComponent.extend("mycomp.Component", { + metadata: { + interfaces: ["sap.ui.core.IAsyncContentCreation"], + manifest: { + _version: "1.12.0", + + "sap.app": { + id: "mycomp", + type: "application", + i18n: "i18n/i18n.properties", + title: "{{appTitle}}", + description: "{{appDescription}}", + applicationVersion: { + version: "1.0.0", + }, + }, + + "sap.ui5": { + rootView: { + viewName: "mycomp.view.App", + type: "XML", + async: true, + id: "app", + }, + + routing: { + config: { + routerClass: "sap.m.routing.Router", + viewType: "XML", + viewPath: "mycomp.view", + controlId: "app", + controlAggregation: "pages", + async: true, + }, + routes: [ + { + pattern: "", + name: "main", + target: "main", + }, + ], + targets: { + main: { + viewId: "main", + viewName: "Main", + }, + }, + }, + }, + }, + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_05/Component.js b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_05/Component.js new file mode 100644 index 00000000..a6474f7b --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_05/Component.js @@ -0,0 +1,11 @@ +// Fixture description: +// IAsyncContentCreation interface is implemented, redundant async flags in manifest of parent component +sap.ui.define(["mycomp/subdir/ParentComponent"], function (ParentComponent) { + "use strict"; + + return ParentComponent.extend("mycomp.Component", { + metadata: { + interfaces: ["sap.ui.core.IAsyncContentCreation"], + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_05/subdir/ParentComponent.js b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_05/subdir/ParentComponent.js new file mode 100644 index 00000000..7f65dca0 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_05/subdir/ParentComponent.js @@ -0,0 +1,18 @@ +sap.ui.define(["sap/ui/core/UIComponent"], function (UIComponent) { + "use strict"; + + return UIComponent.extend("mycomp.subdir.ParentComponent", { + metadata: { + manifest: { + "sap.ui5": { + rootView: { + viewName: "mycomp.view.App", + type: "XML", + id: "app", + async: true + }, + } + } + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_06/Component.js b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_06/Component.js new file mode 100644 index 00000000..8d7041b4 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_06/Component.js @@ -0,0 +1,12 @@ +// Fixture description: +// IAsyncContentCreation interface is implemented, redundant async flag (rootView only) in manifest.json +// No manifest: "json" configuration in metadata +sap.ui.define(["sap/ui/core/UIComponent"], function (UIComponent) { + "use strict"; + + return UIComponent.extend("mycomp.Component", { + metadata: { + interfaces: ["sap.ui.core.IAsyncContentCreation"], + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_06/manifest.json b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_06/manifest.json new file mode 100644 index 00000000..f014842f --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_06/manifest.json @@ -0,0 +1,46 @@ +{ + "_version": "1.12.0", + + "sap.app": { + "id": "mycomp", + "type": "application", + "i18n": "i18n/i18n.properties", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "applicationVersion": { + "version": "1.0.0" + } + }, + + "sap.ui5": { + "rootView": { + "viewName": "mycomp.view.App", + "type": "XML", + "id": "app", + "async": true + }, + + "routing": { + "config": { + "routerClass": "sap.m.routing.Router", + "viewType": "XML", + "viewPath": "mycomp.view", + "controlId": "app", + "controlAggregation": "pages" + }, + "routes": [ + { + "pattern": "", + "name": "main", + "target": "main" + } + ], + "targets": { + "main": { + "viewId": "main", + "viewName": "Main" + } + } + } + } +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_07/Component.js b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_07/Component.js new file mode 100644 index 00000000..22139a00 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_07/Component.js @@ -0,0 +1,11 @@ +// Fixture description: +// No IAsyncContentCreation interface is implemented, no async flag (rootView only) in manifest.json +sap.ui.define(["sap/ui/core/UIComponent"], function (UIComponent) { + "use strict"; + + return UIComponent.extend("mycomp.Component", { + metadata: { + manifest: "json" + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_07/manifest.json b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_07/manifest.json new file mode 100644 index 00000000..2aa1076d --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_07/manifest.json @@ -0,0 +1,22 @@ +{ + "_version": "1.12.0", + + "sap.app": { + "id": "mycomp", + "type": "application", + "i18n": "i18n/i18n.properties", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "applicationVersion": { + "version": "1.0.0" + } + }, + + "sap.ui5": { + "rootView": { + "viewName": "mycomp.view.App", + "type": "XML", + "id": "app" + } + } +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_08/Component.js b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_08/Component.js new file mode 100644 index 00000000..1e781a42 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_08/Component.js @@ -0,0 +1,11 @@ +// Fixture description: +// No IAsyncContentCreation interface is implemented, no async flag (routing only) in manifest.json +sap.ui.define(["sap/ui/core/UIComponent"], function (UIComponent) { + "use strict"; + + return UIComponent.extend("mycomp.Component", { + metadata: { + manifest: "json" + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_08/manifest.json b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_08/manifest.json new file mode 100644 index 00000000..2ec69c85 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_08/manifest.json @@ -0,0 +1,39 @@ +{ + "_version": "1.12.0", + + "sap.app": { + "id": "mycomp", + "type": "application", + "i18n": "i18n/i18n.properties", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "applicationVersion": { + "version": "1.0.0" + } + }, + + "sap.ui5": { + "routing": { + "config": { + "routerClass": "sap.m.routing.Router", + "viewType": "XML", + "viewPath": "mycomp.view", + "controlId": "app", + "controlAggregation": "pages" + }, + "routes": [ + { + "pattern": "", + "name": "main", + "target": "main" + } + ], + "targets": { + "main": { + "viewId": "main", + "viewName": "Main" + } + } + } + } +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_09/Component.js b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_09/Component.js new file mode 100644 index 00000000..72aa31bb --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_09/Component.js @@ -0,0 +1,12 @@ +// IAsyncContentCreation interface is implemented via sap.ui.core.library property +// which is a bad practice (see https://github.com/SAP/openui5/issues/3895) and therefore ignored +sap.ui.define(["sap/ui/core/UIComponent", "sap/ui/core/library"], function (UIComponent, coreLib) { + "use strict"; + + return UIComponent.extend("mycomp.Component", { + "metadata": { + "interfaces": [coreLib.IAsyncContentCreation], + "manifest": "json", + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_09/manifest.json b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_09/manifest.json new file mode 100644 index 00000000..63a4fffc --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_09/manifest.json @@ -0,0 +1,45 @@ +{ + "_version": "1.12.0", + + "sap.app": { + "id": "mycomp", + "type": "application", + "i18n": "i18n/i18n.properties", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "applicationVersion": { + "version": "1.0.0" + } + }, + + "sap.ui5": { + "rootView": { + "viewName": "mycomp.view.App", + "type": "XML", + "id": "app" + }, + + "routing": { + "config": { + "routerClass": "sap.m.routing.Router", + "viewType": "XML", + "viewPath": "mycomp.view", + "controlId": "app", + "controlAggregation": "pages" + }, + "routes": [ + { + "pattern": "", + "name": "main", + "target": "main" + } + ], + "targets": { + "main": { + "viewId": "main", + "viewName": "Main" + } + } + } + } +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_10/Component.js b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_10/Component.js new file mode 100644 index 00000000..4e57f991 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_10/Component.js @@ -0,0 +1,10 @@ +// Component which does not inherit from UIComponent (this should actually not be analyzed) +sap.ui.define(["sap/ui/core/Component"], function (Component) { + "use strict"; + + return Component.extend("mycomp.Component", { + "metadata": { + "manifest": "json", + }, + }); +}); diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_10/manifest.json b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_10/manifest.json new file mode 100644 index 00000000..63a4fffc --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_10/manifest.json @@ -0,0 +1,45 @@ +{ + "_version": "1.12.0", + + "sap.app": { + "id": "mycomp", + "type": "application", + "i18n": "i18n/i18n.properties", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "applicationVersion": { + "version": "1.0.0" + } + }, + + "sap.ui5": { + "rootView": { + "viewName": "mycomp.view.App", + "type": "XML", + "id": "app" + }, + + "routing": { + "config": { + "routerClass": "sap.m.routing.Router", + "viewType": "XML", + "viewPath": "mycomp.view", + "controlId": "app", + "controlAggregation": "pages" + }, + "routes": [ + { + "pattern": "", + "name": "main", + "target": "main" + } + ], + "targets": { + "main": { + "viewId": "main", + "viewName": "Main" + } + } + } + } +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_11/Component.ts b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_11/Component.ts new file mode 100644 index 00000000..ee83207c --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_11/Component.ts @@ -0,0 +1,9 @@ +// Fixture description: +// TypeScript component which does not implement IAsyncContentCreation interface, no async flags in manifest.json +import UIComponent from "sap/ui/core/UIComponent"; + +export default class Component extends UIComponent { + constructor() { + super("my.comp.Component"); + } +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/Positive_11/manifest.json b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_11/manifest.json new file mode 100644 index 00000000..63a4fffc --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/Positive_11/manifest.json @@ -0,0 +1,45 @@ +{ + "_version": "1.12.0", + + "sap.app": { + "id": "mycomp", + "type": "application", + "i18n": "i18n/i18n.properties", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "applicationVersion": { + "version": "1.0.0" + } + }, + + "sap.ui5": { + "rootView": { + "viewName": "mycomp.view.App", + "type": "XML", + "id": "app" + }, + + "routing": { + "config": { + "routerClass": "sap.m.routing.Router", + "viewType": "XML", + "viewPath": "mycomp.view", + "controlId": "app", + "controlAggregation": "pages" + }, + "routes": [ + { + "pattern": "", + "name": "main", + "target": "main" + } + ], + "targets": { + "main": { + "viewId": "main", + "viewName": "Main" + } + } + } + } +} diff --git a/test/fixtures/linter/rules/AsyncComponentFlags/test-overview.md b/test/fixtures/linter/rules/AsyncComponentFlags/test-overview.md new file mode 100644 index 00000000..504685b7 --- /dev/null +++ b/test/fixtures/linter/rules/AsyncComponentFlags/test-overview.md @@ -0,0 +1,20 @@ +## Test Variants + +### IAsyncContentCreation Interface + +Implemented either directly by the Component, by the Parent Component or not at all. + +Can be implemented by defining the string `sap.ui.core.IAsyncContentCreation` in the "interface" array of the component metadata, +or by using the IAsyncContentCreation property of the `sap/ui/core/library` module. The latter is discouraged as it does not work +in TypeScript (see also https://github.com/SAP/openui5/issues/3895), hence it is currently not detected. + +In case of TypeScript files, it can also be define using the `implements` keyword. + +### Async Manifest Flags + +There are two relevant flags in the component manifest. The manifest can either be a separate `manifest.json` file or defined inline in the component metadata. + +The first flag is `"sap.ui5".rootView.async`, which is only evaluated if `"sap.ui5".rootView` is defined. If this configuration object is not provided on a Component, a parent's + +The second flag is `"sap.ui5".routing.config.async`, which is only evaluated if `"sap.ui5".routing` is defined. + diff --git a/test/lib/linter/_linterHelper.ts b/test/lib/linter/_linterHelper.ts index 9050b538..61963533 100644 --- a/test/lib/linter/_linterHelper.ts +++ b/test/lib/linter/_linterHelper.ts @@ -2,6 +2,7 @@ import anyTest, {ExecutionContext, TestFn} from "ava"; import sinonGlobal, {SinonStub} from "sinon"; import util from "util"; import {readdirSync} from "node:fs"; +import path from "node:path"; import esmock from "esmock"; import SourceFileLinter from "../../../src/linter/ui5Types/SourceFileLinter.js"; import {SourceFile, TypeChecker} from "typescript"; @@ -24,19 +25,20 @@ test.before(async (t) => { export async function esmockDeprecationText() { const typeLinterModule = await esmock("../../../src/linter/ui5Types/TypeLinter.js", { "../../../src/linter/ui5Types/SourceFileLinter.js": - function ( - context: LinterContext, filePath: string, sourceFile: SourceFile, - sourceMap: string | undefined, checker: TypeChecker, - reportCoverage: boolean | undefined = false, - messageDetails: boolean | undefined = false - ) { - // Don't use sinon's stubs as it's hard to clean after them in this case and it leaks memory. - const linter = new SourceFileLinter( - context, filePath, sourceFile, sourceMap, checker, reportCoverage, messageDetails - ); - linter.getDeprecationText = () => "Deprecated test message"; - return linter; - }, + function ( + context: LinterContext, filePath: string, sourceFile: SourceFile, + sourceMap: string | undefined, checker: TypeChecker, + reportCoverage: boolean | undefined = false, + messageDetails: boolean | undefined = false, + manifestContent?: string | undefined + ) { + // Don't use sinon's stubs as it's hard to clean after them in this case and it leaks memory. + const linter = new SourceFileLinter( + context, filePath, sourceFile, sourceMap, checker, reportCoverage, messageDetails, manifestContent + ); + linter.getDeprecationText = () => "Deprecated test message"; + return linter; + }, }); const lintWorkspaceModule = await esmock("../../../src/linter/lintWorkspace.js", { "../../../src/linter/ui5Types/TypeLinter.js": typeLinterModule, @@ -62,48 +64,43 @@ export function assertExpectedLintResults( // Helper function to create linting tests for all files in a directory export function createTestsForFixtures(fixturesPath: string) { try { - const testFiles = readdirSync(fixturesPath); + const testFiles = readdirSync(fixturesPath, {withFileTypes: true}).filter((dirEntries) => { + return dirEntries.isFile(); + }).map((dirEntries) => { + return dirEntries.name; + }); if (!testFiles.length) { throw new Error(`Failed to find any fixtures in directory ${fixturesPath}`); } - for (const fileName of testFiles) { - if (!fileName.endsWith(".js") && - !fileName.endsWith(".xml") && - !fileName.endsWith(".json") && - !fileName.endsWith(".html") && - !fileName.endsWith(".yaml")) { - // Ignore non-JavaScript, non-XML, non-JSON, non-HTML and non-YAML files - continue; - } - let testName = fileName; - let defineTest = test.serial; - if (fileName.startsWith("_")) { - // Skip tests for files starting with underscore - defineTest = defineTest.skip as typeof test; - testName = fileName.slice(1); - } else if (fileName.startsWith("only_")) { - // Only run test when file starts with only_ - defineTest = defineTest.only as typeof test; - testName = fileName.slice(5); - } - // Executing linting in parallel might lead to OOM errors in the CI - // Therefore always use serial - defineTest(`General: ${testName}`, async (t) => { - const filePaths = [fileName]; - const {lintFile} = t.context; + if (fixturesPath.includes("AsyncComponentFlags")) { + const dirName = path.basename(fixturesPath); + testDefinition({ + testName: dirName, + namespace: "mycomp", + fileName: dirName, + fixturesPath, + // Needed, because without a namespace, TS's type definition detection + // does not function properly for the inheritance case + filePaths: testFiles.map((fileName) => path.join("resources", "mycomp", fileName)), + }); + } else { + for (const fileName of testFiles) { + if (!fileName.endsWith(".js") && + !fileName.endsWith(".xml") && + !fileName.endsWith(".json") && + !fileName.endsWith(".html") && + !fileName.endsWith(".yaml")) { + // Ignore non-JavaScript, non-XML, non-JSON, non-HTML and non-YAML files + continue; + } - const res = await lintFile({ - rootDir: fixturesPath, - pathsToLint: filePaths, - reportCoverage: true, - includeMessageDetails: true, + testDefinition({ + testName: fileName, + fileName, + fixturesPath, + filePaths: [fileName], }); - assertExpectedLintResults(t, res, fixturesPath, filePaths); - res.forEach((results) => { - results.filePath = testName; - }); - t.snapshot(res); - }); + } } } catch (err) { if (err instanceof Error) { @@ -114,6 +111,47 @@ export function createTestsForFixtures(fixturesPath: string) { } } +function testDefinition( + {testName, fileName, fixturesPath, filePaths, namespace}: + {testName: string; fileName: string; fixturesPath: string; filePaths: string[]; namespace?: string}) { + let defineTest = test.serial; + + if (fileName.startsWith("_")) { + // Skip tests for files starting with underscore + defineTest = defineTest.skip as typeof test; + testName = fileName.slice(1); + } else if (fileName.startsWith("only_")) { + // Only run test when file starts with only_ + defineTest = defineTest.only as typeof test; + testName = fileName.slice(5); + } + // Executing linting in parallel might lead to OOM errors in the CI + // Therefore always use serial + defineTest(`General: ${testName}`, async (t) => { + const {lintFile} = t.context; + + const res = await lintFile({ + rootDir: fixturesPath, + namespace, + pathsToLint: filePaths, + reportCoverage: true, + includeMessageDetails: true, + }); + assertExpectedLintResults(t, res, fixturesPath, filePaths); + res.forEach((result) => { + const resultFileName = path.basename(result.filePath); + if (resultFileName === fileName) { + // Use "clean" testName instead of the fileName which might contain modifiers like "only_" + result.filePath = testName; + } else { + // Use only the file name without the directory (which might contain modifiers) + result.filePath = resultFileName; + } + }); + t.snapshot(res); + }); +} + export function preprocessLintResultsForSnapshot(res: LintResult[]) { res.sort((a, b) => { return a.filePath.localeCompare(b.filePath); @@ -124,3 +162,13 @@ export function preprocessLintResultsForSnapshot(res: LintResult[]) { }); return res; } + +export function runLintRulesTests(filePath: string, fixturesPath?: string) { + if (!fixturesPath) { + const __dirname = path.dirname(filePath); + const fileName = path.basename(filePath, ".ts"); + fixturesPath = path.join(__dirname, "..", "..", "..", "fixtures", "linter", "rules", fileName); + } + + createTestsForFixtures(fixturesPath); +} diff --git a/test/lib/linter/rules/AsyncComponentFlags.ts b/test/lib/linter/rules/AsyncComponentFlags.ts new file mode 100644 index 00000000..11b62da1 --- /dev/null +++ b/test/lib/linter/rules/AsyncComponentFlags.ts @@ -0,0 +1,18 @@ +import {fileURLToPath} from "node:url"; +import {runLintRulesTests} from "../_linterHelper.js"; +import path from "node:path"; +import {readdirSync, lstatSync} from "node:fs"; + +const filePath = fileURLToPath(import.meta.url); +const __dirname = path.dirname(filePath); +const fileName = path.basename(filePath, ".ts"); +const fixturesPath = path.join(__dirname, "..", "..", "..", "fixtures", "linter", "rules", fileName); + +const testSubDirs = readdirSync(fixturesPath); + +for (const subDir of testSubDirs) { + const dirPath = path.join(fixturesPath, subDir); + if (lstatSync(dirPath).isDirectory()) { + runLintRulesTests(fileName, dirPath); + } +} diff --git a/test/lib/linter/rules/CSPCompliance.ts b/test/lib/linter/rules/CSPCompliance.ts index 57b18a4f..103ec030 100644 --- a/test/lib/linter/rules/CSPCompliance.ts +++ b/test/lib/linter/rules/CSPCompliance.ts @@ -1,10 +1,5 @@ -import path from "node:path"; import {fileURLToPath} from "node:url"; -import {createTestsForFixtures} from "../_linterHelper.js"; +import {runLintRulesTests} from "../_linterHelper.js"; const filePath = fileURLToPath(import.meta.url); -const __dirname = path.dirname(filePath); -const fileName = path.basename(filePath, ".ts"); -const fixturesPath = path.join(__dirname, "..", "..", "..", "fixtures", "linter", "rules", fileName); - -createTestsForFixtures(fixturesPath); +runLintRulesTests(filePath); diff --git a/test/lib/linter/rules/NoGlobals.ts b/test/lib/linter/rules/NoGlobals.ts index 57b18a4f..103ec030 100644 --- a/test/lib/linter/rules/NoGlobals.ts +++ b/test/lib/linter/rules/NoGlobals.ts @@ -1,10 +1,5 @@ -import path from "node:path"; import {fileURLToPath} from "node:url"; -import {createTestsForFixtures} from "../_linterHelper.js"; +import {runLintRulesTests} from "../_linterHelper.js"; const filePath = fileURLToPath(import.meta.url); -const __dirname = path.dirname(filePath); -const fileName = path.basename(filePath, ".ts"); -const fixturesPath = path.join(__dirname, "..", "..", "..", "fixtures", "linter", "rules", fileName); - -createTestsForFixtures(fixturesPath); +runLintRulesTests(filePath); diff --git a/test/lib/linter/rules/NoPseudoModules.ts b/test/lib/linter/rules/NoPseudoModules.ts index 57b18a4f..103ec030 100644 --- a/test/lib/linter/rules/NoPseudoModules.ts +++ b/test/lib/linter/rules/NoPseudoModules.ts @@ -1,10 +1,5 @@ -import path from "node:path"; import {fileURLToPath} from "node:url"; -import {createTestsForFixtures} from "../_linterHelper.js"; +import {runLintRulesTests} from "../_linterHelper.js"; const filePath = fileURLToPath(import.meta.url); -const __dirname = path.dirname(filePath); -const fileName = path.basename(filePath, ".ts"); -const fixturesPath = path.join(__dirname, "..", "..", "..", "fixtures", "linter", "rules", fileName); - -createTestsForFixtures(fixturesPath); +runLintRulesTests(filePath); diff --git a/test/lib/linter/rules/snapshots/AsyncComponentFlags.ts.md b/test/lib/linter/rules/snapshots/AsyncComponentFlags.ts.md new file mode 100644 index 00000000..31565e14 --- /dev/null +++ b/test/lib/linter/rules/snapshots/AsyncComponentFlags.ts.md @@ -0,0 +1,483 @@ +# Snapshot report for `test/lib/linter/rules/AsyncComponentFlags.ts` + +The actual snapshot is saved in `AsyncComponentFlags.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## General: Negative_01 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 0, + fatalErrorCount: 0, + filePath: 'Component.js', + messages: [], + warningCount: 0, + }, + ] + +## General: Negative_02 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 0, + fatalErrorCount: 0, + filePath: 'Component.js', + messages: [], + warningCount: 0, + }, + ] + +## General: Negative_03 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 0, + fatalErrorCount: 0, + filePath: 'Component.js', + messages: [], + warningCount: 0, + }, + ] + +## General: Negative_04 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 0, + fatalErrorCount: 0, + filePath: 'Component.js', + messages: [], + warningCount: 0, + }, + ] + +## General: Negative_05 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 0, + fatalErrorCount: 0, + filePath: 'Component.js', + messages: [], + warningCount: 0, + }, + ] + +## General: Negative_06 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 0, + fatalErrorCount: 0, + filePath: 'Component.js', + messages: [], + warningCount: 0, + }, + ] + +## General: Negative_07 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 0, + fatalErrorCount: 0, + filePath: 'Component.js', + messages: [], + warningCount: 0, + }, + ] + +## General: Negative_08 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 0, + fatalErrorCount: 0, + filePath: 'Component.js', + messages: [], + warningCount: 0, + }, + ] + +## General: Negative_09 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 0, + fatalErrorCount: 0, + filePath: 'Component.ts', + messages: [], + warningCount: 0, + }, + ] + +## General: Negative_10 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 0, + fatalErrorCount: 0, + filePath: 'Component.ts', + messages: [], + warningCount: 0, + }, + ] + +## General: Positive_01 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 1, + fatalErrorCount: 0, + filePath: 'Component.js', + messages: [ + { + column: 9, + fatal: undefined, + line: 6, + message: 'Component Root View and Routing are not configured to load their modules asynchronously.', + messageDetails: 'Use Asynchronous Loading (https://ui5.sap.com/1.120/#/topic/676b636446c94eada183b1218a824717). Implement sap.ui.core.IAsyncContentCreation interface in Component.js or set the "async" flags for "sap.ui5/routing/config" and "sap.ui5/rootView" in the component manifest.', + ruleId: 'ui5-linter-async-component-flags', + severity: 2, + }, + ], + warningCount: 0, + }, + ] + +## General: Positive_02 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 0, + fatalErrorCount: 0, + filePath: 'manifest.json', + messages: [ + { + column: 12, + line: 18, + message: 'Component implements the sap.ui.core.IAsyncContentCreation interface. The redundant "async" flag for "sap.ui5/rootView" should be removed from the component manifest', + messageDetails: 'sap.ui.core.IAsyncContentCreation (https://ui5.sap.com/1.120/#/api/sap.ui.core.IAsyncContentCreation)', + pos: 325, + ruleId: 'ui5-linter-async-component-flags', + severity: 1, + }, + { + column: 16, + line: 29, + message: 'Component implements the sap.ui.core.IAsyncContentCreation interface. The redundant "async" flag for "sap.ui5/routing/config" should be removed from the component manifest', + messageDetails: 'sap.ui.core.IAsyncContentCreation (https://ui5.sap.com/1.120/#/api/sap.ui.core.IAsyncContentCreation)', + pos: 551, + ruleId: 'ui5-linter-async-component-flags', + severity: 1, + }, + ], + warningCount: 2, + }, + { + coverageInfo: [], + errorCount: 0, + fatalErrorCount: 0, + filePath: 'Component.js', + messages: [], + warningCount: 0, + }, + ] + +## General: Positive_03 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 1, + fatalErrorCount: 0, + filePath: 'Component.js', + messages: [ + { + column: 9, + fatal: undefined, + line: 6, + message: 'Component Root View and Routing are not configured to load their modules asynchronously.', + messageDetails: 'Use Asynchronous Loading (https://ui5.sap.com/1.120/#/topic/676b636446c94eada183b1218a824717). Implement sap.ui.core.IAsyncContentCreation interface in Component.js or set the "async" flags for "sap.ui5/routing/config" and "sap.ui5/rootView" in the component manifest.', + ruleId: 'ui5-linter-async-component-flags', + severity: 2, + }, + ], + warningCount: 0, + }, + ] + +## General: Positive_04 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 0, + fatalErrorCount: 0, + filePath: 'Component.js', + messages: [ + { + column: 9, + fatal: undefined, + line: 6, + message: 'Component implements the sap.ui.core.IAsyncContentCreation interface. The redundant "async" flag for "sap.ui5/rootView" should be removed from the component manifest', + messageDetails: 'sap.ui.core.IAsyncContentCreation (https://ui5.sap.com/1.120/#/api/sap.ui.core.IAsyncContentCreation)', + ruleId: 'ui5-linter-async-component-flags', + severity: 1, + }, + { + column: 9, + fatal: undefined, + line: 6, + message: 'Component implements the sap.ui.core.IAsyncContentCreation interface. The redundant "async" flag for "sap.ui5/routing/config" should be removed from the component manifest', + messageDetails: 'sap.ui.core.IAsyncContentCreation (https://ui5.sap.com/1.120/#/api/sap.ui.core.IAsyncContentCreation)', + ruleId: 'ui5-linter-async-component-flags', + severity: 1, + }, + ], + warningCount: 2, + }, + ] + +## General: Positive_05 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 0, + fatalErrorCount: 0, + filePath: 'Component.js', + messages: [ + { + column: 9, + fatal: undefined, + line: 6, + message: 'Component implements the sap.ui.core.IAsyncContentCreation interface. The redundant "async" flag for "sap.ui5/rootView" should be removed from the component manifest', + messageDetails: 'sap.ui.core.IAsyncContentCreation (https://ui5.sap.com/1.120/#/api/sap.ui.core.IAsyncContentCreation)', + ruleId: 'ui5-linter-async-component-flags', + severity: 1, + }, + ], + warningCount: 1, + }, + ] + +## General: Positive_06 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 0, + fatalErrorCount: 0, + filePath: 'manifest.json', + messages: [ + { + column: 12, + line: 19, + message: 'Component implements the sap.ui.core.IAsyncContentCreation interface. The redundant "async" flag for "sap.ui5/rootView" should be removed from the component manifest', + messageDetails: 'sap.ui.core.IAsyncContentCreation (https://ui5.sap.com/1.120/#/api/sap.ui.core.IAsyncContentCreation)', + pos: 341, + ruleId: 'ui5-linter-async-component-flags', + severity: 1, + }, + ], + warningCount: 1, + }, + { + coverageInfo: [], + errorCount: 0, + fatalErrorCount: 0, + filePath: 'Component.js', + messages: [ + { + column: 9, + fatal: undefined, + line: 7, + message: 'Component does not specify that it uses the descriptor via the manifest.json file', + messageDetails: 'A manifest.json has been found in the same directory as the component. Although it will be used at runtime automatically, this should still be expressed in the metadata of the component class (https://ui5.sap.com/1.120/#/topic/0187ea5e2eff4166b0453b9dcc8fc64f).', + ruleId: 'ui5-linter-async-component-flags', + severity: 1, + }, + ], + warningCount: 1, + }, + ] + +## General: Positive_07 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 1, + fatalErrorCount: 0, + filePath: 'Component.js', + messages: [ + { + column: 9, + fatal: undefined, + line: 6, + message: 'Component Root View is not configured to load its views asynchronously.', + messageDetails: 'Use Asynchronous Loading (https://ui5.sap.com/1.120/#/topic/676b636446c94eada183b1218a824717). Implement sap.ui.core.IAsyncContentCreation interface in Component.js or set the "async" flag for "sap.ui5/rootView" in the component manifest.', + ruleId: 'ui5-linter-async-component-flags', + severity: 2, + }, + ], + warningCount: 0, + }, + ] + +## General: Positive_08 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 1, + fatalErrorCount: 0, + filePath: 'Component.js', + messages: [ + { + column: 9, + fatal: undefined, + line: 6, + message: 'Component Routing is not configured to load its targets asynchronously.', + messageDetails: 'Use Asynchronous Loading (https://ui5.sap.com/1.120/#/topic/676b636446c94eada183b1218a824717). Implement sap.ui.core.IAsyncContentCreation interface in Component.js or set the "async" flag for "sap.ui5/routing/config" in the component manifest.', + ruleId: 'ui5-linter-async-component-flags', + severity: 2, + }, + ], + warningCount: 0, + }, + ] + +## General: Positive_09 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 1, + fatalErrorCount: 0, + filePath: 'Component.js', + messages: [ + { + column: 9, + fatal: undefined, + line: 6, + message: 'Component Root View and Routing are not configured to load their modules asynchronously.', + messageDetails: 'Use Asynchronous Loading (https://ui5.sap.com/1.120/#/topic/676b636446c94eada183b1218a824717). Implement sap.ui.core.IAsyncContentCreation interface in Component.js or set the "async" flags for "sap.ui5/routing/config" and "sap.ui5/rootView" in the component manifest.', + ruleId: 'ui5-linter-async-component-flags', + severity: 2, + }, + ], + warningCount: 0, + }, + ] + +## General: Positive_10 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 1, + fatalErrorCount: 0, + filePath: 'Component.js', + messages: [ + { + column: 9, + fatal: undefined, + line: 5, + message: 'Component Root View and Routing are not configured to load their modules asynchronously.', + messageDetails: 'Use Asynchronous Loading (https://ui5.sap.com/1.120/#/topic/676b636446c94eada183b1218a824717). Implement sap.ui.core.IAsyncContentCreation interface in Component.js or set the "async" flags for "sap.ui5/routing/config" and "sap.ui5/rootView" in the component manifest.', + ruleId: 'ui5-linter-async-component-flags', + severity: 2, + }, + ], + warningCount: 0, + }, + ] + +## General: Positive_11 + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 1, + fatalErrorCount: 0, + filePath: 'Component.ts', + messages: [ + { + column: 1, + fatal: undefined, + line: 5, + message: 'Component does not specify that it uses the descriptor via the manifest.json file', + messageDetails: 'A manifest.json has been found in the same directory as the component. Although it will be used at runtime automatically, this should still be expressed in the metadata of the component class (https://ui5.sap.com/1.120/#/topic/0187ea5e2eff4166b0453b9dcc8fc64f).', + ruleId: 'ui5-linter-async-component-flags', + severity: 1, + }, + { + column: 1, + fatal: undefined, + line: 5, + message: 'Component Root View and Routing are not configured to load their modules asynchronously.', + messageDetails: 'Use Asynchronous Loading (https://ui5.sap.com/1.120/#/topic/676b636446c94eada183b1218a824717). Implement sap.ui.core.IAsyncContentCreation interface in Component.ts or set the "async" flags for "sap.ui5/routing/config" and "sap.ui5/rootView" in the component manifest.', + ruleId: 'ui5-linter-async-component-flags', + severity: 2, + }, + ], + warningCount: 1, + }, + ] diff --git a/test/lib/linter/rules/snapshots/AsyncComponentFlags.ts.snap b/test/lib/linter/rules/snapshots/AsyncComponentFlags.ts.snap new file mode 100644 index 0000000000000000000000000000000000000000..20d8a8dda6c723ef0c22993ea859743d426b6d27 GIT binary patch literal 1902 zcmV-!2a)(eRzVSuU_ufEt?jO!sY_4S&{e&gMSPHGGzkfkT!Io2FF{1a1VhNvg%CspBf$q>M10VY zmvD*?QBfZ>J^yZ}Z+2#HZs&I9=zZDguBqy(-|yF--><)2bJkxCl>4ZD;bmc@352;z zgd*a>ZR|c#v`PphsifBBH~qXBJ>r=4E7V+X}d;5k&2F~I5f>uHd0@h@wwr_okPA= z@MR!QaZ~GF^O&kPR477IJgu8Es%@H4p)R!MRiRVE>|N(kD8p*+l?pw!vwCdT+h%WS z8{o5r%x6>Qv%{Isrp{-Jna`%qXG@vSrp{-T%x6>Qv*pZZQ|Ggl%x6>Qvm=?$hWN}( zn$ODfna`%gXQz~wTk0V8@g4QqTL9h%FiU`2w(GNdNA+1UmGQt)j>l<)KKt>&X9%|R z-X;tGkhCpa_!9x%Cjg)T&USJg}9VtHY{^qG=T4e6FD%azs2;mYD-#XYhpxW~&YhgZuB zWx6uljS8t#IoqBNEA;ruXs#_Mug^AB)E|(WHK}&^0_OR4B6&j zV=7`=m}J3huDQ-R<_Em0nXe)?*BW`L6tzrNYME-TX~s>B$|S~}O9oE{a=V7F^HBOi zo3zbxQ5=X9Ugwa-a_LaeoaCYABo1|lIMkd?`UcWsO+?acv_pXoU``($FaOFW=2-Ip zDgf>n#vR(cEADP}yq1tBlK-TMpmzZL8o&ntas)U?fJX>mh9Iaw^4}vsa;KdmKysg1 z6qu*L7ig+$ehYfl>F#EneO)J?)+%gH=}9^gy%Gy|m-YHCFY96^zc<>dhBDdukOF_F zz<=l2ae}~$MdP~xl7Cpdk4_}6NBkbe~b(GmmN~-CY~+nu-IDttS2IYE5{|k5qjNFm5ZC5HWnp19J**$kNau4y5K{HB@T? zHoY@l=j<@Q$u@#vc0e%PI3yT;cEy9CH(g$ZA@Mmn=8zbW()+yT$sQxJ$B67PB72M& z+hatZ*Oa-tcVBKAnK@&tl1D^NQlkzjlb<6%^0f5wFz&R<-Jg?AxqF^;%iUW)MoyUm zCn@kK1-?yzdoAD(7VxPd2r7`miVb|;2EJ?qkJ-Q(8~CaNoOgg*bC+4}j)`DdDSX*= z&n}d^;|hkX6rRkG7?67U?6$Bz%u>(Jr52B_a%_Rh8x z?(XqPtHa`FE#Qm=ylw&SS*^mIIl%1>@U`JG_e&0N)&bsjfIm9GzZ{^J1764jzfa5D zdv;Lf-k;(6%4wPpOn}TiYF`do-^L=E+F7MnxNp>)uEzvZH~i zskmB8Cb}+zAc?$K3y*QbqBu0NE*OuEs&gY<9t0Z)nW;(LiBoObeZ*spNa&9|423+gy$ZYfFm1V(~#Dehs#d4*xI={Gdc=d?qx+}h0S@aK#S-2WhBlOur zYTuBFoIemWXlsmus%(-uZnA84&+UaqQ4(*}&5_@KYQ3l@0va1|D{RXL7)A oFL%dK+9f-MuuLqzn#JN>46H0S4$Y0`YRQfN2eQ9BDH%-w0H;K-+W-In literal 0 HcmV?d00001 diff --git a/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.md b/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.md index 9880d65f..c288160a 100644 --- a/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.md +++ b/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.md @@ -408,7 +408,7 @@ Generated by [AVA](https://avajs.dev). column: 1, line: 6, message: 'Usage of native HTML in XML Views/Fragments is deprecated', - messageDetails: '{@link topic:be54950cae1041f59d4aa97a6bade2d8 Using Native HTML in XML Views (deprecated)}', + messageDetails: 'Using Native HTML in XML Views (deprecated) (https://ui5.sap.com/1.120/#/topic/be54950cae1041f59d4aa97a6bade2d8)', ruleId: 'ui5-linter-no-deprecated-api', severity: 2, }, diff --git a/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.snap b/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.snap index 1b59165a3fed7e39b22cf37a8483e0ac9ce4a3b8..f0bf172740a007417cd4404cdd75a275da11c2a0 100644 GIT binary patch literal 6729 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