You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
632 lines
20 KiB
632 lines
20 KiB
/** |
|
* @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" |
|
); |
|
} |
|
|
|
/** |
|
* Check whether the given node is a directive or not. |
|
* @param {ASTNode} node The node to check. |
|
* @param {SourceCode} sourceCode The source code object to get tokens. |
|
* @returns {boolean} `true` if the node is a directive. |
|
*/ |
|
function isDirective(node, sourceCode) { |
|
return ( |
|
node.type === "ExpressionStatement" && |
|
( |
|
node.parent.type === "Program" || |
|
( |
|
node.parent.type === "BlockStatement" && |
|
astUtils.isFunction(node.parent.parent) |
|
) |
|
) && |
|
node.expression.type === "Literal" && |
|
typeof node.expression.value === "string" && |
|
!astUtils.isParenthesised(sourceCode, node.expression) |
|
); |
|
} |
|
|
|
/** |
|
* Check whether the given node is a part of directive prologue or not. |
|
* @param {ASTNode} node The node to check. |
|
* @param {SourceCode} sourceCode The source code object to get tokens. |
|
* @returns {boolean} `true` if the node is a part of directive prologue. |
|
*/ |
|
function isDirectivePrologue(node, sourceCode) { |
|
if (isDirective(node, sourceCode)) { |
|
for (const sibling of node.parent.body) { |
|
if (sibling === node) { |
|
break; |
|
} |
|
if (!isDirective(sibling, sourceCode)) { |
|
return false; |
|
} |
|
} |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
/** |
|
* 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.getSourceCode().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.getSourceCode(); |
|
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: isDirectivePrologue |
|
}, |
|
expression: { |
|
test: (node, sourceCode) => |
|
node.type === "ExpressionStatement" && |
|
!isDirectivePrologue(node, sourceCode) |
|
}, |
|
iife: { |
|
test: isIIFEStatement |
|
}, |
|
"multiline-block-like": { |
|
test: (node, sourceCode) => |
|
node.loc.start.line !== node.loc.end.line && |
|
isBlockLikeStatement(sourceCode, node) |
|
}, |
|
"multiline-expression": { |
|
test: (node, sourceCode) => |
|
node.loc.start.line !== node.loc.end.line && |
|
node.type === "ExpressionStatement" && |
|
!isDirectivePrologue(node, sourceCode) |
|
}, |
|
|
|
"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/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.getSourceCode(); |
|
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 |
|
}; |
|
} |
|
};
|
|
|