Skip to content

Fixed too eager type resolution when looking for symbol aliases #57396

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
50 changes: 46 additions & 4 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5731,15 +5731,57 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
});
}

function resolveTypeAliasSymbol(symbol: Symbol): Symbol | undefined {
if (!symbol.declarations) {
return;
}
for (const decl of symbol.declarations) {
if (!isTypeAliasDeclaration(decl)) {
continue;
}
const location = getDirectTypeAliasLocation(decl);
if (!location) {
continue;
}
const aliased = resolveSymbol(getSymbolAtLocation(location));
if (aliased) {
return aliased.flags & SymbolFlags.TypeAlias && resolveTypeAliasSymbol(aliased) || aliased;
}
}

function getDirectTypeAliasLocation(decl: TypeAliasDeclaration): Node | undefined {
Copy link
Member

Choose a reason for hiding this comment

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

I'm not a huge fan of this - this looks like it's attempting to reverse some of the logic done within getTypeFromTypeAliasReference and the like to assign alias symbols to a type in the first place, and I can't be confident it covers all cases, since that operates on types, while this is basically alternating between symbols and AST nodes while avoiding the information trafficked in the (declared) type. I'd be much happier if the logic here was actually shared with type alias assignments, eg, it actually used getAliasSymbolForTypeNode to lookup the symbol, and then shared logic with type creation to determine if that symbol actually applied to the resulting type. (Eg, this looks kinda like the isTypeReferenceType(node) branch in getTypeFromTypeAliasReference, with some heuristic only-types-without-type-parameters-would-hold-old-aliases thrown in, but because the logic isn't actually shared, I can't be sure they match)

Copy link
Contributor Author

@Andarist Andarist Feb 19, 2024

Choose a reason for hiding this comment

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

This kinda tries to follow checkTypeReferenceOrImport. I understand your hesitancy here but I'm not quite sure how I'm supposed to unify this with this function or the ones that were mentioned. The problem is that all of them primarily operate on the declared types so they resolve this information eagerly (what the previous code was doing).

The problem with the eager resolution is that type printing in error messages goes through similar code paths as the declaration emit. So an error raised on some relation ended up resolving types that were previously not resolved and that led to the raised circularity.

This new code is also recursive and the mentioned functions are not.

Consider this:

import { SomeType as IndirectSomeType } from './indirection'
export type SomeType/*1*/ = IndirectSomeType/*2*/;

When requesting getAliasSymbolForTypeNode on 2 we just end up on 1. This holds true:

getAliasSymbolForTypeNode(decl.type).declarations[0] === decl // true

So it doesn't give us any new information because it doesn't even go to the import location.

In addition to that, resolveTypeReferenceName (that I could use instead of getSymbolAtLocation) goes just to the import statement. And even resolveAlias goes just to the intermediate declaration site in ./indirection.ts and not to ./inner.ts.

So all in all, I don't see how to reuse the existing machinery, in a clean way, because that machinery is just shallow and still relies on declared types heavily - and that has to be avoided here. So I think that any attempt at unifying this would not be that clean/straightforward.

Alternatively, I was thinking about using the existing solution (what is currently on main) but to combine it with either one of those 2:

  • pass around some flag that would avoid this lookup when printing errors
  • make relation error printing lazy so by the time they are printed all the declared types are already set up correctly

PS. I have quite huge gaps in my understanding of how symbol aliases etc work today. It's not something that I have touched a lot.

Eg, this looks kinda like the isTypeReferenceType(node) branch in getTypeFromTypeAliasReference,

yes, kinda - that branch is only entered when type parameters are involved though and this code specifically wants to resolve those symbols when there are no type parameters. I feel like this is another indication that this is actually not that same-y

Copy link
Member

Choose a reason for hiding this comment

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

make relation error printing lazy so by the time they are printed all the declared types are already set up correctly

Personally, I think I'd much prefer this - the need for a setup phase like that is why we use checkNodeDeferred for many node kinds - it's a common pattern for us.

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 different from checkNodeDeferred - we don't want to defer checking, just error reporting. I thought that I could use addLazyDiagnostic for this but it turns out that it's still often called quite eagerly by checkSourceFileWithEagerDiagnostics. So - unless there is already something like this - I'd have to introduce a new (small) mechanism (maybe just an extension to either checkNodeDeferred or addLazyDiagnostic). I'll look into this tomorrow.

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've looked into it more. If you don't like the current approach then deferring those errors would be best.

However, it's not that straightforward. Reporting relation errors is quite convoluted and spread out across multiple places. reportRelationError eagerly converts types to strings using getTypeNamesForErrorDisplay (there are some extra eager typeToString calls there too).

It wouldn't be hard to adjust this and make it lazier if only this would directly be added to the diagnostic collection here. That's not the case though - this just creates (overrides?) errorInfo, that is later conditionally chained with containingMessageChain in checkTypeRelatedTo, relatedInformation might get appended to it, and only after that DiagnosticWithLocation gets created, and added to the diagnostic collection.

All of this assumes already a certain structure - one that is string-oriented. It's not impossible to adjust this. I feel that it would be a disruptive change though. I can commit to working on it if you feel that it's the best solution.

This PR fixes a regression and it would be great to backport a fix to the upcoming 5.4. Since making this type-to-string conversion deferred isn't straightforward I feel like it's too risky for it to be that (at this moment). After thinking about it more, I don't think the currently proposed solution is that bad. It does its job in an isolated manner and the rules around this lookup are pretty straightforward.

If you feel strongly that this is just not the best solution for the problem (which it might not be, I'm not the judge of that :P) then I propose adding a new internal TypeFormatFlags/NodeBuilderFlags/SymbolFormatFlags based on which the extra lookup introduced by #56087 would be avoided when printing types for relation errors. This would be less than ideal than the currently proposed solution since, well, a better print wouldn't be used by relation error messages - that's not a regression though (with 5.3 as the baseline). With this as the temporary fix in place, the regression would get addressed and I could work on making the relation error lazier for 5.5

if (decl.typeParameters) {
return;
}
const typeNode = decl.type;
switch (typeNode.kind) {
case SyntaxKind.TypeReference: {
const { typeName, typeArguments } = typeNode as TypeReferenceNode;
if (typeArguments) {
return;
}
return typeName;
}
case SyntaxKind.ImportType: {
const { qualifier, typeArguments } = typeNode as ImportTypeNode;
if (typeArguments) {
return;
}
return qualifier || typeNode;
}
}
}
}

/**
* Checks if two symbols, through aliasing and/or merging, refer to the same thing
*/
function getSymbolIfSameReference(s1: Symbol, s2: Symbol) {
if (s1.flags & SymbolFlags.TypeAlias && s2.declarations?.find(isTypeAlias)) {
s2 = getDeclaredTypeOfTypeAlias(s2).aliasSymbol || s2;
if (s1.flags & SymbolFlags.TypeAlias) {
s2 = resolveTypeAliasSymbol(s2) || s2;
}
if (s2.flags & SymbolFlags.TypeAlias && s1.declarations?.find(isTypeAlias)) {
s1 = getDeclaredTypeOfTypeAlias(s1).aliasSymbol || s1;
if (s2.flags & SymbolFlags.TypeAlias) {
s1 = resolveTypeAliasSymbol(s1) || s1;
}
if (getMergedSymbol(resolveSymbol(getMergedSymbol(s1))) === getMergedSymbol(resolveSymbol(getMergedSymbol(s2)))) {
return s1;
Expand Down
53 changes: 53 additions & 0 deletions tests/baselines/reference/declarationEmitUsingTypeAlias2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//// [tests/cases/compiler/declarationEmitUsingTypeAlias2.ts] ////

//// [inner.d.ts]
export declare type Other = { other: string };
export declare type SomeType = { arg: Other };

//// [indirection.d.ts]
import { Other as IndirectOther, SomeType as IndirectSomeType } from './inner';
export declare type Other = IndirectOther;
export declare type SomeType = IndirectSomeType

//// [index.d.ts]
import { Other, SomeType as IndirectSomeType } from './indirection';
export type OtherType = Other;
export type SomeType = IndirectSomeType;

//// [package.json]
{
"name": "some-dep",
"exports": {
".": "./dist/index.js"
}
}

//// [index.ts]
import { SomeType } from "some-dep";

export const foo = (thing: SomeType) => {
return thing;
};

export const bar = (thing: SomeType) => {
return thing.arg;
};

//// [index.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.bar = exports.foo = void 0;
var foo = function (thing) {
return thing;
};
exports.foo = foo;
var bar = function (thing) {
return thing.arg;
};
exports.bar = bar;


//// [index.d.ts]
import { SomeType } from "some-dep";
export declare const foo: (thing: SomeType) => import("some-dep").SomeType;
export declare const bar: (thing: SomeType) => import("some-dep").OtherType;
66 changes: 66 additions & 0 deletions tests/baselines/reference/declarationEmitUsingTypeAlias2.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//// [tests/cases/compiler/declarationEmitUsingTypeAlias2.ts] ////

=== node_modules/some-dep/dist/inner.d.ts ===
export declare type Other = { other: string };
>Other : Symbol(Other, Decl(inner.d.ts, 0, 0))
>other : Symbol(other, Decl(inner.d.ts, 0, 29))

export declare type SomeType = { arg: Other };
>SomeType : Symbol(SomeType, Decl(inner.d.ts, 0, 46))
>arg : Symbol(arg, Decl(inner.d.ts, 1, 32))
>Other : Symbol(Other, Decl(inner.d.ts, 0, 0))

=== node_modules/some-dep/dist/indirection.d.ts ===
import { Other as IndirectOther, SomeType as IndirectSomeType } from './inner';
>Other : Symbol(IndirectOther, Decl(inner.d.ts, 0, 0))
>IndirectOther : Symbol(IndirectOther, Decl(indirection.d.ts, 0, 8))
>SomeType : Symbol(IndirectSomeType, Decl(inner.d.ts, 0, 46))
>IndirectSomeType : Symbol(IndirectSomeType, Decl(indirection.d.ts, 0, 32))

export declare type Other = IndirectOther;
>Other : Symbol(Other, Decl(indirection.d.ts, 0, 79))
>IndirectOther : Symbol(IndirectOther, Decl(indirection.d.ts, 0, 8))

export declare type SomeType = IndirectSomeType
>SomeType : Symbol(SomeType, Decl(indirection.d.ts, 1, 42))
>IndirectSomeType : Symbol(IndirectSomeType, Decl(indirection.d.ts, 0, 32))

=== node_modules/some-dep/dist/index.d.ts ===
import { Other, SomeType as IndirectSomeType } from './indirection';
>Other : Symbol(Other, Decl(index.d.ts, 0, 8))
>SomeType : Symbol(IndirectSomeType, Decl(indirection.d.ts, 1, 42))
>IndirectSomeType : Symbol(IndirectSomeType, Decl(index.d.ts, 0, 15))

export type OtherType = Other;
>OtherType : Symbol(OtherType, Decl(index.d.ts, 0, 68))
>Other : Symbol(Other, Decl(index.d.ts, 0, 8))

export type SomeType = IndirectSomeType;
>SomeType : Symbol(SomeType, Decl(index.d.ts, 1, 30))
>IndirectSomeType : Symbol(IndirectSomeType, Decl(index.d.ts, 0, 15))

=== src/index.ts ===
import { SomeType } from "some-dep";
>SomeType : Symbol(SomeType, Decl(index.ts, 0, 8))

export const foo = (thing: SomeType) => {
>foo : Symbol(foo, Decl(index.ts, 2, 12))
>thing : Symbol(thing, Decl(index.ts, 2, 20))
>SomeType : Symbol(SomeType, Decl(index.ts, 0, 8))

return thing;
>thing : Symbol(thing, Decl(index.ts, 2, 20))

};

export const bar = (thing: SomeType) => {
>bar : Symbol(bar, Decl(index.ts, 6, 12))
>thing : Symbol(thing, Decl(index.ts, 6, 20))
>SomeType : Symbol(SomeType, Decl(index.ts, 0, 8))

return thing.arg;
>thing.arg : Symbol(arg, Decl(inner.d.ts, 1, 32))
>thing : Symbol(thing, Decl(index.ts, 6, 20))
>arg : Symbol(arg, Decl(inner.d.ts, 1, 32))

};
61 changes: 61 additions & 0 deletions tests/baselines/reference/declarationEmitUsingTypeAlias2.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//// [tests/cases/compiler/declarationEmitUsingTypeAlias2.ts] ////

=== node_modules/some-dep/dist/inner.d.ts ===
export declare type Other = { other: string };
>Other : { other: string; }
>other : string

export declare type SomeType = { arg: Other };
>SomeType : { arg: Other; }
>arg : Other

=== node_modules/some-dep/dist/indirection.d.ts ===
import { Other as IndirectOther, SomeType as IndirectSomeType } from './inner';
>Other : any
>IndirectOther : any
>SomeType : any
>IndirectSomeType : any

export declare type Other = IndirectOther;
>Other : IndirectOther

export declare type SomeType = IndirectSomeType
>SomeType : IndirectSomeType

=== node_modules/some-dep/dist/index.d.ts ===
import { Other, SomeType as IndirectSomeType } from './indirection';
>Other : any
>SomeType : any
>IndirectSomeType : any

export type OtherType = Other;
>OtherType : import("node_modules/some-dep/dist/inner").Other

export type SomeType = IndirectSomeType;
>SomeType : import("node_modules/some-dep/dist/inner").SomeType

=== src/index.ts ===
import { SomeType } from "some-dep";
>SomeType : any

export const foo = (thing: SomeType) => {
>foo : (thing: SomeType) => import("node_modules/some-dep/dist/inner").SomeType
>(thing: SomeType) => { return thing;} : (thing: SomeType) => import("node_modules/some-dep/dist/inner").SomeType
>thing : import("node_modules/some-dep/dist/inner").SomeType

return thing;
>thing : import("node_modules/some-dep/dist/inner").SomeType

};

export const bar = (thing: SomeType) => {
>bar : (thing: SomeType) => import("node_modules/some-dep/dist/inner").Other
>(thing: SomeType) => { return thing.arg;} : (thing: SomeType) => import("node_modules/some-dep/dist/inner").Other
>thing : import("node_modules/some-dep/dist/inner").SomeType

return thing.arg;
>thing.arg : import("node_modules/some-dep/dist/inner").Other
>thing : import("node_modules/some-dep/dist/inner").SomeType
>arg : import("node_modules/some-dep/dist/inner").Other

};
52 changes: 52 additions & 0 deletions tests/baselines/reference/errorReportNotCircular1.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
data.ts(6,24): error TS2345: Argument of type '{}' is not assignable to parameter of type 'Data'.
Property 'quantity' is missing in type '{}' but required in type 'Data'.
data.ts(6,28): error TS1360: Type '{ quantity: Value; }' does not satisfy the expected type '[]'.


==== data.ts (2 errors) ====
// https://github.com/microsoft/TypeScript/issues/57357

import type { Data } from "./processor";

export function getData() {
return constructData({}) satisfies []; // error
~~
!!! error TS2345: Argument of type '{}' is not assignable to parameter of type 'Data'.
!!! error TS2345: Property 'quantity' is missing in type '{}' but required in type 'Data'.
!!! related TS2728 processor.ts:6:3: 'quantity' is declared here.
~~~~~~~~~
!!! error TS1360: Type '{ quantity: Value; }' does not satisfy the expected type '[]'.
}

function constructData(data: Data) {
const { ...props } = data;
return {
...props,
};
}

==== type.ts (0 errors) ====
export type Value = {};

==== processor.ts (0 errors) ====
import { getData } from "./data";

import type { Value } from "./type";

export type Data = {
quantity: Value;
};

declare function createRequestProcessor<Req>(pipeline: () => Req): Req;

export const processor = createRequestProcessor(getData);

==== main.ts (0 errors) ====
import { processor } from "./processor";
export default processor;

==== workerinterface.ts (0 errors) ====
import type Server from "./main";

export type _T = typeof Server;

65 changes: 65 additions & 0 deletions tests/baselines/reference/errorReportNotCircular1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//// [tests/cases/compiler/errorReportNotCircular1.ts] ////

//// [data.ts]
// https://github.com/microsoft/TypeScript/issues/57357

import type { Data } from "./processor";

export function getData() {
return constructData({}) satisfies []; // error
}

function constructData(data: Data) {
const { ...props } = data;
return {
...props,
};
}

//// [type.ts]
export type Value = {};

//// [processor.ts]
import { getData } from "./data";

import type { Value } from "./type";

export type Data = {
quantity: Value;
};

declare function createRequestProcessor<Req>(pipeline: () => Req): Req;

export const processor = createRequestProcessor(getData);

//// [main.ts]
import { processor } from "./processor";
export default processor;

//// [workerinterface.ts]
import type Server from "./main";

export type _T = typeof Server;


//// [type.js]
export {};
//// [processor.js]
import { getData } from "./data";
export const processor = createRequestProcessor(getData);
//// [data.js]
// https://github.com/microsoft/TypeScript/issues/57357
export function getData() {
return constructData({}); // error
}
function constructData(data) {
const { ...props } = data;
return {
...props,
};
}
//// [main.js]
import { processor } from "./processor";
export default processor;
//// [workerinterface.js]
export {};
Loading