/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import {withSyncPerfMeasurements} from 'react-devtools-shared/src/PerformanceLoggingUtils';
import {decode} from 'sourcemap-codec';
import type {
IndexSourceMap,
IndexSourceMapSection,
BasicSourceMap,
MixedSourceMap,
} from './SourceMapTypes';
type SearchPosition = {
columnNumber: number,
lineNumber: number,
};
type ResultPosition = {
column: number,
line: number,
sourceContent: string,
sourceURL: string,
};
export type SourceMapConsumerType = {
originalPositionFor: SearchPosition => ResultPosition,
};
type Mappings = Array<Array<Array<number>>>;
export default function SourceMapConsumer(
sourceMapJSON: MixedSourceMap | IndexSourceMapSection,
): SourceMapConsumerType {
if (sourceMapJSON.sections != null) {
return IndexedSourceMapConsumer(((sourceMapJSON: any): IndexSourceMap));
} else {
return BasicSourceMapConsumer(((sourceMapJSON: any): BasicSourceMap));
}
}
function BasicSourceMapConsumer(sourceMapJSON: BasicSourceMap) {
const decodedMappings: Mappings = withSyncPerfMeasurements(
'Decoding source map mappings with sourcemap-codec',
() => decode(sourceMapJSON.mappings),
);
function originalPositionFor({
columnNumber,
lineNumber,
}: SearchPosition): ResultPosition {
// Error.prototype.stack columns are 1-based (like most IDEs) but ASTs are 0-based.
const targetColumnNumber = columnNumber - 1;
const lineMappings = decodedMappings[lineNumber - 1];
let nearestEntry = null;
let startIndex = 0;
let stopIndex = lineMappings.length - 1;
let index = -1;
while (startIndex <= stopIndex) {
index = Math.floor((stopIndex + startIndex) / 2);
nearestEntry = lineMappings[index];
const currentColumn = nearestEntry[0];
if (currentColumn === targetColumnNumber) {
break;
} else {
if (currentColumn > targetColumnNumber) {
if (stopIndex - index > 0) {
stopIndex = index;
} else {
index = stopIndex;
break;
}
} else {
if (index - startIndex > 0) {
startIndex = index;
} else {
index = startIndex;
break;
}
}
}
}
// We have found either the exact element, or the next-closest element.
// However there may be more than one such element.
// Make sure we always return the smallest of these.
while (index > 0) {
const previousEntry = lineMappings[index - 1];
const currentColumn = previousEntry[0];
if (currentColumn !== targetColumnNumber) {
break;
}
index--;
}
if (nearestEntry == null) {
// TODO maybe fall back to the runtime source instead of throwing?
throw Error(
`Could not find runtime location for line:${lineNumber} and column:${columnNumber}`,
);
}
const sourceIndex = nearestEntry[1];
const sourceContent =
sourceMapJSON.sourcesContent != null
? sourceMapJSON.sourcesContent[sourceIndex]
: null;
const sourceURL = sourceMapJSON.sources[sourceIndex] ?? null;
const line = nearestEntry[2] + 1;
const column = nearestEntry[3];
if (sourceContent === null || sourceURL === null) {
// TODO maybe fall back to the runtime source instead of throwing?
throw Error(
`Could not find original source for line:${lineNumber} and column:${columnNumber}`,
);
}
return {
column,
line,
sourceContent: ((sourceContent: any): string),
sourceURL: ((sourceURL: any): string),
};
}
return (({
originalPositionFor,
}: any): SourceMapConsumerType);
}
type Section = {
+generatedColumn: number,
+generatedLine: number,
+map: MixedSourceMap,
// Lazily parsed only when/as the section is needed.
sourceMapConsumer: SourceMapConsumerType | null,
};
function IndexedSourceMapConsumer(sourceMapJSON: IndexSourceMap) {
let lastOffset = {
line: -1,
column: 0,
};
const sections: Array<Section> = sourceMapJSON.sections.map(section => {
const offset = section.offset;
const offsetLine = offset.line;
const offsetColumn = offset.column;
if (
offsetLine < lastOffset.line ||
(offsetLine === lastOffset.line && offsetColumn < lastOffset.column)
) {
throw new Error('Section offsets must be ordered and non-overlapping.');
}
lastOffset = offset;
return {
// The offset fields are 0-based, but we use 1-based indices when encoding/decoding from VLQ.
generatedLine: offsetLine + 1,
generatedColumn: offsetColumn + 1,
map: section.map,
sourceMapConsumer: null,
};
});
function originalPositionFor({
columnNumber,
lineNumber,
}: SearchPosition): ResultPosition {
// Error.prototype.stack columns are 1-based (like most IDEs) but ASTs are 0-based.
const targetColumnNumber = columnNumber - 1;
let section = null;
let startIndex = 0;
let stopIndex = sections.length - 1;
let index = -1;
while (startIndex <= stopIndex) {
index = Math.floor((stopIndex + startIndex) / 2);
section = sections[index];
const currentLine = section.generatedLine;
if (currentLine === lineNumber) {
const currentColumn = section.generatedColumn;
if (currentColumn === lineNumber) {
break;
} else {
if (currentColumn > targetColumnNumber) {
if (stopIndex - index > 0) {
stopIndex = index;
} else {
index = stopIndex;
break;
}
} else {
if (index - startIndex > 0) {
startIndex = index;
} else {
index = startIndex;
break;
}
}
}
} else {
if (currentLine > lineNumber) {
if (stopIndex - index > 0) {
stopIndex = index;
} else {
index = stopIndex;
break;
}
} else {
if (index - startIndex > 0) {
startIndex = index;
} else {
index = startIndex;
break;
}
}
}
}
if (section == null) {
// TODO maybe fall back to the runtime source instead of throwing?
throw Error(
`Could not find matching section for line:${lineNumber} and column:${columnNumber}`,
);
}
if (section.sourceMapConsumer === null) {
// Lazily parse the section only when it's needed.
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
section.sourceMapConsumer = new SourceMapConsumer(section.map);
}
return section.sourceMapConsumer.originalPositionFor({
columnNumber,
lineNumber,
});
}
return (({
originalPositionFor,
}: any): SourceMapConsumerType);
}