diff --git a/.nycrc b/.nycrc index 6eb878f66..068466d06 100644 --- a/.nycrc +++ b/.nycrc @@ -8,10 +8,10 @@ "src/linter/xmlTemplate/lib/JSTokenizer.js" ], "check-coverage": true, - "statements": 82, - "branches": 74, - "functions": 89, - "lines": 82, + "statements": 87, + "branches": 79, + "functions": 94, + "lines": 87, "watermarks": { "statements": [70, 90], "branches": [70, 90], diff --git a/package.json b/package.json index 6ccdbc07a..fba111c91 100644 --- a/package.json +++ b/package.json @@ -28,12 +28,13 @@ "type": "module", "types": "lib/index.d.ts", "scripts": { - "build": "npm run cleanup && tsc -p tsconfig.build.json", + "build": "npm run clean-lib && tsc -p tsconfig.build.json", "build-test": "tsc --noEmit -p .", - "build-watch": "npm run cleanup && tsc -w -p tsconfig.build.json", + "build-watch": "npm run clean-lib && tsc -w -p tsconfig.build.json", "check-licenses": "licensee --errors-only", - "cleanup": "rimraf lib coverage", - "coverage": "nyc ava --node-arguments=\"--experimental-loader=@istanbuljs/esm-loader-hook\"", + "clean-coverage": "rimraf coverage", + "clean-lib": "rimraf lib", + "coverage": "npm run clean-coverage && nyc ava --node-arguments=\"--experimental-loader=@istanbuljs/esm-loader-hook\"", "depcheck": "depcheck --ignores @commitlint/config-conventional,@istanbuljs/esm-loader-hook,rimraf,sap,mycomp,@ui5/linter", "hooks:pre-push": "npm run lint:commit", "lint": "eslint .", diff --git a/src/cli/base.ts b/src/cli/base.ts index 0d61069b2..544b82bea 100644 --- a/src/cli/base.ts +++ b/src/cli/base.ts @@ -163,7 +163,7 @@ async function handleLint(argv: ArgumentsCamelCase) { if (coverage) { const coverageFormatter = new Coverage(); - await writeFile("ui5lint-report.html", await coverageFormatter.format(res)); + await writeFile("ui5lint-report.html", await coverageFormatter.format(res, new Date())); } if (format === "json") { diff --git a/src/formatter/coverage.ts b/src/formatter/coverage.ts index 23ebacea8..f67865834 100644 --- a/src/formatter/coverage.ts +++ b/src/formatter/coverage.ts @@ -1,10 +1,9 @@ -import path from "node:path"; import { LintResult, CoverageInfo, CoverageCategory, } from "../linter/LinterContext.js"; -import {readFile} from "fs/promises"; +import {readFile} from "node:fs/promises"; import {LintMessageSeverity} from "../linter/messages.js"; const visualizedSpace = "\u00b7"; @@ -17,7 +16,7 @@ function formatSeverity(severity: LintMessageSeverity) { } else if (severity === LintMessageSeverity.Warning) { return "warning"; } else { - throw new Error(`Unknown severity: ${LintMessageSeverity[severity]}`); + throw new Error(`Unknown severity: ${severity as number}`); } } @@ -45,13 +44,13 @@ function escape(str: string) { export class Coverage { #buffer = ""; - async format(lintResults: LintResult[]) { + async format(lintResults: LintResult[], reportDate: Date) { this.#writeln(` - UI5Lint Coverage Report ${new Date().toLocaleString()} + UI5Lint Coverage Report ${reportDate.toLocaleString("en-US")} ␊ + ␊ + ␊ +
manifest.json␊ +
␊ + 1␊ + {␊ +
␊ +
␊ + 2␊ + » "_version":·"1.61.0",␊ +
␊ +
␊ + 3␊ + » "sap.app":·{␊ +
␊ +
␊ + 4␊ + » » "id":·"sap.ui.demo.todo",␊ +
␊ +
␊ + 5␊ + » » "type":·"application"␊ +
␊ +
␊ + 6␊ + » },␊ +
␊ +
␊ + 7␊ + » "sap.ui5":·{␊ +
␊ +
␊ + 8␊ + » » "dependencies":·{␊ +
␊ +
␊ + 9␊ + » » » "minUI5Version":·"1.121.0",␊ +
␊ +
␊ + 10␊ + » » » "libs":·{␊ +
␊ +
␊ + 11␊ + » » » » "sap.ui.core":·{}␊ +
␊ +
␊ + 12␊ + » » » }␊ +
␊ +
␊ + 13␊ + » » },␊ +
␊ +
␊ + 14␊ + » » "rootView":·{␊ +
␊ +
␊ + 15␊ + » » » "viewName":·"sap.ui.demo.todo.view.App",␊ +
␊ +
␊ + 16␊ + » » » "type":·"XML",␊ +
␊ +
␊ + 17␊ + » » » "id":·"app",␊ + warning <message>␊ +
␊ +
␊ + 18␊ + » » » "async":·true␊ +
␊ +
␊ + 19␊ + » » }␊ +
␊ +
␊ + 20␊ + » }␊ +
␊ +
␊ + 21␊ + }␊ +
␊ +
␊ + 22␊ + ␊ +
␊ +
␊ +
Test.js␊ +
␊ + 1␊ + sap.ui.define(()·=>·{␊ +
␊ +
␊ + 2␊ + » sap.ui.getCore();␊ + error <message> & error <message>␊ +
␊ +
␊ + 3␊ + » unknownFunctionCall();␊ +
␊ +
␊ + 4␊ + });␊ +
␊ +
␊ + 5␊ + ␊ +
␊ +
␊ + ␊ + ␊ + ` diff --git a/test/lib/formatter/snapshots/coverage.ts.snap b/test/lib/formatter/snapshots/coverage.ts.snap new file mode 100644 index 000000000..bb0a8c5ea Binary files /dev/null and b/test/lib/formatter/snapshots/coverage.ts.snap differ diff --git a/test/lib/linter/amdTranspiler/transpiler.ts b/test/lib/linter/amdTranspiler/transpiler.ts index ee8ee5a84..707acf62f 100644 --- a/test/lib/linter/amdTranspiler/transpiler.ts +++ b/test/lib/linter/amdTranspiler/transpiler.ts @@ -1,9 +1,7 @@ import anyTest, {TestFn} from "ava"; import sinonGlobal from "sinon"; import path from "node:path"; -// import fs from "node:fs/promises"; import {fileURLToPath} from "node:url"; -// import {amdToEsm} from "../../../../../src/detectors/transpilers/amd/transpiler.js"; import {createTestsForFixtures} from "./_helper.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/test/lib/linter/amdTranspiler/transpiler_errorHandling.ts b/test/lib/linter/amdTranspiler/transpiler_errorHandling.ts new file mode 100644 index 000000000..6181f793c --- /dev/null +++ b/test/lib/linter/amdTranspiler/transpiler_errorHandling.ts @@ -0,0 +1,135 @@ +import anyTest, {TestFn} from "ava"; +import sinonGlobal from "sinon"; +import LinterContext from "../../../../src/linter/LinterContext.js"; +import {UnsupportedModuleError} from "../../../../src/linter/ui5Types/amdTranspiler/util.js"; +import esmock from "esmock"; + +const test = anyTest as TestFn<{ + sinon: sinonGlobal.SinonSandbox; + log: {verbose: sinonGlobal.SinonStub}; + transformFunctionStub: sinonGlobal.SinonStub; + transpileAmdToEsm: typeof import("../../../../src/linter/ui5Types/amdTranspiler/transpiler.js").default; +}>; + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + t.context.transformFunctionStub = sinon.stub(); + t.context.log = { + verbose: sinon.stub(), + }; + t.context.transpileAmdToEsm = await esmock("../../../../src/linter/ui5Types/amdTranspiler/transpiler.js", { + "../../../../src/linter/ui5Types/amdTranspiler/tsTransformer.js": { + createTransformer: () => () => t.context.transformFunctionStub, + }, + "@ui5/logger": { + getLogger: sinon.stub().withArgs("linter:ui5Types:amdTranspiler:transpiler").returns(t.context.log), + }, + }); +}); + +test.serial("Error: Unexpected Error during transformation", (t) => { + const {transpileAmdToEsm, transformFunctionStub} = t.context; + + const context = new LinterContext({ + rootDir: "/", + namespace: "my/app", + }); + + // Stub the createTransformer function so that the transpiler has to handle an unexpected error + transformFunctionStub.throws(new Error("TEST: Unexpected error from createTransformer")); + + t.throws(() => transpileAmdToEsm("/resources/my/app/x.js", "const x = 1;", context), { + message: "TEST: Unexpected error from createTransformer", + }); +}); + +test.serial("Error: Unexpected UnsupportedModuleError during transformation", (t) => { + const {transpileAmdToEsm, transformFunctionStub, log} = t.context; + + const context = new LinterContext({ + rootDir: "/", + namespace: "my/app", + }); + + const inputSource = "const x = 1;"; + + // Stub the createTransformer function so that the transpiler has to handle an unexpected UnsupportedModuleError + // Usually all UnsupportedModuleError are handled internally so that they are not thrown, but the catch clause + // is a safety net in case an error is thrown anyway + const error = new UnsupportedModuleError("TEST"); + transformFunctionStub.throws(error); + + const {source, map} = transpileAmdToEsm("/resources/my/app/x.js", inputSource, context); + + t.is(source, inputSource); + t.is(map, ""); + + t.is(log.verbose.callCount, 3); + t.deepEqual(log.verbose.getCall(0).args, ["Failed to transform module x.js: TEST"]); + t.deepEqual(log.verbose.getCall(1).args, ["Stack trace:"]); + t.deepEqual(log.verbose.getCall(2).args, [error.stack]); +}); + +test.serial("Error: Unexpected UnsupportedModuleError during transformation (strict = true)", (t) => { + const {transpileAmdToEsm, transformFunctionStub} = t.context; + + const context = new LinterContext({ + rootDir: "/", + namespace: "my/app", + }); + + const inputSource = "const x = 1;"; + + // Stub the createTransformer function so that the transpiler has to handle an unexpected UnsupportedModuleError + // Usually all UnsupportedModuleError are handled internally so that they are not thrown, but the catch clause + // is a safety net in case an error is thrown anyway + transformFunctionStub.throws(new UnsupportedModuleError("TEST")); + + t.throws(() => transpileAmdToEsm("/resources/my/app/x.js", inputSource, context, true), { + message: "TEST", + instanceOf: UnsupportedModuleError, + }); +}); + +test.serial("Error: Unexpected TypeScript Debug Failure during transformation", (t) => { + const {transpileAmdToEsm, transformFunctionStub, log} = t.context; + + const context = new LinterContext({ + rootDir: "/", + namespace: "my/app", + }); + + const inputSource = "const x = 1;"; + + // Stub the createTransformer function so that the transpiler has to handle an unexpected error from TypeScript + const error = new Error("Debug Failure. TEST"); + transformFunctionStub.throws(error); + + const {source, map} = transpileAmdToEsm("/resources/my/app/x.js", inputSource, context); + + t.is(source, inputSource); + t.is(map, ""); + + t.is(log.verbose.callCount, 3); + t.deepEqual(log.verbose.getCall(0).args, ["AST transformation failed for module x.js: Debug Failure. TEST"]); + t.deepEqual(log.verbose.getCall(1).args, ["Stack trace:"]); + t.deepEqual(log.verbose.getCall(2).args, [error.stack]); +}); + +test.serial("Error: Unexpected TypeScript Debug Failure during transformation (strict = true)", (t) => { + const {transpileAmdToEsm, transformFunctionStub} = t.context; + + const context = new LinterContext({ + rootDir: "/", + namespace: "my/app", + }); + + const inputSource = "const x = 1;"; + + // Stub the createTransformer function so that the transpiler has to handle an unexpected error from TypeScript + transformFunctionStub.throws(new Error("Debug Failure. TEST")); + + t.throws(() => transpileAmdToEsm("/resources/my/app/x.js", inputSource, context, true), { + message: "Debug Failure. TEST", + }); +});