Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 667b1273e1 |
@@ -0,0 +1,58 @@
|
||||
import {registerLogger} from "./log";
|
||||
import {createDocument} from "./test-utils/document";
|
||||
import {TestLogger} from "./test-utils/logger";
|
||||
import {clearCache} from "./utils/workflow-cache";
|
||||
import {validate} from "./validate";
|
||||
|
||||
registerLogger(new TestLogger());
|
||||
|
||||
beforeEach(() => {
|
||||
clearCache();
|
||||
});
|
||||
|
||||
describe("block scalar chomping - allowed cases", () => {
|
||||
it("does NOT warn for step.run with clip chomping (exception)", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo \${{ github.event_name }}
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not warn for inline expression", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
if: \${{ github.event_name == 'push' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not warn for quoted string", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
if: "\${{ github.event_name == 'push' }}"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
import {DiagnosticSeverity} from "vscode-languageserver-types";
|
||||
import {registerLogger} from "./log";
|
||||
import {createDocument} from "./test-utils/document";
|
||||
import {TestLogger} from "./test-utils/logger";
|
||||
import {clearCache} from "./utils/workflow-cache";
|
||||
import {validate} from "./validate";
|
||||
|
||||
registerLogger(new TestLogger());
|
||||
|
||||
beforeEach(() => {
|
||||
clearCache();
|
||||
});
|
||||
|
||||
describe("block scalar chomping - boolean fields", () => {
|
||||
describe("job continue-on-error", () => {
|
||||
it("errors with clip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: |
|
||||
\${{ matrix.experimental }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks boolean evaluation. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("errors with keep chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: |+
|
||||
\${{ matrix.experimental }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks boolean evaluation. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("does not error with strip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: |-
|
||||
\${{ matrix.experimental }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("step continue-on-error", () => {
|
||||
it("errors with clip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
continue-on-error: |
|
||||
\${{ matrix.experimental }}
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks boolean evaluation. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("errors with keep chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
continue-on-error: |+
|
||||
\${{ matrix.experimental }}
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks boolean evaluation. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("does not error with strip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
continue-on-error: |-
|
||||
\${{ matrix.experimental }}
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,188 @@
|
||||
import {DiagnosticSeverity} from "vscode-languageserver-types";
|
||||
import {registerLogger} from "./log";
|
||||
import {createDocument} from "./test-utils/document";
|
||||
import {TestLogger} from "./test-utils/logger";
|
||||
import {clearCache} from "./utils/workflow-cache";
|
||||
import {validate} from "./validate";
|
||||
|
||||
registerLogger(new TestLogger());
|
||||
|
||||
beforeEach(() => {
|
||||
clearCache();
|
||||
});
|
||||
|
||||
describe("block scalar chomping - if fields", () => {
|
||||
describe("job-if", () => {
|
||||
it("errors with clip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
if: |
|
||||
\${{ github.event_name == 'push' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks boolean evaluation. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("errors with keep chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
if: |+
|
||||
\${{ github.event_name == 'push' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks boolean evaluation. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("does not error with strip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
if: |-
|
||||
\${{ github.event_name == 'push' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
|
||||
it("errors without ${{ }} (isExpression)", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
if: |
|
||||
github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks boolean evaluation. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("uses > indicator in error message for folded scalars", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
if: >
|
||||
\${{ github.event_name == 'push' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks boolean evaluation. Use '>-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("step-if", () => {
|
||||
it("errors with clip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- if: |
|
||||
\${{ github.event_name == 'push' }}
|
||||
run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks boolean evaluation. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("errors with keep chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- if: |+
|
||||
\${{ github.event_name == 'push' }}
|
||||
run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks boolean evaluation. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("does not error with strip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- if: |-
|
||||
\${{ github.event_name == 'push' }}
|
||||
run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,428 @@
|
||||
import {DiagnosticSeverity} from "vscode-languageserver-types";
|
||||
import {registerLogger} from "./log";
|
||||
import {createDocument} from "./test-utils/document";
|
||||
import {TestLogger} from "./test-utils/logger";
|
||||
import {clearCache} from "./utils/workflow-cache";
|
||||
import {validate} from "./validate";
|
||||
|
||||
registerLogger(new TestLogger());
|
||||
|
||||
beforeEach(() => {
|
||||
clearCache();
|
||||
});
|
||||
|
||||
describe("block scalar chomping - number fields", () => {
|
||||
describe("job timeout-minutes", () => {
|
||||
it("errors with clip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: |
|
||||
\${{ matrix.timeout }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("errors with keep chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: |+
|
||||
\${{ matrix.timeout }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("does not error with strip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: |-
|
||||
\${{ matrix.timeout }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("container.ports", () => {
|
||||
it("errors with clip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:16
|
||||
ports: |
|
||||
\${{ fromJSON('[80, 443]') }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("errors with keep chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:16
|
||||
ports: |+
|
||||
\${{ fromJSON('[8080, 9090]') }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("does not error with strip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:16
|
||||
ports: |-
|
||||
\${{ fromJSON('[80, 443]') }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("container.volumes", () => {
|
||||
it("errors with clip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:16
|
||||
volumes: |
|
||||
\${{ fromJSON('["/data:/data"]') }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("errors with keep chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:16
|
||||
volumes: |+
|
||||
\${{ fromJSON('["/data:/data"]') }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("does not error with strip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:16
|
||||
volumes: |-
|
||||
\${{ fromJSON('["/data:/data"]') }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("services.ports", () => {
|
||||
it("errors with clip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14
|
||||
ports: |
|
||||
\${{ fromJSON('[5432]') }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("errors with keep chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14
|
||||
ports: |+
|
||||
\${{ fromJSON('[5432]') }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("does not error with strip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14
|
||||
ports: |-
|
||||
\${{ fromJSON('[5432]') }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("services.volumes", () => {
|
||||
it("errors with clip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14
|
||||
volumes: |
|
||||
\${{ fromJSON('["/var/lib/postgresql/data"]') }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("errors with keep chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14
|
||||
volumes: |+
|
||||
\${{ fromJSON('["/var/lib/postgresql/data"]') }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("does not error with strip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14
|
||||
volumes: |-
|
||||
\${{ fromJSON('["/var/lib/postgresql/data"]') }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("step timeout-minutes", () => {
|
||||
it("errors with clip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
timeout-minutes: |
|
||||
\${{ matrix.timeout }}
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("errors with keep chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
timeout-minutes: |+
|
||||
\${{ matrix.timeout }}
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Error
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("does not error with strip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
timeout-minutes: |-
|
||||
\${{ matrix.timeout }}
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,227 @@
|
||||
import {DiagnosticSeverity} from "vscode-languageserver-types";
|
||||
import {registerLogger} from "./log";
|
||||
import {createDocument} from "./test-utils/document";
|
||||
import {TestLogger} from "./test-utils/logger";
|
||||
import {clearCache} from "./utils/workflow-cache";
|
||||
import {validate} from "./validate";
|
||||
|
||||
registerLogger(new TestLogger());
|
||||
|
||||
beforeEach(() => {
|
||||
clearCache();
|
||||
});
|
||||
|
||||
describe("expression validation", () => {
|
||||
describe("block scalar chomping - fields that warn only for clip", () => {
|
||||
describe("env", () => {
|
||||
it("warns with clip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo $VAR
|
||||
env:
|
||||
VAR: |
|
||||
\${{ matrix.value }}
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline to expression result. Use '|-' to strip or '|+' to keep trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Warning
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("does not warn with keep chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo $VAR
|
||||
env:
|
||||
VAR: |+
|
||||
\${{ matrix.value }}
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not warn with strip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo $VAR
|
||||
env:
|
||||
VAR: |-
|
||||
\${{ matrix.value }}
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
|
||||
it("uses > indicator in warning message for folded scalars", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo $VAR
|
||||
env:
|
||||
VAR: >
|
||||
\${{ matrix.value }}
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline to expression result. Use '>-' to strip or '>+' to keep trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Warning
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("action input", () => {
|
||||
it("warns with clip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: |
|
||||
\${{ github.ref }}
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline to expression result. Use '|-' to strip or '|+' to keep trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Warning
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("does not warn with keep chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: |+
|
||||
\${{ github.ref }}
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not warn with strip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: |-
|
||||
\${{ github.ref }}
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("matrix value", () => {
|
||||
it("warns with clip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
version: |
|
||||
\${{ fromJSON('[1, 2, 3]') }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Block scalar adds trailing newline to expression result. Use '|-' to strip or '|+' to keep trailing newlines.",
|
||||
code: "expression-block-scalar-chomping",
|
||||
severity: DiagnosticSeverity.Warning
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("does not warn with keep chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
version: |+
|
||||
\${{ fromJSON('[1, 2, 3]') }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not warn with strip chomping", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
version: |-
|
||||
\${{ fromJSON('[1, 2, 3]') }}
|
||||
steps:
|
||||
- run: echo hi
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,12 +3,14 @@ import {Expr} from "@actions/expressions/ast";
|
||||
import {ParseWorkflowResult, WorkflowTemplate, isBasicExpression, isString} from "@actions/workflow-parser";
|
||||
import {ErrorPolicy} from "@actions/workflow-parser/model/convert";
|
||||
import {splitAllowedContext} from "@actions/workflow-parser/templates/allowed-context";
|
||||
import {Definition} from "@actions/workflow-parser/templates/schema/definition";
|
||||
import {BasicExpressionToken} from "@actions/workflow-parser/templates/tokens/basic-expression-token";
|
||||
import {StringToken} from "@actions/workflow-parser/templates/tokens/string-token";
|
||||
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token";
|
||||
import {TokenRange} from "@actions/workflow-parser/templates/tokens/token-range";
|
||||
import {File} from "@actions/workflow-parser/workflows/file";
|
||||
import {FileProvider} from "@actions/workflow-parser/workflows/file-provider";
|
||||
import {Scalar} from "yaml";
|
||||
import {TextDocument} from "vscode-languageserver-textdocument";
|
||||
import {Diagnostic, DiagnosticSeverity, URI} from "vscode-languageserver-types";
|
||||
import {ActionMetadata, ActionReference} from "./action";
|
||||
@@ -106,6 +108,8 @@ async function additionalValidations(
|
||||
config?.contextProviderConfig,
|
||||
getProviderContext(documentUri, template, root, token.range)
|
||||
);
|
||||
|
||||
validateChomp(diagnostics, token, parent, key, validationDefinition);
|
||||
}
|
||||
|
||||
if (token.definition?.key === "regular-step" && token.range) {
|
||||
@@ -217,3 +221,98 @@ async function validateExpression(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateChomp(
|
||||
diagnostics: Diagnostic[],
|
||||
token: BasicExpressionToken,
|
||||
parent: TemplateToken | undefined,
|
||||
key: TemplateToken | undefined,
|
||||
validationDefinition: Definition | undefined
|
||||
): void {
|
||||
// Not "clip" or "keep" chomp style?
|
||||
if (token.chompStyle !== "clip" && token.chompStyle !== "keep") {
|
||||
return;
|
||||
}
|
||||
|
||||
// No definition? This can happen when the token is in an invalid position or the workflow has parse errors.
|
||||
if (!validationDefinition) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Step "run"?
|
||||
if (parent?.definition?.key === "run-step" && key?.isScalar && key.toString() === "run") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Block scalar indicator, i.e. | or >
|
||||
const scalarIndicator = token.scalarType === Scalar.BLOCK_LITERAL ? "|" : ">";
|
||||
|
||||
const defKey = validationDefinition.key;
|
||||
const parentDefKey = parent?.definition?.key;
|
||||
|
||||
// Error for boolean fields
|
||||
if (
|
||||
defKey === "job-if" ||
|
||||
defKey === "step-if" ||
|
||||
defKey === "step-continue-on-error" ||
|
||||
(parentDefKey === "job-factory" && key?.isScalar && key.toString() === "continue-on-error")
|
||||
) {
|
||||
diagnostics.push({
|
||||
message: `Block scalar adds trailing newline which breaks boolean evaluation. Use '${scalarIndicator}-' to strip trailing newlines.`,
|
||||
range: mapRange(token.range),
|
||||
severity: DiagnosticSeverity.Error,
|
||||
code: "expression-block-scalar-chomping",
|
||||
source: "github-actions"
|
||||
});
|
||||
}
|
||||
// Error for number fields
|
||||
else if (
|
||||
defKey === "step-timeout-minutes" ||
|
||||
(parentDefKey === "container-mapping" && key?.isScalar && ["ports", "volumes"].includes(key.toString())) ||
|
||||
(parentDefKey === "job-factory" && key?.isScalar && key.toString() === "timeout-minutes")
|
||||
) {
|
||||
diagnostics.push({
|
||||
message: `Block scalar adds trailing newline which breaks number parsing. Use '${scalarIndicator}-' to strip trailing newlines.`,
|
||||
range: mapRange(token.range),
|
||||
severity: DiagnosticSeverity.Error,
|
||||
code: "expression-block-scalar-chomping",
|
||||
source: "github-actions"
|
||||
});
|
||||
}
|
||||
// Error for specific string fields
|
||||
else if (
|
||||
defKey === "run-name" ||
|
||||
defKey === "step-name" ||
|
||||
defKey === "container" ||
|
||||
defKey === "services-container" ||
|
||||
defKey === "job-environment" ||
|
||||
defKey === "job-environment-name" ||
|
||||
defKey === "runs-on" ||
|
||||
defKey === "runs-on-labels" ||
|
||||
(parentDefKey === "container-mapping" && key?.isScalar && ["image", "credentials"].includes(key.toString())) ||
|
||||
(parentDefKey === "job-defaults-run" && key?.isScalar && ["shell", "working-directory"].includes(key.toString())) ||
|
||||
(parentDefKey === "job-environment-mapping" && key?.isScalar && key.toString() === "url") ||
|
||||
(parentDefKey === "job-factory" && key?.isScalar && key.toString() === "name") ||
|
||||
(parentDefKey === "workflow-job" && key?.isScalar && key.toString() === "name") ||
|
||||
(parentDefKey === "run-step" && key?.isScalar && key.toString() === "working-directory") ||
|
||||
(parentDefKey === "runs-on-mapping" && key?.isScalar && key.toString() === "group")
|
||||
) {
|
||||
diagnostics.push({
|
||||
message: `Block scalar adds trailing newline. Use '${scalarIndicator}-' to strip trailing newlines.`,
|
||||
range: mapRange(token.range),
|
||||
severity: DiagnosticSeverity.Error,
|
||||
code: "expression-block-scalar-chomping",
|
||||
source: "github-actions"
|
||||
});
|
||||
}
|
||||
// Warning for everything else, but only on clip (default)
|
||||
else if (token.chompStyle === "clip") {
|
||||
diagnostics.push({
|
||||
message: `Block scalar adds trailing newline to expression result. Use '${scalarIndicator}-' to strip or '${scalarIndicator}+' to keep trailing newlines.`,
|
||||
range: mapRange(token.range),
|
||||
severity: DiagnosticSeverity.Warning,
|
||||
code: "expression-block-scalar-chomping",
|
||||
source: "github-actions"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,4 +200,284 @@ jobs:
|
||||
throw new Error("expected if to be a basic expression");
|
||||
}
|
||||
});
|
||||
|
||||
describe("Block scalar chomp style preservation", () => {
|
||||
it("preserves clip chomping (|) for literal block scalar", () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
\${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- run: echo hi`
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||
|
||||
const workflowRoot = result.value!.assertMapping("root")!;
|
||||
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||
const build = jobs.get(0).value.assertMapping("job");
|
||||
const ifToken = build.get(1).value;
|
||||
|
||||
if (!isBasicExpression(ifToken)) {
|
||||
throw new Error("expected if to be a basic expression");
|
||||
}
|
||||
|
||||
expect(ifToken.scalarType).toBe("BLOCK_LITERAL");
|
||||
expect(ifToken.chompStyle).toBe("clip");
|
||||
});
|
||||
|
||||
it("preserves strip chomping (|-) for literal block scalar", () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: |-
|
||||
\${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- run: echo hi`
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||
|
||||
const workflowRoot = result.value!.assertMapping("root")!;
|
||||
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||
const build = jobs.get(0).value.assertMapping("job");
|
||||
const ifToken = build.get(1).value;
|
||||
|
||||
if (!isBasicExpression(ifToken)) {
|
||||
throw new Error("expected if to be a basic expression");
|
||||
}
|
||||
|
||||
expect(ifToken.scalarType).toBe("BLOCK_LITERAL");
|
||||
expect(ifToken.chompStyle).toBe("strip");
|
||||
});
|
||||
|
||||
it("preserves keep chomping (|+) for literal block scalar", () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: |+
|
||||
\${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- run: echo hi`
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||
|
||||
const workflowRoot = result.value!.assertMapping("root")!;
|
||||
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||
const build = jobs.get(0).value.assertMapping("job");
|
||||
const ifToken = build.get(1).value;
|
||||
|
||||
if (!isBasicExpression(ifToken)) {
|
||||
throw new Error("expected if to be a basic expression");
|
||||
}
|
||||
|
||||
expect(ifToken.scalarType).toBe("BLOCK_LITERAL");
|
||||
expect(ifToken.chompStyle).toBe("keep");
|
||||
});
|
||||
|
||||
it("preserves folded clip (>) chomping", () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
\${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- run: echo hi`
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||
|
||||
const workflowRoot = result.value!.assertMapping("root")!;
|
||||
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||
const build = jobs.get(0).value.assertMapping("job");
|
||||
const ifToken = build.get(1).value;
|
||||
|
||||
if (!isBasicExpression(ifToken)) {
|
||||
throw new Error("expected if to be a basic expression");
|
||||
}
|
||||
|
||||
expect(ifToken.scalarType).toBe("BLOCK_FOLDED");
|
||||
expect(ifToken.chompStyle).toBe("clip");
|
||||
});
|
||||
|
||||
it("preserves folded strip (>-) chomping", () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
\${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- run: echo hi`
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||
|
||||
const workflowRoot = result.value!.assertMapping("root")!;
|
||||
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||
const build = jobs.get(0).value.assertMapping("job");
|
||||
const ifToken = build.get(1).value;
|
||||
|
||||
if (!isBasicExpression(ifToken)) {
|
||||
throw new Error("expected if to be a basic expression");
|
||||
}
|
||||
|
||||
expect(ifToken.scalarType).toBe("BLOCK_FOLDED");
|
||||
expect(ifToken.chompStyle).toBe("strip");
|
||||
});
|
||||
|
||||
it("preserves with explicit indent (|2)", () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: |2
|
||||
\${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- run: echo hi`
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||
|
||||
const workflowRoot = result.value!.assertMapping("root")!;
|
||||
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||
const build = jobs.get(0).value.assertMapping("job");
|
||||
const ifToken = build.get(1).value;
|
||||
|
||||
if (!isBasicExpression(ifToken)) {
|
||||
throw new Error("expected if to be a basic expression");
|
||||
}
|
||||
|
||||
expect(ifToken.scalarType).toBe("BLOCK_LITERAL");
|
||||
expect(ifToken.chompStyle).toBe("clip");
|
||||
});
|
||||
|
||||
it("preserves with explicit indent and strip (|-2)", () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: |-2
|
||||
\${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- run: echo hi`
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||
|
||||
const workflowRoot = result.value!.assertMapping("root")!;
|
||||
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||
const build = jobs.get(0).value.assertMapping("job");
|
||||
const ifToken = build.get(1).value;
|
||||
|
||||
if (!isBasicExpression(ifToken)) {
|
||||
throw new Error("expected if to be a basic expression");
|
||||
}
|
||||
|
||||
expect(ifToken.scalarType).toBe("BLOCK_LITERAL");
|
||||
expect(ifToken.chompStyle).toBe("strip");
|
||||
});
|
||||
|
||||
it("handles flow scalars (no chomp info for inline)", () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: \${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- run: echo hi`
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||
|
||||
const workflowRoot = result.value!.assertMapping("root")!;
|
||||
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||
const build = jobs.get(0).value.assertMapping("job");
|
||||
const ifToken = build.get(1).value;
|
||||
|
||||
if (!isBasicExpression(ifToken)) {
|
||||
throw new Error("expected if to be a basic expression");
|
||||
}
|
||||
|
||||
expect(ifToken.scalarType).toBeUndefined();
|
||||
expect(ifToken.chompStyle).toBeUndefined();
|
||||
});
|
||||
|
||||
it("handles job-if without ${{ }} (isExpression case)", () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event_name == 'push'
|
||||
steps:
|
||||
- run: echo hi`
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||
|
||||
const workflowRoot = result.value!.assertMapping("root")!;
|
||||
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||
const build = jobs.get(0).value.assertMapping("job");
|
||||
const ifToken = build.get(1).value;
|
||||
|
||||
if (!isBasicExpression(ifToken)) {
|
||||
throw new Error("expected if to be a basic expression");
|
||||
}
|
||||
|
||||
expect(ifToken.scalarType).toBe("BLOCK_LITERAL");
|
||||
expect(ifToken.chompStyle).toBe("clip");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// template-reader *just* does schema validation
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import {Scalar} from "yaml";
|
||||
import {ObjectReader} from "./object-reader";
|
||||
import {TemplateSchema} from "./schema";
|
||||
import {DefinitionInfo} from "./schema/definition-info";
|
||||
@@ -459,7 +460,20 @@ class TemplateReader {
|
||||
// Doesn't contain "${{"
|
||||
// Check if value should still be evaluated as an expression
|
||||
if (definitionInfo.definition instanceof StringDefinition && definitionInfo.definition.isExpression) {
|
||||
const expression = this.parseIntoExpressionToken(token.range!, raw, allowedContext, token, definitionInfo);
|
||||
// For isExpression fields (e.g., 'if' conditions), parse token.value (not raw).
|
||||
// Example YAML:
|
||||
// if: |
|
||||
// github.event_name == 'push'
|
||||
// token.source/raw = "|\n github.event_name == 'push'\n" (includes block scalar header)
|
||||
// token.value = "github.event_name == 'push'\n" (clean expression content)
|
||||
// We need token.value because the '|' would interfere with expression parsing.
|
||||
const expression = this.parseIntoExpressionToken(
|
||||
token.range!,
|
||||
token.value,
|
||||
allowedContext,
|
||||
token,
|
||||
definitionInfo
|
||||
);
|
||||
if (expression) {
|
||||
return expression;
|
||||
}
|
||||
@@ -606,13 +620,17 @@ class TemplateReader {
|
||||
}
|
||||
}
|
||||
|
||||
const blockScalarInfo = parseBlockScalarInfo(token);
|
||||
return new BasicExpressionToken(
|
||||
this._fileId,
|
||||
token.range,
|
||||
`format('${format.join("")}'${args.join("")})`,
|
||||
definitionInfo,
|
||||
expressionTokens,
|
||||
raw
|
||||
raw,
|
||||
undefined,
|
||||
blockScalarInfo.scalarType,
|
||||
blockScalarInfo.chompStyle
|
||||
);
|
||||
}
|
||||
|
||||
@@ -686,6 +704,7 @@ class TemplateReader {
|
||||
};
|
||||
|
||||
// Return the expression
|
||||
const blockScalarInfo = parseBlockScalarInfo(token);
|
||||
return <ParseExpressionResult>{
|
||||
expression: new BasicExpressionToken(
|
||||
this._fileId,
|
||||
@@ -694,7 +713,9 @@ class TemplateReader {
|
||||
definitionInfo,
|
||||
undefined,
|
||||
token.source,
|
||||
expressionRange
|
||||
expressionRange,
|
||||
blockScalarInfo.scalarType,
|
||||
blockScalarInfo.chompStyle
|
||||
),
|
||||
error: undefined
|
||||
};
|
||||
@@ -801,3 +822,53 @@ interface MatchDirectiveResult {
|
||||
parameters: string[];
|
||||
error: Error | undefined;
|
||||
}
|
||||
|
||||
interface BlockScalarInfo {
|
||||
scalarType: Scalar.Type | undefined;
|
||||
chompStyle: "clip" | "strip" | "keep" | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the block scalar info from the StringToken
|
||||
* @param token The StringToken that may contain block scalar information
|
||||
* @returns The scalar type and chomp style
|
||||
*/
|
||||
function parseBlockScalarInfo(token: StringToken): BlockScalarInfo {
|
||||
const scalarType = token.scalarType;
|
||||
|
||||
// Only block scalars have chomp styles
|
||||
if (scalarType !== Scalar.BLOCK_LITERAL && scalarType !== Scalar.BLOCK_FOLDED) {
|
||||
return {scalarType: undefined, chompStyle: undefined};
|
||||
}
|
||||
|
||||
// Parse chomp style from the block scalar header
|
||||
// Look for block scalar indicators at the start: | or >
|
||||
// Followed by optional chomp indicator (-, +) and/or explicit indent (digit)
|
||||
// Examples: |, |-, |+, |2, |-2, |+2, >, >-, >+, >2, >-2, >+2
|
||||
const header = token.blockScalarHeader;
|
||||
if (!header) {
|
||||
// If there's no header, assume clip (default)
|
||||
return {scalarType, chompStyle: "clip"};
|
||||
}
|
||||
|
||||
const blockScalarMatch = header.match(/^(\||>)([-+])?(\d)?/);
|
||||
|
||||
if (!blockScalarMatch) {
|
||||
// Assume clip if we can't parse the indicator
|
||||
return {scalarType, chompStyle: "clip"};
|
||||
}
|
||||
|
||||
const chompIndicator = blockScalarMatch[2];
|
||||
|
||||
let chompStyle: "clip" | "strip" | "keep";
|
||||
if (chompIndicator === "-") {
|
||||
chompStyle = "strip";
|
||||
} else if (chompIndicator === "+") {
|
||||
chompStyle = "keep";
|
||||
} else {
|
||||
// No chomp indicator means clip (default)
|
||||
chompStyle = "clip";
|
||||
}
|
||||
|
||||
return {scalarType, chompStyle};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {Scalar} from "yaml";
|
||||
import {DefinitionInfo} from "../schema/definition-info";
|
||||
|
||||
import {CLOSE_EXPRESSION, OPEN_EXPRESSION} from "../template-constants";
|
||||
@@ -23,6 +24,16 @@ export class BasicExpressionToken extends ExpressionToken {
|
||||
*/
|
||||
public readonly expressionRange: TokenRange | undefined;
|
||||
|
||||
/**
|
||||
* The YAML scalar type (e.g., BLOCK_LITERAL, BLOCK_FOLDED, PLAIN, etc.) if the expression was parsed from a block scalar.
|
||||
*/
|
||||
public readonly scalarType: Scalar.Type | undefined;
|
||||
|
||||
/**
|
||||
* The chomp style of the block scalar: 'clip' (default, keeps one newline), 'strip' (removes all), or 'keep' (keeps all).
|
||||
*/
|
||||
public readonly chompStyle: "clip" | "strip" | "keep" | undefined;
|
||||
|
||||
/**
|
||||
* @param originalExpressions If the basic expression was transformed from individual expressions, these will be the original ones
|
||||
*/
|
||||
@@ -33,13 +44,17 @@ export class BasicExpressionToken extends ExpressionToken {
|
||||
definitionInfo: DefinitionInfo | undefined,
|
||||
originalExpressions: BasicExpressionToken[] | undefined,
|
||||
source: string | undefined,
|
||||
expressionRange?: TokenRange | undefined
|
||||
expressionRange?: TokenRange | undefined,
|
||||
scalarType?: Scalar.Type | undefined,
|
||||
chompStyle?: "clip" | "strip" | "keep" | undefined
|
||||
) {
|
||||
super(TokenType.BasicExpression, file, range, undefined, definitionInfo);
|
||||
this.expr = expression;
|
||||
this.source = source;
|
||||
this.originalExpressions = originalExpressions;
|
||||
this.expressionRange = expressionRange;
|
||||
this.scalarType = scalarType;
|
||||
this.chompStyle = chompStyle;
|
||||
}
|
||||
|
||||
public get expression(): string {
|
||||
@@ -55,7 +70,9 @@ export class BasicExpressionToken extends ExpressionToken {
|
||||
this.definitionInfo,
|
||||
this.originalExpressions,
|
||||
this.source,
|
||||
this.expressionRange
|
||||
this.expressionRange,
|
||||
this.scalarType,
|
||||
this.chompStyle
|
||||
)
|
||||
: new BasicExpressionToken(
|
||||
this.file,
|
||||
@@ -64,7 +81,9 @@ export class BasicExpressionToken extends ExpressionToken {
|
||||
this.definitionInfo,
|
||||
this.originalExpressions,
|
||||
this.source,
|
||||
this.expressionRange
|
||||
this.expressionRange,
|
||||
this.scalarType,
|
||||
this.chompStyle
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {Scalar} from "yaml";
|
||||
import {LiteralToken, TemplateToken} from ".";
|
||||
import {DefinitionInfo} from "../schema/definition-info";
|
||||
import {TokenRange} from "./token-range";
|
||||
@@ -6,23 +7,45 @@ import {TokenType} from "./types";
|
||||
export class StringToken extends LiteralToken {
|
||||
public readonly value: string;
|
||||
public readonly source: string | undefined;
|
||||
public readonly scalarType: Scalar.Type | undefined;
|
||||
public readonly blockScalarHeader: string | undefined;
|
||||
|
||||
public constructor(
|
||||
file: number | undefined,
|
||||
range: TokenRange | undefined,
|
||||
value: string,
|
||||
definitionInfo: DefinitionInfo | undefined,
|
||||
source?: string
|
||||
source?: string,
|
||||
scalarType?: Scalar.Type,
|
||||
blockScalarHeader?: string
|
||||
) {
|
||||
super(TokenType.String, file, range, definitionInfo);
|
||||
this.value = value;
|
||||
this.source = source;
|
||||
this.scalarType = scalarType;
|
||||
this.blockScalarHeader = blockScalarHeader;
|
||||
}
|
||||
|
||||
public override clone(omitSource?: boolean): TemplateToken {
|
||||
return omitSource
|
||||
? new StringToken(undefined, undefined, this.value, this.definitionInfo, this.source)
|
||||
: new StringToken(this.file, this.range, this.value, this.definitionInfo, this.source);
|
||||
? new StringToken(
|
||||
undefined,
|
||||
undefined,
|
||||
this.value,
|
||||
this.definitionInfo,
|
||||
this.source,
|
||||
this.scalarType,
|
||||
this.blockScalarHeader
|
||||
)
|
||||
: new StringToken(
|
||||
this.file,
|
||||
this.range,
|
||||
this.value,
|
||||
this.definitionInfo,
|
||||
this.source,
|
||||
this.scalarType,
|
||||
this.blockScalarHeader
|
||||
);
|
||||
}
|
||||
|
||||
public override toString(): string {
|
||||
|
||||
@@ -117,11 +117,28 @@ export class YamlObjectReader implements ObjectReader {
|
||||
return new BooleanToken(fileId, range, value, undefined);
|
||||
case "string": {
|
||||
let source: string | undefined;
|
||||
const scalarType = token.type;
|
||||
let blockScalarHeader: string | undefined;
|
||||
|
||||
if (token.srcToken && "source" in token.srcToken) {
|
||||
source = token.srcToken.source;
|
||||
// Extract block scalar header. For example |-, |+, >-
|
||||
//
|
||||
// This relies on undocumented internal behavior (srcToken.props).
|
||||
// Feature request for official support: https://github.com/eemeli/yaml/issues/643
|
||||
if (token.srcToken.type === "block-scalar" && "props" in token.srcToken) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const props = token.srcToken.props as any[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
const headerProp = props.find((p: any) => p.type === "block-scalar-header");
|
||||
if (headerProp && "source" in headerProp) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
blockScalarHeader = headerProp.source;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new StringToken(fileId, range, value, undefined, source);
|
||||
return new StringToken(fileId, range, value, undefined, source, scalarType, blockScalarHeader);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unexpected value type '${typeof value}' when reading object`);
|
||||
|
||||
Reference in New Issue
Block a user