From 4762754be8d7ec20a7f1e1a01ccf0e44ec9b5575 Mon Sep 17 00:00:00 2001 From: CatBraaain <84499939+CatBraaain@users.noreply.github.com> Date: Wed, 17 Jun 2026 00:47:05 +0900 Subject: [PATCH] fix: handle undefined initializationOptions in LSP params --- languageserver/src/connection.ts | 436 +++++++++++++++---------------- 1 file changed, 218 insertions(+), 218 deletions(-) diff --git a/languageserver/src/connection.ts b/languageserver/src/connection.ts index 179b05c5..b3a72de6 100644 --- a/languageserver/src/connection.ts +++ b/languageserver/src/connection.ts @@ -1,218 +1,218 @@ -import { - documentLinks, - getCodeActions, - getInlayHints, - hover, - validate, - ValidationConfig -} from "@actions/languageservice"; -import {registerLogger, setLogLevel} from "@actions/languageservice/log"; -import {clearCache, clearCacheEntry} from "@actions/languageservice/utils/workflow-cache"; -import {Octokit} from "@octokit/rest"; -import { - CodeAction, - CodeActionKind, - CodeActionParams, - CompletionItem, - Connection, - DocumentLink, - DocumentLinkParams, - ExecuteCommandParams, - Hover, - HoverParams, - InitializeParams, - InitializeResult, - InlayHint, - InlayHintParams, - TextDocumentIdentifier, - TextDocumentPositionParams, - TextDocuments, - TextDocumentSyncKind -} from "vscode-languageserver"; -import {TextDocument} from "vscode-languageserver-textdocument"; -import {getClient} from "./client.js"; -import {Commands} from "./commands.js"; -import {contextProviders} from "./context-providers.js"; -import {descriptionProvider} from "./description-provider.js"; -import {FeatureFlags} from "@actions/expressions"; -import {getFileProvider} from "./file-provider.js"; -import {InitializationOptions, RepositoryContext} from "./initializationOptions.js"; -import {onCompletion} from "./on-completion.js"; -import {ReadFileRequest, Requests} from "./request.js"; -import {getActionsMetadataProvider} from "./utils/action-metadata.js"; -import {TTLCache} from "./utils/cache.js"; -import {timeOperation} from "./utils/timer.js"; -import {valueProviders} from "./value-providers.js"; - -export function initConnection(connection: Connection) { - const documents: TextDocuments = new TextDocuments(TextDocument); - - let client: Octokit | undefined; - let repos: RepositoryContext[] = []; - const cache = new TTLCache(); - - let hasWorkspaceFolderCapability = false; - let featureFlags = new FeatureFlags(); - - // Register remote console logger with language service - registerLogger(connection.console); - - connection.onInitialize((params: InitializeParams) => { - const capabilities = params.capabilities; - - hasWorkspaceFolderCapability = !!(capabilities.workspace && !!capabilities.workspace.workspaceFolders); - - const options = params.initializationOptions as InitializationOptions; - - if (options.sessionToken) { - client = getClient(options.sessionToken, options.userAgent, options.gitHubApiUrl); - } - - if (options.repos) { - repos = options.repos; - } - - if (options.logLevel !== undefined) { - setLogLevel(options.logLevel); - } - - featureFlags = new FeatureFlags(options.experimentalFeatures); - - const result: InitializeResult = { - capabilities: { - textDocumentSync: TextDocumentSyncKind.Full, - completionProvider: { - resolveProvider: false, - triggerCharacters: [":", "."] - }, - hoverProvider: true, - documentLinkProvider: { - resolveProvider: false - }, - inlayHintProvider: true, - codeActionProvider: { - codeActionKinds: [CodeActionKind.QuickFix] - } - } - }; - - if (hasWorkspaceFolderCapability) { - result.capabilities.workspace = { - workspaceFolders: { - supported: true - } - }; - } - - return result; - }); - - connection.onInitialized(() => { - const enabledFeatures = featureFlags.getEnabledFeatures(); - if (enabledFeatures.length > 0) { - connection.console.info(`Experimental features enabled: ${enabledFeatures.join(", ")}`); - } - - if (hasWorkspaceFolderCapability) { - connection.workspace.onDidChangeWorkspaceFolders(() => { - clearCache(); - }); - } - }); - - // The content of a text document has changed. This event is emitted - // when the text document first opened or when its content has changed. - documents.onDidChangeContent(change => { - clearCacheEntry(change.document.uri); - return timeOperation("validation", async () => await validateTextDocument(change.document)); - }); - - async function validateTextDocument(textDocument: TextDocument): Promise { - const repoContext = repos.find(repo => textDocument.uri.startsWith(repo.workspaceUri)); - - const config: ValidationConfig = { - valueProviderConfig: valueProviders(client, repoContext, cache), - contextProviderConfig: contextProviders(client, repoContext, cache), - actionsMetadataProvider: getActionsMetadataProvider(client, cache), - fileProvider: getFileProvider(client, cache, repoContext?.workspaceUri, async path => { - return await connection.sendRequest(Requests.ReadFile, {path} satisfies ReadFileRequest); - }), - featureFlags - }; - - const result = await validate(textDocument, config); - await connection.sendDiagnostics({uri: textDocument.uri, diagnostics: result}); - } - - connection.onCompletion(async ({position, textDocument}: TextDocumentPositionParams): Promise => { - return timeOperation( - "completion", - async () => - await onCompletion( - connection, - position, - getDocument(documents, textDocument), - client, - repos.find(repo => textDocument.uri.startsWith(repo.workspaceUri)), - cache, - featureFlags - ) - ); - }); - - connection.onHover(async ({position, textDocument}: HoverParams): Promise => { - return timeOperation("hover", async () => { - const repoContext = repos.find(repo => textDocument.uri.startsWith(repo.workspaceUri)); - return await hover(getDocument(documents, textDocument), position, { - descriptionProvider: descriptionProvider(client, cache), - contextProviderConfig: repoContext && contextProviders(client, repoContext, cache), - fileProvider: getFileProvider(client, cache, repoContext?.workspaceUri, async path => { - return await connection.sendRequest(Requests.ReadFile, {path}); - }), - featureFlags - }); - }); - }); - - connection.onRequest("workspace/executeCommand", async (params: ExecuteCommandParams) => { - if (params.command === Commands.ClearCache) { - cache.clear(); - await Promise.all(documents.all().map(doc => validateTextDocument(doc))); - } - }); - - connection.onDocumentLinks(async ({textDocument}: DocumentLinkParams): Promise => { - const repoContext = repos.find(repo => textDocument.uri.startsWith(repo.workspaceUri)); - return documentLinks(getDocument(documents, textDocument), repoContext?.workspaceUri); - }); - - connection.languages.inlayHint.on(async ({textDocument}: InlayHintParams): Promise => { - return timeOperation("inlayHints", () => { - return getInlayHints(getDocument(documents, textDocument)); - }); - }); - - connection.onCodeAction((params: CodeActionParams): CodeAction[] => { - const document = getDocument(documents, params.textDocument); - return getCodeActions({ - uri: params.textDocument.uri, - documentContent: document.getText(), - diagnostics: params.context.diagnostics, - only: params.context.only, - featureFlags - }); - }); - - // Make the text document manager listen on the connection - // for open, change and close text document events - documents.listen(connection); - - // Listen on the connection - connection.listen(); -} - -function getDocument(documents: TextDocuments, id: TextDocumentIdentifier): TextDocument { - // The text document manager should ensure all documents exist - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return documents.get(id.uri)!; -} +import { + documentLinks, + getCodeActions, + getInlayHints, + hover, + validate, + ValidationConfig +} from "@actions/languageservice"; +import {registerLogger, setLogLevel} from "@actions/languageservice/log"; +import {clearCache, clearCacheEntry} from "@actions/languageservice/utils/workflow-cache"; +import {Octokit} from "@octokit/rest"; +import { + CodeAction, + CodeActionKind, + CodeActionParams, + CompletionItem, + Connection, + DocumentLink, + DocumentLinkParams, + ExecuteCommandParams, + Hover, + HoverParams, + InitializeParams, + InitializeResult, + InlayHint, + InlayHintParams, + TextDocumentIdentifier, + TextDocumentPositionParams, + TextDocuments, + TextDocumentSyncKind +} from "vscode-languageserver"; +import {TextDocument} from "vscode-languageserver-textdocument"; +import {getClient} from "./client.js"; +import {Commands} from "./commands.js"; +import {contextProviders} from "./context-providers.js"; +import {descriptionProvider} from "./description-provider.js"; +import {FeatureFlags} from "@actions/expressions"; +import {getFileProvider} from "./file-provider.js"; +import {InitializationOptions, RepositoryContext} from "./initializationOptions.js"; +import {onCompletion} from "./on-completion.js"; +import {ReadFileRequest, Requests} from "./request.js"; +import {getActionsMetadataProvider} from "./utils/action-metadata.js"; +import {TTLCache} from "./utils/cache.js"; +import {timeOperation} from "./utils/timer.js"; +import {valueProviders} from "./value-providers.js"; + +export function initConnection(connection: Connection) { + const documents: TextDocuments = new TextDocuments(TextDocument); + + let client: Octokit | undefined; + let repos: RepositoryContext[] = []; + const cache = new TTLCache(); + + let hasWorkspaceFolderCapability = false; + let featureFlags = new FeatureFlags(); + + // Register remote console logger with language service + registerLogger(connection.console); + + connection.onInitialize((params: InitializeParams) => { + const capabilities = params.capabilities; + + hasWorkspaceFolderCapability = !!(capabilities.workspace && !!capabilities.workspace.workspaceFolders); + + const options = (params.initializationOptions ?? {}) as InitializationOptions; + + if (options.sessionToken) { + client = getClient(options.sessionToken, options.userAgent, options.gitHubApiUrl); + } + + if (options.repos) { + repos = options.repos; + } + + if (options.logLevel !== undefined) { + setLogLevel(options.logLevel); + } + + featureFlags = new FeatureFlags(options.experimentalFeatures); + + const result: InitializeResult = { + capabilities: { + textDocumentSync: TextDocumentSyncKind.Full, + completionProvider: { + resolveProvider: false, + triggerCharacters: [":", "."] + }, + hoverProvider: true, + documentLinkProvider: { + resolveProvider: false + }, + inlayHintProvider: true, + codeActionProvider: { + codeActionKinds: [CodeActionKind.QuickFix] + } + } + }; + + if (hasWorkspaceFolderCapability) { + result.capabilities.workspace = { + workspaceFolders: { + supported: true + } + }; + } + + return result; + }); + + connection.onInitialized(() => { + const enabledFeatures = featureFlags.getEnabledFeatures(); + if (enabledFeatures.length > 0) { + connection.console.info(`Experimental features enabled: ${enabledFeatures.join(", ")}`); + } + + if (hasWorkspaceFolderCapability) { + connection.workspace.onDidChangeWorkspaceFolders(() => { + clearCache(); + }); + } + }); + + // The content of a text document has changed. This event is emitted + // when the text document first opened or when its content has changed. + documents.onDidChangeContent(change => { + clearCacheEntry(change.document.uri); + return timeOperation("validation", async () => await validateTextDocument(change.document)); + }); + + async function validateTextDocument(textDocument: TextDocument): Promise { + const repoContext = repos.find(repo => textDocument.uri.startsWith(repo.workspaceUri)); + + const config: ValidationConfig = { + valueProviderConfig: valueProviders(client, repoContext, cache), + contextProviderConfig: contextProviders(client, repoContext, cache), + actionsMetadataProvider: getActionsMetadataProvider(client, cache), + fileProvider: getFileProvider(client, cache, repoContext?.workspaceUri, async path => { + return await connection.sendRequest(Requests.ReadFile, {path} satisfies ReadFileRequest); + }), + featureFlags + }; + + const result = await validate(textDocument, config); + await connection.sendDiagnostics({uri: textDocument.uri, diagnostics: result}); + } + + connection.onCompletion(async ({position, textDocument}: TextDocumentPositionParams): Promise => { + return timeOperation( + "completion", + async () => + await onCompletion( + connection, + position, + getDocument(documents, textDocument), + client, + repos.find(repo => textDocument.uri.startsWith(repo.workspaceUri)), + cache, + featureFlags + ) + ); + }); + + connection.onHover(async ({position, textDocument}: HoverParams): Promise => { + return timeOperation("hover", async () => { + const repoContext = repos.find(repo => textDocument.uri.startsWith(repo.workspaceUri)); + return await hover(getDocument(documents, textDocument), position, { + descriptionProvider: descriptionProvider(client, cache), + contextProviderConfig: repoContext && contextProviders(client, repoContext, cache), + fileProvider: getFileProvider(client, cache, repoContext?.workspaceUri, async path => { + return await connection.sendRequest(Requests.ReadFile, {path}); + }), + featureFlags + }); + }); + }); + + connection.onRequest("workspace/executeCommand", async (params: ExecuteCommandParams) => { + if (params.command === Commands.ClearCache) { + cache.clear(); + await Promise.all(documents.all().map(doc => validateTextDocument(doc))); + } + }); + + connection.onDocumentLinks(async ({textDocument}: DocumentLinkParams): Promise => { + const repoContext = repos.find(repo => textDocument.uri.startsWith(repo.workspaceUri)); + return documentLinks(getDocument(documents, textDocument), repoContext?.workspaceUri); + }); + + connection.languages.inlayHint.on(async ({textDocument}: InlayHintParams): Promise => { + return timeOperation("inlayHints", () => { + return getInlayHints(getDocument(documents, textDocument)); + }); + }); + + connection.onCodeAction((params: CodeActionParams): CodeAction[] => { + const document = getDocument(documents, params.textDocument); + return getCodeActions({ + uri: params.textDocument.uri, + documentContent: document.getText(), + diagnostics: params.context.diagnostics, + only: params.context.only, + featureFlags + }); + }); + + // Make the text document manager listen on the connection + // for open, change and close text document events + documents.listen(connection); + + // Listen on the connection + connection.listen(); +} + +function getDocument(documents: TextDocuments, id: TextDocumentIdentifier): TextDocument { + // The text document manager should ensure all documents exist + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return documents.get(id.uri)!; +}