Skip to content
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
308 changes: 302 additions & 6 deletions tsc/internal/checker/checker.go

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion tsc/internal/checker/inference.go
Original file line number Diff line number Diff line change
Expand Up @@ -1380,7 +1380,13 @@ func (c *Checker) getInferredType(n *InferenceContext, index int) *Type {
instantiatedConstraint := c.instantiateType(constraint, n.nonFixingMapper)
if inferredType != nil {
constraintWithThis := c.getTypeWithThisArgument(instantiatedConstraint, inferredType, false)
if n.compareTypes(inferredType, constraintWithThis, false) == TernaryFalse {
// Constraint verification must not force a member whose type is still being computed, since an object
// literal argument routinely names the declaration being resolved. A comparison that passed over such a
// member answered for less than the whole type, so it may keep the candidate but never reject it.
comparison, answeredInFull := c.compareProvisionally(func() Ternary {
return n.compareTypes(inferredType, constraintWithThis, false)
})
if comparison == TernaryFalse && answeredInFull {
var filteredByConstraint *Type
if inference.priority == InferencePriorityReturnType {
// If we have a pure return type inference, we may succeed by removing constituents of the inferred type
Expand Down
27 changes: 22 additions & 5 deletions tsc/internal/checker/relater.go
Original file line number Diff line number Diff line change
Expand Up @@ -3137,6 +3137,7 @@ func (r *Relater) recursiveTypeRelatedTo(source *Type, target *Type, reportError
return TernaryMaybe
}
maybeStart := len(r.maybeKeys)
skipsBefore := r.c.unresolvableMembers
r.maybeKeys = append(r.maybeKeys, id)
r.maybeKeysSet.Add(id)
saveExpandingFlags := r.expandingFlags
Expand Down Expand Up @@ -3177,9 +3178,9 @@ func (r *Relater) recursiveTypeRelatedTo(source *Type, target *Type, reportError
r.expandingFlags = saveExpandingFlags
if result != TernaryFalse {
if result == TernaryTrue || (len(r.sourceStack) == 0 && len(r.targetStack) == 0) {
if result == TernaryTrue || result == TernaryMaybe {
// If result is definitely true, record all maybe keys as having succeeded. Also, record Ternary.Maybe
// results as having succeeded once we reach depth 0, but never record Ternary.Unknown results.
if (result == TernaryTrue || result == TernaryMaybe) && r.c.unresolvableMembers == skipsBefore {
// This comparison answered for less than the whole type and the relation cache is global, so the next
// question must ask again rather than read it.
r.resetMaybeStack(maybeStart, propagatingVarianceFlags, true)
} else {
r.resetMaybeStack(maybeStart, propagatingVarianceFlags, false)
Expand Down Expand Up @@ -4263,6 +4264,14 @@ func (r *Relater) propertiesRelatedTo(source *Type, target *Type, reportErrors b
}
requireOptionalProperties := (r.relation == r.c.subtypeRelation || r.relation == r.c.strictSubtypeRelation) && !isObjectLiteralType(source) && !r.c.isEmptyArrayLiteralType(source) && !isTupleType(source)
unmatchedProperty := r.c.getUnmatchedProperty(source, target, requireOptionalProperties, false /*matchDiscriminantProperties*/)
// A miss while the source's table is mid-assembly means "not yet" rather than "absent", but only inside
// a provisional region, and only for a name some base declares. Suppressing it during an ordinary check
// makes an absent property look present, which can send a conditional type down the wrong branch.
if unmatchedProperty != nil && r.c.provisionalDepth != 0 && source.objectFlags&ObjectFlagsUnresolvedMembers != 0 &&
r.c.mayInheritProperty(source, unmatchedProperty.Name, nil) {
unmatchedProperty = nil
r.c.unresolvableMembers++
}
if unmatchedProperty != nil {
if reportErrors && r.c.shouldReportUnmatchedPropertyError(source, target) {
r.reportUnmatchedProperty(source, target, unmatchedProperty, requireOptionalProperties)
Expand Down Expand Up @@ -4370,7 +4379,11 @@ func (r *Relater) isPropertySymbolTypeRelated(sourceProp *ast.Symbol, targetProp
if effectiveTarget.flags&core.IfElse(r.relation == r.c.strictSubtypeRelation, TypeFlagsAny, TypeFlagsAnyOrUnknown) != 0 {
return TernaryTrue
}
effectiveSource := getTypeOfSourceProperty(sourceProp)
effectiveSource, resolved := r.c.tryGetTypeOfMember(getTypeOfSourceProperty, sourceProp)
if !resolved {
r.c.postponeMemberCheck(sourceProp, effectiveTarget, r.relation)
return TernaryTrue
}
return r.isRelatedToEx(effectiveSource, effectiveTarget, RecursionFlagsBoth, reportErrors, nil /*headMessage*/, intersectionState)
}

Expand Down Expand Up @@ -4678,7 +4691,11 @@ func (r *Relater) membersRelatedToIndexInfo(source *Type, targetInfo *IndexInfo,
continue
}
if r.c.isApplicableIndexType(r.c.getLiteralTypeFromProperty(prop, TypeFlagsStringOrNumberLiteralOrUnique, false), keyType) {
propType := r.c.getNonMissingTypeOfSymbol(prop)
propType, resolved := r.c.tryGetTypeOfMember(r.c.getNonMissingTypeOfSymbol, prop)
if !resolved {
r.c.postponeMemberCheck(prop, targetInfo.valueType, r.relation)
continue
}
var t *Type
if r.c.exactOptionalPropertyTypes || propType.flags&TypeFlagsUndefined != 0 || keyType == r.c.numberType || prop.Flags&ast.SymbolFlagsOptional == 0 {
t = propType
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/TypeScript/tsc/internal/fourslash"
"github.com/microsoft/TypeScript/tsc/internal/testutil"
)

// Regression test for https://github.com/microsoft/TypeScript/issues/62181
//
// A getter naming the declaration being resolved used to report as `any`, and asking for that hover
// before pulling diagnostics changed how many errors came back, because the hover fixed the getter at
// `any` for everything that followed. The hover has to report the recursive type it actually has, and
// the diagnostics have to be the same whether or not anything asked for it first.
func TestHoverThenDiagnosticsRecursiveGetter(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, recursiveGetterContent)
defer done()
f.VerifyQuickInfoAt(t, "1", `(accessor) parent: ZodOptional<ZodObject<{
name: ZodString;
readonly parent: ZodOptional<ZodObject<...>>;
}>>`, "")
f.VerifyNoErrors(t)
}

// The same file with nothing asked of it first, so the two can be compared.
func TestDiagnosticsOnlyRecursiveGetter(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, recursiveGetterContent)
defer done()
f.VerifyNoErrors(t)
}

const recursiveGetterContent = `// @Filename: /tsconfig.json
{ "compilerOptions": { "strict": true, "target": "esnext" } }
// @Filename: /file.ts
interface ZodType<T> {
optional: "true" | "false";
output: T;
}

interface ZodString extends ZodType<string> {
optional: "false";
}

type ZodShape = Record<string, any>;
type Prettify<T> = { [K in keyof T]: T[K] } & {};
type InferObjectType<Shape extends ZodShape> = Prettify<
{
[k in keyof Shape as Shape[k] extends { optional: "true" }
? k
: never]?: Shape[k]["output"];
} & {
[k in keyof Shape as Shape[k] extends { optional: "true" }
? never
: k]: Shape[k]["output"];
}
>;
interface ZodObject<T extends ZodShape> extends ZodType<InferObjectType<T>> {
optional: "false";
}

interface ZodOptional<T extends ZodType<any>>
extends ZodType<T["output"] | undefined> {
optional: "true";
}

declare function object<T extends ZodShape>(shape: T): ZodObject<T>;
declare function string(): ZodString;
declare function optional<T extends ZodType<any>>(schema: T): ZodOptional<T>;

const Category = object({
name: string(),
get parent/*1*/() {
return optional(Category);
},
});

export const output = Category.output;`
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/TypeScript/tsc/internal/fourslash"
"github.com/microsoft/TypeScript/tsc/internal/testutil"
)

// A constraint that could not be answered while the getter was still being resolved is postponed and
// made again when a file's deferred work runs. In a batch compile every file is checked, so the queue
// always drains. The language service checks only what someone asks about, so the risk is that the
// answer depends on which file that was -- which is the complaint in
// https://github.com/microsoft/TypeScript/issues/62181 one layer down.
//
// `bad` holds an array of schemas, which is not a Schema, so it violates the constraint on S. The
// error belongs to schema.ts. These three pin that it is reported there in every order, including
// when the file carrying it is never the one asked about first.

func TestPostponedConstraintDiagnosticsSchemaFirst(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, postponedConstraintContent)
defer done()
f.GoToFile(t, "/schema.ts")
f.GoToFile(t, "/consumer.ts")
f.VerifyBaselineNonSuggestionDiagnostics(t)
}

func TestPostponedConstraintDiagnosticsConsumerFirst(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, postponedConstraintContent)
defer done()
f.GoToFile(t, "/consumer.ts")
f.GoToFile(t, "/schema.ts")
f.VerifyBaselineNonSuggestionDiagnostics(t)
}

// The file the error belongs to is never opened.
func TestPostponedConstraintDiagnosticsConsumerOnly(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, postponedConstraintContent)
defer done()
f.GoToFile(t, "/consumer.ts")
f.VerifyBaselineNonSuggestionDiagnostics(t)
}

const postponedConstraintContent = `// @Filename: /tsconfig.json
{ "compilerOptions": { "strict": true, "target": "esnext", "module": "nodenext", "moduleResolution": "nodenext" } }
// @Filename: /schema.ts
export interface Schema<O> {
readonly out: O;
}
export type Shape = Record<string, Schema<any>>;
export declare function object<S extends Shape>(shape: S): Schema<{ [K in keyof S]: S[K]["out"] }> & { shape: S };

export const tree = object({
get bad() {
return [tree];
},
});
// @Filename: /consumer.ts
import { tree } from "./schema.js";

export const out = tree.out;`
Loading