diff options
Diffstat (limited to 'devtools/client/shared/source-map-loader/source-map.js')
-rw-r--r-- | devtools/client/shared/source-map-loader/source-map.js | 521 |
1 files changed, 521 insertions, 0 deletions
diff --git a/devtools/client/shared/source-map-loader/source-map.js b/devtools/client/shared/source-map-loader/source-map.js new file mode 100644 index 0000000000..5fb22cdde4 --- /dev/null +++ b/devtools/client/shared/source-map-loader/source-map.js @@ -0,0 +1,521 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +"use strict"; + +/** + * Source Map Worker + * @module utils/source-map-worker + */ + +const { + SourceMapConsumer, +} = require("resource://devtools/client/shared/vendor/source-map/source-map.js"); + +// Initialize the source-map library right away so that all other code can use it. +SourceMapConsumer.initialize({ + "lib/mappings.wasm": + "resource://devtools/client/shared/vendor/source-map/lib/mappings.wasm", +}); + +const { + networkRequest, +} = require("resource://devtools/client/shared/source-map-loader/utils/network-request.js"); +const assert = require("resource://devtools/client/shared/source-map-loader/utils/assert.js"); +const { + fetchSourceMap, + hasOriginalURL, + clearOriginalURLs, +} = require("resource://devtools/client/shared/source-map-loader/utils/fetchSourceMap.js"); +const { + getSourceMap, + getSourceMapWithMetadata, + setSourceMap, + clearSourceMaps: clearSourceMapsRequests, +} = require("resource://devtools/client/shared/source-map-loader/utils/sourceMapRequests.js"); +const { + originalToGeneratedId, + generatedToOriginalId, + isGeneratedId, + isOriginalId, + getContentType, +} = require("resource://devtools/client/shared/source-map-loader/utils/index.js"); +const { + clearWasmXScopes, +} = require("resource://devtools/client/shared/source-map-loader/wasm-dwarf/wasmXScopes.js"); + +async function getOriginalURLs(generatedSource) { + await fetchSourceMap(generatedSource); + const data = await getSourceMapWithMetadata(generatedSource.id); + return data ? data.sources : null; +} + +async function getSourceMapIgnoreList(generatedSourceId) { + const data = await getSourceMapWithMetadata(generatedSourceId); + return data ? data.ignoreListUrls : []; +} + +const COMPUTED_SPANS = new WeakSet(); + +const SOURCE_MAPPINGS = new WeakMap(); +async function getOriginalRanges(sourceId) { + if (!isOriginalId(sourceId)) { + return []; + } + + const generatedSourceId = originalToGeneratedId(sourceId); + const data = await getSourceMapWithMetadata(generatedSourceId); + if (!data) { + return []; + } + const { map } = data; + const url = data.urlsById.get(sourceId); + + let mappings = SOURCE_MAPPINGS.get(map); + if (!mappings) { + mappings = new Map(); + SOURCE_MAPPINGS.set(map, mappings); + } + + let fileMappings = mappings.get(url); + if (!fileMappings) { + fileMappings = []; + mappings.set(url, fileMappings); + + const originalMappings = fileMappings; + map.eachMapping( + mapping => { + if (mapping.source !== url) { + return; + } + + const last = originalMappings[originalMappings.length - 1]; + + if (last && last.line === mapping.originalLine) { + if (last.columnStart < mapping.originalColumn) { + last.columnEnd = mapping.originalColumn; + } else { + // Skip this duplicate original location, + return; + } + } + + originalMappings.push({ + line: mapping.originalLine, + columnStart: mapping.originalColumn, + columnEnd: Infinity, + }); + }, + null, + SourceMapConsumer.ORIGINAL_ORDER + ); + } + + return fileMappings; +} + +/** + * Given an original location, find the ranges on the generated file that + * are mapped from the original range containing the location. + */ +async function getGeneratedRanges(location) { + if (!isOriginalId(location.sourceId)) { + return []; + } + + const generatedSourceId = originalToGeneratedId(location.sourceId); + const data = await getSourceMapWithMetadata(generatedSourceId); + if (!data) { + return []; + } + const { urlsById, map } = data; + + if (!COMPUTED_SPANS.has(map)) { + COMPUTED_SPANS.add(map); + map.computeColumnSpans(); + } + + // We want to use 'allGeneratedPositionsFor' to get the _first_ generated + // location, but it hard-codes SourceMapConsumer.LEAST_UPPER_BOUND as the + // bias, making it search in the wrong direction for this usecase. + // To work around this, we use 'generatedPositionFor' and then look up the + // exact original location, making any bias value unnecessary, and then + // use that location for the call to 'allGeneratedPositionsFor'. + const genPos = map.generatedPositionFor({ + source: urlsById.get(location.sourceId), + line: location.line, + column: location.column == null ? 0 : location.column, + bias: SourceMapConsumer.GREATEST_LOWER_BOUND, + }); + if (genPos.line === null) { + return []; + } + + const positions = map.allGeneratedPositionsFor( + map.originalPositionFor({ + line: genPos.line, + column: genPos.column, + }) + ); + + return positions + .map(mapping => ({ + line: mapping.line, + columnStart: mapping.column, + columnEnd: mapping.lastColumn, + })) + .sort((a, b) => { + const line = a.line - b.line; + return line === 0 ? a.column - b.column : line; + }); +} + +async function getGeneratedLocation(location) { + if (!isOriginalId(location.sourceId)) { + return null; + } + + const generatedSourceId = originalToGeneratedId(location.sourceId); + const data = await getSourceMapWithMetadata(generatedSourceId); + if (!data) { + return null; + } + const { urlsById, map } = data; + + const positions = map.allGeneratedPositionsFor({ + source: urlsById.get(location.sourceId), + line: location.line, + column: location.column == null ? 0 : location.column, + }); + + // Prior to source-map 0.7, the source-map module returned the earliest + // generated location in the file when there were multiple generated + // locations. The current comparison fn in 0.7 does not appear to take + // generated location into account properly. + let match; + for (const pos of positions) { + if (!match || pos.line < match.line || pos.column < match.column) { + match = pos; + } + } + + if (!match) { + match = map.generatedPositionFor({ + source: urlsById.get(location.sourceId), + line: location.line, + column: location.column == null ? 0 : location.column, + bias: SourceMapConsumer.LEAST_UPPER_BOUND, + }); + } + + return { + sourceId: generatedSourceId, + line: match.line, + column: match.column, + }; +} + +async function getOriginalLocations(locations, options = {}) { + const maps = {}; + + const results = []; + for (const location of locations) { + let map = maps[location.sourceId]; + if (map === undefined) { + map = await getSourceMap(location.sourceId); + maps[location.sourceId] = map || null; + } + + results.push(map ? getOriginalLocationSync(map, location, options) : null); + } + return results; +} + +function getOriginalLocationSync(map, location, { search } = {}) { + // First check for an exact match + let match = map.originalPositionFor({ + line: location.line, + column: location.column == null ? 0 : location.column, + }); + + // If there is not an exact match, look for a match with a bias at the + // current location and then on subsequent lines + if (search) { + let line = location.line; + let column = location.column == null ? 0 : location.column; + + while (match.source === null) { + match = map.originalPositionFor({ + line, + column, + bias: SourceMapConsumer[search], + }); + + line += search == "LEAST_UPPER_BOUND" ? 1 : -1; + column = search == "LEAST_UPPER_BOUND" ? 0 : Infinity; + } + } + + const { source: sourceUrl, line, column } = match; + if (sourceUrl == null) { + // No url means the location didn't map. + return null; + } + + return { + sourceId: generatedToOriginalId(location.sourceId, sourceUrl), + sourceUrl, + line, + column, + }; +} + +async function getOriginalLocation(location, options = {}) { + if (!isGeneratedId(location.sourceId)) { + return null; + } + + const map = await getSourceMap(location.sourceId); + if (!map) { + return null; + } + + return getOriginalLocationSync(map, location, options); +} + +async function getOriginalSourceText(originalSourceId) { + assert(isOriginalId(originalSourceId), "Source is not an original source"); + + const generatedSourceId = originalToGeneratedId(originalSourceId); + const data = await getSourceMapWithMetadata(generatedSourceId); + if (!data) { + return null; + } + const { urlsById, map } = data; + + const url = urlsById.get(originalSourceId); + let text = map.sourceContentFor(url, true); + if (!text) { + try { + const response = await networkRequest(url, { + sourceMapBaseURL: map.sourceMapBaseURL, + loadFromCache: false, + allowsRedirects: false, + }); + text = response.content; + } catch (err) { + // Workers exceptions are processed by worker-utils module and + // only metadata attribute is transferred between threads. + // Notify the main thread about which url failed loading. + err.metadata = { + url, + }; + throw err; + } + } + + return { + text, + contentType: getContentType(url || ""), + }; +} + +/** + * Find the set of ranges on the generated file that map from the original + * file's locations. + * + * @param sourceId - The original ID of the file we are processing. + * @param url - The original URL of the file we are processing. + * @param mergeUnmappedRegions - If unmapped regions are encountered between + * two mappings for the given original file, allow the two mappings to be + * merged anyway. This is useful if you are more interested in the general + * contiguous ranges associated with a file, rather than the specifics of + * the ranges provided by the sourcemap. + */ +const GENERATED_MAPPINGS = new WeakMap(); +async function getGeneratedRangesForOriginal( + sourceId, + mergeUnmappedRegions = false +) { + assert(isOriginalId(sourceId), "Source is not an original source"); + + const data = await getSourceMapWithMetadata(originalToGeneratedId(sourceId)); + // NOTE: this is only needed for Flow + if (!data) { + return []; + } + const { urlsById, map } = data; + const url = urlsById.get(sourceId); + + if (!COMPUTED_SPANS.has(map)) { + COMPUTED_SPANS.add(map); + map.computeColumnSpans(); + } + + if (!GENERATED_MAPPINGS.has(map)) { + GENERATED_MAPPINGS.set(map, new Map()); + } + + const generatedRangesMap = GENERATED_MAPPINGS.get(map); + if (!generatedRangesMap) { + return []; + } + + if (generatedRangesMap.has(sourceId)) { + // NOTE we need to coerce the result to an array for Flow + return generatedRangesMap.get(sourceId) || []; + } + + // Gather groups of mappings on the generated file, with new groups created + // if we cross a mapping for a different file. + let currentGroup = []; + const originalGroups = [currentGroup]; + map.eachMapping( + mapping => { + if (mapping.source === url) { + currentGroup.push({ + start: { + line: mapping.generatedLine, + column: mapping.generatedColumn, + }, + end: { + line: mapping.generatedLine, + // The lastGeneratedColumn value is an inclusive value so we add + // one to it to get the exclusive end position. + column: mapping.lastGeneratedColumn + 1, + }, + }); + } else if (typeof mapping.source === "string" && currentGroup.length) { + // If there is a URL, but it is for a _different_ file, we create a + // new group of mappings so that we can tell + currentGroup = []; + originalGroups.push(currentGroup); + } + }, + null, + SourceMapConsumer.GENERATED_ORDER + ); + + const generatedMappingsForOriginal = []; + if (mergeUnmappedRegions) { + // If we don't care about excluding unmapped regions, then we just need to + // create a range that is the fully encompasses each group, ignoring the + // empty space between each individual range. + for (const group of originalGroups) { + if (group.length) { + generatedMappingsForOriginal.push({ + start: group[0].start, + end: group[group.length - 1].end, + }); + } + } + } else { + let lastEntry; + for (const group of originalGroups) { + lastEntry = null; + for (const { start, end } of group) { + const lastEnd = lastEntry + ? wrappedMappingPosition(lastEntry.end) + : null; + + // If this entry comes immediately after the previous one, extend the + // range of the previous entry instead of adding a new one. + if ( + lastEntry && + lastEnd && + lastEnd.line === start.line && + lastEnd.column === start.column + ) { + lastEntry.end = end; + } else { + const newEntry = { start, end }; + generatedMappingsForOriginal.push(newEntry); + lastEntry = newEntry; + } + } + } + } + + generatedRangesMap.set(sourceId, generatedMappingsForOriginal); + return generatedMappingsForOriginal; +} + +function wrappedMappingPosition(pos) { + if (pos.column !== Infinity) { + return pos; + } + + // If the end of the entry consumes the whole line, treat it as wrapping to + // the next line. + return { + line: pos.line + 1, + column: 0, + }; +} + +async function getFileGeneratedRange(originalSourceId) { + assert(isOriginalId(originalSourceId), "Source is not an original source"); + + const data = await getSourceMapWithMetadata( + originalToGeneratedId(originalSourceId) + ); + if (!data) { + return null; + } + const { urlsById, map } = data; + + const start = map.generatedPositionFor({ + source: urlsById.get(originalSourceId), + line: 1, + column: 0, + bias: SourceMapConsumer.LEAST_UPPER_BOUND, + }); + + const end = map.generatedPositionFor({ + source: urlsById.get(originalSourceId), + line: Number.MAX_SAFE_INTEGER, + column: Number.MAX_SAFE_INTEGER, + bias: SourceMapConsumer.GREATEST_LOWER_BOUND, + }); + + return { + start, + end, + }; +} + +/** + * Set the sourceMap for multiple passed source ids. + * + * @param {Array<string>} generatedSourceIds + * @param {Object} map: An actual sourcemap (as generated with SourceMapGenerator#toJSON) + */ +function setSourceMapForGeneratedSources(generatedSourceIds, map) { + const sourceMapConsumer = new SourceMapConsumer(map); + for (const generatedId of generatedSourceIds) { + setSourceMap(generatedId, Promise.resolve(sourceMapConsumer)); + } +} + +function clearSourceMaps() { + clearSourceMapsRequests(); + clearWasmXScopes(); + clearOriginalURLs(); +} + +module.exports = { + getOriginalURLs, + hasOriginalURL, + getOriginalRanges, + getGeneratedRanges, + getGeneratedLocation, + getOriginalLocation, + getOriginalLocations, + getOriginalSourceText, + getGeneratedRangesForOriginal, + getFileGeneratedRange, + getSourceMapIgnoreList, + setSourceMapForGeneratedSources, + clearSourceMaps, +}; |