Skip to content
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

Fixed a crash when inferring return type of an accessor with errors in its return statement #56258

Merged
Merged
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
2 changes: 1 addition & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7312,7 +7312,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

if (propertySymbol.flags & SymbolFlags.Accessor) {
const writeType = getWriteTypeOfSymbol(propertySymbol);
if (propertyType !== writeType) {
if (propertyType !== writeType && !isErrorType(propertyType) && !isErrorType(writeType)) {
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 feel like it might be seen as a little bit of an ad-hoc fix. The problem is that a resolved type of a position with a cycle usually is anyType but when we are resolving the type initially that type is returned as an error type when the cycle is detected.

So there is some small mismatch between the return values of functions like getTypeOfAccessors - one that depends on the timing of the call to them. I assume that this is intentional.

I noticed that getWriteTypeOfAccessors could accidentally-ish return the errorType and cache it. So I tried to fix it with:

diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index 074c3d3034..59c538e298 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -11758,7 +11758,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
                 writeType = anyType;
             }
             // Absent an explicit setter type annotation we use the read type of the accessor.
-            links.writeType = writeType || getTypeOfAccessors(symbol);
+            if (!writeType) {
+                const readType = getTypeOfAccessors(symbol);
+                writeType = readType !== errorType ? readType : anyType;
+            }
+            links.writeType = writeType;
         }
         return links.writeType;
     }

That didn't fix the issue though because when this line (the one that I'm changing here) was hit for the first time we had a situation like this:

propertyType // errorType
writeType // anyType

So the mismatch was still here - it just happened sooner. Originally, the mismatch could happen later when the property type already had a chance to "settle" as anyType but the writeType was already cached as the errorType.

I think this fix is quite fine since errorType being returned while resolving the type initially is expected and when that happens we don't quite need to serialize the property as one with divergent accessors. Maybe some further fine-tuning can be done here. I imagine that maybe there is some value in cases that could be serialized as:

export declare var basePrototype: {
    get primaryPath(): string;
    set primaryPath(v: any); // coming from the error
};

I don't have a test case for that at hand though. This whole issue that is being fixed by this PR is a regression so it's worth fixing it sooner than later 😉

I also think that maybe the patch that I posted above might still be worth pulling in since caching errorType here looks like something that is not intended. I don't have any test case that would prove it though.

Copy link
Member

Choose a reason for hiding this comment

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

The problem is that a resolved type of a position with a cycle usually is anyType

Maybe my memory is shot, but I kinda thought that we always returned errorType whenever a cycle occurred? Maybe that's just in the push/pop resolution world?

Copy link
Member

@DanielRosenwasser DanielRosenwasser Nov 14, 2023

Choose a reason for hiding this comment

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

I don't think there is anything wrong with letting an errorType propagate further (or get cached or whatever). It exists to act as an any and to specially handle more permissively in other cases.

(I could be wrong!)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Perhaps this is just somewhat inconsistent across different callers. I see that some similar ones are returning errorType just fine. However, for example here the cycle is detected and errorType is returned immediately but when we climb up the stack to the first "visitor" of this type that errorType is converted to anyType here. A similar situation ("converting" errorType to anyType) can be seen in getTypeOfAccessors here and in getWriteTypeOfAccessors here.

I don't think there is anything wrong with letting an errorType propagate further (or get cached or whatever). It exists to act as an any and to specially handle more permissively in other cases.

Ye, it might not be a problem at all. I just tried to follow the pre-existing conventions in other functions.

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'll prepare an experiment later that will just return/cache errorType in all of those locations that detect the cycle.

const getterDeclaration = getDeclarationOfKind<GetAccessorDeclaration>(propertySymbol, SyntaxKind.GetAccessor)!;
const getterSignature = getSignatureFromDeclaration(getterDeclaration);
typeElements.push(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
accessorInferredReturnTypeErrorInReturnStatement.ts(2,7): error TS7023: 'primaryPath' implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.
accessorInferredReturnTypeErrorInReturnStatement.ts(4,18): error TS2339: Property 'collection' does not exist on type '{ readonly primaryPath: any; }'.


==== accessorInferredReturnTypeErrorInReturnStatement.ts (2 errors) ====
export var basePrototype = {
get primaryPath() {
~~~~~~~~~~~
!!! error TS7023: 'primaryPath' implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.
var _this = this;
return _this.collection.schema.primaryPath;
~~~~~~~~~~
!!! error TS2339: Property 'collection' does not exist on type '{ readonly primaryPath: any; }'.
},
};

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//// [tests/cases/compiler/accessorInferredReturnTypeErrorInReturnStatement.ts] ////

//// [accessorInferredReturnTypeErrorInReturnStatement.ts]
export var basePrototype = {
get primaryPath() {
var _this = this;
return _this.collection.schema.primaryPath;
},
};


//// [accessorInferredReturnTypeErrorInReturnStatement.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.basePrototype = void 0;
exports.basePrototype = {
get primaryPath() {
var _this = this;
return _this.collection.schema.primaryPath;
},
};


//// [accessorInferredReturnTypeErrorInReturnStatement.d.ts]
export declare var basePrototype: {
readonly primaryPath: any;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//// [tests/cases/compiler/accessorInferredReturnTypeErrorInReturnStatement.ts] ////

=== accessorInferredReturnTypeErrorInReturnStatement.ts ===
export var basePrototype = {
>basePrototype : Symbol(basePrototype, Decl(accessorInferredReturnTypeErrorInReturnStatement.ts, 0, 10))

get primaryPath() {
>primaryPath : Symbol(primaryPath, Decl(accessorInferredReturnTypeErrorInReturnStatement.ts, 0, 28))

var _this = this;
>_this : Symbol(_this, Decl(accessorInferredReturnTypeErrorInReturnStatement.ts, 2, 7))
>this : Symbol(basePrototype, Decl(accessorInferredReturnTypeErrorInReturnStatement.ts, 0, 26))

return _this.collection.schema.primaryPath;
>_this : Symbol(_this, Decl(accessorInferredReturnTypeErrorInReturnStatement.ts, 2, 7))

},
};

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//// [tests/cases/compiler/accessorInferredReturnTypeErrorInReturnStatement.ts] ////

=== accessorInferredReturnTypeErrorInReturnStatement.ts ===
export var basePrototype = {
>basePrototype : { readonly primaryPath: any; }
>{ get primaryPath() { var _this = this; return _this.collection.schema.primaryPath; }, } : { readonly primaryPath: any; }

get primaryPath() {
>primaryPath : any

var _this = this;
>_this : { readonly primaryPath: any; }
>this : { readonly primaryPath: any; }

return _this.collection.schema.primaryPath;
>_this.collection.schema.primaryPath : any
>_this.collection.schema : any
>_this.collection : any
>_this : { readonly primaryPath: any; }
>collection : any
>schema : any
>primaryPath : any

},
};

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @strict: true
// @declaration: true

export var basePrototype = {
get primaryPath() {
var _this = this;
return _this.collection.schema.primaryPath;
},
};
Loading