Lint the workflow parser package

This commit is contained in:
Josh Gross
2023-03-17 11:17:18 -04:00
parent ac5b14b4c0
commit 272dec83ce
15 changed files with 47 additions and 41 deletions
+4
View File
@@ -4,4 +4,8 @@ module.exports = {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
rules: {
// TypeScript doesn't correctly detect toString() implementations on the token classes
"@typescript-eslint/restrict-template-expressions": "off",
}
}
+8 -7
View File
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import {StringToken} from "./templates/tokens";
import {isBasicExpression, isString} from "./templates/tokens/type-guards";
import {nullTrace} from "./test-utils/null-trace";
@@ -25,7 +26,7 @@ jobs:
const runNameMapping = run.get(1)!;
expect(runNameMapping?.key?.assertString("run-name key").value).toBe("run-name");
const v = runNameMapping.value!;
const v = runNameMapping.value;
expect(v).not.toBeUndefined();
if (!isBasicExpression(v)) {
@@ -61,14 +62,14 @@ jobs:
const jobsMapping = run.get(1)!;
expect(jobsMapping?.key?.assertString("jobs").value).toBe("jobs");
const job = jobsMapping.value!.assertMapping("jobs")!.get(0)!.value!.assertMapping("job");
const job = jobsMapping.value.assertMapping("jobs").get(0).value.assertMapping("job");
const stepRun = job
.get(1)
.value!.assertSequence("steps")
.value.assertSequence("steps")
.get(0)
.assertMapping("step")
.get(0)
.value!.assertScalar("step-run");
.value.assertScalar("step-run");
if (!isBasicExpression(stepRun)) {
throw new Error("expected run-name to be a basic expression");
@@ -123,14 +124,14 @@ jobs:
const jobsMapping = run.get(1)!;
expect(jobsMapping?.key?.assertString("jobs").value).toBe("jobs");
const job = jobsMapping.value!.assertMapping("jobs")!.get(0)!.value!.assertMapping("job");
const job = jobsMapping.value.assertMapping("jobs").get(0).value.assertMapping("job");
const stepRun = job
.get(1)
.value!.assertSequence("steps")
.value.assertSequence("steps")
.get(0)
.assertMapping("step")
.get(0)
.value!.assertScalar("step-run");
.value.assertScalar("step-run");
expect(isString(stepRun)).toBe(true);
expect((stepRun as StringToken).value).toContain("${{");
+1 -1
View File
@@ -72,7 +72,7 @@ jobs:
expect(result.context.errors.getErrors()).toHaveLength(0);
expect(result.value).not.toBeUndefined();
const root = result.value!.assertMapping("root");
const root = result.value!.assertMapping("root"); // eslint-disable-line @typescript-eslint/no-non-null-assertion
expect(root.description).toBe("Workflow file with strict validation");
for (const pair of root) {
const key = pair.key.assertString("key").value;
@@ -12,7 +12,7 @@ export function convertToJobContainer(context: TemplateContext, container: Templ
// Skip validation for expressions for now to match
// behavior of the other parsers
for (const [_, token, __] of TemplateToken.traverse(container)) {
for (const [, token] of TemplateToken.traverse(container)) {
if (token.isExpression) {
return;
}
@@ -47,7 +47,7 @@ export class IdBuilder {
if (attempt === 1) {
suffix = "";
} else {
suffix = "_" + attempt;
suffix = `_${attempt}`;
}
const candidate = original.substring(0, Math.min(original.length, MAX_LENGTH - suffix.length)) + suffix;
+1 -1
View File
@@ -1,6 +1,6 @@
import {TemplateContext} from "../../templates/template-context";
import {BasicExpressionToken, MappingToken, ScalarToken, StringToken, TemplateToken} from "../../templates/tokens";
import {isMapping, isSequence, isString} from "../../templates/tokens/type-guards";
import {isSequence, isString} from "../../templates/tokens/type-guards";
import {Step, WorkflowJob} from "../workflow-template";
import {convertConcurrency} from "./concurrency";
import {convertToJobContainer, convertToJobServices} from "./container";
@@ -13,10 +13,10 @@ export function convertWorkflowJobInputs(context: TemplateContext, job: Reusable
const inputValues = createTokenMap(job["input-values"]?.assertMapping("workflow job input values"), "with");
if (inputDefinitions !== undefined) {
for (const [_, [name, value]] of inputDefinitions) {
const inputSpec = createTokenMap(value.assertMapping(`input ${name}`), `input ${name} key`)!;
for (const [, [name, value]] of inputDefinitions) {
const inputSpec = createTokenMap(value.assertMapping(`input ${name}`), `input ${name} key`);
const inputTypeToken = inputSpec.get("type")?.[1];
const inputTypeToken = inputSpec?.get("type")?.[1];
if (!inputTypeToken) {
// This should be validated by the template reader per the schema
continue;
@@ -31,7 +31,7 @@ export function convertWorkflowJobInputs(context: TemplateContext, job: Reusable
}
if (inputValues !== undefined) {
for (const [_, [name, value]] of inputValues) {
for (const [, [name, value]] of inputValues) {
if (!inputDefinitions?.has(name.toLowerCase())) {
context.error(value, `Invalid input, ${name} is not defined in the referenced workflow.`);
}
@@ -5,7 +5,7 @@ import {createTokenMap} from "./inputs";
export function convertWorkflowJobSecrets(context: TemplateContext, job: ReusableWorkflowJob) {
// No validation if job passes all secrets
if (!!job["inherit-secrets"]) {
if (job["inherit-secrets"]) {
return;
}
@@ -17,14 +17,14 @@ export function convertWorkflowJobSecrets(context: TemplateContext, job: Reusabl
const secretValues = createTokenMap(job["secret-values"]?.assertMapping("workflow job secret values"), "secrets");
if (secretDefinitions !== undefined) {
for (const [_, [name, value]] of secretDefinitions) {
for (const [, [name, value]] of secretDefinitions) {
if (value instanceof NullToken) {
continue;
}
const secretSpec = createTokenMap(value.assertMapping(`secret ${name}`), `secret ${name} key`)!;
const secretSpec = createTokenMap(value.assertMapping(`secret ${name}`), `secret ${name} key`);
const required = secretSpec.get("required")?.[1].assertBoolean(`secret ${name} required`).value;
const required = secretSpec?.get("required")?.[1].assertBoolean(`secret ${name} required`).value;
if (required) {
if (secretValues == undefined || !secretValues.has(name.toLowerCase())) {
context.error(job.ref, `Secret ${name} is required, but not provided while calling.`);
@@ -34,7 +34,7 @@ export function convertWorkflowJobSecrets(context: TemplateContext, job: Reusabl
}
if (secretValues !== undefined) {
for (const [_, [name, value]] of secretValues) {
for (const [, [name, value]] of secretValues) {
if (!secretDefinitions?.has(name.toLowerCase())) {
context.error(value, `Invalid secret, ${name} is not defined in the referenced workflow.`);
}
@@ -1,4 +1,3 @@
import {TemplateSchema} from ".";
import {DEFINITION, BOOLEAN} from "../template-constants";
import {MappingToken, LiteralToken} from "../tokens";
import {TokenType} from "../tokens/types";
@@ -40,5 +39,7 @@ export class BooleanDefinition extends ScalarDefinition {
return literal.templateTokenType === TokenType.Boolean;
}
public override validate(schema: TemplateSchema, name: string): void {}
public override validate(): void {
// no-op
}
}
@@ -1,4 +1,3 @@
import {TemplateSchema} from "./template-schema";
import {DEFINITION, NULL} from "../template-constants";
import {MappingToken, LiteralToken} from "../tokens";
import {DefinitionType} from "./definition-type";
@@ -40,5 +39,7 @@ export class NullDefinition extends ScalarDefinition {
return literal.templateTokenType === TokenType.Null;
}
public override validate(schema: TemplateSchema, name: string): void {}
public override validate(): void {
// no-op
}
}
@@ -1,4 +1,3 @@
import {TemplateSchema} from "./template-schema";
import {DEFINITION, NUMBER} from "../template-constants";
import {MappingToken, LiteralToken} from "../tokens";
import {DefinitionType} from "./definition-type";
@@ -40,5 +39,7 @@ export class NumberDefinition extends ScalarDefinition {
return literal.templateTokenType === TokenType.Number;
}
public override validate(schema: TemplateSchema, name: string): void {}
public override validate(): void {
// no-op
}
}
@@ -3,7 +3,6 @@ import {LiteralToken, MappingToken, StringToken} from "../tokens";
import {TokenType} from "../tokens/types";
import {DefinitionType} from "./definition-type";
import {ScalarDefinition} from "./scalar-definition";
import {TemplateSchema} from "./template-schema";
export class StringDefinition extends ScalarDefinition {
public constant = "";
@@ -80,7 +79,7 @@ export class StringDefinition extends ScalarDefinition {
return false;
}
public override validate(schema: TemplateSchema, name: string): void {
public override validate(): void {
if (this.constant && this.requireNonEmpty) {
throw new Error(`Properties '${CONSTANT}' and '${REQUIRE_NON_EMPTY}' cannot both be set`);
}
@@ -324,6 +324,7 @@ export class TemplateSchema {
const template = readTemplate(context, TEMPLATE_SCHEMA, objectReader, undefined);
context.errors.check();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const mapping = template!.assertMapping(TEMPLATE_SCHEMA);
const schema = new TemplateSchema(mapping);
schema.validate();
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion */
import {nullTrace} from "../../test-utils/null-trace";
import {parseWorkflow} from "../../workflows/workflow-parser";
import {StringToken} from "./string-token";
@@ -20,13 +21,13 @@ describe("traverse", () => {
expect(traverser.next()!.value).toEqual([undefined, root, undefined]);
// On
const onResult = traverser.next()!.value!;
const onResult = traverser.next().value!;
expect(onResult[0]).toBe(root);
expect(getValue(onResult[1])).toEqual("on");
expect(onResult[2]).toBeUndefined();
// Push
const pushResult = traverser.next()!.value!;
const pushResult = traverser.next().value!;
expect(pushResult[0]).toBe(root);
expect(getValue(pushResult[1])).toEqual("push");
expect(getValue(pushResult[2])).toEqual("on");
+8 -11
View File
@@ -2,7 +2,7 @@ import * as fs from "fs";
import * as path from "path";
import * as YAML from "yaml";
import {convertWorkflowTemplate} from "./model/convert";
import {TraceWriter} from "./templates/trace-writer";
import {NoOperationTraceWriter} from "./templates/trace-writer";
import {File} from "./workflows/file";
import {FileProvider} from "./workflows/file-provider";
import {fileIdentifier, FileReference} from "./workflows/file-reference";
@@ -13,12 +13,7 @@ interface TestOptions {
skip?: string[];
}
const nullTrace: TraceWriter = {
info: x => {},
verbose: x => {},
error: x => {}
};
const nullTrace = new NoOperationTraceWriter();
const testFiles = "./testdata/reader";
const skippedTestsFile = "./testdata/skipped-tests.txt";
@@ -44,7 +39,7 @@ describe("x-lang tests", () => {
const testDocs: string[] = inputFile.split(/\r?\n---\r?\n/);
expect(testDocs.length).toBeGreaterThanOrEqual(3);
const testOptions: TestOptions = YAML.parse(testDocs[0]);
const testOptions = YAML.parse(testDocs[0]) as TestOptions;
const unsupportedTest = skippedTests.has(file);
const test = async () => {
@@ -65,10 +60,10 @@ describe("x-lang tests", () => {
}
const testFileProvider: FileProvider = {
getFileContent: async (ref: FileReference) => {
getFileContent: (ref: FileReference) => {
const file = reusableWorkflows[fileIdentifier(ref)];
if (file) {
return file;
return Promise.resolve(file);
}
throw new Error("File not found: " + fileName);
@@ -87,7 +82,7 @@ describe("x-lang tests", () => {
const workflowTemplate = await convertWorkflowTemplate(
parseResult.context,
parseResult.value!,
parseResult.value!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
testFileProvider,
{
fetchReusableWorkflowDepth: 1
@@ -99,11 +94,13 @@ describe("x-lang tests", () => {
const includeEvents =
testOptions.skip !== undefined && contains(testOptions.skip, "Go") && contains(testOptions.skip, "C#");
if (!includeEvents) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
delete (workflowTemplate as any).events;
}
// Other parsers don't have a partial template when there are errors
if (workflowTemplate.errors) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
delete (workflowTemplate as any).jobs;
}