Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add generatedPositionFor API #1

Merged
merged 6 commits into from Apr 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;