source-code.js 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055
  1. /**
  2. * @fileoverview Abstraction of JavaScript source code.
  3. * @author Nicholas C. Zakas
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const
  10. { isCommentToken } = require("@eslint-community/eslint-utils"),
  11. TokenStore = require("./token-store"),
  12. astUtils = require("../shared/ast-utils"),
  13. Traverser = require("../shared/traverser"),
  14. globals = require("../../conf/globals"),
  15. {
  16. directivesPattern
  17. } = require("../shared/directives"),
  18. /* eslint-disable-next-line n/no-restricted-require -- Too messy to figure out right now. */
  19. ConfigCommentParser = require("../linter/config-comment-parser"),
  20. eslintScope = require("eslint-scope");
  21. //------------------------------------------------------------------------------
  22. // Type Definitions
  23. //------------------------------------------------------------------------------
  24. /** @typedef {import("eslint-scope").Variable} Variable */
  25. //------------------------------------------------------------------------------
  26. // Private
  27. //------------------------------------------------------------------------------
  28. const commentParser = new ConfigCommentParser();
  29. /**
  30. * Validates that the given AST has the required information.
  31. * @param {ASTNode} ast The Program node of the AST to check.
  32. * @throws {Error} If the AST doesn't contain the correct information.
  33. * @returns {void}
  34. * @private
  35. */
  36. function validate(ast) {
  37. if (!ast.tokens) {
  38. throw new Error("AST is missing the tokens array.");
  39. }
  40. if (!ast.comments) {
  41. throw new Error("AST is missing the comments array.");
  42. }
  43. if (!ast.loc) {
  44. throw new Error("AST is missing location information.");
  45. }
  46. if (!ast.range) {
  47. throw new Error("AST is missing range information");
  48. }
  49. }
  50. /**
  51. * Retrieves globals for the given ecmaVersion.
  52. * @param {number} ecmaVersion The version to retrieve globals for.
  53. * @returns {Object} The globals for the given ecmaVersion.
  54. */
  55. function getGlobalsForEcmaVersion(ecmaVersion) {
  56. switch (ecmaVersion) {
  57. case 3:
  58. return globals.es3;
  59. case 5:
  60. return globals.es5;
  61. default:
  62. if (ecmaVersion < 2015) {
  63. return globals[`es${ecmaVersion + 2009}`];
  64. }
  65. return globals[`es${ecmaVersion}`];
  66. }
  67. }
  68. /**
  69. * Check to see if its a ES6 export declaration.
  70. * @param {ASTNode} astNode An AST node.
  71. * @returns {boolean} whether the given node represents an export declaration.
  72. * @private
  73. */
  74. function looksLikeExport(astNode) {
  75. return astNode.type === "ExportDefaultDeclaration" || astNode.type === "ExportNamedDeclaration" ||
  76. astNode.type === "ExportAllDeclaration" || astNode.type === "ExportSpecifier";
  77. }
  78. /**
  79. * Merges two sorted lists into a larger sorted list in O(n) time.
  80. * @param {Token[]} tokens The list of tokens.
  81. * @param {Token[]} comments The list of comments.
  82. * @returns {Token[]} A sorted list of tokens and comments.
  83. * @private
  84. */
  85. function sortedMerge(tokens, comments) {
  86. const result = [];
  87. let tokenIndex = 0;
  88. let commentIndex = 0;
  89. while (tokenIndex < tokens.length || commentIndex < comments.length) {
  90. if (commentIndex >= comments.length || tokenIndex < tokens.length && tokens[tokenIndex].range[0] < comments[commentIndex].range[0]) {
  91. result.push(tokens[tokenIndex++]);
  92. } else {
  93. result.push(comments[commentIndex++]);
  94. }
  95. }
  96. return result;
  97. }
  98. /**
  99. * Normalizes a value for a global in a config
  100. * @param {(boolean|string|null)} configuredValue The value given for a global in configuration or in
  101. * a global directive comment
  102. * @returns {("readable"|"writeable"|"off")} The value normalized as a string
  103. * @throws Error if global value is invalid
  104. */
  105. function normalizeConfigGlobal(configuredValue) {
  106. switch (configuredValue) {
  107. case "off":
  108. return "off";
  109. case true:
  110. case "true":
  111. case "writeable":
  112. case "writable":
  113. return "writable";
  114. case null:
  115. case false:
  116. case "false":
  117. case "readable":
  118. case "readonly":
  119. return "readonly";
  120. default:
  121. throw new Error(`'${configuredValue}' is not a valid configuration for a global (use 'readonly', 'writable', or 'off')`);
  122. }
  123. }
  124. /**
  125. * Determines if two nodes or tokens overlap.
  126. * @param {ASTNode|Token} first The first node or token to check.
  127. * @param {ASTNode|Token} second The second node or token to check.
  128. * @returns {boolean} True if the two nodes or tokens overlap.
  129. * @private
  130. */
  131. function nodesOrTokensOverlap(first, second) {
  132. return (first.range[0] <= second.range[0] && first.range[1] >= second.range[0]) ||
  133. (second.range[0] <= first.range[0] && second.range[1] >= first.range[0]);
  134. }
  135. /**
  136. * Determines if two nodes or tokens have at least one whitespace character
  137. * between them. Order does not matter. Returns false if the given nodes or
  138. * tokens overlap.
  139. * @param {SourceCode} sourceCode The source code object.
  140. * @param {ASTNode|Token} first The first node or token to check between.
  141. * @param {ASTNode|Token} second The second node or token to check between.
  142. * @param {boolean} checkInsideOfJSXText If `true` is present, check inside of JSXText tokens for backward compatibility.
  143. * @returns {boolean} True if there is a whitespace character between
  144. * any of the tokens found between the two given nodes or tokens.
  145. * @public
  146. */
  147. function isSpaceBetween(sourceCode, first, second, checkInsideOfJSXText) {
  148. if (nodesOrTokensOverlap(first, second)) {
  149. return false;
  150. }
  151. const [startingNodeOrToken, endingNodeOrToken] = first.range[1] <= second.range[0]
  152. ? [first, second]
  153. : [second, first];
  154. const firstToken = sourceCode.getLastToken(startingNodeOrToken) || startingNodeOrToken;
  155. const finalToken = sourceCode.getFirstToken(endingNodeOrToken) || endingNodeOrToken;
  156. let currentToken = firstToken;
  157. while (currentToken !== finalToken) {
  158. const nextToken = sourceCode.getTokenAfter(currentToken, { includeComments: true });
  159. if (
  160. currentToken.range[1] !== nextToken.range[0] ||
  161. /*
  162. * For backward compatibility, check spaces in JSXText.
  163. * https://github.com/eslint/eslint/issues/12614
  164. */
  165. (
  166. checkInsideOfJSXText &&
  167. nextToken !== finalToken &&
  168. nextToken.type === "JSXText" &&
  169. /\s/u.test(nextToken.value)
  170. )
  171. ) {
  172. return true;
  173. }
  174. currentToken = nextToken;
  175. }
  176. return false;
  177. }
  178. //-----------------------------------------------------------------------------
  179. // Directive Comments
  180. //-----------------------------------------------------------------------------
  181. /**
  182. * Ensures that variables representing built-in properties of the Global Object,
  183. * and any globals declared by special block comments, are present in the global
  184. * scope.
  185. * @param {Scope} globalScope The global scope.
  186. * @param {Object|undefined} configGlobals The globals declared in configuration
  187. * @param {Object|undefined} inlineGlobals The globals declared in the source code
  188. * @returns {void}
  189. */
  190. function addDeclaredGlobals(globalScope, configGlobals = {}, inlineGlobals = {}) {
  191. // Define configured global variables.
  192. for (const id of new Set([...Object.keys(configGlobals), ...Object.keys(inlineGlobals)])) {
  193. /*
  194. * `normalizeConfigGlobal` will throw an error if a configured global value is invalid. However, these errors would
  195. * typically be caught when validating a config anyway (validity for inline global comments is checked separately).
  196. */
  197. const configValue = configGlobals[id] === void 0 ? void 0 : normalizeConfigGlobal(configGlobals[id]);
  198. const commentValue = inlineGlobals[id] && inlineGlobals[id].value;
  199. const value = commentValue || configValue;
  200. const sourceComments = inlineGlobals[id] && inlineGlobals[id].comments;
  201. if (value === "off") {
  202. continue;
  203. }
  204. let variable = globalScope.set.get(id);
  205. if (!variable) {
  206. variable = new eslintScope.Variable(id, globalScope);
  207. globalScope.variables.push(variable);
  208. globalScope.set.set(id, variable);
  209. }
  210. variable.eslintImplicitGlobalSetting = configValue;
  211. variable.eslintExplicitGlobal = sourceComments !== void 0;
  212. variable.eslintExplicitGlobalComments = sourceComments;
  213. variable.writeable = (value === "writable");
  214. }
  215. /*
  216. * "through" contains all references which definitions cannot be found.
  217. * Since we augment the global scope using configuration, we need to update
  218. * references and remove the ones that were added by configuration.
  219. */
  220. globalScope.through = globalScope.through.filter(reference => {
  221. const name = reference.identifier.name;
  222. const variable = globalScope.set.get(name);
  223. if (variable) {
  224. /*
  225. * Links the variable and the reference.
  226. * And this reference is removed from `Scope#through`.
  227. */
  228. reference.resolved = variable;
  229. variable.references.push(reference);
  230. return false;
  231. }
  232. return true;
  233. });
  234. }
  235. /**
  236. * Sets the given variable names as exported so they won't be triggered by
  237. * the `no-unused-vars` rule.
  238. * @param {eslint.Scope} globalScope The global scope to define exports in.
  239. * @param {Record<string,string>} variables An object whose keys are the variable
  240. * names to export.
  241. * @returns {void}
  242. */
  243. function markExportedVariables(globalScope, variables) {
  244. Object.keys(variables).forEach(name => {
  245. const variable = globalScope.set.get(name);
  246. if (variable) {
  247. variable.eslintUsed = true;
  248. variable.eslintExported = true;
  249. }
  250. });
  251. }
  252. //------------------------------------------------------------------------------
  253. // Public Interface
  254. //------------------------------------------------------------------------------
  255. const caches = Symbol("caches");
  256. /**
  257. * Represents parsed source code.
  258. */
  259. class SourceCode extends TokenStore {
  260. /**
  261. * @param {string|Object} textOrConfig The source code text or config object.
  262. * @param {string} textOrConfig.text The source code text.
  263. * @param {ASTNode} textOrConfig.ast The Program node of the AST representing the code. This AST should be created from the text that BOM was stripped.
  264. * @param {Object|null} textOrConfig.parserServices The parser services.
  265. * @param {ScopeManager|null} textOrConfig.scopeManager The scope of this source code.
  266. * @param {Object|null} textOrConfig.visitorKeys The visitor keys to traverse AST.
  267. * @param {ASTNode} [astIfNoConfig] The Program node of the AST representing the code. This AST should be created from the text that BOM was stripped.
  268. */
  269. constructor(textOrConfig, astIfNoConfig) {
  270. let text, ast, parserServices, scopeManager, visitorKeys;
  271. // Process overloading.
  272. if (typeof textOrConfig === "string") {
  273. text = textOrConfig;
  274. ast = astIfNoConfig;
  275. } else if (typeof textOrConfig === "object" && textOrConfig !== null) {
  276. text = textOrConfig.text;
  277. ast = textOrConfig.ast;
  278. parserServices = textOrConfig.parserServices;
  279. scopeManager = textOrConfig.scopeManager;
  280. visitorKeys = textOrConfig.visitorKeys;
  281. }
  282. validate(ast);
  283. super(ast.tokens, ast.comments);
  284. /**
  285. * General purpose caching for the class.
  286. */
  287. this[caches] = new Map([
  288. ["scopes", new WeakMap()],
  289. ["vars", new Map()],
  290. ["configNodes", void 0]
  291. ]);
  292. /**
  293. * The flag to indicate that the source code has Unicode BOM.
  294. * @type {boolean}
  295. */
  296. this.hasBOM = (text.charCodeAt(0) === 0xFEFF);
  297. /**
  298. * The original text source code.
  299. * BOM was stripped from this text.
  300. * @type {string}
  301. */
  302. this.text = (this.hasBOM ? text.slice(1) : text);
  303. /**
  304. * The parsed AST for the source code.
  305. * @type {ASTNode}
  306. */
  307. this.ast = ast;
  308. /**
  309. * The parser services of this source code.
  310. * @type {Object}
  311. */
  312. this.parserServices = parserServices || {};
  313. /**
  314. * The scope of this source code.
  315. * @type {ScopeManager|null}
  316. */
  317. this.scopeManager = scopeManager || null;
  318. /**
  319. * The visitor keys to traverse AST.
  320. * @type {Object}
  321. */
  322. this.visitorKeys = visitorKeys || Traverser.DEFAULT_VISITOR_KEYS;
  323. // Check the source text for the presence of a shebang since it is parsed as a standard line comment.
  324. const shebangMatched = this.text.match(astUtils.shebangPattern);
  325. const hasShebang = shebangMatched && ast.comments.length && ast.comments[0].value === shebangMatched[1];
  326. if (hasShebang) {
  327. ast.comments[0].type = "Shebang";
  328. }
  329. this.tokensAndComments = sortedMerge(ast.tokens, ast.comments);
  330. /**
  331. * The source code split into lines according to ECMA-262 specification.
  332. * This is done to avoid each rule needing to do so separately.
  333. * @type {string[]}
  334. */
  335. this.lines = [];
  336. this.lineStartIndices = [0];
  337. const lineEndingPattern = astUtils.createGlobalLinebreakMatcher();
  338. let match;
  339. /*
  340. * Previously, this was implemented using a regex that
  341. * matched a sequence of non-linebreak characters followed by a
  342. * linebreak, then adding the lengths of the matches. However,
  343. * this caused a catastrophic backtracking issue when the end
  344. * of a file contained a large number of non-newline characters.
  345. * To avoid this, the current implementation just matches newlines
  346. * and uses match.index to get the correct line start indices.
  347. */
  348. while ((match = lineEndingPattern.exec(this.text))) {
  349. this.lines.push(this.text.slice(this.lineStartIndices[this.lineStartIndices.length - 1], match.index));
  350. this.lineStartIndices.push(match.index + match[0].length);
  351. }
  352. this.lines.push(this.text.slice(this.lineStartIndices[this.lineStartIndices.length - 1]));
  353. // Cache for comments found using getComments().
  354. this._commentCache = new WeakMap();
  355. // don't allow further modification of this object
  356. Object.freeze(this);
  357. Object.freeze(this.lines);
  358. }
  359. /**
  360. * Split the source code into multiple lines based on the line delimiters.
  361. * @param {string} text Source code as a string.
  362. * @returns {string[]} Array of source code lines.
  363. * @public
  364. */
  365. static splitLines(text) {
  366. return text.split(astUtils.createGlobalLinebreakMatcher());
  367. }
  368. /**
  369. * Gets the source code for the given node.
  370. * @param {ASTNode} [node] The AST node to get the text for.
  371. * @param {int} [beforeCount] The number of characters before the node to retrieve.
  372. * @param {int} [afterCount] The number of characters after the node to retrieve.
  373. * @returns {string} The text representing the AST node.
  374. * @public
  375. */
  376. getText(node, beforeCount, afterCount) {
  377. if (node) {
  378. return this.text.slice(Math.max(node.range[0] - (beforeCount || 0), 0),
  379. node.range[1] + (afterCount || 0));
  380. }
  381. return this.text;
  382. }
  383. /**
  384. * Gets the entire source text split into an array of lines.
  385. * @returns {Array} The source text as an array of lines.
  386. * @public
  387. */
  388. getLines() {
  389. return this.lines;
  390. }
  391. /**
  392. * Retrieves an array containing all comments in the source code.
  393. * @returns {ASTNode[]} An array of comment nodes.
  394. * @public
  395. */
  396. getAllComments() {
  397. return this.ast.comments;
  398. }
  399. /**
  400. * Gets all comments for the given node.
  401. * @param {ASTNode} node The AST node to get the comments for.
  402. * @returns {Object} An object containing a leading and trailing array
  403. * of comments indexed by their position.
  404. * @public
  405. * @deprecated replaced by getCommentsBefore(), getCommentsAfter(), and getCommentsInside().
  406. */
  407. getComments(node) {
  408. if (this._commentCache.has(node)) {
  409. return this._commentCache.get(node);
  410. }
  411. const comments = {
  412. leading: [],
  413. trailing: []
  414. };
  415. /*
  416. * Return all comments as leading comments of the Program node when
  417. * there is no executable code.
  418. */
  419. if (node.type === "Program") {
  420. if (node.body.length === 0) {
  421. comments.leading = node.comments;
  422. }
  423. } else {
  424. /*
  425. * Return comments as trailing comments of nodes that only contain
  426. * comments (to mimic the comment attachment behavior present in Espree).
  427. */
  428. if ((node.type === "BlockStatement" || node.type === "ClassBody") && node.body.length === 0 ||
  429. node.type === "ObjectExpression" && node.properties.length === 0 ||
  430. node.type === "ArrayExpression" && node.elements.length === 0 ||
  431. node.type === "SwitchStatement" && node.cases.length === 0
  432. ) {
  433. comments.trailing = this.getTokens(node, {
  434. includeComments: true,
  435. filter: isCommentToken
  436. });
  437. }
  438. /*
  439. * Iterate over tokens before and after node and collect comment tokens.
  440. * Do not include comments that exist outside of the parent node
  441. * to avoid duplication.
  442. */
  443. let currentToken = this.getTokenBefore(node, { includeComments: true });
  444. while (currentToken && isCommentToken(currentToken)) {
  445. if (node.parent && node.parent.type !== "Program" && (currentToken.start < node.parent.start)) {
  446. break;
  447. }
  448. comments.leading.push(currentToken);
  449. currentToken = this.getTokenBefore(currentToken, { includeComments: true });
  450. }
  451. comments.leading.reverse();
  452. currentToken = this.getTokenAfter(node, { includeComments: true });
  453. while (currentToken && isCommentToken(currentToken)) {
  454. if (node.parent && node.parent.type !== "Program" && (currentToken.end > node.parent.end)) {
  455. break;
  456. }
  457. comments.trailing.push(currentToken);
  458. currentToken = this.getTokenAfter(currentToken, { includeComments: true });
  459. }
  460. }
  461. this._commentCache.set(node, comments);
  462. return comments;
  463. }
  464. /**
  465. * Retrieves the JSDoc comment for a given node.
  466. * @param {ASTNode} node The AST node to get the comment for.
  467. * @returns {Token|null} The Block comment token containing the JSDoc comment
  468. * for the given node or null if not found.
  469. * @public
  470. * @deprecated
  471. */
  472. getJSDocComment(node) {
  473. /**
  474. * Checks for the presence of a JSDoc comment for the given node and returns it.
  475. * @param {ASTNode} astNode The AST node to get the comment for.
  476. * @returns {Token|null} The Block comment token containing the JSDoc comment
  477. * for the given node or null if not found.
  478. * @private
  479. */
  480. const findJSDocComment = astNode => {
  481. const tokenBefore = this.getTokenBefore(astNode, { includeComments: true });
  482. if (
  483. tokenBefore &&
  484. isCommentToken(tokenBefore) &&
  485. tokenBefore.type === "Block" &&
  486. tokenBefore.value.charAt(0) === "*" &&
  487. astNode.loc.start.line - tokenBefore.loc.end.line <= 1
  488. ) {
  489. return tokenBefore;
  490. }
  491. return null;
  492. };
  493. let parent = node.parent;
  494. switch (node.type) {
  495. case "ClassDeclaration":
  496. case "FunctionDeclaration":
  497. return findJSDocComment(looksLikeExport(parent) ? parent : node);
  498. case "ClassExpression":
  499. return findJSDocComment(parent.parent);
  500. case "ArrowFunctionExpression":
  501. case "FunctionExpression":
  502. if (parent.type !== "CallExpression" && parent.type !== "NewExpression") {
  503. while (
  504. !this.getCommentsBefore(parent).length &&
  505. !/Function/u.test(parent.type) &&
  506. parent.type !== "MethodDefinition" &&
  507. parent.type !== "Property"
  508. ) {
  509. parent = parent.parent;
  510. if (!parent) {
  511. break;
  512. }
  513. }
  514. if (parent && parent.type !== "FunctionDeclaration" && parent.type !== "Program") {
  515. return findJSDocComment(parent);
  516. }
  517. }
  518. return findJSDocComment(node);
  519. // falls through
  520. default:
  521. return null;
  522. }
  523. }
  524. /**
  525. * Gets the deepest node containing a range index.
  526. * @param {int} index Range index of the desired node.
  527. * @returns {ASTNode} The node if found or null if not found.
  528. * @public
  529. */
  530. getNodeByRangeIndex(index) {
  531. let result = null;
  532. Traverser.traverse(this.ast, {
  533. visitorKeys: this.visitorKeys,
  534. enter(node) {
  535. if (node.range[0] <= index && index < node.range[1]) {
  536. result = node;
  537. } else {
  538. this.skip();
  539. }
  540. },
  541. leave(node) {
  542. if (node === result) {
  543. this.break();
  544. }
  545. }
  546. });
  547. return result;
  548. }
  549. /**
  550. * Determines if two nodes or tokens have at least one whitespace character
  551. * between them. Order does not matter. Returns false if the given nodes or
  552. * tokens overlap.
  553. * @param {ASTNode|Token} first The first node or token to check between.
  554. * @param {ASTNode|Token} second The second node or token to check between.
  555. * @returns {boolean} True if there is a whitespace character between
  556. * any of the tokens found between the two given nodes or tokens.
  557. * @public
  558. */
  559. isSpaceBetween(first, second) {
  560. return isSpaceBetween(this, first, second, false);
  561. }
  562. /**
  563. * Determines if two nodes or tokens have at least one whitespace character
  564. * between them. Order does not matter. Returns false if the given nodes or
  565. * tokens overlap.
  566. * For backward compatibility, this method returns true if there are
  567. * `JSXText` tokens that contain whitespaces between the two.
  568. * @param {ASTNode|Token} first The first node or token to check between.
  569. * @param {ASTNode|Token} second The second node or token to check between.
  570. * @returns {boolean} True if there is a whitespace character between
  571. * any of the tokens found between the two given nodes or tokens.
  572. * @deprecated in favor of isSpaceBetween().
  573. * @public
  574. */
  575. isSpaceBetweenTokens(first, second) {
  576. return isSpaceBetween(this, first, second, true);
  577. }
  578. /**
  579. * Converts a source text index into a (line, column) pair.
  580. * @param {number} index The index of a character in a file
  581. * @throws {TypeError} If non-numeric index or index out of range.
  582. * @returns {Object} A {line, column} location object with a 0-indexed column
  583. * @public
  584. */
  585. getLocFromIndex(index) {
  586. if (typeof index !== "number") {
  587. throw new TypeError("Expected `index` to be a number.");
  588. }
  589. if (index < 0 || index > this.text.length) {
  590. throw new RangeError(`Index out of range (requested index ${index}, but source text has length ${this.text.length}).`);
  591. }
  592. /*
  593. * For an argument of this.text.length, return the location one "spot" past the last character
  594. * of the file. If the last character is a linebreak, the location will be column 0 of the next
  595. * line; otherwise, the location will be in the next column on the same line.
  596. *
  597. * See getIndexFromLoc for the motivation for this special case.
  598. */
  599. if (index === this.text.length) {
  600. return { line: this.lines.length, column: this.lines[this.lines.length - 1].length };
  601. }
  602. /*
  603. * To figure out which line index is on, determine the last place at which index could
  604. * be inserted into lineStartIndices to keep the list sorted.
  605. */
  606. const lineNumber = index >= this.lineStartIndices[this.lineStartIndices.length - 1]
  607. ? this.lineStartIndices.length
  608. : this.lineStartIndices.findIndex(el => index < el);
  609. return { line: lineNumber, column: index - this.lineStartIndices[lineNumber - 1] };
  610. }
  611. /**
  612. * Converts a (line, column) pair into a range index.
  613. * @param {Object} loc A line/column location
  614. * @param {number} loc.line The line number of the location (1-indexed)
  615. * @param {number} loc.column The column number of the location (0-indexed)
  616. * @throws {TypeError|RangeError} If `loc` is not an object with a numeric
  617. * `line` and `column`, if the `line` is less than or equal to zero or
  618. * the line or column is out of the expected range.
  619. * @returns {number} The range index of the location in the file.
  620. * @public
  621. */
  622. getIndexFromLoc(loc) {
  623. if (typeof loc !== "object" || typeof loc.line !== "number" || typeof loc.column !== "number") {
  624. throw new TypeError("Expected `loc` to be an object with numeric `line` and `column` properties.");
  625. }
  626. if (loc.line <= 0) {
  627. throw new RangeError(`Line number out of range (line ${loc.line} requested). Line numbers should be 1-based.`);
  628. }
  629. if (loc.line > this.lineStartIndices.length) {
  630. throw new RangeError(`Line number out of range (line ${loc.line} requested, but only ${this.lineStartIndices.length} lines present).`);
  631. }
  632. const lineStartIndex = this.lineStartIndices[loc.line - 1];
  633. const lineEndIndex = loc.line === this.lineStartIndices.length ? this.text.length : this.lineStartIndices[loc.line];
  634. const positionIndex = lineStartIndex + loc.column;
  635. /*
  636. * By design, getIndexFromLoc({ line: lineNum, column: 0 }) should return the start index of
  637. * the given line, provided that the line number is valid element of this.lines. Since the
  638. * last element of this.lines is an empty string for files with trailing newlines, add a
  639. * special case where getting the index for the first location after the end of the file
  640. * will return the length of the file, rather than throwing an error. This allows rules to
  641. * use getIndexFromLoc consistently without worrying about edge cases at the end of a file.
  642. */
  643. if (
  644. loc.line === this.lineStartIndices.length && positionIndex > lineEndIndex ||
  645. loc.line < this.lineStartIndices.length && positionIndex >= lineEndIndex
  646. ) {
  647. throw new RangeError(`Column number out of range (column ${loc.column} requested, but the length of line ${loc.line} is ${lineEndIndex - lineStartIndex}).`);
  648. }
  649. return positionIndex;
  650. }
  651. /**
  652. * Gets the scope for the given node
  653. * @param {ASTNode} currentNode The node to get the scope of
  654. * @returns {eslint-scope.Scope} The scope information for this node
  655. * @throws {TypeError} If the `currentNode` argument is missing.
  656. */
  657. getScope(currentNode) {
  658. if (!currentNode) {
  659. throw new TypeError("Missing required argument: node.");
  660. }
  661. // check cache first
  662. const cache = this[caches].get("scopes");
  663. const cachedScope = cache.get(currentNode);
  664. if (cachedScope) {
  665. return cachedScope;
  666. }
  667. // On Program node, get the outermost scope to avoid return Node.js special function scope or ES modules scope.
  668. const inner = currentNode.type !== "Program";
  669. for (let node = currentNode; node; node = node.parent) {
  670. const scope = this.scopeManager.acquire(node, inner);
  671. if (scope) {
  672. if (scope.type === "function-expression-name") {
  673. cache.set(currentNode, scope.childScopes[0]);
  674. return scope.childScopes[0];
  675. }
  676. cache.set(currentNode, scope);
  677. return scope;
  678. }
  679. }
  680. cache.set(currentNode, this.scopeManager.scopes[0]);
  681. return this.scopeManager.scopes[0];
  682. }
  683. /**
  684. * Get the variables that `node` defines.
  685. * This is a convenience method that passes through
  686. * to the same method on the `scopeManager`.
  687. * @param {ASTNode} node The node for which the variables are obtained.
  688. * @returns {Array<Variable>} An array of variable nodes representing
  689. * the variables that `node` defines.
  690. */
  691. getDeclaredVariables(node) {
  692. return this.scopeManager.getDeclaredVariables(node);
  693. }
  694. /* eslint-disable class-methods-use-this -- node is owned by SourceCode */
  695. /**
  696. * Gets all the ancestors of a given node
  697. * @param {ASTNode} node The node
  698. * @returns {Array<ASTNode>} All the ancestor nodes in the AST, not including the provided node, starting
  699. * from the root node at index 0 and going inwards to the parent node.
  700. * @throws {TypeError} When `node` is missing.
  701. */
  702. getAncestors(node) {
  703. if (!node) {
  704. throw new TypeError("Missing required argument: node.");
  705. }
  706. const ancestorsStartingAtParent = [];
  707. for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
  708. ancestorsStartingAtParent.push(ancestor);
  709. }
  710. return ancestorsStartingAtParent.reverse();
  711. }
  712. /* eslint-enable class-methods-use-this -- node is owned by SourceCode */
  713. /**
  714. * Marks a variable as used in the current scope
  715. * @param {string} name The name of the variable to mark as used.
  716. * @param {ASTNode} [refNode] The closest node to the variable reference.
  717. * @returns {boolean} True if the variable was found and marked as used, false if not.
  718. */
  719. markVariableAsUsed(name, refNode = this.ast) {
  720. const currentScope = this.getScope(refNode);
  721. let initialScope = currentScope;
  722. /*
  723. * When we are in an ESM or CommonJS module, we need to start searching
  724. * from the top-level scope, not the global scope. For ESM the top-level
  725. * scope is the module scope; for CommonJS the top-level scope is the
  726. * outer function scope.
  727. *
  728. * Without this check, we might miss a variable declared with `var` at
  729. * the top-level because it won't exist in the global scope.
  730. */
  731. if (
  732. currentScope.type === "global" &&
  733. currentScope.childScopes.length > 0 &&
  734. // top-level scopes refer to a `Program` node
  735. currentScope.childScopes[0].block === this.ast
  736. ) {
  737. initialScope = currentScope.childScopes[0];
  738. }
  739. for (let scope = initialScope; scope; scope = scope.upper) {
  740. const variable = scope.variables.find(scopeVar => scopeVar.name === name);
  741. if (variable) {
  742. variable.eslintUsed = true;
  743. return true;
  744. }
  745. }
  746. return false;
  747. }
  748. /**
  749. * Returns an array of all inline configuration nodes found in the
  750. * source code.
  751. * @returns {Array<Token>} An array of all inline configuration nodes.
  752. */
  753. getInlineConfigNodes() {
  754. // check the cache first
  755. let configNodes = this[caches].get("configNodes");
  756. if (configNodes) {
  757. return configNodes;
  758. }
  759. // calculate fresh config nodes
  760. configNodes = this.ast.comments.filter(comment => {
  761. // shebang comments are never directives
  762. if (comment.type === "Shebang") {
  763. return false;
  764. }
  765. const { directivePart } = commentParser.extractDirectiveComment(comment.value);
  766. const directiveMatch = directivesPattern.exec(directivePart);
  767. if (!directiveMatch) {
  768. return false;
  769. }
  770. // only certain comment types are supported as line comments
  771. return comment.type !== "Line" || !!/^eslint-disable-(next-)?line$/u.test(directiveMatch[1]);
  772. });
  773. this[caches].set("configNodes", configNodes);
  774. return configNodes;
  775. }
  776. /**
  777. * Applies language options sent in from the core.
  778. * @param {Object} languageOptions The language options for this run.
  779. * @returns {void}
  780. */
  781. applyLanguageOptions(languageOptions) {
  782. /*
  783. * Add configured globals and language globals
  784. *
  785. * Using Object.assign instead of object spread for performance reasons
  786. * https://github.com/eslint/eslint/issues/16302
  787. */
  788. const configGlobals = Object.assign(
  789. {},
  790. getGlobalsForEcmaVersion(languageOptions.ecmaVersion),
  791. languageOptions.sourceType === "commonjs" ? globals.commonjs : void 0,
  792. languageOptions.globals
  793. );
  794. const varsCache = this[caches].get("vars");
  795. varsCache.set("configGlobals", configGlobals);
  796. }
  797. /**
  798. * Applies configuration found inside of the source code. This method is only
  799. * called when ESLint is running with inline configuration allowed.
  800. * @returns {{problems:Array<Problem>,configs:{config:FlatConfigArray,node:ASTNode}}} Information
  801. * that ESLint needs to further process the inline configuration.
  802. */
  803. applyInlineConfig() {
  804. const problems = [];
  805. const configs = [];
  806. const exportedVariables = {};
  807. const inlineGlobals = Object.create(null);
  808. this.getInlineConfigNodes().forEach(comment => {
  809. const { directiveText, directiveValue } = commentParser.parseDirective(comment);
  810. switch (directiveText) {
  811. case "exported":
  812. Object.assign(exportedVariables, commentParser.parseStringConfig(directiveValue, comment));
  813. break;
  814. case "globals":
  815. case "global":
  816. for (const [id, { value }] of Object.entries(commentParser.parseStringConfig(directiveValue, comment))) {
  817. let normalizedValue;
  818. try {
  819. normalizedValue = normalizeConfigGlobal(value);
  820. } catch (err) {
  821. problems.push({
  822. ruleId: null,
  823. loc: comment.loc,
  824. message: err.message
  825. });
  826. continue;
  827. }
  828. if (inlineGlobals[id]) {
  829. inlineGlobals[id].comments.push(comment);
  830. inlineGlobals[id].value = normalizedValue;
  831. } else {
  832. inlineGlobals[id] = {
  833. comments: [comment],
  834. value: normalizedValue
  835. };
  836. }
  837. }
  838. break;
  839. case "eslint": {
  840. const parseResult = commentParser.parseJsonConfig(directiveValue, comment.loc);
  841. if (parseResult.success) {
  842. configs.push({
  843. config: {
  844. rules: parseResult.config
  845. },
  846. node: comment
  847. });
  848. } else {
  849. problems.push(parseResult.error);
  850. }
  851. break;
  852. }
  853. // no default
  854. }
  855. });
  856. // save all the new variables for later
  857. const varsCache = this[caches].get("vars");
  858. varsCache.set("inlineGlobals", inlineGlobals);
  859. varsCache.set("exportedVariables", exportedVariables);
  860. return {
  861. configs,
  862. problems
  863. };
  864. }
  865. /**
  866. * Called by ESLint core to indicate that it has finished providing
  867. * information. We now add in all the missing variables and ensure that
  868. * state-changing methods cannot be called by rules.
  869. * @returns {void}
  870. */
  871. finalize() {
  872. // Step 1: ensure that all of the necessary variables are up to date
  873. const varsCache = this[caches].get("vars");
  874. const globalScope = this.scopeManager.scopes[0];
  875. const configGlobals = varsCache.get("configGlobals");
  876. const inlineGlobals = varsCache.get("inlineGlobals");
  877. const exportedVariables = varsCache.get("exportedVariables");
  878. addDeclaredGlobals(globalScope, configGlobals, inlineGlobals);
  879. if (exportedVariables) {
  880. markExportedVariables(globalScope, exportedVariables);
  881. }
  882. }
  883. }
  884. module.exports = SourceCode;