Skip to content

Commit 62cdf45

Browse files
committed
remapping: support range mapping composition
1 parent a205dca commit 62cdf45

File tree

2 files changed

+451
-19
lines changed

2 files changed

+451
-19
lines changed

packages/remapping/src/source-map-tree.ts

Lines changed: 283 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
1-
import { GenMapping, maybeAddSegment, setIgnore, setSourceContent } from '@jridgewell/gen-mapping';
2-
import { traceSegment, decodedMappings } from '@jridgewell/trace-mapping';
1+
import {
2+
GenMapping,
3+
maybeAddSegment,
4+
setIgnore,
5+
setSourceContent,
6+
setRangeSegment,
7+
} from '@jridgewell/gen-mapping';
8+
import {
9+
traceSegment,
10+
traceSegmentsInRange,
11+
isRange,
12+
decodedMappings,
13+
decodedRangeMappings,
14+
} from '@jridgewell/trace-mapping';
315

416
import type { TraceMap } from '@jridgewell/trace-mapping';
517

@@ -10,6 +22,8 @@ export type SourceMapSegmentObject = {
1022
source: string;
1123
content: string | null;
1224
ignore: boolean;
25+
isRangeMapping: boolean;
26+
rangeMappingOffset: { line: number; column: number };
1327
};
1428

