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 8ce3f879..15eb006d 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 00000000..20d8a8dd Binary files /dev/null and b/test/lib/linter/rules/snapshots/AsyncComponentFlags.ts.snap differ 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 1b59165a..f0bf1727 100644 Binary files a/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.snap and b/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.snap differ diff --git a/test/lib/linter/xmlTemplate/snapshots/transpiler.ts.md b/test/lib/linter/xmlTemplate/snapshots/transpiler.ts.md index 9882dca2..fc86bf52 100644 --- a/test/lib/linter/xmlTemplate/snapshots/transpiler.ts.md +++ b/test/lib/linter/xmlTemplate/snapshots/transpiler.ts.md @@ -701,7 +701,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/xmlTemplate/snapshots/transpiler.ts.snap b/test/lib/linter/xmlTemplate/snapshots/transpiler.ts.snap index 33cb3f6f..b7f8891d 100644 Binary files a/test/lib/linter/xmlTemplate/snapshots/transpiler.ts.snap and b/test/lib/linter/xmlTemplate/snapshots/transpiler.ts.snap differ