UNPKG

19.3 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to require or disallow newlines between statements
3 * @author Toru Nagashima
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const astUtils = require("../ast-utils");
13
14//------------------------------------------------------------------------------
15// Helpers
16//------------------------------------------------------------------------------
17
18const LT = `[${Array.from(astUtils.LINEBREAKS).join("")}]`;
19const PADDING_LINE_SEQUENCE = new RegExp(
20 String.raw`^(\s*?${LT})\s*${LT}(\s*;?)$`
21);
22const CJS_EXPORT = /^(?:module\s*\.\s*)?exports(?:\s*\.|\s*\[|$)/;
23const CJS_IMPORT = /^require\(/;
24
25/**
26 * Creates tester which check if a node starts with specific keyword.
27 *
28 * @param {string} keyword The keyword to test.
29 * @returns {Object} the created tester.
30 * @private
31 */
32function newKeywordTester(keyword) {
33 return {
34 test: (node, sourceCode) =>
35 sourceCode.getFirstToken(node).value === keyword
36 };
37}
38
39/**
40 * Creates tester which check if a node is specific type.
41 *
42 * @param {string} type The node type to test.
43 * @returns {Object} the created tester.
44 * @private
45 */
46function newNodeTypeTester(type) {
47 return {
48 test: node =>
49 node.type === type
50 };
51}
52
53/**
54 * Checks the given node is an expression statement of IIFE.
55 *
56 * @param {ASTNode} node The node to check.
57 * @returns {boolean} `true` if the node is an expression statement of IIFE.
58 * @private
59 */
60function isIIFEStatement(node) {
61 if (node.type === "ExpressionStatement") {
62 let call = node.expression;
63
64 if (call.type === "UnaryExpression") {
65 call = call.argument;
66 }
67 return call.type === "CallExpression" && astUtils.isFunction(call.callee);
68 }
69 return false;
70}
71
72/**
73 * Checks whether the given node is a block-like statement.
74 * This checks the last token of the node is the closing brace of a block.
75 *
76 * @param {SourceCode} sourceCode The source code to get tokens.
77 * @param {ASTNode} node The node to check.
78 * @returns {boolean} `true` if the node is a block-like statement.
79 * @private
80 */
81function isBlockLikeStatement(sourceCode, node) {
82
83 // do-while with a block is a block-like statement.
84 if (node.type === "DoWhileStatement" && node.body.type === "BlockStatement") {
85 return true;
86 }
87
88 /*
89 * IIFE is a block-like statement specially from
90 * JSCS#disallowPaddingNewLinesAfterBlocks.
91 */
92 if (isIIFEStatement(node)) {
93 return true;
94 }
95
96 // Checks the last token is a closing brace of blocks.
97 const lastToken = sourceCode.getLastToken(node, astUtils.isNotSemicolonToken);
98 const belongingNode = lastToken && astUtils.isClosingBraceToken(lastToken)
99 ? sourceCode.getNodeByRangeIndex(lastToken.range[0])
100 : null;
101
102 return Boolean(belongingNode) && (
103 belongingNode.type === "BlockStatement" ||
104 belongingNode.type === "SwitchStatement"
105 );
106}
107
108/**
109 * Check whether the given node is a directive or not.
110 * @param {ASTNode} node The node to check.
111 * @param {SourceCode} sourceCode The source code object to get tokens.
112 * @returns {boolean} `true` if the node is a directive.
113 */
114function isDirective(node, sourceCode) {
115 return (
116 node.type === "ExpressionStatement" &&
117 (
118 node.parent.type === "Program" ||
119 (
120 node.parent.type === "BlockStatement" &&
121 astUtils.isFunction(node.parent.parent)
122 )
123 ) &&
124 node.expression.type === "Literal" &&
125 typeof node.expression.value === "string" &&
126 !astUtils.isParenthesised(sourceCode, node.expression)
127 );
128}
129
130/**
131 * Check whether the given node is a part of directive prologue or not.
132 * @param {ASTNode} node The node to check.
133 * @param {SourceCode} sourceCode The source code object to get tokens.
134 * @returns {boolean} `true` if the node is a part of directive prologue.
135 */
136function isDirectivePrologue(node, sourceCode) {
137 if (isDirective(node, sourceCode)) {
138 for (const sibling of node.parent.body) {
139 if (sibling === node) {
140 break;
141 }
142 if (!isDirective(sibling, sourceCode)) {
143 return false;
144 }
145 }
146 return true;
147 }
148 return false;
149}
150
151/**
152 * Gets the actual last token.
153 *
154 * If a semicolon is semicolon-less style's semicolon, this ignores it.
155 * For example:
156 *
157 * foo()
158 * ;[1, 2, 3].forEach(bar)
159 *
160 * @param {SourceCode} sourceCode The source code to get tokens.
161 * @param {ASTNode} node The node to get.
162 * @returns {Token} The actual last token.
163 * @private
164 */
165function getActualLastToken(sourceCode, node) {
166 const semiToken = sourceCode.getLastToken(node);
167 const prevToken = sourceCode.getTokenBefore(semiToken);
168 const nextToken = sourceCode.getTokenAfter(semiToken);
169 const isSemicolonLessStyle = Boolean(
170 prevToken &&
171 nextToken &&
172 prevToken.range[0] >= node.range[0] &&
173 astUtils.isSemicolonToken(semiToken) &&
174 semiToken.loc.start.line !== prevToken.loc.end.line &&
175 semiToken.loc.end.line === nextToken.loc.start.line
176 );
177
178 return isSemicolonLessStyle ? prevToken : semiToken;
179}
180
181/**
182 * This returns the concatenation of the first 2 captured strings.
183 * @param {string} _ Unused. Whole matched string.
184 * @param {string} trailingSpaces The trailing spaces of the first line.
185 * @param {string} indentSpaces The indentation spaces of the last line.
186 * @returns {string} The concatenation of trailingSpaces and indentSpaces.
187 * @private
188 */
189function replacerToRemovePaddingLines(_, trailingSpaces, indentSpaces) {
190 return trailingSpaces + indentSpaces;
191}
192
193/**
194 * Check and report statements for `any` configuration.
195 * It does nothing.
196 *
197 * @returns {void}
198 * @private
199 */
200function verifyForAny() {
201}
202
203/**
204 * Check and report statements for `never` configuration.
205 * This autofix removes blank lines between the given 2 statements.
206 * However, if comments exist between 2 blank lines, it does not remove those
207 * blank lines automatically.
208 *
209 * @param {RuleContext} context The rule context to report.
210 * @param {ASTNode} _ Unused. The previous node to check.
211 * @param {ASTNode} nextNode The next node to check.
212 * @param {Array<Token[]>} paddingLines The array of token pairs that blank
213 * lines exist between the pair.
214 * @returns {void}
215 * @private
216 */
217function verifyForNever(context, _, nextNode, paddingLines) {
218 if (paddingLines.length === 0) {
219 return;
220 }
221
222 context.report({
223 node: nextNode,
224 message: "Unexpected blank line before this statement.",
225 fix(fixer) {
226 if (paddingLines.length >= 2) {
227 return null;
228 }
229
230 const prevToken = paddingLines[0][0];
231 const nextToken = paddingLines[0][1];
232 const start = prevToken.range[1];
233 const end = nextToken.range[0];
234 const text = context.getSourceCode().text
235 .slice(start, end)
236 .replace(PADDING_LINE_SEQUENCE, replacerToRemovePaddingLines);
237
238 return fixer.replaceTextRange([start, end], text);
239 }
240 });
241}
242
243/**
244 * Check and report statements for `always` configuration.
245 * This autofix inserts a blank line between the given 2 statements.
246 * If the `prevNode` has trailing comments, it inserts a blank line after the
247 * trailing comments.
248 *
249 * @param {RuleContext} context The rule context to report.
250 * @param {ASTNode} prevNode The previous node to check.
251 * @param {ASTNode} nextNode The next node to check.
252 * @param {Array<Token[]>} paddingLines The array of token pairs that blank
253 * lines exist between the pair.
254 * @returns {void}
255 * @private
256 */
257function verifyForAlways(context, prevNode, nextNode, paddingLines) {
258 if (paddingLines.length > 0) {
259 return;
260 }
261
262 context.report({
263 node: nextNode,
264 message: "Expected blank line before this statement.",
265 fix(fixer) {
266 const sourceCode = context.getSourceCode();
267 let prevToken = getActualLastToken(sourceCode, prevNode);
268 const nextToken = sourceCode.getFirstTokenBetween(
269 prevToken,
270 nextNode,
271 {
272 includeComments: true,
273
274 /**
275 * Skip the trailing comments of the previous node.
276 * This inserts a blank line after the last trailing comment.
277 *
278 * For example:
279 *
280 * foo(); // trailing comment.
281 * // comment.
282 * bar();
283 *
284 * Get fixed to:
285 *
286 * foo(); // trailing comment.
287 *
288 * // comment.
289 * bar();
290 *
291 * @param {Token} token The token to check.
292 * @returns {boolean} `true` if the token is not a trailing comment.
293 * @private
294 */
295 filter(token) {
296 if (astUtils.isTokenOnSameLine(prevToken, token)) {
297 prevToken = token;
298 return false;
299 }
300 return true;
301 }
302 }
303 ) || nextNode;
304 const insertText = astUtils.isTokenOnSameLine(prevToken, nextToken)
305 ? "\n\n"
306 : "\n";
307
308 return fixer.insertTextAfter(prevToken, insertText);
309 }
310 });
311}
312
313/**
314 * Types of blank lines.
315 * `any`, `never`, and `always` are defined.
316 * Those have `verify` method to check and report statements.
317 * @private
318 */
319const PaddingTypes = {
320 any: { verify: verifyForAny },
321 never: { verify: verifyForNever },
322 always: { verify: verifyForAlways }
323};
324
325/**
326 * Types of statements.
327 * Those have `test` method to check it matches to the given statement.
328 * @private
329 */
330const StatementTypes = {
331 "*": { test: () => true },
332 "block-like": {
333 test: (node, sourceCode) => isBlockLikeStatement(sourceCode, node)
334 },
335 "cjs-export": {
336 test: (node, sourceCode) =>
337 node.type === "ExpressionStatement" &&
338 node.expression.type === "AssignmentExpression" &&
339 CJS_EXPORT.test(sourceCode.getText(node.expression.left))
340 },
341 "cjs-import": {
342 test: (node, sourceCode) =>
343 node.type === "VariableDeclaration" &&
344 node.declarations.length > 0 &&
345 Boolean(node.declarations[0].init) &&
346 CJS_IMPORT.test(sourceCode.getText(node.declarations[0].init))
347 },
348 directive: {
349 test: isDirectivePrologue
350 },
351 expression: {
352 test: (node, sourceCode) =>
353 node.type === "ExpressionStatement" &&
354 !isDirectivePrologue(node, sourceCode)
355 },
356 "multiline-block-like": {
357 test: (node, sourceCode) =>
358 node.loc.start.line !== node.loc.end.line &&
359 isBlockLikeStatement(sourceCode, node)
360 },
361 "multiline-expression": {
362 test: (node, sourceCode) =>
363 node.loc.start.line !== node.loc.end.line &&
364 node.type === "ExpressionStatement" &&
365 !isDirectivePrologue(node, sourceCode)
366 },
367
368 block: newNodeTypeTester("BlockStatement"),
369 empty: newNodeTypeTester("EmptyStatement"),
370
371 break: newKeywordTester("break"),
372 case: newKeywordTester("case"),
373 class: newKeywordTester("class"),
374 const: newKeywordTester("const"),
375 continue: newKeywordTester("continue"),
376 debugger: newKeywordTester("debugger"),
377 default: newKeywordTester("default"),
378 do: newKeywordTester("do"),
379 export: newKeywordTester("export"),
380 for: newKeywordTester("for"),
381 function: newKeywordTester("function"),
382 if: newKeywordTester("if"),
383 import: newKeywordTester("import"),
384 let: newKeywordTester("let"),
385 return: newKeywordTester("return"),
386 switch: newKeywordTester("switch"),
387 throw: newKeywordTester("throw"),
388 try: newKeywordTester("try"),
389 var: newKeywordTester("var"),
390 while: newKeywordTester("while"),
391 with: newKeywordTester("with")
392};
393
394//------------------------------------------------------------------------------
395// Rule Definition
396//------------------------------------------------------------------------------
397
398module.exports = {
399 meta: {
400 docs: {
401 description: "require or disallow padding lines between statements",
402 category: "Stylistic Issues",
403 recommended: false,
404 url: "https://eslint.org/docs/rules/padding-line-between-statements"
405 },
406 fixable: "whitespace",
407 schema: {
408 definitions: {
409 paddingType: {
410 enum: Object.keys(PaddingTypes)
411 },
412 statementType: {
413 anyOf: [
414 { enum: Object.keys(StatementTypes) },
415 {
416 type: "array",
417 items: { enum: Object.keys(StatementTypes) },
418 minItems: 1,
419 uniqueItems: true,
420 additionalItems: false
421 }
422 ]
423 }
424 },
425 type: "array",
426 items: {
427 type: "object",
428 properties: {
429 blankLine: { $ref: "#/definitions/paddingType" },
430 prev: { $ref: "#/definitions/statementType" },
431 next: { $ref: "#/definitions/statementType" }
432 },
433 additionalProperties: false,
434 required: ["blankLine", "prev", "next"]
435 },
436 additionalItems: false
437 }
438 },
439
440 create(context) {
441 const sourceCode = context.getSourceCode();
442 const configureList = context.options || [];
443 let scopeInfo = null;
444
445 /**
446 * Processes to enter to new scope.
447 * This manages the current previous statement.
448 * @returns {void}
449 * @private
450 */
451 function enterScope() {
452 scopeInfo = {
453 upper: scopeInfo,
454 prevNode: null
455 };
456 }
457
458 /**
459 * Processes to exit from the current scope.
460 * @returns {void}
461 * @private
462 */
463 function exitScope() {
464 scopeInfo = scopeInfo.upper;
465 }
466
467 /**
468 * Checks whether the given node matches the given type.
469 *
470 * @param {ASTNode} node The statement node to check.
471 * @param {string|string[]} type The statement type to check.
472 * @returns {boolean} `true` if the statement node matched the type.
473 * @private
474 */
475 function match(node, type) {
476 let innerStatementNode = node;
477
478 while (innerStatementNode.type === "LabeledStatement") {
479 innerStatementNode = innerStatementNode.body;
480 }
481 if (Array.isArray(type)) {
482 return type.some(match.bind(null, innerStatementNode));
483 }
484 return StatementTypes[type].test(innerStatementNode, sourceCode);
485 }
486
487 /**
488 * Finds the last matched configure from configureList.
489 *
490 * @param {ASTNode} prevNode The previous statement to match.
491 * @param {ASTNode} nextNode The current statement to match.
492 * @returns {Object} The tester of the last matched configure.
493 * @private
494 */
495 function getPaddingType(prevNode, nextNode) {
496 for (let i = configureList.length - 1; i >= 0; --i) {
497 const configure = configureList[i];
498 const matched =
499 match(prevNode, configure.prev) &&
500 match(nextNode, configure.next);
501
502 if (matched) {
503 return PaddingTypes[configure.blankLine];
504 }
505 }
506 return PaddingTypes.any;
507 }
508
509 /**
510 * Gets padding line sequences between the given 2 statements.
511 * Comments are separators of the padding line sequences.
512 *
513 * @param {ASTNode} prevNode The previous statement to count.
514 * @param {ASTNode} nextNode The current statement to count.
515 * @returns {Array<Token[]>} The array of token pairs.
516 * @private
517 */
518 function getPaddingLineSequences(prevNode, nextNode) {
519 const pairs = [];
520 let prevToken = getActualLastToken(sourceCode, prevNode);
521
522 if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) {
523 do {
524 const token = sourceCode.getTokenAfter(
525 prevToken,
526 { includeComments: true }
527 );
528
529 if (token.loc.start.line - prevToken.loc.end.line >= 2) {
530 pairs.push([prevToken, token]);
531 }
532 prevToken = token;
533
534 } while (prevToken.range[0] < nextNode.range[0]);
535 }
536
537 return pairs;
538 }
539
540 /**
541 * Verify padding lines between the given node and the previous node.
542 *
543 * @param {ASTNode} node The node to verify.
544 * @returns {void}
545 * @private
546 */
547 function verify(node) {
548 const parentType = node.parent.type;
549 const validParent =
550 astUtils.STATEMENT_LIST_PARENTS.has(parentType) ||
551 parentType === "SwitchStatement";
552
553 if (!validParent) {
554 return;
555 }
556
557 // Save this node as the current previous statement.
558 const prevNode = scopeInfo.prevNode;
559
560 // Verify.
561 if (prevNode) {
562 const type = getPaddingType(prevNode, node);
563 const paddingLines = getPaddingLineSequences(prevNode, node);
564
565 type.verify(context, prevNode, node, paddingLines);
566 }
567
568 scopeInfo.prevNode = node;
569 }
570
571 /**
572 * Verify padding lines between the given node and the previous node.
573 * Then process to enter to new scope.
574 *
575 * @param {ASTNode} node The node to verify.
576 * @returns {void}
577 * @private
578 */
579 function verifyThenEnterScope(node) {
580 verify(node);
581 enterScope();
582 }
583
584 return {
585 Program: enterScope,
586 BlockStatement: enterScope,
587 SwitchStatement: enterScope,
588 "Program:exit": exitScope,
589 "BlockStatement:exit": exitScope,
590 "SwitchStatement:exit": exitScope,
591
592 ":statement": verify,
593
594 SwitchCase: verifyThenEnterScope,
595 "SwitchCase:exit": exitScope
596 };
597 }
598};