diff --git a/lib/mapping-list.js b/lib/mapping-list.js index 06d1274a..61eb6572 100644 --- a/lib/mapping-list.js +++ b/lib/mapping-list.js @@ -5,75 +5,236 @@ * http://opensource.org/licenses/BSD-3-Clause */ -var util = require('./util'); +// A data structure that holds generator-side mappings in an Int32Array slab +// instead of one JS object per mapping. Eliminates the per-mapping heap +// allocation that dominated `addMapping` throughput on the previous +// object-array implementation (per bench-data-followups #2 — generator memory +// ratio ≈ generator speed ratio). +// +// Slab layout: 6 i32 slots per mapping, indexed by the F_* constants below. +// Sentinel -1 marks "no value" for the four optional slots (orig line/col, +// source idx, name idx). Source and name strings live in the owning +// SourceMapGenerator's ArraySet pair (`sources`, `names`) — MappingList stores +// indices into those ArraySets, so serialization can read indices directly +// from the slab with no per-mapping `indexOf` lookup. -/** - * Determine whether mappingB is after mappingA with respect to generated - * position. - */ -function generatedPositionAfter(mappingA, mappingB) { - // Optimized for most common case - var lineA = mappingA.generatedLine; - var lineB = mappingB.generatedLine; - var columnA = mappingA.generatedColumn; - var columnB = mappingB.generatedColumn; - return lineB > lineA || lineB == lineA && columnB >= columnA || - util.compareByGeneratedPositionsInflated(mappingA, mappingB) <= 0; -} +var FIELDS_PER_MAPPING = 6; +var F_GEN_LINE = 0; +var F_GEN_COL = 1; +var F_SRC_IDX = 2; // -1 = no source +var F_ORIG_LINE = 3; // -1 = no original line +var F_ORIG_COL = 4; // -1 = no original column +var F_NAME_IDX = 5; // -1 = no name + +var INITIAL_CAPACITY = 16; /** * A data structure to provide a sorted view of accumulated mappings in a - * performance conscious manner. It trades a neglibable overhead in general - * case for a large speedup in case of mappings being added in order. + * performance conscious manner. It trades a negligible overhead in the general + * case for a large speedup in the common case of mappings being added in + * generated-position order. + * + * The owning SourceMapGenerator passes its `sources` and `names` ArraySets so + * we can resolve indices back to strings on `unsortedForEach` / `toArray` / + * `applySourceMap` rebuilds. */ -function MappingList() { - this._array = []; +function MappingList(sources, names) { + this._sources = sources; + this._names = names; + this._capacity = INITIAL_CAPACITY; + this._buf = new Int32Array(INITIAL_CAPACITY * FIELDS_PER_MAPPING); + this._count = 0; this._sorted = true; - // Serves as infimum - this._last = {generatedLine: -1, generatedColumn: 0}; + // Sentinel infimum — first add always sorts strictly after this. + this._lastGenLine = -1; + this._lastGenCol = 0; + this._lastSrcIdx = -1; + this._lastOrigLine = -1; + this._lastOrigCol = -1; + this._lastNameIdx = -1; } +MappingList.prototype._grow = function MappingList_grow() { + var newCap = this._capacity * 2; + var newBuf = new Int32Array(newCap * FIELDS_PER_MAPPING); + newBuf.set(this._buf); + this._buf = newBuf; + this._capacity = newCap; +}; + +/** + * Add a single mapping. All arguments are integers; pass -1 for absent + * source/name/originalLine/originalColumn. + */ +MappingList.prototype.add = function MappingList_add( + genLine, genCol, origLine, origCol, srcIdx, nameIdx +) { + if (this._count === this._capacity) { + this._grow(); + } + + // Sortedness check — equivalent of the old + // `generatedPositionAfter(this._last, newMapping)` returning true. + // Tie-break order uses integer compare on src/name instead of strcmp + // on the source/name strings; that preserves equality classes + // (same srcIdx ⇔ same source string), so the serializer's dedup + // still works. + var after; + if (genLine !== this._lastGenLine) after = genLine > this._lastGenLine; + else if (genCol !== this._lastGenCol) after = genCol > this._lastGenCol; + else if (srcIdx !== this._lastSrcIdx) after = srcIdx > this._lastSrcIdx; + else if (origLine !== this._lastOrigLine) after = origLine > this._lastOrigLine; + else if (origCol !== this._lastOrigCol) after = origCol > this._lastOrigCol; + else after = nameIdx >= this._lastNameIdx; + + if (after) { + this._lastGenLine = genLine; + this._lastGenCol = genCol; + this._lastSrcIdx = srcIdx; + this._lastOrigLine = origLine; + this._lastOrigCol = origCol; + this._lastNameIdx = nameIdx; + } else { + this._sorted = false; + } + + var off = this._count * FIELDS_PER_MAPPING; + var buf = this._buf; + buf[off + F_GEN_LINE] = genLine; + buf[off + F_GEN_COL] = genCol; + buf[off + F_SRC_IDX] = srcIdx; + buf[off + F_ORIG_LINE] = origLine; + buf[off + F_ORIG_COL] = origCol; + buf[off + F_NAME_IDX] = nameIdx; + this._count++; +}; + +/** + * Materialize one mapping at slab index `i` back to a JS object with the + * shape callers used to see. Source/name indices are resolved through the + * owning generator's ArraySets; -1 sentinels become `null`. + */ +MappingList.prototype._materialize = function MappingList_materialize(i) { + var off = i * FIELDS_PER_MAPPING; + var buf = this._buf; + var srcIdx = buf[off + F_SRC_IDX]; + var origLine = buf[off + F_ORIG_LINE]; + var origCol = buf[off + F_ORIG_COL]; + var nameIdx = buf[off + F_NAME_IDX]; + return { + generatedLine: buf[off + F_GEN_LINE], + generatedColumn: buf[off + F_GEN_COL], + source: srcIdx === -1 ? null : this._sources.at(srcIdx), + originalLine: origLine === -1 ? null : origLine, + originalColumn: origCol === -1 ? null : origCol, + name: nameIdx === -1 ? null : this._names.at(nameIdx) + }; +}; + /** - * Iterate through internal items. This method takes the same arguments that - * `Array.prototype.forEach` takes. + * Iterate through internal items. Each callback invocation receives a + * freshly-materialized mapping object. Mutating that object has no effect + * on the underlying slab — callers that need to transform mappings should + * rebuild the list (see SourceMapGenerator.applySourceMap). * * NOTE: The order of the mappings is NOT guaranteed. */ MappingList.prototype.unsortedForEach = function MappingList_forEach(aCallback, aThisArg) { - this._array.forEach(aCallback, aThisArg); + for (var i = 0; i < this._count; i++) { + aCallback.call(aThisArg, this._materialize(i)); + } }; /** - * Add the given source mapping. - * - * @param Object aMapping + * Returns true if the mapping at index `i` is field-for-field identical to + * the mapping at index `i - 1`. Used by `_serializeMappings` to skip + * emitting duplicate segments — equivalent of the old + * `compareByGeneratedPositionsInflated(a, b) === 0` dedup check, but + * direct slab reads. */ -MappingList.prototype.add = function MappingList_add(aMapping) { - if (generatedPositionAfter(this._last, aMapping)) { - this._last = aMapping; - this._array.push(aMapping); - } else { - this._sorted = false; - this._array.push(aMapping); +MappingList.prototype._equalsPrev = function MappingList_equalsPrev(i) { + var a = i * FIELDS_PER_MAPPING; + var b = a - FIELDS_PER_MAPPING; + var buf = this._buf; + return buf[a + F_GEN_LINE] === buf[b + F_GEN_LINE] && + buf[a + F_GEN_COL] === buf[b + F_GEN_COL] && + buf[a + F_SRC_IDX] === buf[b + F_SRC_IDX] && + buf[a + F_ORIG_LINE] === buf[b + F_ORIG_LINE] && + buf[a + F_ORIG_COL] === buf[b + F_ORIG_COL] && + buf[a + F_NAME_IDX] === buf[b + F_NAME_IDX]; +}; + +MappingList.prototype._sort = function MappingList_sort() { + var n = this._count; + var buf = this._buf; + // n <= 1 falls through naturally — perm of length 0/1 sorts no-op, copy + // loop runs 0/1 times. No early-return guard needed. + // Sort a permutation array by mapping fields, then permute the slab. + // Build a packed key for stable sort: V8 .sort is Tim Sort (stable + // since ES2019), so equal keys preserve insertion order. + var perm = new Array(n); + for (var i = 0; i < n; i++) perm[i] = i; + + perm.sort(function (a, b) { + var oa = a * FIELDS_PER_MAPPING; + var ob = b * FIELDS_PER_MAPPING; + var cmp = buf[oa + F_GEN_LINE] - buf[ob + F_GEN_LINE]; + if (cmp !== 0) return cmp; + cmp = buf[oa + F_GEN_COL] - buf[ob + F_GEN_COL]; + if (cmp !== 0) return cmp; + cmp = buf[oa + F_SRC_IDX] - buf[ob + F_SRC_IDX]; + if (cmp !== 0) return cmp; + cmp = buf[oa + F_ORIG_LINE] - buf[ob + F_ORIG_LINE]; + if (cmp !== 0) return cmp; + cmp = buf[oa + F_ORIG_COL] - buf[ob + F_ORIG_COL]; + if (cmp !== 0) return cmp; + return buf[oa + F_NAME_IDX] - buf[ob + F_NAME_IDX]; + }); + + var newBuf = new Int32Array(this._capacity * FIELDS_PER_MAPPING); + for (var k = 0; k < n; k++) { + var src = perm[k] * FIELDS_PER_MAPPING; + var dst = k * FIELDS_PER_MAPPING; + newBuf[dst + F_GEN_LINE] = buf[src + F_GEN_LINE]; + newBuf[dst + F_GEN_COL] = buf[src + F_GEN_COL]; + newBuf[dst + F_SRC_IDX] = buf[src + F_SRC_IDX]; + newBuf[dst + F_ORIG_LINE] = buf[src + F_ORIG_LINE]; + newBuf[dst + F_ORIG_COL] = buf[src + F_ORIG_COL]; + newBuf[dst + F_NAME_IDX] = buf[src + F_NAME_IDX]; } + this._buf = newBuf; }; /** - * Returns the flat, sorted array of mappings. The mappings are sorted by - * generated position. + * Returns the flat, sorted array of materialized mappings. The mappings + * are sorted by generated position. * - * WARNING: This method returns internal data without copying, for - * performance. The return value must NOT be mutated, and should be treated as - * an immutable borrow. If you want to take ownership, you must make your own - * copy. + * Internal hot paths (`_serializeMappings`, `BasicSourceMapConsumer.fromSourceMap`) + * read the slab directly instead. This method is kept for external API + * compatibility — calling it materializes one JS object per mapping. */ MappingList.prototype.toArray = function MappingList_toArray() { if (!this._sorted) { - this._array.sort(util.compareByGeneratedPositionsInflated); + this._sort(); this._sorted = true; } - return this._array; + var out = new Array(this._count); + for (var i = 0; i < this._count; i++) { + out[i] = this._materialize(i); + } + return out; }; exports.MappingList = MappingList; + +// Slab-layout constants exported for the internal hot-path consumers +// (`SourceMapGenerator._serializeMappings`, `BasicSourceMapConsumer.fromSourceMap`) +// that bypass `toArray()` materialization. +exports.FIELDS_PER_MAPPING = FIELDS_PER_MAPPING; +exports.F_GEN_LINE = F_GEN_LINE; +exports.F_GEN_COL = F_GEN_COL; +exports.F_SRC_IDX = F_SRC_IDX; +exports.F_ORIG_LINE = F_ORIG_LINE; +exports.F_ORIG_COL = F_ORIG_COL; +exports.F_NAME_IDX = F_NAME_IDX; diff --git a/lib/source-map-consumer.js b/lib/source-map-consumer.js index 6f961a70..f31964c0 100644 --- a/lib/source-map-consumer.js +++ b/lib/source-map-consumer.js @@ -10,6 +10,14 @@ var binarySearch = require('./binary-search'); var ArraySet = require('./array-set').ArraySet; var base64VLQ = require('./base64-vlq'); var quickSort = require('./quick-sort').quickSort; +var mappingListModule = require('./mapping-list'); +var ML_FIELDS = mappingListModule.FIELDS_PER_MAPPING; +var ML_F_GEN_LINE = mappingListModule.F_GEN_LINE; +var ML_F_GEN_COL = mappingListModule.F_GEN_COL; +var ML_F_SRC_IDX = mappingListModule.F_SRC_IDX; +var ML_F_ORIG_LINE = mappingListModule.F_ORIG_LINE; +var ML_F_ORIG_COL = mappingListModule.F_ORIG_COL; +var ML_F_NAME_IDX = mappingListModule.F_NAME_IDX; function SourceMapConsumer(aSourceMap, aSourceMapURL) { var sourceMap = aSourceMap; @@ -443,45 +451,52 @@ BasicSourceMapConsumer.fromSourceMap = return util.computeSourceURL(smc.sourceRoot, s, aSourceMapURL); }); - // Because we are modifying the entries (by converting string sources and - // names to indices into the sources and names ArraySets), we have to make - // a copy of the entry or else bad things happen. Shared mutable state - // strikes again! See github issue #191. - - var generatedMappings = aSourceMap._mappings.toArray().slice(); - var destGeneratedMappings = smc.__generatedMappings = []; + // Read the generator's MappingList slab directly. The slab already + // stores source/name as integer indices into aSourceMap._sources / + // _names, and our smc._sources / smc._names were initialized from the + // same toArray() above — so the indices are identical and no `indexOf` + // is needed per mapping. + var ml = aSourceMap._mappings; + if (!ml._sorted) { + ml._sort(); + ml._sorted = true; + } + var mlBuf = ml._buf; + var mlCount = ml._count; + var destGeneratedMappings = smc.__generatedMappings = new Array(mlCount); // Bucket original-side mappings by source index — same pattern as // _buildOriginalMappings and IndexedSourceMapConsumer._parseMappings — // so the per-bucket sort can use compareByOriginalPositionsNoSource and // skip the function-call strcmp(source, source) primary key. var originalBuckets = []; - for (var i = 0, length = generatedMappings.length; i < length; i++) { - var srcMapping = generatedMappings[i]; + for (var i = 0; i < mlCount; i++) { + var mlOff = i * ML_FIELDS; var destMapping = new Mapping; - destMapping.generatedLine = srcMapping.generatedLine; - destMapping.generatedColumn = srcMapping.generatedColumn; - - if (srcMapping.source) { - var sourceIdx = sources.indexOf(srcMapping.source); - destMapping.source = sourceIdx; - destMapping.originalLine = srcMapping.originalLine; - destMapping.originalColumn = srcMapping.originalColumn; - - if (srcMapping.name) { - destMapping.name = names.indexOf(srcMapping.name); + destMapping.generatedLine = mlBuf[mlOff + ML_F_GEN_LINE]; + destMapping.generatedColumn = mlBuf[mlOff + ML_F_GEN_COL]; + + var srcIdx = mlBuf[mlOff + ML_F_SRC_IDX]; + if (srcIdx !== -1) { + destMapping.source = srcIdx; + destMapping.originalLine = mlBuf[mlOff + ML_F_ORIG_LINE]; + destMapping.originalColumn = mlBuf[mlOff + ML_F_ORIG_COL]; + + var nameIdx = mlBuf[mlOff + ML_F_NAME_IDX]; + if (nameIdx !== -1) { + destMapping.name = nameIdx; } - while (originalBuckets.length <= sourceIdx) { + while (originalBuckets.length <= srcIdx) { originalBuckets.push(null); } - if (originalBuckets[sourceIdx] === null) { - originalBuckets[sourceIdx] = []; + if (originalBuckets[srcIdx] === null) { + originalBuckets[srcIdx] = []; } - originalBuckets[sourceIdx].push(destMapping); + originalBuckets[srcIdx].push(destMapping); } - destGeneratedMappings.push(destMapping); + destGeneratedMappings[i] = destMapping; } var nonNullBuckets = []; diff --git a/lib/source-map-generator.js b/lib/source-map-generator.js index 4867e0c1..5d8f6d0a 100644 --- a/lib/source-map-generator.js +++ b/lib/source-map-generator.js @@ -8,7 +8,15 @@ var base64VLQ = require('./base64-vlq'); var util = require('./util'); var ArraySet = require('./array-set').ArraySet; -var MappingList = require('./mapping-list').MappingList; +var mappingListModule = require('./mapping-list'); +var MappingList = mappingListModule.MappingList; +var FIELDS_PER_MAPPING = mappingListModule.FIELDS_PER_MAPPING; +var F_GEN_LINE = mappingListModule.F_GEN_LINE; +var F_GEN_COL = mappingListModule.F_GEN_COL; +var F_SRC_IDX = mappingListModule.F_SRC_IDX; +var F_ORIG_LINE = mappingListModule.F_ORIG_LINE; +var F_ORIG_COL = mappingListModule.F_ORIG_COL; +var F_NAME_IDX = mappingListModule.F_NAME_IDX; /** * An instance of the SourceMapGenerator represents a source map which is @@ -30,7 +38,7 @@ function SourceMapGenerator(aArgs) { this._ignoreInvalidMapping = aArgs.ignoreInvalidMapping != null ? aArgs.ignoreInvalidMapping : false; this._sources = new ArraySet(); this._names = new ArraySet(); - this._mappings = new MappingList(); + this._mappings = new MappingList(this._sources, this._names); this._sourcesContents = null; } @@ -119,26 +127,40 @@ SourceMapGenerator.prototype.addMapping = } } + // Resolve source/name to integer indices once here so MappingList.add + // can store them as i32s in the slab. ArraySet.add is idempotent (its + // own has() check internally), and indexOf is a fast Map.get afterward. + var srcIdx = -1; if (source != null) { source = String(source); - // ArraySet.add is idempotent (it does its own duplicate check), so the - // outer has() guard is redundant and just adds an extra function call. this._sources.add(source); + srcIdx = this._sources.indexOf(source); } - + var nameIdx = -1; if (name != null) { name = String(name); this._names.add(name); + nameIdx = this._names.indexOf(name); } - this._mappings.add({ - generatedLine: generated.line, - generatedColumn: generated.column, - originalLine: original != null && original.line, - originalColumn: original != null && original.column, - source: source, - name: name - }); + // _validateMapping enforces numeric original.line/column when original is + // provided; skipValidation users are on their own. -1 sentinel marks the + // "no original" case (source-less mapping). + var origLine = -1; + var origCol = -1; + if (original != null) { + origLine = original.line; + origCol = original.column; + } + + this._mappings.add( + generated.line, + generated.column, + origLine, + origCol, + srcIdx, + nameIdx + ); }; /** @@ -202,49 +224,78 @@ SourceMapGenerator.prototype.applySourceMap = if (sourceRoot != null) { sourceFile = util.relative(sourceRoot, sourceFile); } - // Applying the SourceMap can add and remove items from the sources and - // the names array. + // Applying the SourceMap rebuilds the sources/names ArraySets and the + // MappingList from scratch. The old code mutated mapping objects via the + // unsortedForEach callback — that relied on the callback receiving the + // actual stored reference, which slab-backed storage can't provide. We + // walk the old slab, resolve indices through the old ArraySets, transform + // mappings whose source matches `sourceFile`, and emit into a fresh + // MappingList bound to the new ArraySets. var newSources = new ArraySet(); var newNames = new ArraySet(); - - // Find mappings for the "sourceFile" - this._mappings.unsortedForEach(function (mapping) { - if (mapping.source === sourceFile && mapping.originalLine != null) { - // Check if it can be mapped by the source map, then update the mapping. + var newMappings = new MappingList(newSources, newNames); + + var oldMappings = this._mappings; + var oldBuf = oldMappings._buf; + var oldCount = oldMappings._count; + var oldSources = this._sources; + var oldNames = this._names; + + for (var i = 0; i < oldCount; i++) { + var off = i * FIELDS_PER_MAPPING; + var genLine = oldBuf[off + F_GEN_LINE]; + var genCol = oldBuf[off + F_GEN_COL]; + var srcIdx = oldBuf[off + F_SRC_IDX]; + var origLine = oldBuf[off + F_ORIG_LINE]; + var origCol = oldBuf[off + F_ORIG_COL]; + var nameIdx = oldBuf[off + F_NAME_IDX]; + + var source = srcIdx === -1 ? null : oldSources.at(srcIdx); + var name = nameIdx === -1 ? null : oldNames.at(nameIdx); + + if (source === sourceFile && origLine !== -1) { + // origCol is paired with origLine — addMapping sets both together or + // leaves both as -1. Inside this branch origLine !== -1 so origCol + // is a valid column too. var original = aSourceMapConsumer.originalPositionFor({ - line: mapping.originalLine, - column: mapping.originalColumn + line: origLine, + column: origCol }); if (original.source != null) { - // Copy mapping - mapping.source = original.source; + source = original.source; if (aSourceMapPath != null) { - mapping.source = util.join(aSourceMapPath, mapping.source) + source = util.join(aSourceMapPath, source); } if (sourceRoot != null) { - mapping.source = util.relative(sourceRoot, mapping.source); + source = util.relative(sourceRoot, source); } - mapping.originalLine = original.line; - mapping.originalColumn = original.column; + // originalPositionFor guarantees numeric line/column when source + // is non-null. + origLine = original.line; + origCol = original.column; if (original.name != null) { - mapping.name = original.name; + name = original.name; } } } - var source = mapping.source; - if (source != null && !newSources.has(source)) { + var newSrcIdx = -1; + if (source != null) { newSources.add(source); + newSrcIdx = newSources.indexOf(source); } - - var name = mapping.name; - if (name != null && !newNames.has(name)) { + var newNameIdx = -1; + if (name != null) { newNames.add(name); + newNameIdx = newNames.indexOf(name); } - }, this); + newMappings.add(genLine, genCol, origLine, origCol, newSrcIdx, newNameIdx); + } + this._sources = newSources; this._names = newNames; + this._mappings = newMappings; // Copy sourcesContents of applied map. aSourceMapConsumer.sources.forEach(function (sourceFile) { @@ -351,75 +402,66 @@ SourceMapGenerator.prototype._serializeMappings = var previousName = 0; var previousSource = 0; var result = ''; - var next, val; - var mapping; - var nameIdx; - var sourceIdx; - - // Single-slot caches for the ArraySet.indexOf (Map.get) calls. Consecutive - // mappings almost always share source/name, so cache the last input→idx - // pair to skip the lookup on every match. - var lastSourceStr = null; - var lastSourceIdx = 0; - var lastNameStr = null; - var lastNameIdx = 0; - - var mappings = this._mappings.toArray(); - for (var i = 0, len = mappings.length; i < len; i++) { - mapping = mappings[i]; - next = '' - - if (mapping.generatedLine !== previousGeneratedLine) { + var val; + + // Slab-direct read — bypass MappingList.toArray() materialization. + // Source/name fields stored in the slab are already the int indices + // we need to serialize (resolved when each mapping was added), so + // there's no per-mapping `indexOf` lookup either. + var ml = this._mappings; + if (!ml._sorted) { + ml._sort(); + ml._sorted = true; + } + var buf = ml._buf; + var count = ml._count; + + for (var i = 0; i < count; i++) { + var off = i * FIELDS_PER_MAPPING; + var genLine = buf[off + F_GEN_LINE]; + var genCol = buf[off + F_GEN_COL]; + var srcIdx = buf[off + F_SRC_IDX]; + var origLine = buf[off + F_ORIG_LINE]; + var origCol = buf[off + F_ORIG_COL]; + var nameIdx = buf[off + F_NAME_IDX]; + + var next = ''; + + if (genLine !== previousGeneratedLine) { previousGeneratedColumn = 0; - while (mapping.generatedLine !== previousGeneratedLine) { + while (genLine !== previousGeneratedLine) { next += ';'; previousGeneratedLine++; } - } - else { + } else { if (i > 0) { - if (!util.compareByGeneratedPositionsInflated(mapping, mappings[i - 1])) { - continue; - } + // Dedup: skip when every field equals the previous mapping. + // Equivalent of the old + // `compareByGeneratedPositionsInflated(a, b) === 0` check. + if (ml._equalsPrev(i)) continue; next += ','; } } - val = mapping.generatedColumn - previousGeneratedColumn; + val = genCol - previousGeneratedColumn; next += (val >= -15 && val <= 15) ? vlqEncodeTable[val + 15] : base64VLQ.encode(val); - previousGeneratedColumn = mapping.generatedColumn; + previousGeneratedColumn = genCol; - if (mapping.source != null) { - var srcStr = mapping.source; - if (srcStr === lastSourceStr) { - sourceIdx = lastSourceIdx; - } else { - sourceIdx = this._sources.indexOf(srcStr); - lastSourceStr = srcStr; - lastSourceIdx = sourceIdx; - } - val = sourceIdx - previousSource; + if (srcIdx !== -1) { + val = srcIdx - previousSource; next += (val >= -15 && val <= 15) ? vlqEncodeTable[val + 15] : base64VLQ.encode(val); - previousSource = sourceIdx; + previousSource = srcIdx; // lines are stored 0-based in SourceMap spec version 3 - val = mapping.originalLine - 1 - previousOriginalLine; + val = origLine - 1 - previousOriginalLine; next += (val >= -15 && val <= 15) ? vlqEncodeTable[val + 15] : base64VLQ.encode(val); - previousOriginalLine = mapping.originalLine - 1; + previousOriginalLine = origLine - 1; - val = mapping.originalColumn - previousOriginalColumn; + val = origCol - previousOriginalColumn; next += (val >= -15 && val <= 15) ? vlqEncodeTable[val + 15] : base64VLQ.encode(val); - previousOriginalColumn = mapping.originalColumn; + previousOriginalColumn = origCol; - if (mapping.name != null) { - var nameStr = mapping.name; - if (nameStr === lastNameStr) { - nameIdx = lastNameIdx; - } else { - nameIdx = this._names.indexOf(nameStr); - lastNameStr = nameStr; - lastNameIdx = nameIdx; - } + if (nameIdx !== -1) { val = nameIdx - previousName; next += (val >= -15 && val <= 15) ? vlqEncodeTable[val + 15] : base64VLQ.encode(val); previousName = nameIdx; diff --git a/test/internal/test-mapping-list.js b/test/internal/test-mapping-list.js new file mode 100644 index 00000000..f091ffc6 --- /dev/null +++ b/test/internal/test-mapping-list.js @@ -0,0 +1,278 @@ +/* -*- Mode: js; js-indent-level: 2; -*- */ +/* + * Copyright 2014 Mozilla Foundation and contributors + * Licensed under the New BSD license. See LICENSE or: + * http://opensource.org/licenses/BSD-3-Clause + */ + +// MappingList unit tests. The class is unexported through source-map.js, so +// these tests are kept under test/internal. They exist primarily to cover +// the materialization paths (`unsortedForEach`, `toArray`) — internal hot +// paths (`_serializeMappings`, `BasicSourceMapConsumer.fromSourceMap`) read +// the i32 slab directly and don't exercise these methods. + +var test = require('node:test').test; +var assert = require('node:assert'); + +var ArraySet = require('../../lib/array-set').ArraySet; +var MappingList = require('../../lib/mapping-list').MappingList; + +function makeList() { + var sources = new ArraySet(); + var names = new ArraySet(); + sources.add('a.js'); + sources.add('b.js'); + names.add('foo'); + return { list: new MappingList(sources, names), sources: sources, names: names }; +} + +test('MappingList.unsortedForEach yields materialized mappings with resolved source/name strings', () => { + var ctx = makeList(); + ctx.list.add(1, 0, 5, 2, /*srcIdx=*/0, /*nameIdx=*/0); + ctx.list.add(1, 10, -1, -1, /*srcIdx=*/1, /*nameIdx=*/-1); + + var seen = []; + ctx.list.unsortedForEach(function (m) { + seen.push(m); + }); + + assert.deepStrictEqual(seen[0], { + generatedLine: 1, + generatedColumn: 0, + source: 'a.js', + originalLine: 5, + originalColumn: 2, + name: 'foo' + }); + // -1 sentinels become null in the materialized shape. + assert.deepStrictEqual(seen[1], { + generatedLine: 1, + generatedColumn: 10, + source: 'b.js', + originalLine: null, + originalColumn: null, + name: null + }); +}); + +test('MappingList.unsortedForEach passes thisArg', () => { + var ctx = makeList(); + ctx.list.add(1, 0, -1, -1, -1, -1); + var thisArg = { tag: 'ctx' }; + var seenThis = null; + ctx.list.unsortedForEach(function () { seenThis = this; }, thisArg); + assert.strictEqual(seenThis, thisArg); +}); + +test('MappingList.toArray returns mappings sorted by generated position', () => { + var ctx = makeList(); + // Insert out of order — first add a mapping that's strictly after the + // sentinel, then one that violates it. _sorted should flip to false and + // toArray() should invoke _sort. + ctx.list.add(2, 5, -1, -1, -1, -1); + ctx.list.add(1, 0, -1, -1, -1, -1); + ctx.list.add(2, 0, -1, -1, -1, -1); + + var arr = ctx.list.toArray(); + assert.strictEqual(arr.length, 3); + assert.deepStrictEqual(arr.map(function (m) { + return [m.generatedLine, m.generatedColumn]; + }), [[1, 0], [2, 0], [2, 5]]); +}); + +test('MappingList.toArray on an already-sorted list does not re-sort', () => { + var ctx = makeList(); + ctx.list.add(1, 0, -1, -1, -1, -1); + ctx.list.add(1, 5, -1, -1, -1, -1); + ctx.list.add(2, 0, -1, -1, -1, -1); + + // _sorted invariant: stays true when all adds came in strictly-after order. + assert.strictEqual(ctx.list._sorted, true); + + var arr = ctx.list.toArray(); + assert.deepStrictEqual(arr.map(function (m) { + return [m.generatedLine, m.generatedColumn]; + }), [[1, 0], [1, 5], [2, 0]]); +}); + +// The sortedness check in MappingList.add is a 6-level cascade +// (genLine → genCol → srcIdx → origLine → origCol → nameIdx). Each +// non-tied level returns a boolean directly; only on full equality does +// the chain fall through to the `>= lastNameIdx` final compare. The tests +// below exercise both "after" and "before" outcomes at every level so +// branch coverage matches the runtime decision tree. + +test('MappingList.add flips _sorted at the genLine level', () => { + var ctx = makeList(); + ctx.list.add(2, 0, -1, -1, -1, -1); + ctx.list.add(1, 0, -1, -1, -1, -1); // genLine < lastGenLine + assert.strictEqual(ctx.list._sorted, false); +}); + +test('MappingList.add flips _sorted at the genCol level', () => { + var ctx = makeList(); + ctx.list.add(1, 5, -1, -1, -1, -1); + ctx.list.add(1, 0, -1, -1, -1, -1); // same genLine, genCol < + assert.strictEqual(ctx.list._sorted, false); +}); + +test('MappingList.add flips _sorted at the srcIdx level', () => { + var ctx = makeList(); + ctx.list.add(1, 0, 0, 0, 1, -1); + ctx.list.add(1, 0, 0, 0, 0, -1); // same genPos, srcIdx < + assert.strictEqual(ctx.list._sorted, false); +}); + +test('MappingList.add flips _sorted at the origLine level', () => { + var ctx = makeList(); + ctx.list.add(1, 0, 5, 0, 0, -1); + ctx.list.add(1, 0, 3, 0, 0, -1); // same genPos+srcIdx, origLine < + assert.strictEqual(ctx.list._sorted, false); +}); + +test('MappingList.add flips _sorted at the origCol level', () => { + var ctx = makeList(); + ctx.list.add(1, 0, 5, 7, 0, -1); + ctx.list.add(1, 0, 5, 3, 0, -1); // same up through origLine, origCol < + assert.strictEqual(ctx.list._sorted, false); +}); + +test('MappingList.add flips _sorted at the nameIdx level', () => { + var ctx = makeList(); + ctx.list.add(1, 0, 5, 7, 0, 1); + // Same everything through origCol, smaller nameIdx — final branch uses + // strict `>=` so equal would still count as after; this exercises strict + // <-than-last path that flips _sorted false. + ctx.list.add(1, 0, 5, 7, 0, 0); + assert.strictEqual(ctx.list._sorted, false); +}); + +test('MappingList.add keeps _sorted true when fully equal mapping is added (>= tie)', () => { + var ctx = makeList(); + ctx.list.add(1, 0, 5, 7, 0, 0); + ctx.list.add(1, 0, 5, 7, 0, 0); // exact duplicate — counts as "after" + assert.strictEqual(ctx.list._sorted, true); +}); + +// Each cascade level also has an "after = true at this level" branch that +// fires when the level's field is strictly greater than the previous mapping's. +test('MappingList.add: level genCol after-true (same genLine, greater genCol)', () => { + var ctx = makeList(); + ctx.list.add(1, 0, -1, -1, -1, -1); + ctx.list.add(1, 5, -1, -1, -1, -1); + assert.strictEqual(ctx.list._sorted, true); +}); + +test('MappingList.add: level srcIdx after-true', () => { + var ctx = makeList(); + ctx.list.add(1, 0, 0, 0, 0, -1); + ctx.list.add(1, 0, 0, 0, 1, -1); + assert.strictEqual(ctx.list._sorted, true); +}); + +test('MappingList.add: level origLine after-true', () => { + var ctx = makeList(); + ctx.list.add(1, 0, 3, 0, 0, -1); + ctx.list.add(1, 0, 5, 0, 0, -1); + assert.strictEqual(ctx.list._sorted, true); +}); + +test('MappingList.add: level origCol after-true', () => { + var ctx = makeList(); + ctx.list.add(1, 0, 5, 3, 0, -1); + ctx.list.add(1, 0, 5, 7, 0, -1); + assert.strictEqual(ctx.list._sorted, true); +}); + +test('MappingList.add: level nameIdx after-true', () => { + var ctx = makeList(); + ctx.list.add(1, 0, 5, 7, 0, 0); + ctx.list.add(1, 0, 5, 7, 0, 1); + assert.strictEqual(ctx.list._sorted, true); +}); + +// _sort's comparator has the same 6-level cascade. The straight toArray +// test only hits the F_GEN_LINE/F_GEN_COL branches. Add an out-of-order +// pair tied on gen position that forces the comparator to descend. +test('MappingList.toArray sorts through deeper tie-break levels', () => { + var ctx = makeList(); + // Two mappings same gen pos but different srcIdx, inserted out of order. + ctx.list.add(1, 0, 0, 0, 1, -1); + ctx.list.add(1, 0, 0, 0, 0, -1); + assert.strictEqual(ctx.list._sorted, false); + var arr = ctx.list.toArray(); + // Sort by srcIdx ascending — `a.js` (idx 0) comes before `b.js` (idx 1). + assert.deepStrictEqual(arr.map(function (m) { return m.source; }), ['a.js', 'b.js']); +}); + +// _sort's perm comparator has the same 6-level cascade as add's sortedness +// check. The toArray test above only exercises the genLine/genCol/srcIdx +// branches of that comparator. Each subsequent level needs an out-of-order +// pair tied on the levels above it. +test('MappingList.toArray sorts at the origLine comparator level', () => { + var ctx = makeList(); + ctx.list.add(1, 0, 5, 0, 0, -1); + ctx.list.add(1, 0, 3, 0, 0, -1); + var arr = ctx.list.toArray(); + assert.deepStrictEqual(arr.map(function (m) { return m.originalLine; }), [3, 5]); +}); + +test('MappingList.toArray sorts at the origCol comparator level', () => { + var ctx = makeList(); + ctx.list.add(1, 0, 5, 7, 0, -1); + ctx.list.add(1, 0, 5, 3, 0, -1); + var arr = ctx.list.toArray(); + assert.deepStrictEqual(arr.map(function (m) { return m.originalColumn; }), [3, 7]); +}); + +test('MappingList.toArray sorts at the nameIdx comparator level', () => { + var ctx = makeList(); + ctx.sources.add('a.js'); + ctx.names.add('bar'); // adds nameIdx=1 (after 'foo' at 0) + ctx.list.add(1, 0, 5, 7, 0, 1); + ctx.list.add(1, 0, 5, 7, 0, 0); + var arr = ctx.list.toArray(); + assert.deepStrictEqual(arr.map(function (m) { return m.name; }), ['foo', 'bar']); +}); + +// _equalsPrev is used by SourceMapGenerator._serializeMappings to skip +// emitting duplicate segments. Each level of the 6-field equality chain +// has a short-circuit path that needs coverage. +test('MappingList._equalsPrev returns true for an exact duplicate adjacent pair', () => { + var ctx = makeList(); + ctx.list.add(1, 0, 5, 7, 0, 0); + ctx.list.add(1, 0, 5, 7, 0, 0); + assert.strictEqual(ctx.list._equalsPrev(1), true); +}); + +test('MappingList._equalsPrev short-circuits at each cascade level', () => { + // Helper that builds a fresh list, adds a base mapping plus a mutated + // sibling differing only at the field we want to exercise. The + // assertion is that _equalsPrev returns false because that field + // differs — covering the short-circuit at that level. + function diff(base, mut) { + var ctx = makeList(); + ctx.list.add.apply(ctx.list, base); + ctx.list.add.apply(ctx.list, mut); + return ctx.list._equalsPrev(1); + } + assert.strictEqual(diff([1, 0, 5, 7, 0, 0], [2, 0, 5, 7, 0, 0]), false); // genLine + assert.strictEqual(diff([1, 0, 5, 7, 0, 0], [1, 1, 5, 7, 0, 0]), false); // genCol + assert.strictEqual(diff([1, 0, 5, 7, 0, 0], [1, 0, 5, 7, 1, 0]), false); // srcIdx + assert.strictEqual(diff([1, 0, 5, 7, 0, 0], [1, 0, 6, 7, 0, 0]), false); // origLine + assert.strictEqual(diff([1, 0, 5, 7, 0, 0], [1, 0, 5, 8, 0, 0]), false); // origCol + assert.strictEqual(diff([1, 0, 5, 7, 0, 0], [1, 0, 5, 7, 0, 1]), false); // nameIdx +}); + +test('MappingList grows the slab beyond initial capacity', () => { + var ctx = makeList(); + // INITIAL_CAPACITY is 16 — push past the boundary to exercise _grow. + for (var i = 0; i < 40; i++) { + ctx.list.add(1 + i, 0, -1, -1, -1, -1); + } + assert.strictEqual(ctx.list._count, 40); + var arr = ctx.list.toArray(); + assert.strictEqual(arr.length, 40); + assert.strictEqual(arr[0].generatedLine, 1); + assert.strictEqual(arr[39].generatedLine, 40); +}); diff --git a/test/internal/test-source-map-consumer-internals.js b/test/internal/test-source-map-consumer-internals.js index 3e93a5db..7e12ef69 100644 --- a/test/internal/test-source-map-consumer-internals.js +++ b/test/internal/test-source-map-consumer-internals.js @@ -170,6 +170,69 @@ test('IndexedSourceMapConsumer sorts per-source originalMappings when bucket is assert.deepStrictEqual(lines, [6, 11]); }); +// BasicSourceMapConsumer.fromSourceMap reads the generator's MappingList +// slab directly. The srcIdx === -1 branch (source-less generated mapping) +// isn't covered by the standard fromSourceMap tests since they all add +// mappings with sources. Exercise it here to pin the slab read shape for +// the source-less case. +// applySourceMap walks the generator's MappingList slab. The +// `srcIdx === -1` branch on line 253 (source-less mapping going through +// applySourceMap) was uncovered — all existing applySourceMap tests use +// fully-sourced mappings. Pin the pass-through behavior here. +test('SourceMapGenerator.applySourceMap passes source-less mappings through unchanged', () => { + // Inner map: x.js → y.js + var inner = new SourceMapGenerator({ file: 'x.js' }); + inner.addMapping({ + source: 'y.js', + original: { line: 1, column: 0 }, + generated: { line: 1, column: 0 } + }); + var innerConsumer = new SourceMapConsumer(inner.toJSON()); + + // Outer generator has a source-less mapping alongside an x.js mapping. + var outer = new SourceMapGenerator({ file: 'foo.js' }); + outer.addMapping({ generated: { line: 1, column: 0 } }); // source-less + outer.addMapping({ + source: 'x.js', + original: { line: 1, column: 0 }, + generated: { line: 1, column: 5 } + }); + + outer.applySourceMap(innerConsumer); + + var result = new SourceMapConsumer(outer.toJSON()); + var seen = []; + result.eachMapping(function (m) { + seen.push({ gc: m.generatedColumn, src: m.source }); + }); + assert.deepStrictEqual(seen, [ + { gc: 0, src: null }, // source-less mapping survives unchanged + { gc: 5, src: 'y.js' } // x.js mapping got transformed via inner + ]); +}); + +test('BasicSourceMapConsumer.fromSourceMap handles source-less mappings', () => { + var smg = new SourceMapGenerator({ file: 'foo.js' }); + // Case-1 mapping per _validateMapping: only generated position. + smg.addMapping({ generated: { line: 1, column: 0 } }); + smg.addMapping({ + source: 'x.js', + original: { line: 1, column: 0 }, + generated: { line: 1, column: 5 } + }); + + var smc = SourceMapConsumer.fromSourceMap(smg); + // Source-less mapping survives the round-trip with source/name null. + var lines = []; + smc.eachMapping(function (m) { + lines.push({ gc: m.generatedColumn, src: m.source, name: m.name }); + }); + assert.deepStrictEqual(lines, [ + { gc: 0, src: null, name: null }, + { gc: 5, src: 'x.js', name: null } + ]); +}); + // BasicSourceMapConsumer.fromSourceMap buckets original-side mappings by // source and sorts each bucket with compareByOriginalPositionsNoSource. // MappingList stores mappings in generated-position order, so we need two diff --git a/test/internal/test-util.js b/test/internal/test-util.js index bdc0f38b..e285922e 100644 --- a/test/internal/test-util.js +++ b/test/internal/test-util.js @@ -362,6 +362,14 @@ test('compareByGeneratedPositionsInflated compares source, original, name', () = var cmp = libUtil.compareByGeneratedPositionsInflated; var base = mapping({}); + // Differ on generatedLine — primary key; integration tests previously + // hit this branch via MappingList.add, but the slab-backed rewrite + // doesn't call this comparator any more, so it's pinned directly here. + assert.ok(cmp(mapping({ generatedLine: 2 }), base) > 0); + + // Differ on generatedColumn — secondary key with the same rationale. + assert.ok(cmp(mapping({ generatedColumn: 1 }), base) > 0); + // Differ on source. assert.ok(cmp(mapping({ source: 'b.js' }), mapping({ source: 'a.js' })) > 0);