Download as txt, pdf, or txt
Download as txt, pdf, or txt
You are on page 1of 5

/*

* 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 strict-local
*/

import type {
HookMap,
HookMapEntry,
HookMapLine,
HookMapMappings,
} from './generateHookMap';
import type {Position} from './astUtils';
import {NO_HOOK_NAME} from './astUtils';

/**
* Finds the Hook name assigned to a given location in the source code,
* and a HookMap extracted from an extended source map.
* The given location must correspond to the location in the *original*
* source code (i.e. *not* the generated one).
*
* Note that all locations in the source code are guaranteed to map
* to a name, including a sentinel value that represents a missing
* Hook name: '<no-hook>'.
*
* For more details on the format of the HookMap, see generateHookMap
* and the tests for that function and this function.
*/
export function getHookNameForLocation(
location: Position,
hookMap: HookMap,
): string | null {
const {names, mappings} = hookMap;

// The HookMap mappings are grouped by lines, so first we look up


// which line of mappings covers the target location.
// Note that we expect to find a line since all the locations in the
// source code are guaranteed to map to a name, including a '<no-hook>'
// name.
const foundLine = binSearch(location, mappings, compareLinePositions);
if (foundLine == null) {
throw new Error(
`Expected to find a line in the HookMap that covers the target location at
line: ${location.line}, column: ${location.column}`,
);
}

let foundEntry;
const foundLineNumber = getLineNumberFromLine(foundLine);
// The line found in the mappings will never be larger than the target
// line, and vice-versa, so if the target line doesn't match the found
// line, we immediately know that it must correspond to the last mapping
// entry for that line.
if (foundLineNumber !== location.line) {
foundEntry = foundLine[foundLine.length - 1];
} else {
foundEntry = binSearch(location, foundLine, compareColumnPositions);
}

if (foundEntry == null) {
throw new Error(
`Expected to find a mapping in the HookMap that covers the target location at
line: ${location.line}, column: ${location.column}`,
);
}

const foundNameIndex = getHookNameIndexFromEntry(foundEntry);


if (foundNameIndex == null) {
throw new Error(
`Expected to find a name index in the HookMap that covers the target location
at line: ${location.line}, column: ${location.column}`,
);
}
const foundName = names[foundNameIndex];
if (foundName == null) {
throw new Error(
`Expected to find a name in the HookMap that covers the target location at
line: ${location.line}, column: ${location.column}`,
);
}

if (foundName === NO_HOOK_NAME) {


return null;
}
return foundName;
}

function binSearch<T>(
location: Position,
items: T[],
compare: (
location: Position,
items: T[],
index: number,
) => {index: number | null, direction: number},
): T | null {
let count = items.length;
let index = 0;
let firstElementIndex = 0;
let step;

while (count > 0) {


index = firstElementIndex;
step = Math.floor(count / 2);
index += step;

const comparison = compare(location, items, index);


if (comparison.direction === 0) {
if (comparison.index == null) {
throw new Error('Expected an index when matching element is found.');
}
firstElementIndex = comparison.index;
break;
}
if (comparison.direction > 0) {
index++;
firstElementIndex = index;
count -= step + 1;
} else {
count = step;
}
}

return firstElementIndex != null ? items[firstElementIndex] : null;


}

/**
* Compares the target line location to the current location
* given by the provided index.
*
* If the target line location matches the current location, returns
* the index of the matching line in the mappings. In order for a line
* to match, the target line must match the line exactly, or be within
* the line range of the current line entries and the adjacent line
* entries.
*
* If the line doesn't match, returns the search direction for the
* next step in the binary search.
*/
function compareLinePositions(
location: Position,
mappings: HookMapMappings,
index: number,
): {index: number | null, direction: number} {
const startIndex = index;
const start = mappings[startIndex];
if (start == null) {
throw new Error(`Unexpected line missing in HookMap at index ${index}.`);
}
const startLine = getLineNumberFromLine(start);

let endLine;
let endIndex = index + 1;
const end = mappings[endIndex];
if (end != null) {
endLine = getLineNumberFromLine(end);
} else {
endIndex = startIndex;
endLine = startLine;
}

// When the line matches exactly, return the matching index


if (startLine === location.line) {
return {index: startIndex, direction: 0};
}
if (endLine === location.line) {
return {index: endIndex, direction: 0};
}

// If we're at the end of the mappings, and the target line is greater
// than the current line, then this final line must cover the
// target location, so we return it.
if (location.line > endLine && end == null) {
return {index: endIndex, direction: 0};
}

// If the location is within the current line and the adjacent one,
// we know that the target location must be covered by the current line.
if (startLine < location.line && location.line < endLine) {
return {index: startIndex, direction: 0};
}

// Otherwise, return the next direction in the search.


return {index: null, direction: location.line - startLine};
}

/**
* Compares the target column location to the current location
* given by the provided index.
*
* If the target column location matches the current location, returns
* the index of the matching entry in the mappings. In order for a column
* to match, the target column must match the column exactly, or be within
* the column range of the current entry and the adjacent entry.
*
* If the column doesn't match, returns the search direction for the
* next step in the binary search.
*/
function compareColumnPositions(
location: Position,
line: HookMapLine,
index: number,
): {index: number | null, direction: number} {
const startIndex = index;
const start = line[index];
if (start == null) {
throw new Error(
`Unexpected mapping missing in HookMap line at index ${index}.`,
);
}
const startColumn = getColumnNumberFromEntry(start);

let endColumn;
let endIndex = index + 1;
const end = line[endIndex];
if (end != null) {
endColumn = getColumnNumberFromEntry(end);
} else {
endIndex = startIndex;
endColumn = startColumn;
}

// When the column matches exactly, return the matching index


if (startColumn === location.column) {
return {index: startIndex, direction: 0};
}
if (endColumn === location.column) {
return {index: endIndex, direction: 0};
}

// If we're at the end of the entries for this line, and the target
// column is greater than the current column, then this final entry
// must cover the target location, so we return it.
if (location.column > endColumn && end == null) {
return {index: endIndex, direction: 0};
}

// If the location is within the current column and the adjacent one,
// we know that the target location must be covered by the current entry.
if (startColumn < location.column && location.column < endColumn) {
return {index: startIndex, direction: 0};
}

// Otherwise, return the next direction in the search.


return {index: null, direction: location.column - startColumn};
}

function getLineNumberFromLine(line: HookMapLine): number {


return getLineNumberFromEntry(line[0]);
}

function getLineNumberFromEntry(entry: HookMapEntry): number {


const lineNumber = entry[0];
if (lineNumber == null) {
throw new Error('Unexpected line number missing in entry in HookMap');
}
return lineNumber;
}

function getColumnNumberFromEntry(entry: HookMapEntry): number {


const columnNumber = entry[1];
if (columnNumber == null) {
throw new Error('Unexpected column number missing in entry in HookMap');
}
return columnNumber;
}

function getHookNameIndexFromEntry(entry: HookMapEntry): number {


const hookNameIndex = entry[2];
if (hookNameIndex == null) {
throw new Error('Unexpected hook name index missing in entry in HookMap');
}
return hookNameIndex;
}

You might also like