You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
307 lines
11 KiB
307 lines
11 KiB
/** |
|
* @fileoverview A rule to ensure blank lines within blocks. |
|
* @author Mathias Schreck <https://github.com/lo1tuma> |
|
*/ |
|
|
|
"use strict"; |
|
|
|
//------------------------------------------------------------------------------ |
|
// Requirements |
|
//------------------------------------------------------------------------------ |
|
|
|
const astUtils = require("./utils/ast-utils"); |
|
|
|
//------------------------------------------------------------------------------ |
|
// Rule Definition |
|
//------------------------------------------------------------------------------ |
|
|
|
/** @type {import('../shared/types').Rule} */ |
|
module.exports = { |
|
meta: { |
|
type: "layout", |
|
|
|
docs: { |
|
description: "Require or disallow padding within blocks", |
|
recommended: false, |
|
url: "https://eslint.org/docs/rules/padded-blocks" |
|
}, |
|
|
|
fixable: "whitespace", |
|
|
|
schema: [ |
|
{ |
|
oneOf: [ |
|
{ |
|
enum: ["always", "never"] |
|
}, |
|
{ |
|
type: "object", |
|
properties: { |
|
blocks: { |
|
enum: ["always", "never"] |
|
}, |
|
switches: { |
|
enum: ["always", "never"] |
|
}, |
|
classes: { |
|
enum: ["always", "never"] |
|
} |
|
}, |
|
additionalProperties: false, |
|
minProperties: 1 |
|
} |
|
] |
|
}, |
|
{ |
|
type: "object", |
|
properties: { |
|
allowSingleLineBlocks: { |
|
type: "boolean" |
|
} |
|
}, |
|
additionalProperties: false |
|
} |
|
], |
|
|
|
messages: { |
|
alwaysPadBlock: "Block must be padded by blank lines.", |
|
neverPadBlock: "Block must not be padded by blank lines." |
|
} |
|
}, |
|
|
|
create(context) { |
|
const options = {}; |
|
const typeOptions = context.options[0] || "always"; |
|
const exceptOptions = context.options[1] || {}; |
|
|
|
if (typeof typeOptions === "string") { |
|
const shouldHavePadding = typeOptions === "always"; |
|
|
|
options.blocks = shouldHavePadding; |
|
options.switches = shouldHavePadding; |
|
options.classes = shouldHavePadding; |
|
} else { |
|
if (Object.prototype.hasOwnProperty.call(typeOptions, "blocks")) { |
|
options.blocks = typeOptions.blocks === "always"; |
|
} |
|
if (Object.prototype.hasOwnProperty.call(typeOptions, "switches")) { |
|
options.switches = typeOptions.switches === "always"; |
|
} |
|
if (Object.prototype.hasOwnProperty.call(typeOptions, "classes")) { |
|
options.classes = typeOptions.classes === "always"; |
|
} |
|
} |
|
|
|
if (Object.prototype.hasOwnProperty.call(exceptOptions, "allowSingleLineBlocks")) { |
|
options.allowSingleLineBlocks = exceptOptions.allowSingleLineBlocks === true; |
|
} |
|
|
|
const sourceCode = context.getSourceCode(); |
|
|
|
/** |
|
* Gets the open brace token from a given node. |
|
* @param {ASTNode} node A BlockStatement or SwitchStatement node from which to get the open brace. |
|
* @returns {Token} The token of the open brace. |
|
*/ |
|
function getOpenBrace(node) { |
|
if (node.type === "SwitchStatement") { |
|
return sourceCode.getTokenBefore(node.cases[0]); |
|
} |
|
|
|
if (node.type === "StaticBlock") { |
|
return sourceCode.getFirstToken(node, { skip: 1 }); // skip the `static` token |
|
} |
|
|
|
// `BlockStatement` or `ClassBody` |
|
return sourceCode.getFirstToken(node); |
|
} |
|
|
|
/** |
|
* Checks if the given parameter is a comment node |
|
* @param {ASTNode|Token} node An AST node or token |
|
* @returns {boolean} True if node is a comment |
|
*/ |
|
function isComment(node) { |
|
return node.type === "Line" || node.type === "Block"; |
|
} |
|
|
|
/** |
|
* Checks if there is padding between two tokens |
|
* @param {Token} first The first token |
|
* @param {Token} second The second token |
|
* @returns {boolean} True if there is at least a line between the tokens |
|
*/ |
|
function isPaddingBetweenTokens(first, second) { |
|
return second.loc.start.line - first.loc.end.line >= 2; |
|
} |
|
|
|
|
|
/** |
|
* Checks if the given token has a blank line after it. |
|
* @param {Token} token The token to check. |
|
* @returns {boolean} Whether or not the token is followed by a blank line. |
|
*/ |
|
function getFirstBlockToken(token) { |
|
let prev, |
|
first = token; |
|
|
|
do { |
|
prev = first; |
|
first = sourceCode.getTokenAfter(first, { includeComments: true }); |
|
} while (isComment(first) && first.loc.start.line === prev.loc.end.line); |
|
|
|
return first; |
|
} |
|
|
|
/** |
|
* Checks if the given token is preceded by a blank line. |
|
* @param {Token} token The token to check |
|
* @returns {boolean} Whether or not the token is preceded by a blank line |
|
*/ |
|
function getLastBlockToken(token) { |
|
let last = token, |
|
next; |
|
|
|
do { |
|
next = last; |
|
last = sourceCode.getTokenBefore(last, { includeComments: true }); |
|
} while (isComment(last) && last.loc.end.line === next.loc.start.line); |
|
|
|
return last; |
|
} |
|
|
|
/** |
|
* Checks if a node should be padded, according to the rule config. |
|
* @param {ASTNode} node The AST node to check. |
|
* @throws {Error} (Unreachable) |
|
* @returns {boolean} True if the node should be padded, false otherwise. |
|
*/ |
|
function requirePaddingFor(node) { |
|
switch (node.type) { |
|
case "BlockStatement": |
|
case "StaticBlock": |
|
return options.blocks; |
|
case "SwitchStatement": |
|
return options.switches; |
|
case "ClassBody": |
|
return options.classes; |
|
|
|
/* istanbul ignore next */ |
|
default: |
|
throw new Error("unreachable"); |
|
} |
|
} |
|
|
|
/** |
|
* Checks the given BlockStatement node to be padded if the block is not empty. |
|
* @param {ASTNode} node The AST node of a BlockStatement. |
|
* @returns {void} undefined. |
|
*/ |
|
function checkPadding(node) { |
|
const openBrace = getOpenBrace(node), |
|
firstBlockToken = getFirstBlockToken(openBrace), |
|
tokenBeforeFirst = sourceCode.getTokenBefore(firstBlockToken, { includeComments: true }), |
|
closeBrace = sourceCode.getLastToken(node), |
|
lastBlockToken = getLastBlockToken(closeBrace), |
|
tokenAfterLast = sourceCode.getTokenAfter(lastBlockToken, { includeComments: true }), |
|
blockHasTopPadding = isPaddingBetweenTokens(tokenBeforeFirst, firstBlockToken), |
|
blockHasBottomPadding = isPaddingBetweenTokens(lastBlockToken, tokenAfterLast); |
|
|
|
if (options.allowSingleLineBlocks && astUtils.isTokenOnSameLine(tokenBeforeFirst, tokenAfterLast)) { |
|
return; |
|
} |
|
|
|
if (requirePaddingFor(node)) { |
|
|
|
if (!blockHasTopPadding) { |
|
context.report({ |
|
node, |
|
loc: { |
|
start: tokenBeforeFirst.loc.start, |
|
end: firstBlockToken.loc.start |
|
}, |
|
fix(fixer) { |
|
return fixer.insertTextAfter(tokenBeforeFirst, "\n"); |
|
}, |
|
messageId: "alwaysPadBlock" |
|
}); |
|
} |
|
if (!blockHasBottomPadding) { |
|
context.report({ |
|
node, |
|
loc: { |
|
end: tokenAfterLast.loc.start, |
|
start: lastBlockToken.loc.end |
|
}, |
|
fix(fixer) { |
|
return fixer.insertTextBefore(tokenAfterLast, "\n"); |
|
}, |
|
messageId: "alwaysPadBlock" |
|
}); |
|
} |
|
} else { |
|
if (blockHasTopPadding) { |
|
|
|
context.report({ |
|
node, |
|
loc: { |
|
start: tokenBeforeFirst.loc.start, |
|
end: firstBlockToken.loc.start |
|
}, |
|
fix(fixer) { |
|
return fixer.replaceTextRange([tokenBeforeFirst.range[1], firstBlockToken.range[0] - firstBlockToken.loc.start.column], "\n"); |
|
}, |
|
messageId: "neverPadBlock" |
|
}); |
|
} |
|
|
|
if (blockHasBottomPadding) { |
|
|
|
context.report({ |
|
node, |
|
loc: { |
|
end: tokenAfterLast.loc.start, |
|
start: lastBlockToken.loc.end |
|
}, |
|
messageId: "neverPadBlock", |
|
fix(fixer) { |
|
return fixer.replaceTextRange([lastBlockToken.range[1], tokenAfterLast.range[0] - tokenAfterLast.loc.start.column], "\n"); |
|
} |
|
}); |
|
} |
|
} |
|
} |
|
|
|
const rule = {}; |
|
|
|
if (Object.prototype.hasOwnProperty.call(options, "switches")) { |
|
rule.SwitchStatement = function(node) { |
|
if (node.cases.length === 0) { |
|
return; |
|
} |
|
checkPadding(node); |
|
}; |
|
} |
|
|
|
if (Object.prototype.hasOwnProperty.call(options, "blocks")) { |
|
rule.BlockStatement = function(node) { |
|
if (node.body.length === 0) { |
|
return; |
|
} |
|
checkPadding(node); |
|
}; |
|
rule.StaticBlock = rule.BlockStatement; |
|
} |
|
|
|
if (Object.prototype.hasOwnProperty.call(options, "classes")) { |
|
rule.ClassBody = function(node) { |
|
if (node.body.length === 0) { |
|
return; |
|
} |
|
checkPadding(node); |
|
}; |
|
} |
|
|
|
return rule; |
|
} |
|
};
|
|
|