Skip to content

Fix reference marking of values merged with unresolvable type-only imports #54799

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

Merged
merged 2 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
71 changes: 38 additions & 33 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4457,32 +4457,37 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
* @returns SymbolFlags.All if `symbol` is an alias that ultimately resolves to `unknown`;
* combined flags of all alias targets otherwise.
*/
function getAllSymbolFlags(symbol: Symbol): SymbolFlags {
let flags = symbol.flags;
let seenSymbols;
while (symbol.flags & SymbolFlags.Alias) {
const target = resolveAlias(symbol);
if (target === unknownSymbol) {
return SymbolFlags.All;
}

// Optimizations - try to avoid creating or adding to
// `seenSymbols` if possible
if (target === symbol || seenSymbols?.has(target)) {
break;
}
if (target.flags & SymbolFlags.Alias) {
if (seenSymbols) {
seenSymbols.add(target);
}
else {
seenSymbols = new Set([symbol, target]);
}
}
flags |= target.flags;
symbol = target;
}
return flags;
function getAllSymbolFlags(symbol: Symbol, excludeTypeOnlyMeanings?: boolean): SymbolFlags {
Copy link
Member

Choose a reason for hiding this comment

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

this name makes it sound like all uses of symbol.flags should become getSymbolFlags(symbol). Is that true? It doesn't look like it from the diff.

Copy link
Member Author

Choose a reason for hiding this comment

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

I tried to audit all uses of symbol.flags when I first wrote getAllSymbolFlags and did make many substitutions. The remaining uses, as best I could tell, intentionally only look at local meanings. The name getAllSymbolFlags no longer felt appropriate with arguments that explicitly filter them down from “all.” Perhaps a way to say getSymbolFlagsIncludingNonLocalOnes would make a better name.

const typeOnlyDeclaration = excludeTypeOnlyMeanings && getTypeOnlyAliasDeclaration(symbol);
const typeOnlyResolution = typeOnlyDeclaration && resolveAlias(typeOnlyDeclaration.symbol);
let flags = symbol.flags;
let seenSymbols;
while (symbol.flags & SymbolFlags.Alias) {
const target = resolveAlias(symbol);
if (target === typeOnlyResolution) {
Copy link
Member

Choose a reason for hiding this comment

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

This name is a little confusing since it's hard to notice that it's the exports of a module. But maybe the property that it's targets of resolution is more important.

break;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

The indentation on this function was messed up, so I recommend viewing it with whitespace changes hidden. These few lines are the core change that I mentioned in the PR description.

Copy link
Member

Choose a reason for hiding this comment

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

(dprint when?)

Copy link
Member Author

Choose a reason for hiding this comment

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

My thoughts exactly 👀

if (target === unknownSymbol) {
return SymbolFlags.All;
}

// Optimizations - try to avoid creating or adding to
// `seenSymbols` if possible
if (target === symbol || seenSymbols?.has(target)) {
break;
}
if (target.flags & SymbolFlags.Alias) {
if (seenSymbols) {
seenSymbols.add(target);
}
else {
seenSymbols = new Set([symbol, target]);
}
}
flags |= target.flags;
symbol = target;
}
return flags;
}

/**
Expand Down Expand Up @@ -4572,7 +4577,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const target = resolveAlias(symbol);
if (target) {
const markAlias = target === unknownSymbol ||
((getAllSymbolFlags(target) & SymbolFlags.Value) && !isConstEnumOrConstEnumOnlyModule(target) && !getTypeOnlyAliasDeclaration(symbol, SymbolFlags.Value));
((getAllSymbolFlags(target, /*excludeTypeOnlyMeanings*/ true) & SymbolFlags.Value) && !isConstEnumOrConstEnumOnlyModule(target));

if (markAlias) {
markAliasSymbolAsReferenced(symbol);
Expand Down Expand Up @@ -5567,7 +5572,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function symbolIsValue(symbol: Symbol, includeTypeOnlyMembers?: boolean): boolean {
return !!(
symbol.flags & SymbolFlags.Value ||
symbol.flags & SymbolFlags.Alias && getAllSymbolFlags(symbol) & SymbolFlags.Value && (includeTypeOnlyMembers || !getTypeOnlyAliasDeclaration(symbol)));
symbol.flags & SymbolFlags.Alias && getAllSymbolFlags(symbol, !includeTypeOnlyMembers) & SymbolFlags.Value);
}

