Skip to content

Commit a205dca

Browse files
committed
trace-mapping: add operations needed for remapping
* Adjust rangeSegments to map to range mapping info such as the end point, not just a boolean state of being a range or not. * Add isRange to allow querying the range mapping data for a segment * Add traceSegmentsInRange for finding segments in a range
1 parent 36c640f commit a205dca

File tree

4 files changed

+146
-9
lines changed

4 files changed

+146
-9
lines changed

packages/trace-mapping/src/by-source.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export type Source = {
1515
export default function buildBySources(
1616
decoded: readonly SourceMapSegment[][],
1717
memos: unknown[],
18-
rangeSegments: Set<SourceMapSegment>,
18+
rangeSegments: Map<SourceMapSegment, any>,
1919
): Source[] {
2020
const sources: Source[] = memos.map(() => ({ lines: [], rangeSegments: new Set() }));
2121

packages/trace-mapping/src/trace-mapping.ts

Lines changed: 119 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { encode, decode, encodeRangeMappings, decodeRangeMappings } from '@jridgewell/sourcemap-codec';
1+
import {
2+
encode,
3+
decode,
4+
encodeRangeMappings,
5+
decodeRangeMappings,
6+
} from '@jridgewell/sourcemap-codec';
27

38
import resolver from './resolve';
49
import maybeSort from './sort';
@@ -39,6 +44,7 @@ import type {
3944
XInput,
4045
SectionedSourceMap,
4146
Ro,
47+
RangeInfo,
4248
} from './types';
4349
import type { Source } from './by-source';
4450
import type { MemoState } from './binary-search';
@@ -156,9 +162,9 @@ export class TraceMap implements SourceMap {
156162
declare private _decodedRangeMappings: number[][] | undefined;
157163

158164
/**
159-
* A set of segments that are range mappings.
165+
* A map of segments that are range mappings to info about the range.
160166
*/
161-
declare private _rangeSegments: Set<SourceMapSegment> | undefined;
167+
declare private _rangeSegments: Map<SourceMapSegment, RangeInfo> | undefined;
162168

163169
constructor(map: Ro<SourceMapInput>, mapUrl?: string | null) {
164170
const isString = typeof map === 'string';
@@ -272,6 +278,77 @@ export function traceSegment(
272278
);
273279
}
274280

281+
/**
282+
* Find all the segments that are in a given range. Used to find
283+
* segments that are relevant for composing a range mapping with
284+
* another range.
285+
*/
286+
export function traceSegmentsInRange(
287+
map: TraceMap,
288+
startLine: number,
289+
startColumn: number,
290+
endLine: number,
291+
endColumn: number,
292+
): Readonly<SourceMapSegment>[] {
293+
const lines = decodedMappings(map);
294+
const segments = startLine < lines.length ? lines[startLine] : [];
295+
const memo = cast(map)._decodedMemo;
296+
297+
if (startLine >= lines.length || endLine >= lines.length) return [];
298+
299+
const segmentsInRange = [];
300+
301+
let startIndex = memoizedBinarySearchSegments(segments, startColumn, memo, startLine);
302+
if (bsFound) {
303+
startIndex = lowerBound(segments, startColumn, startIndex);
304+
} else {
305+
startIndex++;
306+
}
307+
if (startIndex === segments.length) return [];
308+
309+
const range = (start: number, end: number) => {
310+
return Array.from({ length: end - start + 1 }, (_, i: number) => i + start);
311+
};
312+
const gen = (line: number) => (index: number) => {
313+
return lines[line][index];
314+
};
315+
316+
function previousPosition(line: number, column: number) {
317+
if (column === 0) {
318+
line--;
319+
column = Infinity;
320+
} else {
321+
column--;
322+
}
323+
324+
return { line, column };
325+
}
326+
327+
if (startLine == endLine) {
328+
// We need to use the position decremented by one position because we
329+
// want the index of a true lower bound, not an equal position. That is,
330+
// the end point of the range should be exclusive and not inclusive.
331+
const { line, column } = previousPosition(endLine, endColumn);
332+
let endIndex = memoizedBinarySearchSegments(segments, column, memo, line);
333+
if (bsFound) {
334+
endIndex = upperBound(segments, column, endIndex);
335+
}
336+
segmentsInRange.push(...range(startIndex, endIndex).map(gen(startLine)));
337+
} else {
338+
segmentsInRange.push(...range(startIndex, segments.length - 1).map(gen(startLine)));
339+
for (let i = startLine + 1; i < endLine; i++) {
340+
segmentsInRange.push(...range(0, lines[i].length - 1).map(gen(i)));
341+
}
342+
let endIndex = memoizedBinarySearchSegments(lines[endLine], endColumn, memo, endLine);
343+
if (bsFound) {
344+
endIndex = upperBound(lines[endLine], endColumn, endIndex);
345+
}
346+
segmentsInRange.push(...range(0, endIndex).map(gen(endLine)));
347+
}
348+
349+
return segmentsInRange;
350+
}
351+
275352
/**
276353
* A higher-level API to find the source/line/column associated with a generated line/column
277354
* (think, from a stack trace). Line is 1-based, but column is 0-based, due to legacy behavior in
@@ -397,6 +474,16 @@ export function isIgnored(map: TraceMap, source: string): boolean {
397474
return index === -1 ? false : ignoreList.includes(index);
398475
}
399476

477+
/**
478+
* Determines if a segment is for a range mapping, and if so returns
479+
* some metadata about the range.
480+
*/
481+
export function isRange(map: TraceMap, segment: Readonly<SourceMapSegment>): RangeInfo | false {
482+
const decoded = decodedMappings(map);
483+
const rangeSegments = initRangeSegments(map, decoded);
484+
return rangeSegments.get(segment as SourceMapSegment) || false;
485+
}
486+
400487
/**
401488
* A helper that skips sorting of the input map's mappings array, which can be expensive for larger
402489
* maps.
@@ -485,23 +572,23 @@ function GMapping(
485572
*/
486573
function traceSegmentInternal<T extends SourceMapSegment | ReverseSegment>(
487574
lines: readonly T[][],
488-
rangeSegments: Set<T>,
575+
rangeSegments: Set<T> | Map<T, RangeInfo>,
489576
memo: MemoState,
490577
line: number,
491578
column: number,
492579
bias: Bias,
493580
): T | null;
494581
function traceSegmentInternal<T extends SourceMapSegment | ReverseSegment>(
495582
lines: readonly T[][],
496-
rangeSegments: Set<T>,
583+
rangeSegments: Set<T> | Map<T, RangeInfo>,
497584
memo: MemoState,
498585
line: number,
499586
column: number,
500587
bias: Bias,
501588
): T | null;
502589
function traceSegmentInternal<T extends SourceMapSegment | ReverseSegment>(
503590
lines: readonly T[][],
504-
rangeSegments: Set<T>,
591+
rangeSegments: Set<T> | Map<T, RangeInfo>,
505592
memo: MemoState,
506593
line: number,
507594
column: number,
@@ -700,7 +787,7 @@ function initRangeSegments(map: TraceMap, decoded: readonly SourceMapSegment[][]
700787
const existing = cast(map)._rangeSegments;
701788
if (existing != null) return existing;
702789

703-
const set = new Set<SourceMapSegment>();
790+
const set = new Map<SourceMapSegment, RangeInfo>();
704791
cast(map)._rangeSegments = set;
705792
const rangeMappings = decodedRangeMappings(map);
706793
if (rangeMappings == null) return set;
@@ -710,13 +797,37 @@ function initRangeSegments(map: TraceMap, decoded: readonly SourceMapSegment[][]
710797
const ranges = rangeMappings[i];
711798
for (let j = 0; j < ranges.length; j++) {
712799
const seg = line[ranges[j]];
713-
set.add(seg);
800+
const { line: endLine, index: endIndex } = findNextSegment(decoded, i, ranges[j]);
801+
const endSegment = endLine && endIndex ? decoded[endLine][endIndex] : null;
802+
set.set(seg, { line: i, endLine, endSegment });
714803
}
715804
}
716805

717806
return set;
718807
}
719808

809+
/**
810+
* Find the index of the next segment in this line or in subsequent lines.
811+
* Return null if there was no next segment.
812+
*/
813+
function findNextSegment<T extends SourceMapSegment>(
814+
lines: readonly T[][],
815+
line: number,
816+
index: number,
817+
): { line: number | null; index: number | null } {
818+
if (index + 1 < lines[line].length) {
819+
return { line, index: index + 1 };
820+
} else {
821+
for (let i = line + 1; i < lines.length; i++) {
822+
for (let j = 0; j < lines[i].length; j++) {
823+
return { line: i, index: j };
824+
}
825+
}
826+
}
827+
828+
return { line: null, index: null };
829+
}
830+
720831
/**
721832
* If we didn't find a match on this line, back searches to find the previous
722833
* line that has a segment.

packages/trace-mapping/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ export type EachMapping =
9191
name: string | null;
9292
};
9393

94+
export type RangeInfo = {
95+
line: number;
96+
endLine: number | null;
97+
endSegment: Readonly<SourceMapSegment> | null;
98+
};
99+
94100
export abstract class SourceMap {
95101
declare version: SourceMapV3['version'];
96102
declare file: SourceMapV3['file'];

packages/trace-mapping/test/trace-mapping.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
encodedMappings,
99
decodedMappings,
1010
traceSegment,
11+
traceSegmentsInRange,
1112
originalPositionFor,
1213
generatedPositionFor,
1314
presortedDecodedMap,
@@ -446,6 +447,25 @@ describe('TraceMap', () => {
446447
[{ line: 1, column: 13 }],
447448
);
448449
});
450+
describe('rangeMappings operations', () => {
451+
// 2nd line, 5th mapping is a range mapping
452+
const mapWithRangeMappings = replaceField(map, 'rangeMappings', ';F;');
453+
454+
it('traceSegmentsInRange', () => {
455+
const tracer = new TraceMap(mapWithRangeMappings);
456+
assert.deepEqual(traceSegmentsInRange(tracer, 0, 9, 0, 16), [
457+
[9, 0, 0, 9, 0],
458+
[12, 0, 0, 0],
459+
[13, 0, 0, 13, 1],
460+
]);
461+
assert.deepEqual(traceSegmentsInRange(tracer, 0, 16, 1, 9), [
462+
[16, 0, 0, 0],
463+
[18, 0, 0, 33],
464+
[4, 0, 1, 4],
465+
[8, 0, 1, 10],
466+
]);
467+
});
468+
});
449469
};
450470
}
451471

0 commit comments

Comments
 (0)