@croct/eslint-plugin
Version:
ESLint rules and presets applied to all Croct JavaScript projects.
166 lines (165 loc) • 6.91 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.minChainedCallDepth = void 0;
const utils_1 = require("@typescript-eslint/utils");
const ast_utils_1 = require("@typescript-eslint/utils/ast-utils");
const createRule_1 = require("../createRule");
exports.minChainedCallDepth = (0, createRule_1.createRule)({
name: 'min-chained-call-depth',
meta: {
type: 'layout',
docs: {
description: 'Enforces a minimum depth for multiline chained calls.',
recommended: 'stylistic',
},
fixable: 'whitespace',
schema: [
{
type: 'object',
properties: {
maxLineLength: {
type: 'integer',
minimum: 1,
default: 100,
},
ignoreChainDeeperThan: {
type: 'integer',
minimum: 1,
maximum: 10,
default: 2,
},
},
additionalProperties: false,
},
],
messages: {
unexpectedLineBreak: 'Unexpected line break.',
},
},
defaultOptions: [
{
maxLineLength: 100,
},
{
ignoreChainDeeperThan: 3,
},
],
create: context => {
const sourceCode = context.getSourceCode();
let maxDepth = 0;
function getDepth(node) {
let depth = 0;
let currentNode = node;
while (currentNode.type === utils_1.AST_NODE_TYPES.CallExpression
|| currentNode.type === utils_1.AST_NODE_TYPES.MemberExpression) {
if (currentNode.type === utils_1.AST_NODE_TYPES.MemberExpression) {
currentNode = currentNode.object;
depth += 1;
}
else if (currentNode.type === utils_1.AST_NODE_TYPES.CallExpression) {
currentNode = currentNode.callee;
}
}
return depth;
}
function check(node) {
// If the node is a member expression inside a call/new expression skip,
// this is to ensure that we consider the correct line length of the result.
//
// Example:
// ```ts
// foo
// .bar();
// ```
// The replacement of this input should be `foo.bar();`, which has 10 character.
// Without this check it would consider the length up to `r`, which is 7.
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression
&& node.parent?.type === utils_1.AST_NODE_TYPES.CallExpression) {
return;
}
// If the node is a call/new expression we need to validate it's callee as a member
// expression.
// If the node itself is already a member expression, like the
// `property` in `this.property.function()`, we validate the node directly.
const callee = node.type === utils_1.AST_NODE_TYPES.CallExpression
? node.callee
: node;
if (
// If the callee is not a member expression, skip.
// For example, root level calls like `foo();`.
callee.type !== utils_1.AST_NODE_TYPES.MemberExpression
// If the callee is a computed member expression, like `foo[bar]()`, skip.
|| callee.computed
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment --
* NewExpression is a possible callee object type
*/
// @ts-ignore
|| callee.object.type === utils_1.AST_NODE_TYPES.NewExpression
// If the callee is already in the same line as it's object, skip.
|| callee.object.loc.end.line === callee.property.loc.start.line) {
return;
}
const currentDepth = getDepth(callee);
maxDepth = Math.max(maxDepth, currentDepth);
// Only affect the root level as the total depth is must be known.
// If the current call is nested inside another call, skip.
if (currentDepth > 1) {
return;
}
const { maxLineLength = 100, ignoreChainDeeperThan = 2 } = context.options[0] ?? {};
// If the max depth is greater than ignore threshold, skip
//
// Example:
// ```ts
// Array(10)
// .fill(0)
// .map(x => x + 1)
// .slice(0, 5);
// ```
// In this case the depth is 3, and the default value of ignoreChainDeeperThan is 2.
// So the check can be skipped.
if (maxDepth > ignoreChainDeeperThan) {
return;
}
const { property } = callee;
const lastToken = sourceCode.getLastToken(node, {
filter: token => token.loc.end.line === property.loc.start.line,
});
const semicolon = sourceCode.getLastToken(node.parent, {
filter: token => (token.loc.start.line === property.loc.start.line
&& token.type === utils_1.AST_TOKEN_TYPES.Punctuator
&& token.value === ';'),
});
const lineLength = callee.object.loc.end.column
+ lastToken.loc.end.column - property.loc.start.column
+ 1
+ (semicolon !== null ? 1 : 0);
if (maxLineLength !== null && lineLength > maxLineLength) {
return;
}
const punctuator = sourceCode.getTokenBefore(callee.property);
const previousToken = sourceCode.getTokenBefore(punctuator, { includeComments: true });
const nextToken = sourceCode.getTokenAfter(punctuator, { includeComments: true });
if ((0, ast_utils_1.isCommentToken)(previousToken) || (0, ast_utils_1.isCommentToken)(nextToken)) {
return;
}
context.report({
node: node,
loc: {
start: callee.object.loc.end,
end: callee.property.loc.start,
},
messageId: 'unexpectedLineBreak',
fix: fixer => fixer.replaceTextRange([previousToken.range[1], nextToken.range[0]], punctuator.value),
});
}
return {
CallExpression: (node) => {
check(node);
},
MemberExpression: (node) => {
check(node);
},
};
},
});