Skip to content

Commit

Permalink
Fix min-chained-call-depth rule (#134)
Browse files Browse the repository at this point in the history
  • Loading branch information
georgekaran authored Mar 31, 2022
1 parent 1c13dbd commit db5eefc
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 30 deletions.
21 changes: 21 additions & 0 deletions docs/rules/min-chained-call-depth.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,27 @@ expect(screen.getElementById("very-long-identifier"))
.toBe(true);
```

### `ignoreChainDeeperThan`

Chains that are deeper than the specified number are allowed to break line, default is 2.

#### ❌ Incorrect

```jsx
Array(10)
.fill(0)
.map(foo => foo);
```

#### ✅ Correct

```jsx
/* eslint min-chained-call-depth: ["error", {"ignoreChainDeeperThan": 1}] */
Array(10)
.fill(0)
.map(foo => foo);
```

## Attributes

- [ ] ✅ Recommended
Expand Down
2 changes: 1 addition & 1 deletion docs/rules/newline-per-chained-call.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ foo.bar.baz.qux;

These are the available options:

### `ignoreChainWithDepth`
### `ignoreChainDeeperThan`

Specifies the maximum depth of chained allowed, default is 2.

Expand Down
52 changes: 41 additions & 11 deletions src/rules/min-chained-call-depth/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ const ruleTester = new ESLintUtils.RuleTester({

ruleTester.run('min-chained-call-depth', minChainedCallDepth, {
valid: [
{
code: 'Array(10)\n.fill(0)\n.map(foo => foo)\n.slice(1);',
},
{
code: 'Array(10).fill(0)\n.map(foo => foo)\n.slice(1);',
options: [
{
ignoreChainDeeperThan: 2,
},
],
},
{
code: 'Array(10)\n.foo\n.fill(0)\n.map(foo => foo)\n.slice(1);',
},
{
code: 'new StringSchema<ApiKeyPermission>()'
+ '\n.required()'
Expand Down Expand Up @@ -54,41 +68,46 @@ ruleTester.run('min-chained-call-depth', minChainedCallDepth, {
],
invalid: [
{
code: 'a()\n.b()\n.c();',
output: 'a().b()\n.c();',
code: 'Array(10)\n.fill(0)\n.map(foo => foo);',
output: 'Array(10).fill(0)\n.map(foo => foo);',
options: [
{
ignoreChainDeeperThan: 3,
},
],
errors: [
{
line: 1,
column: 4,
column: 10,
messageId: 'unexpectedLineBreak',
},
],
},
{
code: 'a()\n.b\n.c();',
output: 'a().b\n.c();',
code: 'Array(10)\n.fill(10);',
output: 'Array(10).fill(10);',
errors: [
{
line: 1,
column: 4,
column: 10,
messageId: 'unexpectedLineBreak',
},
],
},
{
code: 'a\n.b()\n.c();',
output: 'a.b()\n.c();',
code: 'a()\n.b()\n.c();',
output: 'a().b()\n.c();',
errors: [
{
line: 1,
column: 2,
column: 4,
messageId: 'unexpectedLineBreak',
},
],
},
{
code: 'a()\n.b()\n.c()\n.d();',
output: 'a().b()\n.c()\n.d();',
code: 'a()\n.b\n.c();',
output: 'a().b\n.c();',
errors: [
{
line: 1,
Expand All @@ -97,6 +116,17 @@ ruleTester.run('min-chained-call-depth', minChainedCallDepth, {
},
],
},
{
code: 'a\n.b()\n.c();',
output: 'a.b()\n.c();',
errors: [
{
line: 1,
column: 2,
messageId: 'unexpectedLineBreak',
},
],
},
{
code: 'a()\n.b();',
output: 'a().b();',
Expand Down
48 changes: 39 additions & 9 deletions src/rules/min-chained-call-depth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export const minChainedCallDepth = createRule({
minimum: 1,
default: 100,
},
ignoreChainDeeperThan: {
type: 'integer',
minimum: 1,
maximum: 10,
default: 2,
},
},
additionalProperties: false,
},
Expand All @@ -37,9 +43,13 @@ export const minChainedCallDepth = createRule({
{
maxLineLength: 100,
},
{
ignoreChainDeeperThan: 3,
},
],
create: context => {
const sourceCode = context.getSourceCode();
let maxDepth = 0;

function getDepth(node: TSESTree.MemberExpression | TSESTree.CallExpression): number {
let depth = 0;
Expand Down Expand Up @@ -88,29 +98,49 @@ export const minChainedCallDepth = createRule({
: node;

if (
// If the callee is not a member expression, we can skip.
// If the callee is not a member expression, skip.
// For example, root level calls like `foo();`.
callee.type !== AST_NODE_TYPES.MemberExpression
// If the callee is a computed member expression, like `foo[bar]()`, we can skip.
// 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
*/
* NewExpression is a possible callee object type
*/
// @ts-ignore
|| callee.object.type === AST_NODE_TYPES.NewExpression
// If the callee is already in the same line as it's object, we can skip.
// 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;
}

// We only inline the first level of chained calls.
// If the current call is nested inside another call, we can skip.
if (getDepth(callee) > 1) {
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 {maxLineLength = 100} = context.options[0] ?? {};
const {property} = callee;
const lastToken = sourceCode.getLastToken(node, {
filter: token => token.loc.end.line === property.loc.start.line,
Expand Down
12 changes: 6 additions & 6 deletions src/rules/newline-per-chained-call/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ ruleTester.run('newline-per-chained-call', newlinePerChainedCall, {
code: 'const a = m1().m2.m3().m4;',
options: [
{
ignoreChainWithDepth: 4,
ignoreChainDeeperThan: 4,
},
],
},
{
code: 'const a = m1().m2.m3().m4.m5().m6.m7().m8.m9();',
options: [
{
ignoreChainWithDepth: 8,
ignoreChainDeeperThan: 8,
},
],
},
Expand Down Expand Up @@ -183,7 +183,7 @@ ruleTester.run('newline-per-chained-call', newlinePerChainedCall, {
output: 'const a = m1()\n.m2\n.m3()\n.m4()\n.m5()\n.m6()\n.m7();',
options: [
{
ignoreChainWithDepth: 3,
ignoreChainDeeperThan: 3,
},
],
errors: [
Expand Down Expand Up @@ -219,7 +219,7 @@ ruleTester.run('newline-per-chained-call', newlinePerChainedCall, {
output: '(foo).bar()\n.biz()',
options: [
{
ignoreChainWithDepth: 1,
ignoreChainDeeperThan: 1,
},
],
errors: [
Expand All @@ -235,7 +235,7 @@ ruleTester.run('newline-per-chained-call', newlinePerChainedCall, {
output: 'foo.bar()\n. /* comment */ biz()',
options: [
{
ignoreChainWithDepth: 1,
ignoreChainDeeperThan: 1,
},
],
errors: [
Expand All @@ -251,7 +251,7 @@ ruleTester.run('newline-per-chained-call', newlinePerChainedCall, {
output: 'foo.bar() /* comment */ \n.biz()',
options: [
{
ignoreChainWithDepth: 1,
ignoreChainDeeperThan: 1,
},
],
errors: [
Expand Down
6 changes: 3 additions & 3 deletions src/rules/newline-per-chained-call/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const newlinePerChainedCall = createRule({
{
type: 'object',
properties: {
ignoreChainWithDepth: {
ignoreChainDeeperThan: {
type: 'integer',
minimum: 1,
maximum: 10,
Expand All @@ -37,12 +37,12 @@ export const newlinePerChainedCall = createRule({
},
defaultOptions: [
{
ignoreChainWithDepth: 2,
ignoreChainDeeperThan: 2,
},
],
create: context => {
const options = context.options[0] ?? {};
const ignoreChainWithDepth = options.ignoreChainWithDepth ?? 2;
const ignoreChainWithDepth = options.ignoreChainDeeperThan ?? 2;

const sourceCode = context.getSourceCode();

Expand Down

0 comments on commit db5eefc

Please sign in to comment.