]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
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") { | |
6f036462 | 88 | let call = astUtils.skipChainExpression(node.expression); |
eb39fafa DC |
89 | |
90 | if (call.type === "UnaryExpression") { | |
6f036462 | 91 | call = astUtils.skipChainExpression(call.argument); |
eb39fafa DC |
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]; | |
f2a92ac6 | 256 | const text = context.sourceCode.text |
eb39fafa DC |
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) { | |
f2a92ac6 | 287 | const sourceCode = context.sourceCode; |
eb39fafa DC |
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 | ||
34eeec05 | 428 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
429 | module.exports = { |
430 | meta: { | |
431 | type: "layout", | |
432 | ||
433 | docs: { | |
8f9d1d4d | 434 | description: "Require or disallow padding lines between statements", |
eb39fafa | 435 | recommended: false, |
f2a92ac6 | 436 | url: "https://eslint.org/docs/latest/rules/padding-line-between-statements" |
eb39fafa DC |
437 | }, |
438 | ||
439 | fixable: "whitespace", | |
440 | ||
441 | schema: { | |
442 | definitions: { | |
443 | paddingType: { | |
444 | enum: Object.keys(PaddingTypes) | |
445 | }, | |
446 | statementType: { | |
447 | anyOf: [ | |
448 | { enum: Object.keys(StatementTypes) }, | |
449 | { | |
450 | type: "array", | |
451 | items: { enum: Object.keys(StatementTypes) }, | |
452 | minItems: 1, | |
8f9d1d4d | 453 | uniqueItems: true |
eb39fafa DC |
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"] | |
8f9d1d4d | 468 | } |
eb39fafa DC |
469 | }, |
470 | ||
471 | messages: { | |
472 | unexpectedBlankLine: "Unexpected blank line before this statement.", | |
473 | expectedBlankLine: "Expected blank line before this statement." | |
474 | } | |
475 | }, | |
476 | ||
477 | create(context) { | |
f2a92ac6 | 478 | const sourceCode = context.sourceCode; |
eb39fafa DC |
479 | const configureList = context.options || []; |
480 | let scopeInfo = null; | |
481 | ||
482 | /** | |
483 | * Processes to enter to new scope. | |
484 | * This manages the current previous statement. | |
485 | * @returns {void} | |
486 | * @private | |
487 | */ | |
488 | function enterScope() { | |
489 | scopeInfo = { | |
490 | upper: scopeInfo, | |
491 | prevNode: null | |
492 | }; | |
493 | } | |
494 | ||
495 | /** | |
496 | * Processes to exit from the current scope. | |
497 | * @returns {void} | |
498 | * @private | |
499 | */ | |
500 | function exitScope() { | |
501 | scopeInfo = scopeInfo.upper; | |
502 | } | |
503 | ||
504 | /** | |
505 | * Checks whether the given node matches the given type. | |
506 | * @param {ASTNode} node The statement node to check. | |
507 | * @param {string|string[]} type The statement type to check. | |
508 | * @returns {boolean} `true` if the statement node matched the type. | |
509 | * @private | |
510 | */ | |
511 | function match(node, type) { | |
512 | let innerStatementNode = node; | |
513 | ||
514 | while (innerStatementNode.type === "LabeledStatement") { | |
515 | innerStatementNode = innerStatementNode.body; | |
516 | } | |
517 | if (Array.isArray(type)) { | |
518 | return type.some(match.bind(null, innerStatementNode)); | |
519 | } | |
520 | return StatementTypes[type].test(innerStatementNode, sourceCode); | |
521 | } | |
522 | ||
523 | /** | |
524 | * Finds the last matched configure from configureList. | |
525 | * @param {ASTNode} prevNode The previous statement to match. | |
526 | * @param {ASTNode} nextNode The current statement to match. | |
527 | * @returns {Object} The tester of the last matched configure. | |
528 | * @private | |
529 | */ | |
530 | function getPaddingType(prevNode, nextNode) { | |
531 | for (let i = configureList.length - 1; i >= 0; --i) { | |
532 | const configure = configureList[i]; | |
533 | const matched = | |
534 | match(prevNode, configure.prev) && | |
535 | match(nextNode, configure.next); | |
536 | ||
537 | if (matched) { | |
538 | return PaddingTypes[configure.blankLine]; | |
539 | } | |
540 | } | |
541 | return PaddingTypes.any; | |
542 | } | |
543 | ||
544 | /** | |
545 | * Gets padding line sequences between the given 2 statements. | |
546 | * Comments are separators of the padding line sequences. | |
547 | * @param {ASTNode} prevNode The previous statement to count. | |
548 | * @param {ASTNode} nextNode The current statement to count. | |
549 | * @returns {Array<Token[]>} The array of token pairs. | |
550 | * @private | |
551 | */ | |
552 | function getPaddingLineSequences(prevNode, nextNode) { | |
553 | const pairs = []; | |
554 | let prevToken = getActualLastToken(sourceCode, prevNode); | |
555 | ||
556 | if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) { | |
557 | do { | |
558 | const token = sourceCode.getTokenAfter( | |
559 | prevToken, | |
560 | { includeComments: true } | |
561 | ); | |
562 | ||
563 | if (token.loc.start.line - prevToken.loc.end.line >= 2) { | |
564 | pairs.push([prevToken, token]); | |
565 | } | |
566 | prevToken = token; | |
567 | ||
568 | } while (prevToken.range[0] < nextNode.range[0]); | |
569 | } | |
570 | ||
571 | return pairs; | |
572 | } | |
573 | ||
574 | /** | |
575 | * Verify padding lines between the given node and the previous node. | |
576 | * @param {ASTNode} node The node to verify. | |
577 | * @returns {void} | |
578 | * @private | |
579 | */ | |
580 | function verify(node) { | |
581 | const parentType = node.parent.type; | |
582 | const validParent = | |
583 | astUtils.STATEMENT_LIST_PARENTS.has(parentType) || | |
584 | parentType === "SwitchStatement"; | |
585 | ||
586 | if (!validParent) { | |
587 | return; | |
588 | } | |
589 | ||
590 | // Save this node as the current previous statement. | |
591 | const prevNode = scopeInfo.prevNode; | |
592 | ||
593 | // Verify. | |
594 | if (prevNode) { | |
595 | const type = getPaddingType(prevNode, node); | |
596 | const paddingLines = getPaddingLineSequences(prevNode, node); | |
597 | ||
598 | type.verify(context, prevNode, node, paddingLines); | |
599 | } | |
600 | ||
601 | scopeInfo.prevNode = node; | |
602 | } | |
603 | ||
604 | /** | |
605 | * Verify padding lines between the given node and the previous node. | |
606 | * Then process to enter to new scope. | |
607 | * @param {ASTNode} node The node to verify. | |
608 | * @returns {void} | |
609 | * @private | |
610 | */ | |
611 | function verifyThenEnterScope(node) { | |
612 | verify(node); | |
613 | enterScope(); | |
614 | } | |
615 | ||
616 | return { | |
617 | Program: enterScope, | |
618 | BlockStatement: enterScope, | |
619 | SwitchStatement: enterScope, | |
609c276f | 620 | StaticBlock: enterScope, |
eb39fafa DC |
621 | "Program:exit": exitScope, |
622 | "BlockStatement:exit": exitScope, | |
623 | "SwitchStatement:exit": exitScope, | |
609c276f | 624 | "StaticBlock:exit": exitScope, |
eb39fafa DC |
625 | |
626 | ":statement": verify, | |
627 | ||
628 | SwitchCase: verifyThenEnterScope, | |
629 | "SwitchCase:exit": exitScope | |
630 | }; | |
631 | } | |
632 | }; |