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..940046c4700c3 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, @@ -3181,6 +3182,109 @@ describe("Checker - isArrayType / isTupleType", () => { }); }); +describe("Checker - isReadonlySymbol", () => { + test("properties with a 'readonly' modifier", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/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 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(); + } + }); +}); + 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..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, @@ -3189,6 +3190,109 @@ describe("Checker - isArrayType / isTupleType", () => { }); }); +describe("Checker - isReadonlySymbol", () => { + test("properties with a 'readonly' modifier", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/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 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(); + } + }); +}); + 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) }