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
8 changes: 8 additions & 0 deletions packages/typescript/src/api/async/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1742,6 +1742,14 @@ export class Checker {
});
}

async isReadonlySymbol(symbol: Symbol): Promise<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. */
async getReturnTypeOfSignature(signature: Signature): Promise<Type> {
return signature.getReturnType();
Expand Down
1 change: 1 addition & 0 deletions packages/typescript/src/api/proto.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export interface APIMethodInfo {
getDocumentationComment: APIMethod<CheckerSymbolParams, string>;
isArrayType: APIMethod<CheckerTypeParams, boolean>;
isTupleType: APIMethod<CheckerTypeParams, boolean>;
isReadonlySymbol: APIMethod<CheckerSymbolParams, boolean>;
getReferencesToSymbolInFile: APIMethod<GetReferencesToSymbolInFileParams, string[]>;
getReferencedSymbolsForNode: APIMethod<GetReferencedSymbolsForNodeParams, ReferencedSymbolEntry[] | null>;
getSignatureUsages: APIMethod<GetSignatureUsagesParams, SignatureUsageResponse[] | null>;
Expand Down
8 changes: 8 additions & 0 deletions packages/typescript/src/api/sync/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
104 changes: 104 additions & 0 deletions packages/typescript/test/async/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
isFunctionDeclaration,
isIdentifier,
isImportDeclaration,
isInterfaceDeclaration,
isJSDocParameterTag,
isNamedImports,
isReturnStatement,
Expand Down Expand Up @@ -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<User>;
`,
});
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<typeof Alpha>;
export type B = InstanceType<typeof Bravo>;
`,
});
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({
Expand Down
104 changes: 104 additions & 0 deletions packages/typescript/test/sync/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
isFunctionDeclaration,
isIdentifier,
isImportDeclaration,
isInterfaceDeclaration,
isJSDocParameterTag,
isNamedImports,
isReturnStatement,
Expand Down Expand Up @@ -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<User>;
`,
});
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<typeof Alpha>;
export type B = InstanceType<typeof Bravo>;
`,
});
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({
Expand Down
2 changes: 2 additions & 0 deletions tsc/internal/api/proto.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ const (
MethodGetDocumentationComment Method = "getDocumentationComment"
MethodIsArrayType Method = "isArrayType"
MethodIsTupleType Method = "isTupleType"
MethodIsReadonlySymbol Method = "isReadonlySymbol"

// Reference methods
MethodGetReferencesToSymbolInFile Method = "getReferencesToSymbolInFile"
Expand Down Expand Up @@ -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],
Expand Down
18 changes: 18 additions & 0 deletions tsc/internal/api/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions tsc/internal/checker/exports.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down