Remove isExpression flag and implement convertToIfCondition to align with Go parser architecture (#217)

This commit is contained in:
eric sciple
2025-11-24 09:12:26 -06:00
committed by GitHub
parent 7f8bba4305
commit bad1fb96af
12 changed files with 232 additions and 38 deletions
@@ -1,6 +1,6 @@
import {Pos} from "@actions/expressions/lexer";
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token";
import {isBasicExpression} from "@actions/workflow-parser/templates/tokens/type-guards";
import {isBasicExpression, isString} from "@actions/workflow-parser/templates/tokens/type-guards";
import {Position, Range as LSPRange} from "vscode-languageserver-textdocument";
import {mapRange} from "../utils/range";
import {posWithinRange} from "./pos-range";
@@ -16,12 +16,52 @@ export type ExpressionPos = {
documentRange: LSPRange;
};
/**
* Maps a document position to an expression position for hover/completion features.
*
* This handles both explicit expressions (with ${{ }}) and implicit expressions (like if conditions).
* For if conditions without ${{ }}, this applies the same conversion as the parser's convertToIfCondition,
* wrapping them in `success() && (...)` when no status function is present.
*
* @param token The template token at the position
* @param position The position in the document
* @returns Expression and adjusted position, or undefined if not an expression
*/
export function mapToExpressionPos(token: TemplateToken, position: Position): ExpressionPos | undefined {
const pos: Pos = {
line: position.line + 1,
column: position.character + 1
};
// Handle if conditions that are string tokens (job-if, step-if, snapshot-if)
const definitionKey = token.definition?.key;
if (
isString(token) &&
token.range &&
(definitionKey === "job-if" || definitionKey === "step-if" || definitionKey === "snapshot-if")
) {
const condition = token.value.trim();
if (condition) {
// Check if the condition already contains a status function
const hasStatusFunction = /\b(success|failure|cancelled|always)\s*\(/.test(condition);
const finalCondition = hasStatusFunction ? condition : `success() && (${condition})`;
const exprRange = mapRange(token.range);
// Calculate offset for wrapping
const offset = hasStatusFunction ? 0 : "success() && (".length;
return {
expression: finalCondition,
position: {
line: pos.line - exprRange.start.line - 1,
column: pos.column - exprRange.start.character - 1 + offset
},
documentRange: exprRange
};
}
}
if (!isBasicExpression(token)) {
return undefined;
}
@@ -155,8 +155,8 @@ jobs:
contents:
"Causes the step to always execute, and returns `true`, even when canceled. The `always` expression is best used at the step level or on tasks that you expect to run even when a job is canceled. For example, you can use `always` to send logs even when a job is canceled.",
range: {
start: {line: 3, character: 11},
end: {line: 3, character: 17}
start: {line: 3, character: 8},
end: {line: 3, character: 14}
}
});
});
@@ -1,12 +1,11 @@
import {isString} from "@actions/workflow-parser";
import {DefinitionType} from "@actions/workflow-parser/templates/schema/definition-type";
import {StringDefinition} from "@actions/workflow-parser/templates/schema/string-definition";
import {OPEN_EXPRESSION} from "@actions/workflow-parser/templates/template-constants";
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/index";
export function isPotentiallyExpression(token: TemplateToken): boolean {
const isAlwaysExpression =
token.definition?.definitionType === DefinitionType.String && (token.definition as StringDefinition).isExpression;
const containsExpression = isString(token) && token.value != null && token.value.indexOf(OPEN_EXPRESSION) >= 0;
return isAlwaysExpression || containsExpression;
// If conditions are always expressions (job-if, step-if, snapshot-if)
const definitionKey = token.definition?.key;
const isIfCondition = definitionKey === "job-if" || definitionKey === "step-if" || definitionKey === "snapshot-if";
return containsExpression || isIfCondition;
}
+34
View File
@@ -108,6 +108,40 @@ async function additionalValidations(
);
}
// If this is a job-if, step-if, or snapshot-if field (which are strings that should be treated as expressions), validate it
const definitionKey = token.definition?.key;
if (
isString(token) &&
token.range &&
(definitionKey === "job-if" || definitionKey === "step-if" || definitionKey === "snapshot-if")
) {
// Convert the string to an expression token for validation
const condition = token.value.trim();
if (condition) {
// Check if the condition already contains a status function
const hasStatusFunction = /\b(success|failure|cancelled|always)\s*\(/.test(condition);
const finalCondition = hasStatusFunction ? condition : `success() && (${condition})`;
// Create a BasicExpressionToken for validation
const expressionToken = new BasicExpressionToken(
token.file,
token.range,
finalCondition,
token.definitionInfo,
undefined,
token.source
);
await validateExpression(
diagnostics,
expressionToken,
validationToken.definitionInfo?.allowedContext || [],
config?.contextProviderConfig,
getProviderContext(documentUri, template, root, token.range)
);
}
}
if (token.definition?.key === "regular-step" && token.range) {
const context = getProviderContext(documentUri, template, root, token.range);
await validateAction(diagnostics, token, context.step, config);