54404aa9ff
Validates format() function calls for: - Invalid syntax (missing closing brace, empty placeholder, non-numeric placeholder) - Argument count mismatch (placeholder references arg that doesn't exist) Port of Go's format_validator.go from actions-workflow-parser.
274 lines
7.5 KiB
TypeScript
274 lines
7.5 KiB
TypeScript
import {DiagnosticSeverity} from "vscode-languageserver-types";
|
|
import {createDocument} from "./test-utils/document.js";
|
|
import {validate} from "./validate.js";
|
|
import {clearCache} from "./utils/workflow-cache.js";
|
|
import {validateFormatString} from "./validate-format-string.js";
|
|
|
|
beforeEach(() => {
|
|
clearCache();
|
|
});
|
|
|
|
describe("format string validation", () => {
|
|
describe("validateFormatString unit tests", () => {
|
|
it("returns valid for simple placeholder", () => {
|
|
const result = validateFormatString("{0}");
|
|
expect(result).toEqual({valid: true, maxArgIndex: 0});
|
|
});
|
|
|
|
it("returns valid for multiple placeholders", () => {
|
|
const result = validateFormatString("{0} {1} {2}");
|
|
expect(result).toEqual({valid: true, maxArgIndex: 2});
|
|
});
|
|
|
|
it("returns valid for text with placeholder", () => {
|
|
const result = validateFormatString("hello {0} world");
|
|
expect(result).toEqual({valid: true, maxArgIndex: 0});
|
|
});
|
|
|
|
it("returns valid for escaped left braces", () => {
|
|
const result = validateFormatString("{{0}} {0}");
|
|
expect(result).toEqual({valid: true, maxArgIndex: 0});
|
|
});
|
|
|
|
it("returns valid for escaped right braces", () => {
|
|
const result = validateFormatString("{0}}}");
|
|
expect(result).toEqual({valid: true, maxArgIndex: 0});
|
|
});
|
|
|
|
it("returns valid for no placeholders", () => {
|
|
const result = validateFormatString("hello world");
|
|
expect(result).toEqual({valid: true, maxArgIndex: -1});
|
|
});
|
|
|
|
it("returns invalid for missing closing brace", () => {
|
|
const result = validateFormatString("{0");
|
|
expect(result).toEqual({valid: false, maxArgIndex: -1});
|
|
});
|
|
|
|
it("returns invalid for empty placeholder", () => {
|
|
const result = validateFormatString("{}");
|
|
expect(result).toEqual({valid: false, maxArgIndex: -1});
|
|
});
|
|
|
|
it("returns invalid for non-numeric placeholder", () => {
|
|
const result = validateFormatString("{abc}");
|
|
expect(result).toEqual({valid: false, maxArgIndex: -1});
|
|
});
|
|
|
|
it("returns invalid for unescaped closing brace", () => {
|
|
const result = validateFormatString("text } more");
|
|
expect(result).toEqual({valid: false, maxArgIndex: -1});
|
|
});
|
|
|
|
it("handles out-of-order placeholders", () => {
|
|
const result = validateFormatString("{2} {0} {1}");
|
|
expect(result).toEqual({valid: true, maxArgIndex: 2});
|
|
});
|
|
|
|
it("handles repeated placeholders", () => {
|
|
const result = validateFormatString("{0} {0} {0}");
|
|
expect(result).toEqual({valid: true, maxArgIndex: 0});
|
|
});
|
|
});
|
|
|
|
describe("InvalidFormatString workflow validation", () => {
|
|
it("errors on missing closing brace", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo \${{ format('{0', github.event_name) }}
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
expect(result).toContainEqual(
|
|
expect.objectContaining({
|
|
code: "invalid-format-string",
|
|
severity: DiagnosticSeverity.Error
|
|
})
|
|
);
|
|
});
|
|
|
|
it("errors on empty braces", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo \${{ format('{}', github.event_name) }}
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
expect(result).toContainEqual(
|
|
expect.objectContaining({
|
|
code: "invalid-format-string"
|
|
})
|
|
);
|
|
});
|
|
|
|
it("errors on non-numeric placeholder", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo \${{ format('{abc}', github.event_name) }}
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
expect(result).toContainEqual(
|
|
expect.objectContaining({
|
|
code: "invalid-format-string"
|
|
})
|
|
);
|
|
});
|
|
|
|
it("allows valid format strings", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo \${{ format('{0} {1}', github.event_name, github.ref) }}
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
expect(result).not.toContainEqual(
|
|
expect.objectContaining({
|
|
code: "invalid-format-string"
|
|
})
|
|
);
|
|
});
|
|
|
|
it("allows escaped braces", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo \${{ format('{{0}} {0}', github.event_name) }}
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
expect(result).not.toContainEqual(
|
|
expect.objectContaining({
|
|
code: "invalid-format-string"
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("FormatArgCountMismatch workflow validation", () => {
|
|
it("errors when placeholder exceeds arg count", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo \${{ format('{2}', 'arg0', 'arg1') }}
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
expect(result).toContainEqual(
|
|
expect.objectContaining({
|
|
code: "format-arg-count-mismatch",
|
|
severity: DiagnosticSeverity.Error
|
|
})
|
|
);
|
|
});
|
|
|
|
it("errors when referencing arg 0 with no args", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo \${{ format('{0}') }}
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
expect(result).toContainEqual(
|
|
expect.objectContaining({
|
|
code: "format-arg-count-mismatch"
|
|
})
|
|
);
|
|
});
|
|
|
|
it("allows when arg count matches", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo \${{ format('{0} {1} {2}', 'a', 'b', 'c') }}
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
expect(result).not.toContainEqual(
|
|
expect.objectContaining({
|
|
code: "format-arg-count-mismatch"
|
|
})
|
|
);
|
|
});
|
|
|
|
it("handles no placeholders correctly", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo \${{ format('hello world') }}
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
expect(result).not.toContainEqual(
|
|
expect.objectContaining({
|
|
code: "format-arg-count-mismatch"
|
|
})
|
|
);
|
|
});
|
|
|
|
it("skips validation for dynamic format strings", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo \${{ format(env.FORMAT_STRING, 'arg') }}
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
// Should not have format errors since we can't validate dynamic strings
|
|
expect(result).not.toContainEqual(
|
|
expect.objectContaining({
|
|
code: "invalid-format-string"
|
|
})
|
|
);
|
|
expect(result).not.toContainEqual(
|
|
expect.objectContaining({
|
|
code: "format-arg-count-mismatch"
|
|
})
|
|
);
|
|
});
|
|
|
|
it("validates nested format calls", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo \${{ format('{0}', format('{2}', 'a')) }}
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
// The inner format call has an error
|
|
expect(result).toContainEqual(
|
|
expect.objectContaining({
|
|
code: "format-arg-count-mismatch"
|
|
})
|
|
);
|
|
});
|
|
});
|
|
});
|