main
js 274 lines 8.41 KB
Raw
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 }