Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 185 additions & 53 deletions lib/internal/source_map/source_map.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,16 @@ const {
ArrayPrototypePush,
ArrayPrototypeSlice,
ArrayPrototypeSort,
Int32Array,
MathMax,
ObjectFreeze,
ObjectPrototypeHasOwnProperty,
StringPrototypeCharAt,
StringPrototypeCharCodeAt,
Symbol,
TypedArrayPrototypeSet,
TypedArrayPrototypeSlice,
TypedArrayPrototypeSubarray,
} = primordials;

const { validateObject } = require('internal/validators');
Expand All @@ -85,7 +91,20 @@ const VLQ_BASE_SHIFT = 5;
const VLQ_BASE_MASK = (1 << 5) - 1;
const VLQ_CONTINUATION_MASK = 1 << 5;

const kMappings = Symbol('kMappings');
const kFirstEntry = Symbol('kFirstEntry');

// Each entry is stored as kEntrySize consecutive Int32 fields: generated line,
// generated column, source index, original line, original column and name
// index. Large maps have millions of entries, and one array per entry costs
// several times more memory than the numbers it holds.
const kEntrySize = 6;
// Source index of an entry that has only a generated column.
const kNoSource = -1;
// Source index of an entry whose source index is outside `sources`.
const kUnknownSource = -2;
const kNoName = -1;
const kComma = 0x2C;
const kSemicolon = 0x3B;