function findConstructorDeclaration(node: ClassLikeDeclaration): ConstructorDeclaration | undefined {
Expand Down Expand Up @@ -27808,9 +27813,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (!canCollectSymbolAliasAccessabilityData) {
return;
}
if (isNonLocalAlias(symbol, /*excludes*/ SymbolFlags.Value) && !isInTypeQuery(location) && !getTypeOnlyAliasDeclaration(symbol, SymbolFlags.Value)) {
if (isNonLocalAlias(symbol, /*excludes*/ SymbolFlags.Value) && !isInTypeQuery(location)) {
const target = resolveAlias(symbol);
if (getAllSymbolFlags(target) & (SymbolFlags.Value | SymbolFlags.ExportValue)) {
if (getAllSymbolFlags(target, /*excludeTypeOnlyMeanings*/ true) & (SymbolFlags.Value | SymbolFlags.ExportValue)) {
// An alias resolving to a const enum cannot be elided if (1) 'isolatedModules' is enabled
// (because the const enum value will not be inlined), or if (2) the alias is an export
// of a const enum declaration that will be preserved.
Expand Down Expand Up @@ -46312,7 +46317,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
case SyntaxKind.ImportSpecifier:
case SyntaxKind.ExportSpecifier:
const symbol = getSymbolOfDeclaration(node as ImportClause | NamespaceImport | ImportSpecifier | ExportSpecifier);
return !!symbol && isAliasResolvedToValue(symbol) && !getTypeOnlyAliasDeclaration(symbol, SymbolFlags.Value);
return !!symbol && isAliasResolvedToValue(symbol, /*excludeTypeOnlyValues*/ true);
case SyntaxKind.ExportDeclaration:
const exportClause = (node as ExportDeclaration).exportClause;
return !!exportClause && (
Expand All @@ -46338,7 +46343,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return isValue && node.moduleReference && !nodeIsMissing(node.moduleReference);
}

function isAliasResolvedToValue(symbol: Symbol | undefined): boolean {
function isAliasResolvedToValue(symbol: Symbol | undefined, excludeTypeOnlyValues?: boolean): boolean {
if (!symbol) {
return false;
}
Expand All @@ -46348,7 +46353,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
// const enums and modules that contain only const enums are not considered values from the emit perspective
// unless 'preserveConstEnums' option is set to true
return !!((getAllSymbolFlags(target) ?? -1) & SymbolFlags.Value) &&
return !!((getAllSymbolFlags(target, excludeTypeOnlyValues) ?? -1) & SymbolFlags.Value) &&
(shouldPreserveConstEnums(compilerOptions) || !isConstEnumOrConstEnumOnlyModule(target));
}

Expand Down
12 changes: 12 additions & 0 deletions src/testRunner/unittests/services/transpile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,4 +620,16 @@ export * as alias from './file';`, {
testVerbatimModuleSyntax: true
}
);

transpilesCorrectly("Preserves exported const merged with type-only import", `
import fooValue from "./values";
import type {Foo} from "./types";

const Foo: Foo = fooValue as any as Foo;

export {Foo};
`, {
options: { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ESNext } },
testVerbatimModuleSyntax: true
});
});
43 changes: 43 additions & 0 deletions tests/baselines/reference/namespaceImportTypeQuery2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//// [tests/cases/conformance/externalModules/typeOnly/namespaceImportTypeQuery2.ts] ////

//// [z.ts]
interface A {}
export type { A };

//// [a.ts]
import { A } from './z';
const A = 0;
export { A };
export class B {};

//// [b.ts]
import * as types from './a';
let t: typeof types = {
A: undefined as any, // ok
B: undefined as any,
}


//// [z.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//// [a.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.B = exports.A = void 0;
var A = 0;
exports.A = A;
var B = /** @class */ (function () {
function B() {
}
return B;
}());
exports.B = B;
;
//// [b.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var t = {
A: undefined,
B: undefined,
};
39 changes: 39 additions & 0 deletions tests/baselines/reference/namespaceImportTypeQuery2.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//// [tests/cases/conformance/externalModules/typeOnly/namespaceImportTypeQuery2.ts] ////

=== /z.ts ===
interface A {}
>A : Symbol(A, Decl(z.ts, 0, 0))

export type { A };
>A : Symbol(A, Decl(z.ts, 1, 13))

=== /a.ts ===
import { A } from './z';
>A : Symbol(A, Decl(a.ts, 0, 8), Decl(a.ts, 1, 5))

const A = 0;
>A : Symbol(A, Decl(a.ts, 0, 8), Decl(a.ts, 1, 5))

export { A };
>A : Symbol(A, Decl(a.ts, 2, 8))

export class B {};
>B : Symbol(B, Decl(a.ts, 2, 13))

=== /b.ts ===
import * as types from './a';
>types : Symbol(types, Decl(b.ts, 0, 6))

let t: typeof types = {
>t : Symbol(t, Decl(b.ts, 1, 3))
>types : Symbol(types, Decl(b.ts, 0, 6))

A: undefined as any, // ok
>A : Symbol(A, Decl(b.ts, 1, 23))
>undefined : Symbol(undefined)

B: undefined as any,
>B : Symbol(B, Decl(b.ts, 2, 22))
>undefined : Symbol(undefined)
}

41 changes: 41 additions & 0 deletions tests/baselines/reference/namespaceImportTypeQuery2.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//// [tests/cases/conformance/externalModules/typeOnly/namespaceImportTypeQuery2.ts] ////

=== /z.ts ===
interface A {}
export type { A };
>A : A

=== /a.ts ===
import { A } from './z';
>A : 0

const A = 0;
>A : 0
>0 : 0

export { A };
>A : 0

export class B {};
>B : B

=== /b.ts ===
import * as types from './a';
>types : typeof types

let t: typeof types = {
>t : typeof types
>types : typeof types
>{ A: undefined as any, // ok B: undefined as any,} : { A: any; B: any; }

A: undefined as any, // ok
>A : any
>undefined as any : any
>undefined : undefined

B: undefined as any,
>B : any
>undefined as any : any
>undefined : undefined
}

18 changes: 18 additions & 0 deletions tests/baselines/reference/namespaceImportTypeQuery3.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/a.ts(1,24): error TS2307: Cannot find module './z' or its corresponding type declarations.


==== /a.ts (1 errors) ====
import type { A } from './z'; // unresolved
~~~~~
!!! error TS2307: Cannot find module './z' or its corresponding type declarations.
const A = 0;
export { A };
export class B {};

==== /b.ts (0 errors) ====
import * as types from './a';
let t: typeof types = {
A: undefined as any, // ok
B: undefined as any,
}

35 changes: 35 additions & 0 deletions tests/baselines/reference/namespaceImportTypeQuery3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//// [tests/cases/conformance/externalModules/typeOnly/namespaceImportTypeQuery3.ts] ////

//// [a.ts]
import type { A } from './z'; // unresolved
const A = 0;
export { A };
export class B {};

//// [b.ts]
import * as types from './a';
let t: typeof types = {
A: undefined as any, // ok
B: undefined as any,
}


//// [a.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.B = void 0;
var A = 0;
var B = /** @class */ (function () {
function B() {
}
return B;
}());
exports.B = B;
;
//// [b.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var t = {
A: undefined,
B: undefined,
};
32 changes: 32 additions & 0 deletions tests/baselines/reference/namespaceImportTypeQuery3.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//// [tests/cases/conformance/externalModules/typeOnly/namespaceImportTypeQuery3.ts] ////

=== /a.ts ===
import type { A } from './z'; // unresolved
>A : Symbol(A, Decl(a.ts, 0, 13), Decl(a.ts, 1, 5))

const A = 0;
>A : Symbol(A, Decl(a.ts, 0, 13), Decl(a.ts, 1, 5))

export { A };
>A : Symbol(A, Decl(a.ts, 2, 8))

export class B {};
>B : Symbol(B, Decl(a.ts, 2, 13))

=== /b.ts ===
import * as types from './a';
>types : Symbol(types, Decl(b.ts, 0, 6))

let t: typeof types = {
>t : Symbol(t, Decl(b.ts, 1, 3))
>types : Symbol(types, Decl(b.ts, 0, 6))

A: undefined as any, // ok
>A : Symbol(A, Decl(b.ts, 1, 23))
>undefined : Symbol(undefined)

B: undefined as any,
>B : Symbol(B, Decl(b.ts, 2, 22))
>undefined : Symbol(undefined)
}

36 changes: 36 additions & 0 deletions tests/baselines/reference/namespaceImportTypeQuery3.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//// [tests/cases/conformance/externalModules/typeOnly/namespaceImportTypeQuery3.ts] ////

=== /a.ts ===
import type { A } from './z'; // unresolved
>A : any

const A = 0;
>A : 0
>0 : 0

export { A };
>A : 0

export class B {};
>B : B

=== /b.ts ===
import * as types from './a';
>types : typeof types

let t: typeof types = {
>t : typeof types
>types : typeof types
>{ A: undefined as any, // ok B: undefined as any,} : { A: any; B: any; }

A: undefined as any, // ok
>A : any
>undefined as any : any
>undefined : undefined

B: undefined as any,
>B : any
>undefined as any : any
>undefined : undefined
}

Loading