diff --git a/README.md b/README.md index ee6dde497..bf222c88c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,35 @@ [![npm Package Version](https://badge.fury.io/js/%40ui5%2Flinter.svg)](https://www.npmjs.com/package/@ui5/linter) [![Coverage Status](https://coveralls.io/repos/github/SAP/ui5-linter/badge.svg)](https://coveralls.io/github/SAP/ui5-linter) +- [UI5 Linter](#ui5-linter) + - [Description](#description) + - [Features](#features) + - [Rules](#rules) + - [Requirements](#requirements) + - [Installation](#installation) + - [Usage](#usage) + - [Options](#options) + - [`--details`](#--details) + - [`--format`](#--format) + - [`--ignore-pattern`](#--ignore-pattern) + - [`--config`](#--config) + - [`--ui5-config`](#--ui5-config) + - [Configuration](#configuration) + - [Configuration File Location](#configuration-file-location) + - [Supported Configuration File Names](#supported-configuration-file-names) + - [Configuration File Format](#configuration-file-format) + - [ESM (ECMAScript Modules):](#esm-ecmascript-modules) + - [CommonJS:](#commonjs) + - [Configuration Options](#configuration-options) + - [Directives](#directives) + - [Specifying Rules](#specifying-rules) + - [Scope](#scope) + - [Internals](#internals) + - [Support, Feedback, Contributing](#support-feedback-contributing) + - [Security / Disclosure](#security--disclosure) + - [Code of Conduct](#code-of-conduct) + - [Licensing](#licensing) + ## Description UI5 linter is a static code analysis tool for UI5 projects. @@ -27,6 +56,12 @@ UI5 linter scans your UI5 project and detects issues that might interfere with i > [!NOTE] > While UI5 linter already provides many detection features, it is not yet covering all aspects and best practices for UI5 2.x. The intention of UI5 linter is to detect as many issues as possible that a project running with UI5 2.x might be facing. However, you'll still need to test your UI5 project with UI5 2.x as soon as it is made available. To reveal additional issues, the UI5 team plans to release more versions of UI5 linter over the next months. +## Rules + +UI5 linter comes with a set of predefined rules that are enabled by default. You can disable specific rules in the code via [Directives](#directives). + +A list of all available rules can be found on the [Rules](./docs/Rules.md) page. + ## Requirements - [Node.js](https://nodejs.org/) Version v20.11.x, v22.0.0, or higher diff --git a/docs/PERFORMANCE.md b/docs/Performance.md similarity index 100% rename from docs/PERFORMANCE.md rename to docs/Performance.md diff --git a/docs/Rules.md b/docs/Rules.md new file mode 100644 index 000000000..40df46ab0 --- /dev/null +++ b/docs/Rules.md @@ -0,0 +1,84 @@ +# Rules Reference + +- [Rules Reference](#rules-reference) + - [async-component-flags](#async-component-flags) + - [csp-unsafe-inline-script](#csp-unsafe-inline-script) + - [no-deprecated-api](#no-deprecated-api) + - [no-deprecated-component](#no-deprecated-component) + - [no-deprecated-control-renderer-declaration](#no-deprecated-control-renderer-declaration) + - [no-deprecated-library](#no-deprecated-library) + - [no-deprecated-theme](#no-deprecated-theme) + - [no-globals](#no-globals) + - [no-pseudo-modules](#no-pseudo-modules) + - [parsing-error](#parsing-error) + - [ui5-class-declaration](#ui5-class-declaration) + +## async-component-flags + +Checks whether a Component is configured for asynchronous loading via the `sap.ui.core.IAsyncContentCreation` interface in the Component metadata or via `async` flags in the `manifest.json`. + +**Related information** +- [Use Asynchronous Loading](https://ui5.sap.com/#/topic/676b636446c94eada183b1218a824717) +- [Component Metadata](https://ui5.sap.com/#/topic/0187ea5e2eff4166b0453b9dcc8fc64f) +- [sap.ui.core.IAsyncContentCreation](https://ui5.sap.com/1.120/#/api/sap.ui.core.IAsyncContentCreation) + +## csp-unsafe-inline-script + +Checks whether inline scripts are used in HTML files in accordance with Content Security Policy (CSP) best practices. + +**Related information** +- [Content Security Policy](https://ui5.sap.com/#/topic/fe1a6dba940e479fb7c3bc753f92b28c) + +## no-deprecated-api + +Checks whether deprecated APIs, features or parameters are used in the project. + +**Related information** +- [Best Practices for Developers](https://ui5.sap.com/#/topic/28fcd55b04654977b63dacbee0552712) + +## no-deprecated-component + +Checks for dependencies to deprecated components in `manifest.json`. + +**Related information** +- [Deprecated Themes and Libraries](https://ui5.sap.com/#/topic/a87ca843bcee469f82a9072927a7dcdb) + +## no-deprecated-control-renderer-declaration + +Checks whether the renderer of a control is declared correctly. + +## no-deprecated-library + +Checks for dependencies to deprecated libraries in `manifest.json` and `ui5.yaml`. + +**Related information** +- [Deprecated Themes and Libraries](https://ui5.sap.com/#/topic/a87ca843bcee469f82a9072927a7dcdb) + +## no-deprecated-theme + +Checks for usage of deprecated themes in the code and HTML files. + +**Related information** +- [Deprecated Themes and Libraries](https://ui5.sap.com/#/topic/a87ca843bcee469f82a9072927a7dcdb) + +## no-globals + +Checks for the usage of global variables in the code. + +**Related information** +- [Best Practices for Developers](https://ui5.sap.com/#/topic/28fcd55b04654977b63dacbee0552712) + +## no-pseudo-modules + +Checks for dependencies to pseudo modules in the code. + +**Related information** +- [Best Practices for Loading Modules - Migrating Access to Pseudo Modules](https://ui5.sap.com/#/topic/00737d6c1b864dc3ab72ef56611491c4) + +## parsing-error + +Syntax/parsing errors that appear during the linting process are reported with this rule. + +## ui5-class-declaration + +Checks whether the declaration of UI5 classes is correct. This rule only applies to TypeScript code where built-in ECMAScript classes are used instead of an `.extend()` call. diff --git a/src/cli.ts b/src/cli.ts index 5587b9ba2..b0bb60230 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,7 +2,7 @@ import yargs from "yargs"; import {hideBin} from "yargs/helpers"; import base from "./cli/base.js"; import {fileURLToPath} from "node:url"; -import {setVersion} from "./cli/version.js"; +import {getFormattedVersion, setVersionInfo} from "./cli/version.js"; import {createRequire} from "module"; export default async function () { @@ -17,10 +17,9 @@ export default async function () { const require = createRequire(import.meta.url); const pkg = require("../package.json") as {version: string}; const ui5LintJsPath = fileURLToPath(new URL("../bin/ui5lint.js", import.meta.url)); - const pkgVersion = `${pkg.version} (from ${ui5LintJsPath})`; - setVersion(pkgVersion); - cli.version(pkgVersion); + setVersionInfo(pkg.version, ui5LintJsPath); + cli.version(getFormattedVersion()); // Explicitly set script name to prevent windows from displaying "ui5-linter.js" cli.scriptName("ui5lint"); diff --git a/src/cli/base.ts b/src/cli/base.ts index 544b82bea..df4f5d543 100644 --- a/src/cli/base.ts +++ b/src/cli/base.ts @@ -9,6 +9,7 @@ import baseMiddleware from "./middlewares/base.js"; import chalk from "chalk"; import {isLogLevelEnabled} from "@ui5/logger"; import ConsoleWriter from "@ui5/logger/writers/Console"; +import {getVersion} from "./version.js"; export interface LinterArg { coverage: boolean; @@ -172,7 +173,7 @@ async function handleLint(argv: ArgumentsCamelCase) { process.stdout.write("\n"); } else if (format === "markdown") { const markdownFormatter = new Markdown(); - process.stdout.write(markdownFormatter.format(res, details)); + process.stdout.write(markdownFormatter.format(res, details, getVersion())); process.stdout.write("\n"); } else if (format === "" || format === "stylish") { const textFormatter = new Text(rootDir); diff --git a/src/cli/middlewares/logger.ts b/src/cli/middlewares/logger.ts index d910cc72b..9cc11609b 100644 --- a/src/cli/middlewares/logger.ts +++ b/src/cli/middlewares/logger.ts @@ -1,6 +1,6 @@ import {setLogLevel, isLogLevelEnabled, getLogger} from "@ui5/logger"; import ConsoleWriter from "@ui5/logger/writers/Console"; -import {getVersion} from "../version.js"; +import {getFormattedVersion} from "../version.js"; import type {ArgumentsCamelCase} from "yargs"; /** * Logger middleware to enable logging capabilities @@ -28,7 +28,7 @@ export async function initLogger(argv: ArgumentsCamelCase) { ConsoleWriter.init(); if (isLogLevelEnabled("verbose")) { const log = getLogger("cli:middlewares:base"); - log.verbose(`using ui5lint version ${getVersion()}`); + log.verbose(`using ui5lint version ${getFormattedVersion()}`); log.verbose(`using node version ${process.version}`); } } diff --git a/src/cli/version.ts b/src/cli/version.ts index 98d70eff3..18959e9e5 100644 --- a/src/cli/version.ts +++ b/src/cli/version.ts @@ -1,9 +1,14 @@ let version: string; +let formattedVersion: string; // This module holds the CLI's version information (set via cli.js) for later retrieval (e.g. from middlewares/logger) -export function setVersion(v: string) { +export function setVersionInfo(v: string, p: string) { version = v; + formattedVersion = `${v} (from ${p})`; } export function getVersion(): string { return version || ""; } +export function getFormattedVersion(): string { + return formattedVersion || ""; +} diff --git a/src/formatter/markdown.ts b/src/formatter/markdown.ts index e94b8d67c..602edeb8d 100644 --- a/src/formatter/markdown.ts +++ b/src/formatter/markdown.ts @@ -2,7 +2,7 @@ import {LintResult, LintMessage} from "../linter/LinterContext.js"; import {LintMessageSeverity} from "../linter/messages.js"; export class Markdown { - format(lintResults: LintResult[], showDetails: boolean): string { + format(lintResults: LintResult[], showDetails: boolean, version: string): string { let totalErrorCount = 0; let totalWarningCount = 0; let totalFatalErrorCount = 0; @@ -21,11 +21,11 @@ export class Markdown { // Add the file path as a section header findings += `### ${filePath}\n\n`; if (showDetails === true) { - findings += `| Severity | Line | Message | Details |\n`; - findings += `|----------|------|---------|---------|\n`; + findings += `| Severity | Rule | Location | Message | Details |\n`; + findings += `|----------|------|----------|---------|---------|\n`; } else { - findings += `| Severity | Line | Message |\n`; - findings += `|----------|------|---------|\n`; + findings += `| Severity | Rule | Location | Message |\n`; + findings += `|----------|------|----------|---------|\n`; } // Sort messages by severity (sorting order: fatal-errors, errors, warnings) @@ -50,6 +50,7 @@ export class Markdown { messages.forEach((msg) => { const severity = this.formatSeverity(msg.severity, msg.fatal); const location = this.formatLocation(msg.line, msg.column); + const rule = this.formatRuleId(msg.ruleId, version); let details; if (showDetails) { details = ` ${this.formatMessageDetails(msg)} |`; @@ -57,7 +58,7 @@ export class Markdown { details = ""; } - findings += `| ${severity} | \`${location}\` | ${msg.message} |${details}\n`; + findings += `| ${severity} | ${rule} | \`${location}\` | ${msg.message} |${details}\n`; }); findings += "\n"; @@ -113,4 +114,9 @@ ${findings}`; // Replace multiple spaces or newlines with a single space for clean output return `${msg.messageDetails.replace(/\s\s+|\n/g, " ")}`; } + + // Formats the rule of the lint message (ruleId and link to rules.md) + private formatRuleId(ruleId: string, version: string): string { + return `[${ruleId}](https://github.com/SAP/ui5-linter/blob/v${version}/docs/Rules.md#${ruleId})`; + } } diff --git a/test/lib/cli.ts b/test/lib/cli.ts index 7f770a548..bfea0bd6d 100644 --- a/test/lib/cli.ts +++ b/test/lib/cli.ts @@ -17,7 +17,8 @@ const test = anyTest as TestFn<{ argv: () => unknown; }; yargs: SinonStub; - setVersion: SinonStub; + setVersionInfo: SinonStub; + getFormattedVersion: SinonStub; cliBase: SinonStub; readdir: SinonStub; cli: MockFunction; @@ -44,13 +45,15 @@ test.beforeEach(async (t) => { t.context.yargs = sinon.stub().returns(t.context.yargsInstance).named("yargs"); - t.context.setVersion = sinon.stub().named("setVersion"); + t.context.setVersionInfo = sinon.stub().named("setVersionInfo"); + t.context.getFormattedVersion = sinon.stub().returns("1.2.3 (from /path/to/cli.js)").named("getFormattedVersion"); t.context.cliBase = sinon.stub().named("cliBase"); t.context.cli = await esmock.p("../../src/cli.js", { "yargs": t.context.yargs, "../../src/cli/version.js": { - setVersion: t.context.setVersion, + setVersionInfo: t.context.setVersionInfo, + getFormattedVersion: t.context.getFormattedVersion, }, "../../src/cli/base.js": t.context.cliBase, "module": { @@ -68,7 +71,7 @@ test.afterEach.always((t) => { test.serial("CLI", async (t) => { const { cli, argvGetter, yargsInstance, yargs, - setVersion, cliBase, + setVersionInfo, cliBase, getFormattedVersion, } = t.context; await cli("module"); @@ -81,14 +84,17 @@ test.serial("CLI", async (t) => { "parse-numbers": false, }]); - t.is(setVersion.callCount, 1); - t.deepEqual(setVersion.getCall(0).args, [ - `${pkg.version} (from ${fileURLToPath(new URL("../../bin/ui5lint.js", import.meta.url))})`, + t.is(setVersionInfo.callCount, 1); + t.deepEqual(setVersionInfo.getCall(0).args, [ + pkg.version, fileURLToPath(new URL("../../bin/ui5lint.js", import.meta.url)), ]); + t.is(getFormattedVersion.callCount, 1); + t.deepEqual(getFormattedVersion.getCall(0).args, []); + t.is(yargsInstance.version.callCount, 1); t.deepEqual(yargsInstance.version.getCall(0).args, [ - `${pkg.version} (from ${fileURLToPath(new URL("../../bin/ui5lint.js", import.meta.url))})`, + getFormattedVersion.getCall(0).returnValue, ]); t.is(yargsInstance.scriptName.callCount, 1); @@ -111,7 +117,7 @@ test.serial("CLI", async (t) => { sinon.assert.callOrder( yargs, yargsInstance.parserConfiguration, - setVersion, + setVersionInfo, yargsInstance.version, yargsInstance.scriptName, cliBase, diff --git a/test/lib/cli/middlewares/logger.ts b/test/lib/cli/middlewares/logger.ts index b7928bb8d..a68bcb181 100644 --- a/test/lib/cli/middlewares/logger.ts +++ b/test/lib/cli/middlewares/logger.ts @@ -6,7 +6,7 @@ const test = anyTest as TestFn<{ verboseLogStub: SinonStub; setLogLevelStub: SinonStub; isLogLevelEnabledStub: SinonStub; - getVersionStub: SinonStub; + getFormattedVersionStub: SinonStub; logger: MockFunction & { initLogger: (args: {loglevel?: string; verbose?: boolean; perf?: boolean; silent?: boolean}) => Promise | void; @@ -17,10 +17,10 @@ test.beforeEach(async (t) => { t.context.verboseLogStub = sinon.stub(); t.context.setLogLevelStub = sinon.stub(); t.context.isLogLevelEnabledStub = sinon.stub().returns(true); - t.context.getVersionStub = sinon.stub().returns("1.0.0"); + t.context.getFormattedVersionStub = sinon.stub().returns("1.0.0"); t.context.logger = await esmock("../../../../src/cli/middlewares/logger.js", { "../../../../src/cli/version.js": { - getVersion: t.context.getVersionStub, + getFormattedVersion: t.context.getFormattedVersionStub, }, "@ui5/logger": { getLogger: () => ({ @@ -33,13 +33,13 @@ test.beforeEach(async (t) => { }); test.serial("init logger", async (t) => { - const {logger, setLogLevelStub, isLogLevelEnabledStub, verboseLogStub, getVersionStub} = t.context; + const {logger, setLogLevelStub, isLogLevelEnabledStub, verboseLogStub, getFormattedVersionStub} = t.context; await logger.initLogger({}); t.is(setLogLevelStub.callCount, 0, "setLevel has not been called"); t.is(isLogLevelEnabledStub.callCount, 1, "isLogLevelEnabled has been called once"); t.is(isLogLevelEnabledStub.firstCall.firstArg, "verbose", "isLogLevelEnabled has been called with expected argument"); - t.is(getVersionStub.callCount, 1, "getVersion has been called once"); + t.is(getFormattedVersionStub.callCount, 1, "getFormattedVersion has been called once"); t.is(verboseLogStub.callCount, 2, "log.verbose has been called twice"); t.is(verboseLogStub.firstCall.firstArg, "using ui5lint version 1.0.0", "log.verbose has been called with expected argument on first call"); diff --git a/test/lib/cli/version.ts b/test/lib/cli/version.ts index 0a5c723cc..f141c7c16 100644 --- a/test/lib/cli/version.ts +++ b/test/lib/cli/version.ts @@ -1,12 +1,19 @@ import test from "ava"; -import {setVersion, getVersion} from "../../../src/cli/version.js"; +import {setVersionInfo, getFormattedVersion, getVersion} from "../../../src/cli/version.js"; test("Set and get version", (t) => { + const sampleVersion = "1.2.3"; + const sampleVersion2 = "4.5.6-foo.bar"; + const samplePath = "/path/to/cli.js"; + + t.is(getFormattedVersion(), ""); t.is(getVersion(), ""); - setVersion("1.2.3"); - t.is(getVersion(), "1.2.3"); + setVersionInfo(sampleVersion, samplePath); + t.is(getFormattedVersion(), `${sampleVersion} (from ${samplePath})`); + t.is(getVersion(), sampleVersion); - setVersion("4.5.6-foo.bar"); - t.is(getVersion(), "4.5.6-foo.bar"); + setVersionInfo(sampleVersion2, samplePath); + t.is(getFormattedVersion(), `${sampleVersion2} (from ${samplePath})`); + t.is(getVersion(), sampleVersion2); }); diff --git a/test/lib/formatter/markdown.ts b/test/lib/formatter/markdown.ts index 3966ffc00..98fc42f71 100644 --- a/test/lib/formatter/markdown.ts +++ b/test/lib/formatter/markdown.ts @@ -92,7 +92,7 @@ test("Default", (t) => { const {lintResults} = t.context; const markdownFormatter = new Markdown(); - const markdownResult = markdownFormatter.format(lintResults, false); + const markdownResult = markdownFormatter.format(lintResults, false, "1.2.3"); t.snapshot(markdownResult); }); @@ -101,14 +101,14 @@ test("Details", (t) => { const {lintResults} = t.context; const markdownFormatter = new Markdown(); - const markdownResult = markdownFormatter.format(lintResults, true); + const markdownResult = markdownFormatter.format(lintResults, true, "1.2.3"); t.snapshot(markdownResult); }); test("No findings", (t) => { const markdownFormatter = new Markdown(); - const markdownResult = markdownFormatter.format([], true); + const markdownResult = markdownFormatter.format([], true, "1.2.3"); t.snapshot(markdownResult); }); diff --git a/test/lib/formatter/snapshots/markdown.ts.md b/test/lib/formatter/snapshots/markdown.ts.md index beb8e8261..6856a480f 100644 --- a/test/lib/formatter/snapshots/markdown.ts.md +++ b/test/lib/formatter/snapshots/markdown.ts.md @@ -17,20 +17,20 @@ Generated by [AVA](https://avajs.dev). ## Findings␊ ### webapp/Component.js␊ ␊ - | Severity | Line | Message |␊ - |----------|------|---------|␊ - | Error | \`1:1\` | Error message |␊ - | Warning | \`2:2\` | Warning message |␊ + | Severity | Rule | Location | Message |␊ + |----------|------|----------|---------|␊ + | Error | [rule1](https://github.com/SAP/ui5-linter/blob/v1.2.3/docs/Rules.md#rule1) | \`1:1\` | Error message |␊ + | Warning | [rule2](https://github.com/SAP/ui5-linter/blob/v1.2.3/docs/Rules.md#rule2) | \`2:2\` | Warning message |␊ ␊ ### webapp/Main.controller.js␊ ␊ - | Severity | Line | Message |␊ - |----------|------|---------|␊ - | Fatal Error | \`3:6\` | Another error message |␊ - | Fatal Error | \`12:3\` | Another error message |␊ - | Error | \`11:2\` | Another error message |␊ - | Error | \`11:3\` | Another error message |␊ - | Warning | \`12:3\` | Another error message |␊ + | Severity | Rule | Location | Message |␊ + |----------|------|----------|---------|␊ + | Fatal Error | [rule3](https://github.com/SAP/ui5-linter/blob/v1.2.3/docs/Rules.md#rule3) | \`3:6\` | Another error message |␊ + | Fatal Error | [rule3](https://github.com/SAP/ui5-linter/blob/v1.2.3/docs/Rules.md#rule3) | \`12:3\` | Another error message |␊ + | Error | [rule3](https://github.com/SAP/ui5-linter/blob/v1.2.3/docs/Rules.md#rule3) | \`11:2\` | Another error message |␊ + | Error | [rule3](https://github.com/SAP/ui5-linter/blob/v1.2.3/docs/Rules.md#rule3) | \`11:3\` | Another error message |␊ + | Warning | [rule3](https://github.com/SAP/ui5-linter/blob/v1.2.3/docs/Rules.md#rule3) | \`12:3\` | Another error message |␊ ␊ **Note:** Use \`ui5lint --details\` to show more information about the findings.␊ ` @@ -48,20 +48,20 @@ Generated by [AVA](https://avajs.dev). ## Findings␊ ### webapp/Component.js␊ ␊ - | Severity | Line | Message | Details |␊ - |----------|------|---------|---------|␊ - | Error | \`1:1\` | Error message | Message details |␊ - | Warning | \`2:2\` | Warning message | Message details |␊ + | Severity | Rule | Location | Message | Details |␊ + |----------|------|----------|---------|---------|␊ + | Error | [rule1](https://github.com/SAP/ui5-linter/blob/v1.2.3/docs/Rules.md#rule1) | \`1:1\` | Error message | Message details |␊ + | Warning | [rule2](https://github.com/SAP/ui5-linter/blob/v1.2.3/docs/Rules.md#rule2) | \`2:2\` | Warning message | Message details |␊ ␊ ### webapp/Main.controller.js␊ ␊ - | Severity | Line | Message | Details |␊ - |----------|------|---------|---------|␊ - | Fatal Error | \`3:6\` | Another error message | Message details |␊ - | Fatal Error | \`12:3\` | Another error message | Message details |␊ - | Error | \`11:2\` | Another error message | Message details |␊ - | Error | \`11:3\` | Another error message | Message details |␊ - | Warning | \`12:3\` | Another error message | Message details |␊ + | Severity | Rule | Location | Message | Details |␊ + |----------|------|----------|---------|---------|␊ + | Fatal Error | [rule3](https://github.com/SAP/ui5-linter/blob/v1.2.3/docs/Rules.md#rule3) | \`3:6\` | Another error message | Message details |␊ + | Fatal Error | [rule3](https://github.com/SAP/ui5-linter/blob/v1.2.3/docs/Rules.md#rule3) | \`12:3\` | Another error message | Message details |␊ + | Error | [rule3](https://github.com/SAP/ui5-linter/blob/v1.2.3/docs/Rules.md#rule3) | \`11:2\` | Another error message | Message details |␊ + | Error | [rule3](https://github.com/SAP/ui5-linter/blob/v1.2.3/docs/Rules.md#rule3) | \`11:3\` | Another error message | Message details |␊ + | Warning | [rule3](https://github.com/SAP/ui5-linter/blob/v1.2.3/docs/Rules.md#rule3) | \`12:3\` | Another error message | Message details |␊ ␊ ` diff --git a/test/lib/formatter/snapshots/markdown.ts.snap b/test/lib/formatter/snapshots/markdown.ts.snap index db0985595..720e7936c 100644 Binary files a/test/lib/formatter/snapshots/markdown.ts.snap and b/test/lib/formatter/snapshots/markdown.ts.snap differ