diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index e80759433c3f0..abd0080400f39 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -2215,7 +2215,19 @@ export class Checker { project: this.project.id, location: getNodeId(node), }); - return typeof data === "string" || typeof data === "number" ? data : undefined; + if (!data || (typeof data.value !== "string" && typeof data.value !== "number")) { + return undefined; + } + if (data.isNumber && typeof data.value === "string") { + if (data.value === "+Infinity") { + return Infinity; + } + else if (data.value === "-Infinity") { + return -Infinity; + } + return NaN; + } + return data.value; } /** Get the signature of a function-like declaration. Always returns a signature. */ @@ -2668,7 +2680,24 @@ class TypeObject implements Type { // BigInt literal values are serialized as decimal strings (e.g. "-123") because // JSON cannot represent bigint. Decode them back into a real bigint here. const value = data.value as string | number | boolean; - this.value = (data.flags & TypeFlags.BigIntLiteral) ? BigInt(value as string) : value; + if (data.flags & TypeFlags.BigIntLiteral) { + this.value = BigInt(value as string); + } + // JSON cannot represent infinities, so the API serializes them as strings. + else if (data.flags & TypeFlags.NumberLiteral && typeof value === "string") { + if (value === "+Infinity") { + this.value = Infinity; + } + else if (value === "-Infinity") { + this.value = -Infinity; + } + else { + this.value = NaN; + } + } + else { + this.value = value; + } } if (data.intrinsicName !== undefined) this.intrinsicName = data.intrinsicName; if (data.isThisType !== undefined) this.isThisType = data.isThisType; diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 6feefcc38038a..2ba967940a310 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -121,7 +121,7 @@ export interface APIMethodInfo { getImportAdderEdits: APIMethod; getTrueTypeOfConditionalType: APIMethod; getFalseTypeOfConditionalType: APIMethod; - getConstantValue: APIMethod; + getConstantValue: APIMethod; getSignatureFromDeclaration: APIMethod; getExportSpecifierLocalTargetSymbol: APIMethod; getAliasedSymbol: APIMethod; @@ -797,6 +797,11 @@ export interface CheckerNodeParams { location: string; } +export interface ConstantValueResponse { + isNumber: boolean; + value: unknown; +} + /** CheckerSymbolParams are parameters for checker methods that operate on a symbol. */ export interface CheckerSymbolParams { snapshot: number; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 5a0972e0f0b00..8d44ff7b455ab 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -4839,7 +4839,19 @@ export class Checker { project: owner.project.id, location: getNodeId(node), }); - return typeof data === "string" || typeof data === "number" ? data : undefined; + if (!data || (typeof data.value !== "string" && typeof data.value !== "number")) { + return undefined; + } + if (data.isNumber && typeof data.value === "string") { + if (data.value === "+Infinity") { + return Infinity; + } + else if (data.value === "-Infinity") { + return -Infinity; + } + return NaN; + } + return data.value; }, function* (node: Node): Generator { const data = yield* apiRequest("getConstantValue", { @@ -4847,7 +4859,19 @@ export class Checker { project: owner.project.id, location: getNodeId(node), }); - return typeof data === "string" || typeof data === "number" ? data : undefined; + if (!data || (typeof data.value !== "string" && typeof data.value !== "number")) { + return undefined; + } + if (data.isNumber && typeof data.value === "string") { + if (data.value === "+Infinity") { + return Infinity; + } + else if (data.value === "-Infinity") { + return -Infinity; + } + return NaN; + } + return data.value; }, ); } @@ -5767,7 +5791,24 @@ class TypeObject implements Type { // BigInt literal values are serialized as decimal strings (e.g. "-123") because // JSON cannot represent bigint. Decode them back into a real bigint here. const value = data.value as string | number | boolean; - this.value = (data.flags & TypeFlags.BigIntLiteral) ? BigInt(value as string) : value; + if (data.flags & TypeFlags.BigIntLiteral) { + this.value = BigInt(value as string); + } + // JSON cannot represent infinities, so the API serializes them as strings. + else if (data.flags & TypeFlags.NumberLiteral && typeof value === "string") { + if (value === "+Infinity") { + this.value = Infinity; + } + else if (value === "-Infinity") { + this.value = -Infinity; + } + else { + this.value = NaN; + } + } + else { + this.value = value; + } } if (data.intrinsicName !== undefined) this.intrinsicName = data.intrinsicName; if (data.isThisType !== undefined) this.isThisType = data.isThisType; diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 2dda1f8544278..38dd9868b76ae 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -69,6 +69,7 @@ import { ModifierFlags, ModuleKind, ModuleResolutionKind, + type NumberLiteralType, ObjectFlags, type Signature, SignatureKind, @@ -5090,6 +5091,40 @@ describe("Checker - getConstantValue", () => { assert.equal(value, 2); }); + test("returns infinite numeric enum values without changing equivalent strings", async () => { + await using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export enum E { + Positive = 1e999, + Negative = -1e999, + PositiveNaN = NaN, + NegativeNaN = -NaN, + PositiveString = "+Infinity", + NegativeString = "-Infinity", + NaNString = "NaN", + }`, + }); + + 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 members: Node[] = []; + sourceFile.forEachChild(function visit(node) { + if (node.kind === SyntaxKind.EnumMember) members.push(node); + node.forEachChild(visit); + }); + assert.equal(members.length, 7); + + assert.equal(await project.checker.getConstantValue(members[0]), Infinity); + assert.equal(await project.checker.getConstantValue(members[1]), -Infinity); + assert.equal(await project.checker.getConstantValue(members[2]), NaN); + assert.equal(await project.checker.getConstantValue(members[3]), NaN); + assert.equal(await project.checker.getConstantValue(members[4]), "+Infinity"); + assert.equal(await project.checker.getConstantValue(members[5]), "-Infinity"); + assert.equal(await project.checker.getConstantValue(members[6]), "NaN"); + }); + test("returns string value of a string-initialized enum member", async () => { await using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), @@ -5594,6 +5629,35 @@ describe("FreshableType - getFreshType and getRegularType", () => { assert.equal(negLiteral.value, -123n); }); + test("NumberLiteralType.value is infinity (positive and negative)", async () => { + const src = `\nexport const pos = 1e999;\nexport const neg = -1e999;\n`; + await using api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/main.ts": src, + }); + + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + + const posSymbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("pos =")); + assert.ok(posSymbol); + const posType = await project.checker.getTypeOfSymbol(posSymbol); + assert.ok(posType); + assert.ok(posType.flags & TypeFlags.NumberLiteral, "Expected NumberLiteral"); + const posLiteral = posType as NumberLiteralType; + assert.equal(typeof posLiteral.value, "number"); + assert.equal(posLiteral.value, Infinity); + + const negSymbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("neg =")); + assert.ok(negSymbol); + const negType = await project.checker.getTypeOfSymbol(negSymbol); + assert.ok(negType); + assert.ok(negType.flags & TypeFlags.NumberLiteral, "Expected NumberLiteral"); + const negLiteral = negType as NumberLiteralType; + assert.equal(typeof negLiteral.value, "number"); + assert.equal(negLiteral.value, -Infinity); + }); + test("getFreshType() returns a fresh twin with matching value", async () => { const src = `\nexport const greeting: "hello" = "hello";\n`; await using api = spawnAPI({ diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index a7678eed037aa..3183f8f33e9a8 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -84,6 +84,7 @@ import { ModifierFlags, ModuleKind, ModuleResolutionKind, + type NumberLiteralType, ObjectFlags, type Signature, SignatureKind, @@ -4958,6 +4959,40 @@ describe("Checker - getConstantValue", () => { assert.equal(value, 2); }); + test("returns infinite numeric enum values without changing equivalent strings", () => { + using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export enum E { + Positive = 1e999, + Negative = -1e999, + PositiveNaN = NaN, + NegativeNaN = -NaN, + PositiveString = "+Infinity", + NegativeString = "-Infinity", + NaNString = "NaN", + }`, + }); + + 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 members: Node[] = []; + sourceFile.forEachChild(function visit(node) { + if (node.kind === SyntaxKind.EnumMember) members.push(node); + node.forEachChild(visit); + }); + assert.equal(members.length, 7); + + assert.equal(project.checker.getConstantValue(members[0]), Infinity); + assert.equal(project.checker.getConstantValue(members[1]), -Infinity); + assert.equal(project.checker.getConstantValue(members[2]), NaN); + assert.equal(project.checker.getConstantValue(members[3]), NaN); + assert.equal(project.checker.getConstantValue(members[4]), "+Infinity"); + assert.equal(project.checker.getConstantValue(members[5]), "-Infinity"); + assert.equal(project.checker.getConstantValue(members[6]), "NaN"); + }); + test("returns string value of a string-initialized enum member", () => { using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), @@ -5462,6 +5497,35 @@ describe("FreshableType - getFreshType and getRegularType", () => { assert.equal(negLiteral.value, -123n); }); + test("NumberLiteralType.value is infinity (positive and negative)", () => { + const src = `\nexport const pos = 1e999;\nexport const neg = -1e999;\n`; + using api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/main.ts": src, + }); + + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + + const posSymbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("pos =")); + assert.ok(posSymbol); + const posType = project.checker.getTypeOfSymbol(posSymbol); + assert.ok(posType); + assert.ok(posType.flags & TypeFlags.NumberLiteral, "Expected NumberLiteral"); + const posLiteral = posType as NumberLiteralType; + assert.equal(typeof posLiteral.value, "number"); + assert.equal(posLiteral.value, Infinity); + + const negSymbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("neg =")); + assert.ok(negSymbol); + const negType = project.checker.getTypeOfSymbol(negSymbol); + assert.ok(negType); + assert.ok(negType.flags & TypeFlags.NumberLiteral, "Expected NumberLiteral"); + const negLiteral = negType as NumberLiteralType; + assert.equal(typeof negLiteral.value, "number"); + assert.equal(negLiteral.value, -Infinity); + }); + test("getFreshType() returns a fresh twin with matching value", () => { const src = `\nexport const greeting: "hello" = "hello";\n`; using api = spawnAPI({ diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 14cb062642920..587d563c2f1f4 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -1085,6 +1085,15 @@ func literalValueToJSON(value any) any { case string: return v case jsnum.Number: + if v.IsInf() { + if v > 0 { + return "+Infinity" + } + return "-Infinity" + } + if v.IsNaN() { + return "NaN" + } return float64(v) case bool: return v @@ -1097,6 +1106,11 @@ func literalValueToJSON(value any) any { } } +type ConstantValueResponse struct { + IsNumber bool `json:"isNumber"` + Value any `json:"value"` +} + type SignatureResponse struct { Id SignatureID `json:"id"` Flags uint32 `json:"flags"` diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 1f0faff868ce7..eda1ccb189a1f 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -23,6 +23,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/diagnostics" "github.com/microsoft/TypeScript/tsc/internal/format" "github.com/microsoft/TypeScript/tsc/internal/ipc" + "github.com/microsoft/TypeScript/tsc/internal/jsnum" "github.com/microsoft/TypeScript/tsc/internal/json" "github.com/microsoft/TypeScript/tsc/internal/ls" "github.com/microsoft/TypeScript/tsc/internal/ls/autoimport" @@ -3791,7 +3792,7 @@ func (s *Session) handleGetTypeOfPropertyOfType(ctx context.Context, params *Get // handleGetConstantValue returns the constant value of an enum member or const enum access. // @gen-proto-nullable -func (s *Session) handleGetConstantValue(ctx context.Context, params *CheckerNodeParams) (any, error) { +func (s *Session) handleGetConstantValue(ctx context.Context, params *CheckerNodeParams) (*ConstantValueResponse, error) { setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) if err != nil { return nil, err @@ -3806,7 +3807,11 @@ func (s *Session) handleGetConstantValue(ctx context.Context, params *CheckerNod return nil, nil } - return literalValueToJSON(setup.checker.GetConstantValue(node)), nil + result := &ConstantValueResponse{} + value := setup.checker.GetConstantValue(node) + _, result.IsNumber = value.(jsnum.Number) + result.Value = literalValueToJSON(value) + return result, nil } // handleGetSignatureFromDeclaration returns the signature of a function-like declaration.