diff --git a/lib/internal/modules/typescript.js b/lib/internal/modules/typescript.js index 8d2a97259704a1..86ef400ca7559e 100644 --- a/lib/internal/modules/typescript.js +++ b/lib/internal/modules/typescript.js @@ -145,6 +145,11 @@ function processTypeScriptCode(code, options) { return transformedCode; } +function stripTypeScriptTypesForCoverage(code) { + validateString(code, 'code'); + return processTypeScriptCode(code, { mode: 'strip-only' }); +} + /** * Performs type-stripping to TypeScript source code internally. @@ -205,4 +210,5 @@ function addSourceMap(code, sourceMap) { module.exports = { stripTypeScriptModuleTypes, stripTypeScriptTypes, + stripTypeScriptTypesForCoverage, }; diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index 8fa9c872568d1e..c3ab152be135fb 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -16,6 +16,7 @@ const { StringPrototypeIncludes, StringPrototypeLocaleCompare, StringPrototypeStartsWith, + StringPrototypeTrim, } = primordials; const { copyFileSync, @@ -44,6 +45,20 @@ const kIgnoreRegex = /\/\* node:coverage ignore next (?\d+ )?\*\//; const kLineEndingRegex = /\r?\n$/u; const kLineSplitRegex = /(?<=\r?\n)/u; const kStatusRegex = /\/\* node:coverage (?enable|disable) \*\//; +const kTypeOnlyImportRegex = /^\s*import\s+type\b/u; +const kTypeScriptSourceRegex = /\.(?:cts|mts|ts)$/u; + +let stripTypeScriptTypesForCoverage; + +function getStripTypeScriptTypesForCoverage() { + if (!process.config.variables.node_use_amaro) { + return; + } + + stripTypeScriptTypesForCoverage ??= + require('internal/modules/typescript').stripTypeScriptTypesForCoverage; + return stripTypeScriptTypesForCoverage; +} class CoverageLine { constructor(line, startOffset, src, length = src?.length) { @@ -69,6 +84,7 @@ class TestCoverage { } #sourceLines = new SafeMap(); + #typeScriptLines = new SafeSet(); getLines(fileUrl, source) { // Split the file source into lines. Make sure the lines maintain their @@ -133,6 +149,57 @@ class TestCoverage { return lines; } + markTypeScriptOnlyLines(fileUrl, source) { + if (this.#typeScriptLines.has(fileUrl)) { + return; + } + this.#typeScriptLines.add(fileUrl); + + if (RegExpPrototypeExec(kTypeScriptSourceRegex, fileUrl) === null) { + return; + } + + const lines = this.getLines(fileUrl, source); + if (!lines) { + return; + } + + let strippedLines; + const stripSource = getStripTypeScriptTypesForCoverage(); + + if (stripSource) { + source ??= readFileSync(fileURLToPath(fileUrl), 'utf8'); + + try { + strippedLines = RegExpPrototypeSymbolSplit( + kLineSplitRegex, + stripSource(source), + ); + } catch { + strippedLines = undefined; + } + } + + for (let i = 0; i < lines.length; ++i) { + const originalLine = lines[i].src; + + if (StringPrototypeTrim(originalLine).length === 0) { + continue; + } + + if (strippedLines?.[i] !== undefined) { + if (StringPrototypeTrim(strippedLines[i]).length === 0) { + lines[i].ignore = true; + } + continue; + } + + if (RegExpPrototypeExec(kTypeOnlyImportRegex, originalLine) !== null) { + lines[i].ignore = true; + } + } + } + summary() { internalBinding('profiler').takeCoverage(); const coverage = this.getCoverageFromDirectory(); @@ -368,10 +435,12 @@ class TestCoverage { offset += length + 1; return coverageLine; }); - if (data.sourcesContent != null) { - for (let j = 0; j < data.sources.length; ++j) { - this.getLines(data.sources[j], data.sourcesContent[j]); + for (let j = 0; j < data.sources.length; ++j) { + const source = data.sourcesContent?.[j]; + if (source != null) { + this.getLines(data.sources[j], source); } + this.markTypeScriptOnlyLines(data.sources[j], source); } const sourceMap = new SourceMap(data, { __proto__: null, lineLengths }); diff --git a/test/fixtures/test-runner/source-maps/type-only-import/dist/a.mjs b/test/fixtures/test-runner/source-maps/type-only-import/dist/a.mjs new file mode 100644 index 00000000000000..1cb9576a123d5a --- /dev/null +++ b/test/fixtures/test-runner/source-maps/type-only-import/dist/a.mjs @@ -0,0 +1,3 @@ +console.log('Hi'); +export {}; +//# sourceMappingURL=a.mjs.map diff --git a/test/fixtures/test-runner/source-maps/type-only-import/dist/a.mjs.map b/test/fixtures/test-runner/source-maps/type-only-import/dist/a.mjs.map new file mode 100644 index 00000000000000..acbd8f58897d9a --- /dev/null +++ b/test/fixtures/test-runner/source-maps/type-only-import/dist/a.mjs.map @@ -0,0 +1 @@ +{"version":3,"file":"a.mjs","sourceRoot":"","sources":["../src/a.mts"],"names":[],"mappings":"AAEA,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC"} diff --git a/test/fixtures/test-runner/source-maps/type-only-import/src/a.mts b/test/fixtures/test-runner/source-maps/type-only-import/src/a.mts new file mode 100644 index 00000000000000..16e1f9f65763a7 --- /dev/null +++ b/test/fixtures/test-runner/source-maps/type-only-import/src/a.mts @@ -0,0 +1,3 @@ +import type {} from 'node:assert'; + +console.log('Hi'); diff --git a/test/fixtures/test-runner/source-maps/type-only-import/test.mjs b/test/fixtures/test-runner/source-maps/type-only-import/test.mjs new file mode 100644 index 00000000000000..dd791e471160c8 --- /dev/null +++ b/test/fixtures/test-runner/source-maps/type-only-import/test.mjs @@ -0,0 +1 @@ +import './dist/a.mjs'; diff --git a/test/parallel/test-runner-coverage-source-map.js b/test/parallel/test-runner-coverage-source-map.js index e3b0676a557a9f..64a452aba00779 100644 --- a/test/parallel/test-runner-coverage-source-map.js +++ b/test/parallel/test-runner-coverage-source-map.js @@ -73,6 +73,31 @@ describe('Coverage with source maps', async () => { t.assert.strictEqual(spawned.code, 1); }); + await it('should ignore erased TypeScript import type lines', async (t) => { + const report = generateReport([ + '# ----------------------------------------------------------', + '# file | line % | branch % | funcs % | uncovered lines', + '# ----------------------------------------------------------', + '# src | | | | ', + '# a.mts | 100.00 | 100.00 | 100.00 | ', + '# test.mjs | 100.00 | 100.00 | 100.00 | ', + '# ----------------------------------------------------------', + '# all files | 100.00 | 100.00 | 100.00 | ', + '# ----------------------------------------------------------', + ]); + + const spawned = await common.spawnPromisified(process.execPath, [ + ...flags, + 'test.mjs', + ], { + cwd: fixtures.path('test-runner', 'source-maps', 'type-only-import'), + }); + + t.assert.strictEqual(spawned.stderr, ''); + t.assert.ok(spawned.stdout.includes(report)); + t.assert.strictEqual(spawned.code, 0); + }); + await it('properly accounts for line endings in source maps', async (t) => { const report = generateReport([ '# ------------------------------------------------------------------',