index.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879
  1. // @ts-self-types="./index.d.ts"
  2. import levn from 'levn';
  3. /**
  4. * @fileoverview Config Comment Parser
  5. * @author Nicholas C. Zakas
  6. */
  7. //-----------------------------------------------------------------------------
  8. // Type Definitions
  9. //-----------------------------------------------------------------------------
  10. /** @import * as $eslintcore from "@eslint/core"; */
  11. /** @typedef {$eslintcore.RuleConfig} RuleConfig */
  12. /** @typedef {$eslintcore.RulesConfig} RulesConfig */
  13. /** @import * as $typests from "./types.ts"; */
  14. /** @typedef {$typests.StringConfig} StringConfig */
  15. /** @typedef {$typests.BooleanConfig} BooleanConfig */
  16. //-----------------------------------------------------------------------------
  17. // Helpers
  18. //-----------------------------------------------------------------------------
  19. const directivesPattern = /^([a-z]+(?:-[a-z]+)*)(?:\s|$)/u;
  20. const validSeverities = new Set([0, 1, 2, "off", "warn", "error"]);
  21. /**
  22. * Determines if the severity in the rule configuration is valid.
  23. * @param {RuleConfig} ruleConfig A rule's configuration.
  24. * @returns {boolean} `true` if the severity is valid, otherwise `false`.
  25. */
  26. function isSeverityValid(ruleConfig) {
  27. const severity = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;
  28. return validSeverities.has(severity);
  29. }
  30. /**
  31. * Determines if all severities in the rules configuration are valid.
  32. * @param {RulesConfig} rulesConfig The rules configuration to check.
  33. * @returns {boolean} `true` if all severities are valid, otherwise `false`.
  34. */
  35. function isEverySeverityValid(rulesConfig) {
  36. return Object.values(rulesConfig).every(isSeverityValid);
  37. }
  38. /**
  39. * Represents a directive comment.
  40. */
  41. class DirectiveComment {
  42. /**
  43. * The label of the directive, such as "eslint", "eslint-disable", etc.
  44. * @type {string}
  45. */
  46. label = "";
  47. /**
  48. * The value of the directive (the string after the label).
  49. * @type {string}
  50. */
  51. value = "";
  52. /**
  53. * The justification of the directive (the string after the --).
  54. * @type {string}
  55. */
  56. justification = "";
  57. /**
  58. * Creates a new directive comment.
  59. * @param {string} label The label of the directive.
  60. * @param {string} value The value of the directive.
  61. * @param {string} justification The justification of the directive.
  62. */
  63. constructor(label, value, justification) {
  64. this.label = label;
  65. this.value = value;
  66. this.justification = justification;
  67. }
  68. }
  69. //------------------------------------------------------------------------------
  70. // Public Interface
  71. //------------------------------------------------------------------------------
  72. /**
  73. * Object to parse ESLint configuration comments.
  74. */
  75. class ConfigCommentParser {
  76. /**
  77. * Parses a list of "name:string_value" or/and "name" options divided by comma or
  78. * whitespace. Used for "global" comments.
  79. * @param {string} string The string to parse.
  80. * @returns {StringConfig} Result map object of names and string values, or null values if no value was provided.
  81. */
  82. parseStringConfig(string) {
  83. const items = /** @type {StringConfig} */ ({});
  84. // Collapse whitespace around `:` and `,` to make parsing easier
  85. const trimmedString = string
  86. .trim()
  87. .replace(/(?<!\s)\s*([:,])\s*/gu, "$1");
  88. trimmedString.split(/\s|,+/u).forEach(name => {
  89. if (!name) {
  90. return;
  91. }
  92. // value defaults to null (if not provided), e.g: "foo" => ["foo", null]
  93. const [key, value = null] = name.split(":");
  94. items[key] = value;
  95. });
  96. return items;
  97. }
  98. /**
  99. * Parses a JSON-like config.
  100. * @param {string} string The string to parse.
  101. * @returns {({ok: true, config: RulesConfig}|{ok: false, error: {message: string}})} Result map object
  102. */
  103. parseJSONLikeConfig(string) {
  104. // Parses a JSON-like comment by the same way as parsing CLI option.
  105. try {
  106. const items =
  107. /** @type {RulesConfig} */ (levn.parse("Object", string)) || {};
  108. /*
  109. * When the configuration has any invalid severities, it should be completely
  110. * ignored. This is because the configuration is not valid and should not be
  111. * applied.
  112. *
  113. * For example, the following configuration is invalid:
  114. *
  115. * "no-alert: 2 no-console: 2"
  116. *
  117. * This results in a configuration of { "no-alert": "2 no-console: 2" }, which is
  118. * not valid. In this case, the configuration should be ignored.
  119. */
  120. if (isEverySeverityValid(items)) {
  121. return {
  122. ok: true,
  123. config: items,
  124. };
  125. }
  126. } catch {
  127. // levn parsing error: ignore to parse the string by a fallback.
  128. }
  129. /*
  130. * Optionator cannot parse commaless notations.
  131. * But we are supporting that. So this is a fallback for that.
  132. */
  133. const normalizedString = string
  134. .replace(/(?<![-a-zA-Z0-9/])([-a-zA-Z0-9/]+):/gu, '"$1":')
  135. .replace(/([\]0-9])\s+(?=")/u, "$1,");
  136. try {
  137. const items = JSON.parse(`{${normalizedString}}`);
  138. return {
  139. ok: true,
  140. config: items,
  141. };
  142. } catch (ex) {
  143. const errorMessage = ex instanceof Error ? ex.message : String(ex);
  144. return {
  145. ok: false,
  146. error: {
  147. message: `Failed to parse JSON from '${normalizedString}': ${errorMessage}`,
  148. },
  149. };
  150. }
  151. }
  152. /**
  153. * Parses a config of values separated by comma.
  154. * @param {string} string The string to parse.
  155. * @returns {BooleanConfig} Result map of values and true values
  156. */
  157. parseListConfig(string) {
  158. const items = /** @type {BooleanConfig} */ ({});
  159. string.split(",").forEach(name => {
  160. const trimmedName = name
  161. .trim()
  162. .replace(
  163. /^(?<quote>['"]?)(?<ruleId>.*)\k<quote>$/su,
  164. "$<ruleId>",
  165. );
  166. if (trimmedName) {
  167. items[trimmedName] = true;
  168. }
  169. });
  170. return items;
  171. }
  172. /**
  173. * Extract the directive and the justification from a given directive comment and trim them.
  174. * @param {string} value The comment text to extract.
  175. * @returns {{directivePart: string, justificationPart: string}} The extracted directive and justification.
  176. */
  177. #extractDirectiveComment(value) {
  178. const match = /\s-{2,}\s/u.exec(value);
  179. if (!match) {
  180. return { directivePart: value.trim(), justificationPart: "" };
  181. }
  182. const directive = value.slice(0, match.index).trim();
  183. const justification = value.slice(match.index + match[0].length).trim();
  184. return { directivePart: directive, justificationPart: justification };
  185. }
  186. /**
  187. * Parses a directive comment into directive text and value.
  188. * @param {string} string The string with the directive to be parsed.
  189. * @returns {DirectiveComment|undefined} The parsed directive or `undefined` if the directive is invalid.
  190. */
  191. parseDirective(string) {
  192. const { directivePart, justificationPart } =
  193. this.#extractDirectiveComment(string);
  194. const match = directivesPattern.exec(directivePart);
  195. if (!match) {
  196. return undefined;
  197. }
  198. const directiveText = match[1];
  199. const directiveValue = directivePart.slice(
  200. match.index + directiveText.length,
  201. );
  202. return new DirectiveComment(
  203. directiveText,
  204. directiveValue.trim(),
  205. justificationPart,
  206. );
  207. }
  208. }
  209. /**
  210. * @fileoverview A collection of helper classes for implementing `SourceCode`.
  211. * @author Nicholas C. Zakas
  212. */
  213. /* eslint class-methods-use-this: off -- Required to complete interface. */
  214. //-----------------------------------------------------------------------------
  215. // Type Definitions
  216. //-----------------------------------------------------------------------------
  217. /** @typedef {$eslintcore.VisitTraversalStep} VisitTraversalStep */
  218. /** @typedef {$eslintcore.CallTraversalStep} CallTraversalStep */
  219. /** @typedef {$eslintcore.TraversalStep} TraversalStep */
  220. /** @typedef {$eslintcore.SourceLocation} SourceLocation */
  221. /** @typedef {$eslintcore.SourceLocationWithOffset} SourceLocationWithOffset */
  222. /** @typedef {$eslintcore.SourceRange} SourceRange */
  223. /** @typedef {$eslintcore.Directive} IDirective */
  224. /** @typedef {$eslintcore.DirectiveType} DirectiveType */
  225. /** @typedef {$eslintcore.SourceCodeBaseTypeOptions} SourceCodeBaseTypeOptions */
  226. /**
  227. * @typedef {import("@eslint/core").TextSourceCode<Options>} TextSourceCode<Options>
  228. * @template {SourceCodeBaseTypeOptions} [Options=SourceCodeBaseTypeOptions]
  229. */
  230. //-----------------------------------------------------------------------------
  231. // Helpers
  232. //-----------------------------------------------------------------------------
  233. /**
  234. * Determines if a node has ESTree-style loc information.
  235. * @param {object} node The node to check.
  236. * @returns {node is {loc:SourceLocation}} `true` if the node has ESTree-style loc information, `false` if not.
  237. */
  238. function hasESTreeStyleLoc(node) {
  239. return "loc" in node;
  240. }
  241. /**
  242. * Determines if a node has position-style loc information.
  243. * @param {object} node The node to check.
  244. * @returns {node is {position:SourceLocation}} `true` if the node has position-style range information, `false` if not.
  245. */
  246. function hasPosStyleLoc(node) {
  247. return "position" in node;
  248. }
  249. /**
  250. * Determines if a node has ESTree-style range information.
  251. * @param {object} node The node to check.
  252. * @returns {node is {range:SourceRange}} `true` if the node has ESTree-style range information, `false` if not.
  253. */
  254. function hasESTreeStyleRange(node) {
  255. return "range" in node;
  256. }
  257. /**
  258. * Determines if a node has position-style range information.
  259. * @param {object} node The node to check.
  260. * @returns {node is {position:SourceLocationWithOffset}} `true` if the node has position-style range information, `false` if not.
  261. */
  262. function hasPosStyleRange(node) {
  263. return "position" in node;
  264. }
  265. /**
  266. * Performs binary search to find the line number containing a given target index.
  267. * Returns the lower bound - the index of the first element greater than the target.
  268. * **Please note that the `lineStartIndices` should be sorted in ascending order**.
  269. * - Time Complexity: O(log n) - Significantly faster than linear search for large files.
  270. * @param {number[]} lineStartIndices Sorted array of line start indices.
  271. * @param {number} targetIndex The target index to find the line number for.
  272. * @returns {number} The line number for the target index.
  273. */
  274. function findLineNumberBinarySearch(lineStartIndices, targetIndex) {
  275. let low = 0;
  276. let high = lineStartIndices.length - 1;
  277. while (low < high) {
  278. const mid = ((low + high) / 2) | 0; // Use bitwise OR to floor the division.
  279. if (targetIndex < lineStartIndices[mid]) {
  280. high = mid;
  281. } else {
  282. low = mid + 1;
  283. }
  284. }
  285. return low;
  286. }
  287. //-----------------------------------------------------------------------------
  288. // Exports
  289. //-----------------------------------------------------------------------------
  290. /**
  291. * A class to represent a step in the traversal process where a node is visited.
  292. * @implements {VisitTraversalStep}
  293. */
  294. class VisitNodeStep {
  295. /**
  296. * The type of the step.
  297. * @type {"visit"}
  298. * @readonly
  299. */
  300. type = "visit";
  301. /**
  302. * The kind of the step. Represents the same data as the `type` property
  303. * but it's a number for performance.
  304. * @type {1}
  305. * @readonly
  306. */
  307. kind = 1;
  308. /**
  309. * The target of the step.
  310. * @type {object}
  311. */
  312. target;
  313. /**
  314. * The phase of the step.
  315. * @type {1|2}
  316. */
  317. phase;
  318. /**
  319. * The arguments of the step.
  320. * @type {Array<any>}
  321. */
  322. args;
  323. /**
  324. * Creates a new instance.
  325. * @param {Object} options The options for the step.
  326. * @param {object} options.target The target of the step.
  327. * @param {1|2} options.phase The phase of the step.
  328. * @param {Array<any>} options.args The arguments of the step.
  329. */
  330. constructor({ target, phase, args }) {
  331. this.target = target;
  332. this.phase = phase;
  333. this.args = args;
  334. }
  335. }
  336. /**
  337. * A class to represent a step in the traversal process where a
  338. * method is called.
  339. * @implements {CallTraversalStep}
  340. */
  341. class CallMethodStep {
  342. /**
  343. * The type of the step.
  344. * @type {"call"}
  345. * @readonly
  346. */
  347. type = "call";
  348. /**
  349. * The kind of the step. Represents the same data as the `type` property
  350. * but it's a number for performance.
  351. * @type {2}
  352. * @readonly
  353. */
  354. kind = 2;
  355. /**
  356. * The name of the method to call.
  357. * @type {string}
  358. */
  359. target;
  360. /**
  361. * The arguments to pass to the method.
  362. * @type {Array<any>}
  363. */
  364. args;
  365. /**
  366. * Creates a new instance.
  367. * @param {Object} options The options for the step.
  368. * @param {string} options.target The target of the step.
  369. * @param {Array<any>} options.args The arguments of the step.
  370. */
  371. constructor({ target, args }) {
  372. this.target = target;
  373. this.args = args;
  374. }
  375. }
  376. /**
  377. * A class to represent a directive comment.
  378. * @implements {IDirective}
  379. */
  380. class Directive {
  381. /**
  382. * The type of directive.
  383. * @type {DirectiveType}
  384. * @readonly
  385. */
  386. type;
  387. /**
  388. * The node representing the directive.
  389. * @type {unknown}
  390. * @readonly
  391. */
  392. node;
  393. /**
  394. * Everything after the "eslint-disable" portion of the directive,
  395. * but before the "--" that indicates the justification.
  396. * @type {string}
  397. * @readonly
  398. */
  399. value;
  400. /**
  401. * The justification for the directive.
  402. * @type {string}
  403. * @readonly
  404. */
  405. justification;
  406. /**
  407. * Creates a new instance.
  408. * @param {Object} options The options for the directive.
  409. * @param {"disable"|"enable"|"disable-next-line"|"disable-line"} options.type The type of directive.
  410. * @param {unknown} options.node The node representing the directive.
  411. * @param {string} options.value The value of the directive.
  412. * @param {string} options.justification The justification for the directive.
  413. */
  414. constructor({ type, node, value, justification }) {
  415. this.type = type;
  416. this.node = node;
  417. this.value = value;
  418. this.justification = justification;
  419. }
  420. }
  421. /**
  422. * Source Code Base Object
  423. * @template {SourceCodeBaseTypeOptions & {RootNode: object, SyntaxElementWithLoc: object}} [Options=SourceCodeBaseTypeOptions & {RootNode: object, SyntaxElementWithLoc: object}]
  424. * @implements {TextSourceCode<Options>}
  425. */
  426. class TextSourceCodeBase {
  427. /**
  428. * The lines of text in the source code.
  429. * @type {Array<string>}
  430. */
  431. #lines = [];
  432. /**
  433. * The indices of the start of each line in the source code.
  434. * @type {Array<number>}
  435. */
  436. #lineStartIndices = [0];
  437. /**
  438. * The pattern to match lineEndings in the source code.
  439. * @type {RegExp}
  440. */
  441. #lineEndingPattern;
  442. /**
  443. * The AST of the source code.
  444. * @type {Options['RootNode']}
  445. */
  446. ast;
  447. /**
  448. * The text of the source code.
  449. * @type {string}
  450. */
  451. text;
  452. /**
  453. * Creates a new instance.
  454. * @param {Object} options The options for the instance.
  455. * @param {string} options.text The source code text.
  456. * @param {Options['RootNode']} options.ast The root AST node.
  457. * @param {RegExp} [options.lineEndingPattern] The pattern to match lineEndings in the source code. Defaults to `/\r?\n/u`.
  458. */
  459. constructor({ text, ast, lineEndingPattern = /\r?\n/u }) {
  460. this.ast = ast;
  461. this.text = text;
  462. // Remove the global(`g`) and sticky(`y`) flags from the `lineEndingPattern` to avoid issues with lastIndex.
  463. this.#lineEndingPattern = new RegExp(
  464. lineEndingPattern.source,
  465. lineEndingPattern.flags.replace(/[gy]/gu, ""),
  466. );
  467. }
  468. /**
  469. * Finds the next line in the source text and updates `#lines` and `#lineStartIndices`.
  470. * @param {string} text The text to search for the next line.
  471. * @returns {boolean} `true` if a next line was found, `false` otherwise.
  472. */
  473. #findNextLine(text) {
  474. const match = this.#lineEndingPattern.exec(text);
  475. if (!match) {
  476. return false;
  477. }
  478. this.#lines.push(text.slice(0, match.index));
  479. this.#lineStartIndices.push(
  480. (this.#lineStartIndices.at(-1) ?? 0) +
  481. match.index +
  482. match[0].length,
  483. );
  484. return true;
  485. }
  486. /**
  487. * Ensures `#lines` is lazily calculated from the source text.
  488. * @returns {void}
  489. */
  490. #ensureLines() {
  491. // If `#lines` has already been calculated, do nothing.
  492. if (this.#lines.length === this.#lineStartIndices.length) {
  493. return;
  494. }
  495. while (
  496. this.#findNextLine(this.text.slice(this.#lineStartIndices.at(-1)))
  497. ) {
  498. // Continue parsing until no more matches are found.
  499. }
  500. this.#lines.push(this.text.slice(this.#lineStartIndices.at(-1)));
  501. Object.freeze(this.#lines);
  502. }
  503. /**
  504. * Ensures `#lineStartIndices` is lazily calculated up to the specified index.
  505. * @param {number} index The index of a character in a file.
  506. * @returns {void}
  507. */
  508. #ensureLineStartIndicesFromIndex(index) {
  509. // If we've already parsed up to or beyond this index, do nothing.
  510. if (index <= (this.#lineStartIndices.at(-1) ?? 0)) {
  511. return;
  512. }
  513. while (
  514. index > (this.#lineStartIndices.at(-1) ?? 0) &&
  515. this.#findNextLine(this.text.slice(this.#lineStartIndices.at(-1)))
  516. ) {
  517. // Continue parsing until no more matches are found.
  518. }
  519. }
  520. /**
  521. * Ensures `#lineStartIndices` is lazily calculated up to the specified loc.
  522. * @param {Object} loc A line/column location.
  523. * @param {number} loc.line The line number of the location. (0 or 1-indexed based on language.)
  524. * @param {number} lineStart The line number at which the parser starts counting.
  525. * @returns {void}
  526. */
  527. #ensureLineStartIndicesFromLoc(loc, lineStart) {
  528. // Calculate line indices up to the potentially next line, as it is needed for the follow‑up calculation.
  529. const nextLocLineIndex = loc.line - lineStart + 1;
  530. const lastCalculatedLineIndex = this.#lineStartIndices.length - 1;
  531. let additionalLinesNeeded = nextLocLineIndex - lastCalculatedLineIndex;
  532. // If we've already parsed up to or beyond this line, do nothing.
  533. if (additionalLinesNeeded <= 0) {
  534. return;
  535. }
  536. while (
  537. additionalLinesNeeded > 0 &&
  538. this.#findNextLine(this.text.slice(this.#lineStartIndices.at(-1)))
  539. ) {
  540. // Continue parsing until no more matches are found or we have enough lines.
  541. additionalLinesNeeded -= 1;
  542. }
  543. }
  544. /**
  545. * Returns the loc information for the given node or token.
  546. * @param {Options['SyntaxElementWithLoc']} nodeOrToken The node or token to get the loc information for.
  547. * @returns {SourceLocation} The loc information for the node or token.
  548. * @throws {Error} If the node or token does not have loc information.
  549. */
  550. getLoc(nodeOrToken) {
  551. if (hasESTreeStyleLoc(nodeOrToken)) {
  552. return nodeOrToken.loc;
  553. }
  554. if (hasPosStyleLoc(nodeOrToken)) {
  555. return nodeOrToken.position;
  556. }
  557. throw new Error(
  558. "Custom getLoc() method must be implemented in the subclass.",
  559. );
  560. }
  561. /**
  562. * Converts a source text index into a `{ line: number, column: number }` pair.
  563. * @param {number} index The index of a character in a file.
  564. * @throws {TypeError|RangeError} If non-numeric index or index out of range.
  565. * @returns {{line: number, column: number}} A `{ line: number, column: number }` location object with 0 or 1-indexed line and 0 or 1-indexed column based on language.
  566. * @public
  567. */
  568. getLocFromIndex(index) {
  569. if (typeof index !== "number") {
  570. throw new TypeError("Expected `index` to be a number.");
  571. }
  572. if (index < 0 || index > this.text.length) {
  573. throw new RangeError(
  574. `Index out of range (requested index ${index}, but source text has length ${this.text.length}).`,
  575. );
  576. }
  577. const {
  578. start: { line: lineStart, column: columnStart },
  579. end: { line: lineEnd, column: columnEnd },
  580. } = this.getLoc(this.ast);
  581. // If the index is at the start, return the start location of the root node.
  582. if (index === 0) {
  583. return {
  584. line: lineStart,
  585. column: columnStart,
  586. };
  587. }
  588. // If the index is `this.text.length`, return the location one "spot" past the last character of the file.
  589. if (index === this.text.length) {
  590. return {
  591. line: lineEnd,
  592. column: columnEnd,
  593. };
  594. }
  595. // Ensure `#lineStartIndices` are lazily calculated.
  596. this.#ensureLineStartIndicesFromIndex(index);
  597. /*
  598. * To figure out which line `index` is on, determine the last place at which index could
  599. * be inserted into `#lineStartIndices` to keep the list sorted.
  600. */
  601. const lineNumber =
  602. (index >= (this.#lineStartIndices.at(-1) ?? 0)
  603. ? this.#lineStartIndices.length
  604. : findLineNumberBinarySearch(this.#lineStartIndices, index)) -
  605. 1 +
  606. lineStart;
  607. return {
  608. line: lineNumber,
  609. column:
  610. index -
  611. this.#lineStartIndices[lineNumber - lineStart] +
  612. columnStart,
  613. };
  614. }
  615. /**
  616. * Converts a `{ line: number, column: number }` pair into a source text index.
  617. * @param {Object} loc A line/column location.
  618. * @param {number} loc.line The line number of the location. (0 or 1-indexed based on language.)
  619. * @param {number} loc.column The column number of the location. (0 or 1-indexed based on language.)
  620. * @throws {TypeError|RangeError} If `loc` is not an object with a numeric
  621. * `line` and `column`, if the `line` is less than or equal to zero or
  622. * the `line` or `column` is out of the expected range.
  623. * @returns {number} The index of the line/column location in a file.
  624. * @public
  625. */
  626. getIndexFromLoc(loc) {
  627. if (
  628. loc === null ||
  629. typeof loc !== "object" ||
  630. typeof loc.line !== "number" ||
  631. typeof loc.column !== "number"
  632. ) {
  633. throw new TypeError(
  634. "Expected `loc` to be an object with numeric `line` and `column` properties.",
  635. );
  636. }
  637. const {
  638. start: { line: lineStart, column: columnStart },
  639. end: { line: lineEnd, column: columnEnd },
  640. } = this.getLoc(this.ast);
  641. if (loc.line < lineStart || lineEnd < loc.line) {
  642. throw new RangeError(
  643. `Line number out of range (line ${loc.line} requested). Valid range: ${lineStart}-${lineEnd}`,
  644. );
  645. }
  646. // If the loc is at the start, return the start index of the root node.
  647. if (loc.line === lineStart && loc.column === columnStart) {
  648. return 0;
  649. }
  650. // If the loc is at the end, return the index one "spot" past the last character of the file.
  651. if (loc.line === lineEnd && loc.column === columnEnd) {
  652. return this.text.length;
  653. }
  654. // Ensure `#lineStartIndices` are lazily calculated.
  655. this.#ensureLineStartIndicesFromLoc(loc, lineStart);
  656. const isLastLine = loc.line === lineEnd;
  657. const lineStartIndex = this.#lineStartIndices[loc.line - lineStart];
  658. const lineEndIndex = isLastLine
  659. ? this.text.length
  660. : this.#lineStartIndices[loc.line - lineStart + 1];
  661. const positionIndex = lineStartIndex + loc.column - columnStart;
  662. if (
  663. loc.column < columnStart ||
  664. (isLastLine && positionIndex > lineEndIndex) ||
  665. (!isLastLine && positionIndex >= lineEndIndex)
  666. ) {
  667. throw new RangeError(
  668. `Column number out of range (column ${loc.column} requested). Valid range for line ${loc.line}: ${columnStart}-${lineEndIndex - lineStartIndex + columnStart + (isLastLine ? 0 : -1)}`,
  669. );
  670. }
  671. return positionIndex;
  672. }
  673. /**
  674. * Returns the range information for the given node or token.
  675. * @param {Options['SyntaxElementWithLoc']} nodeOrToken The node or token to get the range information for.
  676. * @returns {SourceRange} The range information for the node or token.
  677. * @throws {Error} If the node or token does not have range information.
  678. */
  679. getRange(nodeOrToken) {
  680. if (hasESTreeStyleRange(nodeOrToken)) {
  681. return nodeOrToken.range;
  682. }
  683. if (hasPosStyleRange(nodeOrToken)) {
  684. return [
  685. nodeOrToken.position.start.offset,
  686. nodeOrToken.position.end.offset,
  687. ];
  688. }
  689. throw new Error(
  690. "Custom getRange() method must be implemented in the subclass.",
  691. );
  692. }
  693. /* eslint-disable no-unused-vars -- Required to complete interface. */
  694. /**
  695. * Returns the parent of the given node.
  696. * @param {Options['SyntaxElementWithLoc']} node The node to get the parent of.
  697. * @returns {Options['SyntaxElementWithLoc']|undefined} The parent of the node.
  698. * @throws {Error} If the method is not implemented in the subclass.
  699. */
  700. getParent(node) {
  701. throw new Error("Not implemented.");
  702. }
  703. /* eslint-enable no-unused-vars -- Required to complete interface. */
  704. /**
  705. * Gets all the ancestors of a given node
  706. * @param {Options['SyntaxElementWithLoc']} node The node
  707. * @returns {Array<Options['SyntaxElementWithLoc']>} All the ancestor nodes in the AST, not including the provided node, starting
  708. * from the root node at index 0 and going inwards to the parent node.
  709. * @throws {TypeError} When `node` is missing.
  710. */
  711. getAncestors(node) {
  712. if (!node) {
  713. throw new TypeError("Missing required argument: node.");
  714. }
  715. const ancestorsStartingAtParent = [];
  716. for (
  717. let ancestor = this.getParent(node);
  718. ancestor;
  719. ancestor = this.getParent(ancestor)
  720. ) {
  721. ancestorsStartingAtParent.push(ancestor);
  722. }
  723. return ancestorsStartingAtParent.reverse();
  724. }
  725. /**
  726. * Gets the source code for the given node.
  727. * @param {Options['SyntaxElementWithLoc']} [node] The AST node to get the text for.
  728. * @param {number} [beforeCount] The number of characters before the node to retrieve.
  729. * @param {number} [afterCount] The number of characters after the node to retrieve.
  730. * @returns {string} The text representing the AST node.
  731. * @public
  732. */
  733. getText(node, beforeCount, afterCount) {
  734. if (node) {
  735. const range = this.getRange(node);
  736. return this.text.slice(
  737. Math.max(range[0] - (beforeCount || 0), 0),
  738. range[1] + (afterCount || 0),
  739. );
  740. }
  741. return this.text;
  742. }
  743. /**
  744. * Gets the entire source text split into an array of lines.
  745. * @returns {Array<string>} The source text as an array of lines.
  746. * @public
  747. */
  748. get lines() {
  749. this.#ensureLines(); // Ensure `#lines` is lazily calculated.
  750. return this.#lines;
  751. }
  752. /**
  753. * Traverse the source code and return the steps that were taken.
  754. * @returns {Iterable<TraversalStep>} The steps that were taken while traversing the source code.
  755. */
  756. traverse() {
  757. throw new Error("Not implemented.");
  758. }
  759. }
  760. export { CallMethodStep, ConfigCommentParser, Directive, TextSourceCodeBase, VisitNodeStep };