Permalink
Cannot retrieve contributors at this time
587 lines (526 sloc)
18.8 KB
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
codeql-action/node_modules/eslint/lib/rules/padding-line-between-statements.js
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @fileoverview Rule to require or disallow newlines between statements | |
* @author Toru Nagashima | |
*/ | |
"use strict"; | |
//------------------------------------------------------------------------------ | |
// Requirements | |
//------------------------------------------------------------------------------ | |
const astUtils = require("./utils/ast-utils"); | |
//------------------------------------------------------------------------------ | |
// Helpers | |
//------------------------------------------------------------------------------ | |
const LT = `[${Array.from(astUtils.LINEBREAKS).join("")}]`; | |
const PADDING_LINE_SEQUENCE = new RegExp( | |
String.raw`^(\s*?${LT})\s*${LT}(\s*;?)$`, | |
"u" | |
); | |
const CJS_EXPORT = /^(?:module\s*\.\s*)?exports(?:\s*\.|\s*\[|$)/u; | |
const CJS_IMPORT = /^require\(/u; | |
/** | |
* Creates tester which check if a node starts with specific keyword. | |
* @param {string} keyword The keyword to test. | |
* @returns {Object} the created tester. | |
* @private | |
*/ | |
function newKeywordTester(keyword) { | |
return { | |
test: (node, sourceCode) => | |
sourceCode.getFirstToken(node).value === keyword | |
}; | |
} | |
/** | |
* Creates tester which check if a node starts with specific keyword and spans a single line. | |
* @param {string} keyword The keyword to test. | |
* @returns {Object} the created tester. | |
* @private | |
*/ | |
function newSinglelineKeywordTester(keyword) { | |
return { | |
test: (node, sourceCode) => | |
node.loc.start.line === node.loc.end.line && | |
sourceCode.getFirstToken(node).value === keyword | |
}; | |
} | |
/** | |
* Creates tester which check if a node starts with specific keyword and spans multiple lines. | |
* @param {string} keyword The keyword to test. | |
* @returns {Object} the created tester. | |
* @private | |
*/ | |
function newMultilineKeywordTester(keyword) { | |
return { | |
test: (node, sourceCode) => | |
node.loc.start.line !== node.loc.end.line && | |
sourceCode.getFirstToken(node).value === keyword | |
}; | |
} | |
/** | |
* Creates tester which check if a node is specific type. | |
* @param {string} type The node type to test. | |
* @returns {Object} the created tester. | |
* @private | |
*/ | |
function newNodeTypeTester(type) { | |
return { | |
test: node => | |
node.type === type | |
}; | |
} | |
/** | |
* Checks the given node is an expression statement of IIFE. | |
* @param {ASTNode} node The node to check. | |
* @returns {boolean} `true` if the node is an expression statement of IIFE. | |
* @private | |
*/ | |
function isIIFEStatement(node) { | |
if (node.type === "ExpressionStatement") { | |
let call = astUtils.skipChainExpression(node.expression); | |
if (call.type === "UnaryExpression") { | |
call = astUtils.skipChainExpression(call.argument); | |
} | |
return call.type === "CallExpression" && astUtils.isFunction(call.callee); | |
} | |
return false; | |
} | |
/** | |
* Checks whether the given node is a block-like statement. | |
* This checks the last token of the node is the closing brace of a block. | |
* @param {SourceCode} sourceCode The source code to get tokens. | |
* @param {ASTNode} node The node to check. | |
* @returns {boolean} `true` if the node is a block-like statement. | |
* @private | |
*/ | |
function isBlockLikeStatement(sourceCode, node) { | |
// do-while with a block is a block-like statement. | |
if (node.type === "DoWhileStatement" && node.body.type === "BlockStatement") { | |
return true; | |
} | |
/* | |
* IIFE is a block-like statement specially from | |
* JSCS#disallowPaddingNewLinesAfterBlocks. | |
*/ | |
if (isIIFEStatement(node)) { | |
return true; | |
} | |
// Checks the last token is a closing brace of blocks. | |
const lastToken = sourceCode.getLastToken(node, astUtils.isNotSemicolonToken); | |
const belongingNode = lastToken && astUtils.isClosingBraceToken(lastToken) | |
? sourceCode.getNodeByRangeIndex(lastToken.range[0]) | |
: null; | |
return Boolean(belongingNode) && ( | |
belongingNode.type === "BlockStatement" || | |
belongingNode.type === "SwitchStatement" | |
); | |
} | |
/** | |
* Gets the actual last token. | |
* | |
* If a semicolon is semicolon-less style's semicolon, this ignores it. | |
* For example: | |
* | |
* foo() | |
* ;[1, 2, 3].forEach(bar) | |
* @param {SourceCode} sourceCode The source code to get tokens. | |
* @param {ASTNode} node The node to get. | |
* @returns {Token} The actual last token. | |
* @private | |
*/ | |
function getActualLastToken(sourceCode, node) { | |
const semiToken = sourceCode.getLastToken(node); | |
const prevToken = sourceCode.getTokenBefore(semiToken); | |
const nextToken = sourceCode.getTokenAfter(semiToken); | |
const isSemicolonLessStyle = Boolean( | |
prevToken && | |
nextToken && | |
prevToken.range[0] >= node.range[0] && | |
astUtils.isSemicolonToken(semiToken) && | |
semiToken.loc.start.line !== prevToken.loc.end.line && | |
semiToken.loc.end.line === nextToken.loc.start.line | |
); | |
return isSemicolonLessStyle ? prevToken : semiToken; | |
} | |
/** | |
* This returns the concatenation of the first 2 captured strings. | |
* @param {string} _ Unused. Whole matched string. | |
* @param {string} trailingSpaces The trailing spaces of the first line. | |
* @param {string} indentSpaces The indentation spaces of the last line. | |
* @returns {string} The concatenation of trailingSpaces and indentSpaces. | |
* @private | |
*/ | |
function replacerToRemovePaddingLines(_, trailingSpaces, indentSpaces) { | |
return trailingSpaces + indentSpaces; | |
} | |
/** | |
* Check and report statements for `any` configuration. | |
* It does nothing. | |
* @returns {void} | |
* @private | |
*/ | |
function verifyForAny() { | |
} | |
/** | |
* Check and report statements for `never` configuration. | |
* This autofix removes blank lines between the given 2 statements. | |
* However, if comments exist between 2 blank lines, it does not remove those | |
* blank lines automatically. | |
* @param {RuleContext} context The rule context to report. | |
* @param {ASTNode} _ Unused. The previous node to check. | |
* @param {ASTNode} nextNode The next node to check. | |
* @param {Array<Token[]>} paddingLines The array of token pairs that blank | |
* lines exist between the pair. | |
* @returns {void} | |
* @private | |
*/ | |
function verifyForNever(context, _, nextNode, paddingLines) { | |
if (paddingLines.length === 0) { | |
return; | |
} | |
context.report({ | |
node: nextNode, | |
messageId: "unexpectedBlankLine", | |
fix(fixer) { | |
if (paddingLines.length >= 2) { | |
return null; | |
} | |
const prevToken = paddingLines[0][0]; | |
const nextToken = paddingLines[0][1]; | |
const start = prevToken.range[1]; | |
const end = nextToken.range[0]; | |
const text = context.sourceCode.text | |
.slice(start, end) | |
.replace(PADDING_LINE_SEQUENCE, replacerToRemovePaddingLines); | |
return fixer.replaceTextRange([start, end], text); | |
} | |
}); | |
} | |
/** | |
* Check and report statements for `always` configuration. | |
* This autofix inserts a blank line between the given 2 statements. | |
* If the `prevNode` has trailing comments, it inserts a blank line after the | |
* trailing comments. | |
* @param {RuleContext} context The rule context to report. | |
* @param {ASTNode} prevNode The previous node to check. | |
* @param {ASTNode} nextNode The next node to check. | |
* @param {Array<Token[]>} paddingLines The array of token pairs that blank | |
* lines exist between the pair. | |
* @returns {void} | |
* @private | |
*/ | |
function verifyForAlways(context, prevNode, nextNode, paddingLines) { | |
if (paddingLines.length > 0) { | |
return; | |
} | |
context.report({ | |
node: nextNode, | |
messageId: "expectedBlankLine", | |
fix(fixer) { | |
const sourceCode = context.sourceCode; | |
let prevToken = getActualLastToken(sourceCode, prevNode); | |
const nextToken = sourceCode.getFirstTokenBetween( | |
prevToken, | |
nextNode, | |
{ | |
includeComments: true, | |
/** | |
* Skip the trailing comments of the previous node. | |
* This inserts a blank line after the last trailing comment. | |
* | |
* For example: | |
* | |
* foo(); // trailing comment. | |
* // comment. | |
* bar(); | |
* | |
* Get fixed to: | |
* | |
* foo(); // trailing comment. | |
* | |
* // comment. | |
* bar(); | |
* @param {Token} token The token to check. | |
* @returns {boolean} `true` if the token is not a trailing comment. | |
* @private | |
*/ | |
filter(token) { | |
if (astUtils.isTokenOnSameLine(prevToken, token)) { | |
prevToken = token; | |
return false; | |
} | |
return true; | |
} | |
} | |
) || nextNode; | |
const insertText = astUtils.isTokenOnSameLine(prevToken, nextToken) | |
? "\n\n" | |
: "\n"; | |
return fixer.insertTextAfter(prevToken, insertText); | |
} | |
}); | |
} | |
/** | |
* Types of blank lines. | |
* `any`, `never`, and `always` are defined. | |
* Those have `verify` method to check and report statements. | |
* @private | |
*/ | |
const PaddingTypes = { | |
any: { verify: verifyForAny }, | |
never: { verify: verifyForNever }, | |
always: { verify: verifyForAlways } | |
}; | |
/** | |
* Types of statements. | |
* Those have `test` method to check it matches to the given statement. | |
* @private | |
*/ | |
const StatementTypes = { | |
"*": { test: () => true }, | |
"block-like": { | |
test: (node, sourceCode) => isBlockLikeStatement(sourceCode, node) | |
}, | |
"cjs-export": { | |
test: (node, sourceCode) => | |
node.type === "ExpressionStatement" && | |
node.expression.type === "AssignmentExpression" && | |
CJS_EXPORT.test(sourceCode.getText(node.expression.left)) | |
}, | |
"cjs-import": { | |
test: (node, sourceCode) => | |
node.type === "VariableDeclaration" && | |
node.declarations.length > 0 && | |
Boolean(node.declarations[0].init) && | |
CJS_IMPORT.test(sourceCode.getText(node.declarations[0].init)) | |
}, | |
directive: { | |
test: astUtils.isDirective | |
}, | |
expression: { | |
test: node => node.type === "ExpressionStatement" && !astUtils.isDirective(node) | |
}, | |
iife: { | |
test: isIIFEStatement | |
}, | |
"multiline-block-like": { | |
test: (node, sourceCode) => | |
node.loc.start.line !== node.loc.end.line && | |
isBlockLikeStatement(sourceCode, node) | |
}, | |
"multiline-expression": { | |
test: node => | |
node.loc.start.line !== node.loc.end.line && | |
node.type === "ExpressionStatement" && | |
!astUtils.isDirective(node) | |
}, | |
"multiline-const": newMultilineKeywordTester("const"), | |
"multiline-let": newMultilineKeywordTester("let"), | |
"multiline-var": newMultilineKeywordTester("var"), | |
"singleline-const": newSinglelineKeywordTester("const"), | |
"singleline-let": newSinglelineKeywordTester("let"), | |
"singleline-var": newSinglelineKeywordTester("var"), | |
block: newNodeTypeTester("BlockStatement"), | |
empty: newNodeTypeTester("EmptyStatement"), | |
function: newNodeTypeTester("FunctionDeclaration"), | |
break: newKeywordTester("break"), | |
case: newKeywordTester("case"), | |
class: newKeywordTester("class"), | |
const: newKeywordTester("const"), | |
continue: newKeywordTester("continue"), | |
debugger: newKeywordTester("debugger"), | |
default: newKeywordTester("default"), | |
do: newKeywordTester("do"), | |
export: newKeywordTester("export"), | |
for: newKeywordTester("for"), | |
if: newKeywordTester("if"), | |
import: newKeywordTester("import"), | |
let: newKeywordTester("let"), | |
return: newKeywordTester("return"), | |
switch: newKeywordTester("switch"), | |
throw: newKeywordTester("throw"), | |
try: newKeywordTester("try"), | |
var: newKeywordTester("var"), | |
while: newKeywordTester("while"), | |
with: newKeywordTester("with") | |
}; | |
//------------------------------------------------------------------------------ | |
// Rule Definition | |
//------------------------------------------------------------------------------ | |
/** @type {import('../shared/types').Rule} */ | |
module.exports = { | |
meta: { | |
type: "layout", | |
docs: { | |
description: "Require or disallow padding lines between statements", | |
recommended: false, | |
url: "https://eslint.org/docs/latest/rules/padding-line-between-statements" | |
}, | |
fixable: "whitespace", | |
schema: { | |
definitions: { | |
paddingType: { | |
enum: Object.keys(PaddingTypes) | |
}, | |
statementType: { | |
anyOf: [ | |
{ enum: Object.keys(StatementTypes) }, | |
{ | |
type: "array", | |
items: { enum: Object.keys(StatementTypes) }, | |
minItems: 1, | |
uniqueItems: true | |
} | |
] | |
} | |
}, | |
type: "array", | |
items: { | |
type: "object", | |
properties: { | |
blankLine: { $ref: "#/definitions/paddingType" }, | |
prev: { $ref: "#/definitions/statementType" }, | |
next: { $ref: "#/definitions/statementType" } | |
}, | |
additionalProperties: false, | |
required: ["blankLine", "prev", "next"] | |
} | |
}, | |
messages: { | |
unexpectedBlankLine: "Unexpected blank line before this statement.", | |
expectedBlankLine: "Expected blank line before this statement." | |
} | |
}, | |
create(context) { | |
const sourceCode = context.sourceCode; | |
const configureList = context.options || []; | |
let scopeInfo = null; | |
/** | |
* Processes to enter to new scope. | |
* This manages the current previous statement. | |
* @returns {void} | |
* @private | |
*/ | |
function enterScope() { | |
scopeInfo = { | |
upper: scopeInfo, | |
prevNode: null | |
}; | |
} | |
/** | |
* Processes to exit from the current scope. | |
* @returns {void} | |
* @private | |
*/ | |
function exitScope() { | |
scopeInfo = scopeInfo.upper; | |
} | |
/** | |
* Checks whether the given node matches the given type. | |
* @param {ASTNode} node The statement node to check. | |
* @param {string|string[]} type The statement type to check. | |
* @returns {boolean} `true` if the statement node matched the type. | |
* @private | |
*/ | |
function match(node, type) { | |
let innerStatementNode = node; | |
while (innerStatementNode.type === "LabeledStatement") { | |
innerStatementNode = innerStatementNode.body; | |
} | |
if (Array.isArray(type)) { | |
return type.some(match.bind(null, innerStatementNode)); | |
} | |
return StatementTypes[type].test(innerStatementNode, sourceCode); | |
} | |
/** | |
* Finds the last matched configure from configureList. | |
* @param {ASTNode} prevNode The previous statement to match. | |
* @param {ASTNode} nextNode The current statement to match. | |
* @returns {Object} The tester of the last matched configure. | |
* @private | |
*/ | |
function getPaddingType(prevNode, nextNode) { | |
for (let i = configureList.length - 1; i >= 0; --i) { | |
const configure = configureList[i]; | |
const matched = | |
match(prevNode, configure.prev) && | |
match(nextNode, configure.next); | |
if (matched) { | |
return PaddingTypes[configure.blankLine]; | |
} | |
} | |
return PaddingTypes.any; | |
} | |
/** | |
* Gets padding line sequences between the given 2 statements. | |
* Comments are separators of the padding line sequences. | |
* @param {ASTNode} prevNode The previous statement to count. | |
* @param {ASTNode} nextNode The current statement to count. | |
* @returns {Array<Token[]>} The array of token pairs. | |
* @private | |
*/ | |
function getPaddingLineSequences(prevNode, nextNode) { | |
const pairs = []; | |
let prevToken = getActualLastToken(sourceCode, prevNode); | |
if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) { | |
do { | |
const token = sourceCode.getTokenAfter( | |
prevToken, | |
{ includeComments: true } | |
); | |
if (token.loc.start.line - prevToken.loc.end.line >= 2) { | |
pairs.push([prevToken, token]); | |
} | |
prevToken = token; | |
} while (prevToken.range[0] < nextNode.range[0]); | |
} | |
return pairs; | |
} | |
/** | |
* Verify padding lines between the given node and the previous node. | |
* @param {ASTNode} node The node to verify. | |
* @returns {void} | |
* @private | |
*/ | |
function verify(node) { | |
const parentType = node.parent.type; | |
const validParent = | |
astUtils.STATEMENT_LIST_PARENTS.has(parentType) || | |
parentType === "SwitchStatement"; | |
if (!validParent) { | |
return; | |
} | |
// Save this node as the current previous statement. | |
const prevNode = scopeInfo.prevNode; | |
// Verify. | |
if (prevNode) { | |
const type = getPaddingType(prevNode, node); | |
const paddingLines = getPaddingLineSequences(prevNode, node); | |
type.verify(context, prevNode, node, paddingLines); | |
} | |
scopeInfo.prevNode = node; | |
} | |
/** | |
* Verify padding lines between the given node and the previous node. | |
* Then process to enter to new scope. | |
* @param {ASTNode} node The node to verify. | |
* @returns {void} | |
* @private | |
*/ | |
function verifyThenEnterScope(node) { | |
verify(node); | |
enterScope(); | |
} | |
return { | |
Program: enterScope, | |
BlockStatement: enterScope, | |
SwitchStatement: enterScope, | |
StaticBlock: enterScope, | |
"Program:exit": exitScope, | |
"BlockStatement:exit": exitScope, | |
"SwitchStatement:exit": exitScope, | |
"StaticBlock:exit": exitScope, | |
":statement": verify, | |
SwitchCase: verifyThenEnterScope, | |
"SwitchCase:exit": exitScope | |
}; | |
} | |
}; |