diff --git a/_packages/native-preview/src/api/async/api.ts b/_packages/native-preview/src/api/async/api.ts index ec37e1a61a..d725d24a0a 100644 --- a/_packages/native-preview/src/api/async/api.ts +++ b/_packages/native-preview/src/api/async/api.ts @@ -81,6 +81,7 @@ import type { InterfaceType, IntersectionType, IntrinsicType, + JSDocTagInfo, LiteralType, NumberLiteralType, ObjectType, @@ -101,7 +102,7 @@ import type { export { CompletionItemKind, DiagnosticCategory, ElementFlags, ModifierFlags, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypePredicateKind }; export type { APIOptions, ClientSocketOptions, ClientSpawnOptions, DocumentIdentifier, DocumentPosition, LSPConnectionOptions }; -export type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, BigIntLiteralType, BooleanLiteralType, CompletionEntry, CompletionInfo, CompletionOptions, ConditionalType, Diagnostic, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, LiteralType, NumberLiteralType, ObjectType, StringLiteralType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType }; +export type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, BigIntLiteralType, BooleanLiteralType, CompletionEntry, CompletionInfo, CompletionOptions, ConditionalType, Diagnostic, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, JSDocTagInfo, LiteralType, NumberLiteralType, ObjectType, StringLiteralType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType }; export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts"; export class API { @@ -1055,6 +1056,22 @@ export class Checker { }); } + async isArrayType(type: Type): Promise { + return this.client.apiRequest("isArrayType", { + snapshot: this.snapshotId, + project: this.projectId, + type: type.id, + }); + } + + async isTupleType(type: Type): Promise { + return this.client.apiRequest("isTupleType", { + snapshot: this.snapshotId, + project: this.projectId, + type: type.id, + }); + } + async getReturnTypeOfSignature(signature: Signature): Promise { const data = await this.client.apiRequest("getReturnTypeOfSignature", { snapshot: this.snapshotId, @@ -1130,6 +1147,87 @@ export class Checker { return data ? this.objectRegistry.getOrCreateType(data) : undefined; } + async getBaseConstraintOfType(type: Type): Promise { + const data = await this.client.apiRequest("getBaseConstraintOfType", { + snapshot: this.snapshotId, + project: this.projectId, + type: type.id, + }); + return data ? this.objectRegistry.getOrCreateType(data) : undefined; + } + + async getPropertyOfType(type: Type, name: string): Promise { + const data = await this.client.apiRequest("getPropertyOfType", { + snapshot: this.snapshotId, + project: this.projectId, + type: type.id, + name, + }); + return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; + } + + async getConstantValue(node: Node): Promise { + const data = await this.client.apiRequest("getConstantValue", { + snapshot: this.snapshotId, + project: this.projectId, + location: getNodeId(node), + }); + return data ?? undefined; + } + + async getSignatureFromDeclaration(node: Node): Promise { + const data = await this.client.apiRequest("getSignatureFromDeclaration", { + snapshot: this.snapshotId, + project: this.projectId, + location: getNodeId(node), + }); + return data ? this.objectRegistry.getOrCreateSignature(data) : undefined; + } + + async getExportSpecifierLocalTargetSymbol(node: Node): Promise { + const data = await this.client.apiRequest("getExportSpecifierLocalTargetSymbol", { + snapshot: this.snapshotId, + project: this.projectId, + location: getNodeId(node), + }); + return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; + } + + async getAliasedSymbol(symbol: Symbol): Promise { + const data = await this.client.apiRequest("getAliasedSymbol", { + snapshot: this.snapshotId, + project: this.projectId, + symbol: symbol.id, + }); + return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; + } + + async getExportsOfModule(symbol: Symbol): Promise { + const data = await this.client.apiRequest("getExportsOfModule", { + snapshot: this.snapshotId, + project: this.projectId, + symbol: symbol.id, + }); + return data ? data.map(d => this.objectRegistry.getOrCreateSymbol(d)) : []; + } + + async getJsDocTagsOfSymbol(symbol: Symbol): Promise { + const data = await this.client.apiRequest("getJsDocTags", { + snapshot: this.snapshotId, + project: this.projectId, + symbol: symbol.id, + }); + return data ?? []; + } + + async getDocumentationCommentOfSymbol(symbol: Symbol): Promise { + return this.client.apiRequest("getDocumentationComment", { + snapshot: this.snapshotId, + project: this.projectId, + symbol: symbol.id, + }); + } + async getTypeArguments(type: Type): Promise { const data = await this.client.apiRequest("getTypeArguments", { snapshot: this.snapshotId, @@ -1248,6 +1346,14 @@ export class Symbol { if (!this.exportSymbol) return this; return this.objectRegistry.fetchSymbol(this, "getExportSymbolOfSymbol", this.exportSymbol); } + + async getJsDocTags(checker: Checker): Promise { + return checker.getJsDocTagsOfSymbol(this); + } + + async getDocumentationComment(checker: Checker): Promise { + return checker.getDocumentationCommentOfSymbol(this); + } } class TypeObject implements Type { diff --git a/_packages/native-preview/src/api/async/types.ts b/_packages/native-preview/src/api/async/types.ts index 955d36178d..ca6a4ca788 100644 --- a/_packages/native-preview/src/api/async/types.ts +++ b/_packages/native-preview/src/api/async/types.ts @@ -230,6 +230,16 @@ export interface IndexInfo { readonly declaration?: NodeHandle; } +/** + * A single JSDoc tag attached to a symbol — e.g. `@param`, `@returns`. + */ +export interface JSDocTagInfo { + /** The tag name, without the leading `@` — e.g. `"param"`. */ + readonly name: string; + /** The rendered tag text, if any — e.g. `"a the first number"` for `@param a the first number`. */ + readonly text?: string; +} + export interface CompletionEntryLabelDetails { detail?: string; description?: string; diff --git a/_packages/native-preview/src/api/sync/api.ts b/_packages/native-preview/src/api/sync/api.ts index 40224ef06e..b08014e246 100644 --- a/_packages/native-preview/src/api/sync/api.ts +++ b/_packages/native-preview/src/api/sync/api.ts @@ -89,6 +89,7 @@ import type { InterfaceType, IntersectionType, IntrinsicType, + JSDocTagInfo, LiteralType, NumberLiteralType, ObjectType, @@ -109,7 +110,7 @@ import type { export { CompletionItemKind, DiagnosticCategory, ElementFlags, ModifierFlags, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypePredicateKind }; export type { APIOptions, ClientSocketOptions, ClientSpawnOptions, DocumentIdentifier, DocumentPosition, LSPConnectionOptions }; -export type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, BigIntLiteralType, BooleanLiteralType, CompletionEntry, CompletionInfo, CompletionOptions, ConditionalType, Diagnostic, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, LiteralType, NumberLiteralType, ObjectType, StringLiteralType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType }; +export type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, BigIntLiteralType, BooleanLiteralType, CompletionEntry, CompletionInfo, CompletionOptions, ConditionalType, Diagnostic, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, JSDocTagInfo, LiteralType, NumberLiteralType, ObjectType, StringLiteralType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType }; export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts"; export class API { @@ -1063,6 +1064,22 @@ export class Checker { }); } + isArrayType(type: Type): boolean { + return this.client.apiRequest("isArrayType", { + snapshot: this.snapshotId, + project: this.projectId, + type: type.id, + }); + } + + isTupleType(type: Type): boolean { + return this.client.apiRequest("isTupleType", { + snapshot: this.snapshotId, + project: this.projectId, + type: type.id, + }); + } + getReturnTypeOfSignature(signature: Signature): Type | undefined { const data = this.client.apiRequest("getReturnTypeOfSignature", { snapshot: this.snapshotId, @@ -1138,6 +1155,87 @@ export class Checker { return data ? this.objectRegistry.getOrCreateType(data) : undefined; } + getBaseConstraintOfType(type: Type): Type | undefined { + const data = this.client.apiRequest("getBaseConstraintOfType", { + snapshot: this.snapshotId, + project: this.projectId, + type: type.id, + }); + return data ? this.objectRegistry.getOrCreateType(data) : undefined; + } + + getPropertyOfType(type: Type, name: string): Symbol | undefined { + const data = this.client.apiRequest("getPropertyOfType", { + snapshot: this.snapshotId, + project: this.projectId, + type: type.id, + name, + }); + return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; + } + + getConstantValue(node: Node): string | number | undefined { + const data = this.client.apiRequest("getConstantValue", { + snapshot: this.snapshotId, + project: this.projectId, + location: getNodeId(node), + }); + return data ?? undefined; + } + + getSignatureFromDeclaration(node: Node): Signature | undefined { + const data = this.client.apiRequest("getSignatureFromDeclaration", { + snapshot: this.snapshotId, + project: this.projectId, + location: getNodeId(node), + }); + return data ? this.objectRegistry.getOrCreateSignature(data) : undefined; + } + + getExportSpecifierLocalTargetSymbol(node: Node): Symbol | undefined { + const data = this.client.apiRequest("getExportSpecifierLocalTargetSymbol", { + snapshot: this.snapshotId, + project: this.projectId, + location: getNodeId(node), + }); + return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; + } + + getAliasedSymbol(symbol: Symbol): Symbol | undefined { + const data = this.client.apiRequest("getAliasedSymbol", { + snapshot: this.snapshotId, + project: this.projectId, + symbol: symbol.id, + }); + return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; + } + + getExportsOfModule(symbol: Symbol): readonly Symbol[] { + const data = this.client.apiRequest("getExportsOfModule", { + snapshot: this.snapshotId, + project: this.projectId, + symbol: symbol.id, + }); + return data ? data.map(d => this.objectRegistry.getOrCreateSymbol(d)) : []; + } + + getJsDocTagsOfSymbol(symbol: Symbol): readonly JSDocTagInfo[] { + const data = this.client.apiRequest("getJsDocTags", { + snapshot: this.snapshotId, + project: this.projectId, + symbol: symbol.id, + }); + return data ?? []; + } + + getDocumentationCommentOfSymbol(symbol: Symbol): string { + return this.client.apiRequest("getDocumentationComment", { + snapshot: this.snapshotId, + project: this.projectId, + symbol: symbol.id, + }); + } + getTypeArguments(type: Type): readonly Type[] { const data = this.client.apiRequest("getTypeArguments", { snapshot: this.snapshotId, @@ -1256,6 +1354,14 @@ export class Symbol { if (!this.exportSymbol) return this; return this.objectRegistry.fetchSymbol(this, "getExportSymbolOfSymbol", this.exportSymbol); } + + getJsDocTags(checker: Checker): readonly JSDocTagInfo[] { + return checker.getJsDocTagsOfSymbol(this); + } + + getDocumentationComment(checker: Checker): string { + return checker.getDocumentationCommentOfSymbol(this); + } } class TypeObject implements Type { diff --git a/_packages/native-preview/src/api/sync/types.ts b/_packages/native-preview/src/api/sync/types.ts index 86d31f003a..0815ffaa62 100644 --- a/_packages/native-preview/src/api/sync/types.ts +++ b/_packages/native-preview/src/api/sync/types.ts @@ -238,6 +238,16 @@ export interface IndexInfo { readonly declaration?: NodeHandle; } +/** + * A single JSDoc tag attached to a symbol — e.g. `@param`, `@returns`. + */ +export interface JSDocTagInfo { + /** The tag name, without the leading `@` — e.g. `"param"`. */ + readonly name: string; + /** The rendered tag text, if any — e.g. `"a the first number"` for `@param a the first number`. */ + readonly text?: string; +} + export interface CompletionEntryLabelDetails { detail?: string; description?: string; diff --git a/_packages/native-preview/test/async/api.test.ts b/_packages/native-preview/test/async/api.test.ts index 015cd521e3..90597db018 100644 --- a/_packages/native-preview/test/async/api.test.ts +++ b/_packages/native-preview/test/async/api.test.ts @@ -1946,6 +1946,140 @@ describe("readFile callback semantics", () => { }); }); +describe("Checker - isArrayType / isTupleType", () => { + test("number[] is array, not tuple", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export const xs: number[] = [];`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const src = `export const xs: number[] = [];`; + const pos = src.indexOf("xs"); + const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const type = await project.checker.getTypeOfSymbol(symbol); + assert.ok(type); + assert.equal(await project.checker.isArrayType(type), true); + assert.equal(await project.checker.isTupleType(type), false); + } + finally { + await api.close(); + } + }); + + test("readonly number[] is array", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export const xs: readonly number[] = [];`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const src = `export const xs: readonly number[] = [];`; + const pos = src.indexOf("xs"); + const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const type = await project.checker.getTypeOfSymbol(symbol); + assert.ok(type); + assert.equal(await project.checker.isArrayType(type), true); + assert.equal(await project.checker.isTupleType(type), false); + } + finally { + await api.close(); + } + }); + + test("Array is array, not tuple", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export const xs: Array = [];`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const src = `export const xs: Array = [];`; + const pos = src.indexOf("xs"); + const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const type = await project.checker.getTypeOfSymbol(symbol); + assert.ok(type); + assert.equal(await project.checker.isArrayType(type), true); + assert.equal(await project.checker.isTupleType(type), false); + } + finally { + await api.close(); + } + }); + + test("[number, string] is tuple, not array", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export const tup: [number, string] = [1, "a"];`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const src = `export const tup: [number, string] = [1, "a"];`; + const pos = src.indexOf("tup"); + const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const type = await project.checker.getTypeOfSymbol(symbol); + assert.ok(type); + assert.equal(await project.checker.isArrayType(type), false); + assert.equal(await project.checker.isTupleType(type), true); + } + finally { + await api.close(); + } + }); + + test("readonly [number, string] is tuple, not array", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export const tup: readonly [number, string] = [1, "a"];`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const src = `export const tup: readonly [number, string] = [1, "a"];`; + const pos = src.indexOf("tup"); + const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const type = await project.checker.getTypeOfSymbol(symbol); + assert.ok(type); + assert.equal(await project.checker.isArrayType(type), false); + assert.equal(await project.checker.isTupleType(type), true); + } + finally { + await api.close(); + } + }); + + test("string is neither array nor tuple", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export const str: string = "";`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const src = `export const str: string = "";`; + const pos = src.indexOf("str"); + const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const type = await project.checker.getTypeOfSymbol(symbol); + assert.ok(type); + assert.equal(await project.checker.isArrayType(type), false); + assert.equal(await project.checker.isTupleType(type), false); + } + finally { + await api.close(); + } + }); +}); + describe("Checker - getReturnTypeOfSignature", () => { test("returns the return type of a function signature", async () => { const api = spawnAPI({ @@ -2334,6 +2468,306 @@ describe("Checker - getTypeArguments", () => { }); }); +describe("Checker - getBaseConstraintOfType", () => { + test("returns the base constraint of a type parameter", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export function identity(x: T): T { return x; }`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const src = `export function identity(x: T): T { return x; }`; + const pos = src.indexOf("identity<"); + const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const type = await project.checker.getTypeOfSymbol(symbol); + assert.ok(type); + const sigs = await project.checker.getSignaturesOfType(type, SignatureKind.Call); + assert.ok(sigs.length > 0); + const typeParams = await sigs[0].getTypeParameters(); + const constraint = await project.checker.getBaseConstraintOfType(typeParams[0]); + assert.ok(constraint, "Should resolve a base constraint"); + assert.ok(constraint.flags & TypeFlags.String, `Expected string constraint, got flags ${constraint.flags}`); + } + finally { + await api.close(); + } + }); + + test("returns undefined for a non-instantiable type", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export const x: number = 1;`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const pos = `export const x: number = 1;`.indexOf("x:"); + const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const type = await project.checker.getTypeOfSymbol(symbol); + assert.ok(type); + const constraint = await project.checker.getBaseConstraintOfType(type); + assert.equal(constraint, undefined); + } + finally { + await api.close(); + } + }); +}); + +describe("Checker - getPropertyOfType", () => { + test("returns a named property symbol of a type", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": ` +export interface Person { + name: string; + age: number; +} +export declare const p: Person; +`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const src = `\nexport interface Person {\n name: string;\n age: number;\n}\nexport declare const p: Person;\n`; + const pos = src.indexOf("p: Person"); + const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const type = await project.checker.getTypeOfSymbol(symbol); + assert.ok(type); + const nameProp = await project.checker.getPropertyOfType(type, "name"); + assert.ok(nameProp, "Should find 'name' property"); + assert.equal(nameProp.name, "name"); + const missing = await project.checker.getPropertyOfType(type, "doesNotExist"); + assert.equal(missing, undefined); + } + finally { + await api.close(); + } + }); +}); + +describe("Checker - getConstantValue", () => { + test("returns numeric value of an enum member", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export enum E { A = 1, B = 2 }`, + }); + 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); + let memberB: Node | undefined; + sourceFile.forEachChild(function visit(node) { + if (node.kind === SyntaxKind.EnumMember) { + const text = sourceFile.text.slice(node.pos, node.end).trim(); + if (text.startsWith("B")) memberB = node; + } + node.forEachChild(visit); + }); + assert.ok(memberB, "Should find enum member B"); + const value = await project.checker.getConstantValue(memberB); + assert.equal(value, 2); + } + finally { + await api.close(); + } + }); + + test("returns string value of a string-initialized enum member", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export enum Color { Red = "red" }`, + }); + 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); + let member: Node | undefined; + sourceFile.forEachChild(function visit(node) { + if (node.kind === SyntaxKind.EnumMember) member = node; + node.forEachChild(visit); + }); + assert.ok(member); + const value = await project.checker.getConstantValue(member); + assert.equal(value, "red"); + } + finally { + await api.close(); + } + }); +}); + +describe("Checker - getSignatureFromDeclaration", () => { + test("returns the signature of a function declaration", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export function add(a: number, b: number): number { return a + b; }`, + }); + 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); + let funcDecl: Node | undefined; + sourceFile.forEachChild(function visit(node) { + if (isFunctionDeclaration(node)) funcDecl = node; + node.forEachChild(visit); + }); + assert.ok(funcDecl, "Should find the function declaration"); + const sig = await project.checker.getSignatureFromDeclaration(funcDecl); + assert.ok(sig, "Should resolve a signature"); + assert.equal(sig.parameters.length, 2); + const returnType = await project.checker.getReturnTypeOfSignature(sig); + assert.ok(returnType); + assert.ok(returnType.flags & TypeFlags.Number); + } + finally { + await api.close(); + } + }); +}); + +describe("Checker - getExportSpecifierLocalTargetSymbol", () => { + test("resolves the local target of an export specifier", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": ` +const value = 42; +export { value as renamed }; +`, + }); + 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); + let exportSpecifier: Node | undefined; + sourceFile.forEachChild(function visit(node) { + if (node.kind === SyntaxKind.ExportSpecifier) exportSpecifier = node; + node.forEachChild(visit); + }); + assert.ok(exportSpecifier, "Should find the export specifier"); + const target = await project.checker.getExportSpecifierLocalTargetSymbol(exportSpecifier); + assert.ok(target, "Should resolve a local target symbol"); + assert.equal(target.name, "value"); + } + finally { + await api.close(); + } + }); +}); + +describe("Checker - getAliasedSymbol", () => { + test("resolves an import alias to its target symbol", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/foo.ts": `export const foo = 42;`, + "/src/main.ts": `import { foo } from "./foo";\nexport const usage = foo;`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const pos = `import { foo } from "./foo";`.indexOf("foo }"); + const aliasSymbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(aliasSymbol); + assert.ok(aliasSymbol.flags & SymbolFlags.Alias, "Import binding should be an alias"); + const aliased = await project.checker.getAliasedSymbol(aliasSymbol); + assert.ok(aliased, "Should resolve the aliased symbol"); + assert.equal(aliased.name, "foo"); + assert.ok(!(aliased.flags & SymbolFlags.Alias), "Target should not be an alias"); + } + finally { + await api.close(); + } + }); +}); + +describe("Checker - getExportsOfModule", () => { + test("returns all exports including re-exports via 'export *'", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/inner.ts": `export const innerValue = 1;`, + "/src/index.ts": ` +export const direct = 1; +export * from "./inner"; +`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const sourceFile = await project.program.getSourceFile("/src/index.ts"); + assert.ok(sourceFile); + const moduleSymbol = await project.checker.getSymbolAtLocation(sourceFile); + assert.ok(moduleSymbol, "Source file should have a module symbol"); + const exports = await project.checker.getExportsOfModule(moduleSymbol); + const names = exports.map(e => e.name); + assert.ok(names.includes("direct"), "should include directly-declared export"); + assert.ok(names.includes("innerValue"), "should include 'export *' re-export"); + } + finally { + await api.close(); + } + }); +}); + +describe("Symbol - getDocumentationComment and getJsDocTags", () => { + const docFiles = { + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": ` +/** + * Adds two numbers together. + * @param a the first number + * @returns the sum + */ +export function add(a: number, b: number): number { return a + b; } +`, + }; + + test("getDocumentationComment returns the leading comment text", async () => { + const api = spawnAPI(docFiles); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const pos = docFiles["/src/main.ts"].indexOf("add(a"); + const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const doc = await symbol.getDocumentationComment(project.checker); + assert.ok(doc.includes("Adds two numbers together"), `Expected documentation, got: ${doc}`); + assert.ok(!doc.includes("@param"), "Documentation comment should not include tags"); + } + finally { + await api.close(); + } + }); + + test("getJsDocTags returns structured tag name/text pairs", async () => { + const api = spawnAPI(docFiles); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const pos = docFiles["/src/main.ts"].indexOf("add(a"); + const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const tags = await symbol.getJsDocTags(project.checker); + const param = tags.find(t => t.name === "param"); + assert.ok(param, `Expected a @param tag, got: ${JSON.stringify(tags)}`); + assert.equal(param.text, "a the first number"); + const returns = tags.find(t => t.name === "returns"); + assert.ok(returns, `Expected a @returns tag, got: ${JSON.stringify(tags)}`); + assert.equal(returns.text, "the sum"); + } + finally { + await api.close(); + } + }); +}); + describe("TypeParameter - isThisType", () => { test("isThisType is true for the polymorphic 'this' type in a class method", async () => { const src = `\nexport class Builder {\n setName(name: string): this { return this; }\n}\n`; diff --git a/_packages/native-preview/test/sync/api.test.ts b/_packages/native-preview/test/sync/api.test.ts index 74041ca1e8..b410b887e7 100644 --- a/_packages/native-preview/test/sync/api.test.ts +++ b/_packages/native-preview/test/sync/api.test.ts @@ -1954,6 +1954,140 @@ describe("readFile callback semantics", () => { }); }); +describe("Checker - isArrayType / isTupleType", () => { + test("number[] is array, not tuple", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export const xs: number[] = [];`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const src = `export const xs: number[] = [];`; + const pos = src.indexOf("xs"); + const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const type = project.checker.getTypeOfSymbol(symbol); + assert.ok(type); + assert.equal(project.checker.isArrayType(type), true); + assert.equal(project.checker.isTupleType(type), false); + } + finally { + api.close(); + } + }); + + test("readonly number[] is array", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export const xs: readonly number[] = [];`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const src = `export const xs: readonly number[] = [];`; + const pos = src.indexOf("xs"); + const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const type = project.checker.getTypeOfSymbol(symbol); + assert.ok(type); + assert.equal(project.checker.isArrayType(type), true); + assert.equal(project.checker.isTupleType(type), false); + } + finally { + api.close(); + } + }); + + test("Array is array, not tuple", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export const xs: Array = [];`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const src = `export const xs: Array = [];`; + const pos = src.indexOf("xs"); + const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const type = project.checker.getTypeOfSymbol(symbol); + assert.ok(type); + assert.equal(project.checker.isArrayType(type), true); + assert.equal(project.checker.isTupleType(type), false); + } + finally { + api.close(); + } + }); + + test("[number, string] is tuple, not array", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export const tup: [number, string] = [1, "a"];`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const src = `export const tup: [number, string] = [1, "a"];`; + const pos = src.indexOf("tup"); + const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const type = project.checker.getTypeOfSymbol(symbol); + assert.ok(type); + assert.equal(project.checker.isArrayType(type), false); + assert.equal(project.checker.isTupleType(type), true); + } + finally { + api.close(); + } + }); + + test("readonly [number, string] is tuple, not array", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export const tup: readonly [number, string] = [1, "a"];`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const src = `export const tup: readonly [number, string] = [1, "a"];`; + const pos = src.indexOf("tup"); + const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const type = project.checker.getTypeOfSymbol(symbol); + assert.ok(type); + assert.equal(project.checker.isArrayType(type), false); + assert.equal(project.checker.isTupleType(type), true); + } + finally { + api.close(); + } + }); + + test("string is neither array nor tuple", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export const str: string = "";`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const src = `export const str: string = "";`; + const pos = src.indexOf("str"); + const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const type = project.checker.getTypeOfSymbol(symbol); + assert.ok(type); + assert.equal(project.checker.isArrayType(type), false); + assert.equal(project.checker.isTupleType(type), false); + } + finally { + api.close(); + } + }); +}); + describe("Checker - getReturnTypeOfSignature", () => { test("returns the return type of a function signature", () => { const api = spawnAPI({ @@ -2342,6 +2476,306 @@ describe("Checker - getTypeArguments", () => { }); }); +describe("Checker - getBaseConstraintOfType", () => { + test("returns the base constraint of a type parameter", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export function identity(x: T): T { return x; }`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const src = `export function identity(x: T): T { return x; }`; + const pos = src.indexOf("identity<"); + const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const type = project.checker.getTypeOfSymbol(symbol); + assert.ok(type); + const sigs = project.checker.getSignaturesOfType(type, SignatureKind.Call); + assert.ok(sigs.length > 0); + const typeParams = sigs[0].getTypeParameters(); + const constraint = project.checker.getBaseConstraintOfType(typeParams[0]); + assert.ok(constraint, "Should resolve a base constraint"); + assert.ok(constraint.flags & TypeFlags.String, `Expected string constraint, got flags ${constraint.flags}`); + } + finally { + api.close(); + } + }); + + test("returns undefined for a non-instantiable type", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export const x: number = 1;`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const pos = `export const x: number = 1;`.indexOf("x:"); + const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const type = project.checker.getTypeOfSymbol(symbol); + assert.ok(type); + const constraint = project.checker.getBaseConstraintOfType(type); + assert.equal(constraint, undefined); + } + finally { + api.close(); + } + }); +}); + +describe("Checker - getPropertyOfType", () => { + test("returns a named property symbol of a type", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": ` +export interface Person { + name: string; + age: number; +} +export declare const p: Person; +`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const src = `\nexport interface Person {\n name: string;\n age: number;\n}\nexport declare const p: Person;\n`; + const pos = src.indexOf("p: Person"); + const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const type = project.checker.getTypeOfSymbol(symbol); + assert.ok(type); + const nameProp = project.checker.getPropertyOfType(type, "name"); + assert.ok(nameProp, "Should find 'name' property"); + assert.equal(nameProp.name, "name"); + const missing = project.checker.getPropertyOfType(type, "doesNotExist"); + assert.equal(missing, undefined); + } + finally { + api.close(); + } + }); +}); + +describe("Checker - getConstantValue", () => { + test("returns numeric value of an enum member", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export enum E { A = 1, B = 2 }`, + }); + 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); + let memberB: Node | undefined; + sourceFile.forEachChild(function visit(node) { + if (node.kind === SyntaxKind.EnumMember) { + const text = sourceFile.text.slice(node.pos, node.end).trim(); + if (text.startsWith("B")) memberB = node; + } + node.forEachChild(visit); + }); + assert.ok(memberB, "Should find enum member B"); + const value = project.checker.getConstantValue(memberB); + assert.equal(value, 2); + } + finally { + api.close(); + } + }); + + test("returns string value of a string-initialized enum member", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export enum Color { Red = "red" }`, + }); + 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); + let member: Node | undefined; + sourceFile.forEachChild(function visit(node) { + if (node.kind === SyntaxKind.EnumMember) member = node; + node.forEachChild(visit); + }); + assert.ok(member); + const value = project.checker.getConstantValue(member); + assert.equal(value, "red"); + } + finally { + api.close(); + } + }); +}); + +describe("Checker - getSignatureFromDeclaration", () => { + test("returns the signature of a function declaration", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export function add(a: number, b: number): number { return a + b; }`, + }); + 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); + let funcDecl: Node | undefined; + sourceFile.forEachChild(function visit(node) { + if (isFunctionDeclaration(node)) funcDecl = node; + node.forEachChild(visit); + }); + assert.ok(funcDecl, "Should find the function declaration"); + const sig = project.checker.getSignatureFromDeclaration(funcDecl); + assert.ok(sig, "Should resolve a signature"); + assert.equal(sig.parameters.length, 2); + const returnType = project.checker.getReturnTypeOfSignature(sig); + assert.ok(returnType); + assert.ok(returnType.flags & TypeFlags.Number); + } + finally { + api.close(); + } + }); +}); + +describe("Checker - getExportSpecifierLocalTargetSymbol", () => { + test("resolves the local target of an export specifier", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": ` +const value = 42; +export { value as renamed }; +`, + }); + 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); + let exportSpecifier: Node | undefined; + sourceFile.forEachChild(function visit(node) { + if (node.kind === SyntaxKind.ExportSpecifier) exportSpecifier = node; + node.forEachChild(visit); + }); + assert.ok(exportSpecifier, "Should find the export specifier"); + const target = project.checker.getExportSpecifierLocalTargetSymbol(exportSpecifier); + assert.ok(target, "Should resolve a local target symbol"); + assert.equal(target.name, "value"); + } + finally { + api.close(); + } + }); +}); + +describe("Checker - getAliasedSymbol", () => { + test("resolves an import alias to its target symbol", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/foo.ts": `export const foo = 42;`, + "/src/main.ts": `import { foo } from "./foo";\nexport const usage = foo;`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const pos = `import { foo } from "./foo";`.indexOf("foo }"); + const aliasSymbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(aliasSymbol); + assert.ok(aliasSymbol.flags & SymbolFlags.Alias, "Import binding should be an alias"); + const aliased = project.checker.getAliasedSymbol(aliasSymbol); + assert.ok(aliased, "Should resolve the aliased symbol"); + assert.equal(aliased.name, "foo"); + assert.ok(!(aliased.flags & SymbolFlags.Alias), "Target should not be an alias"); + } + finally { + api.close(); + } + }); +}); + +describe("Checker - getExportsOfModule", () => { + test("returns all exports including re-exports via 'export *'", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/inner.ts": `export const innerValue = 1;`, + "/src/index.ts": ` +export const direct = 1; +export * from "./inner"; +`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const sourceFile = project.program.getSourceFile("/src/index.ts"); + assert.ok(sourceFile); + const moduleSymbol = project.checker.getSymbolAtLocation(sourceFile); + assert.ok(moduleSymbol, "Source file should have a module symbol"); + const exports = project.checker.getExportsOfModule(moduleSymbol); + const names = exports.map(e => e.name); + assert.ok(names.includes("direct"), "should include directly-declared export"); + assert.ok(names.includes("innerValue"), "should include 'export *' re-export"); + } + finally { + api.close(); + } + }); +}); + +describe("Symbol - getDocumentationComment and getJsDocTags", () => { + const docFiles = { + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": ` +/** + * Adds two numbers together. + * @param a the first number + * @returns the sum + */ +export function add(a: number, b: number): number { return a + b; } +`, + }; + + test("getDocumentationComment returns the leading comment text", () => { + const api = spawnAPI(docFiles); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const pos = docFiles["/src/main.ts"].indexOf("add(a"); + const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const doc = symbol.getDocumentationComment(project.checker); + assert.ok(doc.includes("Adds two numbers together"), `Expected documentation, got: ${doc}`); + assert.ok(!doc.includes("@param"), "Documentation comment should not include tags"); + } + finally { + api.close(); + } + }); + + test("getJsDocTags returns structured tag name/text pairs", () => { + const api = spawnAPI(docFiles); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const pos = docFiles["/src/main.ts"].indexOf("add(a"); + const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); + assert.ok(symbol); + const tags = symbol.getJsDocTags(project.checker); + const param = tags.find(t => t.name === "param"); + assert.ok(param, `Expected a @param tag, got: ${JSON.stringify(tags)}`); + assert.equal(param.text, "a the first number"); + const returns = tags.find(t => t.name === "returns"); + assert.ok(returns, `Expected a @returns tag, got: ${JSON.stringify(tags)}`); + assert.equal(returns.text, "the sum"); + } + finally { + api.close(); + } + }); +}); + describe("TypeParameter - isThisType", () => { test("isThisType is true for the polymorphic 'this' type in a class method", () => { const src = `\nexport class Builder {\n setName(name: string): this { return this; }\n}\n`; diff --git a/internal/api/proto.go b/internal/api/proto.go index 8128018f3e..01efb9da38 100644 --- a/internal/api/proto.go +++ b/internal/api/proto.go @@ -126,9 +126,20 @@ const ( MethodGetTypePredicateOfSignature Method = "getTypePredicateOfSignature" MethodGetBaseTypes Method = "getBaseTypes" MethodGetPropertiesOfType Method = "getPropertiesOfType" + MethodGetPropertyOfType Method = "getPropertyOfType" MethodGetIndexInfosOfType Method = "getIndexInfosOfType" MethodGetConstraintOfTypeParameter Method = "getConstraintOfTypeParameter" + MethodGetBaseConstraintOfType Method = "getBaseConstraintOfType" MethodGetTypeArguments Method = "getTypeArguments" + MethodGetConstantValue Method = "getConstantValue" + MethodGetSignatureFromDeclaration Method = "getSignatureFromDeclaration" + MethodGetExportSpecifierLocalTarget Method = "getExportSpecifierLocalTargetSymbol" + MethodGetAliasedSymbol Method = "getAliasedSymbol" + MethodGetExportsOfModule Method = "getExportsOfModule" + MethodGetJSDocTags Method = "getJsDocTags" + MethodGetDocumentationComment Method = "getDocumentationComment" + MethodIsArrayType Method = "isArrayType" + MethodIsTupleType Method = "isTupleType" // Reference methods MethodGetReferencesToSymbolInFile Method = "getReferencesToSymbolInFile" @@ -373,9 +384,20 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetTypePredicateOfSignature: unmarshallerFor[CheckerSignatureParams], MethodGetBaseTypes: unmarshallerFor[CheckerTypeParams], MethodGetPropertiesOfType: unmarshallerFor[CheckerTypeParams], + MethodGetPropertyOfType: unmarshallerFor[GetPropertyOfTypeParams], MethodGetIndexInfosOfType: unmarshallerFor[CheckerTypeParams], MethodGetConstraintOfTypeParameter: unmarshallerFor[CheckerTypeParams], + MethodGetBaseConstraintOfType: unmarshallerFor[CheckerTypeParams], MethodGetTypeArguments: unmarshallerFor[CheckerTypeParams], + MethodGetConstantValue: unmarshallerFor[CheckerNodeParams], + MethodGetSignatureFromDeclaration: unmarshallerFor[CheckerNodeParams], + MethodGetExportSpecifierLocalTarget: unmarshallerFor[CheckerNodeParams], + MethodGetAliasedSymbol: unmarshallerFor[CheckerSymbolParams], + MethodGetExportsOfModule: unmarshallerFor[CheckerSymbolParams], + MethodGetJSDocTags: unmarshallerFor[CheckerSymbolParams], + MethodGetDocumentationComment: unmarshallerFor[CheckerSymbolParams], + MethodIsArrayType: unmarshallerFor[CheckerTypeParams], + MethodIsTupleType: unmarshallerFor[CheckerTypeParams], MethodGetReferencesToSymbolInFile: unmarshallerFor[GetReferencesToSymbolInFileParams], MethodGetReferencedSymbolsForNode: unmarshallerFor[GetReferencedSymbolsForNodeParams], MethodGetSignatureUsages: unmarshallerFor[GetSignatureUsagesParams], @@ -935,6 +957,35 @@ type CheckerTypeParams struct { Type TypeID `json:"type"` } +// GetPropertyOfTypeParams are parameters for getPropertyOfType (a named property of a type). +type GetPropertyOfTypeParams struct { + Snapshot SnapshotID `json:"snapshot"` + Project ProjectID `json:"project"` + Type TypeID `json:"type"` + Name string `json:"name"` +} + +// CheckerNodeParams are parameters for checker methods that operate on a node location. +type CheckerNodeParams struct { + Snapshot SnapshotID `json:"snapshot"` + Project ProjectID `json:"project"` + Location NodeHandle `json:"location"` +} + +// CheckerSymbolParams are parameters for checker methods that operate on a symbol. +type CheckerSymbolParams struct { + Snapshot SnapshotID `json:"snapshot"` + Project ProjectID `json:"project"` + Symbol SymbolID `json:"symbol"` +} + +// JSDocTagInfo is a single JSDoc tag, mirroring Strada's JSDocTagInfo but with the tag text +// rendered as a plain string rather than SymbolDisplayPart[]. +type JSDocTagInfo struct { + Name string `json:"name"` + Text string `json:"text,omitempty"` +} + // CheckerSignatureParams are parameters for checker methods that operate on a signature. type CheckerSignatureParams struct { Snapshot SnapshotID `json:"snapshot"` diff --git a/internal/api/session.go b/internal/api/session.go index 1a8fd17bb3..a517998850 100644 --- a/internal/api/session.go +++ b/internal/api/session.go @@ -604,12 +604,34 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetBaseTypes(ctx, parsed.(*CheckerTypeParams)) case string(MethodGetPropertiesOfType): return s.handleGetPropertiesOfType(ctx, parsed.(*CheckerTypeParams)) + case string(MethodGetPropertyOfType): + return s.handleGetPropertyOfType(ctx, parsed.(*GetPropertyOfTypeParams)) case string(MethodGetIndexInfosOfType): return s.handleGetIndexInfosOfType(ctx, parsed.(*CheckerTypeParams)) case string(MethodGetConstraintOfTypeParameter): return s.handleGetConstraintOfTypeParameter(ctx, parsed.(*CheckerTypeParams)) + case string(MethodGetBaseConstraintOfType): + return s.handleGetBaseConstraintOfType(ctx, parsed.(*CheckerTypeParams)) case string(MethodGetTypeArguments): return s.handleGetTypeArguments(ctx, parsed.(*CheckerTypeParams)) + case string(MethodGetConstantValue): + return s.handleGetConstantValue(ctx, parsed.(*CheckerNodeParams)) + case string(MethodGetSignatureFromDeclaration): + return s.handleGetSignatureFromDeclaration(ctx, parsed.(*CheckerNodeParams)) + case string(MethodGetExportSpecifierLocalTarget): + return s.handleGetExportSpecifierLocalTargetSymbol(ctx, parsed.(*CheckerNodeParams)) + case string(MethodGetAliasedSymbol): + return s.handleGetAliasedSymbol(ctx, parsed.(*CheckerSymbolParams)) + case string(MethodGetExportsOfModule): + return s.handleGetExportsOfModule(ctx, parsed.(*CheckerSymbolParams)) + case string(MethodGetJSDocTags): + return s.handleGetJSDocTags(ctx, parsed.(*CheckerSymbolParams)) + case string(MethodGetDocumentationComment): + return s.handleGetDocumentationComment(ctx, parsed.(*CheckerSymbolParams)) + case string(MethodIsArrayType): + return s.handleIsArrayType(ctx, parsed.(*CheckerTypeParams)) + case string(MethodIsTupleType): + return s.handleIsTupleType(ctx, parsed.(*CheckerTypeParams)) case string(MethodGetAnyType): return s.handleGetIntrinsicType(ctx, parsed.(*GetIntrinsicTypeParams), (*checker.Checker).GetAnyType) case string(MethodGetStringType): @@ -2002,6 +2024,38 @@ func (s *Session) handleGetTypePredicateOfSignature(ctx context.Context, params return resp, nil } +// handleIsArrayType returns whether a type is Array or ReadonlyArray. +func (s *Session) handleIsArrayType(ctx context.Context, params *CheckerTypeParams) (bool, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return false, err + } + defer setup.done() + + t, err := setup.resolveTypeHandle(params.Type) + if err != nil { + return false, err + } + + return setup.checker.IsArrayType(t), nil +} + +// handleIsTupleType returns whether a type is a tuple type. +func (s *Session) handleIsTupleType(ctx context.Context, params *CheckerTypeParams) (bool, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return false, err + } + defer setup.done() + + t, err := setup.resolveTypeHandle(params.Type) + if err != nil { + return false, err + } + + return checker.IsTupleType(t), nil +} + // handleGetBaseTypes returns the base types of an interface/class type. func (s *Session) handleGetBaseTypes(ctx context.Context, params *CheckerTypeParams) ([]*TypeResponse, error) { setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) @@ -2108,6 +2162,225 @@ func (s *Session) handleGetConstraintOfTypeParameter(ctx context.Context, params return setup.newTypeResponse(constraint), nil } +// handleGetBaseConstraintOfType returns the base constraint of an instantiable type. +func (s *Session) handleGetBaseConstraintOfType(ctx context.Context, params *CheckerTypeParams) (*TypeResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + t, err := setup.resolveTypeHandle(params.Type) + if err != nil { + return nil, err + } + + constraint := setup.checker.GetBaseConstraintOfType(t) + if constraint == nil { + return nil, nil + } + + return setup.newTypeResponse(constraint), nil +} + +// handleGetPropertyOfType returns a named property symbol of a type. +func (s *Session) handleGetPropertyOfType(ctx context.Context, params *GetPropertyOfTypeParams) (*SymbolResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + t, err := setup.resolveTypeHandle(params.Type) + if err != nil { + return nil, err + } + + prop := setup.checker.GetPropertyOfType(t, params.Name) + if prop == nil { + return nil, nil + } + + return setup.newSymbolResponse(prop), nil +} + +// handleGetConstantValue returns the constant value of an enum member or const enum access. +func (s *Session) handleGetConstantValue(ctx context.Context, params *CheckerNodeParams) (any, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + node, err := setup.sd.resolveNodeHandle(setup.program, params.Location) + if err != nil { + return nil, err + } + if node == nil { + return nil, nil + } + + return literalValueToJSON(setup.checker.GetConstantValue(node)), nil +} + +// handleGetSignatureFromDeclaration returns the signature of a function-like declaration. +func (s *Session) handleGetSignatureFromDeclaration(ctx context.Context, params *CheckerNodeParams) (*SignatureResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + node, err := setup.sd.resolveNodeHandle(setup.program, params.Location) + if err != nil { + return nil, err + } + if node == nil { + return nil, nil + } + + sig := setup.checker.GetSignatureFromDeclaration(node) + if sig == nil { + return nil, nil + } + + return setup.newSignatureResponse(sig), nil +} + +// handleGetExportSpecifierLocalTargetSymbol returns the local target symbol of an export specifier. +func (s *Session) handleGetExportSpecifierLocalTargetSymbol(ctx context.Context, params *CheckerNodeParams) (*SymbolResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + node, err := setup.sd.resolveNodeHandle(setup.program, params.Location) + if err != nil { + return nil, err + } + if node == nil { + return nil, nil + } + + symbol := setup.checker.GetExportSpecifierLocalTargetSymbol(node) + if symbol == nil { + return nil, nil + } + + return setup.newSymbolResponse(symbol), nil +} + +// handleGetAliasedSymbol resolves an alias symbol to its target. +func (s *Session) handleGetAliasedSymbol(ctx context.Context, params *CheckerSymbolParams) (*SymbolResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + symbol, err := setup.resolveSymbolHandle(params.Symbol) + if err != nil { + return nil, err + } + if symbol == nil { + return nil, nil + } + + aliased := setup.checker.GetAliasedSymbol(symbol) + if aliased == nil { + return nil, nil + } + + return setup.newSymbolResponse(aliased), nil +} + +// handleGetExportsOfModule returns the resolved exports of a module symbol, +// including those introduced by `export *` and re-exports. +func (s *Session) handleGetExportsOfModule(ctx context.Context, params *CheckerSymbolParams) ([]*SymbolResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + symbol, err := setup.resolveSymbolHandle(params.Symbol) + if err != nil { + return nil, err + } + if symbol == nil { + return nil, nil + } + + exports := setup.checker.GetExportsOfModule(symbol) + if len(exports) == 0 { + return nil, nil + } + + results := make([]*SymbolResponse, len(exports)) + for i, exp := range exports { + results[i] = setup.newSymbolResponse(exp) + } + + return results, nil +} + +// handleGetJSDocTags returns the JSDoc tags of a symbol as structured name/text pairs. +func (s *Session) handleGetJSDocTags(ctx context.Context, params *CheckerSymbolParams) ([]*JSDocTagInfo, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + symbol, err := setup.resolveSymbolHandle(params.Symbol) + if err != nil { + return nil, err + } + if symbol == nil { + return nil, nil + } + + langSvc, err := s.setupLanguageService(setup.sd, setup.program, params.Project, "") + if err != nil { + return nil, err + } + + tags := langSvc.GetSymbolJSDocTags(symbol) + if len(tags) == 0 { + return nil, nil + } + results := make([]*JSDocTagInfo, len(tags)) + for i, tag := range tags { + results[i] = &JSDocTagInfo{Name: tag.Name, Text: tag.Text} + } + return results, nil +} + +// handleGetDocumentationComment returns the rendered documentation comment of a symbol as plain text. +func (s *Session) handleGetDocumentationComment(ctx context.Context, params *CheckerSymbolParams) (string, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return "", err + } + defer setup.done() + + symbol, err := setup.resolveSymbolHandle(params.Symbol) + if err != nil { + return "", err + } + if symbol == nil { + return "", nil + } + + langSvc, err := s.setupLanguageService(setup.sd, setup.program, params.Project, "") + if err != nil { + return "", err + } + + return langSvc.GetSymbolDocumentationComment(setup.checker, symbol), nil +} + // handleGetTypeArguments returns the type arguments of a type reference. func (s *Session) handleGetTypeArguments(ctx context.Context, params *CheckerTypeParams) ([]*TypeResponse, error) { setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) diff --git a/internal/checker/exports.go b/internal/checker/exports.go index 516f5f149b..d6a84df0dc 100644 --- a/internal/checker/exports.go +++ b/internal/checker/exports.go @@ -193,6 +193,10 @@ func IsTupleType(t *Type) bool { return isTupleType(t) } +func (c *Checker) IsArrayType(t *Type) bool { + return c.isArrayType(t) +} + func (c *Checker) GetReturnTypeOfSignature(sig *Signature) *Type { return c.getReturnTypeOfSignature(sig) } diff --git a/internal/ls/jsdoc.go b/internal/ls/jsdoc.go new file mode 100644 index 0000000000..a5992fa735 --- /dev/null +++ b/internal/ls/jsdoc.go @@ -0,0 +1,161 @@ +package ls + +import ( + "slices" + "strings" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/checker" + "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/scanner" +) + +// JSDocTagInfo mirrors Strada's `JSDocTagInfo`, but renders the tag's text as a +// plain string instead of `SymbolDisplayPart[]`. +type JSDocTagInfo struct { + Name string + Text string +} + +// GetSymbolDocumentationComment renders a symbol's documentation comment as plain text. +// It backs the API's Symbol.getDocumentationComment and mirrors Strada's +// getJsDocCommentsFromDeclarations: comments are gathered from each unique declaration, +// deduplicated, and joined with line breaks. Like Strada, it does not resolve aliases — +// consumers resolve aliases themselves (via getAliasedSymbol) and re-query if desired. +func (l *LanguageService) GetSymbolDocumentationComment(c *checker.Checker, symbol *ast.Symbol) string { + if symbol == nil { + return "" + } + var parts []string + var seen collections.Set[*ast.Node] + for _, decl := range symbol.Declarations { + if decl == nil { + continue + } + if !seen.AddIfAbsent(decl) { + continue + } + if doc := l.getDocumentationFromDeclaration(c, symbol, decl, decl, lsproto.MarkupKindPlainText, true /*commentOnly*/); doc != "" && !slices.Contains(parts, doc) { + parts = append(parts, doc) + } + } + return strings.Join(parts, "\n") +} + +// GetSymbolJSDocTags collects a symbol's JSDoc tags. It backs the API's Symbol.getJsDocTags +// and mirrors Strada's getJsDocTagsFromDeclarations, except each tag's text is rendered as a +// plain string rather than SymbolDisplayPart[]. Tags with no text have an empty Text field. +func (l *LanguageService) GetSymbolJSDocTags(symbol *ast.Symbol) []JSDocTagInfo { + if symbol == nil { + return nil + } + var infos []JSDocTagInfo + var seen collections.Set[*ast.Node] + for _, decl := range symbol.Declarations { + if decl == nil { + continue + } + if !seen.AddIfAbsent(decl) { + continue + } + tags := declarationJSDocTags(decl) + // Skip comments containing @typedef/@callback since they're not associated with a + // particular declaration, unless they also carry @param/@return (treated as local docs). + hasTypedef := core.Some(tags, func(t *ast.Node) bool { + return t.Kind == ast.KindJSDocTypedefTag || t.Kind == ast.KindJSDocCallbackTag + }) + hasParamOrReturn := core.Some(tags, func(t *ast.Node) bool { + return t.Kind == ast.KindJSDocParameterTag || t.Kind == ast.KindJSDocReturnTag + }) + if hasTypedef && !hasParamOrReturn { + continue + } + for _, tag := range tags { + infos = append(infos, JSDocTagInfo{Name: tag.TagName().Text(), Text: getJSDocTagText(tag)}) + } + } + return infos +} + +// declarationJSDocTags returns the JSDoc tags associated with a declaration, walking the +// JSDoc comment location chain like the checker's getAllJSDocTags. +func declarationJSDocTags(node *ast.Node) []*ast.Node { + if node.Flags&ast.NodeFlagsJSDoc == 0 { + for current := node; current != nil; current = ast.GetNextJSDocCommentLocation(current) { + jsdocs := current.JSDoc(nil) + if len(jsdocs) == 0 { + continue + } + lastJSDoc := jsdocs[len(jsdocs)-1].AsJSDoc() + if lastJSDoc.Tags != nil { + return lastJSDoc.Tags.Nodes + } + } + } + return nil +} + +// getJSDocTagText renders the text of a single JSDoc tag as a plain string, mirroring +// Strada's getCommentDisplayParts collapsed from SymbolDisplayPart[] to a string. +func getJSDocTagText(tag *ast.Node) string { + comment := scanner.GetTextOfJSDocComment(tag.CommentList()) + addComment := func(s string) string { + if comment == "" { + return s + } + return s + " " + comment + } + switch tag.Kind { + case ast.KindJSDocThrowsTag: + if te := tag.AsJSDocThrowsTag().TypeExpression; te != nil { + return addComment(scanner.GetTextOfNode(te)) + } + return comment + case ast.KindJSDocImplementsTag: + return addComment(scanner.GetTextOfNode(tag.AsJSDocImplementsTag().ClassName)) + case ast.KindJSDocAugmentsTag: + return addComment(scanner.GetTextOfNode(tag.AsJSDocAugmentsTag().ClassName)) + case ast.KindJSDocTemplateTag: + templateTag := tag.AsJSDocTemplateTag() + var b strings.Builder + if templateTag.Constraint != nil { + b.WriteString(scanner.GetTextOfNode(templateTag.Constraint)) + } + if templateTag.TypeParameters != nil { + for i, tp := range templateTag.TypeParameters.Nodes { + if i == 0 && b.Len() != 0 { + b.WriteString(" ") + } + if i != 0 { + b.WriteString(", ") + } + b.WriteString(scanner.GetTextOfNode(tp)) + } + } + if comment != "" { + if b.Len() != 0 { + b.WriteString(" ") + } + b.WriteString(comment) + } + return b.String() + case ast.KindJSDocTypeTag: + return addComment(scanner.GetTextOfNode(tag.AsJSDocTypeTag().TypeExpression)) + case ast.KindJSDocSatisfiesTag: + return addComment(scanner.GetTextOfNode(tag.AsJSDocSatisfiesTag().TypeExpression)) + case ast.KindJSDocSeeTag: + if ne := tag.AsJSDocSeeTag().NameExpression; ne != nil { + return addComment(scanner.GetTextOfNode(ne)) + } + return comment + case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag: + if name := tag.Name(); name != nil { + return addComment(scanner.GetTextOfNode(name)) + } + return comment + default: + return comment + } +}