UNPKG

19.1 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 while (node.type === "LabeledStatement") {
477 node = node.body;
478 }
479 if (Array.isArray(type)) {
480 return type.some(match.bind(null, node));
481 }
482 return StatementTypes[type].test(node, sourceCode);
483 }
484
485 /**
486 * Finds the last matched configure from configureList.
487 *
488 * @param {ASTNode} prevNode The previous statement to match.
489 * @param {ASTNode} nextNode The current statement to match.
490 * @returns {Object} The tester of the last matched configure.
491 * @private
492 */
493 function getPaddingType(prevNode, nextNode) {
494 for (let i = configureList.length - 1; i >= 0; --i) {
495 const configure = configureList[i];
496 const matched =
497 match(prevNode, configure.prev) &&
498 match(nextNode, configure.next);
499
500 if (matched) {
501 return PaddingTypes[configure.blankLine];
502 }
503 }
504 return PaddingTypes.any;
505 }
506
507 /**
508 * Gets padding line sequences between the given 2 statements.
509 * Comments are separators of the padding line sequences.
510 *
511 * @param {ASTNode} prevNode The previous statement to count.
512 * @param {ASTNode} nextNode The current statement to count.
513 * @returns {Array<Token[]>} The array of token pairs.
514 * @private
515 */
516 function getPaddingLineSequences(prevNode, nextNode) {
517 const pairs = [];
518 let prevToken = getActualLastToken(sourceCode, prevNode);
519
520 if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) {
521 do {
522 const token = sourceCode.getTokenAfter(
523 prevToken,
524 { includeComments: true }
525 );
526
527 if (token.loc.start.line - prevToken.loc.end.line >= 2) {
528 pairs.push([prevToken, token]);
529 }
530 prevToken = token;
531
532 } while (prevToken.range[0] < nextNode.range[0]);
533 }
534
535 return pairs;
536 }
537
538 /**
539 * Verify padding lines between the given node and the previous node.
540 *
541 * @param {ASTNode} node The node to verify.
542 * @returns {void}
543 * @private
544 */
545 function verify(node) {
546 const parentType = node.parent.type;
547 const validParent =
548 astUtils.STATEMENT_LIST_PARENTS.has(parentType) ||
549 parentType === "SwitchStatement";
550
551 if (!validParent) {
552 return;
553 }
554
555 // Save this node as the current previous statement.
556 const prevNode = scopeInfo.prevNode;
557
558 // Verify.
559 if (prevNode) {
560 const type = getPaddingType(prevNode, node);
561 const paddingLines = getPaddingLineSequences(prevNode, node);
562
563 type.verify(context, prevNode, node, paddingLines);
564 }
565
566 scopeInfo.prevNode = node;
567 }
568
569 /**
570 * Verify padding lines between the given node and the previous node.
571 * Then process to enter to new scope.
572 *
573 * @param {ASTNode} node The node to verify.
574 * @returns {void}
575 * @private
576 */
577 function verifyThenEnterScope(node) {
578 verify(node);
579 enterScope();
580 }
581
582 return {
583 Program: enterScope,
584 BlockStatement: enterScope,
585 SwitchStatement: enterScope,
586 "Program:exit": exitScope,
587 "BlockStatement:exit": exitScope,
588 "SwitchStatement:exit": exitScope,
589
590 ":statement": verify,
591
592 SwitchCase: verifyThenEnterScope,
593 "SwitchCase:exit": exitScope
594 };
595 }
596};