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 {ParseWorkflowResult, WorkflowTemplate, isBasicExpression, isString} from "@actions/workflow-parser";
|
||||||
import {ErrorPolicy} from "@actions/workflow-parser/model/convert";
|
import {ErrorPolicy} from "@actions/workflow-parser/model/convert";
|
||||||
import {splitAllowedContext} from "@actions/workflow-parser/templates/allowed-context";
|
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 {BasicExpressionToken} from "@actions/workflow-parser/templates/tokens/basic-expression-token";
|
||||||
import {StringToken} from "@actions/workflow-parser/templates/tokens/string-token";
|
import {StringToken} from "@actions/workflow-parser/templates/tokens/string-token";
|
||||||
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token";
|
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token";
|
||||||
import {TokenRange} from "@actions/workflow-parser/templates/tokens/token-range";
|
import {TokenRange} from "@actions/workflow-parser/templates/tokens/token-range";
|
||||||
import {File} from "@actions/workflow-parser/workflows/file";
|
import {File} from "@actions/workflow-parser/workflows/file";
|
||||||
import {FileProvider} from "@actions/workflow-parser/workflows/file-provider";
|
import {FileProvider} from "@actions/workflow-parser/workflows/file-provider";
|
||||||
|
import {Scalar} from "yaml";
|
||||||
import {TextDocument} from "vscode-languageserver-textdocument";
|
import {TextDocument} from "vscode-languageserver-textdocument";
|
||||||
import {Diagnostic, DiagnosticSeverity, URI} from "vscode-languageserver-types";
|
import {Diagnostic, DiagnosticSeverity, URI} from "vscode-languageserver-types";
|
||||||
import {ActionMetadata, ActionReference} from "./action";
|
import {ActionMetadata, ActionReference} from "./action";
|
||||||
@@ -106,6 +108,8 @@ async function additionalValidations(
|
|||||||
config?.contextProviderConfig,
|
config?.contextProviderConfig,
|
||||||
getProviderContext(documentUri, template, root, token.range)
|
getProviderContext(documentUri, template, root, token.range)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
validateChomp(diagnostics, token, parent, key, validationDefinition);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token.definition?.key === "regular-step" && token.range) {
|
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");
|
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
|
// template-reader *just* does schema validation
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
|
||||||
|
import {Scalar} from "yaml";
|
||||||
import {ObjectReader} from "./object-reader";
|
import {ObjectReader} from "./object-reader";
|
||||||
import {TemplateSchema} from "./schema";
|
import {TemplateSchema} from "./schema";
|
||||||
import {DefinitionInfo} from "./schema/definition-info";
|
import {DefinitionInfo} from "./schema/definition-info";
|
||||||
@@ -459,7 +460,20 @@ class TemplateReader {
|
|||||||
// Doesn't contain "${{"
|
// Doesn't contain "${{"
|
||||||
// Check if value should still be evaluated as an expression
|
// Check if value should still be evaluated as an expression
|
||||||
if (definitionInfo.definition instanceof StringDefinition && definitionInfo.definition.isExpression) {
|
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) {
|
if (expression) {
|
||||||
return expression;
|
return expression;
|
||||||
}
|
}
|
||||||
@@ -606,13 +620,17 @@ class TemplateReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const blockScalarInfo = parseBlockScalarInfo(token);
|
||||||
return new BasicExpressionToken(
|
return new BasicExpressionToken(
|
||||||
this._fileId,
|
this._fileId,
|
||||||
token.range,
|
token.range,
|
||||||
`format('${format.join("")}'${args.join("")})`,
|
`format('${format.join("")}'${args.join("")})`,
|
||||||
definitionInfo,
|
definitionInfo,
|
||||||
expressionTokens,
|
expressionTokens,
|
||||||
raw
|
raw,
|
||||||
|
undefined,
|
||||||
|
blockScalarInfo.scalarType,
|
||||||
|
blockScalarInfo.chompStyle
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,6 +704,7 @@ class TemplateReader {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Return the expression
|
// Return the expression
|
||||||
|
const blockScalarInfo = parseBlockScalarInfo(token);
|
||||||
return <ParseExpressionResult>{
|
return <ParseExpressionResult>{
|
||||||
expression: new BasicExpressionToken(
|
expression: new BasicExpressionToken(
|
||||||
this._fileId,
|
this._fileId,
|
||||||
@@ -694,7 +713,9 @@ class TemplateReader {
|
|||||||
definitionInfo,
|
definitionInfo,
|
||||||
undefined,
|
undefined,
|
||||||
token.source,
|
token.source,
|
||||||
expressionRange
|
expressionRange,
|
||||||
|
blockScalarInfo.scalarType,
|
||||||
|
blockScalarInfo.chompStyle
|
||||||
),
|
),
|
||||||
error: undefined
|
error: undefined
|
||||||
};
|
};
|
||||||
@@ -801,3 +822,53 @@ interface MatchDirectiveResult {
|
|||||||
parameters: string[];
|
parameters: string[];
|
||||||
error: Error | undefined;
|
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 {DefinitionInfo} from "../schema/definition-info";
|
||||||
|
|
||||||
import {CLOSE_EXPRESSION, OPEN_EXPRESSION} from "../template-constants";
|
import {CLOSE_EXPRESSION, OPEN_EXPRESSION} from "../template-constants";
|
||||||
@@ -23,6 +24,16 @@ export class BasicExpressionToken extends ExpressionToken {
|
|||||||
*/
|
*/
|
||||||
public readonly expressionRange: TokenRange | undefined;
|
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
|
* @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,
|
definitionInfo: DefinitionInfo | undefined,
|
||||||
originalExpressions: BasicExpressionToken[] | undefined,
|
originalExpressions: BasicExpressionToken[] | undefined,
|
||||||
source: string | 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);
|
super(TokenType.BasicExpression, file, range, undefined, definitionInfo);
|
||||||
this.expr = expression;
|
this.expr = expression;
|
||||||
this.source = source;
|
this.source = source;
|
||||||
this.originalExpressions = originalExpressions;
|
this.originalExpressions = originalExpressions;
|
||||||
this.expressionRange = expressionRange;
|
this.expressionRange = expressionRange;
|
||||||
|
this.scalarType = scalarType;
|
||||||
|
this.chompStyle = chompStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get expression(): string {
|
public get expression(): string {
|
||||||
@@ -55,7 +70,9 @@ export class BasicExpressionToken extends ExpressionToken {
|
|||||||
this.definitionInfo,
|
this.definitionInfo,
|
||||||
this.originalExpressions,
|
this.originalExpressions,
|
||||||
this.source,
|
this.source,
|
||||||
this.expressionRange
|
this.expressionRange,
|
||||||
|
this.scalarType,
|
||||||
|
this.chompStyle
|
||||||
)
|
)
|
||||||
: new BasicExpressionToken(
|
: new BasicExpressionToken(
|
||||||
this.file,
|
this.file,
|
||||||
@@ -64,7 +81,9 @@ export class BasicExpressionToken extends ExpressionToken {
|
|||||||
this.definitionInfo,
|
this.definitionInfo,
|
||||||
this.originalExpressions,
|
this.originalExpressions,
|
||||||
this.source,
|
this.source,
|
||||||
this.expressionRange
|
this.expressionRange,
|
||||||
|
this.scalarType,
|
||||||
|
this.chompStyle
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {Scalar} from "yaml";
|
||||||
import {LiteralToken, TemplateToken} from ".";
|
import {LiteralToken, TemplateToken} from ".";
|
||||||
import {DefinitionInfo} from "../schema/definition-info";
|
import {DefinitionInfo} from "../schema/definition-info";
|
||||||
import {TokenRange} from "./token-range";
|
import {TokenRange} from "./token-range";
|
||||||
@@ -6,23 +7,45 @@ import {TokenType} from "./types";
|
|||||||
export class StringToken extends LiteralToken {
|
export class StringToken extends LiteralToken {
|
||||||
public readonly value: string;
|
public readonly value: string;
|
||||||
public readonly source: string | undefined;
|
public readonly source: string | undefined;
|
||||||
|
public readonly scalarType: Scalar.Type | undefined;
|
||||||
|
public readonly blockScalarHeader: string | undefined;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
file: number | undefined,
|
file: number | undefined,
|
||||||
range: TokenRange | undefined,
|
range: TokenRange | undefined,
|
||||||
value: string,
|
value: string,
|
||||||
definitionInfo: DefinitionInfo | undefined,
|
definitionInfo: DefinitionInfo | undefined,
|
||||||
source?: string
|
source?: string,
|
||||||
|
scalarType?: Scalar.Type,
|
||||||
|
blockScalarHeader?: string
|
||||||
) {
|
) {
|
||||||
super(TokenType.String, file, range, definitionInfo);
|
super(TokenType.String, file, range, definitionInfo);
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.source = source;
|
this.source = source;
|
||||||
|
this.scalarType = scalarType;
|
||||||
|
this.blockScalarHeader = blockScalarHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override clone(omitSource?: boolean): TemplateToken {
|
public override clone(omitSource?: boolean): TemplateToken {
|
||||||
return omitSource
|
return omitSource
|
||||||
? new StringToken(undefined, undefined, this.value, this.definitionInfo, this.source)
|
? new StringToken(
|
||||||
: new StringToken(this.file, this.range, this.value, this.definitionInfo, this.source);
|
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 {
|
public override toString(): string {
|
||||||
|
|||||||
@@ -117,11 +117,28 @@ export class YamlObjectReader implements ObjectReader {
|
|||||||
return new BooleanToken(fileId, range, value, undefined);
|
return new BooleanToken(fileId, range, value, undefined);
|
||||||
case "string": {
|
case "string": {
|
||||||
let source: string | undefined;
|
let source: string | undefined;
|
||||||
|
const scalarType = token.type;
|
||||||
|
let blockScalarHeader: string | undefined;
|
||||||
|
|
||||||
if (token.srcToken && "source" in token.srcToken) {
|
if (token.srcToken && "source" in token.srcToken) {
|
||||||
source = token.srcToken.source;
|
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:
|
default:
|
||||||
throw new Error(`Unexpected value type '${typeof value}' when reading object`);
|
throw new Error(`Unexpected value type '${typeof value}' when reading object`);
|
||||||
|
|||||||
Reference in New Issue
Block a user