From 71545f35469f912d20d46c0edc4369fd4905acb8 Mon Sep 17 00:00:00 2001 From: mrazauskas Date: Fri, 21 Aug 2026 09:02:28 +0300 Subject: [PATCH 1/3] Add `.isReadonlySymbol()` method Signed-off-by: mrazauskas --- packages/typescript/src/api/async/api.ts | 8 +++++++ .../typescript/src/api/proto.generated.ts | 1 + packages/typescript/src/api/sync/api.ts | 8 +++++++ packages/typescript/test/async/api.test.ts | 22 +++++++++++++++++++ packages/typescript/test/sync/api.test.ts | 22 +++++++++++++++++++ tsc/internal/api/proto.go | 2 ++ tsc/internal/api/session.go | 18 +++++++++++++++ tsc/internal/checker/exports.go | 4 ++++ 8 files changed, 85 insertions(+) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index ad787fb62495d..aa145191ecf96 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1742,6 +1742,14 @@ export class Checker { }); } + async isReadonlySymbol(symbol: Symbol): Promise { + return this.client.apiRequest("isReadonlySymbol", { + snapshot: this.snapshotId, + project: this.project.id, + symbol: symbol.id, + }); + } + /** Get the return type of a signature. Always returns a type. */ async getReturnTypeOfSignature(signature: Signature): Promise { return signature.getReturnType(); diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 35c08679d2aa4..730cf7a557c6d 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -113,6 +113,7 @@ export interface APIMethodInfo { getDocumentationComment: APIMethod; isArrayType: APIMethod; isTupleType: APIMethod; + isReadonlySymbol: APIMethod; getReferencesToSymbolInFile: APIMethod; getReferencedSymbolsForNode: APIMethod; getSignatureUsages: APIMethod; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 074471e86cfbb..a1fd372cf3076 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -1750,6 +1750,14 @@ export class Checker { }); } + isReadonlySymbol(symbol: Symbol): boolean { + return this.client.apiRequest("isReadonlySymbol", { + snapshot: this.snapshotId, + project: this.project.id, + symbol: symbol.id, + }); + } + /** Get the return type of a signature. Always returns a type. */ getReturnTypeOfSignature(signature: Signature): Type { return signature.getReturnType(); diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index fe08206bc486b..69b5d2c9d4902 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -3181,6 +3181,28 @@ describe("Checker - isArrayType / isTupleType", () => { }); }); +describe("Checker - isReadonlySymbol", () => { + test("returns whether a symbol is a readonly symbol", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export const foo = 1; export let bar = 2;`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const { checker } = snapshot.getProject("/tsconfig.json")!; + const foo = await checker.getSymbolAtPosition("/src/main.ts", "export const ".length); + const bar = await checker.getSymbolAtPosition("/src/main.ts", "export const foo = 1; export let ".length); + assert.ok(foo); + assert.ok(bar); + assert.equal(await checker.isReadonlySymbol(foo), true); + assert.equal(await checker.isReadonlySymbol(bar), false); + } + finally { + await api.close(); + } + }); +}); + describe("Checker - getReturnTypeOfSignature", () => { test("returns the return type of a function signature", async () => { const api = spawnAPI({ diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 6974061a4a6fb..c05da585fe476 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -3189,6 +3189,28 @@ describe("Checker - isArrayType / isTupleType", () => { }); }); +describe("Checker - isReadonlySymbol", () => { + test("returns whether a symbol is a readonly symbol", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export const foo = 1; export let bar = 2;`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const { checker } = snapshot.getProject("/tsconfig.json")!; + const foo = checker.getSymbolAtPosition("/src/main.ts", "export const ".length); + const bar = checker.getSymbolAtPosition("/src/main.ts", "export const foo = 1; export let ".length); + assert.ok(foo); + assert.ok(bar); + assert.equal(checker.isReadonlySymbol(foo), true); + assert.equal(checker.isReadonlySymbol(bar), false); + } + finally { + api.close(); + } + }); +}); + describe("Checker - getReturnTypeOfSignature", () => { test("returns the return type of a function signature", () => { const api = spawnAPI({ diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 45793d96a61be..9093b1cb10906 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -171,6 +171,7 @@ const ( MethodGetDocumentationComment Method = "getDocumentationComment" MethodIsArrayType Method = "isArrayType" MethodIsTupleType Method = "isTupleType" + MethodIsReadonlySymbol Method = "isReadonlySymbol" // Reference methods MethodGetReferencesToSymbolInFile Method = "getReferencesToSymbolInFile" @@ -508,6 +509,7 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetDocumentationComment: unmarshallerFor[CheckerSymbolParams], MethodIsArrayType: unmarshallerFor[CheckerTypeParams], MethodIsTupleType: unmarshallerFor[CheckerTypeParams], + MethodIsReadonlySymbol: unmarshallerFor[CheckerSymbolParams], MethodGetReferencesToSymbolInFile: unmarshallerFor[GetReferencesToSymbolInFileParams], MethodGetReferencedSymbolsForNode: unmarshallerFor[GetReferencedSymbolsForNodeParams], MethodGetSignatureUsages: unmarshallerFor[GetSignatureUsagesParams], diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index d5c668385b849..7067e51b7d3f8 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -817,6 +817,8 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. 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): return s.handleGetIntrinsicType(ctx, parsed.(*GetIntrinsicTypeParams), (*checker.Checker).GetAnyType) case string(MethodGetStringType): @@ -2972,6 +2974,22 @@ func (s *Session) handleIsTupleType(ctx context.Context, params *CheckerTypePara 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) + if err != nil { + return false, err + } + defer setup.done() + + symbol, err := setup.resolveSymbolHandle(params.Symbol) + if err != nil { + return false, err + } + + return setup.checker.IsReadonlySymbol(symbol), nil +} + // handleGetBaseTypes returns the base types of an interface/class type. // @gen-proto-nullable func (s *Session) handleGetBaseTypes(ctx context.Context, params *CheckerTypeParams) ([]*TypeResponse, error) { diff --git a/tsc/internal/checker/exports.go b/tsc/internal/checker/exports.go index 49efc3208d430..6292a4a95b1a5 100644 --- a/tsc/internal/checker/exports.go +++ b/tsc/internal/checker/exports.go @@ -221,6 +221,10 @@ func (c *Checker) IsArrayType(t *Type) bool { return c.isArrayType(t) } +func (c *Checker) IsReadonlySymbol(symbol *ast.Symbol) bool { + return c.isReadonlySymbol(symbol) +} + func (c *Checker) GetReturnTypeOfSignature(sig *Signature) *Type { return c.getReturnTypeOfSignature(sig) } From 38ca4fd9c617b78abb5a7f7fdc3a7a0e4ce425af Mon Sep 17 00:00:00 2001 From: mrazauskas Date: Fri, 21 Aug 2026 10:11:06 +0300 Subject: [PATCH 2/3] add more tests Signed-off-by: mrazauskas --- packages/typescript/test/async/api.test.ts | 98 ++++++++++++++++++++-- packages/typescript/test/sync/api.test.ts | 98 ++++++++++++++++++++-- 2 files changed, 180 insertions(+), 16 deletions(-) diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 69b5d2c9d4902..a477c8b144276 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -10,6 +10,7 @@ import { isFunctionDeclaration, isIdentifier, isImportDeclaration, + isInterfaceDeclaration, isJSDocParameterTag, isNamedImports, isReturnStatement, @@ -3182,20 +3183,101 @@ describe("Checker - isArrayType / isTupleType", () => { }); describe("Checker - isReadonlySymbol", () => { - test("returns whether a symbol is a readonly symbol", async () => { + test("properties with a 'readonly' modifier", async () => { const api = spawnAPI({ "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), - "/src/main.ts": `export const foo = 1; export let bar = 2;`, + "/src/main.ts": ` +export interface User { + readonly name: string; + age: number; +} + +export type ReadonlyUser = Readonly; +`, + }); + try { + 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 user = sourceFile.statements.find(isInterfaceDeclaration); + assert.ok(user); + const userProperties = await project.checker.getPropertiesOfType( + await project.checker.getTypeAtLocation(user) + ); + assert.equal(await project.checker.isReadonlySymbol(userProperties[0]), true); + assert.equal(await project.checker.isReadonlySymbol(userProperties[1]), false); + const readonlyUser = sourceFile.statements.find(isTypeAliasDeclaration); + assert.ok(readonlyUser); + const readonlyUserProperties = await project.checker.getPropertiesOfType( + await project.checker.getTypeAtLocation(readonlyUser) + ); + assert.equal(await project.checker.isReadonlySymbol(readonlyUserProperties[0]), true); + assert.equal(await project.checker.isReadonlySymbol(readonlyUserProperties[1]), true); + } + finally { + await api.close(); + } + }); + + test("variables declared with 'const'", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export const a = 1; export let b = 2;`, }); try { const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); const { checker } = snapshot.getProject("/tsconfig.json")!; - const foo = await checker.getSymbolAtPosition("/src/main.ts", "export const ".length); - const bar = await checker.getSymbolAtPosition("/src/main.ts", "export const foo = 1; export let ".length); - assert.ok(foo); - assert.ok(bar); - assert.equal(await checker.isReadonlySymbol(foo), true); - assert.equal(await checker.isReadonlySymbol(bar), false); + const a = await checker.getSymbolAtPosition("/src/main.ts", "export const ".length); + const b = await checker.getSymbolAtPosition("/src/main.ts", "export const a = 1; export let ".length); + assert.ok(a); + assert.ok(b); + assert.equal(await checker.isReadonlySymbol(a), true); + assert.equal(await checker.isReadonlySymbol(b), false); + } + finally { + await api.close(); + } + }); + + test("get accessors without matching set accessors", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": ` +class Alpha { + private _value!: number; + get value(): number { + return this._value; + } +} +class Bravo { + private _value!: number; + get value(): number { + return this._value; + } + set value(newValue: number) { + this._value = newValue; + } +} +export type A = InstanceType; +export type B = InstanceType; +`, + }); + try { + 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 typeAliases = sourceFile.statements.filter(isTypeAliasDeclaration); + assert.equal(typeAliases.length, 2); + const aProperties = await project.checker.getPropertiesOfType( + await project.checker.getTypeAtLocation(typeAliases[0]) + ); + assert.equal(await project.checker.isReadonlySymbol(aProperties[1]), true); + const bProperties = await project.checker.getPropertiesOfType( + await project.checker.getTypeAtLocation(typeAliases[1]) + ); + assert.equal(await project.checker.isReadonlySymbol(bProperties[1]), false); } finally { await api.close(); diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index c05da585fe476..7fa03f929e7a7 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -18,6 +18,7 @@ import { isFunctionDeclaration, isIdentifier, isImportDeclaration, + isInterfaceDeclaration, isJSDocParameterTag, isNamedImports, isReturnStatement, @@ -3190,20 +3191,101 @@ describe("Checker - isArrayType / isTupleType", () => { }); describe("Checker - isReadonlySymbol", () => { - test("returns whether a symbol is a readonly symbol", () => { + test("properties with a 'readonly' modifier", () => { const api = spawnAPI({ "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), - "/src/main.ts": `export const foo = 1; export let bar = 2;`, + "/src/main.ts": ` +export interface User { + readonly name: string; + age: number; +} + +export type ReadonlyUser = Readonly; +`, + }); + try { + 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 user = sourceFile.statements.find(isInterfaceDeclaration); + assert.ok(user); + const userProperties = project.checker.getPropertiesOfType( + project.checker.getTypeAtLocation(user), + ); + assert.equal(project.checker.isReadonlySymbol(userProperties[0]), true); + assert.equal(project.checker.isReadonlySymbol(userProperties[1]), false); + const readonlyUser = sourceFile.statements.find(isTypeAliasDeclaration); + assert.ok(readonlyUser); + const readonlyUserProperties = project.checker.getPropertiesOfType( + project.checker.getTypeAtLocation(readonlyUser), + ); + assert.equal(project.checker.isReadonlySymbol(readonlyUserProperties[0]), true); + assert.equal(project.checker.isReadonlySymbol(readonlyUserProperties[1]), true); + } + finally { + api.close(); + } + }); + + test("variables declared with 'const'", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export const a = 1; export let b = 2;`, }); try { const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); const { checker } = snapshot.getProject("/tsconfig.json")!; - const foo = checker.getSymbolAtPosition("/src/main.ts", "export const ".length); - const bar = checker.getSymbolAtPosition("/src/main.ts", "export const foo = 1; export let ".length); - assert.ok(foo); - assert.ok(bar); - assert.equal(checker.isReadonlySymbol(foo), true); - assert.equal(checker.isReadonlySymbol(bar), false); + const a = checker.getSymbolAtPosition("/src/main.ts", "export const ".length); + const b = checker.getSymbolAtPosition("/src/main.ts", "export const a = 1; export let ".length); + assert.ok(a); + assert.ok(b); + assert.equal(checker.isReadonlySymbol(a), true); + assert.equal(checker.isReadonlySymbol(b), false); + } + finally { + api.close(); + } + }); + + test("get accessors without matching set accessors", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": ` +class Alpha { + private _value!: number; + get value(): number { + return this._value; + } +} +class Bravo { + private _value!: number; + get value(): number { + return this._value; + } + set value(newValue: number) { + this._value = newValue; + } +} +export type A = InstanceType; +export type B = InstanceType; +`, + }); + try { + 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 typeAliases = sourceFile.statements.filter(isTypeAliasDeclaration); + assert.equal(typeAliases.length, 2); + const aProperties = project.checker.getPropertiesOfType( + project.checker.getTypeAtLocation(typeAliases[0]), + ); + assert.equal(project.checker.isReadonlySymbol(aProperties[1]), true); + const bProperties = project.checker.getPropertiesOfType( + project.checker.getTypeAtLocation(typeAliases[1]), + ); + assert.equal(project.checker.isReadonlySymbol(bProperties[1]), false); } finally { api.close(); From fe407554ad7cf42e75b658b54018dd1244d2bb2a Mon Sep 17 00:00:00 2001 From: mrazauskas Date: Fri, 21 Aug 2026 10:19:40 +0300 Subject: [PATCH 3/3] format Signed-off-by: mrazauskas --- packages/typescript/test/async/api.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index a477c8b144276..940046c4700c3 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -3198,19 +3198,19 @@ export type ReadonlyUser = Readonly; try { const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); const project = snapshot.getProject("/tsconfig.json")!; - const sourceFile = await project.program.getSourceFile( "/src/main.ts"); + const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const user = sourceFile.statements.find(isInterfaceDeclaration); assert.ok(user); const userProperties = await project.checker.getPropertiesOfType( - await project.checker.getTypeAtLocation(user) + await project.checker.getTypeAtLocation(user), ); assert.equal(await project.checker.isReadonlySymbol(userProperties[0]), true); assert.equal(await project.checker.isReadonlySymbol(userProperties[1]), false); const readonlyUser = sourceFile.statements.find(isTypeAliasDeclaration); assert.ok(readonlyUser); const readonlyUserProperties = await project.checker.getPropertiesOfType( - await project.checker.getTypeAtLocation(readonlyUser) + await project.checker.getTypeAtLocation(readonlyUser), ); assert.equal(await project.checker.isReadonlySymbol(readonlyUserProperties[0]), true); assert.equal(await project.checker.isReadonlySymbol(readonlyUserProperties[1]), true); @@ -3266,16 +3266,16 @@ export type B = InstanceType; try { const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); const project = snapshot.getProject("/tsconfig.json")!; - const sourceFile = await project.program.getSourceFile( "/src/main.ts"); + const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const typeAliases = sourceFile.statements.filter(isTypeAliasDeclaration); assert.equal(typeAliases.length, 2); const aProperties = await project.checker.getPropertiesOfType( - await project.checker.getTypeAtLocation(typeAliases[0]) + await project.checker.getTypeAtLocation(typeAliases[0]), ); assert.equal(await project.checker.isReadonlySymbol(aProperties[1]), true); const bProperties = await project.checker.getPropertiesOfType( - await project.checker.getTypeAtLocation(typeAliases[1]) + await project.checker.getTypeAtLocation(typeAliases[1]), ); assert.equal(await project.checker.isReadonlySymbol(bProperties[1]), false); }