This repository has been archived by the owner on Mar 25, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 889
Whitelist option for no-unbound-method
#4472
Merged
JoshuaKGoldberg
merged 15 commits into
palantir:master
from
piotrgajow:issue/3755/UnboundMethodExpect
Feb 3, 2019
Merged
Changes from 12 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
f3bfb0b
Add tests for unbound method whitelist
piotrgajow 98c5864
Extend no-unbound-method parameters
piotrgajow d584708
Merge whitelist parameters into one
piotrgajow b67bdfb
Handle whitelisting typeof expression
piotrgajow 1e3c935
Handle method reference in whitelisted function call
piotrgajow 91cc507
Update rule description and examples
piotrgajow 541568e
Fix rule description
piotrgajow 9cc9647
Use is* type guard from tsutils
piotrgajow b42389b
Change parameter types and parsing
piotrgajow f27072b
Fix error with addint elements to set
piotrgajow d51bf19
Move handling of typeof to separate option
piotrgajow f378058
Fix linter issue
piotrgajow 71761ae
Use expression.text instead of expression.escapedText
piotrgajow bdb09d3
Fix for edge case
piotrgajow 7794931
Fix linter issue
piotrgajow 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 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 |
---|---|---|
|
@@ -15,27 +15,76 @@ | |
* limitations under the License. | ||
*/ | ||
|
||
import { hasModifier, isPropertyAccessExpression } from "tsutils"; | ||
import { | ||
hasModifier, | ||
isCallExpression, | ||
isPropertyAccessExpression, | ||
isTypeOfExpression, | ||
} from "tsutils"; | ||
import * as ts from "typescript"; | ||
import * as Lint from "../index"; | ||
|
||
const OPTION_IGNORE_STATIC = "ignore-static"; | ||
const OPTION_WHITELIST = "whitelist"; | ||
const OPTION_ALLOW_TYPEOF = "allow-typeof"; | ||
|
||
const OPTION_WHITELIST_EXAMPLE = [ | ||
true, | ||
{ | ||
[OPTION_IGNORE_STATIC]: true, | ||
[OPTION_WHITELIST]: ["expect"], | ||
[OPTION_ALLOW_TYPEOF]: true, | ||
}, | ||
]; | ||
|
||
interface Options { | ||
allowTypeof: boolean; | ||
ignoreStatic: boolean; | ||
whitelist: Set<string>; | ||
} | ||
|
||
interface OptionsInput { | ||
[OPTION_ALLOW_TYPEOF]?: boolean; | ||
[OPTION_IGNORE_STATIC]?: boolean; | ||
[OPTION_WHITELIST]?: string[]; | ||
} | ||
|
||
export class Rule extends Lint.Rules.TypedRule { | ||
/* tslint:disable:object-literal-sort-keys */ | ||
public static metadata: Lint.IRuleMetadata = { | ||
ruleName: "no-unbound-method", | ||
description: "Warns when a method is used outside of a method call.", | ||
optionsDescription: `You may optionally pass "${OPTION_IGNORE_STATIC}" to ignore static methods.`, | ||
optionsDescription: Lint.Utils.dedent` | ||
You may additionally pass "${OPTION_IGNORE_STATIC}" to ignore static methods, or an options object. | ||
|
||
The object may have three properties: | ||
|
||
* "${OPTION_IGNORE_STATIC}" - to ignore static methods. | ||
* "${OPTION_ALLOW_TYPEOF}" - ignore methods referenced in a typeof expression. | ||
* "${OPTION_WHITELIST}" - ignore method references in parameters of specifed function calls. | ||
|
||
`, | ||
options: { | ||
type: "string", | ||
enum: [OPTION_IGNORE_STATIC], | ||
anyOf: [ | ||
{ | ||
type: "string", | ||
enum: [OPTION_IGNORE_STATIC], | ||
}, | ||
{ | ||
type: "object", | ||
properties: { | ||
[OPTION_ALLOW_TYPEOF]: { type: "boolean" }, | ||
[OPTION_IGNORE_STATIC]: { type: "boolean" }, | ||
[OPTION_WHITELIST]: { | ||
type: "array", | ||
items: { type: "string" }, | ||
minLength: 1, | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
optionExamples: [true, [true, OPTION_IGNORE_STATIC]], | ||
optionExamples: [true, [true, OPTION_IGNORE_STATIC], OPTION_WHITELIST_EXAMPLE], | ||
rationale: Lint.Utils.dedent` | ||
Class functions don't preserve the class scope when passed as standalone variables. | ||
For example, this code will log the global scope (\`window\`/\`global\`), not the class instance: | ||
|
@@ -87,20 +136,46 @@ export class Rule extends Lint.Rules.TypedRule { | |
return this.applyWithFunction( | ||
sourceFile, | ||
walk, | ||
{ | ||
ignoreStatic: this.ruleArguments.indexOf(OPTION_IGNORE_STATIC) !== -1, | ||
}, | ||
parseArguments(this.ruleArguments), | ||
program.getTypeChecker(), | ||
); | ||
} | ||
} | ||
|
||
function parseArguments(args: Array<string | OptionsInput>): Options { | ||
const options: Options = { | ||
allowTypeof: false, | ||
ignoreStatic: false, | ||
whitelist: new Set(), | ||
}; | ||
|
||
for (const arg of args) { | ||
if (typeof arg === "string") { | ||
if (arg === OPTION_IGNORE_STATIC) { | ||
options.ignoreStatic = true; | ||
} | ||
} else { | ||
options.allowTypeof = arg[OPTION_ALLOW_TYPEOF] || false; | ||
options.ignoreStatic = arg[OPTION_IGNORE_STATIC] || false; | ||
options.whitelist = new Set(arg[OPTION_WHITELIST]); | ||
} | ||
} | ||
|
||
return options; | ||
} | ||
|
||
function walk(ctx: Lint.WalkContext<Options>, tc: ts.TypeChecker) { | ||
return ts.forEachChild(ctx.sourceFile, function cb(node): void { | ||
if (isPropertyAccessExpression(node) && !isSafeUse(node)) { | ||
const symbol = tc.getSymbolAtLocation(node); | ||
const declaration = symbol === undefined ? undefined : symbol.valueDeclaration; | ||
if (declaration !== undefined && isMethod(declaration, ctx.options.ignoreStatic)) { | ||
|
||
const isMethodAccess = | ||
declaration !== undefined && isMethod(declaration, ctx.options.ignoreStatic); | ||
const shouldBeReported = | ||
isMethodAccess && | ||
!isWhitelisted(node, ctx.options.whitelist, ctx.options.allowTypeof); | ||
if (shouldBeReported) { | ||
ctx.addFailureAtNode(node, Rule.FAILURE_STRING); | ||
} | ||
} | ||
|
@@ -150,3 +225,15 @@ function isSafeUse(node: ts.Node): boolean { | |
return false; | ||
} | ||
} | ||
|
||
function isWhitelisted(node: ts.Node, whitelist: Set<string>, allowTypeof: boolean): boolean { | ||
if (isTypeOfExpression(node.parent)) { | ||
return allowTypeof; | ||
} | ||
if (isCallExpression(node.parent)) { | ||
const expression = node.parent.expression as ts.Identifier; | ||
const callingMethodName = expression.escapedText as string; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Looks like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that was the issue. Thanks! |
||
return whitelist.has(callingMethodName); | ||
} | ||
return false; | ||
} |
This file contains 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,67 @@ | ||
class C { | ||
method(x: number) {} | ||
property: () => void; | ||
template(strs: TemplateStringsArray, x: any) {} | ||
} | ||
|
||
const c = new C(); | ||
[0].forEach(c.method); | ||
~~~~~~~~ [0] | ||
[0].forEach(x => c.method(x)); | ||
[0].forEach(c.property); | ||
|
||
c.template; | ||
~~~~~~~~~~ [0] | ||
c.template`foo${0}`; | ||
String.raw`${c.template}`; | ||
~~~~~~~~~~ [0] | ||
|
||
expect(c.method).toHaveBeenCalled(); | ||
typeof c.method; | ||
|
||
test(c.method); | ||
~~~~~~~~ [0] | ||
|
||
interface I { | ||
foo(): void; | ||
bar: () => void; | ||
} | ||
declare var i: I; | ||
i.foo; | ||
~~~~~ [0] | ||
i.bar; | ||
|
||
c.method === i.foo; | ||
|
||
// OK in condition | ||
c.method ? 1 : 2; | ||
1 ? c.method : c.method; | ||
~~~~~~~~ [0] | ||
~~~~~~~~ [0] | ||
if (c.method) {} | ||
while (c.method) {} | ||
do {} while (c.method); | ||
for (c.method; c.method; c.method) {} | ||
|
||
|
||
[0].forEach(c.method || i.foo); | ||
~~~~~~~~ [0] | ||
~~~~~ [0] | ||
[0].forEach(c.method.bind(c)); | ||
|
||
<button onClick={c.method}>Click me!</button>; | ||
~~~~~~~~ [0] | ||
|
||
class Validators { | ||
static required() { | ||
return null; | ||
} | ||
static compose(...args: Function[]) {} | ||
} | ||
|
||
Validators.compose(Validators.required); | ||
~~~~~~~~~~~~~~~~~~~ [0] | ||
|
||
|
||
|
||
[0]: Avoid referencing unbound methods which may cause unintentional scoping of 'this'. |
This file contains 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,5 @@ | ||
{ | ||
"compilerOptions": { | ||
"module": "commonjs" | ||
} | ||
} |
This file contains 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,8 @@ | ||
{ | ||
"linterOptions": { | ||
"typeCheck": true | ||
}, | ||
"rules": { | ||
"no-unbound-method": [true, { "whitelist": ["expect"], "allow-typeof": true }] | ||
} | ||
} |
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's actually possible for
node.parent.expression
to not be an identifier. You'll want to add test cases for:I don't think we should worry about trying to allow these even if
expectA
andexpectB
are whitelisted. Suggestion: just don't run this check if the parent's expression isn't an identifier.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, changed as you suggested. I maybe might include it in some future PR