diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..163c0757 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "test/source-map-tests"] + path = test/source-map-tests + url = https://github.com/tc39/source-map-tests.git diff --git a/package.json b/package.json index 00077059..ae0e6552 100644 --- a/package.json +++ b/package.json @@ -65,8 +65,8 @@ }, "license": "BSD-3-Clause", "scripts": { - "lint": "eslint --fix *.js lib/ test/", - "test": "node test/run-tests.js", + "lint": "eslint --fix *.js lib/ test/ --ignore-pattern 'test/source-map-tests/**'", + "test": "git submodule update --init --recursive; node test/run-tests.js", "coverage": "c8 --reporter=text --reporter=html npm test", "prettier": "prettier --write .", "clean": "rm -rf coverage", diff --git a/test/source-map-tests b/test/source-map-tests new file mode 160000 index 00000000..14c89744 --- /dev/null +++ b/test/source-map-tests @@ -0,0 +1 @@ +Subproject commit 14c897444208365fc586a9c00c623bfb1955d731 diff --git a/test/test-spec-tests.js b/test/test-spec-tests.js new file mode 100644 index 00000000..8f113160 --- /dev/null +++ b/test/test-spec-tests.js @@ -0,0 +1,208 @@ +/* -*- Mode: js; js-indent-level: 2; -*- */ +/* + * Copyright 2024 Mozilla Foundation and contributors + * Licensed under the New BSD license. See LICENSE or: + * http://opensource.org/licenses/BSD-3-Clause + */ + +const fs = require("fs").promises; +const SourceMapConsumer = + require("../lib/source-map-consumer").SourceMapConsumer; + +const sourceMapSpecTests = require("./source-map-tests/source-map-spec-tests.json"); + +async function readJSON(path) { + const file = await fs.open(require.resolve(path)); + const json = JSON.parse(await file.readFile()); + file.close(); + return json; +} + +// Known failures due to intentional implementation choices or due to bugs. +const skippedTests = [ + // Versions are explicitly checked a bit loosely. + "versionNumericString", + // Stricter sources array checking isn't implemented. + "sourcesNotStringOrNull", + "sourcesAndSourcesContentBothNull", + // Stricter names array checking isn't implemented. + "namesMissing", + "namesNotString", + // This check isn't as strict in this library. + "invalidMappingNotAString1", + // A mapping segment with no fields is technically invalid in the spec. + "invalidMappingSegmentWithZeroFields", + // These tests fail due to imprecision in the spec about the 32-bit limit. + "invalidMappingSegmentWithColumnExceeding32Bits", + "invalidMappingSegmentWithOriginalLineExceeding32Bits", + "invalidMappingSegmentWithOriginalColumnExceeding32Bits", + // A large VLQ that should parse, but currently does not. + "validMappingLargeVLQ", + // The library currently doesn't check the types of offset lines/columns. + "indexMapOffsetLineWrongType", + "indexMapOffsetColumnWrongType", + // The spec is not totally clear about this case. + "indexMapInvalidBaseMappings", + // The spec's definition of overlap can be refined + "indexMapInvalidOverlap", + // The library doesn't support the new ignoreList feature yet. + "ignoreListWrongType1", + "ignoreListWrongType2", + "ignoreListWrongType3", + "ignoreListOutOfBounds", +]; + +// The source-map library converts null sources to the "null" URL in its +// sources list, so for equality checking we accept this as null. +function nullish(nullOrString) { + if (nullOrString === "null") { + return null; + } + return nullOrString; +} + +function mapLine(line) { + return line + 1; +} + +async function testMappingAction(assert, rawSourceMap, action) { + return SourceMapConsumer.with(rawSourceMap, null, consumer => { + let mappedPosition = consumer.originalPositionFor({ + line: mapLine(action.generatedLine), + column: action.generatedColumn, + }); + + assert.equal( + mappedPosition.line, + mapLine(action.originalLine), + `original line didn't match, expected ${mapLine( + action.originalLine + )} got ${mappedPosition.line}` + ); + assert.equal( + mappedPosition.column, + action.originalColumn, + `original column didn't match, expected ${action.originalColumn} got ${mappedPosition.column}` + ); + assert.equal( + nullish(mappedPosition.source), + action.originalSource, + `original source didn't match, expected ${action.originalSource} got ${mappedPosition.source}` + ); + if (action.mappedName) { + assert.equal( + mappedPosition.name, + action.mappedName, + `mapped name didn't match, expected ${action.mappedName} got ${mappedPosition.name}` + ); + } + + // When the source is null, a reverse lookup may not make sense + // because there isn't a unique way to look it up. + if (action.originalSource !== null) { + mappedPosition = consumer.generatedPositionFor({ + source: action.originalSource, + line: mapLine(action.originalLine), + column: action.originalColumn, + }); + + assert.equal( + mappedPosition.line, + mapLine(action.generatedLine), + `generated line didn't match, expected ${mapLine( + action.generatedLine + )} got ${mappedPosition.line}` + ); + assert.equal( + mappedPosition.column, + action.generatedColumn, + `generated column didn't match, expected ${action.generatedColumn} got ${mappedPosition.column}` + ); + } + }); +} + +async function testTransitiveMappingAction(assert, rawSourceMap, action) { + return SourceMapConsumer.with(rawSourceMap, null, async consumer => { + assert.ok( + Array.isArray(action.intermediateMaps), + "transitive mapping case requires intermediate maps" + ); + + let mappedPosition = consumer.originalPositionFor({ + line: mapLine(action.generatedLine), + column: action.generatedColumn, + }); + + for (const intermediateMapPath of action.intermediateMaps) { + const intermediateMap = await readJSON( + `./source-map-tests/resources/${intermediateMapPath}` + ); + await SourceMapConsumer.with( + intermediateMap, + null, + consumerIntermediate => { + mappedPosition = consumerIntermediate.originalPositionFor({ + line: mappedPosition.line, + column: mappedPosition.column, + }); + } + ); + } + + assert.equal( + mappedPosition.line, + mapLine(action.originalLine), + `original line didn't match, expected ${mapLine( + action.originalLine + )} got ${mappedPosition.line}` + ); + assert.equal( + mappedPosition.column, + action.originalColumn, + `original column didn't match, expected ${action.originalColumn} got ${mappedPosition.column}` + ); + assert.equal( + mappedPosition.source, + action.originalSource, + `original source didn't match, expected ${action.originalSource} got ${mappedPosition.source}` + ); + }); +} + +for (const testCase of sourceMapSpecTests.tests) { + if (skippedTests.includes(testCase.name)) { + continue; + } + exports[`test from source map spec tests, name: ${testCase.name}`] = + async function (assert) { + const json = await readJSON( + `./source-map-tests/resources/${testCase.sourceMapFile}` + ); + try { + const map = await new SourceMapConsumer(json); + map.eachMapping(() => {}); + map.destroy(); + } catch (exn) { + if (testCase.sourceMapIsValid) { + assert.fail( + "Expected valid source map but failed to load successfully: " + + exn.message + ); + } + return; + } + if (!testCase.sourceMapIsValid) { + assert.fail("Expected invalid source map but loaded successfully"); + } + if (testCase.testActions) { + for (const testAction of testCase.testActions) { + if (testAction.actionType == "checkMapping") { + await testMappingAction(assert, json, testAction); + } else if (testAction.actionType == "checkMappingTransitive") { + await testTransitiveMappingAction(assert, json, testAction); + } + } + } + }; +}