Skip to content

Commit

Permalink
Merge pull request #1 from jridgewell/generatedPositionFor
Browse files Browse the repository at this point in the history
Add generatedPositionFor API
  • Loading branch information
jridgewell committed Apr 15, 2022
2 parents b268826 + dd3d0ff commit d4ead19
Show file tree
Hide file tree
Showing 9 changed files with 380 additions and 56 deletions.
10 changes: 8 additions & 2 deletions README.md
Expand Up @@ -6,7 +6,7 @@
original location in the source file through a source map.

You may already be familiar with the [`source-map`][source-map] package's `SourceMapConsumer`. This
provides the same `originalPositionFor` API, without requires WASM.
provides the same `originalPositionFor` and `generatedPositionFor` API, without requires WASM.

## Installation

Expand All @@ -17,7 +17,7 @@ npm install @jridgewell/trace-mapping
## Usage

```typescript
import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping';
import { TraceMap, originalPositionFor, generatedPositionFor } from '@jridgewell/trace-mapping';

const tracer = new TraceMap({
version: 3,
Expand All @@ -34,6 +34,12 @@ assert.deepEqual(traced, {
column: 4,
name: 'foo',
});

const generated = generatedPositionFor(tracer, { source: 'input.js', line: 42, column: 4 });
assert.deepEqual(generated, {
line: 1,
column: 5,
});
```

We also provide a lower level API to get the actual segment that matches our line and column. Unlike
Expand Down
39 changes: 33 additions & 6 deletions src/binary-search.ts
@@ -1,11 +1,14 @@
import type { SourceMapSegment } from './types';
import type { SourceMapSegment, ReverseSegment } from './sourcemap-segment';
import { COLUMN } from './sourcemap-segment';

