codtracker-js/node_modules/eslint/lib/rules/lines-between-class-members.js
2025-04-19 23:12:19 -04:00

338 lines
11 KiB
JavaScript

/**
* @fileoverview Rule to check empty newline between class members
* @author 薛定谔的猫<hh_2013@foxmail.com>
* @deprecated in ESLint v8.53.0
*/
'use strict';
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require('./utils/ast-utils');
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Types of class members.
* Those have `test` method to check it matches to the given class member.
* @private
*/
const ClassMemberTypes = {
'*': { test: () => true },
field: { test: (node) => node.type === 'PropertyDefinition' },
method: { test: (node) => node.type === 'MethodDefinition' },
};
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: {
message: 'Formatting rules are being moved out of ESLint core.',
url: 'https://eslint.org/blog/2023/10/deprecating-formatting-rules/',
deprecatedSince: '8.53.0',
availableUntil: '10.0.0',
replacedBy: [
{
message:
'ESLint Stylistic now maintains deprecated stylistic core rules.',
url: 'https://eslint.style/guide/migration',
plugin: {
name: '@stylistic/eslint-plugin-js',
url: 'https://eslint.style/packages/js',
},
rule: {
name: 'lines-between-class-members',
url: 'https://eslint.style/rules/js/lines-between-class-members',
},
},
],
},
type: 'layout',
docs: {
description: 'Require or disallow an empty line between class members',
recommended: false,
url: 'https://eslint.org/docs/latest/rules/lines-between-class-members',
},
fixable: 'whitespace',
schema: [
{
anyOf: [
{
type: 'object',
properties: {
enforce: {
type: 'array',
items: {
type: 'object',
properties: {
blankLine: {
enum: ['always', 'never'],
},
prev: {
enum: ['method', 'field', '*'],
},
next: {
enum: ['method', 'field', '*'],
},
},
additionalProperties: false,
required: ['blankLine', 'prev', 'next'],
},
minItems: 1,
},
},
additionalProperties: false,
required: ['enforce'],
},
{
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 configureList =
typeof options[0] === 'object' ?
options[0].enforce
: [{ blankLine: options[0], prev: '*', next: '*' }];
const sourceCode = context.sourceCode;
/**
* 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
);
}
/**
* Checks whether the given node matches the given type.
* @param {ASTNode} node The class member node to check.
* @param {string} type The class member type to check.
* @returns {boolean} `true` if the class member node matched the type.
* @private
*/
function match(node, type) {
return ClassMemberTypes[type].test(node);
}
/**
* Finds the last matched configuration from the configureList.
* @param {ASTNode} prevNode The previous node to match.
* @param {ASTNode} nextNode The current node to match.
* @returns {string|null} Padding type or `null` if no matches were found.
* @private
*/
function getPaddingType(prevNode, nextNode) {
for (let i = configureList.length - 1; i >= 0; --i) {
const configure = configureList[i];
const matched =
match(prevNode, configure.prev) && match(nextNode, configure.next);
if (matched) {
return configure.blankLine;
}
}
return null;
}
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
);
const paddingType = getPaddingType(body[i], body[i + 1]);
if (paddingType === 'never' && isPadded) {
context.report({
node: body[i + 1],
messageId: 'never',
fix(fixer) {
if (hasTokenInPadding) {
return null;
}
return fixer.replaceTextRange(
[beforePadding.range[1], afterPadding.range[0]],
'\n'
);
},
});
} else if (paddingType === 'always' && !skip && !isPadded) {
context.report({
node: body[i + 1],
messageId: 'always',
fix(fixer) {
if (hasTokenInPadding) {
return null;
}
return fixer.insertTextAfter(curLineLastToken, '\n');
},
});
}
}
},
};
},
};