123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632 |
- /**
- * @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
- };
- }
- };
|