index.js 16 KB

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