From a9cdcdba803348abff7a6125007f9dd7f7770965 Mon Sep 17 00:00:00 2001 From: Christopher Schleiden Date: Mon, 28 Nov 2022 16:15:57 -0800 Subject: [PATCH] Support allowed/suggested value providers --- actions-languageservice/src/complete.test.ts | 9 ++-- actions-languageservice/src/complete.ts | 4 +- actions-languageservice/src/validate.test.ts | 42 ++++++++++++++++-- actions-languageservice/src/validate.ts | 29 ++++++------- .../src/value-providers/config.ts | 10 ++++- .../src/value-providers/default.ts | 43 ++++++++++--------- .../src/value-providers/definition.ts | 2 +- .../src/value-providers/strings-to-values.ts | 5 +++ 8 files changed, 98 insertions(+), 46 deletions(-) create mode 100644 actions-languageservice/src/value-providers/strings-to-values.ts diff --git a/actions-languageservice/src/complete.test.ts b/actions-languageservice/src/complete.test.ts index 1d9e2e2..9d8124b 100644 --- a/actions-languageservice/src/complete.test.ts +++ b/actions-languageservice/src/complete.test.ts @@ -1,7 +1,7 @@ import {complete} from "./complete"; import {WorkflowContext} from "./context/workflow-context"; import {getPositionFromCursor} from "./test-utils/cursor-position"; -import {ValueProviderConfig} from "./value-providers/config"; +import {ValueProviderConfig, ValueProviderKind} from "./value-providers/config"; describe("completion", () => { it("runs-on", async () => { @@ -180,8 +180,11 @@ jobs: const input = "on: push\njobs:\n build:\n runs-on: |"; const config: ValueProviderConfig = { - "runs-on": async (_: WorkflowContext) => { - return [{label: "my-custom-label"}]; + "runs-on": { + kind: ValueProviderKind.SuggestedValues, + get: async (_: WorkflowContext) => { + return [{label: "my-custom-label"}]; + } } }; const result = await complete(...getPositionFromCursor(input), config); diff --git a/actions-languageservice/src/complete.ts b/actions-languageservice/src/complete.ts index da3b550..7471262 100644 --- a/actions-languageservice/src/complete.ts +++ b/actions-languageservice/src/complete.ts @@ -104,7 +104,7 @@ async function getValues( const existingValues = getExistingValues(token, parent); if (token?.definition?.key) { - const customValues = await valueProviderConfig?.[token.definition.key]?.(workflowContext); + const customValues = await valueProviderConfig?.[token.definition.key]?.get(workflowContext); if (customValues) { return filterAndSortCompletionOptions(customValues, existingValues); @@ -117,7 +117,7 @@ async function getValues( (parent.definition?.key && defaultValueProviders[parent.definition.key]); if (valueProvider) { - const values = await valueProvider(workflowContext); + const values = await valueProvider.get(workflowContext); return filterAndSortCompletionOptions(values, existingValues); } diff --git a/actions-languageservice/src/validate.test.ts b/actions-languageservice/src/validate.test.ts index ce230e6..57d0d38 100644 --- a/actions-languageservice/src/validate.test.ts +++ b/actions-languageservice/src/validate.test.ts @@ -59,7 +59,7 @@ jobs: } as Diagnostic); }); - it("single value not returned by value provider", async () => { + it("single value not returned by suggested value provider", async () => { const result = await validate( createDocument( "wf.yaml", @@ -76,7 +76,7 @@ jobs: expect(result.length).toBe(1); expect(result[0]).toEqual({ message: "Value 'does-not-exist' is not valid", - severity: DiagnosticSeverity.Error, + severity: DiagnosticSeverity.Warning, range: { end: { character: 27, @@ -109,7 +109,7 @@ jobs: expect(result.length).toBe(1); expect(result[0]).toEqual({ message: "Value 'does-not-exist' is not valid", - severity: DiagnosticSeverity.Error, + severity: DiagnosticSeverity.Warning, range: { end: { character: 20, @@ -122,4 +122,40 @@ jobs: } } as Diagnostic); }); + + it("single value not returned by allowed value provider", async () => { + const result = await validate( + createDocument( + "wf.yaml", + `on: push +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo + build: + runs-on: ubuntu-latest + needs: test2 + steps: + - run: echo` + ), + defaultValueProviders + ); + + expect(result.length).toBe(1); + expect(result[0]).toEqual({ + message: "Value 'test2' is not valid", + severity: DiagnosticSeverity.Error, + range: { + end: { + character: 16, + line: 8 + }, + start: { + character: 11, + line: 8 + } + } + } as Diagnostic); + }); }); diff --git a/actions-languageservice/src/validate.ts b/actions-languageservice/src/validate.ts index 89683a1..5209c09 100644 --- a/actions-languageservice/src/validate.ts +++ b/actions-languageservice/src/validate.ts @@ -24,7 +24,7 @@ import {getWorkflowContext, WorkflowContext} from "./context/workflow-context"; import {AccessError, wrapDictionary} from "./expression-validation/error-dictionary"; import {nullTrace} from "./nulltrace"; import {findToken} from "./utils/find-token"; -import {Value, ValueProviderConfig} from "./value-providers/config"; +import {ValueProviderConfig, ValueProviderKind} from "./value-providers/config"; import {defaultValueProviders} from "./value-providers/default"; /** @@ -124,26 +124,23 @@ async function additionalValidations( if (valueProviderConfig && token.range && token.definition?.key) { const defKey = token.definition.key; - let customValues: Value[] | undefined; - - const customValueProvider = valueProviderConfig[defKey]; - if (customValueProvider) { - customValues = await customValueProvider(getProviderContext(documentUri, template, root, token)); - } else { - const defaultValueProvider = defaultValueProviders[defKey]; - if (defaultValueProvider) { - customValues = await defaultValueProvider(getProviderContext(documentUri, template, root, token)); - } + // Try a custom value provider first + let valueProvider = valueProviderConfig[defKey]; + if (!valueProvider) { + // fall back to default + valueProvider = defaultValueProviders[defKey]; } - if (customValues) { + if (valueProvider) { + const customValues = await valueProvider.get(getProviderContext(documentUri, template, root, token)); + if (isSequence(token)) { for (let i = 0; i < token.count; ++i) { const entry = token.get(i); if (isString(entry)) { if (!customValues.map(x => x.label).includes(entry.value)) { - invalidValue(diagnostics, entry); + invalidValue(diagnostics, entry, valueProvider.kind); } } } @@ -151,7 +148,7 @@ async function additionalValidations( if (isString(token)) { if (!customValues.map(x => x.label).includes(token.value)) { - invalidValue(diagnostics, token); + invalidValue(diagnostics, token, valueProvider.kind); } } } @@ -159,10 +156,10 @@ async function additionalValidations( } } -function invalidValue(diagnostics: Diagnostic[], token: StringToken) { +function invalidValue(diagnostics: Diagnostic[], token: StringToken, kind: ValueProviderKind) { diagnostics.push({ message: `Value '${token.value}' is not valid`, - severity: DiagnosticSeverity.Error, + severity: kind === ValueProviderKind.AllowedValues ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning, range: mapRange(token.range) }); } diff --git a/actions-languageservice/src/value-providers/config.ts b/actions-languageservice/src/value-providers/config.ts index 20fb54c..34b7337 100644 --- a/actions-languageservice/src/value-providers/config.ts +++ b/actions-languageservice/src/value-providers/config.ts @@ -5,7 +5,15 @@ export interface Value { description?: string; } -export type ValueProvider = (context: WorkflowContext) => Promise; +export enum ValueProviderKind { + AllowedValues, + SuggestedValues +} + +export type ValueProvider = { + kind: ValueProviderKind; + get: (context: WorkflowContext) => Promise; +}; export interface ValueProviderConfig { [definitionKey: string]: ValueProvider; diff --git a/actions-languageservice/src/value-providers/default.ts b/actions-languageservice/src/value-providers/default.ts index 1b4c877..be59611 100644 --- a/actions-languageservice/src/value-providers/default.ts +++ b/actions-languageservice/src/value-providers/default.ts @@ -1,25 +1,28 @@ import {WorkflowContext} from "../context/workflow-context"; -import {Value, ValueProviderConfig} from "./config"; +import {ValueProviderConfig, ValueProviderKind} from "./config"; import {needs} from "./needs"; +import {stringsToValues} from "./strings-to-values"; export const defaultValueProviders: ValueProviderConfig = { - needs, - "runs-on": async (_: WorkflowContext) => - stringsToValues([ - "ubuntu-latest", - "ubuntu-18.04", - "ubuntu-16.04", - "windows-latest", - "windows-2019", - "windows-2016", - "macos-latest", - "macos-10.15", - "macos-10.14", - "macos-10.13", - "self-hosted" - ]) + needs: { + kind: ValueProviderKind.AllowedValues, + get: needs + }, + "runs-on": { + kind: ValueProviderKind.SuggestedValues, + get: async (_: WorkflowContext) => + stringsToValues([ + "ubuntu-latest", + "ubuntu-18.04", + "ubuntu-16.04", + "windows-latest", + "windows-2019", + "windows-2016", + "macos-latest", + "macos-10.15", + "macos-10.14", + "macos-10.13", + "self-hosted" + ]) + } }; - -export function stringsToValues(labels: string[]): Value[] { - return labels.map(x => ({label: x})); -} diff --git a/actions-languageservice/src/value-providers/definition.ts b/actions-languageservice/src/value-providers/definition.ts index 71aeefb..ad3e5d2 100644 --- a/actions-languageservice/src/value-providers/definition.ts +++ b/actions-languageservice/src/value-providers/definition.ts @@ -5,7 +5,7 @@ import {OneOfDefinition} from "@github/actions-workflow-parser/templates/schema/ import {StringDefinition} from "@github/actions-workflow-parser/templates/schema/string-definition"; import {getWorkflowSchema} from "@github/actions-workflow-parser/workflows/workflow-schema"; import {Value} from "./config"; -import {stringsToValues} from "./default"; +import {stringsToValues} from "./strings-to-values"; export function definitionValues(def: Definition): Value[] { const schema = getWorkflowSchema(); diff --git a/actions-languageservice/src/value-providers/strings-to-values.ts b/actions-languageservice/src/value-providers/strings-to-values.ts new file mode 100644 index 0000000..ff95f87 --- /dev/null +++ b/actions-languageservice/src/value-providers/strings-to-values.ts @@ -0,0 +1,5 @@ +import {Value} from "./config"; + +export function stringsToValues(labels: string[]): Value[] { + return labels.map(x => ({label: x})); +}