Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6718906

Browse files
Josh GoldbergJamesHenry
Josh Goldberg
authored andcommitted
feat(eslint-plugin): added new rule unbound-method (typescript-eslint#204)
1 parent ab3c1a1 commit 6718906

File tree

5 files changed

+491
-1
lines changed

5 files changed

+491
-1
lines changed

packages/eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
152152
| [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async. (`promise-function-async` from TSLint) | | | :thought_balloon: |
153153
| [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | When adding two variables, operands must both be of type number or of type string. (`restrict-plus-operands` from TSLint) | | | :thought_balloon: |
154154
| [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations (`typedef-whitespace` from TSLint) | :heavy_check_mark: | :wrench: | |
155+
| [`@typescript-eslint/unbound-method`](./docs/rules/unbound-method.md) | Enforces unbound methods are called with their expected scope. (`no-unbound-method` from TSLint) | :heavy_check_mark: | | :thought_balloon: |
155156
| [`@typescript-eslint/unified-signatures`](./docs/rules/unified-signatures.md) | Warns for any two overloads that could be unified into one. (`unified-signatures` from TSLint) | | | |
156157

157158
<!-- end rule list -->

packages/eslint-plugin/ROADMAP.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
| [`no-submodule-imports`] | 🌓 | [`import/no-internal-modules`] (slightly different) |
7878
| [`no-switch-case-fall-through`] | 🌟 | [`no-fallthrough`][no-fallthrough] |
7979
| [`no-this-assignment`] || [`@typescript-eslint/no-this-alias`] |
80-
| [`no-unbound-method`] | 🛑 | N/A |
80+
| [`no-unbound-method`] | | [`@typescript-eslint/unbound-method`] |
8181
| [`no-unnecessary-class`] || [`@typescript-eslint/no-extraneous-class`] |
8282
| [`no-unsafe-any`] | 🛑 | N/A |
8383
| [`no-unsafe-finally`] | 🌟 | [`no-unsafe-finally`][no-unsafe-finally] |
@@ -586,6 +586,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint-
586586
[`@typescript-eslint/no-namespace`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-namespace.md
587587
[`@typescript-eslint/no-non-null-assertion`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-non-null-assertion.md
588588
[`@typescript-eslint/no-triple-slash-reference`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-triple-slash-reference.md
589+
[`@typescript-eslint/unbound-method`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/unbound-method.md
589590
[`@typescript-eslint/no-unnecessary-type-assertion`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-type-assertion.md
590591
[`@typescript-eslint/no-var-requires`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-var-requires.md
591592
[`@typescript-eslint/type-annotation-spacing`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/type-annotation-spacing.md
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Enforces unbound methods are called with their expected scope (unbound-method)
2+
3+
Warns when a method is used outside of a method call.
4+
5+
Class functions don't preserve the class scope when passed as standalone variables.
6+
7+
## Rule Details
8+
9+
Examples of **incorrect** code for this rule
10+
11+
```ts
12+
class MyClass {
13+
public log(): void {
14+
console.log(this);
15+
}
16+
}
17+
18+
const instance = new MyClass();
19+
20+
// This logs the global scope (`window`/`global`), not the class instance
21+
const myLog = instance.log;
22+
myLog();
23+
24+
// This log might later be called with an incorrect scope
25+
const { log } = instance;
26+
```
27+
28+
Examples of **correct** code for this rule
29+
30+
```ts
31+
class MyClass {
32+
public logUnbound(): void {
33+
console.log(this);
34+
}
35+
36+
public logBound = () => console.log(this);
37+
}
38+
39+
const instance = new MyClass();
40+
41+
// logBound will always be bound with the correct scope
42+
const { logBound } = instance;
43+
logBound();
44+
45+
// .bind and lambdas will also add a correct scope
46+
const dotBindLog = instance.log.bind(instance);
47+
const innerLog = () => instance.log();
48+
```
49+
50+
## Options
51+
52+
The rule accepts an options object with the following property:
53+
54+
- `ignoreStatic` to not check whether `static` methods are correctly bound
55+
56+
### `ignoreStatic`
57+
58+
Examples of **correct** code for this rule with `{ ignoreStatic: true }`:
59+
60+
```ts
61+
class OtherClass {
62+
static log() {
63+
console.log(OtherClass);
64+
}
65+
}
66+
67+
// With `ignoreStatic`, statics are assumed to not rely on a particular scope
68+
const { log } = OtherClass;
69+
70+
log();
71+
```
72+
73+
### Example
74+
75+
```json
76+
{
77+
"@typescript-eslint/unbound-method": [
78+
"error",
79+
{
80+
"ignoreStatic": true
81+
}
82+
]
83+
}
84+
```
85+
86+
## When Not To Use It
87+
88+
If your code intentionally waits to bind methods after use, such as by passing a `scope: this` along with the method, you can disable this rule.
89+
90+
## Related To
91+
92+
- TSLint: [no-unbound-method](https://palantir.github.io/tslint/rules/no-unbound-method/)
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
2+
import * as tsutils from 'tsutils';
3+
import * as ts from 'typescript';
4+
5+
import * as util from '../util';
6+
7+
//------------------------------------------------------------------------------
8+
// Rule Definition
9+
//------------------------------------------------------------------------------
10+
11+
interface Config {
12+
ignoreStatic: boolean;
13+
}
14+
15+
type Options = [Config];
16+
17+
type MessageIds = 'unbound';
18+
19+
export default util.createRule<Options, MessageIds>({
20+
name: 'unbound-method',
21+
meta: {
22+
docs: {
23+
category: 'Best Practices',
24+
description:
25+
'Enforces unbound methods are called with their expected scope.',
26+
tslintName: 'no-unbound-method',
27+
recommended: 'error',
28+
},
29+
messages: {
30+
unbound:
31+
'Avoid referencing unbound methods which may cause unintentional scoping of `this`.',
32+
},
33+
schema: [
34+
{
35+
type: 'object',
36+
properties: {
37+
ignoreStatic: {
38+
type: 'boolean',
39+
},
40+
},
41+
additionalProperties: false,
42+
},
43+
],
44+
type: 'problem',
45+
},
46+
defaultOptions: [
47+
{
48+
ignoreStatic: false,
49+
},
50+
],
51+
create(context, [{ ignoreStatic }]) {
52+
const parserServices = util.getParserServices(context);
53+
const checker = parserServices.program.getTypeChecker();
54+
55+
return {
56+
[AST_NODE_TYPES.MemberExpression](node: TSESTree.MemberExpression) {
57+
if (isSafeUse(node)) {
58+
return;
59+
}
60+
61+
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
62+
const symbol = checker.getSymbolAtLocation(originalNode);
63+
64+
if (symbol && isDangerousMethod(symbol, ignoreStatic)) {
65+
context.report({
66+
messageId: 'unbound',
67+
node,
68+
});
69+
}
70+
},
71+
};
72+
},
73+
});
74+
75+
function isDangerousMethod(symbol: ts.Symbol, ignoreStatic: boolean) {
76+
const { valueDeclaration } = symbol;
77+
78+
switch (valueDeclaration.kind) {
79+
case ts.SyntaxKind.MethodDeclaration:
80+
case ts.SyntaxKind.MethodSignature:
81+
return !(
82+
ignoreStatic &&
83+
tsutils.hasModifier(
84+
valueDeclaration.modifiers,
85+
ts.SyntaxKind.StaticKeyword,
86+
)
87+
);
88+
}
89+
90+
return false;
91+
}
92+
93+
function isSafeUse(node: TSESTree.Node): boolean {
94+
const parent = node.parent!;
95+
96+
switch (parent.type) {
97+
case AST_NODE_TYPES.IfStatement:
98+
case AST_NODE_TYPES.ForStatement:
99+
case AST_NODE_TYPES.MemberExpression:
100+
case AST_NODE_TYPES.UpdateExpression:
101+
case AST_NODE_TYPES.WhileStatement:
102+
return true;
103+
104+
case AST_NODE_TYPES.CallExpression:
105+
return parent.callee === node;
106+
107+
case AST_NODE_TYPES.ConditionalExpression:
108+
return parent.test === node;
109+
110+
case AST_NODE_TYPES.LogicalExpression:
111+
return parent.operator !== '||';
112+
113+
case AST_NODE_TYPES.TaggedTemplateExpression:
114+
return parent.tag === node;
115+
116+
case AST_NODE_TYPES.TSNonNullExpression:
117+
case AST_NODE_TYPES.TSAsExpression:
118+
case AST_NODE_TYPES.TSTypeAssertion:
119+
return isSafeUse(parent);
120+
}
121+
122+
return false;
123+
}

0 commit comments

Comments
 (0)