diff --git a/expressions/src/completion.ts b/expressions/src/completion.ts index b7cb252..846ebe6 100644 --- a/expressions/src/completion.ts +++ b/expressions/src/completion.ts @@ -35,7 +35,6 @@ export function complete( context: Dictionary, extensionFunctions: FunctionInfo[], functions?: Map, - // eslint-disable-next-line @typescript-eslint/no-unused-vars featureFlags?: FeatureFlags ): CompletionItem[] { // Lex @@ -67,7 +66,7 @@ export function complete( const result = contextKeys(context); // Merge with functions - result.push(...functionItems(extensionFunctions)); + result.push(...functionItems(extensionFunctions, featureFlags)); return result; } @@ -92,10 +91,15 @@ export function complete( return contextKeys(result); } -function functionItems(extensionFunctions: FunctionInfo[]): CompletionItem[] { +function functionItems(extensionFunctions: FunctionInfo[], featureFlags?: FeatureFlags): CompletionItem[] { const result: CompletionItem[] = []; + const flags = featureFlags ?? new FeatureFlags(); for (const fdef of [...Object.values(wellKnownFunctions), ...extensionFunctions]) { + // Filter out case function if feature is disabled + if (fdef.name === "case" && !flags.isEnabled("allowCaseFunction")) { + continue; + } result.push({ label: fdef.name, description: fdef.description, diff --git a/expressions/src/features.test.ts b/expressions/src/features.test.ts index 6694141..f280c14 100644 --- a/expressions/src/features.test.ts +++ b/expressions/src/features.test.ts @@ -51,7 +51,11 @@ describe("FeatureFlags", () => { it("returns all features when all is enabled", () => { const flags = new FeatureFlags({all: true}); - expect(flags.getEnabledFeatures()).toEqual(["missingInputsQuickfix", "blockScalarChompingWarning"]); + expect(flags.getEnabledFeatures()).toEqual([ + "missingInputsQuickfix", + "blockScalarChompingWarning", + "allowCaseFunction" + ]); }); }); }); diff --git a/expressions/src/features.ts b/expressions/src/features.ts index b0a0770..e142aac 100644 --- a/expressions/src/features.ts +++ b/expressions/src/features.ts @@ -28,6 +28,12 @@ export interface ExperimentalFeatures { * @default false */ blockScalarChompingWarning?: boolean; + + /** + * Enable the case() function in expressions. + * @default false + */ + allowCaseFunction?: boolean; } /** @@ -39,7 +45,11 @@ export type ExperimentalFeatureKey = Exclude; * All known experimental feature keys. * This list must be kept in sync with the ExperimentalFeatures interface. */ -const allFeatureKeys: ExperimentalFeatureKey[] = ["missingInputsQuickfix", "blockScalarChompingWarning"]; +const allFeatureKeys: ExperimentalFeatureKey[] = [ + "missingInputsQuickfix", + "blockScalarChompingWarning", + "allowCaseFunction" +]; export class FeatureFlags { private readonly features: ExperimentalFeatures; diff --git a/languageservice/src/complete.expressions.test.ts b/languageservice/src/complete.expressions.test.ts index e20b4cf..0a2f276 100644 --- a/languageservice/src/complete.expressions.test.ts +++ b/languageservice/src/complete.expressions.test.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import {data, DescriptionDictionary} from "@actions/expressions"; +import {data, DescriptionDictionary, FeatureFlags} from "@actions/expressions"; import {CompletionItem, CompletionItemKind, MarkupContent} from "vscode-languageserver-types"; import {complete, getExpressionInput} from "./complete.js"; import {ContextProviderConfig} from "./context-providers/config.js"; @@ -69,7 +69,8 @@ describe("expressions", () => { it("single region", async () => { const input = "run-name: ${{ | }}"; const result = await complete(...getPositionFromCursor(input), { - contextProviderConfig + contextProviderConfig, + featureFlags: new FeatureFlags({allowCaseFunction: true}) }); expect(result.map(x => x.label)).toEqual([ @@ -112,7 +113,8 @@ describe("expressions", () => { it("single region with existing input", async () => { const input = "run-name: ${{ g| }}"; const result = await complete(...getPositionFromCursor(input), { - contextProviderConfig + contextProviderConfig, + featureFlags: new FeatureFlags({allowCaseFunction: true}) }); expect(result.map(x => x.label)).toEqual([ @@ -133,7 +135,8 @@ describe("expressions", () => { it("single region with existing condition", async () => { const input = "run-name: ${{ g| == 'test' }}"; const result = await complete(...getPositionFromCursor(input), { - contextProviderConfig + contextProviderConfig, + featureFlags: new FeatureFlags({allowCaseFunction: true}) }); expect(result.map(x => x.label)).toEqual([ @@ -154,7 +157,8 @@ describe("expressions", () => { it("multiple regions with partial function", async () => { const input = "run-name: Run a ${{ inputs.test }} one-line script ${{ from|('test') == inputs.name }}"; const result = await complete(...getPositionFromCursor(input), { - contextProviderConfig + contextProviderConfig, + featureFlags: new FeatureFlags({allowCaseFunction: true}) }); expect(result.map(x => x.label)).toEqual([ @@ -175,7 +179,8 @@ describe("expressions", () => { it("multiple regions - first region", async () => { const input = "run-name: test-${{ git| == 1 }}-${{ github.event }}"; const result = await complete(...getPositionFromCursor(input), { - contextProviderConfig + contextProviderConfig, + featureFlags: new FeatureFlags({allowCaseFunction: true}) }); expect(result.map(x => x.label)).toEqual([ @@ -196,7 +201,8 @@ describe("expressions", () => { it("multiple regions", async () => { const input = "run-name: test-${{ github }}-${{ | }}"; const result = await complete(...getPositionFromCursor(input), { - contextProviderConfig + contextProviderConfig, + featureFlags: new FeatureFlags({allowCaseFunction: true}) }); expect(result.map(x => x.label)).toEqual([ @@ -1175,7 +1181,8 @@ jobs: `; const result = await complete(...getPositionFromCursor(input), { - contextProviderConfig + contextProviderConfig, + featureFlags: new FeatureFlags({allowCaseFunction: true}) }); expect(result.map(x => x.label)).toEqual([ "env", diff --git a/languageservice/src/complete.test.ts b/languageservice/src/complete.test.ts index ceb1393..99f86c0 100644 --- a/languageservice/src/complete.test.ts +++ b/languageservice/src/complete.test.ts @@ -6,6 +6,7 @@ import {getPositionFromCursor} from "./test-utils/cursor-position.js"; import {TestLogger} from "./test-utils/logger.js"; import {clearCache} from "./utils/workflow-cache.js"; import {ValueProviderConfig, ValueProviderKind} from "./value-providers/config.js"; +import {FeatureFlags} from "@actions/expressions/features"; registerLogger(new TestLogger()); @@ -897,9 +898,11 @@ jobs: }); describe("expression completions", () => { - it("includes case function", async () => { + it("include case function when enabled", async () => { const input = "on: push\njobs:\n build:\n runs-on: ${{ c|"; - const result = await complete(...getPositionFromCursor(input)); + const result = await complete(...getPositionFromCursor(input), { + featureFlags: new FeatureFlags({allowCaseFunction: true}) + }); expect(result).not.toBeUndefined(); // Expression completions starting with 'c': case, contains @@ -907,5 +910,18 @@ jobs: expect(labels).toContain("case"); expect(labels).toContain("contains"); }); + + it("exclude case function when disabled", async () => { + const input = "on: push\njobs:\n build:\n runs-on: ${{ c|"; + const result = await complete(...getPositionFromCursor(input), { + featureFlags: new FeatureFlags({allowCaseFunction: false}) + }); + + expect(result).not.toBeUndefined(); + // Expression completions starting with 'c': contains + const labels = result.map(x => x.label); + expect(labels).not.toContain("case"); + expect(labels).toContain("contains"); + }); }); });