diff --git a/_packages/api/package.json b/_packages/api/package.json index f89cffe9a3..61d5f67257 100644 --- a/_packages/api/package.json +++ b/_packages/api/package.json @@ -39,7 +39,8 @@ "build": "tsc -b", "build:test": "tsc -b test", "bench": "node --experimental-strip-types --no-warnings --conditions @typescript/source test/api.bench.ts", - "test": "node --test --experimental-strip-types --no-warnings --conditions @typescript/source ./test/**/*.test.ts" + "test": "node --test --experimental-strip-types --no-warnings --conditions @typescript/source ./test/**/*.test.ts", + "test:lspapi": "node --test --test-name-pattern=.+ --experimental-strip-types --no-warnings --conditions @typescript/source ./test/lspApi.test.ts" }, "devDependencies": { "tinybench": "^3.1.1" diff --git a/_packages/api/test/lspApi.test.ts b/_packages/api/test/lspApi.test.ts new file mode 100644 index 0000000000..b843f6526d --- /dev/null +++ b/_packages/api/test/lspApi.test.ts @@ -0,0 +1,1074 @@ +import assert from "node:assert" +import { Buffer } from "node:buffer" +import fs from "node:fs" +import subprocess from "node:child_process" +import path, { resolve } from "node:path" +import { afterEach, before, beforeEach, suite, test, type TestContext } from "node:test" +import url from "node:url" +import type { Range } from "vscode-languageserver-types" +import type { Type, Symbol, Node, TypeReference, GenericType, UnionType, LiteralType, IndexType, IndexedAccessType, ConditionalType, SubstitutionType, ObjectType, PseudoBigInt, BigIntLiteralType, TemplateLiteral, TemplateLiteralType, TupleType, Signature, IndexInfo, __String, Declaration, SignatureDeclaration, SourceFile, LineAndCharacter, IndexSignatureDeclaration, server } from "typescript" +// @ts-expect-error +import { TypeFlags } from "../src/typeFlags.ts" +// @ts-expect-error +import { SymbolFlags } from "../src/symbolFlags.ts" + + +suite("TypeScriptGoServiceGetElementTypeTest", {}, () => { + const logFile = path.join(import.meta.dirname, "..", "tsgo.log") + const testProjectDir = path.join(import.meta.dirname, "testProject") + + before(async () => { + if (fs.existsSync(logFile)) + await fs.promises.unlink(logFile) + }) + + type MyContext = TestContext & { + client?: LspClient + openFilesContent?: Map + } + + beforeEach(async ctx => { + const myCtx = ctx as MyContext + + myCtx.openFilesContent ??= new Map() + + try { + const client = new LspClient(ctx.name, logFile, testProjectDir) + await client.spawnAndInit(); + myCtx.client = client + } catch (e) { + await myCtx.client?.kill(); + myCtx.client = undefined + throw e + } + }) + + afterEach(async ctx => { + const myCtx = ctx as MyContext + + myCtx.openFilesContent?.clear() + + try { + await myCtx.client?.shutdown() + } catch (e) { + await myCtx.client?.kill() + throw e + } finally { + myCtx.client = undefined + } + }) + + async function getElementType(ctx: TestContext, content: string, elementText: string) { + const myCtx = ctx as MyContext + + const fileName = "a.ts" + const alreadyOpenedContent = myCtx.openFilesContent!.get(fileName) + if (alreadyOpenedContent == null) { + myCtx.client!.didOpen("a.ts", content) + myCtx.openFilesContent!.set(fileName, content) + } else if (alreadyOpenedContent !== content) { + throw new Error(`File ${fileName} already opened with the different content`) + } + + return await myCtx.client!.getElementType(fileName, getRange(content, elementText)) + } + + // *** Tests *** + + test("any", async ctx => { + const type = await getElementType(ctx, "let foo", "foo") + assert.strictEqual(type.flags, TypeFlags.Any) + }) + + test("unknown", async ctx => { + const type = await getElementType(ctx, "let foo: unknown", "foo") + assert.strictEqual(type.flags, TypeFlags.Unknown) + }) + + test("undefined", async ctx => { + const type = await getElementType(ctx, "let foo: undefined", "foo") + assert.strictEqual(type.flags, TypeFlags.Undefined) + }) + + test("number", async ctx => { + const type = await getElementType(ctx, "declare const foo: number", "foo") + // 8 is a converted flag, use direct instead + assert.strictEqual(type.flags, TypeFlags.Number) + }) + + test("numberLiteral", async ctx => { + const type = await getElementType(ctx, "const foo = 123", "foo") + // 256 is a converted flag + assert.strictEqual(type.flags, TypeFlags.NumberLiteral) + assert.strictEqual((type as LiteralType).value, 123) + }) + + test("string", async ctx => { + const type = await getElementType(ctx, "declare const foo: string", "foo") + assert.strictEqual(type.flags, TypeFlags.String) + }) + + test("stringLiteral", async ctx => { + const type = await getElementType(ctx, "declare const foo: '1'", "foo") + assert.strictEqual(type.flags, TypeFlags.StringLiteral) + assert.strictEqual((type as LiteralType).value, "1") + }) + + test("function", async ctx => { + const content = "function foo(x: number): string {}" + const type = await getElementType(ctx, content, "foo") + assert.strictEqual(type.flags, 1<<20 /* Object */) + assert.strictEqual((type as ObjectType).objectFlags, 16 /* anonymous */) + assert.strictEqual(getFragment(content, type.symbol.declarations![0]), content) + + const [callSignature] = await (type as TypeEx).getCallSignaturesEx() + assert.strictEqual(callSignature.parameters[0].name, "x") + assert.strictEqual((callSignature as SignatureEx).resolvedReturnType.flags, TypeFlags.String) + }) + + test("objectOptionalProperty", async ctx => { + const content = "type Foo = {x?:\n 123}" + + const type = await getElementType(ctx, content, "Foo") + assert.strictEqual(type.flags, 1<<20) + assert.strictEqual((type as ObjectType).objectFlags, 16) + + const properties = await (type as TypeEx).getPropertiesEx() + assert.strictEqual(properties.length, 1) + + const [xProperty] = properties + assert.strictEqual(xProperty.escapedName, "x") + assert.strictEqual(xProperty.name, "x") + assert.strictEqual(xProperty.flags, SymbolFlags.Property | SymbolFlags.Optional) + + assert.strictEqual(xProperty.declarations?.length, 1) + const [xDeclaration] = xProperty.declarations + assert.strictEqual(getFragment(content, xDeclaration), "x?:\n 123") + + assert.deepStrictEqual(await (type as TypeEx).getCallSignaturesEx(), []) + assert.deepStrictEqual(await (type as TypeEx).getConstructSignaturesEx(), []) + }) + + test("genericArguments", async ctx => { + const content = "type Foo = {x: T}" + const type = await getElementType(ctx, content, "Foo") + + const [tArg] = type.aliasTypeArguments! + assert.strictEqual(tArg.flags, TypeFlags.Object) + + const {symbol} = tArg + assert.strictEqual(symbol.name, "T") + assert.strictEqual(symbol.escapedName, "T") + assert.strictEqual(symbol.flags, SymbolFlags.TypeParameter) + assert.strictEqual(getFragment(content, symbol.declarations![0]), "T") + }) + + test("conditionalType", async ctx => { + const content = `type Foo = T extends string ? '1' : 1` + + const type = await getElementType(ctx, content, "Foo") + assert.strictEqual(type.flags, 1<<26 /* Conditional */) + + const {aliasSymbol} = type + assert.strictEqual(aliasSymbol!.flags, TypeFlags.Object) + assert.strictEqual(aliasSymbol!.name, "Foo") + assert.strictEqual(aliasSymbol!.escapedName, "Foo") + + const {extendsType} = type as ConditionalType + assert.strictEqual(extendsType.flags, TypeFlags.String) + + const resolvedTrueType = await (type as TypeEx).getResolvedTrueTypeEx() + assert.strictEqual(resolvedTrueType.flags, TypeFlags.StringLiteral) + assert.strictEqual((resolvedTrueType as LiteralType).value, "1") + + const resolvedFalseType = await (type as TypeEx).getResolvedFalseTypeEx() + assert.strictEqual(resolvedFalseType.flags, TypeFlags.NumberLiteral) + assert.strictEqual((resolvedFalseType as LiteralType).value, 1) + }) + + test("unionType", async ctx => { + const content = "type Foo = 1 | 2" + const type = await getElementType(ctx, content, "Foo") + assert.strictEqual(type.flags, 1<<27 /* Union */) + const {types} = (type as UnionType) + assert.strictEqual(types.length, 2) + const [one, two] = types + + assert.strictEqual(one.flags, TypeFlags.NumberLiteral) + assert.strictEqual((one as LiteralType).value, 1) + + assert.strictEqual(two.flags, TypeFlags.NumberLiteral) + assert.strictEqual((two as LiteralType).value, 2) + }) + + test("symbolType", async ctx => { + const content = "type Foo = {a: 5}" + const type = await getElementType(ctx, content, "Foo") + assert.strictEqual(type.flags, 1<<20) + + const [aProperty] = await (type as TypeEx).getPropertiesEx() + assert.strictEqual(aProperty.name, "a") + + const aType = await (aProperty as SymbolEx).getTypeEx() + assert.strictEqual(aType.flags, TypeFlags.NumberLiteral) + assert.strictEqual((aType as LiteralType).value, 5) + }) + + test("cachedTypes", async ctx => { + const content = "type Foo = T extends 1 ? 1 : 1" + const type = await getElementType(ctx, content, "Foo") + + const trueType = await (type as TypeEx).getResolvedTrueTypeEx() + const falseType = await (type as TypeEx).getResolvedFalseTypeEx() + + // === because the link lspApiObjectIdRef existing within the same response + assert.strictEqual(trueType, falseType) + }) + + test("repeatedRequest", async ctx => { + const content = "type Foo = {a: 12}" + const type0 = await getElementType(ctx, content, "Foo") + const type1 = await getElementType(ctx, content, "Foo") + + // === because caching between requests + assert.strictEqual(type0, type1) + }) + + test("areMutuallyAssignable", async ctx => { + const content = "type Foo = {a: 12}\ntype Bar = {a: 12}" + const typeFoo = await getElementType(ctx, content, "Foo") + const typeBar = await getElementType(ctx, content, "Bar") + assert.notStrictEqual(typeFoo, typeBar) + assert.strictEqual(await (typeFoo as TypeEx).isMutuallyAssignableWith(typeBar), true) + }) + + test("getTypeWithoutOpening", async ctx => { + // This uses self_managed_projects + const content = String(await fs.promises.readFile(path.join(testProjectDir, "b.ts"))) + const type = await (ctx as MyContext).client!.getElementType("b.ts", getRange(content, "Foo")) + assert.strictEqual(type.flags, 1<<20) + assert.strictEqual(type.aliasSymbol?.escapedName, "Foo") + }) +}) + +// *** Util *** + +function getRange(text: string, element: string) { + if (element.includes("\n")) + throw new Error(`Multiline element not implemented: ${element}`) + + const lines = text.split("\n") + const posIndices = lines + .map(line => line.indexOf(element)) + + const lineIndex = posIndices.findIndex(pos => pos !== -1) + if (lineIndex === -1) + throw new Error(`Element ${element} not found in ${text}`) + + return { + start: { line: lineIndex, character: posIndices[lineIndex] }, + end: { line: lineIndex, character: posIndices[lineIndex] + element.length } + } +} + +function getFragment(content: string, node: Node) { + const range = (node as NodeEx).range + const lines = content.split("\n") + + if (range.start.line === range.end.line) + return lines[range.start.line].slice(range.start.character, range.end.character) + + const startLineFragment = lines[range.start.line].slice(range.start.character) + const endLineFragment = lines[range.end.line].slice(0, range.end.character) + const midLines = lines.slice(range.start.line + 1, range.end.line) + return [startLineFragment, ...midLines, endLineFragment].join("\n") +} + +function delay(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms) + }) +} + +// *** Protocol *** + +interface Message { + jsonrpc: "2.0" +} + +interface RequestMessage extends Message { + id: number | string + method: string + params?: unknown[] | object +} + +interface ResponseMessage extends Message { + id: number | string | null + result?: any + error?: any +} + +interface NotificationMessage extends Message { + method: string; + params?: unknown[] | object +} + +// *** Types *** + +type TypeRequestKind = "Default" | "Contextual" | "ContextualCompletions" + +type ServerObjectDef = {lspApiObjectId: number} +type ServerObjectRef = {lspApiObjectIdRef: number} + +type ServerType = ServerObjectDef & { + lspApiObjectType: "TypeObject" + id: number + lspApiProjectId: number + lspApiTypeCheckerId: number +} +type ServerSymbol = ServerObjectDef & { + lspApiObjectType: "SymbolObject" + id: number + lspApiProjectId: number + lspApiTypeCheckerId: number +} + +type ServerSignature = ServerObjectDef & { + lspApiObjectType: "SignatureObject" +} + +type ServerIndexInfo = ServerObjectDef & { + lspApiObjectType: "IndexInfo" +} + +type ServerNode = ServerObjectDef & { + lspApiObjectType: "NodeObject" +} + +type ServerOtherObject = ServerObjectDef & {} + +type ServerObject = ServerObjectRef | ServerType | ServerSymbol | ServerIndexInfo | ServerNode | ServerOtherObject + +/** + * These are properties which were formerly received via Checker, Type etc methods and properties. + * Some of them were internal. + * + * They can be moved now to the `handleCustomTsServerCommand` handler. + */ +type TypeEx = Type & { + /** + * Type.id + */ + id: number + /** + * Checker.getResolvedTypeArguments() + */ + resolvedTypeArguments?: Type[] + /** + * Checker.getBaseConstraintsOfType() + */ + constraint?: Type + /** + * This is enumQualifiedName received from Nodes structure + */ + nameType?: string + /** + * Property of TypeParameter + */ + isThisType?: boolean + /** + * Property of IntrinsicType + */ + intrinsicName?: string + + /** + * Async versions of Type.get...() methods + */ + getCallSignaturesEx(): Promise + getConstructSignaturesEx(): Promise + getPropertiesEx(): Promise + getResolvedFalseTypeEx(): Promise + getResolvedTrueTypeEx(): Promise + + /** + * Results of 'getTypeProperties' + */ + callSignatures?: Signature[] + constructSignatures?: Signature[] + indexInfos?: IndexInfo[] + properties?: Symbol[] + resolvedFalseType?: Type + resolvedProperties?: Symbol[] + resolvedTrueType?: Type + + /** + * Types equivalence via t.isAssignableTo(u) && u.isAssignableTo(t) + */ + isMutuallyAssignableWith(t: Type): Promise +} + +type SymbolEx = Symbol & { + /** + * Checker.getTypeOfSymbolAtLocation() + */ + getTypeEx(): Promise +} + +type SignatureEx = Signature & { + /** + * Internal fields + */ + flags: number + /** + * checker.getReturnTypeOfSignature() + */ + resolvedReturnType: Type +} + +type NodeEx = Node & { + /** + * ts.isComputedPropertyName() + */ + computedProperty: boolean + /** + * instead of pos, end + */ + range: {start: LineAndCharacter, end: LineAndCharacter} +} + +type ClientObject = Type | Symbol | Signature | IndexInfo | Node | PseudoBigInt + +// *** Client *** + +class LspClient { + #testName: string + #logFile: string + #testProjectDir: string + + constructor(testName: string, logFile: string, testProjectDir: string) { + this.#testName = testName + this.#logFile = logFile + this.#testProjectDir = testProjectDir + } + + async spawnAndInit() { + await this.#openLog() + this.#spawnSubprocess() + this.#addStderrHandler() + this.#addStdoutHandler() + await this.#sendRequest({ + jsonrpc: "2.0", + id: this.#nextId(), + method: "initialize", + params: { + processId: process.pid, + rootPath: this.#testProjectDir, + rootUri: url.pathToFileURL(this.#testProjectDir), + capabilities: {}, + clientInfo: { name: "LSP API Test" }, + }, + }) + this.#sendMessage({jsonrpc: "2.0", method:"initialized", params:{}}) + return this + } + + #logFileHandle: fs.promises.FileHandle | undefined + #logFileWritingPromise: Promise | undefined + async #openLog() { + this.#logFileHandle = await fs.promises.open(this.#logFile, "a") + this.#log("---", `Starting test: ${this.#testName}`) + } + + async #log(src: string, msg: string) { + for ( ; ; ) { + const promise = this.#logFileWritingPromise + await promise + if (promise === this.#logFileWritingPromise) break + } + + const newline = msg.at(-1) === "\n" ? "" : "\n" + const writePromise = fs.promises.writeFile(this.#logFileHandle!, Buffer.from(`${src} ${new Date().toISOString()} ${msg}${newline}`)) + this.#logFileWritingPromise = writePromise + return writePromise + } + + #childProcess: subprocess.ChildProcess | undefined + #spawnSubprocess() { + const tsgoExec = path.join(import.meta.dirname, "..", "..", "..", "built", "local", "tsgo") + const args = [ "--lsp", "--stdio" ] + this.#log("---", `Spawning "${tsgoExec}" with args [${args.join(", ")}]`) + this.#childProcess = subprocess.execFile(tsgoExec, args) + this.#log("---", `The subprocess started with pid ${this.#childProcess.pid}`) + } + + #addStderrHandler() { + this.#childProcess!.stderr!.on("data", data => { + this.#log("ERR", String(data)) + }) + } + + #inDataPending = "" + #addStdoutHandler() { + this.#childProcess!.stdout!.on("data", newData => { + this.#log("IN ", String(newData)) + this.#inDataPending += newData + for ( ; ; ) { + const remainingData = this.#handleInData(this.#inDataPending) + if (remainingData !== this.#inDataPending) + this.#inDataPending = remainingData + else + break + } + }) + } + + #handleInData(inData: string) { + const sep = "\r\n\r\n" + const sepPos = inData.indexOf(sep) + if (sepPos === -1) return inData + + const m = /Content-Length: (\d+)\r\n/.exec(inData) + if (!m) throw new Error(`Content-Length not found in ${inData}`) + + const inDataPendingBuf = Buffer.from(inData) // non-ascii + const contentLength = Number(m[1]) + const content = inDataPendingBuf.subarray(sepPos + sep.length, sepPos + sep.length + contentLength) + + this.#handleInputMessage(JSON.parse(String(content))) + + const remainingData = String(inDataPendingBuf.subarray(sepPos + sep.length + contentLength)) + return remainingData + } + + #pendingIdToResponseConsumer = new Map void, reject: (e: unknown) => void}>() + #handleInputMessage(message: RequestMessage | ResponseMessage | NotificationMessage) { + if ("params" in message && "id" in message) { + // server -> client request + this.#sendMessage({jsonrpc: "2.0", id: message.id, result: null}) + } else if ("id" in message && message.id != null) { + const consumer = this.#pendingIdToResponseConsumer.get(message.id) + if (consumer) { + this.#pendingIdToResponseConsumer.delete(message.id) + if ("error" in message) + consumer.reject(message) + else + consumer.resolve(message) + } + } else if ("error" in message) { + this.#rejectAllPendingRequests(message) + } + } + + async #sendRequest(request: RequestMessage) { + return new Promise((resolve: (r: ResponseMessage) => void, reject: any) => { + if (this.#pendingIdToResponseConsumer.has(request.id)) + throw new Error(`Duplicate request id ${request.id}`) + this.#pendingIdToResponseConsumer.set(request.id, {resolve, reject}) + this.#sendMessage(request) + }) + } + + #sendMessage(request: RequestMessage | ResponseMessage | NotificationMessage) { + const requestStr = JSON.stringify(request) + const requestBuf = Buffer.from(requestStr) + const header = `Content-Length: ${requestBuf.length}\r\n\r\n` + this.#log("OUT", header) + this.#log("OUT", requestStr) + this.#childProcess!.stdin!.write(header) + this.#childProcess!.stdin!.write(requestBuf) + } + + #getFileUri(fileRelName: string) { + return url.pathToFileURL(path.join(this.#testProjectDir, fileRelName)) + } + + #_nextId = 0 + #nextId() { + return this.#_nextId++ + } + + didOpen(fileRelName: string, text: string) { + this.#sendMessage({ + jsonrpc: "2.0", + method:"textDocument/didOpen", + params: { + textDocument: { + uri: this.#getFileUri(fileRelName), + languageId: "typescript", + version: 0, + text, + } + } + }) + } + + async getElementType(fileRelName: string, range: Range, forceReturnType: boolean = false, typeRequestKind: TypeRequestKind = "Default") { + const file = this.#getFileUri(fileRelName) + const response = await this.#sendRequest({ + jsonrpc: "2.0", + id: this.#nextId(), + method: "$/handleCustomLspApiCommand", + params: { + lspApiCommand:"getElementType", + args: { + file, + range, + forceReturnType, + typeRequestKind, + } + } + }) + return this.#convertServerType(response.result.response, file) as Type + } + + async getTypeProperties( + typeId: number, + projectId: number, + typeCheckerId: number, + originalRequestUri?: URL, + ) { + const response = await this.#sendRequest({ + jsonrpc: "2.0", + id: this.#nextId(), + method: "$/handleCustomLspApiCommand", + params: { + lspApiCommand:"getTypeProperties", + args: { typeId, projectId, typeCheckerId, originalRequestUri}, + } + }) + return this.#convertServerType(response.result.response) as Type + } + + async getSymbolType( + symbolId: number, + projectId: number, + typeCheckerId: number, + ) { + const response = await this.#sendRequest({ + jsonrpc: "2.0", + id: this.#nextId(), + method: "$/handleCustomLspApiCommand", + params: { + lspApiCommand: "getSymbolType", + args: { symbolId, projectId, typeCheckerId }, + } + }) + return this.#convertServerType(response.result.response) as Type + } + + async areTypesMutuallyAssignable( + projectId: number, + typeCheckerId: number, + type1Id: number, + type2Id: number, + ) { + const response = await this.#sendRequest({ + jsonrpc: "2.0", + id: this.#nextId(), + method: "$/handleCustomLspApiCommand", + params: { + lspApiCommand: "areTypesMutuallyAssignable", + args: { projectId, typeCheckerId, type1Id, type2Id }, + } + }) + return response.result.response.areMutuallyAssignable as boolean + } + + #resolveMapBetweenRequests = new Map() + + #convertServerType(rootServerType: ServerType, fileUri?: URL): ClientObject { + if (!isType(rootServerType)) + throw new Error(`Root server object must be a type: ${JSON.stringify(rootServerType)}`) + + const ths = this + + const resolveMapWithinSameResponse = new Map() + + function resolveOrConvert( + serverObject: From, + converter: (serverObj: From, clientObj: To) => To, + ): To { + const {lspApiObjectIdRef} = serverObject as ServerObjectRef + if (lspApiObjectIdRef != null) { + const objs = resolveMapWithinSameResponse.get(lspApiObjectIdRef) + if (!objs) + throw new Error(`Could not resolve reference ${lspApiObjectIdRef} in ${JSON.stringify(serverObject)}`) + return objs.clientObject as To + } + + const {lspApiObjectId} = serverObject as ServerObjectDef + if (lspApiObjectId != null) { + if (resolveMapWithinSameResponse.has(lspApiObjectId)) + throw new Error(`Duplicate lspApiObjectId ${lspApiObjectId} in ${JSON.stringify(serverObject)}`) + + const clientObject = {} as To + resolveMapWithinSameResponse.set(lspApiObjectId, {serverObject, clientObject}) + if (isType(serverObject)) + ths.#resolveMapBetweenRequests.set(serverObject.id, clientObject) + converter(serverObject, clientObject) + return clientObject + } + + if (isType(serverObject)) { + const convertedEarlier = ths.#resolveMapBetweenRequests.get(serverObject.id) + if (convertedEarlier) + return convertedEarlier as To + } + + throw new Error(`Could not convert or resolve ${JSON.stringify(serverObject)}`) + } + + function convertType(typeServerObj: ServerType, target: Type): Type { + + // Properties must be processed in order because definitions come before refs + for (const [key, value] of Object.entries(typeServerObj)) { + + // Properties returned by both "getElementType" and "getPropertiesOfType" + if (key === "flags" && typeof value === "string") + target.flags = Number(value) + + else if (key === "id" && typeof value === "number") + (target as TypeEx).id = value + + else if (key === "objectFlags" && typeof value === "string") + (target as ObjectType).objectFlags = Number(value) + + // Returned by "getElementType" (alphabetically) + else if (key === "aliasSymbol" && isSymbol(value)) + target.aliasSymbol = resolveOrConvert(value, convertSymbol) + + else if (key === "aliasTypeArguments" && isTypes(value)) + target.aliasTypeArguments = value + .map(arg => resolveOrConvert(arg, convertType)) + + else if (key === "baseType" && isType(value)) + (target as SubstitutionType).baseType = resolveOrConvert(value, convertType) + + else if (key === "checkType" && isType(value)) + (target as ConditionalType).checkType = resolveOrConvert(value, convertType) + + else if (key === "constraint" && isType(value)) + (target as TypeEx).constraint = resolveOrConvert(value, convertType) + + else if (key === "elementFlags" && isNumbers(value)) + (target as TupleType).elementFlags = value + + else if (key === "extendsType" && isType(value)) + (target as ConditionalType).extendsType = resolveOrConvert(value, convertType) + + else if (key === "freshType" && isType(value)) + (target as LiteralType).freshType = resolveOrConvert(value, convertType) as LiteralType + + else if (key === "indexType" && isType(value)) + (target as IndexedAccessType).indexType = resolveOrConvert(value, convertType) + + else if (key === "intrinsicName" && typeof value === "string") + (target as TypeEx).intrinsicName = value + + else if (key === "isThisType" && typeof value === "boolean") + (target as TypeEx).isThisType = value + + else if (key === "nameType" && typeof value === "string") + (target as TypeEx).nameType = value + + else if (key === "objectType" && isType(value)) + (target as IndexedAccessType).objectType = resolveOrConvert(value, convertType) + + else if (key === "resolvedTypeArguments" && isTypes(value)) + (target as TypeEx).resolvedTypeArguments = value + .map(t => resolveOrConvert(t, convertType)) + + else if (key === "symbol" && isSymbol(value)) + target.symbol = resolveOrConvert(value, convertSymbol) + + else if (key === "target" && isType(value)) + (target as TypeReference).target = resolveOrConvert(value, convertType) as GenericType + + else if (key === "texts" && isStrings(value)) + (target as TemplateLiteralType).texts = value + + else if (key === "type" && isType(value)) + (target as IndexType).type = resolveOrConvert(value, convertType) + + else if (key === "types"&& isTypes(value)) + (target as UnionType).types = value + .map(t => resolveOrConvert(t, convertType)) + + else if (key === "value") { + if (isOtherObject(value)) + (target as BigIntLiteralType).value = resolveOrConvert(value, convertPseudoBigInt) + if (typeof value === "string" || typeof value === "number") + (target as LiteralType).value = value + } + + // Returned by "getPropertiesOfType" (alphabetically) + + else if (key === "callSignatures" && isSignatures(value)) + (target as TypeEx).callSignatures = value + .map(sig => resolveOrConvert(sig, convertSignature)) + + else if (key === "constructSignatures" && isSignatures(value)) + (target as TypeEx).constructSignatures = value + .map(sig => resolveOrConvert(sig, convertSignature)) + + else if (key === "indexInfos" && isIndexInfos(value)) + (target as TypeEx).indexInfos = value + .map(info => resolveOrConvert(info, convertIndexInfo)) + + else if (key === "properties" && isSymbols(value)) + (target as TypeEx).properties = value + .map(sym => resolveOrConvert(sym, convertSymbol)) + + else if (key === "resolvedFalseType" && isType(value)) + (target as TypeEx).resolvedFalseType = resolveOrConvert(value, convertType) + + else if (key === "resolvedProperties" && isSymbols(value)) + (target as TypeEx).resolvedProperties = value + .map(sym => resolveOrConvert(sym, convertSymbol)) + + else if (key === "resolvedTrueType" && isType(value)) + (target as TypeEx).resolvedTrueType = resolveOrConvert(value, convertType) + } + + // Adding methods + + target.getFlags = () => target.flags + target.getSymbol = () => target.symbol + + let typePropertiesResponse: Type | undefined + async function getTypeProperties() { + if (!typePropertiesResponse) + typePropertiesResponse = await ths.getTypeProperties( + typeServerObj.id, + typeServerObj.lspApiProjectId, + typeServerObj.lspApiTypeCheckerId, + fileUri, + ) + return typePropertiesResponse as TypeEx + } + + (target as TypeEx).getCallSignaturesEx = async () => (await getTypeProperties()).callSignatures!; + (target as TypeEx).getConstructSignaturesEx = async () => (await getTypeProperties()).constructSignatures!; + (target as TypeEx).getPropertiesEx = async () => (await getTypeProperties()).properties!; + (target as TypeEx).getResolvedFalseTypeEx = async () => (await getTypeProperties()).resolvedFalseType!; + (target as TypeEx).getResolvedTrueTypeEx = async () => (await getTypeProperties()).resolvedTrueType!; + + (target as TypeEx).isMutuallyAssignableWith = async t => + await ths.areTypesMutuallyAssignable( + rootServerType.lspApiProjectId, + rootServerType.lspApiTypeCheckerId, + typeServerObj.id, + (t as TypeEx).id, + ) + + return target + } + + function convertSymbol(symbolServerObj: ServerSymbol, target: Symbol): Symbol { + // Processing in order + + for (const [key, value] of Object.entries(symbolServerObj)) { + if (key === "declarations" && isNodes(value)) + target.declarations = value + .map(node => resolveOrConvert(node, convertNode) as Declaration) + + else if (key === "escapedName" && typeof value === "string") { + target.escapedName = value as __String + (target as {name: string}).name = value + } + + else if (key === "flags" && typeof value === "string") + target.flags = Number(value) + + else if (key === "valueDeclaration" && isNode(value)) + target.valueDeclaration = resolveOrConvert(value, convertNode) as Declaration + } + + + // Method + + let type: Type | undefined + async function getTypeImpl() { + type ??= await ths.getSymbolType(symbolServerObj.id, rootServerType.lspApiProjectId, rootServerType.lspApiTypeCheckerId) + return type + } + + (target as SymbolEx).getTypeEx = async () => await getTypeImpl() + + return target + } + + function convertSignature(signatureServerObj: ServerSignature, target: Signature): Signature { + // Processing in order + + for (const [key, value] of Object.entries(signatureServerObj)) { + if (key === "declaration" && isNode(value)) + target.declaration = resolveOrConvert(value, convertNode) as SignatureDeclaration + + else if (key === "flags" && typeof value === "string") + (target as SignatureEx).flags = Number(value) + + else if (key === "parameters" && isSymbols(value)) + target.parameters = value.map(sym => resolveOrConvert(sym, convertSymbol)) + + else if (key === "resolvedReturnType" && isType(value)) + (target as SignatureEx).resolvedReturnType = resolveOrConvert(value, convertType) + + else if (key === "typeParameters" && isTypes(value)) + target.typeParameters = value.map(type => resolveOrConvert(type, convertType)) + } + + return target + } + + function convertNode(nodeServerObj: ServerNode, target: Node): Node { + for (const [key, value] of Object.entries(nodeServerObj)) { + if (key === "computedProperty" && typeof value === "boolean") + (target as NodeEx).computedProperty = value + + else if (key === "fileName" && typeof value === "string") + (target as SourceFile).fileName = value + + else if (key === "parent" && isNode(value)) + (target as {parent: Node}).parent = resolveOrConvert(value, convertNode) + + else if (key === "range" && typeof value === "object") + (target as NodeEx).range = value as NodeEx["range"] + } + + return target + } + + function convertPseudoBigInt(pseudoBigIntServerObj: ServerOtherObject, target: PseudoBigInt): PseudoBigInt { + for (const [key, value] of Object.entries(pseudoBigIntServerObj)) { + if (key === "negative" && typeof value === "boolean") + target.negative = value + + else if (key === "base10Value" && typeof value === "string") + target.base10Value = value + } + + return target + } + + function convertIndexInfo(indexInfoServerObj: ServerIndexInfo, target: IndexInfo): IndexInfo { + for (const [key, value] of Object.entries(indexInfoServerObj)) { + if (key === "declaration" && isNode(value)) + target.declaration = resolveOrConvert(value, convertNode) as IndexSignatureDeclaration + + else if (key === "isReadonly" && typeof value === "boolean") + target.isReadonly = value + + else if (key === "keyType" && isType(value)) + target.keyType = resolveOrConvert(value, convertType) + + else if (key === "type" && isType(value)) + target.type = resolveOrConvert(value, convertType) + } + + return target + } + + + function isType(serverObject: unknown): serverObject is ServerType { + const isTypeImpl = (o: unknown): o is ServerType => (o as ServerType).lspApiObjectType === "TypeObject" + return isTypeImpl(serverObject) || isRefTo(serverObject, isTypeImpl) + } + + function isRefTo(serverObject: unknown, defChecker: (obj: unknown) => obj is T): serverObject is T { + const {lspApiObjectIdRef} = serverObject as ServerObjectRef + if (lspApiObjectIdRef != null) { + const {serverObject} = resolveMapWithinSameResponse.get(lspApiObjectIdRef) ?? {} + return !!serverObject && defChecker(serverObject) + } + return false + } + + function isTypes(serverObject: unknown): serverObject is ServerType[] { + return Array.isArray(serverObject) && serverObject.every(isType) + } + + function isSymbol(serverObject: unknown): serverObject is ServerSymbol { + const isSymbolImpl = (o: unknown): o is ServerSymbol => (o as ServerSymbol).lspApiObjectType === "SymbolObject" + return isSymbolImpl(serverObject) || isRefTo(serverObject, isSymbolImpl) + } + + function isSymbols(serverObject: unknown): serverObject is ServerSymbol[] { + return Array.isArray(serverObject) && serverObject.every(isSymbol) + } + + function isSignature(serverObject: unknown): serverObject is ServerSignature { + const isSignatureImpl = (o: unknown): o is ServerSignature => (o as ServerSignature).lspApiObjectType === "SignatureObject" + return isSignatureImpl(serverObject) || isRefTo(serverObject, isSignatureImpl) + } + + function isSignatures(serverObject: unknown): serverObject is ServerSignature[] { + return Array.isArray(serverObject) && serverObject.every(isSignature) + } + + function isNode(serverObject: unknown): serverObject is ServerNode { + const isNodeImpl = (o: unknown): o is ServerNode => (o as ServerNode).lspApiObjectType === "NodeObject" + return isNodeImpl(serverObject) || isRefTo(serverObject, isNodeImpl) + } + + function isNodes(serverObject: unknown): serverObject is ServerNode[] { + return Array.isArray(serverObject) && serverObject.every(isNode) + } + + function isIndexInfo(serverObject: unknown): serverObject is ServerIndexInfo { + const isIndexInfoImpl = (o: unknown): o is ServerIndexInfo => (o as ServerIndexInfo).lspApiObjectType === "IndexInfo" + return isIndexInfoImpl(serverObject) || isRefTo(serverObject, isIndexInfoImpl) + } + + function isIndexInfos(serverObject: unknown): serverObject is ServerIndexInfo[] { + return Array.isArray(serverObject) && serverObject.every(isIndexInfo) + } + + function isOtherObject(serverObject: unknown): serverObject is ServerOtherObject { + const isOtherObjectImpl = (o: unknown): o is ServerOtherObject => + serverObject != null + && (serverObject as ServerObjectDef).lspApiObjectId != null + && typeof serverObject === "object" + && !("lspApiObjectType" in serverObject) + + return isOtherObjectImpl(serverObject) || isRefTo(serverObject, isOtherObjectImpl) + } + + function isStrings(serverObject: unknown): serverObject is string[] { + return Array.isArray(serverObject) && serverObject.every(s => typeof s === "string") + } + + function isNumbers(serverObject: unknown): serverObject is number[] { + return Array.isArray(serverObject) && serverObject.every(n => typeof n === "number") + } + + + return resolveOrConvert(rootServerType, convertType) + } + + async shutdown() { + await this.#sendRequest({jsonrpc: "2.0", id: this.#nextId(), method: "shutdown"}) + this.#sendMessage({jsonrpc: "2.0", method: "exit"}) + while (this.#childProcess!.exitCode == null) await delay(50) + await this.#closeLog() + this.#rejectAllPendingRequests(new Error("The subprocess exited")) + } + + async kill() { + this.#childProcess!.kill() + await this.#closeLog() + this.#rejectAllPendingRequests(new Error("The subprocess was killed")) + } + + async #closeLog() { + await this.#log("---", `The subprocess exited with the code: ${this.#childProcess!.exitCode}`) + await this.#log("---", `Finished test: ${this.#testName}`) + await this.#logFileHandle!.close(); + } + + #rejectAllPendingRequests(reason: any) { + for (const {reject} of this.#pendingIdToResponseConsumer.values()) + reject(reason) + this.#pendingIdToResponseConsumer.clear() + } +} diff --git a/_packages/api/test/testProject/b.ts b/_packages/api/test/testProject/b.ts new file mode 100644 index 0000000000..a3a2ec9ba5 --- /dev/null +++ b/_packages/api/test/testProject/b.ts @@ -0,0 +1 @@ +type Foo = {a: 11} diff --git a/_packages/api/test/testProject/tsconfig.json b/_packages/api/test/testProject/tsconfig.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/_packages/api/test/testProject/tsconfig.json @@ -0,0 +1 @@ +{} diff --git a/_packages/api/test/tsconfig.json b/_packages/api/test/tsconfig.json index b1a688b3b8..6454937b71 100644 --- a/_packages/api/test/tsconfig.json +++ b/_packages/api/test/tsconfig.json @@ -1,5 +1,6 @@ { "extends": "../tsconfig.dev.json", + "exclude": ["testProject"], "references": [ { "path": ".." } ] diff --git a/cmd/tsgo/lsp.go b/cmd/tsgo/lsp.go index 0dd04c57fb..04397389cf 100644 --- a/cmd/tsgo/lsp.go +++ b/cmd/tsgo/lsp.go @@ -26,6 +26,7 @@ func runLSP(args []string) int { _ = pipe socket := flag.String("socket", "", "use socket for communication") _ = socket + defaultLibraryPathArg := flag.String("defaultLibraryPath", "", "directory with the default libraries such as lib.es5.d.ts etc") if err := flag.Parse(args); err != nil { return 2 } @@ -42,7 +43,12 @@ func runLSP(args []string) int { } fs := bundled.WrapFS(osvfs.FS()) - defaultLibraryPath := bundled.LibPath() + var defaultLibraryPath string + if *defaultLibraryPathArg != "" { + defaultLibraryPath = *defaultLibraryPathArg + } else { + defaultLibraryPath = bundled.LibPath() + } typingsLocation := getGlobalTypingsCacheLocation() s := lsp.NewServer(&lsp.ServerOptions{ diff --git a/internal/checker/exports.go b/internal/checker/exports.go index 332a72569e..d58e59b167 100644 --- a/internal/checker/exports.go +++ b/internal/checker/exports.go @@ -114,6 +114,22 @@ func (c *Checker) GetEffectiveDeclarationFlags(n *ast.Node, flagsToCheck ast.Mod return c.getEffectiveDeclarationFlags(n, flagsToCheck) } +func (c *Checker) GetIndexInfosOfType(t *Type) []*IndexInfo { + return c.getIndexInfosOfType(t) +} + +func (c *Checker) GetTypeArguments(t *Type) []*Type { + return c.getTypeArguments(t) +} + +func (c *Checker) GetTrueTypeFromConditionalType(t *Type) *Type { + return c.getTrueTypeFromConditionalType(t) +} + +func (c *Checker) GetFalseTypeFromConditionalType(t *Type) *Type { + return c.getFalseTypeFromConditionalType(t) +} + func (c *Checker) GetBaseConstraintOfType(t *Type) *Type { return c.getBaseConstraintOfType(t) } diff --git a/internal/checker/relater.go b/internal/checker/relater.go index db83af0745..474daba41b 100644 --- a/internal/checker/relater.go +++ b/internal/checker/relater.go @@ -147,6 +147,10 @@ func (c *Checker) compareTypesSubtypeOf(source *Type, target *Type) Ternary { return TernaryFalse } +func (c *Checker) IsTypeAssignableTo(source *Type, target *Type) bool { + return c.isTypeAssignableTo(source, target) +} + func (c *Checker) isTypeAssignableTo(source *Type, target *Type) bool { return c.isTypeRelatedTo(source, target, c.assignableRelation) } diff --git a/internal/checker/types.go b/internal/checker/types.go index 0c2ff33a41..f6bc407100 100644 --- a/internal/checker/types.go +++ b/internal/checker/types.go @@ -572,6 +572,10 @@ func (t *Type) ObjectFlags() ObjectFlags { return t.objectFlags } +func (t *Type) Alias() *TypeAlias { + return t.alias +} + // Casts for concrete struct types func (t *Type) AsIntrinsicType() *IntrinsicType { return t.data.(*IntrinsicType) } @@ -771,6 +775,10 @@ func (t *LiteralType) String() string { return ValueToString(t.value) } +func (t *LiteralType) FreshType() *Type { + return t.freshType +} + // UniqueESSymbolTypeData type UniqueESSymbolType struct { @@ -930,6 +938,10 @@ type TupleElementInfo struct { func (t *TupleElementInfo) TupleElementFlags() ElementFlags { return t.flags } func (t *TupleElementInfo) LabeledDeclaration() *ast.Node { return t.labeledDeclaration } +func (i *TupleElementInfo) ElementFlags() ElementFlags { + return i.flags +} + type TupleType struct { InterfaceType elementInfos []TupleElementInfo @@ -947,6 +959,12 @@ func (t *TupleType) ElementFlags() []ElementFlags { } return elementFlags } + +type SingleSignatureType struct { + ObjectType + outerTypeParameters []*Type +} + func (t *TupleType) ElementInfos() []TupleElementInfo { return t.elementInfos } // InstantiationExpressionType @@ -1029,6 +1047,10 @@ type TypeParameter struct { resolvedDefaultType *Type } +func (t *TypeParameter) IsThisType() bool { + return t.isThisType +} + // IndexFlags type IndexFlags uint32 @@ -1057,12 +1079,24 @@ type IndexedAccessType struct { accessFlags AccessFlags // Only includes AccessFlags.Persistent } +func (t *IndexedAccessType) ObjectType() *Type { + return t.objectType +} + +func (t *IndexedAccessType) IndexType() *Type { + return t.indexType +} + type TemplateLiteralType struct { ConstrainedType texts []string // Always one element longer than types types []*Type // Always at least one element } +func (t *TemplateLiteralType) Texts() []string { + return t.texts +} + type StringMappingType struct { ConstrainedType target *Type @@ -1074,6 +1108,10 @@ type SubstitutionType struct { constraint *Type // Constraint that target type is known to satisfy } +func (t *SubstitutionType) BaseType() *Type { + return t.baseType +} + type ConditionalRoot struct { node *ast.ConditionalTypeNode checkType *Type @@ -1099,6 +1137,14 @@ type ConditionalType struct { combinedMapper *TypeMapper } +func (t *ConditionalType) CheckType() *Type { + return t.checkType +} + +func (t *ConditionalType) ExtendsType() *Type { + return t.extendsType +} + // SignatureFlags type SignatureFlags uint32 @@ -1157,6 +1203,10 @@ func (s *Signature) ThisParameter() *ast.Symbol { return s.thisParameter } +func (s *Signature) ReturnType() *Type { + return s.resolvedReturnType +} + func (s *Signature) Parameters() []*ast.Symbol { return s.parameters } @@ -1165,6 +1215,10 @@ func (s *Signature) HasRestParameter() bool { return s.flags&SignatureFlagsHasRestParameter != 0 } +func (s *Signature) IsSignatureCandidateForOverloadFailure() bool { + return s.flags&SignatureFlagsIsSignatureCandidateForOverloadFailure != 0 +} + type CompositeSignature struct { isUnion bool // True for union, false for intersection signatures []*Signature // Individual signatures @@ -1200,6 +1254,22 @@ type IndexInfo struct { components []*ast.Node // ElementWithComputedPropertyName } +func (i *IndexInfo) KeyType() *Type { + return i.keyType +} + +func (i *IndexInfo) ValueType() *Type { + return i.valueType +} + +func (i *IndexInfo) IsReadonly() bool { + return i.isReadonly +} + +func (i *IndexInfo) Declaration() *ast.Node { + return i.declaration +} + /** * Ternary values are defined such that * x & y picks the lesser in the order False < Unknown < Maybe < True, and diff --git a/internal/ls/completions.go b/internal/ls/completions.go index a7f38f4afd..5005e071ad 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -3054,6 +3054,10 @@ func getSwitchedType(caseClause *ast.CaseClauseNode, typeChecker *checker.Checke return typeChecker.GetTypeAtLocation(caseClause.Parent.Parent.Expression()) } +func IsEqualityOperatorKind(kind ast.Kind) bool { + return isEqualityOperatorKind(kind) +} + func isEqualityOperatorKind(kind ast.Kind) bool { switch kind { case ast.KindEqualsEqualsEqualsToken, ast.KindEqualsEqualsToken, diff --git a/internal/lsp/lsp_api_cmd_handler.go b/internal/lsp/lsp_api_cmd_handler.go new file mode 100644 index 0000000000..40156376ad --- /dev/null +++ b/internal/lsp/lsp_api_cmd_handler.go @@ -0,0 +1,150 @@ +package lsp + +import ( + "context" + "errors" + "runtime/debug" + + "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/project" +) + +var ProjectNotFoundError = errors.New("ProjectNotFoundError") + +func (s *Server) handleCustomTsServerCommand(ctx context.Context, req *lsproto.RequestMessage) error { + + defer func() { + if r := recover(); r != nil { + stack := debug.Stack() + s.logger.Log("panic running handleCustomTsServerCommand:", r, string(stack)) + s.sendResult(req.ID, &map[string]string{}) + } + }() + + params := req.Params.(*lsproto.HandleCustomLspApiCommandParams) + switch params.LspApiCommand { + case lsproto.CommandGetElementType: + { + args := params.Arguments.(*lsproto.GetElementTypeArguments) + project, file, err := s.GetProjectAndFileName(args.ProjectFileName, args.File, ctx) + if err != nil { + s.sendCustomCmdResult(req.ID, nil, err) + return nil + } + + element, err := GetTypeOfElement(ctx, project, file, &args.Range, args.ForceReturnType, args.TypeRequestKind) + s.sendCustomCmdResult(req.ID, element, err) + } + case lsproto.CommandGetSymbolType: + { + args := params.Arguments.(*lsproto.GetSymbolTypeArguments) + symbolType, err := GetSymbolType(ctx, args.ProjectId, uint64(args.TypeCheckerId), args.SymbolId) + s.sendCustomCmdResult(req.ID, symbolType, err) + } + case lsproto.CommandGetTypeProperties: + { + args := params.Arguments.(*lsproto.GetTypePropertiesArguments) + typeProperties, err := GetTypeProperties(ctx, args.ProjectId, uint64(args.TypeCheckerId), args.TypeId) + s.sendCustomCmdResult(req.ID, typeProperties, err) + } + case lsproto.CommandGetTypeProperty: + { + args := params.Arguments.(*lsproto.GetTypePropertyArguments) + symbol, err := GetTypeProperty(ctx, args.ProjectId, uint64(args.TypeCheckerId), args.TypeId, args.PropertyName) + s.sendCustomCmdResult(req.ID, symbol, err) + } + + case lsproto.CommandAreTypesMutuallyAssignable: + { + args := params.Arguments.(*lsproto.AreTypesMutuallyAssignableArguments) + result, err := AreTypesMutuallyAssignable(ctx, args.ProjectId, uint64(args.TypeCheckerId), args.Type1Id, args.Type2Id) + s.sendCustomCmdResult(req.ID, result, err) + } + case lsproto.CommandGetResolvedSignature: + { + args := params.Arguments.(*lsproto.GetResolvedSignatureArguments) + project, file, err := s.GetProjectAndFileName(args.ProjectFileName, args.File, ctx) + if err != nil { + s.sendCustomCmdResult(req.ID, nil, err) + return nil + } + + result, err := GetResolvedSignature(ctx, project, file, args.Range) + s.sendCustomCmdResult(req.ID, result, err) + } + } + snapshot, release := s.session.Snapshot() + defer release() + + CleanupProjectsCache(append(snapshot.ProjectCollection.Projects(), GetAllSelfManagedProjects(s, ctx)...), s.logger) + return nil +} + +func (s *Server) GetProjectAndFileName( + projectFileNameUri *lsproto.DocumentUri, + fileUri lsproto.DocumentUri, + ctx context.Context, +) (*project.Project, string, error) { + file := fileUri.FileName() + + snapshot, release := s.session.Snapshot() + released := false + releaseOnce := func() { + if !released { + release() + released = true + } + } + defer releaseOnce() + + if projectFileNameUri != nil { + projectFileName := projectFileNameUri.FileName() + + if IsSelfManagedProject(projectFileName) { + if p := GetOrCreateSelfManagedProjectForFile(s, projectFileName, file, ctx); p != nil { + return p, file, nil + } + } + + for _, p := range snapshot.ProjectCollection.Projects() { + if p.Name() == projectFileName && p.GetProgram().GetSourceFile(file) != nil { + return p, file, nil + } + } + + if p := GetOrCreateSelfManagedProjectForFile(s, projectFileName, file, ctx); p != nil { + return p, file, nil + } + } + + if p := snapshot.GetDefaultProject(fileUri); p != nil { + return p, file, nil + } + + releaseOnce() + + if _, err := s.session.GetLanguageService(ctx, fileUri); err == nil { + // Get a fresh snapshot since GetLanguageService may have updated it + newSnapshot, release := s.session.Snapshot() + defer release() + if p := newSnapshot.GetDefaultProject(fileUri); p != nil { + return p, file, nil + } + } + + // No project found + return nil, file, ProjectNotFoundError +} + +func (s *Server) sendCustomCmdResult(id *lsproto.ID, result *collections.OrderedMap[string, interface{}], err error) { + response := make(map[string]interface{}) + if err == nil { + response["response"] = result + } else { + errorResponse := make(map[string]interface{}) + errorResponse["error"] = err.Error() + response["response"] = errorResponse + } + s.sendResult(id, response) +} diff --git a/internal/lsp/lsp_api_convert.go b/internal/lsp/lsp_api_convert.go new file mode 100644 index 0000000000..2a0040ff97 --- /dev/null +++ b/internal/lsp/lsp_api_convert.go @@ -0,0 +1,862 @@ +package lsp + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + "sync" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/astnav" + "github.com/microsoft/typescript-go/internal/checker" + "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/jsnum" + "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/project" + "github.com/microsoft/typescript-go/internal/project/logging" + "github.com/microsoft/typescript-go/internal/scanner" +) + +var ( + //nolint + OutdatedProjectVersionError = errors.New("OutdatedTypeCheckerIdException") + //nolint + OutdatedProjectIdError = errors.New("OutdatedProjectIdException") +) + +var ( + nextProjectId = 1 + project2IdMap = make(map[*project.Project]int) + id2ProjectMap = make(map[int]*project.Project) + projectMapMutex = &sync.Mutex{} + projectCacheMap = make(map[int]*ProjectCache) +) + +type ProjectCache struct { + projectVersion uint64 + requestedTypeIds *collections.Set[checker.TypeId] + seenTypeIds map[checker.TypeId]*checker.Type + seenSymbolIds map[ast.SymbolId]*ast.Symbol +} + +func getProjectId(project *project.Project) int { + projectMapMutex.Lock() + defer projectMapMutex.Unlock() + result, ok := project2IdMap[project] + if !ok { + result = nextProjectId + nextProjectId++ + project2IdMap[project] = result + id2ProjectMap[result] = project + } + return result +} + +func getProject(projectId int) (*project.Project, bool) { + projectMapMutex.Lock() + defer projectMapMutex.Unlock() + id, ok := id2ProjectMap[projectId] + return id, ok +} + +func getProjectCache(projectId int, projectVersion uint64) *ProjectCache { + projectMapMutex.Lock() + defer projectMapMutex.Unlock() + result, ok := projectCacheMap[projectId] + if !ok || result.projectVersion != projectVersion { + result = &ProjectCache{ + projectVersion: projectVersion, + requestedTypeIds: collections.NewSetWithSizeHint[checker.TypeId](10), + seenTypeIds: make(map[checker.TypeId]*checker.Type), + seenSymbolIds: make(map[ast.SymbolId]*ast.Symbol), + } + projectCacheMap[projectId] = result + } + return result +} + +func CleanupProjectsCache(openedProjects []*project.Project, logger logging.Logger) { + openedProjectsSet := collections.NewSetWithSizeHint[*project.Project](len(openedProjects)) + for _, p := range openedProjects { + if p != nil { + openedProjectsSet.Add(p) + } + } + + removedIds := cleanupProjectsCacheImpl(openedProjectsSet) + + if removedIds.Len() > 0 && len(openedProjects) > 0 && openedProjects[0] != nil { + ids := make([]int, 0, removedIds.Len()) + for id := range removedIds.Keys() { + ids = append(ids, id) + } + logger.Logf("CleanupProjectsCache: removed project ids: %v", ids) + } +} + +func cleanupProjectsCacheImpl(openedProjectsSet *collections.Set[*project.Project]) *collections.Set[int] { + projectMapMutex.Lock() + defer projectMapMutex.Unlock() + + removedIds := collections.NewSetWithSizeHint[int](len(id2ProjectMap) + len(projectCacheMap)) + + for p := range project2IdMap { + if !openedProjectsSet.Has(p) { + delete(project2IdMap, p) + } + } + + validProjectIdsSet := collections.NewSetWithSizeHint[int](len(project2IdMap)) + for _, id := range project2IdMap { + validProjectIdsSet.Add(id) + } + + for id := range id2ProjectMap { + if !validProjectIdsSet.Has(id) { + delete(id2ProjectMap, id) + removedIds.Add(id) + } + } + + for id := range projectCacheMap { + if !validProjectIdsSet.Has(id) { + delete(projectCacheMap, id) + removedIds.Add(id) + } + } + + return removedIds +} + +var ( + lspApiObjectId = "lspApiObjectId" + lspApiObjectIdRef = "lspApiObjectIdRef" + lspApiObjectType = "lspApiObjectType" + lspApiProjectId = "lspApiProjectId" + lspApiTypeCheckerId = "lspApiTypeCheckerId" +) + +func GetTypeOfElement( + ctx context.Context, + project *project.Project, + fileName string, + Range *lsproto.Range, + forceReturnType bool, + typeRequestKind lsproto.TypeRequestKind, +) (*collections.OrderedMap[string, interface{}], error) { + projectIdNum := getProjectId(project) + projectVersion := project.ProgramLastUpdate // TODO: possible race condition here + program := project.GetProgram() + sourceFile := program.GetSourceFile(fileName) + if sourceFile == nil { + return nil, nil + } + + startOffset := scanner.GetECMAPositionOfLineAndCharacter(sourceFile, int(Range.Start.Line), int(Range.Start.Character)) + endOffset := scanner.GetECMAPositionOfLineAndCharacter(sourceFile, int(Range.End.Line), int(Range.End.Character)) + + node := astnav.GetTokenAtPosition(sourceFile, startOffset).AsNode() + for node != nil && node.End() < endOffset { + node = node.Parent + } + if node == nil || node == sourceFile.AsNode() { + return nil, nil + } + + contextFlags := typeRequestKindToContextFlags(typeRequestKind) + isContextual := contextFlags >= 0 + if isContextual && !ast.IsExpression(node) || (!isContextual && (ast.IsStringLiteral(node) || ast.IsNumericLiteral(node))) || ast.IsIdentifier(node) && ast.IsTypeReferenceNode(node.Parent) { + if node.Pos() == node.Parent.Pos() && node.End() == node.Parent.End() { + node = node.Parent + } + } + + typeChecker, done := program.GetTypeCheckerForFile(ctx, sourceFile) + defer done() + + var t *checker.Type + if contextFlags >= 0 { + t = typeChecker.GetContextualType(node, checker.ContextFlags(contextFlags)) + } else { + t = typeChecker.GetTypeAtLocation(node) + } + + if t == nil && isContextual && node.Parent.Kind == ast.KindBinaryExpression { + parentBinaryExpr := node.Parent.AsBinaryExpression() + // from getContextualType in services/completions.ts + right := parentBinaryExpr.Right + if ls.IsEqualityOperatorKind(parentBinaryExpr.OperatorToken.Kind) { + if node == right { + t = typeChecker.GetTypeAtLocation(parentBinaryExpr.Left) + } else { + t = typeChecker.GetTypeAtLocation(right) + } + } + } + if t == nil { + return nil, nil + } + + convertContext := NewConvertContext(typeChecker, projectIdNum, projectVersion) + + var typeMap *collections.OrderedMap[string, interface{}] + if forceReturnType || !convertContext.requestedTypeIds.Has(t.Id()) { + typeMap = ConvertType(t, convertContext) + convertContext.requestedTypeIds.Add(t.Id()) + } else { + typeMap = collections.NewOrderedMapWithSizeHint[string, interface{}](4) + typeMap.Set("id", t.Id()) + typeMap.Set(lspApiObjectType, "TypeObject") + } + typeMap.Set(lspApiProjectId, projectIdNum) + typeMap.Set(lspApiTypeCheckerId, projectVersion) + return typeMap, nil +} + +func getConvertContext( + ctx context.Context, + projectId int, + projectVersion uint64, +) (*ConvertContext, func(), error) { + project, ok := getProject(projectId) + if !ok { + return nil, func() {}, OutdatedProjectIdError + } + + if projectVersion != project.ProgramLastUpdate { + return nil, func() {}, OutdatedProjectVersionError + } + checker, done := project.GetProgram().GetTypeChecker(ctx) + return NewConvertContext(checker, projectId, projectVersion), func() { + done() + }, nil +} + +func GetSymbolType( + ctx context.Context, + projectId int, + projectVersion uint64, + symbolId int, +) (*collections.OrderedMap[string, interface{}], error) { + convertContext, done, err := getConvertContext(ctx, projectId, projectVersion) + defer done() + if err != nil { + return nil, err + } + + symbol, exists := convertContext.seenSymbolIds[ast.SymbolId(symbolId)] + if !exists { + return nil, errors.New("symbol not found") + } + + t := convertContext.checker.GetTypeOfSymbol(symbol) + result := ConvertType(t, convertContext) + result.Set(lspApiProjectId, projectId) + result.Set(lspApiTypeCheckerId, projectVersion) + return result, nil +} + +func GetTypeProperties( + ctx context.Context, + projectId int, + projectVersion uint64, + typeId int, +) (*collections.OrderedMap[string, interface{}], error) { + convertContext, done, err := getConvertContext(ctx, projectId, projectVersion) + defer done() + if err != nil { + return nil, err + } + + t, exists := convertContext.seenTypeIds[checker.TypeId(typeId)] + if !exists { + return nil, errors.New("type not found") + } + + result := ConvertTypeProperties(t, convertContext) + result.Set(lspApiProjectId, projectId) + result.Set(lspApiTypeCheckerId, projectVersion) + return result, nil +} + +func GetTypeProperty( + ctx context.Context, + projectId int, + projectVersion uint64, + typeId int, + propertyName string, +) (*collections.OrderedMap[string, interface{}], error) { + convertContext, done, err := getConvertContext(ctx, projectId, projectVersion) + defer done() + if err != nil { + return nil, err + } + + t, exists := convertContext.seenTypeIds[checker.TypeId(typeId)] + if !exists { + return nil, errors.New("type not found") + } + + symbol := convertContext.checker.GetPropertyOfType(t, propertyName) + if symbol == nil { + return nil, nil + } + result := ConvertSymbol(symbol, convertContext) + return result, nil +} + +func AreTypesMutuallyAssignable( + ctx context.Context, + projectId int, + projectVersion uint64, + type1Id int, + type2Id int, +) (*collections.OrderedMap[string, interface{}], error) { + convertCtx, done, err := getConvertContext(ctx, projectId, projectVersion) + defer done() + if err != nil { + return nil, err + } + + type1, exists := convertCtx.seenTypeIds[checker.TypeId(type1Id)] + if !exists { + return nil, errors.New("type1 not found") + } + + type2, exists := convertCtx.seenTypeIds[checker.TypeId(type2Id)] + if !exists { + return nil, errors.New("type2 not found") + } + + isType1To2 := convertCtx.checker.IsTypeAssignableTo(type1, type2) + isType2To1 := convertCtx.checker.IsTypeAssignableTo(type2, type1) + + areMutuallyAssignable := isType1To2 && isType2To1 + + result := collections.NewOrderedMapWithSizeHint[string, interface{}](2) + result.Set("areMutuallyAssignable", areMutuallyAssignable) + return result, nil +} + +func GetResolvedSignature( + ctx context.Context, + project *project.Project, + fileName string, + Range lsproto.Range, +) (*collections.OrderedMap[string, interface{}], error) { + projectId := getProjectId(project) + projectVersion := project.ProgramLastUpdate + program := project.Program + sourceFile := program.GetSourceFile(fileName) + if sourceFile == nil { + return nil, nil + } + + startOffset := scanner.GetECMAPositionOfLineAndCharacter(sourceFile, int(Range.Start.Line), int(Range.Start.Character)) + endOffset := scanner.GetECMAPositionOfLineAndCharacter(sourceFile, int(Range.End.Line), int(Range.End.Character)) + + typeChecker, done := program.GetTypeCheckerForFile(ctx, sourceFile) + defer done() + + // Find the node at the given position + node := astnav.GetTokenAtPosition(sourceFile, startOffset).AsNode() + for node != nil && node.End() < endOffset { + node = node.Parent + } + + if node == nil || node == sourceFile.AsNode() { + return nil, nil + } + + // Find the call expression + for !ast.IsCallLikeExpression(node) { + node = node.Parent + if node == nil || node == sourceFile.AsNode() { + return nil, nil + } + } + + // Get the resolved signature + signature := typeChecker.GetResolvedSignature(node) + if signature == nil { + return nil, nil + } + + // Return the signature information + convertCtx := NewConvertContext(typeChecker, projectId, projectVersion) + prepared := ConvertSignature(signature, convertCtx) + prepared.Set(lspApiTypeCheckerId, projectVersion) + prepared.Set(lspApiProjectId, projectId) + prepared.Set(lspApiObjectType, "SignatureObject") + + return prepared, nil +} + +type ConvertContext struct { + nextId int + createdObjectsLspApiIds map[interface{}]int + checker *checker.Checker + requestedTypeIds *collections.Set[checker.TypeId] + seenTypeIds map[checker.TypeId]*checker.Type + seenSymbolIds map[ast.SymbolId]*ast.Symbol +} + +func NewConvertContext(checker *checker.Checker, projectId int, projectVersion uint64) *ConvertContext { + cache := getProjectCache(projectId, projectVersion) + return &ConvertContext{ + nextId: 0, + createdObjectsLspApiIds: make(map[interface{}]int), + checker: checker, + requestedTypeIds: cache.requestedTypeIds, + seenTypeIds: cache.seenTypeIds, + seenSymbolIds: cache.seenSymbolIds, + } +} + +func (ctx *ConvertContext) GetLspApiObjectId(obj interface{}) (int, bool) { + id, exists := ctx.createdObjectsLspApiIds[obj] + return id, exists +} + +func (ctx *ConvertContext) RegisterLspApiObject(obj interface{}) int { + id := ctx.nextId + ctx.nextId++ + ctx.createdObjectsLspApiIds[obj] = id + return id +} + +func FindReferenceOrConvert(sourceObj interface{}, + convertTarget func(lspApiObjectId int) *collections.OrderedMap[string, interface{}], ctx *ConvertContext, +) *collections.OrderedMap[string, interface{}] { + lspApiObjectId, exists := ctx.GetLspApiObjectId(sourceObj) + if exists { + result := collections.NewOrderedMapWithSizeHint[string, interface{}](1) + result.Set(lspApiObjectIdRef, lspApiObjectId) + return result + } + + lspApiObjectId = ctx.RegisterLspApiObject(sourceObj) + newObject := convertTarget(lspApiObjectId) + return newObject +} + +func ConvertType(t *checker.Type, ctx *ConvertContext) *collections.OrderedMap[string, interface{}] { + return FindReferenceOrConvert(t, func(_lspApiObjectId int) *collections.OrderedMap[string, interface{}] { + tscType := collections.NewOrderedMapWithSizeHint[string, interface{}](15) + tscType.Set(lspApiObjectId, _lspApiObjectId) + tscType.Set(lspApiObjectType, "TypeObject") + tscType.Set("flags", strconv.Itoa(int(t.Flags()))) // Flags are u32, LSP number is s32 + + // Handle aliasTypeArguments + aliasType := t.Alias() + if aliasType != nil && aliasType.TypeArguments() != nil { + aliasArgs := make([]interface{}, 0) + for _, t := range aliasType.TypeArguments() { + aliasArgs = append(aliasArgs, ConvertType(t, ctx)) + } + tscType.Set("aliasTypeArguments", aliasArgs) + } + + if t.Flags()&checker.TypeFlagsObject != 0 { + if target := t.Target(); target != nil { + tscType.Set("target", ConvertType(target, ctx)) + } + + if (t.ObjectFlags() & checker.ObjectFlagsReference) != 0 { + resolvedArgs := make([]interface{}, 0) + typeArgs := ctx.checker.GetTypeArguments(t) + for _, t := range typeArgs { + // Filter out 'this' type + if t.Flags()&checker.TypeFlagsTypeParameter == 0 || !t.AsTypeParameter().IsThisType() { + resolvedArgs = append(resolvedArgs, ConvertType(t, ctx)) + } + } + tscType.Set("resolvedTypeArguments", resolvedArgs) + } + } + + // For UnionOrIntersection and TemplateLiteral types + if t.Flags()&(checker.TypeFlagsUnionOrIntersection|checker.TypeFlagsTemplateLiteral) != 0 { + typesArr := t.Types() + types := make([]interface{}, 0) + for _, t := range typesArr { + types = append(types, ConvertType(t, ctx)) + } + tscType.Set("types", types) + } + + // For Literal types with freshType + if t.Flags()&checker.TypeFlagsLiteral != 0 { + literalType := t.AsLiteralType() + if freshType := literalType.FreshType(); freshType != nil { + tscType.Set("freshType", ConvertType(freshType, ctx)) + } + } + + // For TypeParameter types + if t.Flags()&checker.TypeFlagsTypeParameter != 0 { + if constraint := ctx.checker.GetBaseConstraintOfType(t); constraint != nil { + tscType.Set("constraint", ConvertType(constraint, ctx)) + } + } + + // For Index types + if t.Flags()&checker.TypeFlagsIndex != 0 { + indexType := t.AsIndexType() + tscType.Set("type", ConvertType(indexType.Target(), ctx)) + } + + // For IndexedAccess types + if t.Flags()&checker.TypeFlagsIndexedAccess != 0 { + indexedAccessType := t.AsIndexedAccessType() + tscType.Set("objectType", ConvertType(indexedAccessType.ObjectType(), ctx)) + tscType.Set("indexType", ConvertType(indexedAccessType.IndexType(), ctx)) + } + + // For Conditional types + if t.Flags()&checker.TypeFlagsConditional != 0 { + conditionalType := t.AsConditionalType() + tscType.Set("checkType", ConvertType(conditionalType.CheckType(), ctx)) + tscType.Set("extendsType", ConvertType(conditionalType.ExtendsType(), ctx)) + } + + // For Substitution types + if t.Flags()&checker.TypeFlagsSubstitution != 0 { + substitutionType := t.AsSubstitutionType() + tscType.Set("baseType", ConvertType(substitutionType.BaseType(), ctx)) + } + + // Add symbol and aliasSymbol + if t.Symbol() != nil { + tscType.Set("symbol", ConvertSymbol(t.Symbol(), ctx)) + } + if t.Alias() != nil && t.Alias().Symbol() != nil { + tscType.Set("aliasSymbol", ConvertSymbol(t.Alias().Symbol(), ctx)) + } + + // Handle object flags + if t.Flags()&checker.TypeFlagsObject != 0 { + tscType.Set("objectFlags", strconv.Itoa(int(t.ObjectFlags()))) + } + + // Handle literal type values + if t.Flags()&checker.TypeFlagsLiteral != 0 { + literalType := t.AsLiteralType() + if t.Flags()&checker.TypeFlagsBigIntLiteral != 0 { + // Convert BigInt literal value + tscType.Set("value", ConvertPseudoBigInt(literalType.Value().(jsnum.PseudoBigInt), ctx)) + } else { + // For other literal types + tscType.Set("value", literalType.Value()) + } + } + + // Handle enum literal + if t.Flags()&checker.TypeFlagsEnumLiteral != 0 { + tscType.Set("nameType", getEnumQualifiedName(t)) + } + + // Handle template literal + if t.Flags()&checker.TypeFlagsTemplateLiteral != 0 { + templateLiteralType := t.AsTemplateLiteralType() + tscType.Set("texts", templateLiteralType.Texts()) + } + + // Handle type parameter isThisType + if t.Flags()&checker.TypeFlagsTypeParameter != 0 { + typeParam := t.AsTypeParameter() + if typeParam.IsThisType() { + tscType.Set("isThisType", true) + } + } + + // Handle intrinsic name + if t.Flags()&checker.TypeFlagsIntrinsic != 0 { + intrinsicType := t.AsIntrinsicType() + if intrinsicType != nil && intrinsicType.IntrinsicName() != "" { + tscType.Set("intrinsicName", intrinsicType.IntrinsicName()) + } + } + + // Handle tuple element flags + if t.Flags()&checker.TypeFlagsObject != 0 && t.ObjectFlags()&checker.ObjectFlagsTuple != 0 { + tupleType := t.AsTupleType() + elementInfos := tupleType.ElementInfos() + elementFlags := make([]interface{}, len(elementInfos)) + for i, elementInfo := range elementInfos { + elementFlags[i] = strconv.Itoa(int(elementInfo.ElementFlags())) + } + tscType.Set("elementFlags", elementFlags) + } + + // Add type ID + typeId := t.Id() + tscType.Set("id", typeId) + ctx.seenTypeIds[typeId] = t + return tscType + }, ctx) +} + +func getSourceFileParent(node *ast.Node) *ast.Node { + if ast.IsSourceFile(node) { + return nil + } + current := node.Parent + for current != nil { + if ast.IsSourceFile(current) { + return current + } + current = current.Parent + } + return nil +} + +func ConvertNode(node *ast.Node, ctx *ConvertContext) *collections.OrderedMap[string, interface{}] { + return FindReferenceOrConvert(node, func(_lspApiObjectId int) *collections.OrderedMap[string, interface{}] { + result := collections.NewOrderedMapWithSizeHint[string, interface{}](1) + result.Set(lspApiObjectId, _lspApiObjectId) + + if ast.IsSourceFile(node) { + result.Set(lspApiObjectType, "SourceFileObject") + result.Set("fileName", node.AsSourceFile().FileName()) + return result + } else { + result.Set(lspApiObjectType, "NodeObject") + sourceFileParent := getSourceFileParent(node) + if sourceFileParent == nil || node.Pos() == -1 || node.End() == -1 { + if sourceFileParent != nil { + result.Set("parent", ConvertNode(sourceFileParent, ctx)) + } + return result + } + + // Add range information + if sourceFileParent != nil { + sourceFile := sourceFileParent.AsSourceFile() + startLine, startChar := scanner.GetECMALineAndCharacterOfPosition(sourceFile, node.Pos()) + endLine, endChar := scanner.GetECMALineAndCharacterOfPosition(sourceFile, node.End()) + result.Set("range", &lsproto.Range{ + Start: lsproto.Position{Line: uint32(startLine), Character: uint32(startChar)}, + End: lsproto.Position{Line: uint32(endLine), Character: uint32(endChar)}, + }) + } + + // Add parent information + if sourceFileParent != nil { + result.Set("parent", ConvertNode(sourceFileParent, ctx)) + } + + // Check for computed property + if node.Name() != nil && ast.IsComputedPropertyName(node.Name()) { + result.Set("computedProperty", true) + } + return result + } + }, ctx) +} + +func getEnumQualifiedName(t *checker.Type) string { + if t == nil || t.Symbol() == nil { + return "" + } + + qName := "" + current := t.Symbol().Parent + for current != nil && !(current.ValueDeclaration != nil && ast.IsSourceFile(current.ValueDeclaration)) { + if qName == "" { + qName = current.Name + } else { + qName = current.Name + "." + qName + } + current = current.Parent + } + return qName +} + +func ConvertPseudoBigInt(pseudoBigInt jsnum.PseudoBigInt, ctx *ConvertContext) *collections.OrderedMap[string, interface{}] { + return FindReferenceOrConvert(pseudoBigInt, func(_lspApiObjectId int) *collections.OrderedMap[string, interface{}] { + result := collections.NewOrderedMapWithSizeHint[string, interface{}](4) + result.Set(lspApiObjectId, _lspApiObjectId) + result.Set("negative", pseudoBigInt.Negative) + result.Set("base10Value", pseudoBigInt.Base10Value) + return result + }, ctx) +} + +func ConvertIndexInfo(indexInfo *checker.IndexInfo, ctx *ConvertContext) *collections.OrderedMap[string, interface{}] { + return FindReferenceOrConvert(indexInfo, func(_lspApiObjectId int) *collections.OrderedMap[string, interface{}] { + result := collections.NewOrderedMapWithSizeHint[string, interface{}](7) + result.Set(lspApiObjectId, _lspApiObjectId) + result.Set(lspApiObjectType, "IndexInfo") + result.Set("keyType", ConvertType(indexInfo.KeyType(), ctx)) + result.Set("type", ConvertType(indexInfo.ValueType(), ctx)) + result.Set("isReadonly", indexInfo.IsReadonly()) + + if indexInfo.Declaration() != nil { + result.Set("declaration", ConvertNode(indexInfo.Declaration(), ctx)) + } + return result + }, ctx) +} + +func ConvertSymbol(symbol *ast.Symbol, ctx *ConvertContext) *collections.OrderedMap[string, interface{}] { + return FindReferenceOrConvert(symbol, func(_lspApiObjectId int) *collections.OrderedMap[string, interface{}] { + result := collections.NewOrderedMapWithSizeHint[string, interface{}](7) + result.Set(lspApiObjectId, _lspApiObjectId) + result.Set(lspApiObjectType, "SymbolObject") + result.Set("flags", strconv.Itoa(int(symbol.Flags))) + escapedName := symbol.Name + if strings.Contains(escapedName, ast.InternalSymbolNamePrefix) { + escapedName = strings.ReplaceAll(escapedName, ast.InternalSymbolNamePrefix, "__") + } + result.Set("escapedName", escapedName) + + if symbol.Declarations != nil && len(symbol.Declarations) > 0 { + declarations := make([]interface{}, 0) + for _, d := range symbol.Declarations { + declarations = append(declarations, ConvertNode(d, ctx)) + } + result.Set("declarations", declarations) + } + + if symbol.ValueDeclaration != nil { + result.Set("valueDeclaration", ConvertNode(symbol.ValueDeclaration, ctx)) + } + + // Get symbol ID + symbolId := ast.GetSymbolId(symbol) + ctx.seenSymbolIds[symbolId] = symbol + result.Set("id", symbolId) + + return result + }, ctx) +} + +func ConvertTypeProperties(t *checker.Type, ctx *ConvertContext) *collections.OrderedMap[string, interface{}] { + return FindReferenceOrConvert(t, func(_lspApiObjectId int) *collections.OrderedMap[string, interface{}] { + prepared := collections.NewOrderedMapWithSizeHint[string, interface{}](10) + prepared.Set(lspApiObjectId, _lspApiObjectId) + prepared.Set(lspApiObjectType, "TypeObject") + prepared.Set("flags", strconv.Itoa(int(t.Flags()))) + prepared.Set("objectFlags", strconv.Itoa(int(t.ObjectFlags()))) + + if t.Flags()&checker.TypeFlagsObject != 0 { + assignObjectTypeProperties(t.AsObjectType(), ctx, prepared) + } + if t.Flags()&checker.TypeFlagsUnionOrIntersection != 0 { + assignUnionOrIntersectionTypeProperties(t.AsUnionOrIntersectionType(), ctx, prepared) + } + if t.Flags()&checker.TypeFlagsConditional != 0 { + assignConditionalTypeProperties(t.AsConditionalType(), ctx, prepared) + } + return prepared + }, ctx) +} + +func assignObjectTypeProperties(t *checker.ObjectType, ctx *ConvertContext, tscType *collections.OrderedMap[string, interface{}]) { + constructSignatures := make([]interface{}, 0) + for _, s := range ctx.checker.GetConstructSignatures(t.AsType()) { + constructSignatures = append(constructSignatures, ConvertSignature(s, ctx)) + } + tscType.Set("constructSignatures", constructSignatures) + + callSignatures := make([]interface{}, 0) + for _, s := range ctx.checker.GetCallSignatures(t.AsType()) { + callSignatures = append(callSignatures, ConvertSignature(s, ctx)) + } + tscType.Set("callSignatures", callSignatures) + + properties := make([]interface{}, 0) + for _, p := range ctx.checker.GetPropertiesOfType(t.AsType()) { + properties = append(properties, ConvertSymbol(p, ctx)) + } + tscType.Set("properties", properties) + + indexInfos := make([]interface{}, 0) + for _, info := range ctx.checker.GetIndexInfosOfType(t.AsType()) { + indexInfos = append(indexInfos, ConvertIndexInfo(info, ctx)) + } + tscType.Set("indexInfos", indexInfos) +} + +func assignUnionOrIntersectionTypeProperties(t *checker.UnionOrIntersectionType, ctx *ConvertContext, tscType *collections.OrderedMap[string, interface{}]) { + resolvedProperties := make([]interface{}, 0) + for _, p := range ctx.checker.GetPropertiesOfType(t.AsType()) { + resolvedProperties = append(resolvedProperties, ConvertSymbol(p, ctx)) + } + tscType.Set("resolvedProperties", resolvedProperties) + + callSignatures := make([]interface{}, 0) + for _, s := range ctx.checker.GetCallSignatures(t.AsType()) { + callSignatures = append(callSignatures, ConvertSignature(s, ctx)) + } + tscType.Set("callSignatures", callSignatures) + + constructSignatures := make([]interface{}, 0) + for _, s := range ctx.checker.GetConstructSignatures(t.AsType()) { + constructSignatures = append(constructSignatures, ConvertSignature(s, ctx)) + } + tscType.Set("constructSignatures", constructSignatures) +} + +func assignConditionalTypeProperties(t *checker.ConditionalType, ctx *ConvertContext, tscType *collections.OrderedMap[string, interface{}]) { + trueType := ctx.checker.GetTrueTypeFromConditionalType(t.AsType()) + if trueType != nil { + tscType.Set("resolvedTrueType", ConvertType(trueType, ctx)) + } + + falseType := ctx.checker.GetFalseTypeFromConditionalType(t.AsType()) + if falseType != nil { + tscType.Set("resolvedFalseType", ConvertType(falseType, ctx)) + } +} + +func ConvertSignature(signature *checker.Signature, ctx *ConvertContext) *collections.OrderedMap[string, interface{}] { + return FindReferenceOrConvert(signature, func(_lspApiObjectId int) *collections.OrderedMap[string, interface{}] { + result := collections.NewOrderedMapWithSizeHint[string, interface{}](5) + result.Set(lspApiObjectId, _lspApiObjectId) + result.Set(lspApiObjectType, "SignatureObject") + + if declaration := signature.Declaration(); declaration != nil { + result.Set("declaration", ConvertNode(declaration, ctx)) + } + + if returnType := ctx.checker.GetReturnTypeOfSignature(signature); returnType != nil { + result.Set("resolvedReturnType", ConvertType(returnType, ctx)) + } + + parameters := make([]interface{}, 0) + for _, param := range signature.Parameters() { + parameters = append(parameters, ConvertSymbol(param, ctx)) + } + result.Set("parameters", parameters) + + result.Set("flags", strconv.Itoa(convertSignatureFlags(signature))) + + return result + }, ctx) +} + +func convertSignatureFlags(signature *checker.Signature) int { + var result = 0 + if signature.IsSignatureCandidateForOverloadFailure() { + result = result | (1 << 7) + } + return result +} + +func typeRequestKindToContextFlags(typeRequestKind lsproto.TypeRequestKind) int { + switch typeRequestKind { + case lsproto.TypeRequestKindDefault: + return -1 + case lsproto.TypeRequestKindContextual: + return 0 + case lsproto.TypeRequestKindContextualCompletions: + return 4 + default: + panic(fmt.Sprintf("Unexpected typeRequestKind %s", typeRequestKind)) + } +} diff --git a/internal/lsp/lsp_api_self_managed_projects.go b/internal/lsp/lsp_api_self_managed_projects.go new file mode 100644 index 0000000000..ad6aae45fe --- /dev/null +++ b/internal/lsp/lsp_api_self_managed_projects.go @@ -0,0 +1,124 @@ +package lsp + +import ( + "context" + "sync" + "time" + + "github.com/microsoft/typescript-go/internal/project" +) + +type SelfManagedProjectInfo struct { + Project *project.Project + LastAccessed int64 + FilesLastUpdated map[string]int64 +} + +var ( + mu sync.Mutex + projectInfoByName = make(map[string]*SelfManagedProjectInfo) +) + +func GetAllSelfManagedProjects(s *Server, ctx context.Context) []*project.Project { + cleanupStaleProjects(s, ctx) + + mu.Lock() + defer mu.Unlock() + + var projects []*project.Project + for _, info := range projectInfoByName { + projects = append(projects, info.Project) + } + return projects +} + +func IsSelfManagedProject(projectFileName string) bool { + mu.Lock() + defer mu.Unlock() + return projectInfoByName[projectFileName] != nil +} + +func GetOrCreateSelfManagedProjectForFile(s *Server, projectFileName string, file string, ctx context.Context) *project.Project { + defer cleanupStaleProjects(s, ctx) + return getProjectForFileImpl(s, projectFileName, file, ctx) +} + +func getProjectForFileImpl(s *Server, projectFileName string, file string, ctx context.Context) *project.Project { + nowMs := time.Now().UnixMilli() + fileMod := getFileModTimeMs(s, file) + + mu.Lock() + defer mu.Unlock() + + if info := projectInfoByName[projectFileName]; info != nil { + prevMod, hadPrev := info.FilesLastUpdated[file] + if !hadPrev || fileMod == prevMod { + info.LastAccessed = nowMs + return info.Project + } + + closeProject(s, projectFileName, ctx) + delete(projectInfoByName, projectFileName) + } + + newProject := createNewSelfManagedProject(s, projectFileName, file, ctx) + if newProject == nil { + return nil + } + + newInfo := &SelfManagedProjectInfo{ + Project: newProject, + LastAccessed: nowMs, + FilesLastUpdated: make(map[string]int64), + } + newInfo.FilesLastUpdated[file] = fileMod + projectInfoByName[projectFileName] = newInfo + + return newProject +} + +func cleanupStaleProjects(s *Server, ctx context.Context) { + nowMs := time.Now().UnixMilli() + const ttl = int64(5 * time.Minute / time.Millisecond) + + mu.Lock() + defer mu.Unlock() + + for name, info := range projectInfoByName { + if nowMs-info.LastAccessed > ttl { + closeProject(s, name, ctx) + delete(projectInfoByName, name) + } + } +} + +func getFileModTimeMs(s *Server, file string) int64 { + if !s.fs.FileExists(file) { + return 0 + } + fi := s.fs.Stat(file) + if fi == nil { + return 0 + } + return fi.ModTime().UnixMilli() +} + +func createNewSelfManagedProject(s *Server, projectFileName string, file string, ctx context.Context) *project.Project { + var p *project.Project + var err error + + if p, err = s.session.OpenProject(ctx, projectFileName); err == nil && p != nil { + if p.GetProgram() != nil && p.GetProgram().GetSourceFile(file) != nil { + return p + } + } + + return nil +} + +func closeProject(s *Server, projectFileName string, ctx context.Context) { + err := s.session.CloseProject(ctx, projectFileName) + if err != nil { + s.logger.Log("SelfManagedProjects:: Error closing project " + projectFileName + ": " + err.Error()) + } +} diff --git a/internal/lsp/lsproto/lsp_api_proto.go b/internal/lsp/lsproto/lsp_api_proto.go new file mode 100644 index 0000000000..840a73a39a --- /dev/null +++ b/internal/lsp/lsproto/lsp_api_proto.go @@ -0,0 +1,143 @@ +package lsproto + +import ( + //nolint + "encoding/json" + "fmt" +) + +const ( + MethodHandleCustomLspApiCommand Method = "$/handleCustomLspApiCommand" +) + +type HandleCustomLspApiCommandParams struct { + LspApiCommand LspApiCommand `json:"lspApiCommand"` + Arguments interface{} `json:"args"` +} + +type LspApiCommand string + +const ( + CommandGetElementType LspApiCommand = "getElementType" + CommandGetSymbolType LspApiCommand = "getSymbolType" + CommandGetTypeProperties LspApiCommand = "getTypeProperties" + CommandGetTypeProperty LspApiCommand = "getTypeProperty" + CommandAreTypesMutuallyAssignable LspApiCommand = "areTypesMutuallyAssignable" + CommandGetResolvedSignature LspApiCommand = "getResolvedSignature" +) + +type TypeRequestKind string + +const ( + TypeRequestKindDefault TypeRequestKind = "Default" + TypeRequestKindContextual TypeRequestKind = "Contextual" + TypeRequestKindContextualCompletions TypeRequestKind = "ContextualCompletions" +) + +type GetElementTypeArguments struct { + File DocumentUri `json:"file"` + Range Range `json:"range"` + TypeRequestKind TypeRequestKind `json:"typeRequestKind"` + ProjectFileName *DocumentUri `json:"projectFileName,omitempty"` + ForceReturnType bool `json:"forceReturnType"` +} + +type GetSymbolTypeArguments struct { + TypeCheckerId int `json:"typeCheckerId"` + ProjectId int `json:"projectId"` + SymbolId int `json:"symbolId"` +} + +type GetTypePropertiesArguments struct { + TypeCheckerId int `json:"typeCheckerId"` + ProjectId int `json:"projectId"` + TypeId int `json:"typeId"` +} + +type GetTypePropertyArguments struct { + TypeCheckerId int `json:"typeCheckerId"` + ProjectId int `json:"projectId"` + TypeId int `json:"typeId"` + PropertyName string `json:"propertyName"` +} + +type AreTypesMutuallyAssignableArguments struct { + TypeCheckerId int `json:"typeCheckerId"` + ProjectId int `json:"projectId"` + Type1Id int `json:"type1Id"` + Type2Id int `json:"type2Id"` +} + +type GetResolvedSignatureArguments struct { + File DocumentUri `json:"file"` + Range Range `json:"range"` + ProjectFileName *DocumentUri `json:"projectFileName"` +} + +func (p *HandleCustomLspApiCommandParams) UnmarshalJSON(data []byte) error { + // First unmarshal into a temporary structure to get the lspApiCommand + type TempParams struct { + LspApiCommand LspApiCommand `json:"lspApiCommand"` + Arguments json.RawMessage `json:"args"` + } + + var temp TempParams + if err := json.Unmarshal(data, &temp); err != nil { + return fmt.Errorf("failed to unmarshal HandleCustomTsServerCommandParams: %w", err) + } + + // Set the LspApiCommand + p.LspApiCommand = temp.LspApiCommand + + // Based on LspApiCommand, unmarshal args into the appropriate type + var args interface{} + switch temp.LspApiCommand { + case CommandGetElementType: + var typedArgs GetElementTypeArguments + if err := json.Unmarshal(temp.Arguments, &typedArgs); err != nil { + return fmt.Errorf("failed to unmarshal GetElementTypeArguments: %w", err) + } + args = &typedArgs + + case CommandGetSymbolType: + var typedArgs GetSymbolTypeArguments + if err := json.Unmarshal(temp.Arguments, &typedArgs); err != nil { + return fmt.Errorf("failed to unmarshal GetSymbolTypeArguments: %w", err) + } + args = &typedArgs + + case CommandGetTypeProperties: + var typedArgs GetTypePropertiesArguments + if err := json.Unmarshal(temp.Arguments, &typedArgs); err != nil { + return fmt.Errorf("failed to unmarshal GetTypePropertiesArguments: %w", err) + } + args = &typedArgs + + case CommandGetTypeProperty: + var typedArgs GetTypePropertyArguments + if err := json.Unmarshal(temp.Arguments, &typedArgs); err != nil { + return fmt.Errorf("failed to unmarshal GetTypePropertyArguments: %w", err) + } + args = &typedArgs + + case CommandAreTypesMutuallyAssignable: + var typedArgs AreTypesMutuallyAssignableArguments + if err := json.Unmarshal(temp.Arguments, &typedArgs); err != nil { + return fmt.Errorf("failed to unmarshal AreTypesMutuallyAssignableArguments: %w", err) + } + args = &typedArgs + + case CommandGetResolvedSignature: + var typedArgs GetResolvedSignatureArguments + if err := json.Unmarshal(temp.Arguments, &typedArgs); err != nil { + return fmt.Errorf("failed to unmarshal GetResolvedSignatureArguments: %w", err) + } + args = &typedArgs + + default: + return fmt.Errorf("unknown LspApiCommand: %s", temp.LspApiCommand) + } + + p.Arguments = args + return nil +} diff --git a/internal/lsp/lsproto/lsp_generated.go b/internal/lsp/lsproto/lsp_generated.go index a93e686a73..817be80aaf 100644 --- a/internal/lsp/lsproto/lsp_generated.go +++ b/internal/lsp/lsproto/lsp_generated.go @@ -23108,6 +23108,8 @@ func unmarshalParams(method Method, data []byte) (any, error) { return unmarshalPtrTo[CancelParams](data) case MethodProgress: return unmarshalPtrTo[ProgressParams](data) + case MethodHandleCustomLspApiCommand: + return unmarshalPtrTo[HandleCustomLspApiCommandParams](data) default: return unmarshalAny(data) } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 17856f8d37..f7b38fea44 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -497,6 +497,11 @@ func (s *Server) sendResponse(resp *lsproto.ResponseMessage) { func (s *Server) handleRequestOrNotification(ctx context.Context, req *lsproto.RequestMessage) error { ctx = lsproto.WithClientCapabilities(ctx, &s.clientCapabilities) + switch req.Params.(type) { + case *lsproto.HandleCustomLspApiCommandParams: + return s.handleCustomTsServerCommand(ctx, req) + } + if handler := handlers()[req.Method]; handler != nil { return handler(s, ctx, req) } diff --git a/internal/project/api.go b/internal/project/api.go index ad96de170f..1cbdf2ed3b 100644 --- a/internal/project/api.go +++ b/internal/project/api.go @@ -4,6 +4,7 @@ import ( "context" "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/tspath" ) func (s *Session) OpenProject(ctx context.Context, configFileName string) (*Project, error) { @@ -27,3 +28,17 @@ func (s *Session) OpenProject(ctx context.Context, configFileName string) (*Proj return project, nil } + +// Because flushChanges is private +func (s *Session) CloseProject(ctx context.Context, configFileName string) error { + fileChanges, overlays, ataChanges, _ := s.flushChanges(ctx) + newSnapshot := s.UpdateSnapshot(ctx, overlays, SnapshotChange{ + fileChanges: fileChanges, + ataChanges: ataChanges, + apiRequest: &APISnapshotRequest{ + CloseProjects: collections.NewSetFromItems[tspath.Path](s.toPath(configFileName)), + }, + }) + + return newSnapshot.apiError +} diff --git a/package.json b/package.json index 45cb9dabd2..8880d97b5c 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "scripts": { "build": "hereby build", "build:watch": "hereby build:watch", + "build:debug": "hereby build --debug", "build:watch:debug": "hereby build:watch --debug", "test": "hereby test", "api:build": "npm run -w @typescript/api build",