| /** |
| * @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 = node.expression; |
| |
| if (call.type === "UnaryExpression") { |
| call = 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, |
| message: "Unexpected blank line before this statement.", |
| 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, |
| message: "Expected blank line before this statement.", |
| 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 |
| //------------------------------------------------------------------------------ |
| |
| module.exports = { |
| meta: { |
| type: "layout", |
| |
| docs: { |
| description: "require or disallow padding lines between statements", |
| category: "Stylistic Issues", |
| 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, |
| additionalItems: false |
| } |
| ] |
| } |
| }, |
| type: "array", |
| items: { |
| type: "object", |
| properties: { |
| blankLine: { $ref: "#/definitions/paddingType" }, |
| prev: { $ref: "#/definitions/statementType" }, |
| next: { $ref: "#/definitions/statementType" } |
| }, |
| additionalProperties: false, |
| required: ["blankLine", "prev", "next"] |
| }, |
| additionalItems: false |
| } |
| }, |
| |
| 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, |
| "Program:exit": exitScope, |
| "BlockStatement:exit": exitScope, |
| "SwitchStatement:exit": exitScope, |
| |
| ":statement": verify, |
| |
| SwitchCase: verifyThenEnterScope, |
| "SwitchCase:exit": exitScope |
| }; |
| } |
| }; |