2 * @fileoverview Rule to require or disallow newlines between statements
3 * @author Toru Nagashima
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const astUtils
= require("./utils/ast-utils");
14 //------------------------------------------------------------------------------
16 //------------------------------------------------------------------------------
18 const LT
= `[${Array.from(astUtils.LINEBREAKS).join("")}]`;
19 const PADDING_LINE_SEQUENCE
= new RegExp(
20 String
.raw
`^(\s*?${LT})\s*${LT}(\s*;?)$`,
23 const CJS_EXPORT
= /^(?:module\s*\.\s*)?exports(?:\s*\.|\s*\[|$)/u;
24 const CJS_IMPORT
= /^require\(/u;
27 * Creates tester which check if a node starts with specific keyword.
28 * @param {string} keyword The keyword to test.
29 * @returns {Object} the created tester.
32 function newKeywordTester(keyword
) {
34 test
: (node
, sourceCode
) =>
35 sourceCode
.getFirstToken(node
).value
=== keyword
40 * Creates tester which check if a node starts with specific keyword and spans a single line.
41 * @param {string} keyword The keyword to test.
42 * @returns {Object} the created tester.
45 function newSinglelineKeywordTester(keyword
) {
47 test
: (node
, sourceCode
) =>
48 node
.loc
.start
.line
=== node
.loc
.end
.line
&&
49 sourceCode
.getFirstToken(node
).value
=== keyword
54 * Creates tester which check if a node starts with specific keyword and spans multiple lines.
55 * @param {string} keyword The keyword to test.
56 * @returns {Object} the created tester.
59 function newMultilineKeywordTester(keyword
) {
61 test
: (node
, sourceCode
) =>
62 node
.loc
.start
.line
!== node
.loc
.end
.line
&&
63 sourceCode
.getFirstToken(node
).value
=== keyword
68 * Creates tester which check if a node is specific type.
69 * @param {string} type The node type to test.
70 * @returns {Object} the created tester.
73 function newNodeTypeTester(type
) {
81 * Checks the given node is an expression statement of IIFE.
82 * @param {ASTNode} node The node to check.
83 * @returns {boolean} `true` if the node is an expression statement of IIFE.
86 function isIIFEStatement(node
) {
87 if (node
.type
=== "ExpressionStatement") {
88 let call
= astUtils
.skipChainExpression(node
.expression
);
90 if (call
.type
=== "UnaryExpression") {
91 call
= astUtils
.skipChainExpression(call
.argument
);
93 return call
.type
=== "CallExpression" && astUtils
.isFunction(call
.callee
);
99 * Checks whether the given node is a block-like statement.
100 * This checks the last token of the node is the closing brace of a block.
101 * @param {SourceCode} sourceCode The source code to get tokens.
102 * @param {ASTNode} node The node to check.
103 * @returns {boolean} `true` if the node is a block-like statement.
106 function isBlockLikeStatement(sourceCode
, node
) {
108 // do-while with a block is a block-like statement.
109 if (node
.type
=== "DoWhileStatement" && node
.body
.type
=== "BlockStatement") {
114 * IIFE is a block-like statement specially from
115 * JSCS#disallowPaddingNewLinesAfterBlocks.
117 if (isIIFEStatement(node
)) {
121 // Checks the last token is a closing brace of blocks.
122 const lastToken
= sourceCode
.getLastToken(node
, astUtils
.isNotSemicolonToken
);
123 const belongingNode
= lastToken
&& astUtils
.isClosingBraceToken(lastToken
)
124 ? sourceCode
.getNodeByRangeIndex(lastToken
.range
[0])
127 return Boolean(belongingNode
) && (
128 belongingNode
.type
=== "BlockStatement" ||
129 belongingNode
.type
=== "SwitchStatement"
134 * Check whether the given node is a directive or not.
135 * @param {ASTNode} node The node to check.
136 * @param {SourceCode} sourceCode The source code object to get tokens.
137 * @returns {boolean} `true` if the node is a directive.
139 function isDirective(node
, sourceCode
) {
141 node
.type
=== "ExpressionStatement" &&
143 node
.parent
.type
=== "Program" ||
145 node
.parent
.type
=== "BlockStatement" &&
146 astUtils
.isFunction(node
.parent
.parent
)
149 node
.expression
.type
=== "Literal" &&
150 typeof node
.expression
.value
=== "string" &&
151 !astUtils
.isParenthesised(sourceCode
, node
.expression
)
156 * Check whether the given node is a part of directive prologue or not.
157 * @param {ASTNode} node The node to check.
158 * @param {SourceCode} sourceCode The source code object to get tokens.
159 * @returns {boolean} `true` if the node is a part of directive prologue.
161 function isDirectivePrologue(node
, sourceCode
) {
162 if (isDirective(node
, sourceCode
)) {
163 for (const sibling
of node
.parent
.body
) {
164 if (sibling
=== node
) {
167 if (!isDirective(sibling
, sourceCode
)) {
177 * Gets the actual last token.
179 * If a semicolon is semicolon-less style's semicolon, this ignores it.
183 * ;[1, 2, 3].forEach(bar)
184 * @param {SourceCode} sourceCode The source code to get tokens.
185 * @param {ASTNode} node The node to get.
186 * @returns {Token} The actual last token.
189 function getActualLastToken(sourceCode
, node
) {
190 const semiToken
= sourceCode
.getLastToken(node
);
191 const prevToken
= sourceCode
.getTokenBefore(semiToken
);
192 const nextToken
= sourceCode
.getTokenAfter(semiToken
);
193 const isSemicolonLessStyle
= Boolean(
196 prevToken
.range
[0] >= node
.range
[0] &&
197 astUtils
.isSemicolonToken(semiToken
) &&
198 semiToken
.loc
.start
.line
!== prevToken
.loc
.end
.line
&&
199 semiToken
.loc
.end
.line
=== nextToken
.loc
.start
.line
202 return isSemicolonLessStyle
? prevToken
: semiToken
;
206 * This returns the concatenation of the first 2 captured strings.
207 * @param {string} _ Unused. Whole matched string.
208 * @param {string} trailingSpaces The trailing spaces of the first line.
209 * @param {string} indentSpaces The indentation spaces of the last line.
210 * @returns {string} The concatenation of trailingSpaces and indentSpaces.
213 function replacerToRemovePaddingLines(_
, trailingSpaces
, indentSpaces
) {
214 return trailingSpaces
+ indentSpaces
;
218 * Check and report statements for `any` configuration.
223 function verifyForAny() {
227 * Check and report statements for `never` configuration.
228 * This autofix removes blank lines between the given 2 statements.
229 * However, if comments exist between 2 blank lines, it does not remove those
230 * blank lines automatically.
231 * @param {RuleContext} context The rule context to report.
232 * @param {ASTNode} _ Unused. The previous node to check.
233 * @param {ASTNode} nextNode The next node to check.
234 * @param {Array<Token[]>} paddingLines The array of token pairs that blank
235 * lines exist between the pair.
239 function verifyForNever(context
, _
, nextNode
, paddingLines
) {
240 if (paddingLines
.length
=== 0) {
246 messageId
: "unexpectedBlankLine",
248 if (paddingLines
.length
>= 2) {
252 const prevToken
= paddingLines
[0][0];
253 const nextToken
= paddingLines
[0][1];
254 const start
= prevToken
.range
[1];
255 const end
= nextToken
.range
[0];
256 const text
= context
.getSourceCode().text
258 .replace(PADDING_LINE_SEQUENCE
, replacerToRemovePaddingLines
);
260 return fixer
.replaceTextRange([start
, end
], text
);
266 * Check and report statements for `always` configuration.
267 * This autofix inserts a blank line between the given 2 statements.
268 * If the `prevNode` has trailing comments, it inserts a blank line after the
270 * @param {RuleContext} context The rule context to report.
271 * @param {ASTNode} prevNode The previous node to check.
272 * @param {ASTNode} nextNode The next node to check.
273 * @param {Array<Token[]>} paddingLines The array of token pairs that blank
274 * lines exist between the pair.
278 function verifyForAlways(context
, prevNode
, nextNode
, paddingLines
) {
279 if (paddingLines
.length
> 0) {
285 messageId
: "expectedBlankLine",
287 const sourceCode
= context
.getSourceCode();
288 let prevToken
= getActualLastToken(sourceCode
, prevNode
);
289 const nextToken
= sourceCode
.getFirstTokenBetween(
293 includeComments
: true,
296 * Skip the trailing comments of the previous node.
297 * This inserts a blank line after the last trailing comment.
301 * foo(); // trailing comment.
307 * foo(); // trailing comment.
311 * @param {Token} token The token to check.
312 * @returns {boolean} `true` if the token is not a trailing comment.
316 if (astUtils
.isTokenOnSameLine(prevToken
, token
)) {
324 const insertText
= astUtils
.isTokenOnSameLine(prevToken
, nextToken
)
328 return fixer
.insertTextAfter(prevToken
, insertText
);
334 * Types of blank lines.
335 * `any`, `never`, and `always` are defined.
336 * Those have `verify` method to check and report statements.
339 const PaddingTypes
= {
340 any
: { verify
: verifyForAny
},
341 never
: { verify
: verifyForNever
},
342 always
: { verify
: verifyForAlways
}
346 * Types of statements.
347 * Those have `test` method to check it matches to the given statement.
350 const StatementTypes
= {
351 "*": { test
: () => true },
353 test
: (node
, sourceCode
) => isBlockLikeStatement(sourceCode
, node
)
356 test
: (node
, sourceCode
) =>
357 node
.type
=== "ExpressionStatement" &&
358 node
.expression
.type
=== "AssignmentExpression" &&
359 CJS_EXPORT
.test(sourceCode
.getText(node
.expression
.left
))
362 test
: (node
, sourceCode
) =>
363 node
.type
=== "VariableDeclaration" &&
364 node
.declarations
.length
> 0 &&
365 Boolean(node
.declarations
[0].init
) &&
366 CJS_IMPORT
.test(sourceCode
.getText(node
.declarations
[0].init
))
369 test
: isDirectivePrologue
372 test
: (node
, sourceCode
) =>
373 node
.type
=== "ExpressionStatement" &&
374 !isDirectivePrologue(node
, sourceCode
)
377 test
: isIIFEStatement
379 "multiline-block-like": {
380 test
: (node
, sourceCode
) =>
381 node
.loc
.start
.line
!== node
.loc
.end
.line
&&
382 isBlockLikeStatement(sourceCode
, node
)
384 "multiline-expression": {
385 test
: (node
, sourceCode
) =>
386 node
.loc
.start
.line
!== node
.loc
.end
.line
&&
387 node
.type
=== "ExpressionStatement" &&
388 !isDirectivePrologue(node
, sourceCode
)
391 "multiline-const": newMultilineKeywordTester("const"),
392 "multiline-let": newMultilineKeywordTester("let"),
393 "multiline-var": newMultilineKeywordTester("var"),
394 "singleline-const": newSinglelineKeywordTester("const"),
395 "singleline-let": newSinglelineKeywordTester("let"),
396 "singleline-var": newSinglelineKeywordTester("var"),
398 block
: newNodeTypeTester("BlockStatement"),
399 empty
: newNodeTypeTester("EmptyStatement"),
400 function: newNodeTypeTester("FunctionDeclaration"),
402 break: newKeywordTester("break"),
403 case: newKeywordTester("case"),
404 class: newKeywordTester("class"),
405 const: newKeywordTester("const"),
406 continue: newKeywordTester("continue"),
407 debugger: newKeywordTester("debugger"),
408 default: newKeywordTester("default"),
409 do: newKeywordTester("do"),
410 export: newKeywordTester("export"),
411 for: newKeywordTester("for"),
412 if: newKeywordTester("if"),
413 import: newKeywordTester("import"),
414 let: newKeywordTester("let"),
415 return: newKeywordTester("return"),
416 switch: newKeywordTester("switch"),
417 throw: newKeywordTester("throw"),
418 try: newKeywordTester("try"),
419 var: newKeywordTester("var"),
420 while: newKeywordTester("while"),
421 with: newKeywordTester("with")
424 //------------------------------------------------------------------------------
426 //------------------------------------------------------------------------------
433 description
: "require or disallow padding lines between statements",
435 url
: "https://eslint.org/docs/rules/padding-line-between-statements"
438 fixable
: "whitespace",
443 enum: Object
.keys(PaddingTypes
)
447 { enum: Object
.keys(StatementTypes
) },
450 items
: { enum: Object
.keys(StatementTypes
) },
453 additionalItems
: false
462 blankLine
: { $ref
: "#/definitions/paddingType" },
463 prev
: { $ref
: "#/definitions/statementType" },
464 next
: { $ref
: "#/definitions/statementType" }
466 additionalProperties
: false,
467 required
: ["blankLine", "prev", "next"]
469 additionalItems
: false
473 unexpectedBlankLine
: "Unexpected blank line before this statement.",
474 expectedBlankLine
: "Expected blank line before this statement."
479 const sourceCode
= context
.getSourceCode();
480 const configureList
= context
.options
|| [];
481 let scopeInfo
= null;
484 * Processes to enter to new scope.
485 * This manages the current previous statement.
489 function enterScope() {
497 * Processes to exit from the current scope.
501 function exitScope() {
502 scopeInfo
= scopeInfo
.upper
;
506 * Checks whether the given node matches the given type.
507 * @param {ASTNode} node The statement node to check.
508 * @param {string|string[]} type The statement type to check.
509 * @returns {boolean} `true` if the statement node matched the type.
512 function match(node
, type
) {
513 let innerStatementNode
= node
;
515 while (innerStatementNode
.type
=== "LabeledStatement") {
516 innerStatementNode
= innerStatementNode
.body
;
518 if (Array
.isArray(type
)) {
519 return type
.some(match
.bind(null, innerStatementNode
));
521 return StatementTypes
[type
].test(innerStatementNode
, sourceCode
);
525 * Finds the last matched configure from configureList.
526 * @param {ASTNode} prevNode The previous statement to match.
527 * @param {ASTNode} nextNode The current statement to match.
528 * @returns {Object} The tester of the last matched configure.
531 function getPaddingType(prevNode
, nextNode
) {
532 for (let i
= configureList
.length
- 1; i
>= 0; --i
) {
533 const configure
= configureList
[i
];
535 match(prevNode
, configure
.prev
) &&
536 match(nextNode
, configure
.next
);
539 return PaddingTypes
[configure
.blankLine
];
542 return PaddingTypes
.any
;
546 * Gets padding line sequences between the given 2 statements.
547 * Comments are separators of the padding line sequences.
548 * @param {ASTNode} prevNode The previous statement to count.
549 * @param {ASTNode} nextNode The current statement to count.
550 * @returns {Array<Token[]>} The array of token pairs.
553 function getPaddingLineSequences(prevNode
, nextNode
) {
555 let prevToken
= getActualLastToken(sourceCode
, prevNode
);
557 if (nextNode
.loc
.start
.line
- prevToken
.loc
.end
.line
>= 2) {
559 const token
= sourceCode
.getTokenAfter(
561 { includeComments
: true }
564 if (token
.loc
.start
.line
- prevToken
.loc
.end
.line
>= 2) {
565 pairs
.push([prevToken
, token
]);
569 } while (prevToken
.range
[0] < nextNode
.range
[0]);
576 * Verify padding lines between the given node and the previous node.
577 * @param {ASTNode} node The node to verify.
581 function verify(node
) {
582 const parentType
= node
.parent
.type
;
584 astUtils
.STATEMENT_LIST_PARENTS
.has(parentType
) ||
585 parentType
=== "SwitchStatement";
591 // Save this node as the current previous statement.
592 const prevNode
= scopeInfo
.prevNode
;
596 const type
= getPaddingType(prevNode
, node
);
597 const paddingLines
= getPaddingLineSequences(prevNode
, node
);
599 type
.verify(context
, prevNode
, node
, paddingLines
);
602 scopeInfo
.prevNode
= node
;
606 * Verify padding lines between the given node and the previous node.
607 * Then process to enter to new scope.
608 * @param {ASTNode} node The node to verify.
612 function verifyThenEnterScope(node
) {
619 BlockStatement
: enterScope
,
620 SwitchStatement
: enterScope
,
621 StaticBlock
: enterScope
,
622 "Program:exit": exitScope
,
623 "BlockStatement:exit": exitScope
,
624 "SwitchStatement:exit": exitScope
,
625 "StaticBlock:exit": exitScope
,
627 ":statement": verify
,
629 SwitchCase
: verifyThenEnterScope
,
630 "SwitchCase:exit": exitScope