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