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.

182 lines
7.8 KiB

2 years ago
/**
* @fileoverview Rule to check empty newline between class members
* @author 薛定谔的猫<hh_2013@foxmail.com>
*/
"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 an empty line between class members",
recommended: false,
url: "https://eslint.org/docs/rules/lines-between-class-members"
},
fixable: "whitespace",
schema: [
{
enum: ["always", "never"]
},
{
type: "object",
properties: {
exceptAfterSingleLine: {
type: "boolean",
default: false
}
},
additionalProperties: false
}
],
messages: {
never: "Unexpected blank line between class members.",
always: "Expected blank line between class members."
}
},
create(context) {
const options = [];
options[0] = context.options[0] || "always";
options[1] = context.options[1] || { exceptAfterSingleLine: false };
const sourceCode = context.getSourceCode();
/**
* Gets a pair of tokens that should be used to check lines between two class member nodes.
*
* In most cases, this returns the very last token of the current node and
* the very first token of the next node.
* For example:
*
* class C {
* x = 1; // curLast: `;` nextFirst: `in`
* in = 2
* }
*
* There is only one exception. If the given node ends with a semicolon, and it looks like
* a semicolon-less style's semicolon - one that is not on the same line as the preceding
* token, but is on the line where the next class member starts - this returns the preceding
* token and the semicolon as boundary tokens.
* For example:
*
* class C {
* x = 1 // curLast: `1` nextFirst: `;`
* ;in = 2
* }
* When determining the desired layout of the code, we should treat this semicolon as
* a part of the next class member node instead of the one it technically belongs to.
* @param {ASTNode} curNode Current class member node.
* @param {ASTNode} nextNode Next class member node.
* @returns {Token} The actual last token of `node`.
* @private
*/
function getBoundaryTokens(curNode, nextNode) {
const lastToken = sourceCode.getLastToken(curNode);
const prevToken = sourceCode.getTokenBefore(lastToken);
const nextToken = sourceCode.getFirstToken(nextNode); // skip possible lone `;` between nodes
const isSemicolonLessStyle = (
astUtils.isSemicolonToken(lastToken) &&
!astUtils.isTokenOnSameLine(prevToken, lastToken) &&
astUtils.isTokenOnSameLine(lastToken, nextToken)
);
return isSemicolonLessStyle
? { curLast: prevToken, nextFirst: lastToken }
: { curLast: lastToken, nextFirst: nextToken };
}
/**
* Return the last token among the consecutive tokens that have no exceed max line difference in between, before the first token in the next member.
* @param {Token} prevLastToken The last token in the previous member node.
* @param {Token} nextFirstToken The first token in the next member node.
* @param {number} maxLine The maximum number of allowed line difference between consecutive tokens.
* @returns {Token} The last token among the consecutive tokens.
*/
function findLastConsecutiveTokenAfter(prevLastToken, nextFirstToken, maxLine) {
const after = sourceCode.getTokenAfter(prevLastToken, { includeComments: true });
if (after !== nextFirstToken && after.loc.start.line - prevLastToken.loc.end.line <= maxLine) {
return findLastConsecutiveTokenAfter(after, nextFirstToken, maxLine);
}
return prevLastToken;
}
/**
* Return the first token among the consecutive tokens that have no exceed max line difference in between, after the last token in the previous member.
* @param {Token} nextFirstToken The first token in the next member node.
* @param {Token} prevLastToken The last token in the previous member node.
* @param {number} maxLine The maximum number of allowed line difference between consecutive tokens.
* @returns {Token} The first token among the consecutive tokens.
*/
function findFirstConsecutiveTokenBefore(nextFirstToken, prevLastToken, maxLine) {
const before = sourceCode.getTokenBefore(nextFirstToken, { includeComments: true });
if (before !== prevLastToken && nextFirstToken.loc.start.line - before.loc.end.line <= maxLine) {
return findFirstConsecutiveTokenBefore(before, prevLastToken, maxLine);
}
return nextFirstToken;
}
/**
* Checks if there is a token or comment between two tokens.
* @param {Token} before The token before.
* @param {Token} after The token after.
* @returns {boolean} True if there is a token or comment between two tokens.
*/
function hasTokenOrCommentBetween(before, after) {
return sourceCode.getTokensBetween(before, after, { includeComments: true }).length !== 0;
}
return {
ClassBody(node) {
const body = node.body;
for (let i = 0; i < body.length - 1; i++) {
const curFirst = sourceCode.getFirstToken(body[i]);
const { curLast, nextFirst } = getBoundaryTokens(body[i], body[i + 1]);
const isMulti = !astUtils.isTokenOnSameLine(curFirst, curLast);
const skip = !isMulti && options[1].exceptAfterSingleLine;
const beforePadding = findLastConsecutiveTokenAfter(curLast, nextFirst, 1);
const afterPadding = findFirstConsecutiveTokenBefore(nextFirst, curLast, 1);
const isPadded = afterPadding.loc.start.line - beforePadding.loc.end.line > 1;
const hasTokenInPadding = hasTokenOrCommentBetween(beforePadding, afterPadding);
const curLineLastToken = findLastConsecutiveTokenAfter(curLast, nextFirst, 0);
if ((options[0] === "always" && !skip && !isPadded) ||
(options[0] === "never" && isPadded)) {
context.report({
node: body[i + 1],
messageId: isPadded ? "never" : "always",
fix(fixer) {
if (hasTokenInPadding) {
return null;
}
return isPadded
? fixer.replaceTextRange([beforePadding.range[1], afterPadding.range[0]], "\n")
: fixer.insertTextAfter(curLineLastToken, "\n");
}
});
}
}
}
};
}
};