source: imaps-frontend/node_modules/eslint/lib/source-code/source-code.js

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

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