Merge pull request #46 from github/joshmgross/partial-matrix-completion

Support partial matrix context completion
This commit is contained in:
Josh Gross
2022-12-09 10:45:25 -05:00
committed by GitHub
6 changed files with 69 additions and 31 deletions
@@ -532,7 +532,7 @@ jobs:
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual([]); expect(result.map(x => x.label)).toEqual(["animal", "fruit"]);
}); });
it("matrix with expression in property", async () => { it("matrix with expression in property", async () => {
+2 -2
View File
@@ -11,7 +11,7 @@ import {File} from "@github/actions-workflow-parser/workflows/file";
import {Position, TextDocument} from "vscode-languageserver-textdocument"; import {Position, TextDocument} from "vscode-languageserver-textdocument";
import {CompletionItem, TextEdit, Range} from "vscode-languageserver-types"; import {CompletionItem, TextEdit, Range} from "vscode-languageserver-types";
import {ContextProviderConfig} from "./context-providers/config"; import {ContextProviderConfig} from "./context-providers/config";
import {getContext} from "./context-providers/default"; import {getContext, Mode} from "./context-providers/default";
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context"; import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
import {nullTrace} from "./nulltrace"; import {nullTrace} from "./nulltrace";
import {getAllowedContext} from "./utils/allowed-context"; import {getAllowedContext} from "./utils/allowed-context";
@@ -85,7 +85,7 @@ export async function complete(
const expressionInput = (getExpressionInput(currentInput, relCharPos) || "").trim(); const expressionInput = (getExpressionInput(currentInput, relCharPos) || "").trim();
const allowedContext = getAllowedContext(token, parent); const allowedContext = getAllowedContext(token, parent);
const context = await getContext(allowedContext, contextProviderConfig, workflowContext); const context = await getContext(allowedContext, contextProviderConfig, workflowContext, Mode.Completion);
return completeExpression(expressionInput, context, []); return completeExpression(expressionInput, context, []);
} }
@@ -12,16 +12,23 @@ import {getStrategyContext} from "./strategy";
// Null indicates that the context provider doesn't have any value to provide // Null indicates that the context provider doesn't have any value to provide
export type ContextValue = data.Dictionary | data.Null; export type ContextValue = data.Dictionary | data.Null;
export enum Mode {
Completion,
Validation,
Hover
}
export async function getContext( export async function getContext(
names: string[], names: string[],
config: ContextProviderConfig | undefined, config: ContextProviderConfig | undefined,
workflowContext: WorkflowContext workflowContext: WorkflowContext,
mode: Mode
): Promise<data.Dictionary> { ): Promise<data.Dictionary> {
const context = new data.Dictionary(); const context = new data.Dictionary();
const filteredNames = filterContextNames(names, workflowContext); const filteredNames = filterContextNames(names, workflowContext);
for (const contextName of filteredNames) { for (const contextName of filteredNames) {
let value = (await getDefaultContext(contextName, workflowContext)) || new data.Dictionary(); let value = getDefaultContext(contextName, workflowContext, mode) || new data.Dictionary();
if (value.kind === Kind.Null) { if (value.kind === Kind.Null) {
context.add(contextName, value); context.add(contextName, value);
continue; continue;
@@ -35,7 +42,7 @@ export async function getContext(
return context; return context;
} }
async function getDefaultContext(name: string, workflowContext: WorkflowContext): Promise<ContextValue | undefined> { function getDefaultContext(name: string, workflowContext: WorkflowContext, mode: Mode): ContextValue | undefined {
switch (name) { switch (name) {
case "runner": case "runner":
return objectToDictionary({ return objectToDictionary({
@@ -62,7 +69,7 @@ async function getDefaultContext(name: string, workflowContext: WorkflowContext)
return getStrategyContext(workflowContext); return getStrategyContext(workflowContext);
case "matrix": case "matrix":
return getMatrixContext(workflowContext); return getMatrixContext(workflowContext, mode);
} }
return undefined; return undefined;
@@ -7,6 +7,7 @@ import {SequenceToken} from "@github/actions-workflow-parser/templates/tokens/se
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token"; import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
import {WorkflowContext} from "../context/workflow-context"; import {WorkflowContext} from "../context/workflow-context";
import {Mode} from "./default";
import {getMatrixContext} from "./matrix"; import {getMatrixContext} from "./matrix";
type MatrixMap = { type MatrixMap = {
@@ -62,7 +63,7 @@ describe("matrix context", () => {
const workflowContext = {} as WorkflowContext; const workflowContext = {} as WorkflowContext;
expect(workflowContext.job).toBeUndefined(); expect(workflowContext.job).toBeUndefined();
const context = getMatrixContext(workflowContext); const context = getMatrixContext(workflowContext, Mode.Validation);
expect(context).toEqual(new data.Dictionary()); expect(context).toEqual(new data.Dictionary());
}); });
@@ -71,7 +72,7 @@ describe("matrix context", () => {
const workflowContext = {job} as WorkflowContext; const workflowContext = {job} as WorkflowContext;
expect(workflowContext.job!.strategy).toBeUndefined(); expect(workflowContext.job!.strategy).toBeUndefined();
const context = getMatrixContext(workflowContext); const context = getMatrixContext(workflowContext, Mode.Validation);
expect(context).toEqual(new data.Dictionary()); expect(context).toEqual(new data.Dictionary());
}); });
@@ -79,7 +80,7 @@ describe("matrix context", () => {
const workflowContext = contextFromStrategy(stringToToken("hello")); const workflowContext = contextFromStrategy(stringToToken("hello"));
expect(workflowContext.job!.strategy).toBeDefined(); expect(workflowContext.job!.strategy).toBeDefined();
const context = getMatrixContext(workflowContext); const context = getMatrixContext(workflowContext, Mode.Validation);
expect(context).toEqual(new data.Dictionary()); expect(context).toEqual(new data.Dictionary());
}); });
@@ -87,7 +88,7 @@ describe("matrix context", () => {
const strategy = new MappingToken(undefined, undefined, undefined); const strategy = new MappingToken(undefined, undefined, undefined);
const workflowContext = contextFromStrategy(strategy); const workflowContext = contextFromStrategy(strategy);
const context = getMatrixContext(workflowContext); const context = getMatrixContext(workflowContext, Mode.Validation);
expect(context).toEqual(new data.Null()); expect(context).toEqual(new data.Null());
}); });
@@ -96,7 +97,7 @@ describe("matrix context", () => {
strategy.add(stringToToken("matrix"), stringToToken("hello")); strategy.add(stringToToken("matrix"), stringToToken("hello"));
const workflowContext = contextFromStrategy(strategy); const workflowContext = contextFromStrategy(strategy);
const context = getMatrixContext(workflowContext); const context = getMatrixContext(workflowContext, Mode.Validation);
expect(context).toEqual(new data.Null()); expect(context).toEqual(new data.Null());
}); });
@@ -105,7 +106,7 @@ describe("matrix context", () => {
strategy.add(stringToToken("matrix"), new MappingToken(undefined, undefined, undefined)); strategy.add(stringToToken("matrix"), new MappingToken(undefined, undefined, undefined));
const workflowContext = contextFromStrategy(strategy); const workflowContext = contextFromStrategy(strategy);
const context = getMatrixContext(workflowContext); const context = getMatrixContext(workflowContext, Mode.Validation);
expect(context).toEqual(new data.Dictionary()); expect(context).toEqual(new data.Dictionary());
}); });
}); });
@@ -116,7 +117,7 @@ describe("matrix context", () => {
strategy.add(stringToToken("matrix"), expressionToToken("${{ fromJSON(needs.job1.outputs.matrix) }}")); strategy.add(stringToToken("matrix"), expressionToToken("${{ fromJSON(needs.job1.outputs.matrix) }}"));
const workflowContext = contextFromStrategy(strategy); const workflowContext = contextFromStrategy(strategy);
const context = getMatrixContext(workflowContext); const context = getMatrixContext(workflowContext, Mode.Validation);
expect(context).toEqual(new data.Null()); expect(context).toEqual(new data.Null());
}); });
@@ -136,11 +137,37 @@ describe("matrix context", () => {
strategy.add(stringToToken("matrix"), matrix); strategy.add(stringToToken("matrix"), matrix);
const workflowContext = contextFromStrategy(strategy); const workflowContext = contextFromStrategy(strategy);
const context = getMatrixContext(workflowContext); const context = getMatrixContext(workflowContext, Mode.Validation);
expect(context).toEqual(new data.Null()); expect(context).toEqual(new data.Null());
}); });
it("matrix with include expression during completion", () => {
const include = expressionToToken("${{ fromJSON(needs.job1.outputs.matrix) }}");
const nodeSequence = new SequenceToken(undefined, undefined, undefined);
nodeSequence.add(stringToToken("12"));
nodeSequence.add(stringToToken("14"));
const matrix = new MappingToken(undefined, undefined, undefined);
matrix.add(stringToToken("node"), nodeSequence);
matrix.add(stringToToken("include"), include);
const strategy = new MappingToken(undefined, undefined, undefined);
strategy.add(stringToToken("matrix"), matrix);
const workflowContext = contextFromStrategy(strategy);
const context = getMatrixContext(workflowContext, Mode.Completion);
expect(context).toEqual(
new data.Dictionary({
key: "node",
value: new data.Array(new data.StringData("12"), new data.StringData("14"))
})
);
});
it("matrix with expression within property", () => { it("matrix with expression within property", () => {
const version = expressionToToken("${{ github.event.client_payload.versions }}"); const version = expressionToToken("${{ github.event.client_payload.versions }}");
@@ -151,7 +178,7 @@ describe("matrix context", () => {
strategy.add(stringToToken("matrix"), matrix); strategy.add(stringToToken("matrix"), matrix);
const workflowContext = contextFromStrategy(strategy); const workflowContext = contextFromStrategy(strategy);
const context = getMatrixContext(workflowContext); const context = getMatrixContext(workflowContext, Mode.Validation);
expect(context).toEqual( expect(context).toEqual(
new data.Dictionary({ new data.Dictionary({
@@ -166,7 +193,7 @@ describe("matrix context", () => {
it("basic matrix", () => { it("basic matrix", () => {
const workflowContext = createMatrix({os: ["ubuntu-latest", "windows-latest"]}); const workflowContext = createMatrix({os: ["ubuntu-latest", "windows-latest"]});
const context = getMatrixContext(workflowContext); const context = getMatrixContext(workflowContext, Mode.Validation);
expect(context).toEqual( expect(context).toEqual(
new data.Dictionary({ new data.Dictionary({
key: "os", key: "os",
@@ -181,7 +208,7 @@ describe("matrix context", () => {
node: ["12", "14"] node: ["12", "14"]
}); });
const context = getMatrixContext(workflowContext); const context = getMatrixContext(workflowContext, Mode.Validation);
expect(context).toEqual( expect(context).toEqual(
new data.Dictionary( new data.Dictionary(
{ {
@@ -208,7 +235,7 @@ describe("matrix context", () => {
] ]
}); });
const context = getMatrixContext(workflowContext); const context = getMatrixContext(workflowContext, Mode.Validation);
expect(context).toEqual( expect(context).toEqual(
new data.Dictionary( new data.Dictionary(
@@ -242,7 +269,7 @@ describe("matrix context", () => {
] ]
}); });
const context = getMatrixContext(workflowContext); const context = getMatrixContext(workflowContext, Mode.Validation);
expect(context).toEqual( expect(context).toEqual(
new data.Dictionary( new data.Dictionary(
@@ -276,7 +303,7 @@ describe("matrix context", () => {
] ]
}); });
const context = getMatrixContext(workflowContext); const context = getMatrixContext(workflowContext, Mode.Validation);
expect(context).toEqual( expect(context).toEqual(
new data.Dictionary( new data.Dictionary(
@@ -311,7 +338,7 @@ describe("matrix context", () => {
] ]
}); });
const context = getMatrixContext(workflowContext); const context = getMatrixContext(workflowContext, Mode.Validation);
expect(context).toEqual(new data.Dictionary()); expect(context).toEqual(new data.Dictionary());
}); });
@@ -4,9 +4,9 @@ import {KeyValuePair} from "@github/actions-workflow-parser/templates/tokens/key
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token"; import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
import {SequenceToken} from "@github/actions-workflow-parser/templates/tokens/sequence-token"; import {SequenceToken} from "@github/actions-workflow-parser/templates/tokens/sequence-token";
import {WorkflowContext} from "../context/workflow-context"; import {WorkflowContext} from "../context/workflow-context";
import {ContextValue} from "./default"; import {ContextValue, Mode} from "./default";
export function getMatrixContext(workflowContext: WorkflowContext): ContextValue { export function getMatrixContext(workflowContext: WorkflowContext, mode: Mode): ContextValue {
// https://docs.github.com/en/actions/learn-github-actions/contexts#matrix-context // https://docs.github.com/en/actions/learn-github-actions/contexts#matrix-context
const strategy = workflowContext.job?.strategy; const strategy = workflowContext.job?.strategy;
if (!strategy || !isMapping(strategy)) { if (!strategy || !isMapping(strategy)) {
@@ -19,7 +19,7 @@ export function getMatrixContext(workflowContext: WorkflowContext): ContextValue
return new data.Null(); return new data.Null();
} }
const properties = matrixProperties(matrix); const properties = matrixProperties(matrix, mode);
if (!properties) { if (!properties) {
// Matrix included an expression, so there's no context we can provide // Matrix included an expression, so there's no context we can provide
return new data.Null(); return new data.Null();
@@ -94,7 +94,7 @@ export function getMatrixContext(workflowContext: WorkflowContext): ContextValue
* *
* Keys: os, version, environment * Keys: os, version, environment
*/ */
function matrixProperties(matrix: MappingToken): Map<string, Set<string> | undefined> | undefined { function matrixProperties(matrix: MappingToken, mode: Mode): Map<string, Set<string> | undefined> | undefined {
const properties = new Map<string, Set<string> | undefined>(); const properties = new Map<string, Set<string> | undefined>();
let include: SequenceToken | undefined; let include: SequenceToken | undefined;
@@ -108,9 +108,14 @@ function matrixProperties(matrix: MappingToken): Map<string, Set<string> | undef
const key = pair.key.value; const key = pair.key.value;
switch (key) { switch (key) {
case "include": case "include":
// If "include" is an expression, we can't know the properties of the matrix // If "include" is an expression, we can't know the full properties of the matrix
if (isBasicExpression(pair.value) || !isSequence(pair.value)) { if (isBasicExpression(pair.value) || !isSequence(pair.value)) {
return; // Without the full properties of the matrix, we shouldn't validate anything
if (mode === Mode.Validation) {
return;
} else {
continue;
}
} }
include = pair.value; include = pair.value;
break; break;
+2 -3
View File
@@ -10,7 +10,6 @@ import {
} from "@github/actions-workflow-parser"; } from "@github/actions-workflow-parser";
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert"; import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
import {splitAllowedContext} from "@github/actions-workflow-parser/templates/allowed-context"; import {splitAllowedContext} from "@github/actions-workflow-parser/templates/allowed-context";
import {Definition} from "@github/actions-workflow-parser/templates/schema/definition";
import {BasicExpressionToken} from "@github/actions-workflow-parser/templates/tokens/basic-expression-token"; import {BasicExpressionToken} from "@github/actions-workflow-parser/templates/tokens/basic-expression-token";
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token"; import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
@@ -19,7 +18,7 @@ import {TextDocument} from "vscode-languageserver-textdocument";
import {Diagnostic, DiagnosticSeverity, URI} from "vscode-languageserver-types"; import {Diagnostic, DiagnosticSeverity, URI} from "vscode-languageserver-types";
import {ContextProviderConfig} from "./context-providers/config"; import {ContextProviderConfig} from "./context-providers/config";
import {getContext} from "./context-providers/default"; import {getContext, Mode} from "./context-providers/default";
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context"; import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
import {AccessError, wrapDictionary} from "./expression-validation/error-dictionary"; import {AccessError, wrapDictionary} from "./expression-validation/error-dictionary";
import {error} from "./log"; import {error} from "./log";
@@ -196,7 +195,7 @@ async function validateExpression(
} }
try { try {
const context = await getContext(namedContexts, contextProviderConfig, workflowContext); const context = await getContext(namedContexts, contextProviderConfig, workflowContext, Mode.Validation);
const e = new Evaluator(expr, wrapDictionary(context)); const e = new Evaluator(expr, wrapDictionary(context));
e.evaluate(); e.evaluate();