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
27 changes: 25 additions & 2 deletions packages/typescript/src/api/async/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2101,7 +2101,16 @@ 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;
}
return -Infinity;
}
return data.value;
}

/** Get the signature of a function-like declaration. Always returns a signature. */
Expand Down Expand Up @@ -2544,7 +2553,21 @@ 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 {
this.value = -Infinity;
}
}
else {
this.value = value;
}
}
if (data.intrinsicName !== undefined) this.intrinsicName = data.intrinsicName;
if (data.isThisType !== undefined) this.isThisType = data.isThisType;
Expand Down
7 changes: 6 additions & 1 deletion packages/typescript/src/api/proto.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export interface APIMethodInfo {
getImportAdderEdits: APIMethod<GetImportAdderEditsParams, TextEdit[]>;
getTrueTypeOfConditionalType: APIMethod<GetTypePropertyParams, TypeResponse>;
getFalseTypeOfConditionalType: APIMethod<GetTypePropertyParams, TypeResponse>;
getConstantValue: APIMethod<CheckerNodeParams, unknown | null>;
getConstantValue: APIMethod<CheckerNodeParams, ConstantValueResponse | null>;
getSignatureFromDeclaration: APIMethod<CheckerNodeParams, SignatureResponse>;
getExportSpecifierLocalTargetSymbol: APIMethod<CheckerNodeParams, SymbolResponse | null>;
getAliasedSymbol: APIMethod<CheckerSymbolParams, SymbolResponse>;
Expand Down Expand Up @@ -720,6 +720,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;
Expand Down
38 changes: 35 additions & 3 deletions packages/typescript/src/api/sync/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4568,15 +4568,33 @@ 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;
}
return -Infinity;
}
return data.value;
},
function* (node: Node): Generator<ProtocolRequest, string | number | undefined, ProtocolResponse["result"]> {
const data = yield* apiRequest("getConstantValue", {
snapshot: owner.snapshotId,
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;
}
return -Infinity;
}
return data.value;
},
);
}
Expand Down Expand Up @@ -5468,7 +5486,21 @@ 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 {
this.value = -Infinity;
}
}
else {
this.value = value;
}
}
if (data.intrinsicName !== undefined) this.intrinsicName = data.intrinsicName;
if (data.isThisType !== undefined) this.isThisType = data.isThisType;
Expand Down
53 changes: 53 additions & 0 deletions packages/typescript/test/async/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
type LiteralType,
ModifierFlags,
ModuleKind,
type NumberLiteralType,
ObjectFlags,
type Signature,
SignatureKind,
Expand Down Expand Up @@ -4947,6 +4948,29 @@ 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, PositiveString = "+Infinity", NegativeString = "-Infinity" }`,
});

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, 4);

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]), "+Infinity");
assert.equal(await project.checker.getConstantValue(members[3]), "-Infinity");
});

test("returns string value of a string-initialized enum member", async () => {
await using api = spawnAPI({
"/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }),
Expand Down Expand Up @@ -5451,6 +5475,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({
Expand Down
53 changes: 53 additions & 0 deletions packages/typescript/test/sync/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {
type LiteralType,
ModifierFlags,
ModuleKind,
type NumberLiteralType,
ObjectFlags,
type Signature,
SignatureKind,
Expand Down Expand Up @@ -4815,6 +4816,29 @@ 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, PositiveString = "+Infinity", NegativeString = "-Infinity" }`,
});

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, 4);

assert.equal(project.checker.getConstantValue(members[0]), Infinity);
assert.equal(project.checker.getConstantValue(members[1]), -Infinity);
assert.equal(project.checker.getConstantValue(members[2]), "+Infinity");
assert.equal(project.checker.getConstantValue(members[3]), "-Infinity");
});

test("returns string value of a string-initialized enum member", () => {
using api = spawnAPI({
"/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }),
Expand Down Expand Up @@ -5319,6 +5343,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({
Expand Down
11 changes: 11 additions & 0 deletions tsc/internal/api/proto.go
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,12 @@ func literalValueToJSON(value any) any {
case string:
return v
case jsnum.Number:
if v.IsInf() {
if v > 0 {
return "+Infinity"
}
return "-Infinity"
Comment thread
auvred marked this conversation as resolved.
}
return float64(v)
case bool:
return v
Expand All @@ -1068,6 +1074,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"`
Expand Down
9 changes: 7 additions & 2 deletions tsc/internal/api/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -3549,7 +3550,7 @@ func (s *Session) handleGetPropertyOfType(ctx context.Context, params *GetProper

// 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
Expand All @@ -3564,7 +3565,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.
Expand Down