class StringCharIterator {
/**
Expand Down Expand Up @@ -126,7 +145,10 @@ class StringCharIterator {
*/
class SourceMap {
#payload;
#mappings = [];
#entries;
#entryCount = 0;
#sourceURLs = [];
#names = [];
#sources = {};
#sourceContentByURL = {};
#lineLengths = undefined;
Expand Down Expand Up @@ -156,8 +178,8 @@ class SourceMap {
return this.#payload;
}

get [kMappings]() {
return this.#mappings;
get [kFirstEntry]() {
return this.#entryCount ? this.#entryAt(0) : {};
}

/**
Expand All @@ -168,14 +190,112 @@ class SourceMap {
}

#parseMappingPayload = () => {
if (this.#payload.sections) {
this.#parseSections(this.#payload.sections);
const { sections } = this.#payload;
let segments = 0;
if (sections) {
for (let i = 0; i < sections.length; ++i) {
segments += countSegments(sections[i]?.map?.mappings);
}
} else {
segments = countSegments(this.#payload.mappings);
}
this.#entries = new Int32Array(segments * kEntrySize);
if (sections) {
this.#parseSections(sections);
} else {
this.#parseMap(this.#payload, 0, 0);
}
ArrayPrototypeSort(this.#mappings, compareSourceMapEntry);
this.#sortEntries();
};

#sortEntries() {
const entries = this.#entries;
const count = this.#entryCount;
const compare = (a, b) => {
const lineDelta = entries[a * kEntrySize] - entries[b * kEntrySize];
return lineDelta ||
entries[a * kEntrySize + 1] - entries[b * kEntrySize + 1];
};
let sorted = true;
for (let i = 1; i < count; ++i) {
if (compare(i - 1, i) > 0) {
sorted = false;
break;
}
}
if (sorted) {
if (entries.length !== count * kEntrySize) {
this.#entries =
TypedArrayPrototypeSlice(entries, 0, count * kEntrySize);
}
return;
}
// Sorting indices keeps equal positions in insertion order, like the
// stable sort of entries this replaces.
const order = [];
for (let i = 0; i < count; ++i) {
ArrayPrototypePush(order, i);
}
ArrayPrototypeSort(order, compare);
const sortedEntries = new Int32Array(count * kEntrySize);
for (let i = 0; i < count; ++i) {
const offset = order[i] * kEntrySize;
TypedArrayPrototypeSet(
sortedEntries,
TypedArrayPrototypeSubarray(entries, offset, offset + kEntrySize),
i * kEntrySize,
);
}
this.#entries = sortedEntries;
}

#pushEntry(lineNumber, columnNumber, sourceIndex, sourceLineNumber,
sourceColumnNumber, nameIndex) {
const offset = this.#entryCount * kEntrySize;
if (offset === this.#entries.length) {
// Only reached when the parser finds more segments than
// countSegments(), which happens for malformed mappings.
const grown =
new Int32Array(MathMax(this.#entries.length * 2, kEntrySize * 16));
TypedArrayPrototypeSet(grown, this.#entries);
this.#entries = grown;
}
const entries = this.#entries;
entries[offset] = lineNumber;
entries[offset + 1] = columnNumber;
entries[offset + 2] = sourceIndex;
entries[offset + 3] = sourceLineNumber;
entries[offset + 4] = sourceColumnNumber;
entries[offset + 5] = nameIndex;
this.#entryCount++;
}

#entryAt(index) {
const entries = this.#entries;
const offset = index * kEntrySize;
const sourceIndex = entries[offset + 2];
if (sourceIndex === kNoSource) {
return {
generatedLine: entries[offset],
generatedColumn: entries[offset + 1],
originalSource: undefined,
originalLine: undefined,
originalColumn: undefined,
name: undefined,
};
}
const nameIndex = entries[offset + 5];
return {
generatedLine: entries[offset],
generatedColumn: entries[offset + 1],
originalSource: sourceIndex === kUnknownSource ?
undefined : this.#sourceURLs[sourceIndex],
originalLine: entries[offset + 3],
originalColumn: entries[offset + 4],
name: nameIndex === kNoName ? undefined : this.#names[nameIndex],
};
}

/**
* @param {Array.<SourceMapV3.Section>} sections
*/
Expand All @@ -192,35 +312,30 @@ class SourceMap {
* @returns {object} representing start of range if found, or empty object
*/
findEntry(lineOffset, columnOffset) {
const entries = this.#entries;
let first = 0;
let count = this.#mappings.length;
let count = this.#entryCount;
if (!count) {
return {};
}
while (count > 1) {
const step = count >> 1;
const middle = first + step;
const mapping = this.#mappings[middle];
if (lineOffset < mapping[0] ||
(lineOffset === mapping[0] && columnOffset < mapping[1])) {
const line = entries[middle * kEntrySize];
if (lineOffset < line ||
(lineOffset === line &&
columnOffset < entries[middle * kEntrySize + 1])) {
count = step;
} else {
first = middle;
count -= step;
}
}
const entry = this.#mappings[first];
if (!first && entry && (lineOffset < entry[0] ||
(lineOffset === entry[0] && columnOffset < entry[1]))) {
return {};
} else if (!entry) {
if (!first && (lineOffset < entries[0] ||
(lineOffset === entries[0] && columnOffset < entries[1]))) {
return {};
}
return {
generatedLine: entry[0],
generatedColumn: entry[1],
originalSource: entry[2],
originalLine: entry[3],
originalColumn: entry[4],
name: entry[5],
};
return this.#entryAt(first);
}

/**
Expand Down Expand Up @@ -260,18 +375,24 @@ class SourceMap {

const sources = [];
const originalToCanonicalURLMap = {};
const sourceBase = this.#sourceURLs.length;
for (let i = 0; i < map.sources.length; ++i) {
const url = map.sources[i];
originalToCanonicalURLMap[url] = url;
ArrayPrototypePush(sources, url);
ArrayPrototypePush(this.#sourceURLs, url);
this.#sources[url] = true;

if (map.sourcesContent?.[i])
this.#sourceContentByURL[url] = map.sourcesContent[i];
}
const names = map.names ?? [];
const nameBase = this.#names.length;
for (let i = 0; i < names.length; ++i) {
ArrayPrototypePush(this.#names, names[i]);
}

const stringCharIterator = new StringCharIterator(map.mappings);
let sourceURL = sources[sourceIndex];
while (true) {
if (stringCharIterator.peek() === ',')
stringCharIterator.next();
Expand All @@ -287,33 +408,59 @@ class SourceMap {

columnNumber += decodeVLQ(stringCharIterator);
if (isSeparator(stringCharIterator.peek())) {
ArrayPrototypePush(this.#mappings, [lineNumber, columnNumber]);
this.#pushEntry(lineNumber, columnNumber, kNoSource, 0, 0, kNoName);
continue;
}

const sourceIndexDelta = decodeVLQ(stringCharIterator);
if (sourceIndexDelta) {
sourceIndex += sourceIndexDelta;
sourceURL = sources[sourceIndex];
}
sourceIndex += decodeVLQ(stringCharIterator);
sourceLineNumber += decodeVLQ(stringCharIterator);
sourceColumnNumber += decodeVLQ(stringCharIterator);

let name;
let entryNameIndex = kNoName;
if (!isSeparator(stringCharIterator.peek())) {
nameIndex += decodeVLQ(stringCharIterator);
name = map.names?.[nameIndex];
if (nameIndex >= 0 && nameIndex < names.length) {
entryNameIndex = nameBase + nameIndex;
}
}

ArrayPrototypePush(
this.#mappings,
[lineNumber, columnNumber, sourceURL, sourceLineNumber,
sourceColumnNumber, name],
this.#pushEntry(
lineNumber,
columnNumber,
sourceIndex >= 0 && sourceIndex < sources.length ?
sourceBase + sourceIndex : kUnknownSource,
sourceLineNumber,
sourceColumnNumber,
entryNameIndex,
);
}
}
}

/**
* Counts the segments in a mappings string, so entry storage can be
* allocated once.
* @param {string} mappings
* @returns {number}
*/
function countSegments(mappings) {
if (typeof mappings !== 'string') {
return 0;
}
let count = 0;
let inSegment = false;
for (let i = 0; i < mappings.length; ++i) {
const code = StringPrototypeCharCodeAt(mappings, i);
if (code === kComma || code === kSemicolon) {
inSegment = false;
} else if (!inSegment) {
inSegment = true;
count++;
}
}
return count;
}

/**
* @param {string} char
* @returns {boolean}
Expand Down Expand Up @@ -370,22 +517,7 @@ function cloneSourceMapV3(payload) {
return ObjectFreeze(payload);
}

/**
* @param {Array} entry1 source map entry [lineNumber, columnNumber, sourceURL,
* sourceLineNumber, sourceColumnNumber]
* @param {Array} entry2 source map entry.
* @returns {number}
*/
function compareSourceMapEntry(entry1, entry2) {
const { 0: lineNumber1, 1: columnNumber1 } = entry1;
const { 0: lineNumber2, 1: columnNumber2 } = entry2;
if (lineNumber1 !== lineNumber2) {
return lineNumber1 - lineNumber2;
}
return columnNumber1 - columnNumber2;
}

module.exports = {
kMappings,
kFirstEntry,
SourceMap,
};
4 changes: 2 additions & 2 deletions lib/internal/test_runner/coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const { isWindows, setupCoverageHooks } = require('internal/util');
const { tmpdir } = require('os');
const { join, resolve, relative } = require('path');
const { fileURLToPath, pathToFileURL, URL } = require('internal/url');
const { kMappings, SourceMap } = require('internal/source_map/source_map');
const { kFirstEntry, SourceMap } = require('internal/source_map/source_map');
const {
codes: {
ERR_OPERATION_FAILED,
Expand Down Expand Up @@ -521,7 +521,7 @@ class TestCoverage {
if (!startEntry.originalSource && endEntry.originalSource &&
lines[0].line === 1 && startOffset === 0 && lines[0].startOffset === 0) {
// Edge case when the first line is not mappable
const { 2: originalSource, 3: originalLine, 4: originalColumn } = sourceMap[kMappings][0];
const { originalSource, originalLine, originalColumn } = sourceMap[kFirstEntry];
startEntry = { __proto__: null, originalSource, originalLine, originalColumn };
}

Expand Down
Loading
Loading