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
181 changes: 145 additions & 36 deletions tsc/internal/checker/checker.go

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions tsc/internal/checker/checker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,61 @@ export type E = D;`,
assert.Equal(t, defaultClause, defaultReference)
}

func TestGetLiteralTypeOriginCachesFlattenedUnion(t *testing.T) {
t.Parallel()

fs := bundled.WrapFS(vfstest.FromMap(map[string]string{
"/types.ts": `type Choice = "a" | "b" | string;
type Optional = Choice | undefined;
type Merged = Choice | "c" | string;
type Numeric = 1 | 2 | number;
type Big = 1n | 2n | bigint;
type Generic<Value extends string> = "fixed" | string | Value;
type Instantiated = Generic<"instantiated">;
type Plain = string;
type Ordinary = "a" | "b";`,
"/tsconfig.json": `{
"compilerOptions": { "strict": true, "target": "esnext" },
"files": ["types.ts"]
}`,
}, tspath.CaseInsensitive))
host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil)
parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, fs, nil)
assert.Equal(t, len(errors), 0, "Expected no errors in parsed command line")
p := compiler.NewProgram(compiler.ProgramOptions{Config: parsed, Host: host})
p.BindSourceFiles()
c, done := p.GetTypeChecker(t.Context())
defer done()
file := p.GetSourceFile("/types.ts")
for i, expected := range []string{
`string | "a" | "b"`,
`string | "a" | "b" | undefined`,
`string | "a" | "b" | "c"`,
`number | 1 | 2`,
`bigint | 1n | 2n`,
`string | "fixed" | Value`,
`string | "fixed" | "instantiated"`,
"",
"",
} {
typ := c.GetTypeAtLocation(file.Statements.Nodes[i].Name())
rawOrigin := c.GetTypeOrigin(typ)
origin := c.GetLiteralTypeOrigin(typ)
if expected == "" {
assert.Assert(t, origin == nil)
} else {
assert.Assert(t, origin != nil)
assert.Equal(t, c.TypeToString(origin), expected)
}
typeCount := c.TypeCount
for range 100 {
assert.Equal(t, c.GetLiteralTypeOrigin(typ), origin)
}
assert.Equal(t, c.TypeCount, typeCount, "Repeated origin queries must not create types")
assert.Equal(t, c.GetTypeOrigin(typ), rawOrigin)
}
}

func BenchmarkNewChecker(b *testing.B) {
fs := bundled.WrapFS(osvfs.FS())
rootPath := tspath.RootedDirectoryPathFromAbsolute(filepath.Join(repo.TestDataPath(), "fixtures/compiler"))
Expand Down
17 changes: 12 additions & 5 deletions tsc/internal/checker/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -1313,10 +1313,13 @@ func (c *Checker) getTypeAtFlowBranchLabel(f *FlowState, flow *ast.FlowNode, ant
// finalize all evolving array types.
func (c *Checker) getUnionOrEvolvingArrayType(f *FlowState, types []*Type, subtypeReduction UnionReduction) *Type {
if isEvolvingArrayTypeList(types) {
return c.getEvolvingArrayType(c.getUnionType(core.Map(types, c.getElementTypeOfEvolvingArrayType)))
return c.getEvolvingArrayType(c.getUnionTypeWithLiteralOrigins(core.Map(types, c.getElementTypeOfEvolvingArrayType), UnionReductionLiteral))
}
result := c.recombineUnknownType(c.getUnionTypeEx(core.SameMap(types, c.finalizeEvolvingArrayType), subtypeReduction, nil, nil))
if result != f.declaredType && result.flags&f.declaredType.flags&TypeFlagsUnion != 0 && slices.Equal(result.AsUnionType().types, f.declaredType.AsUnionType().types) {
// Literal-origin metadata can change type identity without narrowing the semantic target.
if result != f.declaredType &&
(c.getOriginTarget(result) == c.getOriginTarget(f.declaredType) ||
result.flags&f.declaredType.flags&TypeFlagsUnion != 0 && slices.Equal(result.AsUnionType().types, f.declaredType.AsUnionType().types)) {
return f.declaredType
}
return result
Expand Down Expand Up @@ -1557,10 +1560,14 @@ func (c *Checker) isEvolvingArrayOperationTarget(node *ast.Node) bool {
func (c *Checker) addEvolvingArrayElementType(evolvingArrayType *Type, node *ast.Node) *Type {
newElementType := c.getRegularTypeOfObjectLiteral(c.getBaseTypeOfLiteralType(c.getContextFreeTypeOfExpression(node)))
elementType := evolvingArrayType.AsEvolvingArrayType().elementType
if c.isTypeSubsetOf(newElementType, elementType) {
newOrigin := c.GetLiteralTypeOrigin(newElementType)
elementOrigin := c.GetLiteralTypeOrigin(elementType)
// Semantic coverage does not imply that the existing completion origins cover the new ones.
if c.isTypeSubsetOf(newElementType, elementType) &&
(newOrigin == nil || elementOrigin != nil && c.isTypeSubsetOf(newOrigin, elementOrigin)) {
return evolvingArrayType
}
return c.getEvolvingArrayType(c.getUnionType([]*Type{elementType, newElementType}))
return c.getEvolvingArrayType(c.getUnionTypeWithLiteralOrigins([]*Type{elementType, newElementType}, UnionReductionLiteral))
}

func (c *Checker) finalizeEvolvingArrayType(t *Type) *Type {
Expand All @@ -1582,7 +1589,7 @@ func (c *Checker) createFinalArrayType(elementType *Type) *Type {
case elementType.flags&TypeFlagsNever != 0:
return c.autoArrayType
case elementType.flags&TypeFlagsUnion != 0:
return c.createArrayType(c.getUnionTypeEx(elementType.Types(), UnionReductionSubtype, nil, nil))
return c.createArrayType(c.getUnionTypeWithLiteralOrigins([]*Type{elementType}, UnionReductionSubtype))
}
return c.createArrayType(elementType)
}
Expand Down
14 changes: 8 additions & 6 deletions tsc/internal/checker/relater.go
Original file line number Diff line number Diff line change
Expand Up @@ -2862,6 +2862,8 @@ func isIgnoredJsxProperty(source *Type, sourceProp *ast.Symbol) bool {
}

func (c *Checker) isTypeSubsetOf(source *Type, target *Type) bool {
source = c.getOriginTarget(source)
target = c.getOriginTarget(target)
return source == target || source.flags&TypeFlagsNever != 0 || target.flags&TypeFlagsUnion != 0 && c.isTypeSubsetOfUnion(source, target)
}

Expand Down Expand Up @@ -3032,7 +3034,7 @@ func (r *Relater) typeRelatedToSomeType(source *Type, target *Type, reportErrors
case source.flags&TypeFlagsBigIntLiteral != 0:
primitive = r.c.bigintType
}
if primitive != nil && containsType(targetTypes, primitive) || alternateForm != nil && containsType(targetTypes, alternateForm) {
if primitive != nil && isSomeTypePrimitive(targetTypes, primitive) || alternateForm != nil && containsType(targetTypes, alternateForm) {
return TernaryTrue
}
return TernaryFalse
Expand Down Expand Up @@ -4439,8 +4441,8 @@ func (r *Relater) tryElaborateArrayLikeErrors(source *Type, target *Type, report
}

func (r *Relater) tryElaborateErrorsForPrimitivesAndObjects(source *Type, target *Type) {
if (source == r.c.globalStringType && target == r.c.stringType) ||
(source == r.c.globalNumberType && target == r.c.numberType) ||
if (source == r.c.globalStringType && target.flags&TypeFlagsString != 0) ||
(source == r.c.globalNumberType && target.flags&TypeFlagsNumber != 0) ||
(source == r.c.globalBooleanType && target == r.c.booleanType) ||
(source == r.c.getGlobalESSymbolType() && target == r.c.esSymbolType) {
r.reportError(diagnostics.X_0_is_a_primitive_but_1_is_a_wrapper_object_Prefer_using_0_when_possible, r.c.TypeToString(target), r.c.TypeToString(source))
Expand Down Expand Up @@ -4613,7 +4615,7 @@ func (r *Relater) indexSignaturesRelatedTo(source *Type, target *Type, sourceIsP
return r.indexSignaturesIdenticalTo(source, target)
}
indexInfos := r.c.getIndexInfosOfType(target)
targetHasStringIndex := core.Some(indexInfos, func(info *IndexInfo) bool { return info.keyType == r.c.stringType })
targetHasStringIndex := core.Some(indexInfos, func(info *IndexInfo) bool { return info.keyType.flags&TypeFlagsString != 0 })
result := TernaryTrue
for _, targetInfo := range indexInfos {
var related Ternary
Expand Down Expand Up @@ -4681,7 +4683,7 @@ func (r *Relater) membersRelatedToIndexInfo(source *Type, targetInfo *IndexInfo,
if r.c.isApplicableIndexType(r.c.getLiteralTypeFromProperty(prop, TypeFlagsStringOrNumberLiteralOrUnique, false), keyType) {
propType := r.c.getNonMissingTypeOfSymbol(prop)
var t *Type
if r.c.exactOptionalPropertyTypes || propType.flags&TypeFlagsUndefined != 0 || keyType == r.c.numberType || prop.Flags()&ast.SymbolFlagsOptional == 0 {
if r.c.exactOptionalPropertyTypes || propType.flags&TypeFlagsUndefined != 0 || keyType.flags&TypeFlagsNumber != 0 || prop.Flags()&ast.SymbolFlagsOptional == 0 {
t = propType
} else {
t = r.c.getTypeWithFacts(propType, TypeFactsNEUndefined)
Expand Down Expand Up @@ -4711,7 +4713,7 @@ func (r *Relater) membersRelatedToIndexInfo(source *Type, targetInfo *IndexInfo,
func (r *Relater) indexInfoRelatedTo(sourceInfo *IndexInfo, targetInfo *IndexInfo, reportErrors bool, intersectionState IntersectionState) Ternary {
related := r.isRelatedToEx(sourceInfo.valueType, targetInfo.valueType, RecursionFlagsBoth, reportErrors, nil /*headMessage*/, intersectionState)
if related == TernaryFalse && reportErrors {
if sourceInfo.keyType == targetInfo.keyType {
if sourceInfo.keyType == targetInfo.keyType || sourceInfo.keyType.flags&targetInfo.keyType.flags&(TypeFlagsString|TypeFlagsNumber) != 0 {
r.reportError(diagnostics.X_0_index_signatures_are_incompatible, r.c.TypeToString(sourceInfo.keyType))
} else {
r.reportError(diagnostics.X_0_and_1_index_signatures_are_incompatible, r.c.TypeToString(sourceInfo.keyType), r.c.TypeToString(targetInfo.keyType))
Expand Down
11 changes: 10 additions & 1 deletion tsc/internal/checker/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,15 @@ func (t *Type) ObjectFlags() ObjectFlags {
return t.objectFlags
}

type ReducedTypeLinks struct {
// The reduced type used for checking and ordinary printing, without literal-origin metadata.
target *Type
// The union before literal reduction, whose members may themselves have literal origins.
origin *Type
// The flattened, deduplicated origin union for display, or nil until GetLiteralTypeOrigin computes it.
literalOrigin *Type
}

// Casts for concrete struct types

func (t *Type) AsIntrinsicType() *IntrinsicType { return castType[IntrinsicType](t, typeKindIntrinsic) }
Expand Down Expand Up @@ -1379,7 +1388,7 @@ type UnionType struct {
UnionOrIntersectionType
resolvedReducedType *Type
regularType *Type
origin *Type // Denormalized union, intersection, or index type in which union originates
origin *Type
keyPropertyName string // Property with unique unit type that exists in every object/intersection in union type
constituentMap map[*Type]*Type // Constituents keyed by unit type discriminants
}
Expand Down
47 changes: 47 additions & 0 deletions tsc/internal/fourslash/tests/evolvingArrayLiteralOrigins_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package fourslash_test

import (
"fmt"
"testing"

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

func TestEvolvingArrayLiteralOrigins(t *testing.T) {
t.Parallel()
for _, test := range []struct {
name string
pushes string
expected string
}{
{"forward", "values.push(a); values.push(b);", `string | "a" | "b"`},
{"reverse", "values.push(b); values.push(a);", `string | "a" | "b"`},
{"repeated", "values.push(a); values.push(b); values.push(a); values.push(b);", `string | "a" | "b"`},
{"plain first", "values.push(plain); values.push(a); values.push(b);", `string | "a" | "b"`},
{"plain last", "values.push(a); values.push(b); values.push(plain);", `string | "a" | "b"`},
{"mixed", "values.push(a); values.push(b); values.push(n); values.push(m);", `string | number | "a" | "b" | 1 | 2`},
{"loop", "values.push(a); for (let i = 0; i < 2; i++) { values.push(b); values.push(a); }", `string | "a" | "b"`},
{"branches", "if (condition) { values.push(a); } else { values.push(b); }", `string | "a" | "b"`},
} {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
content := fmt.Sprintf(`// @strict: true
declare const a: "a" | string;
declare const b: "b" | string;
declare const n: 1 | number;
declare const m: 2 | number;
declare const plain: string;
declare const condition: boolean;
const values = [];
%s
const result/*result*/ = values[0];
`, test.pushes)
f, done := fourslash.NewFourslash(t, nil, content)
defer done()
f.VerifyNoErrors(t)
f.VerifyQuickInfoAt(t, "result", "const result: "+test.expected, "")
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package fourslash_test

import (
"testing"

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

func TestNumericLiteralCompletionsFromReducedUnion(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// @strict: true
// @target: esnext
declare let numeric: 1 | 2 | number;
numeric = /*numeric*/;
declare let big: 1n | 2n | bigint;
big = /*big*/;
declare let legacyNumeric: 1 | 2 | (number & {});
legacyNumeric = /*legacyNumeric*/;
declare let legacyBig: 1n | 2n | (bigint & {});
legacyBig = /*legacyBig*/;
type Numeric = 1 | number;
declare let merged: Numeric | 2 | number | undefined;
merged = /*merged*/;
declare let plainNumber: number;
plainNumber = /*plainNumber*/;
declare let plainBigint: bigint;
plainBigint = /*plainBigint*/;`
f, done := fourslash.NewFourslash(t, nil, content)
defer done()
for _, test := range []struct {
markers []string
includes []fourslash.CompletionsExpectedItem
excludes []string
}{
{[]string{"numeric", "legacyNumeric", "merged"}, []fourslash.CompletionsExpectedItem{"1", "2"}, []string{"1n", "2n"}},
{[]string{"big", "legacyBig"}, []fourslash.CompletionsExpectedItem{"1n", "2n"}, []string{"1", "2"}},
{[]string{"plainNumber", "plainBigint"}, nil, []string{"1", "2", "1n", "2n"}},
} {
f.VerifyCompletions(t, test.markers, &fourslash.CompletionsExpectedList{
IsIncomplete: false,
ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{
CommitCharacters: &DefaultCommitCharacters,
EditRange: Ignored,
},
Items: &fourslash.CompletionsExpectedItems{
Includes: test.includes,
Excludes: test.excludes,
},
})
}
}
45 changes: 45 additions & 0 deletions tsc/internal/fourslash/tests/quickInfoNumericLiteralOrigin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package fourslash_test