type MemoState = {
export type MemoState = {
lastKey: number;
lastNeedle: number;
lastIndex: number;
};

export let found = false;

/**
* A binary search implementation that returns the index if a match is found.
* If no match is found, then the left-index (the index associated with the item that comes just
Expand All @@ -23,16 +26,17 @@ type MemoState = {
* ```
*/
export function binarySearch(
haystack: SourceMapSegment[],
haystack: SourceMapSegment[] | ReverseSegment[],
needle: number,
low: number,
high: number,
): number {
while (low <= high) {
const mid = low + ((high - low) >> 1);
const cmp = haystack[mid][0] - needle;
const cmp = haystack[mid][COLUMN] - needle;

if (cmp === 0) {
found = true;
return mid;
}

Expand All @@ -43,9 +47,32 @@ export function binarySearch(
}
}

found = false;
return low - 1;
}

export function upperBound(
haystack: SourceMapSegment[] | ReverseSegment[],
needle: number,
index: number,
): number {
for (let i = index + 1; i < haystack.length; i++, index++) {
if (haystack[i][COLUMN] !== needle) break;
}
return index;
}

export function lowerBound(
haystack: SourceMapSegment[] | ReverseSegment[],
needle: number,
index: number,
): number {
for (let i = index - 1; i >= 0; i--, index--) {
if (haystack[i][COLUMN] !== needle) break;
}
return index;
}

export function memoizedState(): MemoState {
return {
lastKey: -1,
Expand All @@ -59,7 +86,7 @@ export function memoizedState(): MemoState {
* index, allowing us to skip a few tests if mappings are monotonically increasing.
*/
export function memoizedBinarySearch(
haystack: SourceMapSegment[],
haystack: SourceMapSegment[] | ReverseSegment[],
needle: number,
state: MemoState,
key: number,
Expand All @@ -75,7 +102,7 @@ export function memoizedBinarySearch(

if (needle >= lastNeedle) {
// lastIndex may be -1 if the previous needle was not found.
low = Math.max(lastIndex, 0);
low = lastIndex === -1 ? 0 : lastIndex;
} else {
high = lastIndex;
}
Expand Down
64 changes: 64 additions & 0 deletions src/by-source.ts
@@ -0,0 +1,64 @@
import { COLUMN, SOURCES_INDEX, SOURCE_LINE, SOURCE_COLUMN } from './sourcemap-segment';
import { memoizedBinarySearch, upperBound } from './binary-search';

import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment';
import type { MemoState } from './binary-search';

export type Source = {
__proto__: null;
[line: number]: Exclude<ReverseSegment, [number]>[];
};

// Rebuilds the original source files, with mappings that are ordered by source line/column instead
// of generated line/column.
export default function buildBySources(
decoded: SourceMapSegment[][],
memos: MemoState[],
): Source[] {
const sources: Source[] = memos.map(buildNullArray);

for (let i = 0; i < decoded.length; i++) {
const line = decoded[i];
for (let j = 0; j < line.length; j++) {
const seg = line[j];
if (seg.length === 1) continue;

const sourceIndex = seg[SOURCES_INDEX];
const sourceLine = seg[SOURCE_LINE];
const sourceColumn = seg[SOURCE_COLUMN];
const originalSource = sources[sourceIndex];
const originalLine = (originalSource[sourceLine] ||= []);
const memo = memos[sourceIndex];

// The binary search either found a match, or it found the left-index just before where the
// 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(
originalLine,
sourceColumn,
memoizedBinarySearch(originalLine, sourceColumn, memo, sourceLine),
);

insert(originalLine, (memo.lastIndex = index + 1), [sourceColumn, i, seg[COLUMN]]);
}
}

return sources;
}

function insert<T>(array: T[], index: number, value: T) {
for (let i = array.length; i > index; i--) {
array[i] = array[i - 1];
}
array[index] = value;
}

// Null arrays allow us to use ordered index keys without actually allocating contiguous memory like
// a real array. We use a null-prototype object to avoid prototype pollution and deoptimizations.
// Numeric properties on objects are magically sorted in ascending order by the engine regardless of
// the insertion order. So, by setting any numeric keys, even out of order, we'll get ascending
// order when iterating with for-in.
function buildNullArray<T extends { __proto__: null }>(): T {
return { __proto__: null } as T;
}
8 changes: 5 additions & 3 deletions src/sort.ts
@@ -1,4 +1,6 @@
import type { SourceMapSegment } from './types';
import { COLUMN } from './sourcemap-segment';

import type { SourceMapSegment } from './sourcemap-segment';

export default function maybeSort(
mappings: SourceMapSegment[][],
Expand Down Expand Up @@ -26,7 +28,7 @@ function nextUnsortedSegmentLine(mappings: SourceMapSegment[][], start: number):

function isSorted(line: SourceMapSegment[]): boolean {
for (let j = 1; j < line.length; j++) {
if (line[j][0] < line[j - 1][0]) {
if (line[j][COLUMN] < line[j - 1][COLUMN]) {
return false;
}
}
Expand All @@ -39,5 +41,5 @@ function sortSegments(line: SourceMapSegment[], owned: boolean): SourceMapSegmen
}

function sortComparator(a: SourceMapSegment, b: SourceMapSegment): number {
return a[0] - b[0];
return a[COLUMN] - b[COLUMN];
}
23 changes: 23 additions & 0 deletions src/sourcemap-segment.ts
@@ -0,0 +1,23 @@
type GeneratedColumn = number;
type SourcesIndex = number;
type SourceLine = number;
type SourceColumn = number;
type NamesIndex = number;

type GeneratedLine = number;

export type SourceMapSegment =
| [GeneratedColumn]
| [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn]
| [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex];

export type ReverseSegment = [SourceColumn, GeneratedLine, GeneratedColumn];

export const COLUMN = 0;
export const SOURCES_INDEX = 1;
export const SOURCE_LINE = 2;
export const SOURCE_COLUMN = 3;
export const NAMES_INDEX = 4;

export const REV_GENERATED_LINE = 1;
export const REV_GENERATED_COLUMN = 2;

0 comments on commit d4ead19

Please sign in to comment.