From cc7ca2237d5ae779b36db04a5632918c686b8f0b Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Wed, 15 Feb 2023 14:50:26 -0800 Subject: [PATCH 01/13] Add file provider to hover --- actions-languageserver/src/connection.ts | 5 +- .../src/description-provider.ts | 7 ++- .../reusable-workflow-input.ts | 23 +++++++++ .../src/hover.reusable-workflow.test.ts | 49 +++++++++++++++++++ actions-languageservice/src/hover.ts | 22 +++++++-- .../src/test-utils/test-file-provider.ts | 19 +++++++ 6 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 actions-languageserver/src/description-providers/reusable-workflow-input.ts create mode 100644 actions-languageservice/src/hover.reusable-workflow.test.ts diff --git a/actions-languageserver/src/connection.ts b/actions-languageserver/src/connection.ts index be145ce..496c37d 100644 --- a/actions-languageserver/src/connection.ts +++ b/actions-languageserver/src/connection.ts @@ -134,7 +134,10 @@ export function initConnection(connection: Connection) { return hover(documents.get(textDocument.uri)!, position, { descriptionProvider: descriptionProvider(client, cache), - contextProviderConfig: repoContext && contextProviders(client, repoContext, cache) + contextProviderConfig: repoContext && contextProviders(client, repoContext, cache), + fileProvider: getFileProvider(client, cache, repoContext?.workspaceUri, async path => { + return await connection.sendRequest(Requests.ReadFile, {path}); + }) }); }); diff --git a/actions-languageserver/src/description-provider.ts b/actions-languageserver/src/description-provider.ts index 128a1ee..fc0758f 100644 --- a/actions-languageserver/src/description-provider.ts +++ b/actions-languageserver/src/description-provider.ts @@ -1,10 +1,11 @@ import {DescriptionProvider} from "@github/actions-languageservice/hover"; import {Octokit} from "@octokit/rest"; import {getActionInputDescription} from "./description-providers/action-input"; +import {getReusableWorkflowInputDescription} from "./description-providers/reusable-workflow-input"; import {TTLCache} from "./utils/cache"; export function descriptionProvider(client: Octokit | undefined, cache: TTLCache): DescriptionProvider { - const getDescription: DescriptionProvider["getDescription"] = async (context, token, path) => { + const getDescription: DescriptionProvider["getDescription"] = async (context, token, path, fileProvider) => { if (!client) { return undefined; } @@ -13,6 +14,10 @@ export function descriptionProvider(client: Octokit | undefined, cache: TTLCache if (context.step && parent.definition?.key === "step-with") { return await getActionInputDescription(client, cache, context.step, token); } + + if (context.reusableWorkflowJob && fileProvider && parent.definition?.key === "workflow-job-with") { + return await getReusableWorkflowInputDescription(client, cache, context.reusableWorkflowJob, token, fileProvider); + } }; return { diff --git a/actions-languageserver/src/description-providers/reusable-workflow-input.ts b/actions-languageserver/src/description-providers/reusable-workflow-input.ts new file mode 100644 index 0000000..3914abf --- /dev/null +++ b/actions-languageserver/src/description-providers/reusable-workflow-input.ts @@ -0,0 +1,23 @@ +import {parseActionReference} from "@github/actions-languageservice/action"; +import {isString} from "@github/actions-workflow-parser"; +import {isActionStep, isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards"; +import {FileProvider} from "@github/actions-workflow-parser/workflows/file-provider"; +import {ReusableWorkflowJob} from "@github/actions-workflow-parser/model/workflow-template"; +import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; +import {Octokit} from "@octokit/rest"; +import {fetchActionMetadata} from "../utils/action-metadata"; +import {TTLCache} from "../utils/cache"; + +export async function getReusableWorkflowInputDescription( + client: Octokit, + cache: TTLCache, + reusableWorkflowJob: ReusableWorkflowJob, + token: TemplateToken, + fileProvider: FileProvider +): Promise { + if (!isReusableWorkflowJob(reusableWorkflowJob)) { + return undefined; + } + + return "here's a description" +} diff --git a/actions-languageservice/src/hover.reusable-workflow.test.ts b/actions-languageservice/src/hover.reusable-workflow.test.ts new file mode 100644 index 0000000..d53e858 --- /dev/null +++ b/actions-languageservice/src/hover.reusable-workflow.test.ts @@ -0,0 +1,49 @@ +import {CompletionItem, MarkupContent} from "vscode-languageserver-types"; +import {DescriptionProvider, hover, HoverConfig} from "./hover"; +import {testHoverConfig} from "./hover.test"; +import {getPositionFromCursor} from "./test-utils/cursor-position"; +import {testFileProvider} from "./test-utils/test-file-provider"; + +function mapResult(result: CompletionItem[]) { + return result.map(x => { + return {label: x.label, description: (x.documentation as MarkupContent).value}; + }); +} + +describe("completion with reusable workflows", () => { + it("hover on job input with description", async () => { + const input = ` +on: push + +jobs: + build: + uses: ./reusable-workflow-with-inputs.yaml + with: + us|ername: +`; + const result = await hover( + ...getPositionFromCursor(input), + {fileProvider: testFileProvider} + ); + expect(result).not.toBeUndefined(); + expect(result?.contents).toEqual( + "A username passed from the caller workflow\n\n" + + "**Context:** github, inputs, vars, needs, strategy, matrix" + ); + }); + + it("hover on job input without description", async () => { + const input = ` +on: push + +jobs: + build: + uses: ./reusable-workflow-with-inputs-no-description.yaml + with: + us|ername: +`; + const result = await hover(...getPositionFromCursor(input)); + expect(result).not.toBeUndefined(); + expect(result?.contents).toEqual("**Context:** github, inputs, vars, needs, strategy, matrix"); + }); +}); diff --git a/actions-languageservice/src/hover.ts b/actions-languageservice/src/hover.ts index b36cf16..69f0d69 100644 --- a/actions-languageservice/src/hover.ts +++ b/actions-languageservice/src/hover.ts @@ -9,6 +9,7 @@ import {StringToken} from "@github/actions-workflow-parser/templates/tokens/stri import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; import {isBasicExpression, isString} from "@github/actions-workflow-parser/templates/tokens/type-guards"; import {File} from "@github/actions-workflow-parser/workflows/file"; +import {FileProvider} from "@github/actions-workflow-parser/workflows/file-provider"; import {Position, TextDocument} from "vscode-languageserver-textdocument"; import {Hover} from "vscode-languageserver-types"; import {ContextProviderConfig} from "./context-providers/config"; @@ -26,10 +27,16 @@ import {mapRange} from "./utils/range"; export type HoverConfig = { descriptionProvider?: DescriptionProvider; contextProviderConfig?: ContextProviderConfig; + fileProvider?: FileProvider; }; export type DescriptionProvider = { - getDescription(context: WorkflowContext, token: TemplateToken, path: TemplateToken[]): Promise; + getDescription( + context: WorkflowContext, + token: TemplateToken, + path: TemplateToken[], + fileProvider?: FileProvider + ): Promise }; export async function hover(document: TextDocument, position: Position, config?: HoverConfig): Promise { @@ -103,13 +110,22 @@ async function getDescription( path: TemplateToken[] ) { const defaultDescription = token.description || ""; + // TODO fix this check if (!result?.value || !config?.descriptionProvider) { return defaultDescription; } - const template = await convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion); + const template = await convertWorkflowTemplate( + result.context, + result.value, + ErrorPolicy.TryConversion, + config?.fileProvider, + { + fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0 + } + ); const workflowContext = getWorkflowContext(document.uri, template, path); - const description = await config.descriptionProvider.getDescription(workflowContext, token, path); + const description = await config.descriptionProvider.getDescription(workflowContext, token, path, config.fileProvider); return description || defaultDescription; } diff --git a/actions-languageservice/src/test-utils/test-file-provider.ts b/actions-languageservice/src/test-utils/test-file-provider.ts index c527755..99d075d 100644 --- a/actions-languageservice/src/test-utils/test-file-provider.ts +++ b/actions-languageservice/src/test-utils/test-file-provider.ts @@ -60,6 +60,25 @@ on: required: false type: string +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + ` + }; + + case "./reusable-workflow-with-inputs-no-description.yaml": + return { + name: "reusable-workflow-with-inputs.yaml", + content: ` +on: + workflow_call: + inputs: + username: + required: true + type: string + jobs: build: runs-on: ubuntu-latest From 999a1deb60e4a6144eb47f84539726d4385bd11e Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Wed, 15 Feb 2023 15:57:44 -0800 Subject: [PATCH 02/13] Use file provider in test --- .../src/description-provider.ts | 6 ++--- .../reusable-workflow-input.ts | 7 +++--- .../src/hover.reusable-workflow.test.ts | 25 ++++++++++++++----- actions-languageservice/src/hover.ts | 9 ++++--- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/actions-languageserver/src/description-provider.ts b/actions-languageserver/src/description-provider.ts index fc0758f..8da4c7f 100644 --- a/actions-languageserver/src/description-provider.ts +++ b/actions-languageserver/src/description-provider.ts @@ -5,7 +5,7 @@ import {getReusableWorkflowInputDescription} from "./description-providers/reusa import {TTLCache} from "./utils/cache"; export function descriptionProvider(client: Octokit | undefined, cache: TTLCache): DescriptionProvider { - const getDescription: DescriptionProvider["getDescription"] = async (context, token, path, fileProvider) => { + const getDescription: DescriptionProvider["getDescription"] = async (context, token, path, template) => { if (!client) { return undefined; } @@ -15,8 +15,8 @@ export function descriptionProvider(client: Octokit | undefined, cache: TTLCache return await getActionInputDescription(client, cache, context.step, token); } - if (context.reusableWorkflowJob && fileProvider && parent.definition?.key === "workflow-job-with") { - return await getReusableWorkflowInputDescription(client, cache, context.reusableWorkflowJob, token, fileProvider); + if (context.reusableWorkflowJob && template && parent.definition?.key === "workflow-job-with") { + return await getReusableWorkflowInputDescription(client, cache, context.reusableWorkflowJob, token, template); } }; diff --git a/actions-languageserver/src/description-providers/reusable-workflow-input.ts b/actions-languageserver/src/description-providers/reusable-workflow-input.ts index 3914abf..9c50c90 100644 --- a/actions-languageserver/src/description-providers/reusable-workflow-input.ts +++ b/actions-languageserver/src/description-providers/reusable-workflow-input.ts @@ -1,11 +1,10 @@ -import {parseActionReference} from "@github/actions-languageservice/action"; +import {parseFileReference} from "@github/actions-workflow-parser/workflows/file-reference"; import {isString} from "@github/actions-workflow-parser"; -import {isActionStep, isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards"; +import {isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards"; import {FileProvider} from "@github/actions-workflow-parser/workflows/file-provider"; import {ReusableWorkflowJob} from "@github/actions-workflow-parser/model/workflow-template"; import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; import {Octokit} from "@octokit/rest"; -import {fetchActionMetadata} from "../utils/action-metadata"; import {TTLCache} from "../utils/cache"; export async function getReusableWorkflowInputDescription( @@ -13,7 +12,7 @@ export async function getReusableWorkflowInputDescription( cache: TTLCache, reusableWorkflowJob: ReusableWorkflowJob, token: TemplateToken, - fileProvider: FileProvider + template: FileProvider ): Promise { if (!isReusableWorkflowJob(reusableWorkflowJob)) { return undefined; diff --git a/actions-languageservice/src/hover.reusable-workflow.test.ts b/actions-languageservice/src/hover.reusable-workflow.test.ts index d53e858..3ddc69b 100644 --- a/actions-languageservice/src/hover.reusable-workflow.test.ts +++ b/actions-languageservice/src/hover.reusable-workflow.test.ts @@ -1,13 +1,26 @@ +import {isString} from "@github/actions-workflow-parser"; +import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token"; import {CompletionItem, MarkupContent} from "vscode-languageserver-types"; import {DescriptionProvider, hover, HoverConfig} from "./hover"; -import {testHoverConfig} from "./hover.test"; import {getPositionFromCursor} from "./test-utils/cursor-position"; import {testFileProvider} from "./test-utils/test-file-provider"; -function mapResult(result: CompletionItem[]) { - return result.map(x => { - return {label: x.label, description: (x.documentation as MarkupContent).value}; - }); +export function testHoverReusableWorkflowConfig(tokenValue: string, tokenKey: string) { + return { + descriptionProvider: { + getDescription: async (_, token, __) => { + if (!isString(token)) { + throw new Error("Test provider only supports string tokens"); + } + + expect((token as StringToken).value).toEqual(tokenValue); + expect(token.definition!.key).toEqual(tokenKey); + + return token.description; + } + } satisfies DescriptionProvider, + fileProvider: testFileProvider + } satisfies HoverConfig; } describe("completion with reusable workflows", () => { @@ -23,7 +36,7 @@ jobs: `; const result = await hover( ...getPositionFromCursor(input), - {fileProvider: testFileProvider} + testHoverReusableWorkflowConfig("username", "scalar-needs-context") ); expect(result).not.toBeUndefined(); expect(result?.contents).toEqual( diff --git a/actions-languageservice/src/hover.ts b/actions-languageservice/src/hover.ts index 69f0d69..48045ef 100644 --- a/actions-languageservice/src/hover.ts +++ b/actions-languageservice/src/hover.ts @@ -2,6 +2,7 @@ import {DescriptionDictionary, Parser} from "@github/actions-expressions"; import {FunctionInfo} from "@github/actions-expressions/funcs/info"; import {Lexer} from "@github/actions-expressions/lexer"; import {convertWorkflowTemplate, parseWorkflow, ParseWorkflowResult} from "@github/actions-workflow-parser"; +import {WorkflowTemplate} from "@github/actions-workflow-parser"; import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert"; import {getCronDescription} from "@github/actions-workflow-parser/model/converter/cron"; import {splitAllowedContext} from "@github/actions-workflow-parser/templates/allowed-context"; @@ -35,7 +36,7 @@ export type DescriptionProvider = { context: WorkflowContext, token: TemplateToken, path: TemplateToken[], - fileProvider?: FileProvider + template?: WorkflowTemplate ): Promise }; @@ -110,8 +111,8 @@ async function getDescription( path: TemplateToken[] ) { const defaultDescription = token.description || ""; - // TODO fix this check - if (!result?.value || !config?.descriptionProvider) { + // TODO fix this check - description provider is null for rusable workflows + if (!result?.value/* || !config?.descriptionProvider*/) { return defaultDescription; } @@ -125,7 +126,7 @@ async function getDescription( } ); const workflowContext = getWorkflowContext(document.uri, template, path); - const description = await config.descriptionProvider.getDescription(workflowContext, token, path, config.fileProvider); + const description = await config?.descriptionProvider?.getDescription(workflowContext, token, path, template); return description || defaultDescription; } From 157c6f0672ad540055a9b80d0d2344ec717f980c Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Wed, 15 Feb 2023 17:17:54 -0800 Subject: [PATCH 03/13] More different stuff --- .../src/description-provider.ts | 2 +- .../reusable-workflow-input.ts | 32 +++++++++++++------ actions-languageservice/src/hover.ts | 4 +-- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/actions-languageserver/src/description-provider.ts b/actions-languageserver/src/description-provider.ts index 8da4c7f..9173a8e 100644 --- a/actions-languageserver/src/description-provider.ts +++ b/actions-languageserver/src/description-provider.ts @@ -16,7 +16,7 @@ export function descriptionProvider(client: Octokit | undefined, cache: TTLCache } if (context.reusableWorkflowJob && template && parent.definition?.key === "workflow-job-with") { - return await getReusableWorkflowInputDescription(client, cache, context.reusableWorkflowJob, token, template); + return await getReusableWorkflowInputDescription(context.reusableWorkflowJob, token, template); } }; diff --git a/actions-languageserver/src/description-providers/reusable-workflow-input.ts b/actions-languageserver/src/description-providers/reusable-workflow-input.ts index 9c50c90..71eefef 100644 --- a/actions-languageserver/src/description-providers/reusable-workflow-input.ts +++ b/actions-languageserver/src/description-providers/reusable-workflow-input.ts @@ -1,22 +1,36 @@ -import {parseFileReference} from "@github/actions-workflow-parser/workflows/file-reference"; -import {isString} from "@github/actions-workflow-parser"; +import {WorkflowTemplate} from "@github/actions-workflow-parser"; import {isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards"; -import {FileProvider} from "@github/actions-workflow-parser/workflows/file-provider"; import {ReusableWorkflowJob} from "@github/actions-workflow-parser/model/workflow-template"; import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; -import {Octokit} from "@octokit/rest"; -import {TTLCache} from "../utils/cache"; export async function getReusableWorkflowInputDescription( - client: Octokit, - cache: TTLCache, reusableWorkflowJob: ReusableWorkflowJob, token: TemplateToken, - template: FileProvider + template: WorkflowTemplate ): Promise { if (!isReusableWorkflowJob(reusableWorkflowJob)) { return undefined; } - return "here's a description" + // Filter out just reusable jobs + const templateReusableJobs = template.jobs.filter(isReusableWorkflowJob); + + // Find the reusable job in the template that matches the current reusable job + const templateReusableJob = templateReusableJobs.find(job => job.id.value === reusableWorkflowJob.id.value); + + // Set the description on the reusable job to the one from the template, if any + if (templateReusableJob && reusableWorkflowJob["input-definitions"] && templateReusableJob["input-definitions"]) { + // For each input in the reusable job, see if there's one that matches in the template + for (const input of reusableWorkflowJob["input-definitions"]) { + const templateInput = templateReusableJob["input-definitions"].find(templateInput => templateInput.key.value === input.key.value); + if (templateInput) { + return "fuck me" + } + } + + } + + + + return "Didn't find a matching job description!" } diff --git a/actions-languageservice/src/hover.ts b/actions-languageservice/src/hover.ts index 48045ef..b8766a6 100644 --- a/actions-languageservice/src/hover.ts +++ b/actions-languageservice/src/hover.ts @@ -112,7 +112,7 @@ async function getDescription( ) { const defaultDescription = token.description || ""; // TODO fix this check - description provider is null for rusable workflows - if (!result?.value/* || !config?.descriptionProvider*/) { + if (!result?.value || !config?.descriptionProvider) { return defaultDescription; } @@ -126,7 +126,7 @@ async function getDescription( } ); const workflowContext = getWorkflowContext(document.uri, template, path); - const description = await config?.descriptionProvider?.getDescription(workflowContext, token, path, template); + const description = await config.descriptionProvider.getDescription(workflowContext, token, path, template); return description || defaultDescription; } From 22fa81e96cf3b21637b8fefa7001cb6496abbe41 Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Wed, 15 Feb 2023 17:27:21 -0800 Subject: [PATCH 04/13] Some fix --- .../src/description-providers/reusable-workflow-input.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/actions-languageserver/src/description-providers/reusable-workflow-input.ts b/actions-languageserver/src/description-providers/reusable-workflow-input.ts index 71eefef..1e4c805 100644 --- a/actions-languageserver/src/description-providers/reusable-workflow-input.ts +++ b/actions-languageserver/src/description-providers/reusable-workflow-input.ts @@ -22,10 +22,7 @@ export async function getReusableWorkflowInputDescription( if (templateReusableJob && reusableWorkflowJob["input-definitions"] && templateReusableJob["input-definitions"]) { // For each input in the reusable job, see if there's one that matches in the template for (const input of reusableWorkflowJob["input-definitions"]) { - const templateInput = templateReusableJob["input-definitions"].find(templateInput => templateInput.key.value === input.key.value); - if (templateInput) { - return "fuck me" - } + return "asd" } } From 8ea37b3d640a992ee6f749c372fce34de6df24fe Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Fri, 17 Feb 2023 09:24:55 -0800 Subject: [PATCH 05/13] Find the input description --- .../reusable-workflow-input.ts | 25 ++++++++++++------- actions-languageservice/src/hover.ts | 3 ++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/actions-languageserver/src/description-providers/reusable-workflow-input.ts b/actions-languageserver/src/description-providers/reusable-workflow-input.ts index 1e4c805..e725b6e 100644 --- a/actions-languageserver/src/description-providers/reusable-workflow-input.ts +++ b/actions-languageserver/src/description-providers/reusable-workflow-input.ts @@ -1,6 +1,8 @@ -import {WorkflowTemplate} from "@github/actions-workflow-parser"; +import {WorkflowTemplate, isMapping} from "@github/actions-workflow-parser"; import {isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards"; +import {isString} from "@github/actions-workflow-parser"; import {ReusableWorkflowJob} from "@github/actions-workflow-parser/model/workflow-template"; +import {DESCRIPTION} from "@github/actions-workflow-parser/templates/template-constants"; import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; export async function getReusableWorkflowInputDescription( @@ -12,22 +14,27 @@ export async function getReusableWorkflowInputDescription( return undefined; } + const inputName = isString(token) && token.value; + if (!inputName) { + return undefined; + } + // Filter out just reusable jobs const templateReusableJobs = template.jobs.filter(isReusableWorkflowJob); // Find the reusable job in the template that matches the current reusable job const templateReusableJob = templateReusableJobs.find(job => job.id.value === reusableWorkflowJob.id.value); - // Set the description on the reusable job to the one from the template, if any + // Find the input description in the template, if any if (templateReusableJob && reusableWorkflowJob["input-definitions"] && templateReusableJob["input-definitions"]) { - // For each input in the reusable job, see if there's one that matches in the template - for (const input of reusableWorkflowJob["input-definitions"]) { - return "asd" + const definition = templateReusableJob["input-definitions"].find(token.value) + if (definition && isMapping(definition)) { + const description = definition.find(DESCRIPTION) + if (description && isString(description)) { + return description.value + } } - } - - - return "Didn't find a matching job description!" + return "" } diff --git a/actions-languageservice/src/hover.ts b/actions-languageservice/src/hover.ts index 4adb57d..e16a91b 100644 --- a/actions-languageservice/src/hover.ts +++ b/actions-languageservice/src/hover.ts @@ -119,7 +119,8 @@ async function getDescription( } const template = await convertWorkflowTemplate(result.context, result.value, config?.fileProvider, { - errorPolicy: ErrorPolicy.TryConversion + errorPolicy: ErrorPolicy.TryConversion, + fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0, }); const workflowContext = getWorkflowContext(document.uri, template, path); const description = await config.descriptionProvider.getDescription(workflowContext, token, path, template); From 9bb2426d06e09769e81c1dd3551e058de863a66a Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Fri, 17 Feb 2023 11:53:25 -0800 Subject: [PATCH 06/13] Move description fetching to language service --- .../src/description-provider.ts | 7 +- .../reusable-workflow-input.ts | 40 -------- .../src/hover.reusable-workflow.test.ts | 29 +----- actions-languageservice/src/hover.test.ts | 4 +- actions-languageservice/src/hover.ts | 98 ++++++++++++++----- 5 files changed, 81 insertions(+), 97 deletions(-) delete mode 100644 actions-languageserver/src/description-providers/reusable-workflow-input.ts diff --git a/actions-languageserver/src/description-provider.ts b/actions-languageserver/src/description-provider.ts index 9173a8e..128a1ee 100644 --- a/actions-languageserver/src/description-provider.ts +++ b/actions-languageserver/src/description-provider.ts @@ -1,11 +1,10 @@ import {DescriptionProvider} from "@github/actions-languageservice/hover"; import {Octokit} from "@octokit/rest"; import {getActionInputDescription} from "./description-providers/action-input"; -import {getReusableWorkflowInputDescription} from "./description-providers/reusable-workflow-input"; import {TTLCache} from "./utils/cache"; export function descriptionProvider(client: Octokit | undefined, cache: TTLCache): DescriptionProvider { - const getDescription: DescriptionProvider["getDescription"] = async (context, token, path, template) => { + const getDescription: DescriptionProvider["getDescription"] = async (context, token, path) => { if (!client) { return undefined; } @@ -14,10 +13,6 @@ export function descriptionProvider(client: Octokit | undefined, cache: TTLCache if (context.step && parent.definition?.key === "step-with") { return await getActionInputDescription(client, cache, context.step, token); } - - if (context.reusableWorkflowJob && template && parent.definition?.key === "workflow-job-with") { - return await getReusableWorkflowInputDescription(context.reusableWorkflowJob, token, template); - } }; return { diff --git a/actions-languageserver/src/description-providers/reusable-workflow-input.ts b/actions-languageserver/src/description-providers/reusable-workflow-input.ts deleted file mode 100644 index e725b6e..0000000 --- a/actions-languageserver/src/description-providers/reusable-workflow-input.ts +++ /dev/null @@ -1,40 +0,0 @@ -import {WorkflowTemplate, isMapping} from "@github/actions-workflow-parser"; -import {isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards"; -import {isString} from "@github/actions-workflow-parser"; -import {ReusableWorkflowJob} from "@github/actions-workflow-parser/model/workflow-template"; -import {DESCRIPTION} from "@github/actions-workflow-parser/templates/template-constants"; -import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; - -export async function getReusableWorkflowInputDescription( - reusableWorkflowJob: ReusableWorkflowJob, - token: TemplateToken, - template: WorkflowTemplate -): Promise { - if (!isReusableWorkflowJob(reusableWorkflowJob)) { - return undefined; - } - - const inputName = isString(token) && token.value; - if (!inputName) { - return undefined; - } - - // Filter out just reusable jobs - const templateReusableJobs = template.jobs.filter(isReusableWorkflowJob); - - // Find the reusable job in the template that matches the current reusable job - const templateReusableJob = templateReusableJobs.find(job => job.id.value === reusableWorkflowJob.id.value); - - // Find the input description in the template, if any - if (templateReusableJob && reusableWorkflowJob["input-definitions"] && templateReusableJob["input-definitions"]) { - const definition = templateReusableJob["input-definitions"].find(token.value) - if (definition && isMapping(definition)) { - const description = definition.find(DESCRIPTION) - if (description && isString(description)) { - return description.value - } - } - } - - return "" -} diff --git a/actions-languageservice/src/hover.reusable-workflow.test.ts b/actions-languageservice/src/hover.reusable-workflow.test.ts index 3ddc69b..481a8b0 100644 --- a/actions-languageservice/src/hover.reusable-workflow.test.ts +++ b/actions-languageservice/src/hover.reusable-workflow.test.ts @@ -1,29 +1,8 @@ -import {isString} from "@github/actions-workflow-parser"; -import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token"; -import {CompletionItem, MarkupContent} from "vscode-languageserver-types"; -import {DescriptionProvider, hover, HoverConfig} from "./hover"; +import {hover} from "./hover"; +import {testHoverConfig} from "./hover.test"; import {getPositionFromCursor} from "./test-utils/cursor-position"; -import {testFileProvider} from "./test-utils/test-file-provider"; -export function testHoverReusableWorkflowConfig(tokenValue: string, tokenKey: string) { - return { - descriptionProvider: { - getDescription: async (_, token, __) => { - if (!isString(token)) { - throw new Error("Test provider only supports string tokens"); - } - - expect((token as StringToken).value).toEqual(tokenValue); - expect(token.definition!.key).toEqual(tokenKey); - - return token.description; - } - } satisfies DescriptionProvider, - fileProvider: testFileProvider - } satisfies HoverConfig; -} - -describe("completion with reusable workflows", () => { +describe("hover on reusable workflows", () => { it("hover on job input with description", async () => { const input = ` on: push @@ -36,7 +15,7 @@ jobs: `; const result = await hover( ...getPositionFromCursor(input), - testHoverReusableWorkflowConfig("username", "scalar-needs-context") + testHoverConfig("username", "scalar-needs-context") ); expect(result).not.toBeUndefined(); expect(result?.contents).toEqual( diff --git a/actions-languageservice/src/hover.test.ts b/actions-languageservice/src/hover.test.ts index 7ec5afd..a7c4064 100644 --- a/actions-languageservice/src/hover.test.ts +++ b/actions-languageservice/src/hover.test.ts @@ -2,6 +2,7 @@ import {isString} from "@github/actions-workflow-parser"; import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token"; import {DescriptionProvider, hover, HoverConfig} from "./hover"; import {getPositionFromCursor} from "./test-utils/cursor-position"; +import {testFileProvider} from "./test-utils/test-file-provider"; export function testHoverConfig(tokenValue: string, tokenKey: string, description?: string) { return { @@ -16,7 +17,8 @@ export function testHoverConfig(tokenValue: string, tokenKey: string, descriptio return description; } - } satisfies DescriptionProvider + } satisfies DescriptionProvider, + fileProvider: testFileProvider } satisfies HoverConfig; } diff --git a/actions-languageservice/src/hover.ts b/actions-languageservice/src/hover.ts index e16a91b..dadbf03 100644 --- a/actions-languageservice/src/hover.ts +++ b/actions-languageservice/src/hover.ts @@ -2,10 +2,12 @@ import {DescriptionDictionary, Parser} from "@github/actions-expressions"; import {FunctionInfo} from "@github/actions-expressions/funcs/info"; import {Lexer} from "@github/actions-expressions/lexer"; import {convertWorkflowTemplate, parseWorkflow, ParseWorkflowResult} from "@github/actions-workflow-parser"; -import {WorkflowTemplate} from "@github/actions-workflow-parser"; +import {WorkflowTemplate, isMapping} from "@github/actions-workflow-parser"; import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert"; import {getCronDescription} from "@github/actions-workflow-parser/model/converter/cron"; +import {isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards"; import {splitAllowedContext} from "@github/actions-workflow-parser/templates/allowed-context"; +import {DESCRIPTION} from "@github/actions-workflow-parser/templates/template-constants"; import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token"; import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; import {isBasicExpression, isString} from "@github/actions-workflow-parser/templates/tokens/type-guards"; @@ -35,8 +37,7 @@ export type DescriptionProvider = { getDescription( context: WorkflowContext, token: TemplateToken, - path: TemplateToken[], - template?: WorkflowTemplate + path: TemplateToken[] ): Promise }; @@ -54,17 +55,17 @@ export async function hover(document: TextDocument, position: Position, config?: const {token, keyToken, parent} = tokenResult; const tokenDefinitionInfo = (keyToken || parent || token)?.definitionInfo; + const template = await convertWorkflowTemplate(result.context, result.value, config?.fileProvider, { + errorPolicy: ErrorPolicy.TryConversion, + fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0, + }); + const workflowContext = getWorkflowContext(document.uri, template, tokenResult.path); if (token && tokenDefinitionInfo) { if (isBasicExpression(token) || isPotentiallyExpression(token)) { info(`Calculating expression hover for token with definition ${tokenDefinitionInfo.definition.key}`); const allowedContext = tokenDefinitionInfo.allowedContext || []; const {namedContexts, functions} = splitAllowedContext(allowedContext); - - const template = await convertWorkflowTemplate(result.context, result.value, undefined, { - errorPolicy: ErrorPolicy.TryConversion - }); - const workflowContext = getWorkflowContext(document.uri, template, tokenResult.path); const context = await getContext(namedContexts, config?.contextProviderConfig, workflowContext, Mode.Completion); const exprPos = mapToExpressionPos(token, position); @@ -80,7 +81,7 @@ export async function hover(document: TextDocument, position: Position, config?: info(`Calculating hover for token with definition ${token.definition.key}`); - if (tokenResult.parent && isCronMappingValue(tokenResult)) { + if (tokenResult.parent && isCronMappingValue(tokenResult) && isString(token)) { const tokenValue = (token as StringToken).value; const description = getCronDescription(tokenValue); if (description) { @@ -91,39 +92,46 @@ export async function hover(document: TextDocument, position: Position, config?: } } - let description = await getDescription(document, config, result, token, tokenResult.path); - - const allowedContext = token.definitionInfo?.allowedContext; - if (allowedContext && allowedContext?.length > 0) { - // Only add padding if there is a description - description += `${description.length > 0 ? `\n\n` : ""}**Context:** ${allowedContext.join(", ")}`; + if (tokenResult.parent && isReusableWorkflowJobInput(tokenResult)) { + let description = getReusableWorkflowInputDescription(workflowContext, tokenResult, template) + description = appendContext(token, description); + return { + contents: description, + range: mapRange(token.range) + } as Hover; } + let description = await getDescription(config, workflowContext, token, tokenResult.path); + + description = appendContext(token, description); + return { contents: description, range: mapRange(token.range) } satisfies Hover; } +function appendContext(token: TemplateToken, description: string) { + const allowedContext = token.definitionInfo?.allowedContext; + if (allowedContext && allowedContext?.length > 0) { + // Only add padding if there is a description + description += `${description.length > 0 ? `\n\n` : ""}**Context:** ${allowedContext.join(", ")}`; + } + return description; +} + async function getDescription( - document: TextDocument, config: HoverConfig | undefined, - result: ParseWorkflowResult | undefined, + workflowContext: WorkflowContext, token: TemplateToken, path: TemplateToken[] ) { const defaultDescription = token.description || ""; - // TODO fix this check - description provider is null for rusable workflows - if (!result?.value || !config?.descriptionProvider) { + if (!config?.descriptionProvider) { return defaultDescription; } - const template = await convertWorkflowTemplate(result.context, result.value, config?.fileProvider, { - errorPolicy: ErrorPolicy.TryConversion, - fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0, - }); - const workflowContext = getWorkflowContext(document.uri, template, path); - const description = await config.descriptionProvider.getDescription(workflowContext, token, path, template); + const description = await config.descriptionProvider.getDescription(workflowContext, token, path); return description || defaultDescription; } @@ -135,6 +143,13 @@ function isCronMappingValue(tokenResult: TokenResult): boolean { ); } +function isReusableWorkflowJobInput(tokenResult: TokenResult): boolean { + return ( + tokenResult.parent?.definition?.key === "workflow-job-with" && + isString(tokenResult.token!) + ); +} + function expressionHover( exprPos: ExpressionPos, context: DescriptionDictionary, @@ -178,3 +193,36 @@ function expressionHover( return null; } } + +function getReusableWorkflowInputDescription(workflowContext: WorkflowContext, tokenResult: TokenResult, template: WorkflowTemplate): string { + const reusableWorkflowJob = workflowContext.reusableWorkflowJob + + if (reusableWorkflowJob && !isReusableWorkflowJob(reusableWorkflowJob)) { + return ""; + } + + const inputName = tokenResult.token && isString(tokenResult.token) && tokenResult.token.value; + if (!inputName) { + return ""; + } + + // Filter out just reusable jobs + const templateJobs = template.jobs.filter(isReusableWorkflowJob); + + // Find the reusable job in the template that matches the current reusable job + const templateJob = templateJobs.find(job => job.id.value === reusableWorkflowJob!.id.value); + + // Find the input description in the template, if any + if (templateJob && reusableWorkflowJob!["input-definitions"] && templateJob["input-definitions"]) { + const definition = templateJob["input-definitions"].find((tokenResult.token! as StringToken).value) + if (definition && isMapping(definition)) { + const description = definition.find(DESCRIPTION) + if (description && isString(description)) { + return description.value + } + } + } + + return "" +} + From 050634415e45393f3c35ce2ad8497e3e4652b6bc Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Fri, 17 Feb 2023 12:38:20 -0800 Subject: [PATCH 07/13] Cleanup --- actions-languageserver/src/connection.ts | 14 +++++++------- actions-languageservice/src/hover.ts | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/actions-languageserver/src/connection.ts b/actions-languageserver/src/connection.ts index 83a046f..9b08b70 100644 --- a/actions-languageserver/src/connection.ts +++ b/actions-languageserver/src/connection.ts @@ -137,13 +137,13 @@ export function initConnection(connection: Connection) { connection.onHover(async ({position, textDocument}: HoverParams): Promise => { return timeOperation("hover", async () => { const repoContext = repos.find(repo => textDocument.uri.startsWith(repo.workspaceUri)); - - return await hover(documents.get(textDocument.uri)!, 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}); - }) + return await hover(documents.get(textDocument.uri)!, 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}); + }) + }); }); }); diff --git a/actions-languageservice/src/hover.ts b/actions-languageservice/src/hover.ts index dadbf03..4fe554f 100644 --- a/actions-languageservice/src/hover.ts +++ b/actions-languageservice/src/hover.ts @@ -81,7 +81,7 @@ export async function hover(document: TextDocument, position: Position, config?: info(`Calculating hover for token with definition ${token.definition.key}`); - if (tokenResult.parent && isCronMappingValue(tokenResult) && isString(token)) { + if (tokenResult.parent && isCronMappingValue(tokenResult)) { const tokenValue = (token as StringToken).value; const description = getCronDescription(tokenValue); if (description) { @@ -197,7 +197,7 @@ function expressionHover( function getReusableWorkflowInputDescription(workflowContext: WorkflowContext, tokenResult: TokenResult, template: WorkflowTemplate): string { const reusableWorkflowJob = workflowContext.reusableWorkflowJob - if (reusableWorkflowJob && !isReusableWorkflowJob(reusableWorkflowJob)) { + if (!reusableWorkflowJob || !isReusableWorkflowJob(reusableWorkflowJob)) { return ""; } @@ -213,8 +213,8 @@ function getReusableWorkflowInputDescription(workflowContext: WorkflowContext, t const templateJob = templateJobs.find(job => job.id.value === reusableWorkflowJob!.id.value); // Find the input description in the template, if any - if (templateJob && reusableWorkflowJob!["input-definitions"] && templateJob["input-definitions"]) { - const definition = templateJob["input-definitions"].find((tokenResult.token! as StringToken).value) + if (templateJob && reusableWorkflowJob["input-definitions"] && templateJob["input-definitions"]) { + const definition = templateJob["input-definitions"].find((tokenResult.token as StringToken).value) if (definition && isMapping(definition)) { const description = definition.find(DESCRIPTION) if (description && isString(description)) { From c5caf4b35f81a9e451b1d6c806ced3a70c862941 Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Fri, 17 Feb 2023 12:54:59 -0800 Subject: [PATCH 08/13] Refactor template job filtering --- actions-languageservice/src/hover.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/actions-languageservice/src/hover.ts b/actions-languageservice/src/hover.ts index 4fe554f..3fd85bf 100644 --- a/actions-languageservice/src/hover.ts +++ b/actions-languageservice/src/hover.ts @@ -26,6 +26,7 @@ import {nullTrace} from "./nulltrace"; import {isPotentiallyExpression} from "./utils/expression-detection"; import {findToken, TokenResult} from "./utils/find-token"; import {mapRange} from "./utils/range"; +import { ReusableWorkflowJob } from "@github/actions-workflow-parser/model/workflow-template"; export type HoverConfig = { descriptionProvider?: DescriptionProvider; @@ -206,11 +207,10 @@ function getReusableWorkflowInputDescription(workflowContext: WorkflowContext, t return ""; } - // Filter out just reusable jobs - const templateJobs = template.jobs.filter(isReusableWorkflowJob); - // Find the reusable job in the template that matches the current reusable job - const templateJob = templateJobs.find(job => job.id.value === reusableWorkflowJob!.id.value); + const templateJob = template.jobs.find(job => { + return isReusableWorkflowJob(job) && job.id.value === reusableWorkflowJob.id.value + }) as ReusableWorkflowJob // Find the input description in the template, if any if (templateJob && reusableWorkflowJob["input-definitions"] && templateJob["input-definitions"]) { From 7389a38952f64720d88f79719bced3a3e5f234f3 Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Fri, 17 Feb 2023 13:04:33 -0800 Subject: [PATCH 09/13] Format --- .../src/hover.reusable-workflow.test.ts | 8 +--- actions-languageservice/src/hover.ts | 38 +++++++++---------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/actions-languageservice/src/hover.reusable-workflow.test.ts b/actions-languageservice/src/hover.reusable-workflow.test.ts index 481a8b0..b32d6fc 100644 --- a/actions-languageservice/src/hover.reusable-workflow.test.ts +++ b/actions-languageservice/src/hover.reusable-workflow.test.ts @@ -13,14 +13,10 @@ jobs: with: us|ername: `; - const result = await hover( - ...getPositionFromCursor(input), - testHoverConfig("username", "scalar-needs-context") - ); + const result = await hover(...getPositionFromCursor(input), testHoverConfig("username", "scalar-needs-context")); expect(result).not.toBeUndefined(); expect(result?.contents).toEqual( - "A username passed from the caller workflow\n\n" + - "**Context:** github, inputs, vars, needs, strategy, matrix" + "A username passed from the caller workflow\n\n**Context:** github, inputs, vars, needs, strategy, matrix" ); }); diff --git a/actions-languageservice/src/hover.ts b/actions-languageservice/src/hover.ts index 3fd85bf..170d54c 100644 --- a/actions-languageservice/src/hover.ts +++ b/actions-languageservice/src/hover.ts @@ -26,7 +26,7 @@ import {nullTrace} from "./nulltrace"; import {isPotentiallyExpression} from "./utils/expression-detection"; import {findToken, TokenResult} from "./utils/find-token"; import {mapRange} from "./utils/range"; -import { ReusableWorkflowJob } from "@github/actions-workflow-parser/model/workflow-template"; +import {ReusableWorkflowJob} from "@github/actions-workflow-parser/model/workflow-template"; export type HoverConfig = { descriptionProvider?: DescriptionProvider; @@ -35,11 +35,7 @@ export type HoverConfig = { }; export type DescriptionProvider = { - getDescription( - context: WorkflowContext, - token: TemplateToken, - path: TemplateToken[] - ): Promise + getDescription(context: WorkflowContext, token: TemplateToken, path: TemplateToken[]): Promise; }; export async function hover(document: TextDocument, position: Position, config?: HoverConfig): Promise { @@ -58,7 +54,7 @@ export async function hover(document: TextDocument, position: Position, config?: const tokenDefinitionInfo = (keyToken || parent || token)?.definitionInfo; const template = await convertWorkflowTemplate(result.context, result.value, config?.fileProvider, { errorPolicy: ErrorPolicy.TryConversion, - fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0, + fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0 }); const workflowContext = getWorkflowContext(document.uri, template, tokenResult.path); if (token && tokenDefinitionInfo) { @@ -94,7 +90,7 @@ export async function hover(document: TextDocument, position: Position, config?: } if (tokenResult.parent && isReusableWorkflowJobInput(tokenResult)) { - let description = getReusableWorkflowInputDescription(workflowContext, tokenResult, template) + let description = getReusableWorkflowInputDescription(workflowContext, tokenResult, template); description = appendContext(token, description); return { contents: description, @@ -145,10 +141,7 @@ function isCronMappingValue(tokenResult: TokenResult): boolean { } function isReusableWorkflowJobInput(tokenResult: TokenResult): boolean { - return ( - tokenResult.parent?.definition?.key === "workflow-job-with" && - isString(tokenResult.token!) - ); + return tokenResult.parent?.definition?.key === "workflow-job-with" && isString(tokenResult.token!); } function expressionHover( @@ -195,8 +188,12 @@ function expressionHover( } } -function getReusableWorkflowInputDescription(workflowContext: WorkflowContext, tokenResult: TokenResult, template: WorkflowTemplate): string { - const reusableWorkflowJob = workflowContext.reusableWorkflowJob +function getReusableWorkflowInputDescription( + workflowContext: WorkflowContext, + tokenResult: TokenResult, + template: WorkflowTemplate +): string { + const reusableWorkflowJob = workflowContext.reusableWorkflowJob; if (!reusableWorkflowJob || !isReusableWorkflowJob(reusableWorkflowJob)) { return ""; @@ -209,20 +206,19 @@ function getReusableWorkflowInputDescription(workflowContext: WorkflowContext, t // Find the reusable job in the template that matches the current reusable job const templateJob = template.jobs.find(job => { - return isReusableWorkflowJob(job) && job.id.value === reusableWorkflowJob.id.value - }) as ReusableWorkflowJob + return isReusableWorkflowJob(job) && job.id.value === reusableWorkflowJob.id.value; + }) as ReusableWorkflowJob; // Find the input description in the template, if any if (templateJob && reusableWorkflowJob["input-definitions"] && templateJob["input-definitions"]) { - const definition = templateJob["input-definitions"].find((tokenResult.token as StringToken).value) + const definition = templateJob["input-definitions"].find((tokenResult.token as StringToken).value); if (definition && isMapping(definition)) { - const description = definition.find(DESCRIPTION) + const description = definition.find(DESCRIPTION); if (description && isString(description)) { - return description.value + return description.value; } } } - return "" + return ""; } - From 9bb7372bd8ebd02ff9eb0d773d031aaaac12a237 Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Tue, 21 Feb 2023 09:10:51 -0800 Subject: [PATCH 10/13] Remove extra filter and parameter --- actions-languageservice/src/hover.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/actions-languageservice/src/hover.ts b/actions-languageservice/src/hover.ts index 170d54c..c580d2a 100644 --- a/actions-languageservice/src/hover.ts +++ b/actions-languageservice/src/hover.ts @@ -90,7 +90,7 @@ export async function hover(document: TextDocument, position: Position, config?: } if (tokenResult.parent && isReusableWorkflowJobInput(tokenResult)) { - let description = getReusableWorkflowInputDescription(workflowContext, tokenResult, template); + let description = getReusableWorkflowInputDescription(workflowContext, tokenResult); description = appendContext(token, description); return { contents: description, @@ -190,12 +190,11 @@ function expressionHover( function getReusableWorkflowInputDescription( workflowContext: WorkflowContext, - tokenResult: TokenResult, - template: WorkflowTemplate + tokenResult: TokenResult ): string { const reusableWorkflowJob = workflowContext.reusableWorkflowJob; - if (!reusableWorkflowJob || !isReusableWorkflowJob(reusableWorkflowJob)) { + if (!reusableWorkflowJob) { return ""; } @@ -204,14 +203,9 @@ function getReusableWorkflowInputDescription( return ""; } - // Find the reusable job in the template that matches the current reusable job - const templateJob = template.jobs.find(job => { - return isReusableWorkflowJob(job) && job.id.value === reusableWorkflowJob.id.value; - }) as ReusableWorkflowJob; - // Find the input description in the template, if any - if (templateJob && reusableWorkflowJob["input-definitions"] && templateJob["input-definitions"]) { - const definition = templateJob["input-definitions"].find((tokenResult.token as StringToken).value); + if (reusableWorkflowJob["input-definitions"]) { + const definition = reusableWorkflowJob["input-definitions"].find(inputName); if (definition && isMapping(definition)) { const description = definition.find(DESCRIPTION); if (description && isString(description)) { From cda5e4001c75774bf303ff01eb8e4d5471f73cfd Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Tue, 21 Feb 2023 09:14:50 -0800 Subject: [PATCH 11/13] Format --- actions-languageservice/src/hover.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/actions-languageservice/src/hover.ts b/actions-languageservice/src/hover.ts index c580d2a..2075ac9 100644 --- a/actions-languageservice/src/hover.ts +++ b/actions-languageservice/src/hover.ts @@ -188,10 +188,7 @@ function expressionHover( } } -function getReusableWorkflowInputDescription( - workflowContext: WorkflowContext, - tokenResult: TokenResult -): string { +function getReusableWorkflowInputDescription(workflowContext: WorkflowContext, tokenResult: TokenResult): string { const reusableWorkflowJob = workflowContext.reusableWorkflowJob; if (!reusableWorkflowJob) { From 21a04dfed5d4f018d15914bb03bb1520fc215075 Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Tue, 21 Feb 2023 09:17:07 -0800 Subject: [PATCH 12/13] Remove unused imports --- actions-languageservice/src/hover.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/actions-languageservice/src/hover.ts b/actions-languageservice/src/hover.ts index 2075ac9..91aa2af 100644 --- a/actions-languageservice/src/hover.ts +++ b/actions-languageservice/src/hover.ts @@ -2,10 +2,9 @@ import {DescriptionDictionary, Parser} from "@github/actions-expressions"; import {FunctionInfo} from "@github/actions-expressions/funcs/info"; import {Lexer} from "@github/actions-expressions/lexer"; import {convertWorkflowTemplate, parseWorkflow, ParseWorkflowResult} from "@github/actions-workflow-parser"; -import {WorkflowTemplate, isMapping} from "@github/actions-workflow-parser"; +import {isMapping} from "@github/actions-workflow-parser"; import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert"; import {getCronDescription} from "@github/actions-workflow-parser/model/converter/cron"; -import {isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards"; import {splitAllowedContext} from "@github/actions-workflow-parser/templates/allowed-context"; import {DESCRIPTION} from "@github/actions-workflow-parser/templates/template-constants"; import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token"; @@ -26,7 +25,6 @@ import {nullTrace} from "./nulltrace"; import {isPotentiallyExpression} from "./utils/expression-detection"; import {findToken, TokenResult} from "./utils/find-token"; import {mapRange} from "./utils/range"; -import {ReusableWorkflowJob} from "@github/actions-workflow-parser/model/workflow-template"; export type HoverConfig = { descriptionProvider?: DescriptionProvider; From d81733b160de77515f1d8bfe94d884dee81dbc3f Mon Sep 17 00:00:00 2001 From: Jacob Wallraff Date: Tue, 21 Feb 2023 10:13:18 -0800 Subject: [PATCH 13/13] Refactor into description provider file --- .../reusable-job-inputs.ts | 37 ++++++++++++++++ actions-languageservice/src/hover.ts | 43 +++---------------- 2 files changed, 44 insertions(+), 36 deletions(-) create mode 100644 actions-languageservice/src/description-providers/reusable-job-inputs.ts diff --git a/actions-languageservice/src/description-providers/reusable-job-inputs.ts b/actions-languageservice/src/description-providers/reusable-job-inputs.ts new file mode 100644 index 0000000..78f823d --- /dev/null +++ b/actions-languageservice/src/description-providers/reusable-job-inputs.ts @@ -0,0 +1,37 @@ +import {isMapping, isString, ParseWorkflowResult} from "@github/actions-workflow-parser"; +import {DESCRIPTION} from "@github/actions-workflow-parser/templates/template-constants"; +import {WorkflowContext} from "../context/workflow-context"; +import {TokenResult} from "../utils/find-token"; + +export function isReusableWorkflowJobInput(tokenResult: TokenResult): boolean { + return tokenResult.parent?.definition?.key === "workflow-job-with" && isString(tokenResult.token!); +} + +export function getReusableWorkflowInputDescription( + workflowContext: WorkflowContext, + tokenResult: TokenResult +): string { + const reusableWorkflowJob = workflowContext.reusableWorkflowJob; + + if (!reusableWorkflowJob) { + return ""; + } + + const inputName = tokenResult.token && isString(tokenResult.token) && tokenResult.token.value; + if (!inputName) { + return ""; + } + + // Find the input description in the template, if any + if (reusableWorkflowJob["input-definitions"]) { + const definition = reusableWorkflowJob["input-definitions"].find(inputName); + if (definition && isMapping(definition)) { + const description = definition.find(DESCRIPTION); + if (description && isString(description)) { + return description.value; + } + } + } + + return ""; +} diff --git a/actions-languageservice/src/hover.ts b/actions-languageservice/src/hover.ts index 91aa2af..99f3718 100644 --- a/actions-languageservice/src/hover.ts +++ b/actions-languageservice/src/hover.ts @@ -1,12 +1,10 @@ import {DescriptionDictionary, Parser} from "@github/actions-expressions"; import {FunctionInfo} from "@github/actions-expressions/funcs/info"; import {Lexer} from "@github/actions-expressions/lexer"; -import {convertWorkflowTemplate, parseWorkflow, ParseWorkflowResult} from "@github/actions-workflow-parser"; -import {isMapping} from "@github/actions-workflow-parser"; +import {convertWorkflowTemplate, parseWorkflow} from "@github/actions-workflow-parser"; import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert"; import {getCronDescription} from "@github/actions-workflow-parser/model/converter/cron"; import {splitAllowedContext} from "@github/actions-workflow-parser/templates/allowed-context"; -import {DESCRIPTION} from "@github/actions-workflow-parser/templates/template-constants"; import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token"; import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; import {isBasicExpression, isString} from "@github/actions-workflow-parser/templates/tokens/type-guards"; @@ -17,6 +15,10 @@ import {Hover} from "vscode-languageserver-types"; import {ContextProviderConfig} from "./context-providers/config"; import {getContext, Mode} from "./context-providers/default"; import {getWorkflowContext, WorkflowContext} from "./context/workflow-context"; +import { + isReusableWorkflowJobInput, + getReusableWorkflowInputDescription +} from "./description-providers/reusable-job-inputs"; import {ExpressionPos, mapToExpressionPos} from "./expression-hover/expression-pos"; import {HoverVisitor} from "./expression-hover/visitor"; import {validatorFunctions} from "./expression-validation/functions"; @@ -83,7 +85,7 @@ export async function hover(document: TextDocument, position: Position, config?: return { contents: description, range: mapRange(token.range) - } as Hover; + } satisfies Hover; } } @@ -93,11 +95,10 @@ export async function hover(document: TextDocument, position: Position, config?: return { contents: description, range: mapRange(token.range) - } as Hover; + } satisfies Hover; } let description = await getDescription(config, workflowContext, token, tokenResult.path); - description = appendContext(token, description); return { @@ -138,10 +139,6 @@ function isCronMappingValue(tokenResult: TokenResult): boolean { ); } -function isReusableWorkflowJobInput(tokenResult: TokenResult): boolean { - return tokenResult.parent?.definition?.key === "workflow-job-with" && isString(tokenResult.token!); -} - function expressionHover( exprPos: ExpressionPos, context: DescriptionDictionary, @@ -185,29 +182,3 @@ function expressionHover( return null; } } - -function getReusableWorkflowInputDescription(workflowContext: WorkflowContext, tokenResult: TokenResult): string { - const reusableWorkflowJob = workflowContext.reusableWorkflowJob; - - if (!reusableWorkflowJob) { - return ""; - } - - const inputName = tokenResult.token && isString(tokenResult.token) && tokenResult.token.value; - if (!inputName) { - return ""; - } - - // Find the input description in the template, if any - if (reusableWorkflowJob["input-definitions"]) { - const definition = reusableWorkflowJob["input-definitions"].find(inputName); - if (definition && isMapping(definition)) { - const description = definition.find(DESCRIPTION); - if (description && isString(description)) { - return description.value; - } - } - } - - return ""; -}