1529
export type OriginalSource = {
@@ -40,8 +54,19 @@ function SegmentObject(
4054
name: string,
4155
content: string | null,
4256
ignore: boolean,
57+
isRangeMapping?: boolean,
58+
rangeMappingOffset?: { line: number; column: number },
4359
): SourceMapSegmentObject {
44-
return { source, line, column, name, content, ignore };
60+
return {
61+
source,
62+
line,
63+
column,
64+
name,
65+
content,
66+
ignore,
67+
isRangeMapping: isRangeMapping || false,
68+
rangeMappingOffset: rangeMappingOffset || { line: 0, column: 0 },
69+
};
4570
}
4671

4772
function Source(
@@ -105,36 +130,113 @@ export function traceMappings(tree: MapSource): GenMapping {
105130
const { sources: rootSources, map } = tree;
106131
const rootNames = map.names;
107132
const rootMappings = decodedMappings(map);
133+
const rootRangeMappings = decodedRangeMappings(map) || [];
134+
135+
// Find the next segment either in the current line or in
136+
// the next line if we're at the end and there are further lines.
137+
function nextSegment(line: number, index: number) {
138+
let current = index + 1;
139+
140+
while (line < rootMappings.length) {
141+
if (current < rootMappings[line].length) {
142+
return { line, segment: rootMappings[line][current] };
143+
} else {
144+
line++;
145+
current = 0;
146+
}
147+
}
148+
149+
return null;
150+
}
108151

109152
for (let i = 0; i < rootMappings.length; i++) {
110153
const segments = rootMappings[i];
154+
const rangeMappings = rootRangeMappings[i] || [];
111155

112156
for (let j = 0; j < segments.length; j++) {
113157
const segment = segments[j];
158+
const isRangeMapping = rangeMappings.includes(j);
114159
const genCol = segment[0];
115-
let traced: SourceMapSegmentObject | null = SOURCELESS_MAPPING;
116160

117-
// 1-length segments only move the current generated column, there's no source information
118-
// to gather from it.
119-
if (segment.length !== 1) {
161+
if (segment.length === 1 || !isRangeMapping) {
162+
let tracedSegment: SourceMapSegmentObject | null = SOURCELESS_MAPPING;
163+
164+
// 1-length segments only move the current generated column, there's no source information
165+
// to gather from it.
166+
if (segment.length !== 1) {
167+
const source = rootSources[segment[1]];
168+
169+
tracedSegment = originalPositionFor(
170+
source,
171+
segment[2],
172+
segment[3],
173+
segment.length === 5 ? rootNames[segment[4]] : '',
174+
);
175+
176+
// If the trace is invalid, then the trace ran into a sourcemap that doesn't contain a
177+
// respective segment into an original source.
178+
if (tracedSegment === null) continue;
179+
}
180+
181+
const { column, line, name, content, source, ignore } = tracedSegment!;
182+
183+
maybeAddSegment(gen, i, genCol, source, line, column, name);
184+
if (source && content != null) setSourceContent(gen, source, content);
185+
if (ignore) setIgnore(gen, source, true);
186+
} else {
187+
// isRangeMapping
120188
const source = rootSources[segment[1]];
121-
traced = originalPositionFor(
189+
190+
// Find end segment, if none exists it's an invalid range mapping and
191+
// we will skip it.
192+
const next = nextSegment(i, j);
193+
if (next === null) continue;
194+
const { line: nextSegmentLine, segment: endSegment } = next;
195+
const rangeLineOffset = nextSegmentLine - i;
196+
const rangeColumnOffset = endSegment[0] - segment[0];
197+
const endLine = segment[2] + rangeLineOffset;
198+
const endColumn = segment[2] === endLine ? segment[3] + rangeColumnOffset : endSegment[0];
199+
200+
const tracedSegments = originalPositionsForRange(
122201
source,
123202
segment[2],
124203
segment[3],
125204
segment.length === 5 ? rootNames[segment[4]] : '',
205+
endLine,
206+
endColumn,
207+
false,
126208
);
127209

128-
// If the trace is invalid, then the trace ran into a sourcemap that doesn't contain a
129-
// respective segment into an original source.
130-
if (traced == null) continue;
131-
}
210+
if (tracedSegments.length === 0) continue;
132211

133-
const { column, line, name, content, source, ignore } = traced;
212+
for (const tracedSegment of tracedSegments) {
213+
const {
214+
column,
215+
line,
216+
name,
217+
content,
218+
source,
219+
ignore,
220+
isRangeMapping,
221+
rangeMappingOffset,
222+
} = tracedSegment;
134223

135-
maybeAddSegment(gen, i, genCol, source, line, column, name);
136-
if (source && content != null) setSourceContent(gen, source, content);
137-
if (ignore) setIgnore(gen, source, true);
224+
// The range mapping offset is the amount that we need to offset the
225+
// generated line/column from the root. We have to return this up
226+
// because originalPositionsForRange can't increment it.
227+
const genLine = i + rangeMappingOffset.line;
228+
// If the traced segment isn't on the same line as the range start,
229+
// genCol is irrelevant
230+
const genColumn =
231+
rangeMappingOffset.line === 0
232+
? genCol + rangeMappingOffset.column
233+
: rangeMappingOffset.column;
234+
maybeAddSegment(gen, genLine, genColumn, source, line, column, name, null);
235+
setRangeSegment(gen, genLine, genColumn, isRangeMapping);
236+
if (source && content != null) setSourceContent(gen, source, content);
237+
if (ignore) setIgnore(gen, source, true);
238+
}
239+
}
138240
}
139241
}
140242

@@ -159,14 +261,177 @@ export function originalPositionFor(
159261

160262
// If we couldn't find a segment, then this doesn't exist in the sourcemap.
161263
if (segment == null) return null;
264+
const maybeRange = isRange(source.map, segment);
265+
let startLine = 0; // FIXME this should be the line of the segment
266+
if (maybeRange) {
267+
startLine = maybeRange.line;
268+
}
269+
162270
// 1-length segments only move the current generated column, there's no source information
163271
// to gather from it.
164272
if (segment.length === 1) return SOURCELESS_MAPPING;
165273

274+
// If the child is a range mapping, we need to offset the next lookup point by the
275+
// offset of the parent mapping into the range.
276+
let rangeMappingOffset = { line: 0, column: 0 };
277+
if (maybeRange) {
278+
if (startLine === line) rangeMappingOffset = { line: 0, column: column - segment[0] };
279+
else rangeMappingOffset = { line: line - startLine, column: 0 };
280+
}
281+
166282
return originalPositionFor(
167283
source.sources[segment[1]],
168-
segment[2],
169-
segment[3],
284+
segment[2] + rangeMappingOffset.line,
285+
segment[3] + rangeMappingOffset.column,
170286
segment.length === 5 ? source.map.names[segment[4]] : name,
171287
);
172288
}
289+
290+
function originalPositionsForRange(
291+
source: Sources,
292+
line: number,
293+
column: number,
294+
name: string,
295+
endLine: number,
296+
endColumn: number,
297+
emitEndPoint: boolean,
298+
): SourceMapSegmentObject[] {
299+
// If this is the bottom node then we just return the current range.
300+
if (source.map === null) {
301+
return [
302+
SegmentObject(source.source, line, column, '', source.content, source.ignore, true),
303+
// The end point isn't always emitted, because the end point may be
304+
// a separate mapping that will be processed & translated too. We only emit this
305+
// if we need to make up a mapping because we split or clamped a range.
306+
...(emitEndPoint
307+
? [
308+
SegmentObject(
309+
source.source,
310+
endLine,
311+
endColumn,
312+
'',
313+
source.content,
314+
source.ignore,
315+
false,
316+
{ line: endLine - line, column: endColumn - column },
317+
),
318+
]
319+
: []),
320+
];
321+
}
322+
323+
// We additional trace the start position of the range to because we may need to
324+
// intersect with a range that starts before the given position, or map the start
325+
// of the range to it if there aren't any exact hits.
326+
const initialSegment = traceSegment(source.map, line, column);
327+
const segments = traceSegmentsInRange(source.map, line, column, endLine, endColumn);
328+
329+
// If tracing the start of the range hits a mapping that isn't in the segmenObjects list,
330+
// add it to the list to process first.
331+
if (initialSegment !== null && (segments.length === 0 || initialSegment !== segments[0])) {
332+
segments.splice(0, 0, initialSegment);
333+
}
334+
335+
const originalPositions = [];
336+
for (const segment of segments) {
337+
if (segment == null) continue;
338+
339+
let startLine = 0; // FIXME this should be the line of the segment
340+
let childEndLine, endSegment;
341+
const maybeRange = isRange(source.map, segment);
342+
if (maybeRange) {
343+
startLine = maybeRange.line;
344+
childEndLine = maybeRange.endLine;
345+
endSegment = maybeRange.endSegment;
346+
}
347+
348+
// At the very beginning of a range, the child position might be behind
349+
// the start of the range. In that case we clamp the offset to 0.
350+
const rangeOffsetLine = startLine - line;
351+
const rangeOffsetColumn = rangeOffsetLine === 0 ? Math.max(0, segment[0] - column) : segment[0];
352+
const rangeOffset = { line: rangeOffsetLine, column: rangeOffsetColumn };
353+
354+
// Sourceless mappings just have the offset added and we skip the recursive
355+
// step because there's no source to process.
356+
if (segment.length === 1) {
357+
const mapping = SOURCELESS_MAPPING;
358+
mapping.rangeMappingOffset = rangeOffset;
359+
originalPositions.push(mapping);
360+
continue;
361+
}
362+
363+
if (!maybeRange) {
364+
const position = originalPositionFor(
365+
source.sources[segment[1]],
366+
segment[2],
367+
segment[3],
368+
segment.length === 5 ? source.map.names[segment[4]] : name,
369+
);
370+
if (position !== null) {
371+
position.rangeMappingOffset.line += rangeOffset.line;
372+
position.rangeMappingOffset.column += rangeOffset.column;
373+
originalPositions.push(position);
374+
}
375+
} else {
376+
// Compute the intersection of the child and parent ranges.
377+
//
378+
// line,column endLine,endColumn
379+
// Parent range |-----------------------------|
380+
// Child range |------------------------|
381+
// startLine,segment[0] childEndLine,endSegment[0]
382+
// Child mapped |------------------------|
383+
// segment[2],segment[3] endSegment[2],endSegment[3]
384+
//
385+
// For example, if segment[0] < column as in this diagram, we
386+
// need to clamp the start point to column, which maps to
387+
// segment[3] + (column - segment[0]). The end point needs to
388+
// be clamped if endSegment[3] > endColumn.
389+
const clampedStartLine = Math.max(line, startLine);
390+
const clampedStartColumn = Math.max(column, segment[0]);
391+
const clampedEndLine = Math.min(endLine, childEndLine!);
392+
const clampedEndColumn = Math.min(endColumn, endSegment![0]);
393+
394+
const originalStartLine = segment[2] + (clampedStartLine - startLine);
395+
let originalStartColumn;
396+
if (startLine == line) {
397+
originalStartColumn = segment[3] + (clampedStartColumn - segment[0]);
398+
} else if (startLine > line) {
399+
originalStartColumn = segment[3];
400+
} else {
401+
originalStartColumn = column;
402+
}
403+
404+
const originalEndLine = originalStartLine + (clampedEndLine - clampedStartLine);
405+
// When the range ends on the same line, the end column is calculated from the
406+
// segment distance because the range is exclusive of the end segment.
407+
// If the range ends on a different line, we end on the end segment generated
408+
// column.
409+
const originalEndColumn =
410+
originalStartLine == originalEndLine
411+
? originalStartColumn + (clampedEndColumn - clampedStartColumn)
412+
: clampedEndColumn;
413+
414+
const positions = originalPositionsForRange(
415+
source.sources[segment[1]],
416+
originalStartLine,
417+
originalStartColumn,
418+
segment.length === 5 ? source.map.names[segment[4]] : name,
419+
originalEndLine,
420+
originalEndColumn,
421+
// If the range had to be clamped then we need to emit a new mapping
422+
// for the end, as no existing explicit mapping will exist at that point.
423+
// Otherwise, we should be able to rely on the original end mapping being
424+
// translated appropriately.
425+
clampedEndLine !== childEndLine || clampedEndColumn !== endSegment![0],
426+
);
427+
428+
for (const position of positions) {
429+
position.rangeMappingOffset.line += rangeOffset.line;
430+
position.rangeMappingOffset.column += rangeOffset.column;
431+
}
432+
originalPositions.push(...positions);
433+
}
434+
}
435+
436+
return originalPositions;
437+
}

0 commit comments

Comments
 (0)