Skip to content
This repository has been archived by the owner on Mar 25, 2021. It is now read-only.

Fix more false positives in no-unnecessary-type-assertion #3465

Closed
wants to merge 5 commits into from
Closed

Fix more false positives in no-unnecessary-type-assertion #3465

wants to merge 5 commits into from

Conversation

calebegg
Copy link
Contributor

@calebegg calebegg commented Nov 7, 2017

This technique was suggested by @alexeagle -- thanks!

The approach here is to basically determine what would actually happen from the perspective of the TS typechecker if the suggested replacement was applied. That way, we can catch false positives where the assertion actually affects inference.

There's a new test case that demonstrates an instance of this kind of false positive.

I had hoped that adding a check like this would allow me to also eliminate the false positive check for tuple/literal types, but it doesn't. Even with this new approach, the type we get from the type checker still gets widened in certain cases, so we still have to watch for that. Anyway, the old check is much faster, so it's good to keep around regardless.

Copy link
Contributor

@alexeagle alexeagle left a comment

Choose a reason for hiding this comment

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

Cool! /cc @andy-ms who I think removed this trick from no-unused-variable where I had used it before. I think this time it's appropriate since you only do this after the faster mostly-correct check.

Copy link
Contributor

@ajafff ajafff left a comment

Choose a reason for hiding this comment

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

Based on the experience in #2763 I'm -1 on this change

return modifiedSourceFile;
}
// tslint:disable-next-line:no-unsafe-any Passing args along to original function.
return oldGetSourceFile.apply(this, [fileName, ...args]);
Copy link
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
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
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.

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

function bar(s: string) {}

// Type error without this assertion
const xyzzy = foo() as string;
Copy link
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
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
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
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

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

if (haystack.pos === updatedPos && haystack.end === updatedEnd) {
return haystack;
}
for (const child of haystack.getChildren()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer ts.forEachChild since .getChildren() additionally returns tokens not present in the AST. These tokens are not relevant for the rule.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. That's a very strange/subtle undocumented difference....

}
}
throw new Error("Can't find corresponding node");
Copy link
Contributor

Choose a reason for hiding this comment

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

If an error is thrown, there's not need for the | null in the return type

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@alexeagle
Copy link
Contributor

A bit more context, in the TS design review meeting microsoft/TypeScript#19393 @mhegazy observed that checks which are computationally expensive (eg requiring control flow analysis) have been traditionally been first-party in TypeScript. That wasn't really intended, it seems to me that TypeScript's diagnostics should be concerned with the Language Spec. One suggestion in that meeting was to extend the TypeChecker APIs to make it easier for third-parties like TSLint to write such rules. I think this deserves some other discussion - I just point that out here in case there is some missing API that would make it possible to fix this false positive without creating a new program.

@ajafff
Copy link
Contributor

ajafff commented Nov 7, 2017

in case there is some missing API that would make it possible to fix this false positive without creating a new program.

That goes too deep into TypeScript internals. I don't think they will expose an API to find out if a node is involved in inferring the type of another node.

It'd be great if it was possible to reuse the whole program except for one specific SourceFile. Unfortunately that doesn't work as it messes up everything else: #2736 and #2649

@alexeagle
Copy link
Contributor

I fear that we will write more type-checked rules where we are forced to accept false positives, or worse, we won't be able to write the rule at all given our limited ability to query or interact with the type-checker. At Google, we don't want to train our users to suppress compiler diagnostics due to their incorrectness, and we want to have some rules which cannot be suppressed (eg. related to security sensitive APIs)

Discussed with @mhegazy and @DanielRosenwasser yesterday. There are two options on the table, but neither is a short-term fix for this PR:

  1. Expose additional query operations on the typechecker to support rules like this one. TS team is okay with doing this. I think the big problem is that we can't know a priori what queries we might need for other type-checked rules in the future, so we'll frequently be in the position of waiting for an upstream TS release to unblock a rule implementation.
  2. Make @calebegg's approach here perform. Mohamed points out that it still doesn't scale in general (can't implement every check by creating a new ts.Program) but our objective is only to use it after we've exited the hot code path with other simpler conditionals. Rough proposal needs writing up, but something like:
  • use new ts.CustomTransformers API to create an AST transform. Make sure any new nodes created in this way are bound and have parent pointers.
  • Change TypeScript so that checker.ts doesn't rely on the tree being backed by ts.SourceFile text. This ought to be the design anyway. Would require some refactoring.
  • [mumble mumble] avoid destructive operations on the old ts.Program by doing the right thing. I think this is simply avoiding the hacks we did with the fake host in the unused variable check you mention above.
  • now we can query the type-checker about anything in the transformed program

@giladgray
Copy link

@calebegg please update this branch so we can review it, or close it if not relevant.

this change seems rather dangerous and lacks a safe answer. what would happen if we didn't do this?

@calebegg
Copy link
Contributor Author

I don't think updating this old PR would be a worthwhile use of my time. The decision of whether or not to accept it can be made without doing that.

@johnwiseheart
Copy link
Contributor

Closing

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants