Add experimental flag for case function

This commit is contained in:
Allan Guigou
2026-01-14 13:13:57 +00:00
parent 228acc3cd9
commit 05debf64b0
5 changed files with 82 additions and 15 deletions
+11 -3
View File
@@ -2,6 +2,7 @@ import {DescriptionPair} from "./completion/descriptionDictionary.js";
import {Dictionary, isDictionary} from "./data/dictionary.js";
import {ExpressionData} from "./data/expressiondata.js";
import {Evaluator} from "./evaluator.js";
import {FeatureFlags} from "./features.js";
import {wellKnownFunctions} from "./funcs.js";
import {FunctionDefinition, FunctionInfo} from "./funcs/info.js";
import {Lexer, Token, TokenType} from "./lexer.js";
@@ -26,13 +27,15 @@ export type CompletionItem = {
* @param context Context available for the expression
* @param extensionFunctions List of functions available
* @param functions Optional map of functions to use during evaluation
* @param featureFlags Optional feature flags to control which features are enabled
* @returns Array of completion items
*/
export function complete(
input: string,
context: Dictionary,
extensionFunctions: FunctionInfo[],
functions?: Map<string, FunctionDefinition>
functions?: Map<string, FunctionDefinition>,
featureFlags?: FeatureFlags
): CompletionItem[] {
// Lex
const lexer = new Lexer(input);
@@ -63,7 +66,7 @@ export function complete(
const result = contextKeys(context);
// Merge with functions
result.push(...functionItems(extensionFunctions));
result.push(...functionItems(extensionFunctions, featureFlags));
return result;
}
@@ -88,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,
+7 -1
View File
@@ -28,6 +28,12 @@ export interface ExperimentalFeatures {
* @default false
*/
blockScalarChompingWarning?: boolean;
/**
* Enable the case() function in expressions.
* @default false
*/
allowCaseFunction?: boolean;
}
/**
@@ -39,7 +45,7 @@ export type ExperimentalFeatureKey = Exclude<keyof ExperimentalFeatures, "all">;
* 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;
@@ -68,7 +68,10 @@ describe("expressions", () => {
describe("top-level auto-complete", () => {
it("single region", async () => {
const input = "run-name: ${{ | }}";
const result = await complete(...getPositionFromCursor(input));
const result = await complete(...getPositionFromCursor(input), {
contextProviderConfig,
experimentalFeatures: {allowCaseFunction: true}
});
expect(result.map(x => x.label)).toEqual([
"github",
@@ -109,7 +112,10 @@ describe("expressions", () => {
it("single region with existing input", async () => {
const input = "run-name: ${{ g| }}";
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
const result = await complete(...getPositionFromCursor(input), {
contextProviderConfig,
experimentalFeatures: {allowCaseFunction: true}
});
expect(result.map(x => x.label)).toEqual([
"github",
@@ -128,7 +134,10 @@ describe("expressions", () => {
it("single region with existing condition", async () => {
const input = "run-name: ${{ g| == 'test' }}";
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
const result = await complete(...getPositionFromCursor(input), {
contextProviderConfig,
experimentalFeatures: {allowCaseFunction: true}
});
expect(result.map(x => x.label)).toEqual([
"github",
@@ -147,7 +156,10 @@ 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});
const result = await complete(...getPositionFromCursor(input), {
contextProviderConfig,
experimentalFeatures: {allowCaseFunction: true}
});
expect(result.map(x => x.label)).toEqual([
"github",
@@ -166,7 +178,10 @@ describe("expressions", () => {
it("multiple regions - first region", async () => {
const input = "run-name: test-${{ git| == 1 }}-${{ github.event }}";
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
const result = await complete(...getPositionFromCursor(input), {
contextProviderConfig,
experimentalFeatures: {allowCaseFunction: true}
});
expect(result.map(x => x.label)).toEqual([
"github",
@@ -185,7 +200,10 @@ describe("expressions", () => {
it("multiple regions", async () => {
const input = "run-name: test-${{ github }}-${{ | }}";
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
const result = await complete(...getPositionFromCursor(input), {
contextProviderConfig,
experimentalFeatures: {allowCaseFunction: true}
});
expect(result.map(x => x.label)).toEqual([
"github",
@@ -1132,7 +1150,10 @@ jobs:
run: echo hi
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
const result = await complete(...getPositionFromCursor(input), {
contextProviderConfig,
experimentalFeatures: {allowCaseFunction: true}
});
expect(result.map(x => x.label)).toEqual([
"env",
"github",
+29
View File
@@ -895,4 +895,33 @@ jobs:
expect(result.some(x => x.label === "macos-latest")).toBe(true);
});
});
describe("expression completions", () => {
it("include case function when enabled", async () => {
const input = "on: push\njobs:\n build:\n runs-on: ${{ c|";
const result = await complete(...getPositionFromCursor(input), {
experimentalFeatures: {allowCaseFunction: true}
});
expect(result).not.toBeUndefined();
// Expression completions starting with 'c': case, contains
const labels = result.map(x => x.label);
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), {
experimentalFeatures: {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");
});
});
});
+7 -4
View File
@@ -1,4 +1,4 @@
import {complete as completeExpression, DescriptionDictionary} from "@actions/expressions";
import {complete as completeExpression, DescriptionDictionary, ExperimentalFeatures, FeatureFlags} from "@actions/expressions";
import {CompletionItem as ExpressionCompletionItem} from "@actions/expressions/completion";
import {isBasicExpression, isSequence, isString} from "@actions/workflow-parser";
import {getActionSchema} from "@actions/workflow-parser/actions/action-schema";
@@ -72,6 +72,7 @@ export type CompletionConfig = {
valueProviderConfig?: ValueProviderConfig;
contextProviderConfig?: ContextProviderConfig;
fileProvider?: FileProvider;
experimentalFeatures?: ExperimentalFeatures;
};
export async function complete(
@@ -148,7 +149,7 @@ export async function complete(
Mode.Completion
);
return getExpressionCompletionItems(token, context, newPos);
return getExpressionCompletionItems(token, context, newPos, config?.experimentalFeatures);
}
const indentation = guessIndentation(newDoc, 2, true); // Use 2 spaces as default and most common for YAML
@@ -528,7 +529,8 @@ export function getExistingValues(token: TemplateToken | null, parent: TemplateT
function getExpressionCompletionItems(
token: TemplateToken,
context: DescriptionDictionary,
pos: Position
pos: Position,
experimentalFeatures?: ExperimentalFeatures
): CompletionItem[] {
if (!token.range) {
return [];
@@ -547,7 +549,8 @@ function getExpressionCompletionItems(
const expressionInput = (getExpressionInput(currentInput, cursorOffset) || "").trim();
try {
return completeExpression(expressionInput, context, [], validatorFunctions).map(item =>
const featureFlags = new FeatureFlags(experimentalFeatures);
return completeExpression(expressionInput, context, [], validatorFunctions, featureFlags).map(item =>
mapExpressionCompletionItem(item, currentInput[cursorOffset])
);
} catch (e) {