1. /*
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow strict-local
    
  8.  */
    
  9. 
    
  10. import type {
    
  11.   HookMap,
    
  12.   HookMapEntry,
    
  13.   HookMapLine,
    
  14.   HookMapMappings,
    
  15. } from './generateHookMap';
    
  16. import type {Position} from './astUtils';
    
  17. import {NO_HOOK_NAME} from './astUtils';
    
  18. 
    
  19. /**
    
  20.  * Finds the Hook name assigned to a given location in the source code,
    
  21.  * and a HookMap extracted from an extended source map.
    
  22.  * The given location must correspond to the location in the *original*
    
  23.  * source code (i.e. *not* the generated one).
    
  24.  *
    
  25.  * Note that all locations in the source code are guaranteed to map
    
  26.  * to a name, including a sentinel value that represents a missing
    
  27.  * Hook name: '<no-hook>'.
    
  28.  *
    
  29.  * For more details on the format of the HookMap, see generateHookMap
    
  30.  * and the tests for that function and this function.
    
  31.  */
    
  32. export function getHookNameForLocation(
    
  33.   location: Position,
    
  34.   hookMap: HookMap,
    
  35. ): string | null {
    
  36.   const {names, mappings} = hookMap;
    
  37. 
    
  38.   // The HookMap mappings are grouped by lines, so first we look up
    
  39.   // which line of mappings covers the target location.
    
  40.   // Note that we expect to find a line since all the locations in the
    
  41.   // source code are guaranteed to map to a name, including a '<no-hook>'
    
  42.   // name.
    
  43.   const foundLine = binSearch(location, mappings, compareLinePositions);
    
  44.   if (foundLine == null) {
    
  45.     throw new Error(
    
  46.       `Expected to find a line in the HookMap that covers the target location at line: ${location.line}, column: ${location.column}`,
    
  47.     );
    
  48.   }
    
  49. 
    
  50.   let foundEntry;
    
  51.   const foundLineNumber = getLineNumberFromLine(foundLine);
    
  52.   // The line found in the mappings will never be larger than the target
    
  53.   // line, and vice-versa, so if the target line doesn't match the found
    
  54.   // line, we immediately know that it must correspond to the last mapping
    
  55.   // entry for that line.
    
  56.   if (foundLineNumber !== location.line) {
    
  57.     foundEntry = foundLine[foundLine.length - 1];
    
  58.   } else {
    
  59.     foundEntry = binSearch(location, foundLine, compareColumnPositions);
    
  60.   }
    
  61. 
    
  62.   if (foundEntry == null) {
    
  63.     throw new Error(
    
  64.       `Expected to find a mapping in the HookMap that covers the target location at line: ${location.line}, column: ${location.column}`,
    
  65.     );
    
  66.   }
    
  67. 
    
  68.   const foundNameIndex = getHookNameIndexFromEntry(foundEntry);
    
  69.   if (foundNameIndex == null) {
    
  70.     throw new Error(
    
  71.       `Expected to find a name index in the HookMap that covers the target location at line: ${location.line}, column: ${location.column}`,
    
  72.     );
    
  73.   }
    
  74.   const foundName = names[foundNameIndex];
    
  75.   if (foundName == null) {
    
  76.     throw new Error(
    
  77.       `Expected to find a name in the HookMap that covers the target location at line: ${location.line}, column: ${location.column}`,
    
  78.     );
    
  79.   }
    
  80. 
    
  81.   if (foundName === NO_HOOK_NAME) {
    
  82.     return null;
    
  83.   }
    
  84.   return foundName;
    
  85. }
    
  86. 
    
  87. function binSearch<T>(
    
  88.   location: Position,
    
  89.   items: T[],
    
  90.   compare: (
    
  91.     location: Position,
    
  92.     items: T[],
    
  93.     index: number,
    
  94.   ) => {index: number | null, direction: number},
    
  95. ): T | null {
    
  96.   let count = items.length;
    
  97.   let index = 0;
    
  98.   let firstElementIndex = 0;
    
  99.   let step;
    
  100. 
    
  101.   while (count > 0) {
    
  102.     index = firstElementIndex;
    
  103.     step = Math.floor(count / 2);
    
  104.     index += step;
    
  105. 
    
  106.     const comparison = compare(location, items, index);
    
  107.     if (comparison.direction === 0) {
    
  108.       if (comparison.index == null) {
    
  109.         throw new Error('Expected an index when matching element is found.');
    
  110.       }
    
  111.       firstElementIndex = comparison.index;
    
  112.       break;
    
  113.     }
    
  114. 
    
  115.     if (comparison.direction > 0) {
    
  116.       index++;
    
  117.       firstElementIndex = index;
    
  118.       count -= step + 1;
    
  119.     } else {
    
  120.       count = step;
    
  121.     }
    
  122.   }
    
  123. 
    
  124.   return firstElementIndex != null ? items[firstElementIndex] : null;
    
  125. }
    
  126. 
    
  127. /**
    
  128.  * Compares the target line location to the current location
    
  129.  * given by the provided index.
    
  130.  *
    
  131.  * If the target line location matches the current location, returns
    
  132.  * the index of the matching line in the mappings. In order for a line
    
  133.  * to match, the target line must match the line exactly, or be within
    
  134.  * the line range of the current line entries and the adjacent line
    
  135.  * entries.
    
  136.  *
    
  137.  * If the line doesn't match, returns the search direction for the
    
  138.  * next step in the binary search.
    
  139.  */
    
  140. function compareLinePositions(
    
  141.   location: Position,
    
  142.   mappings: HookMapMappings,
    
  143.   index: number,
    
  144. ): {index: number | null, direction: number} {
    
  145.   const startIndex = index;
    
  146.   const start = mappings[startIndex];
    
  147.   if (start == null) {
    
  148.     throw new Error(`Unexpected line missing in HookMap at index ${index}.`);
    
  149.   }
    
  150.   const startLine = getLineNumberFromLine(start);
    
  151. 
    
  152.   let endLine;
    
  153.   let endIndex = index + 1;
    
  154.   const end = mappings[endIndex];
    
  155.   if (end != null) {
    
  156.     endLine = getLineNumberFromLine(end);
    
  157.   } else {
    
  158.     endIndex = startIndex;
    
  159.     endLine = startLine;
    
  160.   }
    
  161. 
    
  162.   // When the line matches exactly, return the matching index
    
  163.   if (startLine === location.line) {
    
  164.     return {index: startIndex, direction: 0};
    
  165.   }
    
  166.   if (endLine === location.line) {
    
  167.     return {index: endIndex, direction: 0};
    
  168.   }
    
  169. 
    
  170.   // If we're at the end of the mappings, and the target line is greater
    
  171.   // than the current line, then this final line must cover the
    
  172.   // target location, so we return it.
    
  173.   if (location.line > endLine && end == null) {
    
  174.     return {index: endIndex, direction: 0};
    
  175.   }
    
  176. 
    
  177.   // If the location is within the current line and the adjacent one,
    
  178.   // we know that the target location must be covered by the current line.
    
  179.   if (startLine < location.line && location.line < endLine) {
    
  180.     return {index: startIndex, direction: 0};
    
  181.   }
    
  182. 
    
  183.   // Otherwise, return the next direction in the search.
    
  184.   return {index: null, direction: location.line - startLine};
    
  185. }
    
  186. 
    
  187. /**
    
  188.  * Compares the target column location to the current location
    
  189.  * given by the provided index.
    
  190.  *
    
  191.  * If the target column location matches the current location, returns
    
  192.  * the index of the matching entry in the mappings. In order for a column
    
  193.  * to match, the target column must match the column exactly, or be within
    
  194.  * the column range of the current entry and the adjacent entry.
    
  195.  *
    
  196.  * If the column doesn't match, returns the search direction for the
    
  197.  * next step in the binary search.
    
  198.  */
    
  199. function compareColumnPositions(
    
  200.   location: Position,
    
  201.   line: HookMapLine,
    
  202.   index: number,
    
  203. ): {index: number | null, direction: number} {
    
  204.   const startIndex = index;
    
  205.   const start = line[index];
    
  206.   if (start == null) {
    
  207.     throw new Error(
    
  208.       `Unexpected mapping missing in HookMap line at index ${index}.`,
    
  209.     );
    
  210.   }
    
  211.   const startColumn = getColumnNumberFromEntry(start);
    
  212. 
    
  213.   let endColumn;
    
  214.   let endIndex = index + 1;
    
  215.   const end = line[endIndex];
    
  216.   if (end != null) {
    
  217.     endColumn = getColumnNumberFromEntry(end);
    
  218.   } else {
    
  219.     endIndex = startIndex;
    
  220.     endColumn = startColumn;
    
  221.   }
    
  222. 
    
  223.   // When the column matches exactly, return the matching index
    
  224.   if (startColumn === location.column) {
    
  225.     return {index: startIndex, direction: 0};
    
  226.   }
    
  227.   if (endColumn === location.column) {
    
  228.     return {index: endIndex, direction: 0};
    
  229.   }
    
  230. 
    
  231.   // If we're at the end of the entries for this line, and the target
    
  232.   // column is greater than the current column, then this final entry
    
  233.   // must cover the target location, so we return it.
    
  234.   if (location.column > endColumn && end == null) {
    
  235.     return {index: endIndex, direction: 0};
    
  236.   }
    
  237. 
    
  238.   // If the location is within the current column and the adjacent one,
    
  239.   // we know that the target location must be covered by the current entry.
    
  240.   if (startColumn < location.column && location.column < endColumn) {
    
  241.     return {index: startIndex, direction: 0};
    
  242.   }
    
  243. 
    
  244.   // Otherwise, return the next direction in the search.
    
  245.   return {index: null, direction: location.column - startColumn};
    
  246. }
    
  247. 
    
  248. function getLineNumberFromLine(line: HookMapLine): number {
    
  249.   return getLineNumberFromEntry(line[0]);
    
  250. }
    
  251. 
    
  252. function getLineNumberFromEntry(entry: HookMapEntry): number {
    
  253.   const lineNumber = entry[0];
    
  254.   if (lineNumber == null) {
    
  255.     throw new Error('Unexpected line number missing in entry in HookMap');
    
  256.   }
    
  257.   return lineNumber;
    
  258. }
    
  259. 
    
  260. function getColumnNumberFromEntry(entry: HookMapEntry): number {
    
  261.   const columnNumber = entry[1];
    
  262.   if (columnNumber == null) {
    
  263.     throw new Error('Unexpected column number missing in entry in HookMap');
    
  264.   }
    
  265.   return columnNumber;
    
  266. }
    
  267. 
    
  268. function getHookNameIndexFromEntry(entry: HookMapEntry): number {
    
  269.   const hookNameIndex = entry[2];
    
  270.   if (hookNameIndex == null) {
    
  271.     throw new Error('Unexpected hook name index missing in entry in HookMap');
    
  272.   }
    
  273.   return hookNameIndex;
    
  274. }