From fc2a76ad4d784f77eaef3becb941906e4d84aafb Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Sat, 24 Feb 2024 03:08:55 -0500 Subject: [PATCH] Improve DCE by code moving out of static block --- .eslintrc.js | 6 + src/by-source.ts | 5 +- src/trace-mapping.ts | 491 +++++++++++++++++++++---------------------- 3 files changed, 252 insertions(+), 250 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 29a0d66..f8c4211 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,6 +18,12 @@ module.exports = { 'no-constant-condition': 'off', 'no-unused-labels': 'off', 'prefer-rest-params': 'off', + 'prefer-const': [ + 'error', + { + destructuring: 'all', + }, + ], }, overrides: [ { diff --git a/src/by-source.ts b/src/by-source.ts index b4794f6..2af1cf0 100644 --- a/src/by-source.ts +++ b/src/by-source.ts @@ -34,13 +34,14 @@ export default function buildBySources( // segment should go. Either way, we want to insert after that. And there may be multiple // generated segments associated with an original location, so there may need to move several // indexes before we find where we need to insert. - const index = upperBound( + let index = upperBound( originalLine, sourceColumn, memoizedBinarySearch(originalLine, sourceColumn, memo, sourceLine), ); - insert(originalLine, (memo.lastIndex = index + 1), [sourceColumn, i, seg[COLUMN]]); + memo.lastIndex = ++index; + insert(originalLine, index, [sourceColumn, i, seg[COLUMN]]); } } diff --git a/src/trace-mapping.ts b/src/trace-mapping.ts index 780ea1b..157f299 100644 --- a/src/trace-mapping.ts +++ b/src/trace-mapping.ts @@ -61,317 +61,260 @@ export type { SourceNeedle, } from './types'; +interface PublicMap { + _encoded: TraceMap['_encoded']; + _decoded: TraceMap['_decoded']; + _decodedMemo: TraceMap['_decodedMemo']; + _bySources: TraceMap['_bySources']; + _bySourceMemos: TraceMap['_bySourceMemos']; +} + const LINE_GTR_ZERO = '`line` must be greater than 0 (lines start at line 1)'; const COL_GTR_EQ_ZERO = '`column` must be greater than or equal to 0 (columns start at column 0)'; export const LEAST_UPPER_BOUND = -1; export const GREATEST_LOWER_BOUND = 1; +export { AnyMap } from './any-map'; + +export class TraceMap implements SourceMap { + declare version: SourceMapV3['version']; + declare file: SourceMapV3['file']; + declare names: SourceMapV3['names']; + declare sourceRoot: SourceMapV3['sourceRoot']; + declare sources: SourceMapV3['sources']; + declare sourcesContent: SourceMapV3['sourcesContent']; + + declare resolvedSources: string[]; + private declare _encoded: string | undefined; + + private declare _decoded: SourceMapSegment[][] | undefined; + private declare _decodedMemo: MemoState; + + private declare _bySources: Source[] | undefined; + private declare _bySourceMemos: MemoState[] | undefined; + + constructor(map: SourceMapInput, mapUrl?: string | null) { + const isString = typeof map === 'string'; + + if (!isString && (map as unknown as { _decodedMemo: any })._decodedMemo) return map as TraceMap; + + const parsed = (isString ? JSON.parse(map) : map) as DecodedSourceMap | EncodedSourceMap; + + const { version, file, names, sourceRoot, sources, sourcesContent } = parsed; + this.version = version; + this.file = file; + this.names = names || []; + this.sourceRoot = sourceRoot; + this.sources = sources; + this.sourcesContent = sourcesContent; + + const from = resolve(sourceRoot || '', stripFilename(mapUrl)); + this.resolvedSources = sources.map((s) => resolve(s || '', from)); + + const { mappings } = parsed; + if (typeof mappings === 'string') { + this._encoded = mappings; + this._decoded = undefined; + } else { + this._encoded = undefined; + this._decoded = maybeSort(mappings, isString); + } + + this._decodedMemo = memoizedState(); + this._bySources = undefined; + this._bySourceMemos = undefined; + } +} + +/** + * Typescript doesn't allow friend access to private fields, so this just casts the map into a type + * with public access modifiers. + */ +function cast(map: unknown): PublicMap { + return map as any; +} + /** * Returns the encoded (VLQ string) form of the SourceMap's mappings field. */ -export let encodedMappings: (map: TraceMap) => EncodedSourceMap['mappings']; +export function encodedMappings(map: TraceMap): EncodedSourceMap['mappings'] { + return (cast(map)._encoded ??= encode(cast(map)._decoded!)); +} /** * Returns the decoded (array of lines of segments) form of the SourceMap's mappings field. */ -export let decodedMappings: (map: TraceMap) => Readonly; +export function decodedMappings(map: TraceMap): Readonly { + return (cast(map)._decoded ||= decode(cast(map)._encoded!)); +} /** * A low-level API to find the segment associated with a generated line/column (think, from a * stack trace). Line and column here are 0-based, unlike `originalPositionFor`. */ -export let traceSegment: ( +export function traceSegment( map: TraceMap, line: number, column: number, -) => Readonly | null; +): Readonly | null { + const decoded = decodedMappings(map); + + // It's common for parent source maps to have pointers to lines that have no + // mapping (like a "//# sourceMappingURL=") at the end of the child file. + if (line >= decoded.length) return null; + + const segments = decoded[line]; + const index = traceSegmentInternal( + segments, + cast(map)._decodedMemo, + line, + column, + GREATEST_LOWER_BOUND, + ); + + return index === -1 ? null : segments[index]; +} /** * A higher-level API to find the source/line/column associated with a generated line/column * (think, from a stack trace). Line is 1-based, but column is 0-based, due to legacy behavior in * `source-map` library. */ -export let originalPositionFor: ( +export function originalPositionFor( map: TraceMap, needle: Needle, -) => OriginalMapping | InvalidOriginalMapping; +): OriginalMapping | InvalidOriginalMapping { + let { line, column, bias } = needle; + line--; + if (line < 0) throw new Error(LINE_GTR_ZERO); + if (column < 0) throw new Error(COL_GTR_EQ_ZERO); + + const decoded = decodedMappings(map); + + // It's common for parent source maps to have pointers to lines that have no + // mapping (like a "//# sourceMappingURL=") at the end of the child file. + if (line >= decoded.length) return OMapping(null, null, null, null); + + const segments = decoded[line]; + const index = traceSegmentInternal( + segments, + cast(map)._decodedMemo, + line, + column, + bias || GREATEST_LOWER_BOUND, + ); + + if (index === -1) return OMapping(null, null, null, null); + + const segment = segments[index]; + if (segment.length === 1) return OMapping(null, null, null, null); + + const { names, resolvedSources } = map; + return OMapping( + resolvedSources[segment[SOURCES_INDEX]], + segment[SOURCE_LINE] + 1, + segment[SOURCE_COLUMN], + segment.length === 5 ? names[segment[NAMES_INDEX]] : null, + ); +} /** * Finds the generated line/column position of the provided source/line/column source position. */ -export let generatedPositionFor: ( +export function generatedPositionFor( map: TraceMap, needle: SourceNeedle, -) => GeneratedMapping | InvalidGeneratedMapping; +): GeneratedMapping | InvalidGeneratedMapping { + const { source, line, column, bias } = needle; + return generatedPosition(map, source, line, column, bias || GREATEST_LOWER_BOUND, false); +} /** * Finds all generated line/column positions of the provided source/line/column source position. */ -export let allGeneratedPositionsFor: (map: TraceMap, needle: SourceNeedle) => GeneratedMapping[]; +export function allGeneratedPositionsFor(map: TraceMap, needle: SourceNeedle): GeneratedMapping[] { + const { source, line, column, bias } = needle; + // SourceMapConsumer uses LEAST_UPPER_BOUND for some reason, so we follow suit. + return generatedPosition(map, source, line, column, bias || LEAST_UPPER_BOUND, true); +} /** * Iterates each mapping in generated position order. */ -export let eachMapping: (map: TraceMap, cb: (mapping: EachMapping) => void) => void; +export function eachMapping(map: TraceMap, cb: (mapping: EachMapping) => void): void { + const decoded = decodedMappings(map); + const { names, resolvedSources } = map; + + for (let i = 0; i < decoded.length; i++) { + const line = decoded[i]; + for (let j = 0; j < line.length; j++) { + const seg = line[j]; + + const generatedLine = i + 1; + const generatedColumn = seg[0]; + let source = null; + let originalLine = null; + let originalColumn = null; + let name = null; + if (seg.length !== 1) { + source = resolvedSources[seg[1]]; + originalLine = seg[2] + 1; + originalColumn = seg[3]; + } + if (seg.length === 5) name = names[seg[4]]; + + cb({ + generatedLine, + generatedColumn, + source, + originalLine, + originalColumn, + name, + } as EachMapping); + } + } +} /** * Retrieves the source content for a particular source, if its found. Returns null if not. */ -export let sourceContentFor: (map: TraceMap, source: string) => string | null; +export function sourceContentFor(map: TraceMap, source: string): string | null { + const { sources, resolvedSources, sourcesContent } = map; + if (sourcesContent == null) return null; + + let index = sources.indexOf(source); + if (index === -1) index = resolvedSources.indexOf(source); + + return index === -1 ? null : sourcesContent[index]; +} /** * A helper that skips sorting of the input map's mappings array, which can be expensive for larger * maps. */ -export let presortedDecodedMap: (map: DecodedSourceMap, mapUrl?: string) => TraceMap; +export function presortedDecodedMap(map: DecodedSourceMap, mapUrl?: string): TraceMap { + const tracer = new TraceMap(clone(map, []), mapUrl); + cast(tracer)._decoded = map.mappings; + return tracer; +} /** * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects * a sourcemap, or to JSON.stringify. */ -export let decodedMap: ( +export function decodedMap( map: TraceMap, -) => Omit & { mappings: readonly SourceMapSegment[][] }; +): Omit & { mappings: readonly SourceMapSegment[][] } { + return clone(map, decodedMappings(map)); +} /** * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects * a sourcemap, or to JSON.stringify. */ -export let encodedMap: (map: TraceMap) => EncodedSourceMap; - -export { AnyMap } from './any-map'; - -export class TraceMap implements SourceMap { - declare version: SourceMapV3['version']; - declare file: SourceMapV3['file']; - declare names: SourceMapV3['names']; - declare sourceRoot: SourceMapV3['sourceRoot']; - declare sources: SourceMapV3['sources']; - declare sourcesContent: SourceMapV3['sourcesContent']; - - declare resolvedSources: string[]; - private declare _encoded: string | undefined; - - private declare _decoded: SourceMapSegment[][] | undefined; - private declare _decodedMemo: MemoState; - - private declare _bySources: Source[] | undefined; - private declare _bySourceMemos: MemoState[] | undefined; - - constructor(map: SourceMapInput, mapUrl?: string | null) { - const isString = typeof map === 'string'; - - if (!isString && (map as unknown as { _decodedMemo: any })._decodedMemo) return map as TraceMap; - - const parsed = (isString ? JSON.parse(map) : map) as DecodedSourceMap | EncodedSourceMap; - - const { version, file, names, sourceRoot, sources, sourcesContent } = parsed; - this.version = version; - this.file = file; - this.names = names || []; - this.sourceRoot = sourceRoot; - this.sources = sources; - this.sourcesContent = sourcesContent; - - const from = resolve(sourceRoot || '', stripFilename(mapUrl)); - this.resolvedSources = sources.map((s) => resolve(s || '', from)); - - const { mappings } = parsed; - if (typeof mappings === 'string') { - this._encoded = mappings; - this._decoded = undefined; - } else { - this._encoded = undefined; - this._decoded = maybeSort(mappings, isString); - } - - this._decodedMemo = memoizedState(); - this._bySources = undefined; - this._bySourceMemos = undefined; - } - - static { - encodedMappings = (map) => { - return (map._encoded ??= encode(map._decoded!)); - }; - - decodedMappings = (map) => { - return (map._decoded ||= decode(map._encoded!)); - }; - - traceSegment = (map, line, column) => { - const decoded = decodedMappings(map); - - // It's common for parent source maps to have pointers to lines that have no - // mapping (like a "//# sourceMappingURL=") at the end of the child file. - if (line >= decoded.length) return null; - - const segments = decoded[line]; - const index = traceSegmentInternal( - segments, - map._decodedMemo, - line, - column, - GREATEST_LOWER_BOUND, - ); - - return index === -1 ? null : segments[index]; - }; - - originalPositionFor = (map, { line, column, bias }) => { - line--; - if (line < 0) throw new Error(LINE_GTR_ZERO); - if (column < 0) throw new Error(COL_GTR_EQ_ZERO); - - const decoded = decodedMappings(map); - - // It's common for parent source maps to have pointers to lines that have no - // mapping (like a "//# sourceMappingURL=") at the end of the child file. - if (line >= decoded.length) return OMapping(null, null, null, null); - - const segments = decoded[line]; - const index = traceSegmentInternal( - segments, - map._decodedMemo, - line, - column, - bias || GREATEST_LOWER_BOUND, - ); - - if (index === -1) return OMapping(null, null, null, null); - - const segment = segments[index]; - if (segment.length === 1) return OMapping(null, null, null, null); - - const { names, resolvedSources } = map; - return OMapping( - resolvedSources[segment[SOURCES_INDEX]], - segment[SOURCE_LINE] + 1, - segment[SOURCE_COLUMN], - segment.length === 5 ? names[segment[NAMES_INDEX]] : null, - ); - }; - - allGeneratedPositionsFor = (map, { source, line, column, bias }) => { - // SourceMapConsumer uses LEAST_UPPER_BOUND for some reason, so we follow suit. - return generatedPosition(map, source, line, column, bias || LEAST_UPPER_BOUND, true); - }; - - generatedPositionFor = (map, { source, line, column, bias }) => { - return generatedPosition(map, source, line, column, bias || GREATEST_LOWER_BOUND, false); - }; - - eachMapping = (map, cb) => { - const decoded = decodedMappings(map); - const { names, resolvedSources } = map; - - for (let i = 0; i < decoded.length; i++) { - const line = decoded[i]; - for (let j = 0; j < line.length; j++) { - const seg = line[j]; - - const generatedLine = i + 1; - const generatedColumn = seg[0]; - let source = null; - let originalLine = null; - let originalColumn = null; - let name = null; - if (seg.length !== 1) { - source = resolvedSources[seg[1]]; - originalLine = seg[2] + 1; - originalColumn = seg[3]; - } - if (seg.length === 5) name = names[seg[4]]; - - cb({ - generatedLine, - generatedColumn, - source, - originalLine, - originalColumn, - name, - } as EachMapping); - } - } - }; - - sourceContentFor = (map, source) => { - const { sources, resolvedSources, sourcesContent } = map; - if (sourcesContent == null) return null; - - let index = sources.indexOf(source); - if (index === -1) index = resolvedSources.indexOf(source); - - return index === -1 ? null : sourcesContent[index]; - }; - - presortedDecodedMap = (map, mapUrl) => { - const tracer = new TraceMap(clone(map, []), mapUrl); - tracer._decoded = map.mappings; - return tracer; - }; - - decodedMap = (map) => { - return clone(map, decodedMappings(map)); - }; - - encodedMap = (map) => { - return clone(map, encodedMappings(map)); - }; - - function generatedPosition( - map: TraceMap, - source: string, - line: number, - column: number, - bias: Bias, - all: false, - ): GeneratedMapping | InvalidGeneratedMapping; - function generatedPosition( - map: TraceMap, - source: string, - line: number, - column: number, - bias: Bias, - all: true, - ): GeneratedMapping[]; - function generatedPosition( - map: TraceMap, - source: string, - line: number, - column: number, - bias: Bias, - all: boolean, - ): GeneratedMapping | InvalidGeneratedMapping | GeneratedMapping[] { - line--; - if (line < 0) throw new Error(LINE_GTR_ZERO); - if (column < 0) throw new Error(COL_GTR_EQ_ZERO); - - const { sources, resolvedSources } = map; - let sourceIndex = sources.indexOf(source); - if (sourceIndex === -1) sourceIndex = resolvedSources.indexOf(source); - if (sourceIndex === -1) return all ? [] : GMapping(null, null); - - const generated = (map._bySources ||= buildBySources( - decodedMappings(map), - (map._bySourceMemos = sources.map(memoizedState)), - )); - - const segments = generated[sourceIndex][line]; - if (segments == null) return all ? [] : GMapping(null, null); - - const memo = map._bySourceMemos![sourceIndex]; - - if (all) return sliceGeneratedPositions(segments, memo, line, column, bias); - - const index = traceSegmentInternal(segments, memo, line, column, bias); - if (index === -1) return GMapping(null, null); - - const segment = segments[index]; - return GMapping(segment[REV_GENERATED_LINE] + 1, segment[REV_GENERATED_COLUMN]); - } - } +export function encodedMap(map: TraceMap): EncodedSourceMap { + return clone(map, encodedMappings(map)); } function clone( @@ -479,3 +422,55 @@ function sliceGeneratedPositions( } return result; } + +function generatedPosition( + map: TraceMap, + source: string, + line: number, + column: number, + bias: Bias, + all: false, +): GeneratedMapping | InvalidGeneratedMapping; +function generatedPosition( + map: TraceMap, + source: string, + line: number, + column: number, + bias: Bias, + all: true, +): GeneratedMapping[]; +function generatedPosition( + map: TraceMap, + source: string, + line: number, + column: number, + bias: Bias, + all: boolean, +): GeneratedMapping | InvalidGeneratedMapping | GeneratedMapping[] { + line--; + if (line < 0) throw new Error(LINE_GTR_ZERO); + if (column < 0) throw new Error(COL_GTR_EQ_ZERO); + + const { sources, resolvedSources } = map; + let sourceIndex = sources.indexOf(source); + if (sourceIndex === -1) sourceIndex = resolvedSources.indexOf(source); + if (sourceIndex === -1) return all ? [] : GMapping(null, null); + + const generated = (cast(map)._bySources ||= buildBySources( + decodedMappings(map), + (cast(map)._bySourceMemos = sources.map(memoizedState)), + )); + + const segments = generated[sourceIndex][line]; + if (segments == null) return all ? [] : GMapping(null, null); + + const memo = cast(map)._bySourceMemos![sourceIndex]; + + if (all) return sliceGeneratedPositions(segments, memo, line, column, bias); + + const index = traceSegmentInternal(segments, memo, line, column, bias); + if (index === -1) return GMapping(null, null); + + const segment = segments[index]; + return GMapping(segment[REV_GENERATED_LINE] + 1, segment[REV_GENERATED_COLUMN]); +}