import (
"testing"

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

func TestQuickInfoNumericLiteralOrigin(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// @strict: true
// @target: esnext
declare let numeric/*numeric*/: 1 | 2 | number;
declare let big/*big*/: 1n | 2n | bigint;
declare let legacyNumeric/*legacyNumeric*/: 1 | 2 | (number & {});
declare let legacyBig/*legacyBig*/: 1n | 2n | (bigint & {});
type Numeric = 1 | number;
type Big = 1n | bigint;
declare let merged/*merged*/: Numeric | 2 | number;
declare let mergedBig/*mergedBig*/: Big | 2n | bigint;
declare let plainNumber/*plainNumber*/: number;
declare let plainBigint/*plainBigint*/: bigint;
declare let unrelated/*unrelated*/: number | 1n;
if (numeric === 1) numeric/*narrowed*/;
if (big === 1n) big/*narrowedBig*/;
numeric/*joined*/;
big/*joinedBig*/;`
f, done := fourslash.NewFourslash(t, nil, content)
defer done()
f.VerifyQuickInfoAt(t, "numeric", "let numeric: number | 1 | 2", "")
f.VerifyQuickInfoAt(t, "big", "let big: bigint | 1n | 2n", "")
f.VerifyQuickInfoAt(t, "legacyNumeric", "let legacyNumeric: number | 1 | 2", "")
f.VerifyQuickInfoAt(t, "legacyBig", "let legacyBig: bigint | 1n | 2n", "")
f.VerifyQuickInfoAt(t, "merged", "let merged: number | 1 | 2", "")
f.VerifyQuickInfoAt(t, "mergedBig", "let mergedBig: bigint | 1n | 2n", "")
f.VerifyQuickInfoAt(t, "plainNumber", "let plainNumber: number", "")
f.VerifyQuickInfoAt(t, "plainBigint", "let plainBigint: bigint", "")
f.VerifyQuickInfoAt(t, "unrelated", "let unrelated: number | 1n", "")
f.VerifyQuickInfoAt(t, "narrowed", "let numeric: 1", "")
f.VerifyQuickInfoAt(t, "narrowedBig", "let big: 1n", "")
f.VerifyQuickInfoAt(t, "joined", "let numeric: number | 1 | 2", "")
f.VerifyQuickInfoAt(t, "joinedBig", "let big: bigint | 1n | 2n", "")
}
Loading
Loading