Skip to content
This repository was archived by the owner on Mar 25, 2021. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 66 additions & 4 deletions src/rules/noUnnecessaryTypeAssertionRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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());
Copy link
Copy Markdown
Contributor

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 Program was created using a custom CompilerHost.
I know we also do this in Linter.updateProgram, but that's only executed if you run with --fix.

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]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.
Also note that #2763 creates a new Program for every linted SourceFile. This rule does so for every potential failure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these cases should be very rare

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 tslint:disable comments. That doesn't turn off the rule. It just mutes its output.

};
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");
}
7 changes: 7 additions & 0 deletions test/rules/no-unnecessary-type-assertion/test.ts.fix
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/angular/index.d.ts#L2058

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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>();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 const x: string = foo() or an explicit type argument as in const x = foo<string>(); in these cases. The type assertion is still in some sense unnecessary as the code can be written without any type assertions, which is good since those are unsafe. foo() as string should really be reserved for functions like declare function foo(): string | number;.

bar(xyzzy);
7 changes: 7 additions & 0 deletions test/rules/no-unnecessary-type-assertion/test.ts.lint
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,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;
bar(xyzzy);