Merge pull request #294 from actions/allanguigou/case
Add support for case function
This commit is contained in:
@@ -2,6 +2,7 @@ import {DescriptionPair} from "./completion/descriptionDictionary.js";
|
|||||||
import {Dictionary, isDictionary} from "./data/dictionary.js";
|
import {Dictionary, isDictionary} from "./data/dictionary.js";
|
||||||
import {ExpressionData} from "./data/expressiondata.js";
|
import {ExpressionData} from "./data/expressiondata.js";
|
||||||
import {Evaluator} from "./evaluator.js";
|
import {Evaluator} from "./evaluator.js";
|
||||||
|
import {FeatureFlags} from "./features.js";
|
||||||
import {wellKnownFunctions} from "./funcs.js";
|
import {wellKnownFunctions} from "./funcs.js";
|
||||||
import {FunctionDefinition, FunctionInfo} from "./funcs/info.js";
|
import {FunctionDefinition, FunctionInfo} from "./funcs/info.js";
|
||||||
import {Lexer, Token, TokenType} from "./lexer.js";
|
import {Lexer, Token, TokenType} from "./lexer.js";
|
||||||
@@ -26,13 +27,15 @@ export type CompletionItem = {
|
|||||||
* @param context Context available for the expression
|
* @param context Context available for the expression
|
||||||
* @param extensionFunctions List of functions available
|
* @param extensionFunctions List of functions available
|
||||||
* @param functions Optional map of functions to use during evaluation
|
* @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
|
* @returns Array of completion items
|
||||||
*/
|
*/
|
||||||
export function complete(
|
export function complete(
|
||||||
input: string,
|
input: string,
|
||||||
context: Dictionary,
|
context: Dictionary,
|
||||||
extensionFunctions: FunctionInfo[],
|
extensionFunctions: FunctionInfo[],
|
||||||
functions?: Map<string, FunctionDefinition>
|
functions?: Map<string, FunctionDefinition>,
|
||||||
|
featureFlags?: FeatureFlags
|
||||||
): CompletionItem[] {
|
): CompletionItem[] {
|
||||||
// Lex
|
// Lex
|
||||||
const lexer = new Lexer(input);
|
const lexer = new Lexer(input);
|
||||||
@@ -63,7 +66,7 @@ export function complete(
|
|||||||
const result = contextKeys(context);
|
const result = contextKeys(context);
|
||||||
|
|
||||||
// Merge with functions
|
// Merge with functions
|
||||||
result.push(...functionItems(extensionFunctions));
|
result.push(...functionItems(extensionFunctions, featureFlags));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -88,10 +91,15 @@ export function complete(
|
|||||||
return contextKeys(result);
|
return contextKeys(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
function functionItems(extensionFunctions: FunctionInfo[]): CompletionItem[] {
|
function functionItems(extensionFunctions: FunctionInfo[], featureFlags?: FeatureFlags): CompletionItem[] {
|
||||||
const result: CompletionItem[] = [];
|
const result: CompletionItem[] = [];
|
||||||
|
const flags = featureFlags ?? new FeatureFlags();
|
||||||
|
|
||||||
for (const fdef of [...Object.values(wellKnownFunctions), ...extensionFunctions]) {
|
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({
|
result.push({
|
||||||
label: fdef.name,
|
label: fdef.name,
|
||||||
description: fdef.description,
|
description: fdef.description,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export enum ErrorType {
|
|||||||
ErrorExceededMaxLength,
|
ErrorExceededMaxLength,
|
||||||
ErrorTooFewParameters,
|
ErrorTooFewParameters,
|
||||||
ErrorTooManyParameters,
|
ErrorTooManyParameters,
|
||||||
|
ErrorEvenParameters,
|
||||||
ErrorUnrecognizedContext,
|
ErrorUnrecognizedContext,
|
||||||
ErrorUnrecognizedFunction
|
ErrorUnrecognizedFunction
|
||||||
}
|
}
|
||||||
@@ -42,6 +43,8 @@ function errorDescription(typ: ErrorType): string {
|
|||||||
return "Too few parameters supplied";
|
return "Too few parameters supplied";
|
||||||
case ErrorType.ErrorTooManyParameters:
|
case ErrorType.ErrorTooManyParameters:
|
||||||
return "Too many parameters supplied";
|
return "Too many parameters supplied";
|
||||||
|
case ErrorType.ErrorEvenParameters:
|
||||||
|
return "Even number of parameters supplied, requires an odd number of parameters";
|
||||||
case ErrorType.ErrorUnrecognizedContext:
|
case ErrorType.ErrorUnrecognizedContext:
|
||||||
return "Unrecognized named-value";
|
return "Unrecognized named-value";
|
||||||
case ErrorType.ErrorUnrecognizedFunction:
|
case ErrorType.ErrorUnrecognizedFunction:
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ describe("FeatureFlags", () => {
|
|||||||
expect(flags.getEnabledFeatures()).toEqual([
|
expect(flags.getEnabledFeatures()).toEqual([
|
||||||
"missingInputsQuickfix",
|
"missingInputsQuickfix",
|
||||||
"blockScalarChompingWarning",
|
"blockScalarChompingWarning",
|
||||||
"actionScaffoldingSnippets"
|
"actionScaffoldingSnippets",
|
||||||
|
"allowCaseFunction"
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ export interface ExperimentalFeatures {
|
|||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
actionScaffoldingSnippets?: boolean;
|
actionScaffoldingSnippets?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable the case() function in expressions.
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
allowCaseFunction?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,7 +55,8 @@ export type ExperimentalFeatureKey = Exclude<keyof ExperimentalFeatures, "all">;
|
|||||||
const allFeatureKeys: ExperimentalFeatureKey[] = [
|
const allFeatureKeys: ExperimentalFeatureKey[] = [
|
||||||
"missingInputsQuickfix",
|
"missingInputsQuickfix",
|
||||||
"blockScalarChompingWarning",
|
"blockScalarChompingWarning",
|
||||||
"actionScaffoldingSnippets"
|
"actionScaffoldingSnippets",
|
||||||
|
"allowCaseFunction"
|
||||||
];
|
];
|
||||||
|
|
||||||
export class FeatureFlags {
|
export class FeatureFlags {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {ErrorType, ExpressionError} from "./errors.js";
|
import {ErrorType, ExpressionError} from "./errors.js";
|
||||||
|
import {caseFunc} from "./funcs/case.js";
|
||||||
import {contains} from "./funcs/contains.js";
|
import {contains} from "./funcs/contains.js";
|
||||||
import {endswith} from "./funcs/endswith.js";
|
import {endswith} from "./funcs/endswith.js";
|
||||||
import {format} from "./funcs/format.js";
|
import {format} from "./funcs/format.js";
|
||||||
@@ -16,6 +17,7 @@ export type ParseContext = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const wellKnownFunctions: {[name: string]: FunctionDefinition} = {
|
export const wellKnownFunctions: {[name: string]: FunctionDefinition} = {
|
||||||
|
case: caseFunc,
|
||||||
contains: contains,
|
contains: contains,
|
||||||
endswith: endswith,
|
endswith: endswith,
|
||||||
format: format,
|
format: format,
|
||||||
@@ -53,4 +55,9 @@ export function validateFunction(context: ParseContext, identifier: Token, argCo
|
|||||||
if (argCount > f.maxArgs) {
|
if (argCount > f.maxArgs) {
|
||||||
throw new ExpressionError(ErrorType.ErrorTooManyParameters, identifier);
|
throw new ExpressionError(ErrorType.ErrorTooManyParameters, identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// case function requires an odd number of arguments
|
||||||
|
if (name === "case" && argCount % 2 === 0) {
|
||||||
|
throw new ExpressionError(ErrorType.ErrorEvenParameters, identifier);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import {ExpressionData, Kind} from "../data/index.js";
|
||||||
|
import {FunctionDefinition} from "./info.js";
|
||||||
|
|
||||||
|
export const caseFunc: FunctionDefinition = {
|
||||||
|
name: "case",
|
||||||
|
description:
|
||||||
|
"`case( pred1, val1, pred2, val2, ..., default )`\n\nEvaluates predicates in order and returns the value corresponding to the first predicate that evaluates to `true`. If no predicate matches, it returns the last argument as the default value.",
|
||||||
|
minArgs: 3,
|
||||||
|
maxArgs: Number.MAX_SAFE_INTEGER,
|
||||||
|
call: (...args: ExpressionData[]): ExpressionData => {
|
||||||
|
// Evaluate predicate-result pairs
|
||||||
|
for (let i = 0; i < args.length - 1; i += 2) {
|
||||||
|
const predicate = args[i];
|
||||||
|
|
||||||
|
// Predicate must be a boolean
|
||||||
|
if (predicate.kind !== Kind.Boolean) {
|
||||||
|
throw new Error("case predicate must evaluate to a boolean value");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If predicate is true, return the corresponding result
|
||||||
|
if (predicate.value) {
|
||||||
|
return args[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No predicate matched, return default (last argument)
|
||||||
|
return args[args.length - 1];
|
||||||
|
}
|
||||||
|
};
|
||||||
Vendored
+157
@@ -0,0 +1,157 @@
|
|||||||
|
{
|
||||||
|
"case": [
|
||||||
|
{
|
||||||
|
"expr": "case(true, 'first', 'default')",
|
||||||
|
"result": { "kind": "String", "value": "first" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(false, 'first', 'default')",
|
||||||
|
"result": { "kind": "String", "value": "default" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(true, 'first', false, 'second', 'default')",
|
||||||
|
"result": { "kind": "String", "value": "first" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(false, 'first', true, 'second', 'default')",
|
||||||
|
"result": { "kind": "String", "value": "second" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(false, 'first', false, 'second', 'default')",
|
||||||
|
"result": { "kind": "String", "value": "default" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(1 == 1, 'equal', 'not equal')",
|
||||||
|
"result": { "kind": "String", "value": "equal" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(1 == 2, 'equal', 'not equal')",
|
||||||
|
"result": { "kind": "String", "value": "not equal" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(github.ref == 'refs/heads/main', 'main', github.event_name == 'pull_request', 'pr', 'other')",
|
||||||
|
"contexts": {
|
||||||
|
"github": {
|
||||||
|
"ref": "refs/heads/main",
|
||||||
|
"event_name": "push"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "main" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(github.ref == 'refs/heads/main', 'main', github.event_name == 'pull_request', 'pr', 'other')",
|
||||||
|
"contexts": {
|
||||||
|
"github": {
|
||||||
|
"ref": "refs/heads/develop",
|
||||||
|
"event_name": "pull_request"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "pr" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(github.ref == 'refs/heads/main', 'main', github.event_name == 'pull_request', 'pr', 'other')",
|
||||||
|
"contexts": {
|
||||||
|
"github": {
|
||||||
|
"ref": "refs/heads/develop",
|
||||||
|
"event_name": "push"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "other" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(true, 123, 456)",
|
||||||
|
"result": { "kind": "Number", "value": 123 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(false, 123, 456)",
|
||||||
|
"result": { "kind": "Number", "value": 456 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(github.event == 'pull_request', 0, 1)",
|
||||||
|
"contexts": {
|
||||||
|
"github": {
|
||||||
|
"event": "pull_request"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "Number", "value": 0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(false, 0, 1)",
|
||||||
|
"result": { "kind": "Number", "value": 1 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(true, false, true)",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(false, false, true)",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(true, '', 'default')",
|
||||||
|
"result": { "kind": "String", "value": "" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(false, 'first', '')",
|
||||||
|
"result": { "kind": "String", "value": "" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(true, fromJSON('[1,2,3]'), 'default')",
|
||||||
|
"result": { "kind": "Array", "value": [1, 2, 3] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(true, fromJSON('{\"key\":\"value\"}'), 'default')",
|
||||||
|
"result": { "kind": "Object", "value": { "key": "value" } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(false, 'first', false, 'second', false, 'third', false, 'fourth', 'default')",
|
||||||
|
"result": { "kind": "String", "value": "default" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(false, 'first', false, 'second', true, 'third', false, 'fourth', 'default')",
|
||||||
|
"result": { "kind": "String", "value": "third" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case('not a boolean', 'first', 'default')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "case predicate must evaluate to a boolean value"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(1, 'first', 'default')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "case predicate must evaluate to a boolean value"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(null, 'first', 'default')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "case predicate must evaluate to a boolean value"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(fromJSON('[]'), 'first', 'default')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "case predicate must evaluate to a boolean value"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(fromJSON('{}'), 'first', 'default')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "case predicate must evaluate to a boolean value"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "case(true, 'first', false, 'second')",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Even number of parameters supplied, requires an odd number of parameters: 'case'. Located at position 1 within expression: case(true, 'first', false, 'second')"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import {data, DescriptionDictionary} from "@actions/expressions";
|
import {data, DescriptionDictionary, FeatureFlags} from "@actions/expressions";
|
||||||
import {CompletionItem, CompletionItemKind} from "vscode-languageserver-types";
|
import {CompletionItem, CompletionItemKind} from "vscode-languageserver-types";
|
||||||
import {complete, getExpressionInput} from "./complete.js";
|
import {complete, getExpressionInput} from "./complete.js";
|
||||||
import {ContextProviderConfig} from "./context-providers/config.js";
|
import {ContextProviderConfig} from "./context-providers/config.js";
|
||||||
@@ -68,12 +68,16 @@ describe("expressions", () => {
|
|||||||
describe("top-level auto-complete", () => {
|
describe("top-level auto-complete", () => {
|
||||||
it("single region", async () => {
|
it("single region", async () => {
|
||||||
const input = "run-name: ${{ | }}";
|
const input = "run-name: ${{ | }}";
|
||||||
const result = await complete(...getPositionFromCursor(input));
|
const result = await complete(...getPositionFromCursor(input), {
|
||||||
|
contextProviderConfig,
|
||||||
|
featureFlags: new FeatureFlags({allowCaseFunction: true})
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.map(x => x.label)).toEqual([
|
expect(result.map(x => x.label)).toEqual([
|
||||||
"github",
|
"github",
|
||||||
"inputs",
|
"inputs",
|
||||||
"vars",
|
"vars",
|
||||||
|
"case",
|
||||||
"contains",
|
"contains",
|
||||||
"endsWith",
|
"endsWith",
|
||||||
"format",
|
"format",
|
||||||
@@ -108,12 +112,16 @@ describe("expressions", () => {
|
|||||||
|
|
||||||
it("single region with existing input", async () => {
|
it("single region with existing input", async () => {
|
||||||
const input = "run-name: ${{ g| }}";
|
const input = "run-name: ${{ g| }}";
|
||||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
const result = await complete(...getPositionFromCursor(input), {
|
||||||
|
contextProviderConfig,
|
||||||
|
featureFlags: new FeatureFlags({allowCaseFunction: true})
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.map(x => x.label)).toEqual([
|
expect(result.map(x => x.label)).toEqual([
|
||||||
"github",
|
"github",
|
||||||
"inputs",
|
"inputs",
|
||||||
"vars",
|
"vars",
|
||||||
|
"case",
|
||||||
"contains",
|
"contains",
|
||||||
"endsWith",
|
"endsWith",
|
||||||
"format",
|
"format",
|
||||||
@@ -126,12 +134,16 @@ describe("expressions", () => {
|
|||||||
|
|
||||||
it("single region with existing condition", async () => {
|
it("single region with existing condition", async () => {
|
||||||
const input = "run-name: ${{ g| == 'test' }}";
|
const input = "run-name: ${{ g| == 'test' }}";
|
||||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
const result = await complete(...getPositionFromCursor(input), {
|
||||||
|
contextProviderConfig,
|
||||||
|
featureFlags: new FeatureFlags({allowCaseFunction: true})
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.map(x => x.label)).toEqual([
|
expect(result.map(x => x.label)).toEqual([
|
||||||
"github",
|
"github",
|
||||||
"inputs",
|
"inputs",
|
||||||
"vars",
|
"vars",
|
||||||
|
"case",
|
||||||
"contains",
|
"contains",
|
||||||
"endsWith",
|
"endsWith",
|
||||||
"format",
|
"format",
|
||||||
@@ -144,12 +156,16 @@ describe("expressions", () => {
|
|||||||
|
|
||||||
it("multiple regions with partial function", async () => {
|
it("multiple regions with partial function", async () => {
|
||||||
const input = "run-name: Run a ${{ inputs.test }} one-line script ${{ from|('test') == inputs.name }}";
|
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,
|
||||||
|
featureFlags: new FeatureFlags({allowCaseFunction: true})
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.map(x => x.label)).toEqual([
|
expect(result.map(x => x.label)).toEqual([
|
||||||
"github",
|
"github",
|
||||||
"inputs",
|
"inputs",
|
||||||
"vars",
|
"vars",
|
||||||
|
"case",
|
||||||
"contains",
|
"contains",
|
||||||
"endsWith",
|
"endsWith",
|
||||||
"format",
|
"format",
|
||||||
@@ -162,12 +178,16 @@ describe("expressions", () => {
|
|||||||
|
|
||||||
it("multiple regions - first region", async () => {
|
it("multiple regions - first region", async () => {
|
||||||
const input = "run-name: test-${{ git| == 1 }}-${{ github.event }}";
|
const input = "run-name: test-${{ git| == 1 }}-${{ github.event }}";
|
||||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
const result = await complete(...getPositionFromCursor(input), {
|
||||||
|
contextProviderConfig,
|
||||||
|
featureFlags: new FeatureFlags({allowCaseFunction: true})
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.map(x => x.label)).toEqual([
|
expect(result.map(x => x.label)).toEqual([
|
||||||
"github",
|
"github",
|
||||||
"inputs",
|
"inputs",
|
||||||
"vars",
|
"vars",
|
||||||
|
"case",
|
||||||
"contains",
|
"contains",
|
||||||
"endsWith",
|
"endsWith",
|
||||||
"format",
|
"format",
|
||||||
@@ -180,12 +200,16 @@ describe("expressions", () => {
|
|||||||
|
|
||||||
it("multiple regions", async () => {
|
it("multiple regions", async () => {
|
||||||
const input = "run-name: test-${{ github }}-${{ | }}";
|
const input = "run-name: test-${{ github }}-${{ | }}";
|
||||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
const result = await complete(...getPositionFromCursor(input), {
|
||||||
|
contextProviderConfig,
|
||||||
|
featureFlags: new FeatureFlags({allowCaseFunction: true})
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.map(x => x.label)).toEqual([
|
expect(result.map(x => x.label)).toEqual([
|
||||||
"github",
|
"github",
|
||||||
"inputs",
|
"inputs",
|
||||||
"vars",
|
"vars",
|
||||||
|
"case",
|
||||||
"contains",
|
"contains",
|
||||||
"endsWith",
|
"endsWith",
|
||||||
"format",
|
"format",
|
||||||
@@ -1126,7 +1150,10 @@ jobs:
|
|||||||
run: echo hi
|
run: echo hi
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
const result = await complete(...getPositionFromCursor(input), {
|
||||||
|
contextProviderConfig,
|
||||||
|
featureFlags: new FeatureFlags({allowCaseFunction: true})
|
||||||
|
});
|
||||||
expect(result.map(x => x.label)).toEqual([
|
expect(result.map(x => x.label)).toEqual([
|
||||||
"env",
|
"env",
|
||||||
"github",
|
"github",
|
||||||
@@ -1139,6 +1166,7 @@ jobs:
|
|||||||
"steps",
|
"steps",
|
||||||
"strategy",
|
"strategy",
|
||||||
"vars",
|
"vars",
|
||||||
|
"case",
|
||||||
"contains",
|
"contains",
|
||||||
"endsWith",
|
"endsWith",
|
||||||
"format",
|
"format",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {getPositionFromCursor} from "./test-utils/cursor-position.js";
|
|||||||
import {TestLogger} from "./test-utils/logger.js";
|
import {TestLogger} from "./test-utils/logger.js";
|
||||||
import {clearCache} from "./utils/workflow-cache.js";
|
import {clearCache} from "./utils/workflow-cache.js";
|
||||||
import {ValueProviderConfig, ValueProviderKind} from "./value-providers/config.js";
|
import {ValueProviderConfig, ValueProviderKind} from "./value-providers/config.js";
|
||||||
|
import {FeatureFlags} from "@actions/expressions/features";
|
||||||
|
|
||||||
registerLogger(new TestLogger());
|
registerLogger(new TestLogger());
|
||||||
|
|
||||||
@@ -895,4 +896,32 @@ jobs:
|
|||||||
expect(result.some(x => x.label === "macos-latest")).toBe(true);
|
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), {
|
||||||
|
featureFlags: new FeatureFlags({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), {
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export async function complete(
|
|||||||
Mode.Completion
|
Mode.Completion
|
||||||
);
|
);
|
||||||
|
|
||||||
return getExpressionCompletionItems(token, context, newPos);
|
return getExpressionCompletionItems(token, context, newPos, config?.featureFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
const indentation = guessIndentation(newDoc, 2, true); // Use 2 spaces as default and most common for YAML
|
const indentation = guessIndentation(newDoc, 2, true); // Use 2 spaces as default and most common for YAML
|
||||||
@@ -521,7 +521,8 @@ export function getExistingValues(token: TemplateToken | null, parent: TemplateT
|
|||||||
function getExpressionCompletionItems(
|
function getExpressionCompletionItems(
|
||||||
token: TemplateToken,
|
token: TemplateToken,
|
||||||
context: DescriptionDictionary,
|
context: DescriptionDictionary,
|
||||||
pos: Position
|
pos: Position,
|
||||||
|
featureFlags?: FeatureFlags
|
||||||
): CompletionItem[] {
|
): CompletionItem[] {
|
||||||
if (!token.range) {
|
if (!token.range) {
|
||||||
return [];
|
return [];
|
||||||
@@ -540,7 +541,7 @@ function getExpressionCompletionItems(
|
|||||||
const expressionInput = (getExpressionInput(currentInput, cursorOffset) || "").trim();
|
const expressionInput = (getExpressionInput(currentInput, cursorOffset) || "").trim();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return completeExpression(expressionInput, context, [], validatorFunctions).map(item =>
|
return completeExpression(expressionInput, context, [], validatorFunctions, featureFlags).map(item =>
|
||||||
mapExpressionCompletionItem(item, currentInput[cursorOffset])
|
mapExpressionCompletionItem(item, currentInput[cursorOffset])
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user