diff --git a/src/cli/base.ts b/src/cli/base.ts index acb72e5e..543007a1 100644 --- a/src/cli/base.ts +++ b/src/cli/base.ts @@ -18,6 +18,7 @@ export interface LinterArg { details: boolean; format: string; config?: string; + ui5Config?: string; } // yargs type defition is missing the "middelwares" property for the CommandModule type @@ -87,6 +88,10 @@ const lintCommand: FixedCommandModule = { type: "string", choices: ["stylish", "json", "markdown"], }) + .option("ui5-config", { + describe: "Set a custom path for the UI5 Config (default: './ui5.yaml')", + type: "string", + }) .coerce([ // base.js "log-level", @@ -123,6 +128,7 @@ async function handleLint(argv: ArgumentsCamelCase) { details, format, config, + ui5Config, } = argv; let profile; @@ -140,6 +146,7 @@ async function handleLint(argv: ArgumentsCamelCase) { reportCoverage, includeMessageDetails: details, configPath: config, + ui5ConfigPath: ui5Config, }); if (reportCoverage) { diff --git a/src/linter/LinterContext.ts b/src/linter/LinterContext.ts index 206dfd31..8a0ce42a 100644 --- a/src/linter/LinterContext.ts +++ b/src/linter/LinterContext.ts @@ -63,6 +63,7 @@ export interface LinterOptions { reportCoverage?: boolean; includeMessageDetails?: boolean; configPath?: string; + ui5ConfigPath?: string; } export interface LinterParameters { diff --git a/src/linter/lintWorkspace.ts b/src/linter/lintWorkspace.ts index 066b7ea6..3eb173cb 100644 --- a/src/linter/lintWorkspace.ts +++ b/src/linter/lintWorkspace.ts @@ -9,9 +9,10 @@ import TypeLinter from "./ui5Types/TypeLinter.js"; import LinterContext, {LintResult, LinterParameters, LinterOptions} from "./LinterContext.js"; import {createReader} from "@ui5/fs/resourceFactory"; import {resolveIgnoresReader} from "./linter.js"; +import {UI5LintConfigType} from "../utils/ConfigManager.js"; export default async function lintWorkspace( - workspace: AbstractAdapter, options: LinterOptions + workspace: AbstractAdapter, options: LinterOptions, config: UI5LintConfigType ): Promise { const done = taskStart("Linting Workspace"); @@ -22,7 +23,8 @@ export default async function lintWorkspace( createReader({ fsBasePath: options.rootDir, virBasePath: "/", - }) + }), + config )); const params: LinterParameters = { diff --git a/src/linter/linter.ts b/src/linter/linter.ts index 3816a191..9654c37e 100644 --- a/src/linter/linter.ts +++ b/src/linter/linter.ts @@ -8,36 +8,20 @@ import posixPath from "node:path/posix"; import {stat} from "node:fs/promises"; import {ProjectGraph} from "@ui5/project"; import type {AbstractReader, Resource} from "@ui5/fs"; -import ConfigManager from "../utils/ConfigManager.js"; +import ConfigManager, {UI5LintConfigType} from "../utils/ConfigManager.js"; import {Minimatch} from "minimatch"; -async function lint( - resourceReader: AbstractReader, options: LinterOptions -): Promise { - const lintEnd = taskStart("Linting"); - const {ignorePattern, configPath} = options; - - const ignoresReader = await resolveIgnoresReader( - ignorePattern, - options.rootDir, - resourceReader, - configPath - ); - - const workspace = createWorkspace({ - reader: ignoresReader, - }); - - const res = await lintWorkspace(workspace, options); - lintEnd(); - return res; -} - export async function lintProject({ - rootDir, pathsToLint, ignorePattern, reportCoverage, includeMessageDetails, configPath, + rootDir, pathsToLint, ignorePattern, reportCoverage, includeMessageDetails, configPath, ui5ConfigPath, }: LinterOptions): Promise { + const configMngr = new ConfigManager(rootDir, configPath); + const config = await configMngr.getConfiguration(); + + // In case path is set both by CLI and config use CLI + ui5ConfigPath = ui5ConfigPath ?? config.ui5Config; + const projectGraphDone = taskStart("Project Graph creation"); - const graph = await getProjectGraph(rootDir); + const graph = await getProjectGraph(rootDir, ui5ConfigPath); const project = graph.getRoot(); projectGraphDone(); @@ -84,7 +68,8 @@ export async function lintProject({ reportCoverage, includeMessageDetails, configPath, - }); + ui5ConfigPath, + }, config); const relFsBasePath = path.relative(rootDir, fsBasePath); const relFsBasePathTest = fsBasePathTest ? path.relative(rootDir, fsBasePathTest) : undefined; @@ -102,6 +87,9 @@ export async function lintProject({ export async function lintFile({ rootDir, pathsToLint, ignorePattern, namespace, reportCoverage, includeMessageDetails, configPath, }: LinterOptions): Promise { + const configMngr = new ConfigManager(rootDir, configPath); + const config = await configMngr.getConfiguration(); + const reader = createReader({ fsBasePath: rootDir, virBasePath: namespace ? `/resources/${namespace}/` : "/", @@ -121,7 +109,7 @@ export async function lintFile({ reportCoverage, includeMessageDetails, configPath, - }); + }, config); res.forEach((result) => { result.filePath = transformVirtualPathToFilePath(result.filePath, "", "/"); @@ -132,12 +120,36 @@ export async function lintFile({ return res; } -async function getProjectGraph(rootDir: string): Promise { +async function lint( + resourceReader: AbstractReader, options: LinterOptions, config: UI5LintConfigType +): Promise { + const lintEnd = taskStart("Linting"); + const {ignorePattern, ui5ConfigPath} = options; + + const ignoresReader = await resolveIgnoresReader( + ignorePattern, + options.rootDir, + resourceReader, + config, + ui5ConfigPath + ); + + const workspace = createWorkspace({ + reader: ignoresReader, + }); + + const res = await lintWorkspace(workspace, options, config); + lintEnd(); + return res; +} + +async function getProjectGraph(rootDir: string, ui5ConfigPath?: string): Promise { let rootConfigPath, rootConfiguration; - const ui5YamlPath = path.join(rootDir, "ui5.yaml"); + const ui5YamlPath = ui5ConfigPath ? path.join(rootDir, ui5ConfigPath) : path.join(rootDir, "ui5.yaml"); if (await fileExists(ui5YamlPath)) { rootConfigPath = ui5YamlPath; } else { + if (ui5ConfigPath) throw new Error(`Unable to find UI5 config file '${ui5ConfigPath}'`); const isApp = await dirExists(path.join(rootDir, "webapp")); if (isApp) { rootConfiguration = { @@ -309,13 +321,14 @@ export async function resolveIgnoresReader( ignorePattern: string[] | undefined, projectRootDir: string, resourceReader: AbstractReader, - configPath?: string) { + config: UI5LintConfigType, + ui5ConfigPath?: string) { let fsBasePath = ""; let fsBasePathTest = ""; let virBasePath = "/resources/"; let virBasePathTest = "/test-resources/"; try { - const graph = await getProjectGraph(projectRootDir); + const graph = await getProjectGraph(projectRootDir, ui5ConfigPath); const project = graph.getRoot(); projectRootDir = project.getRootPath(); fsBasePath = project.getSourcePath(); @@ -334,11 +347,9 @@ export async function resolveIgnoresReader( const relFsBasePath = path.relative(projectRootDir, fsBasePath); const relFsBasePathTest = fsBasePathTest ? path.relative(projectRootDir, fsBasePathTest) : undefined; - const configMngr = new ConfigManager(projectRootDir, configPath); - const config = await configMngr.getConfiguration(); ignorePattern = [ ...(config.ignores ?? []), - ...(ignorePattern ?? []), // CLI patterns go after config patterns + ...(ignorePattern ?? []), // patterns set by CLI go after config patterns ].filter(($) => $); // Patterns must be only relative (to project's root), diff --git a/src/utils/ConfigManager.ts b/src/utils/ConfigManager.ts index 2c16f184..71a73d7c 100644 --- a/src/utils/ConfigManager.ts +++ b/src/utils/ConfigManager.ts @@ -4,6 +4,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); export interface UI5LintConfigType { ignores?: string[]; + ui5Config?: string; }; const CONFIG_FILENAMES = [ diff --git a/test/fixtures/linter/projects/com.ui5.troublesome.app/configs/ui5-custom.yaml b/test/fixtures/linter/projects/com.ui5.troublesome.app/configs/ui5-custom.yaml new file mode 100644 index 00000000..212d8d53 --- /dev/null +++ b/test/fixtures/linter/projects/com.ui5.troublesome.app/configs/ui5-custom.yaml @@ -0,0 +1,15 @@ +specVersion: '4.0' +metadata: + name: com.ui5.troublesome.app +type: application +framework: + name: OpenUI5 + version: "1.121.0" + libraries: + - name: sap.m + - name: sap.ui.core + - name: sap.landvisz +resources: + configuration: + paths: + webapp: webapp2 \ No newline at end of file diff --git a/test/fixtures/linter/projects/com.ui5.troublesome.app/webapp2/manifest.json b/test/fixtures/linter/projects/com.ui5.troublesome.app/webapp2/manifest.json new file mode 100644 index 00000000..92d1d830 --- /dev/null +++ b/test/fixtures/linter/projects/com.ui5.troublesome.app/webapp2/manifest.json @@ -0,0 +1,113 @@ +{ + "_version": "1.12.0", + + "sap.app": { + "id": "com.ui5.troublesome.app", + "type": "application", + "i18n": "i18n/i18n.properties", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "applicationVersion": { + "version": "1.0.0" + }, + "dataSources": { + "v4": { + "uri": "/api/odata-4/", + "type": "OData", + "settings": { + "odataVersion": "4.0" + } + } + } + }, + + "sap.ui": { + "technology": "UI5", + "icons": {}, + "deviceTypes": { + "desktop": true, + "tablet": true, + "phone": true + } + }, + + "sap.ui5": { + "rootView": { + "viewName": "com.ui5.troublesome.app.view.App", + "type": "XML", + "async": true, + "id": "app" + }, + + "dependencies": { + "minUI5Version": "1.119.0", + "libs": { + "sap.ui.core": {}, + "sap.m": {}, + "sap.ui.commons": {} + } + }, + + "handleValidation": true, + + "contentDensities": { + "compact": true, + "cozy": true + }, + + "resources": { + "js": [{ "uri": "path/to/thirdparty.js" }] + }, + + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "com.ui5.troublesome.app.i18n.i18n" + } + }, + "odata-v4": { + "type": "sap.ui.model.odata.v4.ODataModel", + "settings": { + "synchronizationMode": "None" + } + }, + "odata-v4-via-dataSource": { + "dataSource": "v4", + "settings": { + "synchronizationMode": "None" + } + }, + "odata": { + "type": "sap.ui.model.odata.ODataModel", + "settings": { + "serviceUrl": "/api/odata" + } + } + }, + + "routing": { + "config": { + "routerClass": "sap.m.routing.Router", + "viewType": "XML", + "viewPath": "com.ui5.troublesome.app.view", + "controlId": "app", + "controlAggregation": "pages", + "async": true + }, + "routes": [ + { + "pattern": "", + "name": "main", + "target": "main" + } + ], + "targets": { + "main": { + "viewId": "main", + "viewName": "Main" + } + } + } + } +} diff --git a/test/lib/cli/base.ts b/test/lib/cli/base.ts index d7cfd242..42837293 100644 --- a/test/lib/cli/base.ts +++ b/test/lib/cli/base.ts @@ -106,7 +106,7 @@ test.serial("ui5lint (default) ", async (t) => { t.is(writeFile.callCount, 0, "Coverage was not called"); t.deepEqual(lintProject.getCall(0).args[0], { rootDir: path.join(process.cwd()), pathsToLint: undefined, ignorePattern: undefined, configPath: undefined, - includeMessageDetails: false, reportCoverage: false, + includeMessageDetails: false, reportCoverage: false, ui5ConfigPath: undefined, }); t.is(t.context.consoleLogStub.callCount, 0, "console.log should not be used"); }); @@ -123,7 +123,7 @@ test.serial("ui5lint --file-paths ", async (t) => { t.true(lintProject.calledOnce, "Linter is called"); t.deepEqual(lintProject.getCall(0).args[0], { rootDir: path.join(process.cwd()), pathsToLint: filePaths, ignorePattern: undefined, configPath: undefined, - includeMessageDetails: false, reportCoverage: false, + includeMessageDetails: false, reportCoverage: false, ui5ConfigPath: undefined, }); t.is(t.context.consoleLogStub.callCount, 0, "console.log should not be used"); }); @@ -137,7 +137,7 @@ test.serial("ui5lint --coverage ", async (t) => { t.is(writeFile.callCount, 1, "Coverage was called"); t.deepEqual(lintProject.getCall(0).args[0], { rootDir: path.join(process.cwd()), pathsToLint: undefined, ignorePattern: undefined, configPath: undefined, - includeMessageDetails: false, reportCoverage: true, + includeMessageDetails: false, reportCoverage: true, ui5ConfigPath: undefined, }); t.is(t.context.consoleLogStub.callCount, 0, "console.log should not be used"); }); @@ -169,7 +169,7 @@ test.serial("ui5lint --ignore-pattern ", async (t) => { t.true(lintProject.calledOnce, "Linter is called"); t.deepEqual(lintProject.getCall(0).args[0], { rootDir: path.join(process.cwd()), pathsToLint: undefined, ignorePattern: ["test/**/*"], configPath: undefined, - includeMessageDetails: false, reportCoverage: false, + includeMessageDetails: false, reportCoverage: false, ui5ConfigPath: undefined, }); }); @@ -190,7 +190,19 @@ test.serial("ui5lint --config", async (t) => { t.true(lintProject.calledOnce, "Linter is called"); t.deepEqual(lintProject.getCall(0).args[0], { rootDir: path.join(process.cwd()), pathsToLint: undefined, ignorePattern: undefined, configPath: "config.js", - includeMessageDetails: false, reportCoverage: false, + includeMessageDetails: false, reportCoverage: false, ui5ConfigPath: undefined, + }); +}); + +test.serial("ui5lint --ui5-config", async (t) => { + const {cli, lintProject} = t.context; + + await cli.parseAsync(["--ui5-config", "ui5.yaml"]); + + t.true(lintProject.calledOnce, "Linter is called"); + t.deepEqual(lintProject.getCall(0).args[0], { + rootDir: path.join(process.cwd()), pathsToLint: undefined, ignorePattern: undefined, configPath: undefined, + includeMessageDetails: false, reportCoverage: false, ui5ConfigPath: "ui5.yaml", }); }); diff --git a/test/lib/linter/linter.ts b/test/lib/linter/linter.ts index 2a2766aa..cab58d12 100644 --- a/test/lib/linter/linter.ts +++ b/test/lib/linter/linter.ts @@ -142,3 +142,32 @@ test.serial("lint: All files of com.ui5.troublesome.app with custom config", asy t.snapshot(preprocessLintResultsForSnapshot(res)); }); + +test.serial("lint: com.ui5.troublesome.app with custom UI5 config", async (t) => { + const projectPath = path.join(fixturesProjectsPath, "com.ui5.troublesome.app"); + const {lintProject} = t.context; + + const res = await lintProject({ + rootDir: projectPath, + pathsToLint: [], + reportCoverage: true, + includeMessageDetails: true, + ui5ConfigPath: "./configs/ui5-custom.yaml", + }); + + t.snapshot(preprocessLintResultsForSnapshot(res)); +}); + +test.only("lint: com.ui5.troublesome.app with custom UI5 config which does NOT exist", async (t) => { + const projectPath = path.join(fixturesProjectsPath, "com.ui5.troublesome.app"); + const {lintProject} = t.context; + const ui5ConfigPath = "./configs/ui5-DOES-NOT-EXIST.yaml"; + + await t.throwsAsync(lintProject({ + rootDir: projectPath, + pathsToLint: [], + reportCoverage: true, + includeMessageDetails: true, + ui5ConfigPath, + }), {message: `Unable to find UI5 config file '${ui5ConfigPath}'`}); +}); diff --git a/test/lib/linter/snapshots/linter.ts.md b/test/lib/linter/snapshots/linter.ts.md index 3ae6677e..5477e215 100644 --- a/test/lib/linter/snapshots/linter.ts.md +++ b/test/lib/linter/snapshots/linter.ts.md @@ -1652,3 +1652,74 @@ Generated by [AVA](https://avajs.dev). warningCount: 0, }, ] + +## lint: All files of com.ui5.troublesome.app with custom UI5 config + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 1, + fatalErrorCount: 0, + filePath: 'ui5.yaml', + messages: [ + { + column: 7, + line: 11, + message: 'Use of deprecated library \'sap.landvisz\'', + ruleId: 'no-deprecated-library', + severity: 2, + }, + ], + warningCount: 0, + }, + { + coverageInfo: [], + errorCount: 5, + fatalErrorCount: 0, + filePath: 'webapp2/manifest.json', + messages: [ + { + column: 17, + line: 47, + message: 'Use of deprecated library \'sap.ui.commons\'', + ruleId: 'no-deprecated-library', + severity: 2, + }, + { + column: 13, + line: 59, + message: 'Use of deprecated property \'sap.ui5/resources/js\'', + messageDetails: 'As of version 1.94, the usage of js resources is deprecated. Please use regular dependencies instead.', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 21, + line: 72, + message: 'Usage of deprecated parameter \'synchronizationMode\' of constructor \'sap/ui/model/odata/v4/ODataModel\' (model: \'odata-v4\')', + messageDetails: 'As of version 1.110.0, parameter \'synchronizationMode\' is obsolete and must be omitted. See API reference (https://ui5.sap.com/1.120/#/api/sap/ui/model/odata/v4/ODataModel#constructor)', + ruleId: 'no-deprecated-parameter', + severity: 2, + }, + { + column: 21, + line: 78, + message: 'Usage of deprecated parameter \'synchronizationMode\' of constructor \'sap/ui/model/odata/v4/ODataModel\' (model: \'odata-v4-via-dataSource\')', + messageDetails: 'As of version 1.110.0, parameter \'synchronizationMode\' is obsolete and must be omitted. See API reference (https://ui5.sap.com/1.120/#/api/sap/ui/model/odata/v4/ODataModel#constructor)', + ruleId: 'no-deprecated-parameter', + severity: 2, + }, + { + column: 17, + line: 82, + message: 'Use of deprecated class \'sap.ui.model.odata.ODataModel\'', + messageDetails: 'sap.ui.model.odata.ODataModel (https://ui5.sap.com/1.120/#/api/sap.ui.model.odata.ODataModel)', + ruleId: 'no-deprecated-api', + severity: 2, + }, + ], + warningCount: 0, + }, + ] diff --git a/test/lib/linter/snapshots/linter.ts.snap b/test/lib/linter/snapshots/linter.ts.snap index 6ecc9e77..a13ad3f8 100644 Binary files a/test/lib/linter/snapshots/linter.ts.snap and b/test/lib/linter/snapshots/linter.ts.snap differ