]>
git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/source-code/source-code.js
c13ce29b877a4aa0f83647c5d799408946aaf2c4
2 * @fileoverview Abstraction of JavaScript source code.
3 * @author Nicholas C. Zakas
7 //------------------------------------------------------------------------------
9 //------------------------------------------------------------------------------
12 { isCommentToken
} = require ( "eslint-utils" ),
13 TokenStore
= require ( "./token-store" ),
14 astUtils
= require ( "../shared/ast-utils" ),
15 Traverser
= require ( "../shared/traverser" );
17 //------------------------------------------------------------------------------
19 //------------------------------------------------------------------------------
22 * Validates that the given AST has the required information.
23 * @param {ASTNode} ast The Program node of the AST to check.
24 * @throws {Error} If the AST doesn't contain the correct information.
28 function validate ( ast
) {
30 throw new Error ( "AST is missing the tokens array." );
34 throw new Error ( "AST is missing the comments array." );
38 throw new Error ( "AST is missing location information." );
42 throw new Error ( "AST is missing range information" );
47 * Check to see if its a ES6 export declaration.
48 * @param {ASTNode} astNode An AST node.
49 * @returns {boolean} whether the given node represents an export declaration.
52 function looksLikeExport ( astNode
) {
53 return astNode
. type
=== "ExportDefaultDeclaration" || astNode
. type
=== "ExportNamedDeclaration" ||
54 astNode
. type
=== "ExportAllDeclaration" || astNode
. type
=== "ExportSpecifier" ;
58 * Merges two sorted lists into a larger sorted list in O(n) time.
59 * @param {Token[]} tokens The list of tokens.
60 * @param {Token[]} comments The list of comments.
61 * @returns {Token[]} A sorted list of tokens and comments.
64 function sortedMerge ( tokens
, comments
) {
69 while ( tokenIndex
< tokens
. length
|| commentIndex
< comments
. length
) {
70 if ( commentIndex
>= comments
. length
|| tokenIndex
< tokens
. length
&& tokens
[ tokenIndex
]. range
[ 0 ] < comments
[ commentIndex
]. range
[ 0 ]) {
71 result
. push ( tokens
[ tokenIndex
++]);
73 result
. push ( comments
[ commentIndex
++]);
81 * Determines if two nodes or tokens overlap.
82 * @param {ASTNode|Token} first The first node or token to check.
83 * @param {ASTNode|Token} second The second node or token to check.
84 * @returns {boolean} True if the two nodes or tokens overlap.
87 function nodesOrTokensOverlap ( first
, second
) {
88 return ( first
. range
[ 0 ] <= second
. range
[ 0 ] && first
. range
[ 1 ] >= second
. range
[ 0 ]) ||
89 ( second
. range
[ 0 ] <= first
. range
[ 0 ] && second
. range
[ 1 ] >= first
. range
[ 0 ]);
93 * Determines if two nodes or tokens have at least one whitespace character
94 * between them. Order does not matter. Returns false if the given nodes or
96 * @param {SourceCode} sourceCode The source code object.
97 * @param {ASTNode|Token} first The first node or token to check between.
98 * @param {ASTNode|Token} second The second node or token to check between.
99 * @param {boolean} checkInsideOfJSXText If `true` is present, check inside of JSXText tokens for backward compatibility.
100 * @returns {boolean} True if there is a whitespace character between
101 * any of the tokens found between the two given nodes or tokens.
104 function isSpaceBetween ( sourceCode
, first
, second
, checkInsideOfJSXText
) {
105 if ( nodesOrTokensOverlap ( first
, second
)) {
109 const [ startingNodeOrToken
, endingNodeOrToken
] = first
. range
[ 1 ] <= second
. range
[ 0 ]
112 const firstToken
= sourceCode
. getLastToken ( startingNodeOrToken
) || startingNodeOrToken
;
113 const finalToken
= sourceCode
. getFirstToken ( endingNodeOrToken
) || endingNodeOrToken
;
114 let currentToken
= firstToken
;
116 while ( currentToken
!== finalToken
) {
117 const nextToken
= sourceCode
. getTokenAfter ( currentToken
, { includeComments
: true });
120 currentToken
. range
[ 1 ] !== nextToken
. range
[ 0 ] ||
123 * For backward compatibility, check spaces in JSXText.
124 * https://github.com/eslint/eslint/issues/12614
127 checkInsideOfJSXText
&&
128 nextToken
!== finalToken
&&
129 nextToken
. type
=== "JSXText" &&
130 /\s/u . test ( nextToken
. value
)
136 currentToken
= nextToken
;
142 //------------------------------------------------------------------------------
144 //------------------------------------------------------------------------------
146 class SourceCode
extends TokenStore
{
149 * Represents parsed source code.
150 * @param {string|Object} textOrConfig The source code text or config object.
151 * @param {string} textOrConfig.text The source code text.
152 * @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.
153 * @param {Object|null} textOrConfig.parserServices The parser services.
154 * @param {ScopeManager|null} textOrConfig.scopeManager The scope of this source code.
155 * @param {Object|null} textOrConfig.visitorKeys The visitor keys to traverse AST.
156 * @param {ASTNode} [astIfNoConfig] The Program node of the AST representing the code. This AST should be created from the text that BOM was stripped.
158 constructor ( textOrConfig
, astIfNoConfig
) {
159 let text
, ast
, parserServices
, scopeManager
, visitorKeys
;
161 // Process overloading.
162 if ( typeof textOrConfig
=== "string" ) {
165 } else if ( typeof textOrConfig
=== "object" && textOrConfig
!== null ) {
166 text
= textOrConfig
. text
;
167 ast
= textOrConfig
. ast
;
168 parserServices
= textOrConfig
. parserServices
;
169 scopeManager
= textOrConfig
. scopeManager
;
170 visitorKeys
= textOrConfig
. visitorKeys
;
174 super ( ast
. tokens
, ast
. comments
);
177 * The flag to indicate that the source code has Unicode BOM.
180 this . hasBOM
= ( text
. charCodeAt ( 0 ) === 0xFEFF );
183 * The original text source code.
184 * BOM was stripped from this text.
187 this . text
= ( this . hasBOM
? text
. slice ( 1 ) : text
);
190 * The parsed AST for the source code.
196 * The parser services of this source code.
199 this . parserServices
= parserServices
|| {};
202 * The scope of this source code.
203 * @type {ScopeManager|null}
205 this . scopeManager
= scopeManager
|| null ;
208 * The visitor keys to traverse AST.
211 this . visitorKeys
= visitorKeys
|| Traverser
. DEFAULT_VISITOR_KEYS
;
213 // Check the source text for the presence of a shebang since it is parsed as a standard line comment.
214 const shebangMatched
= this . text
. match ( astUtils
. shebangPattern
);
215 const hasShebang
= shebangMatched
&& ast
. comments
. length
&& ast
. comments
[ 0 ]. value
=== shebangMatched
[ 1 ];
218 ast
. comments
[ 0 ]. type
= "Shebang" ;
221 this . tokensAndComments
= sortedMerge ( ast
. tokens
, ast
. comments
);
224 * The source code split into lines according to ECMA-262 specification.
225 * This is done to avoid each rule needing to do so separately.
229 this . lineStartIndices
= [ 0 ];
231 const lineEndingPattern
= astUtils
. createGlobalLinebreakMatcher ();
235 * Previously, this was implemented using a regex that
236 * matched a sequence of non-linebreak characters followed by a
237 * linebreak, then adding the lengths of the matches. However,
238 * this caused a catastrophic backtracking issue when the end
239 * of a file contained a large number of non-newline characters.
240 * To avoid this, the current implementation just matches newlines
241 * and uses match.index to get the correct line start indices.
243 while (( match
= lineEndingPattern
. exec ( this . text
))) {
244 this . lines
. push ( this . text
. slice ( this . lineStartIndices
[ this . lineStartIndices
. length
- 1 ], match
. index
));
245 this . lineStartIndices
. push ( match
. index
+ match
[ 0 ]. length
);
247 this . lines
. push ( this . text
. slice ( this . lineStartIndices
[ this . lineStartIndices
. length
- 1 ]));
249 // Cache for comments found using getComments().
250 this . _commentCache
= new WeakMap ();
252 // don't allow modification of this object
254 Object
. freeze ( this . lines
);
258 * Split the source code into multiple lines based on the line delimiters.
259 * @param {string} text Source code as a string.
260 * @returns {string[]} Array of source code lines.
263 static splitLines ( text
) {
264 return text
. split ( astUtils
. createGlobalLinebreakMatcher ());
268 * Gets the source code for the given node.
269 * @param {ASTNode} [node] The AST node to get the text for.
270 * @param {int} [beforeCount] The number of characters before the node to retrieve.
271 * @param {int} [afterCount] The number of characters after the node to retrieve.
272 * @returns {string} The text representing the AST node.
275 getText ( node
, beforeCount
, afterCount
) {
277 return this . text
. slice ( Math
. max ( node
. range
[ 0 ] - ( beforeCount
|| 0 ), 0 ),
278 node
. range
[ 1 ] + ( afterCount
|| 0 ));
284 * Gets the entire source text split into an array of lines.
285 * @returns {Array} The source text as an array of lines.
293 * Retrieves an array containing all comments in the source code.
294 * @returns {ASTNode[]} An array of comment nodes.
298 return this . ast
. comments
;
302 * Gets all comments for the given node.
303 * @param {ASTNode} node The AST node to get the comments for.
304 * @returns {Object} An object containing a leading and trailing array
305 * of comments indexed by their position.
307 * @deprecated replaced by getCommentsBefore(), getCommentsAfter(), and getCommentsInside().
310 if ( this . _commentCache
. has ( node
)) {
311 return this . _commentCache
. get ( node
);
320 * Return all comments as leading comments of the Program node when
321 * there is no executable code.
323 if ( node
. type
=== "Program" ) {
324 if ( node
. body
. length
=== 0 ) {
325 comments
. leading
= node
. comments
;
330 * Return comments as trailing comments of nodes that only contain
331 * comments (to mimic the comment attachment behavior present in Espree).
333 if (( node
. type
=== "BlockStatement" || node
. type
=== "ClassBody" ) && node
. body
. length
=== 0 ||
334 node
. type
=== "ObjectExpression" && node
. properties
. length
=== 0 ||
335 node
. type
=== "ArrayExpression" && node
. elements
. length
=== 0 ||
336 node
. type
=== "SwitchStatement" && node
. cases
. length
=== 0
338 comments
. trailing
= this . getTokens ( node
, {
339 includeComments
: true ,
340 filter
: isCommentToken
345 * Iterate over tokens before and after node and collect comment tokens.
346 * Do not include comments that exist outside of the parent node
347 * to avoid duplication.
349 let currentToken
= this . getTokenBefore ( node
, { includeComments
: true });
351 while ( currentToken
&& isCommentToken ( currentToken
)) {
352 if ( node
. parent
&& ( currentToken
. start
< node
. parent
. start
)) {
355 comments
. leading
. push ( currentToken
);
356 currentToken
= this . getTokenBefore ( currentToken
, { includeComments
: true });
359 comments
. leading
. reverse ();
361 currentToken
= this . getTokenAfter ( node
, { includeComments
: true });
363 while ( currentToken
&& isCommentToken ( currentToken
)) {
364 if ( node
. parent
&& ( currentToken
. end
> node
. parent
. end
)) {
367 comments
. trailing
. push ( currentToken
);
368 currentToken
= this . getTokenAfter ( currentToken
, { includeComments
: true });
372 this . _commentCache
. set ( node
, comments
);
377 * Retrieves the JSDoc comment for a given node.
378 * @param {ASTNode} node The AST node to get the comment for.
379 * @returns {Token|null} The Block comment token containing the JSDoc comment
380 * for the given node or null if not found.
384 getJSDocComment ( node
) {
387 * Checks for the presence of a JSDoc comment for the given node and returns it.
388 * @param {ASTNode} astNode The AST node to get the comment for.
389 * @returns {Token|null} The Block comment token containing the JSDoc comment
390 * for the given node or null if not found.
393 const findJSDocComment
= astNode
=> {
394 const tokenBefore
= this . getTokenBefore ( astNode
, { includeComments
: true });
398 isCommentToken ( tokenBefore
) &&
399 tokenBefore
. type
=== "Block" &&
400 tokenBefore
. value
. charAt ( 0 ) === "*" &&
401 astNode
. loc
. start
. line
- tokenBefore
. loc
. end
. line
<= 1
408 let parent
= node
. parent
;
411 case "ClassDeclaration" :
412 case "FunctionDeclaration" :
413 return findJSDocComment ( looksLikeExport ( parent
) ? parent
: node
);
415 case "ClassExpression" :
416 return findJSDocComment ( parent
. parent
);
418 case "ArrowFunctionExpression" :
419 case "FunctionExpression" :
420 if ( parent
. type
!== "CallExpression" && parent
. type
!== "NewExpression" ) {
422 ! this . getCommentsBefore ( parent
). length
&&
423 ! /Function/u . test ( parent
. type
) &&
424 parent
. type
!== "MethodDefinition" &&
425 parent
. type
!== "Property"
427 parent
= parent
. parent
;
434 if ( parent
&& parent
. type
!== "FunctionDeclaration" && parent
. type
!== "Program" ) {
435 return findJSDocComment ( parent
);
439 return findJSDocComment ( node
);
448 * Gets the deepest node containing a range index.
449 * @param {int} index Range index of the desired node.
450 * @returns {ASTNode} The node if found or null if not found.
453 getNodeByRangeIndex ( index
) {
456 Traverser
. traverse ( this . ast
, {
457 visitorKeys
: this . visitorKeys
,
459 if ( node
. range
[ 0 ] <= index
&& index
< node
. range
[ 1 ]) {
466 if ( node
=== result
) {
476 * Determines if two nodes or tokens have at least one whitespace character
477 * between them. Order does not matter. Returns false if the given nodes or
479 * @param {ASTNode|Token} first The first node or token to check between.
480 * @param {ASTNode|Token} second The second node or token to check between.
481 * @returns {boolean} True if there is a whitespace character between
482 * any of the tokens found between the two given nodes or tokens.
485 isSpaceBetween ( first
, second
) {
486 return isSpaceBetween ( this , first
, second
, false );
490 * Determines if two nodes or tokens have at least one whitespace character
491 * between them. Order does not matter. Returns false if the given nodes or
493 * For backward compatibility, this method returns true if there are
494 * `JSXText` tokens that contain whitespaces between the two.
495 * @param {ASTNode|Token} first The first node or token to check between.
496 * @param {ASTNode|Token} second The second node or token to check between.
497 * @returns {boolean} True if there is a whitespace character between
498 * any of the tokens found between the two given nodes or tokens.
499 * @deprecated in favor of isSpaceBetween().
502 isSpaceBetweenTokens ( first
, second
) {
503 return isSpaceBetween ( this , first
, second
, true );
507 * Converts a source text index into a (line, column) pair.
508 * @param {number} index The index of a character in a file
509 * @returns {Object} A {line, column} location object with a 0-indexed column
512 getLocFromIndex ( index
) {
513 if ( typeof index
!== "number" ) {
514 throw new TypeError ( "Expected `index` to be a number." );
517 if ( index
< 0 || index
> this . text
. length
) {
518 throw new RangeError ( `Index out of range (requested index ${index} , but source text has length ${this.text.length} ).` );
522 * For an argument of this.text.length, return the location one "spot" past the last character
523 * of the file. If the last character is a linebreak, the location will be column 0 of the next
524 * line; otherwise, the location will be in the next column on the same line.
526 * See getIndexFromLoc for the motivation for this special case.
528 if ( index
=== this . text
. length
) {
529 return { line
: this . lines
. length
, column
: this . lines
[ this . lines
. length
- 1 ]. length
};
533 * To figure out which line index is on, determine the last place at which index could
534 * be inserted into lineStartIndices to keep the list sorted.
536 const lineNumber
= index
>= this . lineStartIndices
[ this . lineStartIndices
. length
- 1 ]
537 ? this . lineStartIndices
. length
538 : this . lineStartIndices
. findIndex ( el
=> index
< el
);
540 return { line
: lineNumber
, column
: index
- this . lineStartIndices
[ lineNumber
- 1 ] };
544 * Converts a (line, column) pair into a range index.
545 * @param {Object} loc A line/column location
546 * @param {number} loc.line The line number of the location (1-indexed)
547 * @param {number} loc.column The column number of the location (0-indexed)
548 * @returns {number} The range index of the location in the file.
551 getIndexFromLoc ( loc
) {
552 if ( typeof loc
!== "object" || typeof loc
. line
!== "number" || typeof loc
. column
!== "number" ) {
553 throw new TypeError ( "Expected `loc` to be an object with numeric `line` and `column` properties." );
557 throw new RangeError ( `Line number out of range (line ${loc.line} requested). Line numbers should be 1-based.` );
560 if ( loc
. line
> this . lineStartIndices
. length
) {
561 throw new RangeError ( `Line number out of range (line ${loc.line} requested, but only ${this.lineStartIndices.length} lines present).` );
564 const lineStartIndex
= this . lineStartIndices
[ loc
. line
- 1 ];
565 const lineEndIndex
= loc
. line
=== this . lineStartIndices
. length
? this . text
. length
: this . lineStartIndices
[ loc
. line
];
566 const positionIndex
= lineStartIndex
+ loc
. column
;
569 * By design, getIndexFromLoc({ line: lineNum, column: 0 }) should return the start index of
570 * the given line, provided that the line number is valid element of this.lines. Since the
571 * last element of this.lines is an empty string for files with trailing newlines, add a
572 * special case where getting the index for the first location after the end of the file
573 * will return the length of the file, rather than throwing an error. This allows rules to
574 * use getIndexFromLoc consistently without worrying about edge cases at the end of a file.
577 loc
. line
=== this . lineStartIndices
. length
&& positionIndex
> lineEndIndex
||
578 loc
. line
< this . lineStartIndices
. length
&& positionIndex
>= lineEndIndex
580 throw new RangeError ( `Column number out of range (column ${loc.column} requested, but the length of line ${loc.line} is ${lineEndIndex - lineStartIndex} ).` );
583 return positionIndex
;
587 module
. exports
= SourceCode
;