* Fix false positive for literal text in `if` conditions
Use token.value (parsed string without YAML quotes) instead of token.source
(raw YAML text) for expression parsing in single-line strings. This fixes a
false positive where `if: "${{ expr }}"` incorrectly triggered the
"literal text in condition" error because the outer quotes were treated as
literal text.
Follow-up to PR #216
Related issue: https://github.com/github/vscode-github-actions/issues/542
* Move issue reference to comment
315 lines
8.7 KiB
TypeScript
315 lines
8.7 KiB
TypeScript
import {DiagnosticSeverity} from "vscode-languageserver-types";
|
|
import {registerLogger} from "./log.js";
|
|
import {createDocument} from "./test-utils/document.js";
|
|
import {TestLogger} from "./test-utils/logger.js";
|
|
import {clearCache} from "./utils/workflow-cache.js";
|
|
import {validate} from "./validate.js";
|
|
|
|
registerLogger(new TestLogger());
|
|
|
|
beforeEach(() => {
|
|
clearCache();
|
|
});
|
|
|
|
describe("expression literal text in conditions", () => {
|
|
describe("job-if", () => {
|
|
it("errors when literal text mixed with embedded expression", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
if: push == \${{ github.event_name }}
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo hi
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
|
|
expect(result).toContainEqual(
|
|
expect.objectContaining({
|
|
message:
|
|
"Conditional expression contains literal text outside replacement tokens. This will cause the expression to always evaluate to truthy. Did you mean to put the entire expression inside ${{ }}?",
|
|
code: "expression-literal-text-in-condition",
|
|
severity: DiagnosticSeverity.Error
|
|
})
|
|
);
|
|
});
|
|
|
|
it("allows format with only replacement tokens", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
if: \${{ format('{0}', github.event_name) }}
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo hi
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
|
|
expect(result).not.toContainEqual(
|
|
expect.objectContaining({
|
|
code: "expression-literal-text-in-condition"
|
|
})
|
|
);
|
|
});
|
|
|
|
it("allows format with only replacement tokens and whitespace", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
if: \${{ format('{0}{1}', github.event_name, 'test') }}
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo hi
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
|
|
// Only replacement tokens, no literal text
|
|
expect(result).not.toContainEqual(
|
|
expect.objectContaining({
|
|
code: "expression-literal-text-in-condition"
|
|
})
|
|
);
|
|
});
|
|
|
|
it("errors with literal text and replacement tokens mixed", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
if: \${{ format('event is {0}', github.event_name) }}
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo hi
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
|
|
expect(result).toContainEqual(
|
|
expect.objectContaining({
|
|
message:
|
|
"Conditional expression contains literal text outside replacement tokens. This will cause the expression to always evaluate to truthy. Did you mean to put the entire expression inside ${{ }}?",
|
|
code: "expression-literal-text-in-condition",
|
|
severity: DiagnosticSeverity.Error
|
|
})
|
|
);
|
|
});
|
|
|
|
it("errors with escaped left brace followed by replacement token", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
if: \${{ format('{{{0}', github.event_name) }}
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo hi
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
|
|
expect(result).toContainEqual(
|
|
expect.objectContaining({
|
|
message:
|
|
"Conditional expression contains literal text outside replacement tokens. This will cause the expression to always evaluate to truthy. Did you mean to put the entire expression inside ${{ }}?",
|
|
code: "expression-literal-text-in-condition",
|
|
severity: DiagnosticSeverity.Error
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("step-if", () => {
|
|
it("errors when literal text mixed with embedded expression", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- if: success == \${{ job.status }}
|
|
run: echo hi
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
|
|
expect(result).toContainEqual(
|
|
expect.objectContaining({
|
|
message:
|
|
"Conditional expression contains literal text outside replacement tokens. This will cause the expression to always evaluate to truthy. Did you mean to put the entire expression inside ${{ }}?",
|
|
code: "expression-literal-text-in-condition",
|
|
severity: DiagnosticSeverity.Error
|
|
})
|
|
);
|
|
});
|
|
|
|
it("allows valid expressions", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- if: \${{ success() }}
|
|
run: echo hi
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
|
|
expect(result).not.toContainEqual(
|
|
expect.objectContaining({
|
|
code: "expression-literal-text-in-condition"
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("snapshot-if", () => {
|
|
it("errors when literal text mixed with embedded expression", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
matrix:
|
|
os: [ubuntu-latest]
|
|
steps:
|
|
- run: echo hi
|
|
snapshot:
|
|
image-name: my-image
|
|
if: ubuntu == \${{ matrix.os }}
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
|
|
expect(result).toContainEqual(
|
|
expect.objectContaining({
|
|
message:
|
|
"Conditional expression contains literal text outside replacement tokens. This will cause the expression to always evaluate to truthy. Did you mean to put the entire expression inside ${{ }}?",
|
|
code: "expression-literal-text-in-condition",
|
|
severity: DiagnosticSeverity.Error
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("non-if fields", () => {
|
|
it("does not error for format in run", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo \${{ format('Event is {0}', github.event_name) }}
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
|
|
// Format with literal text is OK outside of if conditions
|
|
expect(result).not.toContainEqual(
|
|
expect.objectContaining({
|
|
code: "expression-literal-text-in-condition"
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
// https://github.com/github/vscode-github-actions/issues/542
|
|
describe("YAML-quoted expressions", () => {
|
|
it("allows double-quoted expression in job-if", async () => {
|
|
// Quotes are needed when the expression contains a colon
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
publish:
|
|
if: "\${{ startsWith(github.event.head_commit.message, 'chore: release') }}"
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo hi
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
|
|
expect(result).not.toContainEqual(
|
|
expect.objectContaining({
|
|
code: "expression-literal-text-in-condition"
|
|
})
|
|
);
|
|
});
|
|
|
|
it("allows single-quoted expression in job-if", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
publish:
|
|
if: '\${{ startsWith(github.event.head_commit.message, "chore: release") }}'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo hi
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
|
|
expect(result).not.toContainEqual(
|
|
expect.objectContaining({
|
|
code: "expression-literal-text-in-condition"
|
|
})
|
|
);
|
|
});
|
|
|
|
it("allows double-quoted expression in step-if", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- if: "\${{ contains(github.event.head_commit.message, 'skip: ci') }}"
|
|
run: echo hi
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
|
|
expect(result).not.toContainEqual(
|
|
expect.objectContaining({
|
|
code: "expression-literal-text-in-condition"
|
|
})
|
|
);
|
|
});
|
|
|
|
it("still errors when there is actual literal text outside expression", async () => {
|
|
// Even with quotes, if there's literal text outside ${{ }}, it should error
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
if: "push == \${{ github.event_name }}"
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo hi
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
|
|
expect(result).toContainEqual(
|
|
expect.objectContaining({
|
|
code: "expression-literal-text-in-condition"
|
|
})
|
|
);
|
|
});
|
|
|
|
it("errors on multiple expressions with literal text between them", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
if: "\${{ true }} and \${{ false }}"
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo hi
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input));
|
|
|
|
expect(result).toContainEqual(
|
|
expect.objectContaining({
|
|
code: "expression-literal-text-in-condition"
|
|
})
|
|
);
|
|
});
|
|
});
|
|
});
|