Skip to content

Commit ca04a34

Browse files
feat(no-expression-statements): add option to ignore self returning functions
fix #611
1 parent a7094fc commit ca04a34

File tree

3 files changed

+87
-11
lines changed

3 files changed

+87
-11
lines changed

docs/rules/no-expression-statements.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ This rule accepts an options object of the following type:
6060
type Options = {
6161
ignorePattern?: string[] | string;
6262
ignoreVoid?: boolean;
63+
ignoreSelfReturning?: boolean;
6364
};
6465
```
6566

@@ -68,13 +69,20 @@ type Options = {
6869
```ts
6970
const defaults = {
7071
ignoreVoid: false,
72+
ignoreSelfReturning: false,
7173
};
7274
```
7375

7476
### `ignoreVoid`
7577

7678
When enabled, expression of type void are not flagged as violations. This options requires TypeScript in order to work.
7779

80+
### `ignoreSelfReturning`
81+
82+
Like `ignoreVoid` but instead does not flag function calls that always only return `this`.
83+
84+
Limitation: The function declaration must explicitly use `return this`; equivalents (such as assign this to a variable first, that is then returned) won't be considered valid.
85+
7886
### `ignorePattern`
7987

8088
This option takes a RegExp string or an array of RegExp strings.

src/rules/no-expression-statements.ts

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import {
55
} from "@typescript-eslint/utils/json-schema";
66
import { type RuleContext } from "@typescript-eslint/utils/ts-eslint";
77
import { deepmerge } from "deepmerge-ts";
8+
import { isThisKeyword } from "ts-api-utils";
89

910
import tsApiUtils from "#eslint-plugin-functional/conditional-imports/ts-api-utils";
11+
import typescript from "#eslint-plugin-functional/conditional-imports/typescript";
1012
import { type IgnorePatternOption } from "#eslint-plugin-functional/options";
1113
import {
1214
shouldIgnorePattern,
@@ -21,7 +23,10 @@ import {
2123
createRule,
2224
getTypeOfNode,
2325
} from "#eslint-plugin-functional/utils/rule";
24-
import { isYieldExpression } from "#eslint-plugin-functional/utils/type-guards";
26+
import {
27+
isCallExpression,
28+
isYieldExpression,
29+
} from "#eslint-plugin-functional/utils/type-guards";
2530

2631
/**
2732
* The name of this rule.
@@ -34,6 +39,7 @@ export const name = "no-expression-statements" as const;
3439
type Options = [
3540
IgnorePatternOption & {
3641
ignoreVoid: boolean;
42+
ignoreSelfReturning: boolean;
3743
},
3844
];
3945

@@ -47,6 +53,9 @@ const schema: JSONSchema4[] = [
4753
ignoreVoid: {
4854
type: "boolean",
4955
},
56+
ignoreSelfReturning: {
57+
type: "boolean",
58+
},
5059
} satisfies JSONSchema4ObjectSchema["properties"]),
5160
additionalProperties: false,
5261
},
@@ -58,6 +67,7 @@ const schema: JSONSchema4[] = [
5867
const defaultOptions: Options = [
5968
{
6069
ignoreVoid: false,
70+
ignoreSelfReturning: false,
6171
},
6272
];
6373

@@ -107,18 +117,53 @@ function checkExpressionStatement(
107117
};
108118
}
109119

110-
const { ignoreVoid } = optionsObject;
120+
const { ignoreVoid, ignoreSelfReturning } = optionsObject;
111121

112-
if (ignoreVoid) {
113-
const type = getTypeOfNode(node.expression, context);
122+
if (
123+
(ignoreVoid || ignoreSelfReturning) &&
124+
isCallExpression(node.expression)
125+
) {
126+
const returnType = getTypeOfNode(node.expression.callee, context);
127+
if (returnType === null) {
128+
return {
129+
context,
130+
descriptors: [{ node, messageId: "generic" }],
131+
};
132+
}
114133

115-
return {
116-
context,
117-
descriptors:
118-
type !== null && tsApiUtils?.isIntrinsicVoidType(type) === true
119-
? []
120-
: [{ node, messageId: "generic" }],
121-
};
134+
if (ignoreVoid && tsApiUtils?.isIntrinsicVoidType(returnType) === true) {
135+
return {
136+
context,
137+
descriptors: [],
138+
};
139+
}
140+
141+
const declaration = returnType.getSymbol()?.valueDeclaration;
142+
if (
143+
typescript !== undefined &&
144+
declaration !== undefined &&
145+
typescript.isFunctionLike(declaration) &&
146+
"body" in declaration &&
147+
declaration.body !== undefined &&
148+
typescript.isBlock(declaration.body)
149+
) {
150+
const returnStatements = declaration.body.statements.filter(
151+
typescript.isReturnStatement,
152+
);
153+
154+
if (
155+
returnStatements.every(
156+
(statement) =>
157+
statement.expression !== undefined &&
158+
isThisKeyword(statement.expression),
159+
)
160+
) {
161+
return {
162+
context,
163+
descriptors: [],
164+
};
165+
}
166+
}
122167
}
123168

124169
return {

tests/rules/no-expression-statement/ts/valid.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,29 @@ const tests: Array<ValidTestCaseSet<OptionsOf<typeof rule>>> = [
4343
`,
4444
optionsSet: [[{ ignoreVoid: true }]],
4545
},
46+
// Allowed ignoring self returning expressions.
47+
{
48+
code: dedent`
49+
function foo() { return this; }
50+
foo();
51+
`,
52+
optionsSet: [[{ ignoreSelfReturning: true }]],
53+
},
54+
{
55+
code: dedent`
56+
const foo = { bar() { return this; }};
57+
foo.bar();
58+
`,
59+
optionsSet: [[{ ignoreSelfReturning: true }]],
60+
},
61+
{
62+
code: dedent`
63+
class Foo { bar() { return this; }};
64+
const foo = new Foo();
65+
foo.bar();
66+
`,
67+
optionsSet: [[{ ignoreSelfReturning: true }]],
68+
},
4669
];
4770

4871
export default tests;

0 commit comments

Comments
 (0)