-
Notifications
You must be signed in to change notification settings - Fork 877
Fix more false positives in no-unnecessary-type-assertion #3465
Changes from all commits
cffa68e
02b7f81
fcf0359
3aab8d6
ffcf335
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,13 +42,15 @@ export class Rule extends Lint.Rules.TypedRule { | |
| public static FAILURE_STRING = "This assertion is unnecessary since it does not change the type of the expression."; | ||
|
|
||
| public applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { | ||
| return this.applyWithWalker(new Walker(sourceFile, this.ruleName, this.ruleArguments, program.getTypeChecker())); | ||
| return this.applyWithWalker(new Walker(sourceFile, this.ruleName, this.ruleArguments, program)); | ||
| } | ||
| } | ||
|
|
||
| class Walker extends Lint.AbstractWalker<string[]> { | ||
| constructor(sourceFile: ts.SourceFile, ruleName: string, options: string[], private readonly checker: ts.TypeChecker) { | ||
| private readonly checker: ts.TypeChecker; | ||
| constructor(sourceFile: ts.SourceFile, ruleName: string, options: string[], private readonly program: ts.Program) { | ||
| super(sourceFile, ruleName, options); | ||
| this.checker = program.getTypeChecker(); | ||
| } | ||
|
|
||
| public walk(sourceFile: ts.SourceFile) { | ||
|
|
@@ -92,9 +94,69 @@ class Walker extends Lint.AbstractWalker<string[]> { | |
|
|
||
| const uncastType = this.checker.getTypeAtLocation(node.expression); | ||
| if (uncastType === castType) { | ||
| this.addFailureAtNode(node, Rule.FAILURE_STRING, node.kind === ts.SyntaxKind.TypeAssertionExpression | ||
| // In some cases, this is still not enough; the type in the | ||
| // assertion can actually impact the type in the expression being | ||
| // asserted. To avoid these false positives, we create a new | ||
| // ts.Program with the edited file, to see if the assertion is still | ||
| // unnecessary. | ||
| // This can be a bit slow, so we still want to guard it by the more | ||
| // basic check above. | ||
| const replacement = node.kind === ts.SyntaxKind.TypeAssertionExpression | ||
| ? Lint.Replacement.deleteFromTo(node.getStart(), node.expression.getStart()) | ||
| : Lint.Replacement.deleteFromTo(node.expression.getEnd(), node.getEnd())); | ||
| : Lint.Replacement.deleteFromTo(node.expression.getEnd(), node.getEnd()); | ||
| const sourceFile = this.sourceFile; | ||
| const modifiedHost = ts.createCompilerHost(this.program.getCompilerOptions()); | ||
| const modifiedSourceFile = | ||
| ts.createSourceFile( | ||
| sourceFile.fileName, | ||
| replacement.apply(sourceFile.text), | ||
| sourceFile.languageVersion); | ||
| const oldGetSourceFile = modifiedHost.getSourceFile; | ||
| modifiedHost.getSourceFile = function(fileName: string, ...args: any[]) { | ||
| if (fileName === sourceFile.fileName) { | ||
| return modifiedSourceFile; | ||
| } | ||
| // tslint:disable-next-line:no-unsafe-any Passing args along to original function. | ||
| return oldGetSourceFile.apply(this, [fileName, ...args]); | ||
|
Contributor
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. This will be VERY slow even in small projects. See #2763 (comment). The difference here is, that this implementation reads all but the currently linted sourcefile from disk instead of using the content of the original sourcefile. That makes it even worse.
Contributor
Author
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. I tried it internally and it took about 0.4 seconds on a medium-large project. I think these cases should be very rare, so this "slow path" will almost always only occur with true positives, which should then (hopefully) get removed.
Contributor
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.
Except for people who just enabled the rule. They will probably think the process ran into an endless loop. In addition there are projects that disable specific rules for certain (often legacy) files via |
||
| }; | ||
| const modifiedChecker = | ||
| ts.createProgram( | ||
| this.program.getRootFileNames(), | ||
| this.program.getCompilerOptions(), | ||
| modifiedHost).getTypeChecker(); | ||
|
|
||
| const typeWithoutCast = | ||
| modifiedChecker.getTypeAtLocation( | ||
| getCorrespondingNode(node.expression, modifiedSourceFile, replacement)); | ||
| if (modifiedChecker.typeToString(typeWithoutCast) === | ||
| this.checker.typeToString(castType)) { | ||
| this.addFailureAtNode(node, Rule.FAILURE_STRING, replacement); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Given a node (needle) and a parent node (haystack) from a modified file, | ||
| * find the node that corresponds, taking into account the replacement that was | ||
| * applied. "Corresponds" here means that it has the same start and end. | ||
| */ | ||
| function getCorrespondingNode(needle: ts.Node, haystack: ts.Node, replacement: Lint.Replacement): ts.Node { | ||
| // Update location of needle to account for replacement that's been applied. | ||
| const updatedPos = needle.pos > replacement.start ? needle.pos - replacement.length - 1 : needle.pos; | ||
| const updatedEnd = needle.end > replacement.start ? needle.end - replacement.length : needle.end; | ||
| if (haystack.pos === updatedPos && haystack.end === updatedEnd) { | ||
| return haystack; | ||
| } | ||
| const foundNode = ts.forEachChild(haystack, (child) => { | ||
| if (child.pos <= updatedPos && child.end >= updatedEnd) { | ||
| return getCorrespondingNode(needle, child, replacement); | ||
| } else { | ||
| return undefined; | ||
| } | ||
| }); | ||
| if (foundNode != undefined) { | ||
| return foundNode; | ||
| } | ||
| throw new Error("Can't find corresponding node"); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -83,3 +83,10 @@ const data = { | |
|
|
||
| [1, 2, 3, 4, 5].map(x => [x, 'A' + x] as [number, string]); | ||
| let x: Array<[number, string]> = [1, 2, 3, 4, 5].map(x => [x, 'A' + x] as [number, string]); | ||
|
|
||
| function foo<T>(): T {} | ||
| function bar(s: string) {} | ||
|
|
||
| // Type error without this assertion | ||
| const xyzzy = foo() as string; | ||
|
Contributor
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. If this is the only false positive, I don't think it's worth sacrificing the normal case
Contributor
Author
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. It's come up several times for us. It's not a common pattern, but it does happen with e.g. Angular's .d.ts
Contributor
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. Instead of asserting the type you can just explicitly declare the type argument on the function call: const v = foo<string>();
Contributor
Author
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. That's true, and that's what I suggest when people file bugs. But people keep filing bugs, and I am a limited resource. 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. You could just change the error message to mention that there are cases like this, and recommend using a contextual type as in |
||
| bar(xyzzy); | ||
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.
this won't work if the
Programwas created using a customCompilerHost.I know we also do this in
Linter.updateProgram, but that's only executed if you run with--fix.