@stylistic/eslint-plugin-js
Version:
JavaScript stylistic rules for ESLint, migrated from [eslint core](https://github.com/eslint/eslint).
317 lines (314 loc) • 11.2 kB
JavaScript
import { Q as LINEBREAKS, c as createRule, S as STATEMENT_LIST_PARENTS, R as isDirective, i as isTokenOnSameLine, t as isSemicolonToken, y as isNotSemicolonToken, k as isClosingBraceToken, G as skipChainExpression, s as isFunction } from '../utils.js';
import 'eslint-visitor-keys';
import 'espree';
const LT = `[${Array.from(LINEBREAKS).join("")}]`;
const PADDING_LINE_SEQUENCE = new RegExp(
String.raw`^(\s*?${LT})\s*${LT}(\s*;?)$`,
"u"
);
const CJS_EXPORT = /^(?:module\s*\.\s*)?exports(?:\s*\.|\s*\[|$)/u;
const CJS_IMPORT = /^require\(/u;
function newKeywordTester(keyword) {
return {
test: (node, sourceCode) => sourceCode.getFirstToken(node)?.value === keyword
};
}
function newSinglelineKeywordTester(keyword) {
return {
test: (node, sourceCode) => node.loc.start.line === node.loc.end.line && sourceCode.getFirstToken(node)?.value === keyword
};
}
function newMultilineKeywordTester(keyword) {
return {
test: (node, sourceCode) => node.loc.start.line !== node.loc.end.line && sourceCode.getFirstToken(node)?.value === keyword
};
}
function newNodeTypeTester(type) {
return {
test: (node) => node.type === type
};
}
function isIIFEStatement(node) {
if (node.type === "ExpressionStatement") {
let call = skipChainExpression(node.expression);
if (call.type === "UnaryExpression")
call = skipChainExpression(call.argument);
return call.type === "CallExpression" && isFunction(call.callee);
}
return false;
}
function isBlockLikeStatement(sourceCode, node) {
if (node.type === "DoWhileStatement" && node.body.type === "BlockStatement")
return true;
if (isIIFEStatement(node))
return true;
const lastToken = sourceCode.getLastToken(node, isNotSemicolonToken);
const belongingNode = lastToken && isClosingBraceToken(lastToken) ? sourceCode.getNodeByRangeIndex(lastToken.range[0]) : null;
return Boolean(belongingNode) && (belongingNode?.type === "BlockStatement" || belongingNode?.type === "SwitchStatement");
}
function getActualLastToken(sourceCode, node) {
const semiToken = sourceCode.getLastToken(node);
const prevToken = sourceCode.getTokenBefore(semiToken);
const nextToken = sourceCode.getTokenAfter(semiToken);
const isSemicolonLessStyle = Boolean(
prevToken && nextToken && prevToken.range[0] >= node.range[0] && isSemicolonToken(semiToken) && semiToken.loc.start.line !== prevToken.loc.end.line && semiToken.loc.end.line === nextToken.loc.start.line
);
return isSemicolonLessStyle ? prevToken : semiToken;
}
function replacerToRemovePaddingLines(_, trailingSpaces, indentSpaces) {
return trailingSpaces + indentSpaces;
}
function verifyForAny() {
}
function verifyForNever(context, _, nextNode, paddingLines) {
if (paddingLines.length === 0)
return;
context.report({
node: nextNode,
messageId: "unexpectedBlankLine",
fix(fixer) {
if (paddingLines.length >= 2)
return null;
const prevToken = paddingLines[0][0];
const nextToken = paddingLines[0][1];
const start = prevToken.range[1];
const end = nextToken.range[0];
const text = context.sourceCode.text.slice(start, end).replace(PADDING_LINE_SEQUENCE, replacerToRemovePaddingLines);
return fixer.replaceTextRange([start, end], text);
}
});
}
function verifyForAlways(context, prevNode, nextNode, paddingLines) {
if (paddingLines.length > 0)
return;
context.report({
node: nextNode,
messageId: "expectedBlankLine",
fix(fixer) {
const sourceCode = context.sourceCode;
let prevToken = getActualLastToken(sourceCode, prevNode);
const nextToken = sourceCode.getFirstTokenBetween(
prevToken,
nextNode,
{
includeComments: true,
/**
* Skip the trailing comments of the previous node.
* This inserts a blank line after the last trailing comment.
*
* For example:
*
* foo(); // trailing comment.
* // comment.
* bar();
*
* Get fixed to:
*
* foo(); // trailing comment.
*
* // comment.
* bar();
* @param token The token to check.
* @returns `true` if the token is not a trailing comment.
* @private
*/
filter(token) {
if (isTokenOnSameLine(prevToken, token)) {
prevToken = token;
return false;
}
return true;
}
}
) || nextNode;
const insertText = isTokenOnSameLine(prevToken, nextToken) ? "\n\n" : "\n";
return fixer.insertTextAfter(prevToken, insertText);
}
});
}
const PaddingTypes = {
any: { verify: verifyForAny },
never: { verify: verifyForNever },
always: { verify: verifyForAlways }
};
const StatementTypes = {
"*": { test: () => true },
"block-like": {
test: (node, sourceCode) => isBlockLikeStatement(sourceCode, node)
},
"cjs-export": {
test: (node, sourceCode) => node.type === "ExpressionStatement" && node.expression.type === "AssignmentExpression" && CJS_EXPORT.test(sourceCode.getText(node.expression.left))
},
"cjs-import": {
test: (node, sourceCode) => node.type === "VariableDeclaration" && node.declarations.length > 0 && Boolean(node.declarations[0].init) && CJS_IMPORT.test(sourceCode.getText(node.declarations[0].init))
},
"directive": {
test: isDirective
},
"expression": {
test: (node) => node.type === "ExpressionStatement" && !isDirective(node)
},
"iife": {
test: isIIFEStatement
},
"multiline-block-like": {
test: (node, sourceCode) => node.loc.start.line !== node.loc.end.line && isBlockLikeStatement(sourceCode, node)
},
"multiline-expression": {
test: (node) => node.loc.start.line !== node.loc.end.line && node.type === "ExpressionStatement" && !isDirective(node)
},
"multiline-const": newMultilineKeywordTester("const"),
"multiline-export": newMultilineKeywordTester("export"),
"multiline-let": newMultilineKeywordTester("let"),
"multiline-var": newMultilineKeywordTester("var"),
"singleline-const": newSinglelineKeywordTester("const"),
"singleline-export": newSinglelineKeywordTester("export"),
"singleline-let": newSinglelineKeywordTester("let"),
"singleline-var": newSinglelineKeywordTester("var"),
"block": newNodeTypeTester("BlockStatement"),
"empty": newNodeTypeTester("EmptyStatement"),
"function": newNodeTypeTester("FunctionDeclaration"),
"break": newKeywordTester("break"),
"case": newKeywordTester("case"),
"class": newKeywordTester("class"),
"const": newKeywordTester("const"),
"continue": newKeywordTester("continue"),
"debugger": newKeywordTester("debugger"),
"default": newKeywordTester("default"),
"do": newKeywordTester("do"),
"export": newKeywordTester("export"),
"for": newKeywordTester("for"),
"if": newKeywordTester("if"),
"import": newKeywordTester("import"),
"let": newKeywordTester("let"),
"return": newKeywordTester("return"),
"switch": newKeywordTester("switch"),
"throw": newKeywordTester("throw"),
"try": newKeywordTester("try"),
"var": newKeywordTester("var"),
"while": newKeywordTester("while"),
"with": newKeywordTester("with")
};
var paddingLineBetweenStatements = createRule({
name: "padding-line-between-statements",
package: "js",
meta: {
type: "layout",
docs: {
description: "Require or disallow padding lines between statements"
},
fixable: "whitespace",
schema: {
definitions: {
paddingType: {
type: "string",
enum: Object.keys(PaddingTypes)
},
statementType: {
anyOf: [
{ type: "string", enum: Object.keys(StatementTypes) },
{
type: "array",
items: { type: "string", enum: Object.keys(StatementTypes) },
minItems: 1,
uniqueItems: true
}
]
}
},
type: "array",
items: {
type: "object",
properties: {
blankLine: { $ref: "#/definitions/paddingType" },
prev: { $ref: "#/definitions/statementType" },
next: { $ref: "#/definitions/statementType" }
},
additionalProperties: false,
required: ["blankLine", "prev", "next"]
}
},
messages: {
unexpectedBlankLine: "Unexpected blank line before this statement.",
expectedBlankLine: "Expected blank line before this statement."
}
},
create(context) {
const sourceCode = context.sourceCode;
const configureList = context.options || [];
let scopeInfo = null;
function enterScope() {
scopeInfo = {
upper: scopeInfo,
prevNode: null
};
}
function exitScope() {
scopeInfo = scopeInfo?.upper;
}
function match(node, type) {
let innerStatementNode = node;
while (innerStatementNode.type === "LabeledStatement")
innerStatementNode = innerStatementNode.body;
if (Array.isArray(type))
return type.some(match.bind(null, innerStatementNode));
return StatementTypes[type].test(innerStatementNode, sourceCode);
}
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 PaddingTypes[configure.blankLine];
}
return PaddingTypes.any;
}
function getPaddingLineSequences(prevNode, nextNode) {
const pairs = [];
let prevToken = getActualLastToken(sourceCode, prevNode);
if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) {
do {
const token = sourceCode.getTokenAfter(
prevToken,
{ includeComments: true }
);
if (token.loc.start.line - prevToken.loc.end.line >= 2)
pairs.push([prevToken, token]);
prevToken = token;
} while (prevToken.range[0] < nextNode.range[0]);
}
return pairs;
}
function verify(node) {
const parentType = node.parent.type;
const validParent = STATEMENT_LIST_PARENTS.has(parentType) || parentType === "SwitchStatement";
if (!validParent)
return;
const prevNode = scopeInfo?.prevNode;
if (prevNode) {
const type = getPaddingType(prevNode, node);
const paddingLines = getPaddingLineSequences(prevNode, node);
type.verify(context, prevNode, node, paddingLines);
}
scopeInfo.prevNode = node;
}
function verifyThenEnterScope(node) {
verify(node);
enterScope();
}
return {
"Program": enterScope,
"BlockStatement": enterScope,
"SwitchStatement": enterScope,
"StaticBlock": enterScope,
"Program:exit": exitScope,
"BlockStatement:exit": exitScope,
"SwitchStatement:exit": exitScope,
"StaticBlock:exit": exitScope,
":statement": verify,
"SwitchCase": verifyThenEnterScope,
"SwitchCase:exit": exitScope
};
}
});
export { paddingLineBetweenStatements as default };