Recover from type reuse errors by falling back to inferred type printing (#58720)

This commit is contained in:
Titian Cernicova-Dragomir
2024-05-31 01:51:38 +03:00
committed by GitHub
parent 59e6620260
commit 22eaccba2a
5 changed files with 423 additions and 6 deletions

View File

@@ -8472,17 +8472,30 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
cancellationToken.throwIfCancellationRequested();
}
let hadError = false;
const { finalizeBoundary, startRecoveryScope } = createRecoveryBoundary();
const transformed = visitNode(existing, visitExistingNodeTreeSymbols, isTypeNode);
if (hadError) {
if (!finalizeBoundary()) {
return undefined;
}
context.approximateLength += existing.end - existing.pos;
return transformed;
function visitExistingNodeTreeSymbols(node: Node): Node | undefined {
// If there was an error in a sibling node bail early, the result will be discarded anyway
if (hadError) return node;
const recover = startRecoveryScope();
const onExitNewScope = isNewScopeNode(node) ? onEnterNewScope(node) : undefined;
const result = visitExistingNodeTreeSymbolsWorker(node);
onExitNewScope?.();
// If there was an error, maybe we can recover by serializing the actual type of the node
if (hadError) {
if (isTypeNode(node) && !isTypePredicateNode(node)) {
recover();
return serializeExistingTypeNode(context, node);
}
return node;
}
// We want to clone the subtree, so when we mark it up with __pos and __end in quickfixes,
// we don't get odd behavior because of reused nodes. We also need to clone to _remove_
// the position information if the node comes from a different file than the one the node builder
@@ -8492,6 +8505,86 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return result === node ? setTextRange(context, factory.cloneNode(result), node) : result;
}
function createRecoveryBoundary() {
let unreportedErrors: (() => void)[];
const oldTracker = context.tracker;
const oldTrackedSymbols = context.trackedSymbols;
context.trackedSymbols = [];
const oldEncounteredError = context.encounteredError;
context.tracker = new SymbolTrackerImpl(context, {
...oldTracker.inner,
reportCyclicStructureError() {
markError(() => oldTracker.reportCyclicStructureError());
},
reportInaccessibleThisError() {
markError(() => oldTracker.reportInaccessibleThisError());
},
reportInaccessibleUniqueSymbolError() {
markError(() => oldTracker.reportInaccessibleUniqueSymbolError());
},
reportLikelyUnsafeImportRequiredError(specifier) {
markError(() => oldTracker.reportLikelyUnsafeImportRequiredError(specifier));
},
reportNonSerializableProperty(name) {
markError(() => oldTracker.reportNonSerializableProperty(name));
},
trackSymbol(sym, decl, meaning) {
const accessibility = isSymbolAccessible(sym, decl, meaning, /*shouldComputeAliasesToMakeVisible*/ false);
if (accessibility.accessibility !== SymbolAccessibility.Accessible) {
(context.trackedSymbols ??= []).push([sym, decl, meaning]);
return true;
}
return false;
},
moduleResolverHost: context.tracker.moduleResolverHost,
}, context.tracker.moduleResolverHost);
return {
startRecoveryScope,
finalizeBoundary,
};
function markError(unreportedError: () => void) {
hadError = true;
(unreportedErrors ??= []).push(unreportedError);
}
function startRecoveryScope() {
const initialTrackedSymbolsTop = context.trackedSymbols?.length ?? 0;
const unreportedErrorsTop = unreportedErrors?.length ?? 0;
return () => {
hadError = false;
// Reset the tracked symbols to before the error
if (context.trackedSymbols) {
context.trackedSymbols.length = initialTrackedSymbolsTop;
}
if (unreportedErrors) {
unreportedErrors.length = unreportedErrorsTop;
}
};
}
function finalizeBoundary() {
context.tracker = oldTracker;
const newTrackedSymbols = context.trackedSymbols;
context.trackedSymbols = oldTrackedSymbols;
context.encounteredError = oldEncounteredError;
unreportedErrors?.forEach(fn => fn());
if (hadError) {
return false;
}
newTrackedSymbols?.forEach(
([symbol, enclosingDeclaration, meaning]) =>
context.tracker.trackSymbol(
symbol,
enclosingDeclaration,
meaning,
),
);
return true;
}
}
function onEnterNewScope(node: IntroducesNewScopeNode | ConditionalTypeNode) {
return enterNewScope(context, node, getParametersInScope(node), getTypeParametersInScope(node));
}
@@ -8765,13 +8858,30 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function rewriteModuleSpecifier(parent: ImportTypeNode, lit: StringLiteral) {
if (context.bundled || context.enclosingFile !== getSourceFileOfNode(lit)) {
const targetFile = getExternalModuleFileFromDeclaration(parent);
if (targetFile) {
const newName = getSpecifierForModuleSymbol(targetFile.symbol, context);
if (newName !== lit.text) {
return setOriginalNode(factory.createStringLiteral(newName), lit);
let name = lit.text;
const nodeSymbol = getNodeLinks(node).resolvedSymbol;
const meaning = parent.isTypeOf ? SymbolFlags.Value : SymbolFlags.Type;
const parentSymbol = nodeSymbol
&& isSymbolAccessible(nodeSymbol, context.enclosingDeclaration, meaning, /*shouldComputeAliasesToMakeVisible*/ false).accessibility === SymbolAccessibility.Accessible
&& lookupSymbolChain(nodeSymbol, context, meaning, /*yieldModuleSymbol*/ true)[0];
if (parentSymbol && parentSymbol.flags & SymbolFlags.Module) {
name = getSpecifierForModuleSymbol(parentSymbol, context);
}
else {
const targetFile = getExternalModuleFileFromDeclaration(parent);
if (targetFile) {
name = getSpecifierForModuleSymbol(targetFile.symbol, context);
}
}
if (name.includes("/node_modules/")) {
context.encounteredError = true;
if (context.tracker.reportLikelyUnsafeImportRequiredError) {
context.tracker.reportLikelyUnsafeImportRequiredError(name);
}
}
if (name !== lit.text) {
return setOriginalNode(factory.createStringLiteral(name), lit);
}
}
return visitNode(lit, visitExistingNodeTreeSymbols, isStringLiteral)!;
}

View File

@@ -0,0 +1,61 @@
//// [tests/cases/compiler/declarationEmitUsingTypeAlias2.ts] ////
//// [inner.d.ts]
export declare type Other = { other: string };
export declare type SomeType = { arg: Other };
//// [other.d.ts]
export declare const shouldLookupName: unique symbol;
export declare const shouldReuseLocalName: unique symbol;
export declare const reuseDepName: unique symbol;
export declare const shouldBeElided: unique symbol;
export declare const isNotAccessibleShouldError: unique symbol;
//// [index.d.ts]
import { Other } from './inner'
import { shouldLookupName, reuseDepName, shouldReuseLocalName, shouldBeElided } from './other'
export declare const goodDeclaration: <T>() => () => {
/** Other Can't be named in index.ts, but the whole conditional type can be resolved */
shouldPrintResult: T extends Other? "O": "N",
/** Symbol shouldBeElided should not be present in index.d.ts, it might be since it's tracked before Other is seen and an error reported */
shouldPrintResult2: T extends typeof shouldBeElided? Other: "N",
/** Specifier should come from module, local path should not be reused */
shouldLookupName: typeof import('./other').shouldLookupName,
/** This is imported in index so local name should be reused */
shouldReuseLocalName: typeof shouldReuseLocalName
/** This is NOT imported in index so import should be created */
reuseDepName: typeof reuseDepName,
}
export { shouldLookupName, shouldReuseLocalName, reuseDepName, shouldBeElided };
//// [package.json]
{
"name": "some-dep",
"exports": {
".": "./dist/index.js"
}
}
//// [index.ts]
import { goodDeclaration, shouldReuseLocalName, shouldBeElided } from "some-dep";
export const bar = goodDeclaration<{}>;
//// [index.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.bar = void 0;
const some_dep_1 = require("some-dep");
exports.bar = (some_dep_1.goodDeclaration);
//// [index.d.ts]
import { shouldReuseLocalName } from "some-dep";
export declare const bar: () => () => {
shouldPrintResult: "N";
shouldPrintResult2: "N";
shouldLookupName: typeof import("some-dep").shouldLookupName;
shouldReuseLocalName: typeof shouldReuseLocalName;
reuseDepName: typeof import("some-dep").reuseDepName;
};

View File

@@ -0,0 +1,87 @@
//// [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/other.d.ts ===
export declare const shouldLookupName: unique symbol;
>shouldLookupName : Symbol(shouldLookupName, Decl(other.d.ts, 0, 20))
export declare const shouldReuseLocalName: unique symbol;
>shouldReuseLocalName : Symbol(shouldReuseLocalName, Decl(other.d.ts, 1, 20))
export declare const reuseDepName: unique symbol;
>reuseDepName : Symbol(reuseDepName, Decl(other.d.ts, 2, 20))
export declare const shouldBeElided: unique symbol;
>shouldBeElided : Symbol(shouldBeElided, Decl(other.d.ts, 3, 20))
export declare const isNotAccessibleShouldError: unique symbol;
>isNotAccessibleShouldError : Symbol(isNotAccessibleShouldError, Decl(other.d.ts, 4, 20))
=== node_modules/some-dep/dist/index.d.ts ===
import { Other } from './inner'
>Other : Symbol(Other, Decl(index.d.ts, 0, 8))
import { shouldLookupName, reuseDepName, shouldReuseLocalName, shouldBeElided } from './other'
>shouldLookupName : Symbol(shouldLookupName, Decl(index.d.ts, 1, 8))
>reuseDepName : Symbol(reuseDepName, Decl(index.d.ts, 1, 26))
>shouldReuseLocalName : Symbol(shouldReuseLocalName, Decl(index.d.ts, 1, 40))
>shouldBeElided : Symbol(shouldBeElided, Decl(index.d.ts, 1, 62))
export declare const goodDeclaration: <T>() => () => {
>goodDeclaration : Symbol(goodDeclaration, Decl(index.d.ts, 2, 20))
>T : Symbol(T, Decl(index.d.ts, 2, 39))
/** Other Can't be named in index.ts, but the whole conditional type can be resolved */
shouldPrintResult: T extends Other? "O": "N",
>shouldPrintResult : Symbol(shouldPrintResult, Decl(index.d.ts, 2, 54))
>T : Symbol(T, Decl(index.d.ts, 2, 39))
>Other : Symbol(Other, Decl(index.d.ts, 0, 8))
/** Symbol shouldBeElided should not be present in index.d.ts, it might be since it's tracked before Other is seen and an error reported */
shouldPrintResult2: T extends typeof shouldBeElided? Other: "N",
>shouldPrintResult2 : Symbol(shouldPrintResult2, Decl(index.d.ts, 4, 47))
>T : Symbol(T, Decl(index.d.ts, 2, 39))
>shouldBeElided : Symbol(shouldBeElided, Decl(index.d.ts, 1, 62))
>Other : Symbol(Other, Decl(index.d.ts, 0, 8))
/** Specifier should come from module, local path should not be reused */
shouldLookupName: typeof import('./other').shouldLookupName,
>shouldLookupName : Symbol(shouldLookupName, Decl(index.d.ts, 6, 66))
>shouldLookupName : Symbol(shouldLookupName, Decl(other.d.ts, 0, 20))
/** This is imported in index so local name should be reused */
shouldReuseLocalName: typeof shouldReuseLocalName
>shouldReuseLocalName : Symbol(shouldReuseLocalName, Decl(index.d.ts, 8, 62))
>shouldReuseLocalName : Symbol(shouldReuseLocalName, Decl(index.d.ts, 1, 40))
/** This is NOT imported in index so import should be created */
reuseDepName: typeof reuseDepName,
>reuseDepName : Symbol(reuseDepName, Decl(index.d.ts, 10, 51))
>reuseDepName : Symbol(reuseDepName, Decl(index.d.ts, 1, 26))
}
export { shouldLookupName, shouldReuseLocalName, reuseDepName, shouldBeElided };
>shouldLookupName : Symbol(shouldLookupName, Decl(index.d.ts, 14, 8))
>shouldReuseLocalName : Symbol(shouldReuseLocalName, Decl(index.d.ts, 14, 26))
>reuseDepName : Symbol(reuseDepName, Decl(index.d.ts, 14, 48))
>shouldBeElided : Symbol(shouldBeElided, Decl(index.d.ts, 14, 62))
=== src/index.ts ===
import { goodDeclaration, shouldReuseLocalName, shouldBeElided } from "some-dep";
>goodDeclaration : Symbol(goodDeclaration, Decl(index.ts, 0, 8))
>shouldReuseLocalName : Symbol(shouldReuseLocalName, Decl(index.ts, 0, 25))
>shouldBeElided : Symbol(shouldBeElided, Decl(index.ts, 0, 47))
export const bar = goodDeclaration<{}>;
>bar : Symbol(bar, Decl(index.ts, 1, 12))
>goodDeclaration : Symbol(goodDeclaration, Decl(index.ts, 0, 8))

View File

@@ -0,0 +1,115 @@
//// [tests/cases/compiler/declarationEmitUsingTypeAlias2.ts] ////
=== node_modules/some-dep/dist/inner.d.ts ===
export declare type Other = { other: string };
>Other : Other
> : ^^^^^
>other : string
> : ^^^^^^
export declare type SomeType = { arg: Other };
>SomeType : SomeType
> : ^^^^^^^^
>arg : Other
> : ^^^^^
=== node_modules/some-dep/dist/other.d.ts ===
export declare const shouldLookupName: unique symbol;
>shouldLookupName : unique symbol
> : ^^^^^^^^^^^^^
export declare const shouldReuseLocalName: unique symbol;
>shouldReuseLocalName : unique symbol
> : ^^^^^^^^^^^^^
export declare const reuseDepName: unique symbol;
>reuseDepName : unique symbol
> : ^^^^^^^^^^^^^
export declare const shouldBeElided: unique symbol;
>shouldBeElided : unique symbol
> : ^^^^^^^^^^^^^
export declare const isNotAccessibleShouldError: unique symbol;
>isNotAccessibleShouldError : unique symbol
> : ^^^^^^^^^^^^^
=== node_modules/some-dep/dist/index.d.ts ===
import { Other } from './inner'
>Other : any
> : ^^^
import { shouldLookupName, reuseDepName, shouldReuseLocalName, shouldBeElided } from './other'
>shouldLookupName : unique symbol
> : ^^^^^^^^^^^^^
>reuseDepName : unique symbol
> : ^^^^^^^^^^^^^
>shouldReuseLocalName : unique symbol
> : ^^^^^^^^^^^^^
>shouldBeElided : unique symbol
> : ^^^^^^^^^^^^^
export declare const goodDeclaration: <T>() => () => {
>goodDeclaration : <T>() => () => { shouldPrintResult: T extends Other ? "O" : "N"; shouldPrintResult2: T extends typeof shouldBeElided ? Other : "N"; shouldLookupName: typeof import("./other").shouldLookupName; shouldReuseLocalName: typeof shouldReuseLocalName; reuseDepName: typeof reuseDepName; }
> : ^ ^^^^^^^
/** Other Can't be named in index.ts, but the whole conditional type can be resolved */
shouldPrintResult: T extends Other? "O": "N",
>shouldPrintResult : T extends Other ? "O" : "N"
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^
/** Symbol shouldBeElided should not be present in index.d.ts, it might be since it's tracked before Other is seen and an error reported */
shouldPrintResult2: T extends typeof shouldBeElided? Other: "N",
>shouldPrintResult2 : T extends unique symbol ? Other : "N"
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>shouldBeElided : unique symbol
> : ^^^^^^^^^^^^^
/** Specifier should come from module, local path should not be reused */
shouldLookupName: typeof import('./other').shouldLookupName,
>shouldLookupName : unique symbol
> : ^^^^^^^^^^^^^
>shouldLookupName : error
/** This is imported in index so local name should be reused */
shouldReuseLocalName: typeof shouldReuseLocalName
>shouldReuseLocalName : unique symbol
> : ^^^^^^^^^^^^^
>shouldReuseLocalName : unique symbol
> : ^^^^^^^^^^^^^
/** This is NOT imported in index so import should be created */
reuseDepName: typeof reuseDepName,
>reuseDepName : unique symbol
> : ^^^^^^^^^^^^^
>reuseDepName : unique symbol
> : ^^^^^^^^^^^^^
}
export { shouldLookupName, shouldReuseLocalName, reuseDepName, shouldBeElided };
>shouldLookupName : unique symbol
> : ^^^^^^^^^^^^^
>shouldReuseLocalName : unique symbol
> : ^^^^^^^^^^^^^
>reuseDepName : unique symbol
> : ^^^^^^^^^^^^^
>shouldBeElided : unique symbol
> : ^^^^^^^^^^^^^
=== src/index.ts ===
import { goodDeclaration, shouldReuseLocalName, shouldBeElided } from "some-dep";
>goodDeclaration : <T>() => () => { shouldPrintResult: T extends import("node_modules/some-dep/dist/inner").Other ? "O" : "N"; shouldPrintResult2: T extends typeof shouldBeElided ? import("node_modules/some-dep/dist/inner").Other : "N"; shouldLookupName: typeof import("node_modules/some-dep/dist/other").shouldLookupName; shouldReuseLocalName: typeof shouldReuseLocalName; reuseDepName: typeof import("node_modules/some-dep/dist/other").reuseDepName; }
> : ^ ^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^
>shouldReuseLocalName : unique symbol
> : ^^^^^^^^^^^^^
>shouldBeElided : unique symbol
> : ^^^^^^^^^^^^^
export const bar = goodDeclaration<{}>;
>bar : () => () => { shouldPrintResult: {} extends import("node_modules/some-dep/dist/inner").Other ? "O" : "N"; shouldPrintResult2: {} extends typeof shouldBeElided ? import("node_modules/some-dep/dist/inner").Other : "N"; shouldLookupName: typeof import("node_modules/some-dep/dist/other").shouldLookupName; shouldReuseLocalName: typeof shouldReuseLocalName; reuseDepName: typeof import("node_modules/some-dep/dist/other").reuseDepName; }
> : ^^^^^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^ ^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^
>goodDeclaration<{}> : () => () => { shouldPrintResult: {} extends import("node_modules/some-dep/dist/inner").Other ? "O" : "N"; shouldPrintResult2: {} extends typeof shouldBeElided ? import("node_modules/some-dep/dist/inner").Other : "N"; shouldLookupName: typeof import("node_modules/some-dep/dist/other").shouldLookupName; shouldReuseLocalName: typeof shouldReuseLocalName; reuseDepName: typeof import("node_modules/some-dep/dist/other").reuseDepName; }
> : ^^^^^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^ ^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^
>goodDeclaration : <T>() => () => { shouldPrintResult: T extends import("node_modules/some-dep/dist/inner").Other ? "O" : "N"; shouldPrintResult2: T extends typeof shouldBeElided ? import("node_modules/some-dep/dist/inner").Other : "N"; shouldLookupName: typeof import("node_modules/some-dep/dist/other").shouldLookupName; shouldReuseLocalName: typeof shouldReuseLocalName; reuseDepName: typeof import("node_modules/some-dep/dist/other").reuseDepName; }
> : ^ ^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^

View File

@@ -0,0 +1,44 @@
// @strict: true
// @declaration: true
// @module: nodenext
// @filename: node_modules/some-dep/dist/inner.d.ts
export declare type Other = { other: string };
export declare type SomeType = { arg: Other };
// @filename: node_modules/some-dep/dist/other.d.ts
export declare const shouldLookupName: unique symbol;
export declare const shouldReuseLocalName: unique symbol;
export declare const reuseDepName: unique symbol;
export declare const shouldBeElided: unique symbol;
export declare const isNotAccessibleShouldError: unique symbol;
// @filename: node_modules/some-dep/dist/index.d.ts
import { Other } from './inner'
import { shouldLookupName, reuseDepName, shouldReuseLocalName, shouldBeElided } from './other'
export declare const goodDeclaration: <T>() => () => {
/** Other Can't be named in index.ts, but the whole conditional type can be resolved */
shouldPrintResult: T extends Other? "O": "N",
/** Symbol shouldBeElided should not be present in index.d.ts, it might be since it's tracked before Other is seen and an error reported */
shouldPrintResult2: T extends typeof shouldBeElided? Other: "N",
/** Specifier should come from module, local path should not be reused */
shouldLookupName: typeof import('./other').shouldLookupName,
/** This is imported in index so local name should be reused */
shouldReuseLocalName: typeof shouldReuseLocalName
/** This is NOT imported in index so import should be created */
reuseDepName: typeof reuseDepName,
}
export { shouldLookupName, shouldReuseLocalName, reuseDepName, shouldBeElided };
// @filename: node_modules/some-dep/package.json
{
"name": "some-dep",
"exports": {
".": "./dist/index.js"
}
}
// @filename: src/index.ts
import { goodDeclaration, shouldReuseLocalName, shouldBeElided } from "some-dep";
export const bar = goodDeclaration<{}>;