Use tokenPath for value providers

This commit is contained in:
Christopher Schleiden
2022-11-23 15:35:34 -08:00
parent fd942fc6e2
commit 66e0f47151
9 changed files with 250 additions and 63 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
import {complete} from "./complete";
import {WorkflowContext} from "./context/workflow-context";
import {getPositionFromCursor} from "./test-utils/cursor-position";
import {Value, ValueProviderConfig, WorkflowContext} from "./value-providers/config";
import {Value, ValueProviderConfig} from "./value-providers/config";
describe("completion", () => {
it("runs-on", async () => {
+14 -13
View File
@@ -1,5 +1,5 @@
import {complete as completeExpression} from "@github/actions-expressions";
import {convertWorkflowTemplate, isMapping, isSequence, isString, parseWorkflow} from "@github/actions-workflow-parser";
import {convertWorkflowTemplate, isSequence, isString, parseWorkflow} from "@github/actions-workflow-parser";
import {CLOSE_EXPRESSION, OPEN_EXPRESSION} from "@github/actions-workflow-parser/templates/template-constants";
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/index";
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
@@ -9,10 +9,11 @@ import {Position, TextDocument} from "vscode-languageserver-textdocument";
import {CompletionItem} from "vscode-languageserver-types";
import {ContextProviderConfig} from "./context-providers/config";
import {getContext} from "./context-providers/default";
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
import {nullTrace} from "./nulltrace";
import {findToken} from "./utils/find-token";
import {transform} from "./utils/transform";
import {Value, ValueProviderConfig, WorkflowContext} from "./value-providers/config";
import {Value, ValueProviderConfig} from "./value-providers/config";
import {defaultValueProviders} from "./value-providers/default";
import {definitionValues} from "./value-providers/definition";
@@ -60,7 +61,7 @@ export async function complete(
return [];
}
const {token, keyToken, parent, parentKey} = findToken(newPos, result.value);
const {token, keyToken, parent, path} = findToken(newPos, result.value);
const template = convertWorkflowTemplate(result.context, result.value);
// If we are inside an expression, take a different code-path. The workflow parser does not correctly create
@@ -84,15 +85,14 @@ export async function complete(
}
}
const workflowContext = {uri: textDocument.uri, template: template};
const values = await getValues(token, parent, parentKey, valueProviderConfig, workflowContext);
const workflowContext = getWorkflowContext(textDocument.uri, template, path);
const values = await getValues(token, parent, valueProviderConfig, workflowContext);
return values.map(value => CompletionItem.create(value.label));
}
async function getValues(
token: TemplateToken | null,
parent: TemplateToken | null,
parentKey: TemplateToken | null,
valueProviderConfig: ValueProviderConfig | undefined,
workflowContext: WorkflowContext
): Promise<Value[]> {
@@ -100,7 +100,7 @@ async function getValues(
return [];
}
const existingValues = getExistingValues(token, parent, parentKey);
const existingValues = getExistingValues(token, parent);
if (token?.definition?.key) {
const customValues = await valueProviderConfig?.getCustomValues(token.definition.key, workflowContext);
@@ -130,18 +130,16 @@ async function getValues(
return filterAndSortCompletionOptions(values, existingValues);
}
function getExistingValues(token: TemplateToken | null, parent: TemplateToken, parentKey: TemplateToken | null) {
function getExistingValues(token: TemplateToken | null, parent: TemplateToken) {
// For incomplete YAML, we may only have a parent token
if (token) {
if (!isString(token)) {
return;
}
if (isMapping(parent) && parentKey && isString(parentKey)) {
return new Set<string>([parentKey.value]);
}
if (isSequence(parent)) {
const sequenceValues = new Set<string>();
for (let i = 0; i < parent.count; i++) {
const t = parent.get(i);
if (isString(t)) {
@@ -149,6 +147,7 @@ function getExistingValues(token: TemplateToken | null, parent: TemplateToken, p
sequenceValues.add(t.value);
}
}
return sequenceValues;
}
}
@@ -157,9 +156,11 @@ function getExistingValues(token: TemplateToken | null, parent: TemplateToken, p
// No token and parent is a mapping, so we're completing a key
const mapKeys = new Set<string>();
const mapToken = parent as MappingToken;
for (let i = 0; i < mapToken.count; i++) {
const key = mapToken.get(i).key;
if (key.isLiteral && isString(key)) {
if (isString(key)) {
mapKeys.add(key.value);
}
}
@@ -169,7 +170,7 @@ function getExistingValues(token: TemplateToken | null, parent: TemplateToken, p
}
function filterAndSortCompletionOptions(options: Value[], existingValues?: Set<string>) {
options = options.filter(x => !existingValues || !existingValues.has(x.label));
options = options.filter(x => !existingValues?.has(x.label));
options.sort((a, b) => a.label.localeCompare(b.label));
return options;
}
@@ -0,0 +1,58 @@
import {convertWorkflowTemplate, parseWorkflow, WorkflowTemplate} from "@github/actions-workflow-parser";
import {nullTrace} from "../nulltrace";
import {getPositionFromCursor} from "../test-utils/cursor-position";
import {findToken} from "../utils/find-token";
import {getWorkflowContext, WorkflowContext} from "./workflow-context";
function testGetWorkflowContext(input: string): [context: WorkflowContext, template?: WorkflowTemplate] {
const [textDocument, pos] = getPositionFromCursor(input);
const result = parseWorkflow(
"wf.yaml",
[
{
content: textDocument.getText(),
name: "wf.yaml"
}
],
nullTrace
);
let template: WorkflowTemplate | undefined;
if (result.value) {
template = convertWorkflowTemplate(result.context, result.value);
}
const {path} = findToken(pos, result.value);
return [getWorkflowContext(textDocument.uri, template, path), template];
}
describe("getWorkflowContext", () => {
it("context for workflow", () => {
const [context, template] = testGetWorkflowContext(`on: push
name: te|st
jobs:
build:
runs-on: ubuntu-latest
steps:
- echo Hello`);
expect(context.uri).not.toBe("");
expect(context.template).not.toBeUndefined();
expect(context.job).toBeUndefined();
expect(context.step).toBeUndefined();
});
it("context for workflow job", () => {
const [context, template] = testGetWorkflowContext(`on: push
jobs:
build:
runs-on: ubuntu-lat|est
steps:
- run: echo Hello`);
expect(context.uri).not.toBe("");
expect(context.template).not.toBeUndefined();
expect(context.job).not.toBeUndefined();
expect(context.step).toBeUndefined();
});
});
@@ -0,0 +1,40 @@
import {WorkflowTemplate} from "@github/actions-workflow-parser";
import {JobConfig, StepConfig} from "@github/actions-workflow-parser/model/workflow-template";
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
export interface WorkflowContext {
uri: string;
template: WorkflowTemplate | undefined;
/** If the context is for a position within a job, this will be the job */
job?: JobConfig;
/** If the context is for a position within a step, this will the step */
step?: StepConfig;
}
export function getWorkflowContext(
uri: string,
template: WorkflowTemplate | undefined,
tokenPath: TemplateToken[]
): WorkflowContext {
const context: WorkflowContext = {uri: uri, template};
if (template) {
// Iterate through the token path to find the job and step
for (let i = 0; i < tokenPath.length; ++i) {
const token = tokenPath[i];
switch (token.definition?.key) {
case "job-id": {
const jobID = (token as StringToken).value;
context.job = template.jobs.find(job => job.id === jobID);
}
}
}
}
return context;
}
@@ -23,7 +23,7 @@ function testFindToken(input: string): {
parent: testTokenInfo | null;
key: testTokenInfo | null;
token: testTokenInfo | null;
parentKey: testTokenInfo | null;
path: testTokenInfo[];
} {
const [textDocument, pos] = getPositionFromCursor(input);
const result = parseWorkflow(
@@ -41,17 +41,17 @@ function testFindToken(input: string): {
return {
parent: getTokenInfo(r.parent),
parentKey: getTokenInfo(r.parentKey),
key: getTokenInfo(r.keyToken),
token: getTokenInfo(r.token)
token: getTokenInfo(r.token),
path: r.path.map(x => getTokenInfo(x)!)
};
}
describe("find-token", () => {
it("on string key", () => {
expect(testFindToken(`o|n: push`)).toEqual({
path: [["workflow-root-strict", TokenType.Mapping]],
parent: ["workflow-root-strict", TokenType.Mapping],
parentKey: null,
key: null,
token: [null, TokenType.String, "on"]
});
@@ -59,8 +59,11 @@ describe("find-token", () => {
it("on string value", () => {
expect(testFindToken(`on: pu|sh`)).toEqual({
path: [
["workflow-root-strict", TokenType.Mapping],
[null, TokenType.String, "on"]
],
parent: ["workflow-root-strict", TokenType.Mapping],
parentKey: null,
key: [null, TokenType.String, "on"],
token: ["on-strict", TokenType.String, "push"]
});
@@ -71,8 +74,12 @@ describe("find-token", () => {
testFindToken(`on:
pu|sh:`)
).toEqual({
path: [
["workflow-root-strict", TokenType.Mapping],
[null, TokenType.String, "on"],
["on-mapping-strict", TokenType.Mapping]
],
parent: ["on-mapping-strict", TokenType.Mapping],
parentKey: [null, TokenType.String, "on"],
key: null,
token: [null, TokenType.String, "push"]
});
@@ -83,8 +90,12 @@ describe("find-token", () => {
testFindToken(`on:
- pu|sh`)
).toEqual({
path: [
["workflow-root-strict", TokenType.Mapping],
[null, TokenType.String, "on"],
["on-strict", TokenType.Sequence]
],
parent: ["on-strict", TokenType.Sequence],
parentKey: null,
key: null,
token: ["non-empty-string", TokenType.String, "push"]
});
@@ -95,8 +106,12 @@ describe("find-token", () => {
testFindToken(`on:
-| push`)
).toEqual({
path: [
["workflow-root-strict", TokenType.Mapping],
[null, TokenType.String, "on"],
["on-strict", TokenType.Sequence]
],
parent: ["on-strict", TokenType.Sequence],
parentKey: null,
key: null,
token: null
});
@@ -108,8 +123,12 @@ describe("find-token", () => {
- push
- pull_request|`)
).toEqual({
path: [
["workflow-root-strict", TokenType.Mapping],
[null, TokenType.String, "on"],
["on-strict", TokenType.Sequence]
],
parent: ["on-strict", TokenType.Sequence],
parentKey: null,
key: null,
token: ["non-empty-string", TokenType.String, "pull_request"]
});
@@ -122,8 +141,16 @@ jobs:
build:
runs-on: [ubuntu-latest, self|`)
).toEqual({
path: [
["workflow-root-strict", TokenType.Mapping],
[null, TokenType.String, "jobs"],
["jobs", TokenType.Mapping],
["job-id", TokenType.String, "build"],
["job-factory", TokenType.Mapping],
[null, TokenType.String, "runs-on"],
["runs-on", TokenType.Sequence]
],
parent: ["runs-on", TokenType.Sequence],
parentKey: null,
key: null,
token: ["non-empty-string", TokenType.String, "self"]
});
@@ -135,8 +162,8 @@ jobs:
jo|bs:
build:`)
).toEqual({
path: [["workflow-root-strict", TokenType.Mapping]],
parent: ["workflow-root-strict", TokenType.Mapping],
parentKey: null,
key: null,
token: [null, TokenType.String, "jobs"]
});
@@ -149,8 +176,15 @@ jobs:
build:
runs-on: ubu|`)
).toEqual({
path: [
["workflow-root-strict", TokenType.Mapping],
[null, TokenType.String, "jobs"],
["jobs", TokenType.Mapping],
["job-id", TokenType.String, "build"],
["job-factory", TokenType.Mapping],
[null, TokenType.String, "runs-on"]
],
parent: ["job-factory", TokenType.Mapping],
parentKey: ["job-id", TokenType.String, "build"],
key: [null, TokenType.String, "runs-on"],
token: ["runs-on", TokenType.String, "ubu"]
});
@@ -163,8 +197,14 @@ jobs:
build:
run|s-on: ubu`)
).toEqual({
path: [
["workflow-root-strict", TokenType.Mapping],
[null, TokenType.String, "jobs"],
["jobs", TokenType.Mapping],
["job-id", TokenType.String, "build"],
["job-factory", TokenType.Mapping]
],
parent: ["job-factory", TokenType.Mapping],
parentKey: ["job-id", TokenType.String, "build"],
key: null,
token: [null, TokenType.String, "runs-on"]
});
@@ -177,8 +217,14 @@ jobs:
build:
continue-on-error:|`)
).toEqual({
path: [
["workflow-root-strict", TokenType.Mapping],
[null, TokenType.String, "jobs"],
["jobs", TokenType.Mapping],
["job-id", TokenType.String, "build"],
["job-factory", TokenType.Mapping]
],
parent: ["job-factory", TokenType.Mapping],
parentKey: ["job-id", TokenType.String, "build"],
key: [null, TokenType.String, "continue-on-error"],
token: ["boolean-strategy-context", TokenType.Null, ""]
});
@@ -191,8 +237,14 @@ jobs:
build:
container:|`)
).toEqual({
path: [
["workflow-root-strict", TokenType.Mapping],
[null, TokenType.String, "jobs"],
["jobs", TokenType.Mapping],
["job-id", TokenType.String, "build"],
["job-factory", TokenType.Mapping]
],
parent: ["job-factory", TokenType.Mapping],
parentKey: ["job-id", TokenType.String, "build"],
key: [null, TokenType.String, "container"],
token: ["container", TokenType.String, ""]
});
@@ -205,8 +257,13 @@ jobs:
build:
continue-on-error:|foo`)
).toEqual({
path: [
["workflow-root-strict", TokenType.Mapping],
[null, TokenType.String, "jobs"],
["jobs", TokenType.Mapping],
["job-id", TokenType.String, "build"]
],
parent: ["jobs", TokenType.Mapping],
parentKey: [null, TokenType.String, "jobs"],
key: ["job-id", TokenType.String, "build"],
token: ["job", TokenType.String, "continue-on-error:foo"]
});
@@ -219,8 +276,14 @@ jobs:
build:
continue-on-error:| foo`)
).toEqual({
path: [
["workflow-root-strict", TokenType.Mapping],
[null, TokenType.String, "jobs"],
["jobs", TokenType.Mapping],
["job-id", TokenType.String, "build"],
["job-factory", TokenType.Mapping]
],
parent: ["job-factory", TokenType.Mapping],
parentKey: null,
key: null,
token: null
});
@@ -233,8 +296,14 @@ jobs:
build:
continue-on-error|: foo`)
).toEqual({
path: [
["workflow-root-strict", TokenType.Mapping],
[null, TokenType.String, "jobs"],
["jobs", TokenType.Mapping],
["job-id", TokenType.String, "build"],
["job-factory", TokenType.Mapping]
],
parent: ["job-factory", TokenType.Mapping],
parentKey: ["job-id", TokenType.String, "build"],
key: null,
token: [null, TokenType.String, "continue-on-error"]
});
@@ -247,8 +316,13 @@ jobs:
build:
runs-|`)
).toEqual({
path: [
["workflow-root-strict", TokenType.Mapping],
[null, TokenType.String, "jobs"],
["jobs", TokenType.Mapping],
["job-id", TokenType.String, "build"]
],
parent: ["jobs", TokenType.Mapping],
parentKey: [null, TokenType.String, "jobs"],
key: ["job-id", TokenType.String, "build"],
token: ["job", TokenType.String, "runs-"]
});
@@ -262,8 +336,13 @@ jobs:
runs-|
#`)
).toEqual({
path: [
["workflow-root-strict", TokenType.Mapping],
[null, TokenType.String, "jobs"],
["jobs", TokenType.Mapping],
["job-id", TokenType.String, "build"]
],
parent: ["jobs", TokenType.Mapping],
parentKey: [null, TokenType.String, "jobs"],
key: ["job-id", TokenType.String, "build"],
token: ["job", TokenType.String, "runs-"]
});
@@ -277,8 +356,15 @@ jobs:
concurrency:
runs-on: ubu|`)
).toEqual({
path: [
["workflow-root-strict", TokenType.Mapping],
[null, TokenType.String, "jobs"],
["jobs", TokenType.Mapping],
["job-id", TokenType.String, "build"],
["job-factory", TokenType.Mapping],
[null, TokenType.String, "runs-on"]
],
parent: ["job-factory", TokenType.Mapping],
parentKey: ["job-id", TokenType.String, "build"],
key: [null, TokenType.String, "runs-on"],
token: ["runs-on", TokenType.String, "ubu"]
});
+17 -15
View File
@@ -11,10 +11,11 @@ export function findInnerToken(pos: Position, root?: TemplateToken) {
}
export type TokenResult = {
token: TemplateToken | null;
keyToken: TemplateToken | null;
parent: TemplateToken | null;
parentKey: TemplateToken | null;
keyToken: TemplateToken | null;
token: TemplateToken | null;
path: TemplateToken[];
};
/**
@@ -35,23 +36,24 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
token: null,
keyToken: null,
parent: null,
parentKey: null
path: []
};
}
let lastMatchingToken: TemplateToken | null = null;
let lastMatching: TokenResult | null = null;
const s: TokenResult[] = [
{
token: root,
keyToken: null,
parent: null,
parentKey: null
path: []
}
];
while (s.length > 0) {
const {parent, token, keyToken, parentKey} = s.shift()!;
const result = s.shift()!;
const {parent, token, keyToken, path} = result;
if (!token) {
break;
}
@@ -61,7 +63,7 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
}
// Pos is in token, remember this token
lastMatchingToken = token;
lastMatching = result;
// Position is in token, enqueue children if there are any
switch (token.templateTokenType) {
@@ -76,7 +78,7 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
parent: mappingToken,
keyToken: null,
token: key,
parentKey: keyToken
path: [...path, mappingToken]
};
}
@@ -87,7 +89,7 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
parent: mappingToken,
keyToken: key,
token: value,
parentKey: keyToken
path: [...path, mappingToken]
};
}
@@ -95,7 +97,7 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
parent: mappingToken,
keyToken: key,
token: value,
parentKey: keyToken
path: [...path, mappingToken, key]
});
}
continue;
@@ -107,7 +109,7 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
parent: sequenceToken,
keyToken: null,
token: sequenceToken.get(i),
parentKey: null
path: [...path, sequenceToken]
});
}
continue;
@@ -117,16 +119,16 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
token,
keyToken,
parent,
parentKey
path
};
}
// Did not find a matching token, return the last matching token as parent
return {
token: null,
parent: lastMatchingToken,
parent: lastMatching?.token ?? null,
keyToken: null,
parentKey: null
path: lastMatching?.token ? [...(lastMatching?.path || []), lastMatching?.token] : []
};
}
@@ -1,4 +1,4 @@
import {WorkflowTemplate} from "@github/actions-workflow-parser/.";
import {WorkflowContext} from "../context/workflow-context";
export interface Value {
label: string;
@@ -7,10 +7,6 @@ export interface Value {
export type ValueProvider = () => Value[];
export interface WorkflowContext {
uri: string;
template: WorkflowTemplate | undefined;
}
export interface ValueProviderConfig {
getCustomValues: (key: string, context: WorkflowContext) => Promise<Value[] | undefined>;
getActionInputs?: (owner: string, name: string, ref: string, path?: string) => Promise<ActionInput[]>;
@@ -1,8 +1,9 @@
import {Value, WorkflowContext} from "./config";
import {getJobNames} from "./needs";
import {WorkflowContext} from "../context/workflow-context";
import {Value} from "./config";
import {needs} from "./needs";
export const defaultValueProviders: {[key: string]: (workflowContext: WorkflowContext) => Value[]} = {
needs: (workflowContext: WorkflowContext) => getJobNames(workflowContext.template),
needs,
"runs-on": () =>
stringsToValues([
"ubuntu-latest",
@@ -1,11 +1,13 @@
import {WorkflowTemplate} from "@github/actions-workflow-parser/model/workflow-template";
import {WorkflowContext} from "../context/workflow-context";
import {Value} from "./config";
export function getJobNames(template: WorkflowTemplate | undefined): Value[] {
if (!template) {
export function needs(context: WorkflowContext): Value[] {
if (!context.template) {
return [];
}
const uniquejobIDs = new Set(template.jobs.map(j => j.id)).values();
return Array.from(uniquejobIDs).map(x => ({label: x}));
const uniquejobIDs = new Set(context.template.jobs.map(j => j.id)).values();
return Array.from(uniquejobIDs)
.filter(x => x !== context.job?.id)
.map(x => ({label: x}));
}