index.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  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. /** @typedef {import("@eslint/core").RuleConfig} RuleConfig */
  11. /** @typedef {import("@eslint/core").RulesConfig} RulesConfig */
  12. /** @typedef {import("./types.ts").StringConfig} StringConfig */
  13. /** @typedef {import("./types.ts").BooleanConfig} BooleanConfig */
  14. //-----------------------------------------------------------------------------
  15. // Helpers
  16. //-----------------------------------------------------------------------------
  17. const directivesPattern = /^([a-z]+(?:-[a-z]+)*)(?:\s|$)/u;
  18. const validSeverities = new Set([0, 1, 2, "off", "warn", "error"]);
  19. /**
  20. * Determines if the severity in the rule configuration is valid.
  21. * @param {RuleConfig} ruleConfig A rule's configuration.
  22. */
  23. function isSeverityValid(ruleConfig) {
  24. const severity = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;
  25. return validSeverities.has(severity);
  26. }
  27. /**
  28. * Determines if all severities in the rules configuration are valid.
  29. * @param {RulesConfig} rulesConfig The rules configuration to check.
  30. * @returns {boolean} `true` if all severities are valid, otherwise `false`.
  31. */
  32. function isEverySeverityValid(rulesConfig) {
  33. return Object.values(rulesConfig).every(isSeverityValid);
  34. }
  35. /**
  36. * Represents a directive comment.
  37. */
  38. class DirectiveComment {
  39. /**
  40. * The label of the directive, such as "eslint", "eslint-disable", etc.
  41. * @type {string}
  42. */
  43. label = "";
  44. /**
  45. * The value of the directive (the string after the label).
  46. * @type {string}
  47. */
  48. value = "";
  49. /**
  50. * The justification of the directive (the string after the --).
  51. * @type {string}
  52. */
  53. justification = "";
  54. /**
  55. * Creates a new directive comment.
  56. * @param {string} label The label of the directive.
  57. * @param {string} value The value of the directive.
  58. * @param {string} justification The justification of the directive.
  59. */
  60. constructor(label, value, justification) {
  61. this.label = label;
  62. this.value = value;
  63. this.justification = justification;
  64. }
  65. }
  66. //------------------------------------------------------------------------------
  67. // Public Interface
  68. //------------------------------------------------------------------------------
  69. /**
  70. * Object to parse ESLint configuration comments.
  71. */
  72. class ConfigCommentParser {
  73. /**
  74. * Parses a list of "name:string_value" or/and "name" options divided by comma or
  75. * whitespace. Used for "global" comments.
  76. * @param {string} string The string to parse.
  77. * @returns {StringConfig} Result map object of names and string values, or null values if no value was provided.
  78. */
  79. parseStringConfig(string) {
  80. const items = /** @type {StringConfig} */ ({});
  81. // Collapse whitespace around `:` and `,` to make parsing easier
  82. const trimmedString = string.replace(/\s*([:,])\s*/gu, "$1");
  83. trimmedString.split(/\s|,+/u).forEach(name => {
  84. if (!name) {
  85. return;
  86. }
  87. // value defaults to null (if not provided), e.g: "foo" => ["foo", null]
  88. const [key, value = null] = name.split(":");
  89. items[key] = value;
  90. });
  91. return items;
  92. }
  93. /**
  94. * Parses a JSON-like config.
  95. * @param {string} string The string to parse.
  96. * @returns {({ok: true, config: RulesConfig}|{ok: false, error: {message: string}})} Result map object
  97. */
  98. parseJSONLikeConfig(string) {
  99. // Parses a JSON-like comment by the same way as parsing CLI option.
  100. try {
  101. const items = levn.parse("Object", string) || {};
  102. /*
  103. * When the configuration has any invalid severities, it should be completely
  104. * ignored. This is because the configuration is not valid and should not be
  105. * applied.
  106. *
  107. * For example, the following configuration is invalid:
  108. *
  109. * "no-alert: 2 no-console: 2"
  110. *
  111. * This results in a configuration of { "no-alert": "2 no-console: 2" }, which is
  112. * not valid. In this case, the configuration should be ignored.
  113. */
  114. if (isEverySeverityValid(items)) {
  115. return {
  116. ok: true,
  117. config: items,
  118. };
  119. }
  120. } catch {
  121. // levn parsing error: ignore to parse the string by a fallback.
  122. }
  123. /*
  124. * Optionator cannot parse commaless notations.
  125. * But we are supporting that. So this is a fallback for that.
  126. */
  127. const normalizedString = string
  128. .replace(/([-a-zA-Z0-9/]+):/gu, '"$1":')
  129. .replace(/(\]|[0-9])\s+(?=")/u, "$1,");
  130. try {
  131. const items = JSON.parse(`{${normalizedString}}`);
  132. return {
  133. ok: true,
  134. config: items,
  135. };
  136. } catch (ex) {
  137. const errorMessage = ex instanceof Error ? ex.message : String(ex);
  138. return {
  139. ok: false,
  140. error: {
  141. message: `Failed to parse JSON from '${normalizedString}': ${errorMessage}`,
  142. },
  143. };
  144. }
  145. }
  146. /**
  147. * Parses a config of values separated by comma.
  148. * @param {string} string The string to parse.
  149. * @returns {BooleanConfig} Result map of values and true values
  150. */
  151. parseListConfig(string) {
  152. const items = /** @type {BooleanConfig} */ ({});
  153. string.split(",").forEach(name => {
  154. const trimmedName = name
  155. .trim()
  156. .replace(
  157. /^(?<quote>['"]?)(?<ruleId>.*)\k<quote>$/su,
  158. "$<ruleId>",
  159. );
  160. if (trimmedName) {
  161. items[trimmedName] = true;
  162. }
  163. });
  164. return items;
  165. }
  166. /**
  167. * Extract the directive and the justification from a given directive comment and trim them.
  168. * @param {string} value The comment text to extract.
  169. * @returns {{directivePart: string, justificationPart: string}} The extracted directive and justification.
  170. */
  171. #extractDirectiveComment(value) {
  172. const match = /\s-{2,}\s/u.exec(value);
  173. if (!match) {
  174. return { directivePart: value.trim(), justificationPart: "" };
  175. }
  176. const directive = value.slice(0, match.index).trim();
  177. const justification = value.slice(match.index + match[0].length).trim();
  178. return { directivePart: directive, justificationPart: justification };
  179. }
  180. /**
  181. * Parses a directive comment into directive text and value.
  182. * @param {string} string The string with the directive to be parsed.
  183. * @returns {DirectiveComment|undefined} The parsed directive or `undefined` if the directive is invalid.
  184. */
  185. parseDirective(string) {
  186. const { directivePart, justificationPart } =
  187. this.#extractDirectiveComment(string);
  188. const match = directivesPattern.exec(directivePart);
  189. if (!match) {
  190. return undefined;
  191. }
  192. const directiveText = match[1];
  193. const directiveValue = directivePart.slice(
  194. match.index + directiveText.length,
  195. );
  196. return new DirectiveComment(
  197. directiveText,
  198. directiveValue.trim(),
  199. justificationPart,
  200. );
  201. }
  202. }
  203. /**
  204. * @fileoverview A collection of helper classes for implementing `SourceCode`.
  205. * @author Nicholas C. Zakas
  206. */
  207. /* eslint class-methods-use-this: off -- Required to complete interface. */
  208. //-----------------------------------------------------------------------------
  209. // Type Definitions
  210. //-----------------------------------------------------------------------------
  211. /** @typedef {import("@eslint/core").VisitTraversalStep} VisitTraversalStep */
  212. /** @typedef {import("@eslint/core").CallTraversalStep} CallTraversalStep */
  213. /** @typedef {import("@eslint/core").TextSourceCode} TextSourceCode */
  214. /** @typedef {import("@eslint/core").TraversalStep} TraversalStep */
  215. /** @typedef {import("@eslint/core").SourceLocation} SourceLocation */
  216. /** @typedef {import("@eslint/core").SourceLocationWithOffset} SourceLocationWithOffset */
  217. /** @typedef {import("@eslint/core").SourceRange} SourceRange */
  218. /** @typedef {import("@eslint/core").Directive} IDirective */
  219. /** @typedef {import("@eslint/core").DirectiveType} DirectiveType */
  220. //-----------------------------------------------------------------------------
  221. // Helpers
  222. //-----------------------------------------------------------------------------
  223. /**
  224. * Determines if a node has ESTree-style loc information.
  225. * @param {object} node The node to check.
  226. * @returns {node is {loc:SourceLocation}} `true` if the node has ESTree-style loc information, `false` if not.
  227. */
  228. function hasESTreeStyleLoc(node) {
  229. return "loc" in node;
  230. }
  231. /**
  232. * Determines if a node has position-style loc information.
  233. * @param {object} node The node to check.
  234. * @returns {node is {position:SourceLocation}} `true` if the node has position-style range information, `false` if not.
  235. */
  236. function hasPosStyleLoc(node) {
  237. return "position" in node;
  238. }
  239. /**
  240. * Determines if a node has ESTree-style range information.
  241. * @param {object} node The node to check.
  242. * @returns {node is {range:SourceRange}} `true` if the node has ESTree-style range information, `false` if not.
  243. */
  244. function hasESTreeStyleRange(node) {
  245. return "range" in node;
  246. }
  247. /**
  248. * Determines if a node has position-style range information.
  249. * @param {object} node The node to check.
  250. * @returns {node is {position:SourceLocationWithOffset}} `true` if the node has position-style range information, `false` if not.
  251. */
  252. function hasPosStyleRange(node) {
  253. return "position" in node;
  254. }
  255. //-----------------------------------------------------------------------------
  256. // Exports
  257. //-----------------------------------------------------------------------------
  258. /**
  259. * A class to represent a step in the traversal process where a node is visited.
  260. * @implements {VisitTraversalStep}
  261. */
  262. class VisitNodeStep {
  263. /**
  264. * The type of the step.
  265. * @type {"visit"}
  266. * @readonly
  267. */
  268. type = "visit";
  269. /**
  270. * The kind of the step. Represents the same data as the `type` property
  271. * but it's a number for performance.
  272. * @type {1}
  273. * @readonly
  274. */
  275. kind = 1;
  276. /**
  277. * The target of the step.
  278. * @type {object}
  279. */
  280. target;
  281. /**
  282. * The phase of the step.
  283. * @type {1|2}
  284. */
  285. phase;
  286. /**
  287. * The arguments of the step.
  288. * @type {Array<any>}
  289. */
  290. args;
  291. /**
  292. * Creates a new instance.
  293. * @param {Object} options The options for the step.
  294. * @param {object} options.target The target of the step.
  295. * @param {1|2} options.phase The phase of the step.
  296. * @param {Array<any>} options.args The arguments of the step.
  297. */
  298. constructor({ target, phase, args }) {
  299. this.target = target;
  300. this.phase = phase;
  301. this.args = args;
  302. }
  303. }
  304. /**
  305. * A class to represent a step in the traversal process where a
  306. * method is called.
  307. * @implements {CallTraversalStep}
  308. */
  309. class CallMethodStep {
  310. /**
  311. * The type of the step.
  312. * @type {"call"}
  313. * @readonly
  314. */
  315. type = "call";
  316. /**
  317. * The kind of the step. Represents the same data as the `type` property
  318. * but it's a number for performance.
  319. * @type {2}
  320. * @readonly
  321. */
  322. kind = 2;
  323. /**
  324. * The name of the method to call.
  325. * @type {string}
  326. */
  327. target;
  328. /**
  329. * The arguments to pass to the method.
  330. * @type {Array<any>}
  331. */
  332. args;
  333. /**
  334. * Creates a new instance.
  335. * @param {Object} options The options for the step.
  336. * @param {string} options.target The target of the step.
  337. * @param {Array<any>} options.args The arguments of the step.
  338. */
  339. constructor({ target, args }) {
  340. this.target = target;
  341. this.args = args;
  342. }
  343. }
  344. /**
  345. * A class to represent a directive comment.
  346. * @implements {IDirective}
  347. */
  348. class Directive {
  349. /**
  350. * The type of directive.
  351. * @type {DirectiveType}
  352. * @readonly
  353. */
  354. type;
  355. /**
  356. * The node representing the directive.
  357. * @type {unknown}
  358. * @readonly
  359. */
  360. node;
  361. /**
  362. * Everything after the "eslint-disable" portion of the directive,
  363. * but before the "--" that indicates the justification.
  364. * @type {string}
  365. * @readonly
  366. */
  367. value;
  368. /**
  369. * The justification for the directive.
  370. * @type {string}
  371. * @readonly
  372. */
  373. justification;
  374. /**
  375. * Creates a new instance.
  376. * @param {Object} options The options for the directive.
  377. * @param {"disable"|"enable"|"disable-next-line"|"disable-line"} options.type The type of directive.
  378. * @param {unknown} options.node The node representing the directive.
  379. * @param {string} options.value The value of the directive.
  380. * @param {string} options.justification The justification for the directive.
  381. */
  382. constructor({ type, node, value, justification }) {
  383. this.type = type;
  384. this.node = node;
  385. this.value = value;
  386. this.justification = justification;
  387. }
  388. }
  389. /**
  390. * Source Code Base Object
  391. * @implements {TextSourceCode}
  392. */
  393. class TextSourceCodeBase {
  394. /**
  395. * The lines of text in the source code.
  396. * @type {Array<string>}
  397. */
  398. #lines;
  399. /**
  400. * The AST of the source code.
  401. * @type {object}
  402. */
  403. ast;
  404. /**
  405. * The text of the source code.
  406. * @type {string}
  407. */
  408. text;
  409. /**
  410. * Creates a new instance.
  411. * @param {Object} options The options for the instance.
  412. * @param {string} options.text The source code text.
  413. * @param {object} options.ast The root AST node.
  414. * @param {RegExp} [options.lineEndingPattern] The pattern to match lineEndings in the source code.
  415. */
  416. constructor({ text, ast, lineEndingPattern = /\r?\n/u }) {
  417. this.ast = ast;
  418. this.text = text;
  419. this.#lines = text.split(lineEndingPattern);
  420. }
  421. /**
  422. * Returns the loc information for the given node or token.
  423. * @param {object} nodeOrToken The node or token to get the loc information for.
  424. * @returns {SourceLocation} The loc information for the node or token.
  425. */
  426. getLoc(nodeOrToken) {
  427. if (hasESTreeStyleLoc(nodeOrToken)) {
  428. return nodeOrToken.loc;
  429. }
  430. if (hasPosStyleLoc(nodeOrToken)) {
  431. return nodeOrToken.position;
  432. }
  433. throw new Error(
  434. "Custom getLoc() method must be implemented in the subclass.",
  435. );
  436. }
  437. /**
  438. * Returns the range information for the given node or token.
  439. * @param {object} nodeOrToken The node or token to get the range information for.
  440. * @returns {SourceRange} The range information for the node or token.
  441. */
  442. getRange(nodeOrToken) {
  443. if (hasESTreeStyleRange(nodeOrToken)) {
  444. return nodeOrToken.range;
  445. }
  446. if (hasPosStyleRange(nodeOrToken)) {
  447. return [
  448. nodeOrToken.position.start.offset,
  449. nodeOrToken.position.end.offset,
  450. ];
  451. }
  452. throw new Error(
  453. "Custom getRange() method must be implemented in the subclass.",
  454. );
  455. }
  456. /* eslint-disable no-unused-vars -- Required to complete interface. */
  457. /**
  458. * Returns the parent of the given node.
  459. * @param {object} node The node to get the parent of.
  460. * @returns {object|undefined} The parent of the node.
  461. */
  462. getParent(node) {
  463. throw new Error("Not implemented.");
  464. }
  465. /* eslint-enable no-unused-vars -- Required to complete interface. */
  466. /**
  467. * Gets all the ancestors of a given node
  468. * @param {object} node The node
  469. * @returns {Array<object>} All the ancestor nodes in the AST, not including the provided node, starting
  470. * from the root node at index 0 and going inwards to the parent node.
  471. * @throws {TypeError} When `node` is missing.
  472. */
  473. getAncestors(node) {
  474. if (!node) {
  475. throw new TypeError("Missing required argument: node.");
  476. }
  477. const ancestorsStartingAtParent = [];
  478. for (
  479. let ancestor = this.getParent(node);
  480. ancestor;
  481. ancestor = this.getParent(ancestor)
  482. ) {
  483. ancestorsStartingAtParent.push(ancestor);
  484. }
  485. return ancestorsStartingAtParent.reverse();
  486. }
  487. /**
  488. * Gets the source code for the given node.
  489. * @param {object} [node] The AST node to get the text for.
  490. * @param {number} [beforeCount] The number of characters before the node to retrieve.
  491. * @param {number} [afterCount] The number of characters after the node to retrieve.
  492. * @returns {string} The text representing the AST node.
  493. * @public
  494. */
  495. getText(node, beforeCount, afterCount) {
  496. if (node) {
  497. const range = this.getRange(node);
  498. return this.text.slice(
  499. Math.max(range[0] - (beforeCount || 0), 0),
  500. range[1] + (afterCount || 0),
  501. );
  502. }
  503. return this.text;
  504. }
  505. /**
  506. * Gets the entire source text split into an array of lines.
  507. * @returns {Array<string>} The source text as an array of lines.
  508. * @public
  509. */
  510. get lines() {
  511. return this.#lines;
  512. }
  513. /**
  514. * Traverse the source code and return the steps that were taken.
  515. * @returns {Iterable<TraversalStep>} The steps that were taken while traversing the source code.
  516. */
  517. traverse() {
  518. throw new Error("Not implemented.");
  519. }
  520. }
  521. export { CallMethodStep, ConfigCommentParser, Directive, TextSourceCodeBase, VisitNodeStep };