Skip to content

Commit 747bfcb

Browse files
scottoharabradzacher
authored andcommitted
feat(eslint-plugin): add new rule no-empty-function (#626)
1 parent aa206c4 commit 747bfcb

File tree

6 files changed

+241
-0
lines changed

6 files changed

+241
-0
lines changed

Diff for: packages/eslint-plugin/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
142142
| [`@typescript-eslint/member-ordering`](./docs/rules/member-ordering.md) | Require a consistent member declaration order | | | |
143143
| [`@typescript-eslint/no-angle-bracket-type-assertion`](./docs/rules/no-angle-bracket-type-assertion.md) | Enforces the use of `as Type` assertions instead of `<Type>` assertions | :heavy_check_mark: | | |
144144
| [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | |
145+
| [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | | | |
145146
| [`@typescript-eslint/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces | :heavy_check_mark: | | |
146147
| [`@typescript-eslint/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow usage of the `any` type | :heavy_check_mark: | | |
147148
| [`@typescript-eslint/no-extra-parens`](./docs/rules/no-extra-parens.md) | Disallow unnecessary parentheses | | :wrench: | |
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Disallow Empty Functions (@typescript-eslint/no-empty-function)
2+
3+
Empty functions can reduce readability because readers need to guess whether it’s intentional or not. So writing a clear comment for empty functions is a good practice.
4+
5+
## Rule Details
6+
7+
The `@typescript-eslint/no-empty-function` rule extends the `no-empty-function` rule from ESLint core, and adds support for handling Typescript specific code that would otherwise trigger the rule.
8+
9+
One example of valid Typescript specific code that would otherwise trigger the `no-empty-function` rule is the use of [parameter properties](https://www.typescriptlang.org/docs/handbook/classes.html#parameter-properties) in constructor functions:
10+
11+
```typescript
12+
class Person {
13+
constructor(private firstName: string, private surname: string) {}
14+
}
15+
```
16+
17+
The above code is functionally equivalent to:
18+
19+
```typescript
20+
class Person {
21+
private firstName: string;
22+
private surname: string;
23+
24+
constructor(firstName: string, surname: string) {
25+
this.firstName = firstName;
26+
this.surname = surname;
27+
}
28+
}
29+
```
30+
31+
Parameter properties enable both the _declaration_ and _initialization_ of member properties in a single location, avoiding the boilerplate & duplication that is common when initializing member properties from parameter values in a constructor function.
32+
33+
In these cases, although the constructor has an empty function body, it is technically valid and should not trigger an error.
34+
35+
See the [ESLint documentation](https://eslint.org/docs/rules/no-empty-function) for more details on the `no-empty-function` rule.
36+
37+
## Rule Changes
38+
39+
```cjson
40+
{
41+
// note you must disable the base rule as it can report incorrect errors
42+
"no-empty-function": "off",
43+
"@typescript-eslint/no-empty-function": "error"
44+
}
45+
```
46+
47+
<sup>Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/no-empty-function.md)</sup>

Diff for: packages/eslint-plugin/src/rules/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import memberNaming from './member-naming';
1616
import memberOrdering from './member-ordering';
1717
import noAngleBracketTypeAssertion from './no-angle-bracket-type-assertion';
1818
import noArrayConstructor from './no-array-constructor';
19+
import noEmptyFunction from './no-empty-function';
1920
import noEmptyInterface from './no-empty-interface';
2021
import noExplicitAny from './no-explicit-any';
2122
import noExtraParens from './no-extra-parens';
@@ -73,6 +74,7 @@ export default {
7374
'member-ordering': memberOrdering,
7475
'no-angle-bracket-type-assertion': noAngleBracketTypeAssertion,
7576
'no-array-constructor': noArrayConstructor,
77+
'no-empty-function': noEmptyFunction,
7678
'no-empty-interface': noEmptyInterface,
7779
'no-explicit-any': noExplicitAny,
7880
'no-extra-parens': noExtraParens,
+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import {
2+
TSESTree,
3+
AST_NODE_TYPES,
4+
} from '@typescript-eslint/experimental-utils';
5+
import baseRule from 'eslint/lib/rules/no-empty-function';
6+
import * as util from '../util';
7+
8+
type Options = util.InferOptionsTypeFromRule<typeof baseRule>;
9+
type MessageIds = util.InferMessageIdsTypeFromRule<typeof baseRule>;
10+
11+
export default util.createRule<Options, MessageIds>({
12+
name: 'no-empty-function',
13+
meta: {
14+
type: 'suggestion',
15+
docs: {
16+
description: 'Disallow empty functions',
17+
category: 'Best Practices',
18+
recommended: false,
19+
},
20+
schema: baseRule.meta.schema,
21+
messages: baseRule.meta.messages,
22+
},
23+
defaultOptions: [
24+
{
25+
allow: [],
26+
},
27+
],
28+
create(context) {
29+
const rules = baseRule.create(context);
30+
31+
/**
32+
* Checks if the node is a constructor
33+
* @param node the node to ve validated
34+
* @returns true if the node is a constructor
35+
* @private
36+
*/
37+
function isConstructor(
38+
node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression,
39+
): boolean {
40+
return !!(
41+
node.parent &&
42+
node.parent.type === 'MethodDefinition' &&
43+
node.parent.kind === 'constructor'
44+
);
45+
}
46+
47+
/**
48+
* Check if the method body is empty
49+
* @param node the node to be validated
50+
* @returns true if the body is empty
51+
* @private
52+
*/
53+
function isBodyEmpty(
54+
node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression,
55+
): boolean {
56+
return !node.body || node.body.body.length === 0;
57+
}
58+
59+
/**
60+
* Check if method has parameter properties
61+
* @param node the node to be validated
62+
* @returns true if the body has parameter properties
63+
* @private
64+
*/
65+
function hasParameterProperties(
66+
node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression,
67+
): boolean {
68+
return (
69+
node.params &&
70+
node.params.some(
71+
param => param.type === AST_NODE_TYPES.TSParameterProperty,
72+
)
73+
);
74+
}
75+
76+
/**
77+
* Checks if the method is a concise constructor (no function body, but has parameter properties)
78+
* @param node the node to be validated
79+
* @returns true if the method is a concise constructor
80+
* @private
81+
*/
82+
function isConciseConstructor(
83+
node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression,
84+
) {
85+
// Check TypeScript specific nodes
86+
return (
87+
isConstructor(node) && isBodyEmpty(node) && hasParameterProperties(node)
88+
);
89+
}
90+
91+
return {
92+
FunctionDeclaration(node: TSESTree.FunctionDeclaration) {
93+
if (!isConciseConstructor(node)) {
94+
rules.FunctionDeclaration(node);
95+
}
96+
},
97+
FunctionExpression(node: TSESTree.FunctionExpression) {
98+
if (!isConciseConstructor(node)) {
99+
rules.FunctionExpression(node);
100+
}
101+
},
102+
};
103+
},
104+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import rule from '../../src/rules/no-empty-function';
2+
import { RuleTester } from '../RuleTester';
3+
4+
const ruleTester = new RuleTester({
5+
parser: '@typescript-eslint/parser',
6+
});
7+
8+
ruleTester.run('no-empty-function', rule, {
9+
valid: [
10+
{
11+
code: `class Person {
12+
private name: string
13+
constructor(name: string) {
14+
this.name = name;
15+
}
16+
}`,
17+
},
18+
{
19+
code: `class Person {
20+
constructor(private name: string) {}
21+
}`,
22+
},
23+
{
24+
code: `class Person {
25+
constructor(name: string) {}
26+
}`,
27+
options: [{ allow: ['constructors'] }],
28+
},
29+
{
30+
code: `class Person {
31+
otherMethod(name: string) {}
32+
}`,
33+
options: [{ allow: ['methods'] }],
34+
},
35+
],
36+
37+
invalid: [
38+
{
39+
code: `class Person {
40+
constructor(name: string) {}
41+
}`,
42+
errors: [
43+
{
44+
messageId: 'unexpected',
45+
data: {
46+
name: 'constructor',
47+
},
48+
line: 2,
49+
column: 35,
50+
},
51+
],
52+
},
53+
{
54+
code: `class Person {
55+
otherMethod(name: string) {}
56+
}`,
57+
errors: [
58+
{
59+
messageId: 'unexpected',
60+
data: {
61+
name: "method 'otherMethod'",
62+
},
63+
line: 2,
64+
column: 35,
65+
},
66+
],
67+
},
68+
],
69+
});

Diff for: packages/eslint-plugin/typings/eslint-rules.d.ts

+18
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,24 @@ declare module 'eslint/lib/rules/no-dupe-args' {
155155
export = rule;
156156
}
157157

158+
declare module 'eslint/lib/rules/no-empty-function' {
159+
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
160+
161+
const rule: TSESLint.RuleModule<
162+
'unexpected',
163+
[
164+
{
165+
allow?: string[];
166+
}
167+
],
168+
{
169+
FunctionDeclaration(node: TSESTree.FunctionDeclaration): void;
170+
FunctionExpression(node: TSESTree.FunctionExpression): void;
171+
}
172+
>;
173+
export = rule;
174+
}
175+
158176
declare module 'eslint/lib/rules/no-implicit-globals' {
159177
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
160178

0 commit comments

Comments
 (0)