Files
languageservices/workflow-parser/src/templates/tokens/traversal-state.ts
T
eric scipleandGitHub 2816233a40 Add block scalar newline warning (#295)
In YAML, block scalars (`|` and `>`) silently add a trailing newline by default
("clip" chomping). This can cause subtle bugs when the newline is unintentional.

This PR adds a warning when clip chomping is used in fields where trailing
newlines commonly cause issues:

- Environment variables (workflow, job, step, container, service levels)
- Action inputs (`with:`)
- Reusable workflow inputs and secrets
- Job outputs
- Matrix values (including `include` and `exclude`)
- Concurrency groups

The warning suggests using `|-` (strip) or `|+` (keep) to be explicit.

Intentionally does NOT warn for:
- `run:` scripts (trailing newlines are normal)
- Fields trimmed server-side (`if:`, `name:`, `runs-on:`, etc.)

The feature is gated behind the `blockScalarChompingWarning` feature flag.
2026-01-12 09:36:43 -06:00

85 lines
2.4 KiB
TypeScript

import {TemplateToken} from "./index.js";
import {MappingToken} from "./mapping-token.js";
import {SequenceToken} from "./sequence-token.js";
import {TokenType} from "./types.js";
export class TraversalState {
private readonly _token: TemplateToken;
private index = -1;
private isKey = false;
public readonly parent: TraversalState | undefined;
public current: TemplateToken | undefined;
public currentKey: TemplateToken | undefined;
public constructor(parent: TraversalState | undefined, token: TemplateToken) {
this.parent = parent;
this._token = token;
this.current = token;
}
public moveNext(omitKeys: boolean): boolean {
switch (this._token.templateTokenType) {
case TokenType.Sequence: {
const sequence = this._token as SequenceToken;
if (++this.index < sequence.count) {
this.current = sequence.get(this.index);
return true;
}
this.current = undefined;
return false;
}
case TokenType.Mapping: {
const mapping = this._token as MappingToken;
// Already returned the key, now return the value
if (this.isKey) {
this.isKey = false;
this.currentKey = this.current;
this.current = mapping.get(this.index).value;
return true;
}
// Move next
if (++this.index < mapping.count) {
// Skip the key, return the value
if (omitKeys) {
this.isKey = false;
this.currentKey = mapping.get(this.index).key;
this.current = mapping.get(this.index).value;
return true;
}
// Return the key
this.isKey = true;
this.currentKey = undefined;
this.current = mapping.get(this.index).key;
return true;
}
this.currentKey = undefined;
this.current = undefined;
return false;
}
default:
throw new Error(`Unexpected token type '${this._token.templateTokenType}' when traversing state`);
}
}
/**
* Returns the ancestor tokens from root to the current token's parent container.
*/
public getAncestors(): TemplateToken[] {
const ancestors: TemplateToken[] = [];
let state: TraversalState | undefined = this.parent;
while (state) {
if (state.current) {
ancestors.unshift(state.current);
}
state = state.parent;
}
return ancestors;
}
}