Files
languageservices/languageservice/src/expression-hover/visitor.test.ts
T
eric scipleandGitHub d2ffb50a92 Add language service support for action.yml files (#275)
- Add validation, completion, hover, and document links for action.yml files
- Implement document type detection to route action.yml to action-specific handlers
- Add expression context for composite actions (inputs, steps, github, runner, etc.)
- Add schema validation for required fields, branding, and composite step requirements
- Support JavaScript (node20/node24), Docker, and composite action types
- Validate action references in composite action uses steps
- Add JSDoc comments to parser and template functions
- Refactor hover to use hoverToken consistently
- Fix lint errors and add return type annotations
2026-01-02 10:38:52 -06:00

142 lines
4.3 KiB
TypeScript

import {data, DescriptionDictionary, Lexer, Parser} from "@actions/expressions";
import {convertWorkflowTemplate, parseWorkflow} from "@actions/workflow-parser";
import {ErrorPolicy} from "@actions/workflow-parser/model/convert";
import {File} from "@actions/workflow-parser/workflows/file";
import {ContextProviderConfig} from "../context-providers/config.js";
import {getWorkflowExpressionContext, Mode} from "../context-providers/default.js";
import {getWorkflowContext} from "../context/workflow-context.js";
import {validatorFunctions} from "../expression-validation/functions.js";
import {nullTrace} from "../nulltrace.js";
import {getPositionFromCursor} from "../test-utils/cursor-position.js";
import {HoverVisitor} from "./visitor.js";
const contextProviderConfig: ContextProviderConfig = {
getContext: (context: string) => {
switch (context) {
case "github":
return Promise.resolve(
new DescriptionDictionary(
{
key: "event",
value: new data.StringData("push"),
description: "The event that triggered the workflow"
},
{
key: "test",
value: new DescriptionDictionary({
key: "name",
value: new data.StringData("push"),
description: "Name for the test"
}),
description: "Test dictionary"
}
)
);
}
return Promise.resolve(undefined);
}
};
describe("visitor", () => {
describe("unsupported hover positions", () => {
["1 =|= 2", "12|3", "1 == |(2)", "'ab|c'"].forEach(x =>
it(x, async () => expect(await hoverExpression(x)).toBeUndefined())
);
});
it("top-level context access", async () => {
expect(await hoverExpression("githu|b")).toEqual({
label: "github",
description:
"Information about the workflow run. For more information, see [`github` context](https://docs.github.com/actions/learn-github-actions/contexts#github-context).",
function: false,
range: {
start: {line: 0, column: 0},
end: {line: 0, column: 6}
}
});
});
it("nested context access", async () => {
expect(await hoverExpression("github.test.na|me")).toEqual({
label: "name",
description: "Name for the test",
function: false,
range: {
start: {line: 0, column: 0},
end: {line: 0, column: 16}
}
});
});
it("nested context access with string key", async () => {
expect(await hoverExpression("github['te|st']")).toEqual({
label: "test",
description: "Test dictionary",
function: false,
range: {
start: {line: 0, column: 0},
end: {line: 0, column: 13}
}
});
});
it("function call", async () => {
expect(await hoverExpression("cont|ains(github, 'github')")).toEqual({
label: "contains",
description:
"`contains( search, item )`\n\nReturns `true` if `search` contains `item`. If `search`" +
" is an array, this function returns `true` if the `item` is an element in the array. If `search`" +
" is a string, this function returns `true` if the `item` is a substring of `search`. This function" +
" is not case sensitive. Casts values to a string.",
function: true,
range: {
start: {line: 0, column: 0},
end: {line: 0, column: 8}
}
});
});
});
async function hoverExpression(input: string) {
const [td, pos] = getPositionFromCursor(input);
const allowedContext = ["github"];
const file: File = {
name: td.uri,
content: td.getText()
};
const result = parseWorkflow(file, nullTrace);
if (!result.value) {
return undefined;
}
const template = await convertWorkflowTemplate(result.context, result.value, undefined, {
errorPolicy: ErrorPolicy.TryConversion
});
const workflowContext = getWorkflowContext(td.uri, template, []);
const context = await getWorkflowExpressionContext(
allowedContext,
contextProviderConfig,
workflowContext,
Mode.Completion
);
const l = new Lexer(td.getText());
const lr = l.lex();
const p = new Parser(lr.tokens, ["github"], []);
const expr = p.parse();
const hv = new HoverVisitor(
{
line: pos.line,
column: pos.character
},
context,
validatorFunctions
);
return hv.hover(expr);
}