diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index ef6d7fb7ef10b..f46b9bf16d3c0 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -125,6 +125,7 @@ import type { TemplateLiteralType, ThisTypePredicate, TupleType, + TupleTypeReference, Type, TypeParameter, TypePredicate, @@ -187,6 +188,7 @@ export type { TimingAccumulators, TimingInfo, TupleType, + TupleTypeReference, Type, TypeAcquisition, TypeParameter, @@ -1821,11 +1823,11 @@ export class Checker { } async isTupleType(type: Type): Promise { - return this.client.apiRequest("isTupleType", { - snapshot: this.snapshotId, - project: this.project.id, - type: type.id, - }); + return type.isTupleType(); + } + + async isTupleTypeTarget(type: Type): Promise { + return type.isTupleTypeTarget(); } /** @@ -2334,6 +2336,7 @@ class TypeObject implements Type { readonly freshType!: number; readonly regularType!: number; readonly target!: number; + private readonly tupleType: boolean; readonly typeParameters!: readonly number[]; readonly outerTypeParameters!: readonly number[]; readonly localTypeParameters!: readonly number[]; @@ -2389,13 +2392,16 @@ class TypeObject implements Type { if (data.freshType !== undefined) this.freshType = data.freshType; if (data.regularType !== undefined) this.regularType = data.regularType; if (data.target !== undefined) this.target = data.target; + this.tupleType = data.isTupleType ?? false; this.typeParameters = data.typeParameters ?? []; this.outerTypeParameters = data.outerTypeParameters ?? []; this.localTypeParameters = data.localTypeParameters ?? []; this.aliasTypeArguments = data.aliasTypeArguments ?? []; if (data.aliasSymbol !== undefined) this.aliasSymbol = data.aliasSymbol; - if (data.elementFlags !== undefined) this.elementFlags = data.elementFlags; - if (data.fixedLength !== undefined) this.fixedLength = data.fixedLength; + if (data.fixedLength !== undefined) { + this.elementFlags = data.elementFlags ?? []; + this.fixedLength = data.fixedLength; + } if (data.readonly !== undefined) this.readonly = data.readonly; if (data.texts !== undefined) this.texts = data.texts; if (data.objectType !== undefined) this.objectType = data.objectType; @@ -2660,8 +2666,12 @@ class TypeObject implements Type { return isTypeReference(this); } - isTupleType(): this is TupleType { - return isTupleType(this); + isTupleType(): this is TupleTypeReference { + return this.tupleType; + } + + isTupleTypeTarget(): this is TupleType { + return this.fixedLength !== undefined; } isIndexType(): this is IndexType { @@ -2747,8 +2757,12 @@ export function isTypeReference(type: Type): type is TypeReference { return isObjectType(type) && (type.objectFlags & ObjectFlags.Reference) !== 0; } -export function isTupleType(type: Type): type is TupleType { - return isObjectType(type) && (type.objectFlags & ObjectFlags.Tuple) !== 0; +export function isTupleType(type: Type): type is TupleTypeReference { + return type.isTupleType(); +} + +export function isTupleTypeTarget(type: Type): type is TupleType { + return type.isTupleTypeTarget(); } export function isIndexType(type: Type): type is IndexType { diff --git a/packages/typescript/src/api/async/types.ts b/packages/typescript/src/api/async/types.ts index 79c392aed6b63..316484c68ddc8 100644 --- a/packages/typescript/src/api/async/types.ts +++ b/packages/typescript/src/api/async/types.ts @@ -107,8 +107,10 @@ export interface Type { isBooleanLiteralType(): this is BooleanLiteralType; /** Whether this type is a type reference */ isTypeReference(): this is TypeReference; - /** Whether this type is a tuple type */ - isTupleType(): this is TupleType; + /** Whether this type is a tuple type reference */ + isTupleType(): this is TupleTypeReference; + /** Whether this type owns tuple metadata */ + isTupleTypeTarget(): this is TupleType; /** Whether this type is an index type (`keyof T`) */ isIndexType(): this is IndexType; /** Whether this type is an indexed access type (`T[K]`) */ @@ -177,6 +179,12 @@ export interface TypeReference extends ObjectType { getTarget(): Promise; } +/** References to tuple types */ +export interface TupleTypeReference extends TypeReference { + /** Get the tuple type that describes this reference's shape */ + getTarget(): Promise; +} + /** Interface types — classes and interfaces (ObjectFlags.ClassOrInterface) */ export interface InterfaceType extends TypeReference { /** Get all type parameters (outer + local, excluding thisType) */ @@ -187,8 +195,10 @@ export interface InterfaceType extends TypeReference { getLocalTypeParameters(): Promise; } -/** Tuple types (ObjectFlags.Tuple) */ +/** Tuple type targets (ObjectFlags.Tuple) */ export interface TupleType extends InterfaceType { + /** Get this tuple target */ + getTarget(): Promise; /** Per-element flags (Required, Optional, Rest, Variadic) */ readonly elementFlags: readonly ElementFlags[]; /** Number of initial required or optional elements */ diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 6528b59b0f3f9..9faeb652e4fd0 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -115,7 +115,6 @@ export interface APIMethodInfo { getJsDocTags: APIMethod; getDocumentationComment: APIMethod; isArrayType: APIMethod; - isTupleType: APIMethod; isReadonlySymbol: APIMethod; getReferencesToSymbolInFile: APIMethod; getReferencedSymbolsForNode: APIMethod; @@ -365,6 +364,7 @@ export interface TypeResponse { id: number; flags: number; objectFlags?: number; + isTupleType?: boolean; /** * Value is literal type data. BigInt literals are encoded as signed decimal * strings because JSON cannot represent bigint; absent values are null. @@ -1002,7 +1002,6 @@ export interface BatchRequest { | "isArrayType" | "isContextSensitive" | "isReadonlySymbol" - | "isTupleType" | "isTypeAssignableTo" | "parseCommandLine" | "parseConfigFile" @@ -1149,7 +1148,6 @@ export interface BatchResponse { | "isArrayType" | "isContextSensitive" | "isReadonlySymbol" - | "isTupleType" | "isTypeAssignableTo" | "parseCommandLine" | "parseConfigFile" diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index b92a3fa26fb72..d6c538d1895c4 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -142,6 +142,7 @@ import type { TemplateLiteralType, ThisTypePredicate, TupleType, + TupleTypeReference, Type, TypeParameter, TypePredicate, @@ -204,6 +205,7 @@ export type { TimingAccumulators, TimingInfo, TupleType, + TupleTypeReference, Type, TypeAcquisition, TypeParameter, @@ -3901,18 +3903,27 @@ export class Checker { owner, "isTupleType", function (type: Type): boolean { - return owner.client.apiRequest("isTupleType", { - snapshot: owner.snapshotId, - project: owner.project.id, - type: type.id, - }); + return type.isTupleType(); }, function* (type: Type): Generator { - return yield* apiRequest("isTupleType", { - snapshot: owner.snapshotId, - project: owner.project.id, - type: type.id, - }); + return type.isTupleType(); + }, + ); + } + + get isTupleTypeTarget(): { + (type: Type): boolean; + gen(type: Type): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "isTupleTypeTarget", + function (type: Type): boolean { + return type.isTupleTypeTarget(); + }, + function* (type: Type): Generator { + return type.isTupleTypeTarget(); }, ); } @@ -5088,6 +5099,7 @@ class TypeObject implements Type { readonly freshType!: number; readonly regularType!: number; readonly target!: number; + private readonly tupleType: boolean; readonly typeParameters!: readonly number[]; readonly outerTypeParameters!: readonly number[]; readonly localTypeParameters!: readonly number[]; @@ -5143,13 +5155,16 @@ class TypeObject implements Type { if (data.freshType !== undefined) this.freshType = data.freshType; if (data.regularType !== undefined) this.regularType = data.regularType; if (data.target !== undefined) this.target = data.target; + this.tupleType = data.isTupleType ?? false; this.typeParameters = data.typeParameters ?? []; this.outerTypeParameters = data.outerTypeParameters ?? []; this.localTypeParameters = data.localTypeParameters ?? []; this.aliasTypeArguments = data.aliasTypeArguments ?? []; if (data.aliasSymbol !== undefined) this.aliasSymbol = data.aliasSymbol; - if (data.elementFlags !== undefined) this.elementFlags = data.elementFlags; - if (data.fixedLength !== undefined) this.fixedLength = data.fixedLength; + if (data.fixedLength !== undefined) { + this.elementFlags = data.elementFlags ?? []; + this.fixedLength = data.fixedLength; + } if (data.readonly !== undefined) this.readonly = data.readonly; if (data.texts !== undefined) this.texts = data.texts; if (data.objectType !== undefined) this.objectType = data.objectType; @@ -5897,8 +5912,12 @@ class TypeObject implements Type { return isTypeReference(this); } - isTupleType(): this is TupleType { - return isTupleType(this); + isTupleType(): this is TupleTypeReference { + return this.tupleType; + } + + isTupleTypeTarget(): this is TupleType { + return this.fixedLength !== undefined; } isIndexType(): this is IndexType { @@ -5984,8 +6003,12 @@ export function isTypeReference(type: Type): type is TypeReference { return isObjectType(type) && (type.objectFlags & ObjectFlags.Reference) !== 0; } -export function isTupleType(type: Type): type is TupleType { - return isObjectType(type) && (type.objectFlags & ObjectFlags.Tuple) !== 0; +export function isTupleType(type: Type): type is TupleTypeReference { + return type.isTupleType(); +} + +export function isTupleTypeTarget(type: Type): type is TupleType { + return type.isTupleTypeTarget(); } export function isIndexType(type: Type): type is IndexType { diff --git a/packages/typescript/src/api/sync/types.ts b/packages/typescript/src/api/sync/types.ts index 43a650ef67621..43a7b1720db22 100644 --- a/packages/typescript/src/api/sync/types.ts +++ b/packages/typescript/src/api/sync/types.ts @@ -165,8 +165,10 @@ export interface Type { isBooleanLiteralType(): this is BooleanLiteralType; /** Whether this type is a type reference */ isTypeReference(): this is TypeReference; - /** Whether this type is a tuple type */ - isTupleType(): this is TupleType; + /** Whether this type is a tuple type reference */ + isTupleType(): this is TupleTypeReference; + /** Whether this type owns tuple metadata */ + isTupleTypeTarget(): this is TupleType; /** Whether this type is an index type (`keyof T`) */ isIndexType(): this is IndexType; /** Whether this type is an indexed access type (`T[K]`) */ @@ -244,6 +246,15 @@ export interface TypeReference extends ObjectType { }; } +/** References to tuple types */ +export interface TupleTypeReference extends TypeReference { + /** Get the tuple type that describes this reference's shape */ + getTarget: { + (): TupleType; + gen(): Generator; + }; +} + /** Interface types — classes and interfaces (ObjectFlags.ClassOrInterface) */ export interface InterfaceType extends TypeReference { /** Get all type parameters (outer + local, excluding thisType) */ @@ -263,8 +274,13 @@ export interface InterfaceType extends TypeReference { }; } -/** Tuple types (ObjectFlags.Tuple) */ +/** Tuple type targets (ObjectFlags.Tuple) */ export interface TupleType extends InterfaceType { + /** Get this tuple target */ + getTarget: { + (): TupleType; + gen(): Generator; + }; /** Per-element flags (Required, Optional, Rest, Variadic) */ readonly elementFlags: readonly ElementFlags[]; /** Number of initial required or optional elements */ diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index b40091c90d6c9..0dbe366e50c95 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -2726,15 +2726,59 @@ export const tuple: readonly [number, string?, ...boolean[]] = [1]; } }); - test("TupleType properties", async () => { - const { type, api } = await getTypeAtName(spawnAPI(typeFiles), "tuple:"); + test("tuple metadata is owned by tuple targets", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": ` +declare function empty(value: readonly []): void; +declare function nonempty(value: readonly [number]): void; +declare function array(value: readonly number[]): void; +empty([]); +nonempty([1]); +array([]); +`, + }); try { - assert.ok(type.flags & TypeFlags.Object); - const ref = type as TypeReference; - assert.ok(ref.objectFlags & ObjectFlags.Reference); - const target = await ref.getTarget(); - assert.ok(target); - assert.ok(target.flags & TypeFlags.Object); + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const sourceFile = await project.program.getSourceFile("/src/main.ts"); + assert.ok(sourceFile); + + const arrayLiterals: Node[] = []; + sourceFile.forEachChild(function visit(node) { + if (node.kind === SyntaxKind.ArrayLiteralExpression) { + arrayLiterals.push(node); + } + node.forEachChild(visit); + }); + assert.equal(arrayLiterals.length, 3); + + for (const [index, expectedFixedLength] of [0, 1].entries()) { + const type = await project.checker.getTypeAtLocation(arrayLiterals[index]); + assert.equal(await project.checker.isTupleType(type), true); + assert.equal(await project.checker.isTupleTypeTarget(type), false); + assert.equal(type.isTupleType(), true); + assert.equal(type.isTupleTypeTarget(), false); + assert.ok(type.isTupleType()); + assert.equal(Reflect.get(type, "fixedLength"), undefined); + + const target = await type.getTarget(); + assert.ok(target.objectFlags & ObjectFlags.Tuple); + assert.equal(await project.checker.isTupleType(target), true); + assert.equal(await project.checker.isTupleTypeTarget(target), true); + assert.equal(target.isTupleType(), true); + assert.equal(target.isTupleTypeTarget(), true); + assert.ok(target.isTupleTypeTarget()); + assert.equal(target.fixedLength, expectedFixedLength); + assert.equal(target.elementFlags.length, expectedFixedLength); + assert.equal(target.readonly, false); + } + + const arrayType = await project.checker.getTypeAtLocation(arrayLiterals[2]); + assert.equal(await project.checker.isTupleType(arrayType), false); + assert.equal(await project.checker.isTupleTypeTarget(arrayType), false); + assert.equal(arrayType.isTupleType(), false); + assert.equal(arrayType.isTupleTypeTarget(), false); } finally { await api.close(); diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index 9016e6c75e7de..3d557db4e1f5d 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -814,6 +814,7 @@ describe("API - generator batching", () => { parityCase("Checker", "isContextSensitive", checker.isContextSensitive, assertDeepEquivalent, boxDeclaration.initializer!), parityCase("Checker", "isArrayType", checker.isArrayType, assertDeepEquivalent, arrayType), parityCase("Checker", "isTupleType", checker.isTupleType, assertDeepEquivalent, tupleType), + parityCase("Checker", "isTupleTypeTarget", checker.isTupleTypeTarget, assertDeepEquivalent, tupleType), parityCase("Checker", "getReturnTypeOfSignature", checker.getReturnTypeOfSignature, assertTypesEquivalent, signature), parityCase("Checker", "getRestTypeOfSignature", checker.getRestTypeOfSignature, assertOptionalTypesEquivalent, signature), parityCase("Checker", "getTypePredicateOfSignature", checker.getTypePredicateOfSignature, assertOptionalTypePredicatesEquivalent, predicateSignature), diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index d534bc20a6e4e..7eb0a5e0b510e 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -2642,15 +2642,59 @@ export const tuple: readonly [number, string?, ...boolean[]] = [1]; } }); - test("TupleType properties", () => { - const { type, api } = getTypeAtName(spawnAPI(typeFiles), "tuple:"); + test("tuple metadata is owned by tuple targets", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": ` +declare function empty(value: readonly []): void; +declare function nonempty(value: readonly [number]): void; +declare function array(value: readonly number[]): void; +empty([]); +nonempty([1]); +array([]); +`, + }); try { - assert.ok(type.flags & TypeFlags.Object); - const ref = type as TypeReference; - assert.ok(ref.objectFlags & ObjectFlags.Reference); - const target = ref.getTarget(); - assert.ok(target); - assert.ok(target.flags & TypeFlags.Object); + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const sourceFile = project.program.getSourceFile("/src/main.ts"); + assert.ok(sourceFile); + + const arrayLiterals: Node[] = []; + sourceFile.forEachChild(function visit(node) { + if (node.kind === SyntaxKind.ArrayLiteralExpression) { + arrayLiterals.push(node); + } + node.forEachChild(visit); + }); + assert.equal(arrayLiterals.length, 3); + + for (const [index, expectedFixedLength] of [0, 1].entries()) { + const type = project.checker.getTypeAtLocation(arrayLiterals[index]); + assert.equal(project.checker.isTupleType(type), true); + assert.equal(project.checker.isTupleTypeTarget(type), false); + assert.equal(type.isTupleType(), true); + assert.equal(type.isTupleTypeTarget(), false); + assert.ok(type.isTupleType()); + assert.equal(Reflect.get(type, "fixedLength"), undefined); + + const target = type.getTarget(); + assert.ok(target.objectFlags & ObjectFlags.Tuple); + assert.equal(project.checker.isTupleType(target), true); + assert.equal(project.checker.isTupleTypeTarget(target), true); + assert.equal(target.isTupleType(), true); + assert.equal(target.isTupleTypeTarget(), true); + assert.ok(target.isTupleTypeTarget()); + assert.equal(target.fixedLength, expectedFixedLength); + assert.equal(target.elementFlags.length, expectedFixedLength); + assert.equal(target.readonly, false); + } + + const arrayType = project.checker.getTypeAtLocation(arrayLiterals[2]); + assert.equal(project.checker.isTupleType(arrayType), false); + assert.equal(project.checker.isTupleTypeTarget(arrayType), false); + assert.equal(arrayType.isTupleType(), false); + assert.equal(arrayType.isTupleTypeTarget(), false); } finally { api.close(); diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 84b3673425fce..a7c15ddef7bee 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -176,7 +176,6 @@ const ( MethodGetJSDocTags Method = "getJsDocTags" MethodGetDocumentationComment Method = "getDocumentationComment" MethodIsArrayType Method = "isArrayType" - MethodIsTupleType Method = "isTupleType" MethodIsReadonlySymbol Method = "isReadonlySymbol" // Reference methods @@ -516,7 +515,6 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetJSDocTags: unmarshallerFor[CheckerSymbolParams], MethodGetDocumentationComment: unmarshallerFor[CheckerSymbolParams], MethodIsArrayType: unmarshallerFor[CheckerTypeParams], - MethodIsTupleType: unmarshallerFor[CheckerTypeParams], MethodIsReadonlySymbol: unmarshallerFor[CheckerSymbolParams], MethodGetReferencesToSymbolInFile: unmarshallerFor[GetReferencesToSymbolInFileParams], MethodGetReferencedSymbolsForNode: unmarshallerFor[GetReferencedSymbolsForNodeParams], @@ -829,6 +827,7 @@ type TypeResponse struct { Id TypeID `json:"id"` Flags uint32 `json:"flags"` ObjectFlags uint32 `json:"objectFlags,omitempty"` + IsTupleType bool `json:"isTupleType,omitempty"` // Value is literal type data. BigInt literals are encoded as signed decimal // strings because JSON cannot represent bigint; absent values are null. @@ -911,19 +910,17 @@ func newTypeResponse(t *checker.Type, id TypeID) *TypeResponse { } case flags&checker.TypeFlagsObject != 0: resp.ObjectFlags = uint32(t.ObjectFlags()) + resp.IsTupleType = checker.IsTupleType(t) objectFlags := t.ObjectFlags() if objectFlags&checker.ObjectFlagsReference != 0 { - var ref *checker.TypeReference - if objectFlags&checker.ObjectFlagsTuple != 0 { + ref := t.AsTypeReference() + if checker.IsTupleTypeTarget(t) { tuple := t.AsTupleType() - ref = tuple.AsTypeReference() resp.ElementFlags = tuple.ElementFlags() fixedLen := tuple.FixedLength() resp.FixedLength = &fixedLen isReadonly := tuple.IsReadonly() resp.TupleReadonly = &isReadonly - } else { - ref = t.AsTypeReference() } if ref.Target() != nil { resp.Target = TypeHandle(ref.Target()) diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 03c28a6d665e1..dcbb317a07ea6 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -822,8 +822,6 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetDocumentationComment(ctx, parsed.(*CheckerSymbolParams)) case string(MethodIsArrayType): return s.handleIsArrayType(ctx, parsed.(*CheckerTypeParams)) - case string(MethodIsTupleType): - return s.handleIsTupleType(ctx, parsed.(*CheckerTypeParams)) case string(MethodIsReadonlySymbol): return s.handleIsReadonlySymbol(ctx, parsed.(*CheckerSymbolParams)) case string(MethodGetAnyType): @@ -3005,22 +3003,6 @@ func (s *Session) handleIsArrayType(ctx context.Context, params *CheckerTypePara return setup.checker.IsArrayType(t), nil } -// handleIsTupleType returns whether a type is a tuple type. -func (s *Session) handleIsTupleType(ctx context.Context, params *CheckerTypeParams) (bool, error) { - setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) - if err != nil { - return false, err - } - defer setup.done() - - t, err := setup.resolveTypeHandle(params.Type) - if err != nil { - return false, err - } - - return checker.IsTupleType(t), nil -} - // handleIsReadonlySymbol returns whether a symbol is a readonly symbol. func (s *Session) handleIsReadonlySymbol(ctx context.Context, params *CheckerSymbolParams) (bool, error) { setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) diff --git a/tsc/internal/checker/exports.go b/tsc/internal/checker/exports.go index f58d4a3ab6050..e26b4038fae8a 100644 --- a/tsc/internal/checker/exports.go +++ b/tsc/internal/checker/exports.go @@ -225,6 +225,10 @@ func IsTupleType(t *Type) bool { return isTupleType(t) } +func IsTupleTypeTarget(t *Type) bool { + return isTupleType(t) && t.Target() == t +} + func (c *Checker) IsArrayType(t *Type) bool { return c.isArrayType(t) }