]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/padding-line-between-statements.js
42859dd38b0b8fa12307c664d77f321c4cf728eb
[pve-eslint.git] / eslint / lib / rules / padding-line-between-statements.js
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
12 const astUtils = require("./utils/ast-utils");
13
14 //------------------------------------------------------------------------------
15 // Helpers
16 //------------------------------------------------------------------------------
17
18 const LT = `[${Array.from(astUtils.LINEBREAKS).join("")}]`;
19 const PADDING_LINE_SEQUENCE = new RegExp(
20 String.raw`^(\s*?${LT})\s*${LT}(\s*;?)$`,
21 "u"
22 );
23 const CJS_EXPORT = /^(?:module\s*\.\s*)?exports(?:\s*\.|\s*\[|$)/u;
24 const CJS_IMPORT = /^require\(/u;
25
26 /**
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.
30 * @private
31 */
32 function 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 starts with specific keyword and spans a single line.
41 * @param {string} keyword The keyword to test.
42 * @returns {Object} the created tester.
43 * @private
44 */
45 function newSinglelineKeywordTester(keyword) {
46 return {
47 test: (node, sourceCode) =>
48 node.loc.start.line === node.loc.end.line &&
49 sourceCode.getFirstToken(node).value === keyword
50 };
51 }
52
53 /**
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.
57 * @private
58 */
59 function newMultilineKeywordTester(keyword) {
60 return {
61 test: (node, sourceCode) =>
62 node.loc.start.line !== node.loc.end.line &&
63 sourceCode.getFirstToken(node).value === keyword
64 };
65 }
66
67 /**
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.
71 * @private
72 */
73 function newNodeTypeTester(type) {
74 return {
75 test: node =>
76 node.type === type
77 };
78 }
79
80 /**
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.
84 * @private
85 */
86 function isIIFEStatement(node) {
87 if (node.type === "ExpressionStatement") {
88 let call = astUtils.skipChainExpression(node.expression);
89
90 if (call.type === "UnaryExpression") {
91 call = astUtils.skipChainExpression(call.argument);
92 }
93 return call.type === "CallExpression" && astUtils.isFunction(call.callee);
94 }
95 return false;
96 }
97
98 /**
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.
104 * @private
105 */
106 function isBlockLikeStatement(sourceCode, node) {
107
108 // do-while with a block is a block-like statement.
109 if (node.type === "DoWhileStatement" && node.body.type === "BlockStatement") {
110 return true;
111 }
112
113 /*
114 * IIFE is a block-like statement specially from
115 * JSCS#disallowPaddingNewLinesAfterBlocks.
116 */
117 if (isIIFEStatement(node)) {
118 return true;
119 }
120
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])
125 : null;
126
127 return Boolean(belongingNode) && (
128 belongingNode.type === "BlockStatement" ||
129 belongingNode.type === "SwitchStatement"
130 );
131 }
132
133 /**
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.
138 */
139 function isDirective(node, sourceCode) {
140 return (
141 node.type === "ExpressionStatement" &&
142 (
143 node.parent.type === "Program" ||
144 (
145 node.parent.type === "BlockStatement" &&
146 astUtils.isFunction(node.parent.parent)
147 )
148 ) &&
149 node.expression.type === "Literal" &&
150 typeof node.expression.value === "string" &&
151 !astUtils.isParenthesised(sourceCode, node.expression)
152 );
153 }
154
155 /**
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.
160 */
161 function isDirectivePrologue(node, sourceCode) {
162 if (isDirective(node, sourceCode)) {
163 for (const sibling of node.parent.body) {
164 if (sibling === node) {
165 break;
166 }
167 if (!isDirective(sibling, sourceCode)) {
168 return false;
169 }
170 }
171 return true;
172 }
173 return false;
174 }
175
176 /**
177 * Gets the actual last token.
178 *
179 * If a semicolon is semicolon-less style's semicolon, this ignores it.
180 * For example:
181 *
182 * foo()
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.
187 * @private
188 */
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(
194 prevToken &&
195 nextToken &&
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
200 );
201
202 return isSemicolonLessStyle ? prevToken : semiToken;
203 }
204
205 /**
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.
211 * @private
212 */
213 function replacerToRemovePaddingLines(_, trailingSpaces, indentSpaces) {
214 return trailingSpaces + indentSpaces;
215 }
216
217 /**
218 * Check and report statements for `any` configuration.
219 * It does nothing.
220 * @returns {void}
221 * @private
222 */
223 function verifyForAny() {
224 }
225
226 /**
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.
236 * @returns {void}
237 * @private
238 */
239 function verifyForNever(context, _, nextNode, paddingLines) {
240 if (paddingLines.length === 0) {
241 return;
242 }
243
244 context.report({
245 node: nextNode,
246 messageId: "unexpectedBlankLine",
247 fix(fixer) {
248 if (paddingLines.length >= 2) {
249 return null;
250 }
251
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
257 .slice(start, end)
258 .replace(PADDING_LINE_SEQUENCE, replacerToRemovePaddingLines);
259
260 return fixer.replaceTextRange([start, end], text);
261 }
262 });
263 }
264
265 /**
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
269 * trailing comments.
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.
275 * @returns {void}
276 * @private
277 */
278 function verifyForAlways(context, prevNode, nextNode, paddingLines) {
279 if (paddingLines.length > 0) {
280 return;
281 }
282
283 context.report({
284 node: nextNode,
285 messageId: "expectedBlankLine",
286 fix(fixer) {
287 const sourceCode = context.getSourceCode();
288 let prevToken = getActualLastToken(sourceCode, prevNode);
289 const nextToken = sourceCode.getFirstTokenBetween(
290 prevToken,
291 nextNode,
292 {
293 includeComments: true,
294
295 /**
296 * Skip the trailing comments of the previous node.
297 * This inserts a blank line after the last trailing comment.
298 *
299 * For example:
300 *
301 * foo(); // trailing comment.
302 * // comment.
303 * bar();
304 *
305 * Get fixed to:
306 *
307 * foo(); // trailing comment.
308 *
309 * // comment.
310 * bar();
311 * @param {Token} token The token to check.
312 * @returns {boolean} `true` if the token is not a trailing comment.
313 * @private
314 */
315 filter(token) {
316 if (astUtils.isTokenOnSameLine(prevToken, token)) {
317 prevToken = token;
318 return false;
319 }
320 return true;
321 }
322 }
323 ) || nextNode;
324 const insertText = astUtils.isTokenOnSameLine(prevToken, nextToken)
325 ? "\n\n"
326 : "\n";
327
328 return fixer.insertTextAfter(prevToken, insertText);
329 }
330 });
331 }
332
333 /**
334 * Types of blank lines.
335 * `any`, `never`, and `always` are defined.
336 * Those have `verify` method to check and report statements.
337 * @private
338 */
339 const PaddingTypes = {
340 any: { verify: verifyForAny },
341 never: { verify: verifyForNever },
342 always: { verify: verifyForAlways }
343 };
344
345 /**
346 * Types of statements.
347 * Those have `test` method to check it matches to the given statement.
348 * @private
349 */
350 const StatementTypes = {
351 "*": { test: () => true },
352 "block-like": {
353 test: (node, sourceCode) => isBlockLikeStatement(sourceCode, node)
354 },
355 "cjs-export": {
356 test: (node, sourceCode) =>
357 node.type === "ExpressionStatement" &&
358 node.expression.type === "AssignmentExpression" &&
359 CJS_EXPORT.test(sourceCode.getText(node.expression.left))
360 },
361 "cjs-import": {
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))
367 },
368 directive: {
369 test: isDirectivePrologue
370 },
371 expression: {
372 test: (node, sourceCode) =>
373 node.type === "ExpressionStatement" &&
374 !isDirectivePrologue(node, sourceCode)
375 },
376 iife: {
377 test: isIIFEStatement
378 },
379 "multiline-block-like": {
380 test: (node, sourceCode) =>
381 node.loc.start.line !== node.loc.end.line &&
382 isBlockLikeStatement(sourceCode, node)
383 },
384 "multiline-expression": {
385 test: (node, sourceCode) =>
386 node.loc.start.line !== node.loc.end.line &&
387 node.type === "ExpressionStatement" &&
388 !isDirectivePrologue(node, sourceCode)
389 },
390
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"),
397
398 block: newNodeTypeTester("BlockStatement"),
399 empty: newNodeTypeTester("EmptyStatement"),
400 function: newNodeTypeTester("FunctionDeclaration"),
401
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")
422 };
423
424 //------------------------------------------------------------------------------
425 // Rule Definition
426 //------------------------------------------------------------------------------
427
428 module.exports = {
429 meta: {
430 type: "layout",
431
432 docs: {
433 description: "require or disallow padding lines between statements",
434 recommended: false,
435 url: "https://eslint.org/docs/rules/padding-line-between-statements"
436 },
437
438 fixable: "whitespace",
439
440 schema: {
441 definitions: {
442 paddingType: {
443 enum: Object.keys(PaddingTypes)
444 },
445 statementType: {
446 anyOf: [
447 { enum: Object.keys(StatementTypes) },
448 {
449 type: "array",
450 items: { enum: Object.keys(StatementTypes) },
451 minItems: 1,
452 uniqueItems: true,
453 additionalItems: false
454 }
455 ]
456 }
457 },
458 type: "array",
459 items: {
460 type: "object",
461 properties: {
462 blankLine: { $ref: "#/definitions/paddingType" },
463 prev: { $ref: "#/definitions/statementType" },
464 next: { $ref: "#/definitions/statementType" }
465 },
466 additionalProperties: false,
467 required: ["blankLine", "prev", "next"]
468 },
469 additionalItems: false
470 },
471
472 messages: {
473 unexpectedBlankLine: "Unexpected blank line before this statement.",
474 expectedBlankLine: "Expected blank line before this statement."
475 }
476 },
477
478 create(context) {
479 const sourceCode = context.getSourceCode();
480 const configureList = context.options || [];
481 let scopeInfo = null;
482
483 /**
484 * Processes to enter to new scope.
485 * This manages the current previous statement.
486 * @returns {void}
487 * @private
488 */
489 function enterScope() {
490 scopeInfo = {
491 upper: scopeInfo,
492 prevNode: null
493 };
494 }
495
496 /**
497 * Processes to exit from the current scope.
498 * @returns {void}
499 * @private
500 */
501 function exitScope() {
502 scopeInfo = scopeInfo.upper;
503 }
504
505 /**
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.
510 * @private
511 */
512 function match(node, type) {
513 let innerStatementNode = node;
514
515 while (innerStatementNode.type === "LabeledStatement") {
516 innerStatementNode = innerStatementNode.body;
517 }
518 if (Array.isArray(type)) {
519 return type.some(match.bind(null, innerStatementNode));
520 }
521 return StatementTypes[type].test(innerStatementNode, sourceCode);
522 }
523
524 /**
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.
529 * @private
530 */
531 function getPaddingType(prevNode, nextNode) {
532 for (let i = configureList.length - 1; i >= 0; --i) {
533 const configure = configureList[i];
534 const matched =
535 match(prevNode, configure.prev) &&
536 match(nextNode, configure.next);
537
538 if (matched) {
539 return PaddingTypes[configure.blankLine];
540 }
541 }
542 return PaddingTypes.any;
543 }
544
545 /**
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.
551 * @private
552 */
553 function getPaddingLineSequences(prevNode, nextNode) {
554 const pairs = [];
555 let prevToken = getActualLastToken(sourceCode, prevNode);
556
557 if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) {
558 do {
559 const token = sourceCode.getTokenAfter(
560 prevToken,
561 { includeComments: true }
562 );
563
564 if (token.loc.start.line - prevToken.loc.end.line >= 2) {
565 pairs.push([prevToken, token]);
566 }
567 prevToken = token;
568
569 } while (prevToken.range[0] < nextNode.range[0]);
570 }
571
572 return pairs;
573 }
574
575 /**
576 * Verify padding lines between the given node and the previous node.
577 * @param {ASTNode} node The node to verify.
578 * @returns {void}
579 * @private
580 */
581 function verify(node) {
582 const parentType = node.parent.type;
583 const validParent =
584 astUtils.STATEMENT_LIST_PARENTS.has(parentType) ||
585 parentType === "SwitchStatement";
586
587 if (!validParent) {
588 return;
589 }
590
591 // Save this node as the current previous statement.
592 const prevNode = scopeInfo.prevNode;
593
594 // Verify.
595 if (prevNode) {
596 const type = getPaddingType(prevNode, node);
597 const paddingLines = getPaddingLineSequences(prevNode, node);
598
599 type.verify(context, prevNode, node, paddingLines);
600 }
601
602 scopeInfo.prevNode = node;
603 }
604
605 /**
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.
609 * @returns {void}
610 * @private
611 */
612 function verifyThenEnterScope(node) {
613 verify(node);
614 enterScope();
615 }
616
617 return {
618 Program: enterScope,
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,
626
627 ":statement": verify,
628
629 SwitchCase: verifyThenEnterScope,
630 "SwitchCase:exit": exitScope
631 };
632 }
633 };