-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
feat(eslint-plugin): added new rule unbound-method #204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
JamesHenry
merged 14 commits into
typescript-eslint:master
from
JoshuaKGoldberg:typescript-eslint-no-unbound-method
Apr 3, 2019
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
dd263d4
feat(eslint-plugin): added new rule no-unbound-method
f9e2176
Merge branch 'master'
f1f9159
Added missing reference to ROADMAP.md
0990057
Merge branch 'master' into typescript-eslint-no-unbound-method
c624d05
ignoreStatic: ["Thenable"] typo
30035e8
Merge branch 'master' into typescript-eslint-no-unbound-method
bradzacher 2ae06a3
Merge branch 'master'; converted to TypeScript
dfd5935
Finished rename to unbound-method
a3e5bc4
Removed unecessary docs; switched TS style casing
6082ff0
Merge branch 'master' into typescript-eslint-no-unbound-method
b6f16ff
Formatted; converted test to .ts
b2fc0b9
Used computed property
f642cad
Merge branch 'master' into typescript-eslint-no-unbound-method
bradzacher dcd2d45
Merge branch 'master'
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
# Enforces unbound methods are called with their expected scope (unbound-method) | ||
|
||
Warns when a method is used outside of a method call. | ||
|
||
Class functions don't preserve the class scope when passed as standalone variables. | ||
|
||
## Rule Details | ||
|
||
Examples of **incorrect** code for this rule | ||
|
||
```ts | ||
class MyClass { | ||
public log(): void { | ||
console.log(this); | ||
} | ||
} | ||
|
||
const instance = new MyClass(); | ||
|
||
// This logs the global scope (`window`/`global`), not the class instance | ||
const myLog = instance.log; | ||
myLog(); | ||
|
||
// This log might later be called with an incorrect scope | ||
const { log } = instance; | ||
``` | ||
|
||
Examples of **correct** code for this rule | ||
|
||
```ts | ||
class MyClass { | ||
public logUnbound(): void { | ||
console.log(this); | ||
} | ||
|
||
public logBound = () => console.log(this); | ||
} | ||
|
||
const instance = new MyClass(); | ||
|
||
// logBound will always be bound with the correct scope | ||
const { logBound } = instance; | ||
logBound(); | ||
|
||
// .bind and lambdas will also add a correct scope | ||
const dotBindLog = instance.log.bind(instance); | ||
const innerLog = () => instance.log(); | ||
``` | ||
|
||
## Options | ||
|
||
The rule accepts an options object with the following property: | ||
|
||
- `ignoreStatic` to not check whether `static` methods are correctly bound | ||
|
||
### `ignoreStatic` | ||
|
||
Examples of **correct** code for this rule with `{ ignoreStatic: true }`: | ||
|
||
```ts | ||
class OtherClass { | ||
static log() { | ||
console.log(OtherClass); | ||
} | ||
} | ||
|
||
// With `ignoreStatic`, statics are assumed to not rely on a particular scope | ||
const { log } = OtherClass; | ||
|
||
log(); | ||
``` | ||
|
||
### Example | ||
|
||
```json | ||
{ | ||
"@typescript-eslint/unbound-method": [ | ||
"error", | ||
{ | ||
"ignoreStatic": true | ||
} | ||
] | ||
} | ||
``` | ||
|
||
## When Not To Use It | ||
|
||
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. | ||
|
||
## Related To | ||
|
||
- TSLint: [no-unbound-method](https://palantir.github.io/tslint/rules/no-unbound-method/) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree'; | ||
import * as tsutils from 'tsutils'; | ||
import * as ts from 'typescript'; | ||
|
||
import * as util from '../util'; | ||
|
||
//------------------------------------------------------------------------------ | ||
// Rule Definition | ||
//------------------------------------------------------------------------------ | ||
|
||
interface Config { | ||
ignoreStatic: boolean; | ||
} | ||
|
||
type Options = [Config]; | ||
|
||
type MessageIds = 'unbound'; | ||
|
||
export default util.createRule<Options, MessageIds>({ | ||
name: 'unbound-method', | ||
meta: { | ||
docs: { | ||
category: 'Best Practices', | ||
description: | ||
'Enforces unbound methods are called with their expected scope.', | ||
tslintName: 'no-unbound-method', | ||
recommended: 'error', | ||
}, | ||
messages: { | ||
unbound: | ||
'Avoid referencing unbound methods which may cause unintentional scoping of `this`.', | ||
}, | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
ignoreStatic: { | ||
type: 'boolean', | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
type: 'problem', | ||
}, | ||
defaultOptions: [ | ||
{ | ||
ignoreStatic: false, | ||
}, | ||
], | ||
create(context, [{ ignoreStatic }]) { | ||
const parserServices = util.getParserServices(context); | ||
const checker = parserServices.program.getTypeChecker(); | ||
|
||
return { | ||
[AST_NODE_TYPES.MemberExpression](node: TSESTree.MemberExpression) { | ||
if (isSafeUse(node)) { | ||
return; | ||
} | ||
|
||
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); | ||
const symbol = checker.getSymbolAtLocation(originalNode); | ||
|
||
if (symbol && isDangerousMethod(symbol, ignoreStatic)) { | ||
context.report({ | ||
messageId: 'unbound', | ||
node, | ||
}); | ||
} | ||
}, | ||
}; | ||
}, | ||
}); | ||
|
||
function isDangerousMethod(symbol: ts.Symbol, ignoreStatic: boolean) { | ||
const { valueDeclaration } = symbol; | ||
|
||
switch (valueDeclaration.kind) { | ||
case ts.SyntaxKind.MethodDeclaration: | ||
case ts.SyntaxKind.MethodSignature: | ||
return !( | ||
ignoreStatic && | ||
tsutils.hasModifier( | ||
valueDeclaration.modifiers, | ||
ts.SyntaxKind.StaticKeyword, | ||
) | ||
); | ||
} | ||
|
||
return false; | ||
} | ||
|
||
function isSafeUse(node: TSESTree.Node): boolean { | ||
const parent = node.parent!; | ||
|
||
switch (parent.type) { | ||
case AST_NODE_TYPES.IfStatement: | ||
case AST_NODE_TYPES.ForStatement: | ||
case AST_NODE_TYPES.MemberExpression: | ||
case AST_NODE_TYPES.UpdateExpression: | ||
case AST_NODE_TYPES.WhileStatement: | ||
return true; | ||
|
||
case AST_NODE_TYPES.CallExpression: | ||
return parent.callee === node; | ||
|
||
case AST_NODE_TYPES.ConditionalExpression: | ||
return parent.test === node; | ||
|
||
case AST_NODE_TYPES.LogicalExpression: | ||
return parent.operator !== '||'; | ||
|
||
case AST_NODE_TYPES.TaggedTemplateExpression: | ||
return parent.tag === node; | ||
|
||
case AST_NODE_TYPES.TSNonNullExpression: | ||
case AST_NODE_TYPES.TSAsExpression: | ||
case AST_NODE_TYPES.TSTypeAssertion: | ||
return isSafeUse(parent); | ||
} | ||
|
||
return false; | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.