@actions/languageservice 0.3.34 → 0.3.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/complete-action.d.ts +32 -0
- package/dist/complete-action.d.ts.map +1 -0
- package/dist/complete-action.js +380 -0
- package/dist/complete-action.js.map +1 -0
- package/dist/complete.d.ts +2 -0
- package/dist/complete.d.ts.map +1 -1
- package/dist/complete.js +17 -5
- package/dist/complete.js.map +1 -1
- package/dist/hover.d.ts.map +1 -1
- package/dist/hover.js +12 -0
- package/dist/hover.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/validate-action.d.ts.map +1 -1
- package/dist/validate-action.js +145 -2
- package/dist/validate-action.js.map +1 -1
- package/dist/validate-format-string.d.ts +35 -0
- package/dist/validate-format-string.d.ts.map +1 -0
- package/dist/validate-format-string.js +172 -0
- package/dist/validate-format-string.js.map +1 -0
- package/dist/validate.d.ts +2 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +104 -5
- package/dist/validate.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAC,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAE,gBAAgB,EAAC,MAAM,eAAe,CAAC;AACzD,OAAO,EAAC,qBAAqB,EAAC,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAC,KAAK,EAAC,MAAM,YAAY,CAAC;AACjC,OAAO,EAAC,aAAa,EAAC,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAC,MAAM,EAAE,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAC,MAAM,UAAU,CAAC;AACvE,OAAO,EAAC,QAAQ,EAAE,gBAAgB,EAAE,uBAAuB,EAAC,MAAM,eAAe,CAAC;AAClF,OAAO,EAAC,mBAAmB,EAAE,iBAAiB,EAAC,MAAM,6BAA6B,CAAC"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAmB,MAAM,eAAe,CAAC;AAEzD,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAC,KAAK,EAAC,MAAM,YAAY,CAAC;AACjC,OAAO,EAAC,aAAa,EAAC,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAS,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAC,MAAM,UAAU,CAAC;AACvE,OAAO,EAAC,QAAQ,EAA4C,MAAM,eAAe,CAAC;AAClF,OAAO,EAAsB,iBAAiB,EAAC,MAAM,6BAA6B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-action.d.ts","sourceRoot":"","sources":["../src/validate-action.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"validate-action.d.ts","sourceRoot":"","sources":["../src/validate-action.ts"],"names":[],"mappings":"AAAA;;GAEG;AAUH,OAAO,EAAC,YAAY,EAAC,MAAM,oCAAoC,CAAC;AAChE,OAAO,EAAC,UAAU,EAAqB,MAAM,6BAA6B,CAAC;AAK3E,OAAO,EAAC,gBAAgB,EAAC,MAAM,eAAe,CAAC;AA2B/C;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,YAAY,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAyEjH"}
|
package/dist/validate-action.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { isMapping } from "@actions/workflow-parser";
|
|
5
5
|
import { isActionStep } from "@actions/workflow-parser/model/type-guards";
|
|
6
6
|
import { ErrorPolicy } from "@actions/workflow-parser/model/convert";
|
|
7
|
+
import { MappingToken } from "@actions/workflow-parser/templates/tokens/mapping-token";
|
|
7
8
|
import { SequenceToken } from "@actions/workflow-parser/templates/tokens/sequence-token";
|
|
8
9
|
import { TemplateToken } from "@actions/workflow-parser/templates/tokens/template-token";
|
|
9
10
|
import { DiagnosticSeverity } from "vscode-languageserver-types";
|
|
@@ -11,6 +12,29 @@ import { error } from "./log.js";
|
|
|
11
12
|
import { mapRange } from "./utils/range.js";
|
|
12
13
|
import { getOrConvertActionTemplate, getOrParseAction } from "./utils/workflow-cache.js";
|
|
13
14
|
import { validateActionReference } from "./validate-action-reference.js";
|
|
15
|
+
/**
|
|
16
|
+
* Valid keys for each action type under the `runs:` section.
|
|
17
|
+
* Source: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionManifestManager.cs
|
|
18
|
+
*/
|
|
19
|
+
const NODE_KEYS = new Set(["using", "main", "pre", "post", "pre-if", "post-if"]);
|
|
20
|
+
const COMPOSITE_KEYS = new Set(["using", "steps"]);
|
|
21
|
+
const DOCKER_KEYS = new Set([
|
|
22
|
+
"using",
|
|
23
|
+
"image",
|
|
24
|
+
"args",
|
|
25
|
+
"env",
|
|
26
|
+
"entrypoint",
|
|
27
|
+
"pre-entrypoint",
|
|
28
|
+
"pre-if",
|
|
29
|
+
"post-entrypoint",
|
|
30
|
+
"post-if"
|
|
31
|
+
]);
|
|
32
|
+
/**
|
|
33
|
+
* Required keys for each action type (besides 'using').
|
|
34
|
+
*/
|
|
35
|
+
const NODE_REQUIRED_KEYS = ["main"];
|
|
36
|
+
const COMPOSITE_REQUIRED_KEYS = ["steps"];
|
|
37
|
+
const DOCKER_REQUIRED_KEYS = ["image"];
|
|
14
38
|
/**
|
|
15
39
|
* Validates an action.yml file
|
|
16
40
|
*
|
|
@@ -30,8 +54,14 @@ export async function validateAction(textDocument, config) {
|
|
|
30
54
|
if (!result) {
|
|
31
55
|
return [];
|
|
32
56
|
}
|
|
33
|
-
//
|
|
34
|
-
|
|
57
|
+
// Get schema errors
|
|
58
|
+
const schemaErrors = result.context.errors.getErrors();
|
|
59
|
+
// Run custom runs key validation, which also filters redundant schema errors in place
|
|
60
|
+
if (result.value) {
|
|
61
|
+
diagnostics.push(...validateRunsKeysAndFilterErrors(result.value, schemaErrors));
|
|
62
|
+
}
|
|
63
|
+
// Map remaining schema errors to diagnostics
|
|
64
|
+
for (const err of schemaErrors) {
|
|
35
65
|
const range = mapRange(err.range);
|
|
36
66
|
// Determine severity based on error type
|
|
37
67
|
let severity = DiagnosticSeverity.Error;
|
|
@@ -86,4 +116,117 @@ function findStepsSequence(root) {
|
|
|
86
116
|
}
|
|
87
117
|
return undefined;
|
|
88
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Validates that the keys under `runs:` are valid for the specified `using:` type.
|
|
121
|
+
* Also filters out schema errors (in place) that this validation replaces with more specific messages.
|
|
122
|
+
*/
|
|
123
|
+
function validateRunsKeysAndFilterErrors(root, schemaErrors // mutated: redundant errors are removed
|
|
124
|
+
) {
|
|
125
|
+
const diagnostics = [];
|
|
126
|
+
// Find the runs mapping from the root
|
|
127
|
+
let runsMapping;
|
|
128
|
+
if (root instanceof MappingToken) {
|
|
129
|
+
for (let i = 0; i < root.count; i++) {
|
|
130
|
+
const { key, value } = root.get(i);
|
|
131
|
+
if (key.toString().toLowerCase() === "runs" && value instanceof MappingToken) {
|
|
132
|
+
runsMapping = value;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!runsMapping) {
|
|
138
|
+
return diagnostics;
|
|
139
|
+
}
|
|
140
|
+
// Get the using value from the runs mapping
|
|
141
|
+
let usingValue;
|
|
142
|
+
for (let i = 0; i < runsMapping.count; i++) {
|
|
143
|
+
const { key, value } = runsMapping.get(i);
|
|
144
|
+
if (key.toString().toLowerCase() === "using") {
|
|
145
|
+
usingValue = value.toString();
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!usingValue) {
|
|
150
|
+
return diagnostics; // No using value, let schema validation handle it
|
|
151
|
+
}
|
|
152
|
+
// Determine allowed keys, required keys, and action type name
|
|
153
|
+
let allowedKeys;
|
|
154
|
+
let requiredKeys;
|
|
155
|
+
let actionType;
|
|
156
|
+
if (usingValue.match(/^node\d+$/i)) {
|
|
157
|
+
allowedKeys = NODE_KEYS;
|
|
158
|
+
requiredKeys = NODE_REQUIRED_KEYS;
|
|
159
|
+
actionType = "Node.js";
|
|
160
|
+
}
|
|
161
|
+
else if (usingValue.toLowerCase() === "composite") {
|
|
162
|
+
allowedKeys = COMPOSITE_KEYS;
|
|
163
|
+
requiredKeys = COMPOSITE_REQUIRED_KEYS;
|
|
164
|
+
actionType = "composite";
|
|
165
|
+
}
|
|
166
|
+
else if (usingValue.toLowerCase() === "docker") {
|
|
167
|
+
allowedKeys = DOCKER_KEYS;
|
|
168
|
+
requiredKeys = DOCKER_REQUIRED_KEYS;
|
|
169
|
+
actionType = "Docker";
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
return diagnostics; // Unknown type, let schema validation handle it
|
|
173
|
+
}
|
|
174
|
+
// Get all present keys
|
|
175
|
+
const presentKeys = new Set();
|
|
176
|
+
for (let i = 0; i < runsMapping.count; i++) {
|
|
177
|
+
const { key } = runsMapping.get(i);
|
|
178
|
+
presentKeys.add(key.toString().toLowerCase());
|
|
179
|
+
}
|
|
180
|
+
// Check for invalid keys
|
|
181
|
+
for (let i = 0; i < runsMapping.count; i++) {
|
|
182
|
+
const { key } = runsMapping.get(i);
|
|
183
|
+
const keyStr = key.toString().toLowerCase();
|
|
184
|
+
if (!allowedKeys.has(keyStr)) {
|
|
185
|
+
diagnostics.push({
|
|
186
|
+
severity: DiagnosticSeverity.Error,
|
|
187
|
+
range: mapRange(key.range),
|
|
188
|
+
message: `'${key.toString()}' is not valid for ${actionType} actions (using: ${usingValue})`
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Check for missing required keys
|
|
193
|
+
for (const requiredKey of requiredKeys) {
|
|
194
|
+
if (!presentKeys.has(requiredKey)) {
|
|
195
|
+
// Find the 'using' key to report the error location
|
|
196
|
+
let usingKeyRange = runsMapping.range;
|
|
197
|
+
for (let i = 0; i < runsMapping.count; i++) {
|
|
198
|
+
const { key } = runsMapping.get(i);
|
|
199
|
+
if (key.toString().toLowerCase() === "using") {
|
|
200
|
+
usingKeyRange = key.range;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
diagnostics.push({
|
|
205
|
+
severity: DiagnosticSeverity.Error,
|
|
206
|
+
range: mapRange(usingKeyRange),
|
|
207
|
+
message: `'${requiredKey}' is required for ${actionType} actions (using: ${usingValue})`
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Remove schema errors that we're replacing with more specific messages (mutate in place)
|
|
212
|
+
for (let i = schemaErrors.length - 1; i >= 0; i--) {
|
|
213
|
+
const err = schemaErrors[i];
|
|
214
|
+
// Keep errors not at the runs section start
|
|
215
|
+
if (err.range?.start.line !== runsMapping.range?.start.line ||
|
|
216
|
+
err.range?.start.column !== runsMapping.range?.start.column) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
// Check if this is an error we're replacing
|
|
220
|
+
const isOneOfAmbiguity = err.rawMessage.startsWith("There's not enough info to determine");
|
|
221
|
+
const isRequiredKey = /^Required property is missing: (main|steps|image)$/.test(err.rawMessage);
|
|
222
|
+
if (!isOneOfAmbiguity && !isRequiredKey) {
|
|
223
|
+
continue; // Keep errors we're not replacing
|
|
224
|
+
}
|
|
225
|
+
// Remove only if we have custom diagnostics for this
|
|
226
|
+
if (diagnostics.length > 0) {
|
|
227
|
+
schemaErrors.splice(i, 1);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return diagnostics;
|
|
231
|
+
}
|
|
89
232
|
//# sourceMappingURL=validate-action.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-action.js","sourceRoot":"","sources":["../src/validate-action.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAC,SAAS,EAAC,MAAM,0BAA0B,CAAC;AACnD,OAAO,EAAC,YAAY,EAAC,MAAM,4CAA4C,CAAC;AACxE,OAAO,EAAC,WAAW,EAAC,MAAM,wCAAwC,CAAC;AACnE,OAAO,EAAC,aAAa,EAAC,MAAM,0DAA0D,CAAC;AACvF,OAAO,EAAC,aAAa,EAAC,MAAM,0DAA0D,CAAC;
|
|
1
|
+
{"version":3,"file":"validate-action.js","sourceRoot":"","sources":["../src/validate-action.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAC,SAAS,EAAC,MAAM,0BAA0B,CAAC;AACnD,OAAO,EAAC,YAAY,EAAC,MAAM,4CAA4C,CAAC;AACxE,OAAO,EAAC,WAAW,EAAC,MAAM,wCAAwC,CAAC;AACnE,OAAO,EAAC,YAAY,EAAC,MAAM,yDAAyD,CAAC;AACrF,OAAO,EAAC,aAAa,EAAC,MAAM,0DAA0D,CAAC;AACvF,OAAO,EAAC,aAAa,EAAC,MAAM,0DAA0D,CAAC;AAIvF,OAAO,EAAa,kBAAkB,EAAC,MAAM,6BAA6B,CAAC;AAC3E,OAAO,EAAC,KAAK,EAAC,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAC,QAAQ,EAAC,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAC,0BAA0B,EAAE,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AACvF,OAAO,EAAC,uBAAuB,EAAC,MAAM,gCAAgC,CAAC;AAGvE;;;GAGG;AACH,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC;AACjF,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;AACnD,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC;IAC1B,OAAO;IACP,OAAO;IACP,MAAM;IACN,KAAK;IACL,YAAY;IACZ,gBAAgB;IAChB,QAAQ;IACR,iBAAiB;IACjB,SAAS;CACV,CAAC,CAAC;AAEH;;GAEG;AACH,MAAM,kBAAkB,GAAG,CAAC,MAAM,CAAC,CAAC;AACpC,MAAM,uBAAuB,GAAG,CAAC,OAAO,CAAC,CAAC;AAC1C,MAAM,oBAAoB,GAAG,CAAC,OAAO,CAAC,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,YAA0B,EAAE,MAAyB;IACxF,MAAM,IAAI,GAAS;QACjB,IAAI,EAAE,YAAY,CAAC,GAAG;QACtB,OAAO,EAAE,YAAY,CAAC,OAAO,EAAE;KAChC,CAAC;IAEF,MAAM,WAAW,GAAiB,EAAE,CAAC;IAErC,IAAI;QACF,uDAAuD;QACvD,MAAM,MAAM,GAAG,gBAAgB,CAAC,IAAI,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC;QACxD,IAAI,CAAC,MAAM,EAAE;YACX,OAAO,EAAE,CAAC;SACX;QAED,oBAAoB;QACpB,MAAM,YAAY,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QAEvD,sFAAsF;QACtF,IAAI,MAAM,CAAC,KAAK,EAAE;YAChB,WAAW,CAAC,IAAI,CAAC,GAAG,+BAA+B,CAAC,MAAM,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC;SAClF;QAED,6CAA6C;QAC7C,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE;YAC9B,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAElC,yCAAyC;YACzC,IAAI,QAAQ,GAAuB,kBAAkB,CAAC,KAAK,CAAC;YAE5D,yCAAyC;YACzC,IAAI,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE;gBACzC,QAAQ,GAAG,kBAAkB,CAAC,OAAO,CAAC;aACvC;YAED,WAAW,CAAC,IAAI,CAAC;gBACf,OAAO,EAAE,GAAG,CAAC,UAAU;gBACvB,KAAK;gBACL,QAAQ;aACT,CAAC,CAAC;SACJ;QAED,6DAA6D;QAC7D,IAAI,MAAM,CAAC,KAAK,EAAE;YAChB,MAAM,QAAQ,GAAG,0BAA0B,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,EAAE,YAAY,CAAC,GAAG,EAAE;gBAC1F,WAAW,EAAE,WAAW,CAAC,aAAa;aACvC,CAAC,CAAC;YAEH,gDAAgD;YAChD,IAAI,QAAQ,EAAE,IAAI,EAAE,KAAK,KAAK,WAAW,EAAE;gBACzC,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;gBAExC,2DAA2D;gBAC3D,MAAM,aAAa,GAAG,iBAAiB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACtD,IAAI,aAAa,EAAE;oBACjB,4BAA4B;oBAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;wBACrC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;wBACtB,MAAM,SAAS,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;wBAEvC,sEAAsE;wBACtE,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC,SAAS,CAAC,EAAE;4BAC9C,MAAM,uBAAuB,CAAC,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;yBACrE;qBACF;iBACF;aACF;SACF;KACF;IAAC,OAAO,CAAC,EAAE;QACV,KAAK,CAAC,iDAAkD,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;KAChF;IAED,OAAO,WAAW,CAAC;AACrB,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB,CAAC,IAAmB;IAC5C,KAAK,MAAM,CAAC,EAAE,KAAK,CAAC,IAAI,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE;QACpD,IAAI,KAAK,CAAC,UAAU,EAAE,GAAG,KAAK,iBAAiB,IAAI,KAAK,YAAY,aAAa,EAAE;YACjF,OAAO,KAAK,CAAC;SACd;KACF;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,SAAS,+BAA+B,CACtC,IAAmB,EACnB,YAAuC,CAAC,wCAAwC;;IAEhF,MAAM,WAAW,GAAiB,EAAE,CAAC;IAErC,sCAAsC;IACtC,IAAI,WAAqC,CAAC;IAC1C,IAAI,IAAI,YAAY,YAAY,EAAE;QAChC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;YACnC,MAAM,EAAC,GAAG,EAAE,KAAK,EAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACjC,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC,WAAW,EAAE,KAAK,MAAM,IAAI,KAAK,YAAY,YAAY,EAAE;gBAC5E,WAAW,GAAG,KAAK,CAAC;gBACpB,MAAM;aACP;SACF;KACF;IACD,IAAI,CAAC,WAAW,EAAE;QAChB,OAAO,WAAW,CAAC;KACpB;IAED,4CAA4C;IAC5C,IAAI,UAA8B,CAAC;IACnC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,WAAW,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;QAC1C,MAAM,EAAC,GAAG,EAAE,KAAK,EAAC,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACxC,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC,WAAW,EAAE,KAAK,OAAO,EAAE;YAC5C,UAAU,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC9B,MAAM;SACP;KACF;IACD,IAAI,CAAC,UAAU,EAAE;QACf,OAAO,WAAW,CAAC,CAAC,kDAAkD;KACvE;IAED,8DAA8D;IAC9D,IAAI,WAAwB,CAAC;IAC7B,IAAI,YAAsB,CAAC;IAC3B,IAAI,UAAkB,CAAC;IAEvB,IAAI,UAAU,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE;QAClC,WAAW,GAAG,SAAS,CAAC;QACxB,YAAY,GAAG,kBAAkB,CAAC;QAClC,UAAU,GAAG,SAAS,CAAC;KACxB;SAAM,IAAI,UAAU,CAAC,WAAW,EAAE,KAAK,WAAW,EAAE;QACnD,WAAW,GAAG,cAAc,CAAC;QAC7B,YAAY,GAAG,uBAAuB,CAAC;QACvC,UAAU,GAAG,WAAW,CAAC;KAC1B;SAAM,IAAI,UAAU,CAAC,WAAW,EAAE,KAAK,QAAQ,EAAE;QAChD,WAAW,GAAG,WAAW,CAAC;QAC1B,YAAY,GAAG,oBAAoB,CAAC;QACpC,UAAU,GAAG,QAAQ,CAAC;KACvB;SAAM;QACL,OAAO,WAAW,CAAC,CAAC,gDAAgD;KACrE;IAED,uBAAuB;IACvB,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IACtC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,WAAW,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;QAC1C,MAAM,EAAC,GAAG,EAAC,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACjC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;KAC/C;IAED,yBAAyB;IACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,WAAW,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;QAC1C,MAAM,EAAC,GAAG,EAAC,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC,WAAW,EAAE,CAAC;QAE5C,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;YAC5B,WAAW,CAAC,IAAI,CAAC;gBACf,QAAQ,EAAE,kBAAkB,CAAC,KAAK;gBAClC,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC;gBAC1B,OAAO,EAAE,IAAI,GAAG,CAAC,QAAQ,EAAE,sBAAsB,UAAU,oBAAoB,UAAU,GAAG;aAC7F,CAAC,CAAC;SACJ;KACF;IAED,kCAAkC;IAClC,KAAK,MAAM,WAAW,IAAI,YAAY,EAAE;QACtC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE;YACjC,oDAAoD;YACpD,IAAI,aAAa,GAAG,WAAW,CAAC,KAAK,CAAC;YACtC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,WAAW,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;gBAC1C,MAAM,EAAC,GAAG,EAAC,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBACjC,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC,WAAW,EAAE,KAAK,OAAO,EAAE;oBAC5C,aAAa,GAAG,GAAG,CAAC,KAAK,CAAC;oBAC1B,MAAM;iBACP;aACF;YAED,WAAW,CAAC,IAAI,CAAC;gBACf,QAAQ,EAAE,kBAAkB,CAAC,KAAK;gBAClC,KAAK,EAAE,QAAQ,CAAC,aAAa,CAAC;gBAC9B,OAAO,EAAE,IAAI,WAAW,qBAAqB,UAAU,oBAAoB,UAAU,GAAG;aACzF,CAAC,CAAC;SACJ;KACF;IAED,0FAA0F;IAC1F,KAAK,IAAI,CAAC,GAAG,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE;QACjD,MAAM,GAAG,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;QAE5B,4CAA4C;QAC5C,IACE,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,KAAK,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI;YACvD,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,KAAK,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,EAC3D;YACA,SAAS;SACV;QAED,4CAA4C;QAC5C,MAAM,gBAAgB,GAAG,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,sCAAsC,CAAC,CAAC;QAC3F,MAAM,aAAa,GAAG,oDAAoD,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAEhG,IAAI,CAAC,gBAAgB,IAAI,CAAC,aAAa,EAAE;YACvC,SAAS,CAAC,kCAAkC;SAC7C;QAED,qDAAqD;QACrD,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE;YAC1B,YAAY,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;SAC3B;KACF;IAED,OAAO,WAAW,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format string validation for format() function calls.
|
|
3
|
+
* Port of Go's format_validator.go from actions-workflow-parser.
|
|
4
|
+
*/
|
|
5
|
+
import { Expr } from "@actions/expressions/ast";
|
|
6
|
+
/**
|
|
7
|
+
* Error types for format string validation
|
|
8
|
+
*/
|
|
9
|
+
export type FormatStringError = {
|
|
10
|
+
type: "invalid-syntax";
|
|
11
|
+
message: string;
|
|
12
|
+
} | {
|
|
13
|
+
type: "arg-count-mismatch";
|
|
14
|
+
expected: number;
|
|
15
|
+
provided: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Validates a format string and returns the maximum placeholder index.
|
|
19
|
+
* Port of Go's validateFormatString from format_validator.go.
|
|
20
|
+
*
|
|
21
|
+
* @param formatString The format string to validate
|
|
22
|
+
* @returns { valid: boolean, maxArgIndex: number } where maxArgIndex is -1 if no placeholders
|
|
23
|
+
*/
|
|
24
|
+
export declare function validateFormatString(formatString: string): {
|
|
25
|
+
valid: boolean;
|
|
26
|
+
maxArgIndex: number;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Walks an expression AST to find and validate all format() function calls.
|
|
30
|
+
*
|
|
31
|
+
* @param expr The expression AST to validate
|
|
32
|
+
* @returns Array of validation errors found
|
|
33
|
+
*/
|
|
34
|
+
export declare function validateFormatCalls(expr: Expr): FormatStringError[];
|
|
35
|
+
//# sourceMappingURL=validate-format-string.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-format-string.d.ts","sourceRoot":"","sources":["../src/validate-format-string.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAC,IAAI,EAAuE,MAAM,0BAA0B,CAAC;AAGpH;;GAEG;AACH,MAAM,MAAM,iBAAiB,GACzB;IAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GACzC;IAAC,IAAI,EAAE,oBAAoB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,CAAC;AAErE;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAC,CAyFhG;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,iBAAiB,EAAE,CAsCnE"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format string validation for format() function calls.
|
|
3
|
+
* Port of Go's format_validator.go from actions-workflow-parser.
|
|
4
|
+
*/
|
|
5
|
+
import { FunctionCall, Literal, Binary, Unary, Logical, Grouping, IndexAccess } from "@actions/expressions/ast";
|
|
6
|
+
import { Kind } from "@actions/expressions/data/expressiondata";
|
|
7
|
+
/**
|
|
8
|
+
* Validates a format string and returns the maximum placeholder index.
|
|
9
|
+
* Port of Go's validateFormatString from format_validator.go.
|
|
10
|
+
*
|
|
11
|
+
* @param formatString The format string to validate
|
|
12
|
+
* @returns { valid: boolean, maxArgIndex: number } where maxArgIndex is -1 if no placeholders
|
|
13
|
+
*/
|
|
14
|
+
export function validateFormatString(formatString) {
|
|
15
|
+
let maxIndex = -1;
|
|
16
|
+
let i = 0;
|
|
17
|
+
while (i < formatString.length) {
|
|
18
|
+
// Find next left brace
|
|
19
|
+
let lbrace = -1;
|
|
20
|
+
for (let j = i; j < formatString.length; j++) {
|
|
21
|
+
if (formatString[j] === "{") {
|
|
22
|
+
lbrace = j;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Find next right brace
|
|
27
|
+
let rbrace = -1;
|
|
28
|
+
for (let j = i; j < formatString.length; j++) {
|
|
29
|
+
if (formatString[j] === "}") {
|
|
30
|
+
rbrace = j;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// No more braces
|
|
35
|
+
if (lbrace < 0 && rbrace < 0) {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
// Left brace comes first (or only left brace exists)
|
|
39
|
+
if (lbrace >= 0 && (rbrace < 0 || lbrace < rbrace)) {
|
|
40
|
+
// Check if it's escaped
|
|
41
|
+
if (lbrace + 1 < formatString.length && formatString[lbrace + 1] === "{") {
|
|
42
|
+
// Escaped left brace
|
|
43
|
+
i = lbrace + 2;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// This is a placeholder opening - find the closing brace
|
|
47
|
+
rbrace = -1;
|
|
48
|
+
for (let j = lbrace + 1; j < formatString.length; j++) {
|
|
49
|
+
if (formatString[j] === "}") {
|
|
50
|
+
rbrace = j;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (rbrace < 0) {
|
|
55
|
+
// Missing closing brace
|
|
56
|
+
return { valid: false, maxArgIndex: -1 };
|
|
57
|
+
}
|
|
58
|
+
// Validate placeholder content (must be digits only)
|
|
59
|
+
if (rbrace === lbrace + 1) {
|
|
60
|
+
// Empty placeholder {}
|
|
61
|
+
return { valid: false, maxArgIndex: -1 };
|
|
62
|
+
}
|
|
63
|
+
// Parse the index and validate it's all digits
|
|
64
|
+
let index = 0;
|
|
65
|
+
for (let j = lbrace + 1; j < rbrace; j++) {
|
|
66
|
+
const c = formatString[j];
|
|
67
|
+
if (c < "0" || c > "9") {
|
|
68
|
+
// Non-numeric character
|
|
69
|
+
return { valid: false, maxArgIndex: -1 };
|
|
70
|
+
}
|
|
71
|
+
index = index * 10 + (c.charCodeAt(0) - "0".charCodeAt(0));
|
|
72
|
+
}
|
|
73
|
+
if (index > maxIndex) {
|
|
74
|
+
maxIndex = index;
|
|
75
|
+
}
|
|
76
|
+
i = rbrace + 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// Right brace comes first (or only right brace exists)
|
|
80
|
+
// Check if it's escaped
|
|
81
|
+
if (rbrace + 1 < formatString.length && formatString[rbrace + 1] === "}") {
|
|
82
|
+
// Escaped right brace
|
|
83
|
+
i = rbrace + 2;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// Unescaped right brace outside of placeholder
|
|
87
|
+
return { valid: false, maxArgIndex: -1 };
|
|
88
|
+
}
|
|
89
|
+
return { valid: true, maxArgIndex: maxIndex };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Walks an expression AST to find and validate all format() function calls.
|
|
93
|
+
*
|
|
94
|
+
* @param expr The expression AST to validate
|
|
95
|
+
* @returns Array of validation errors found
|
|
96
|
+
*/
|
|
97
|
+
export function validateFormatCalls(expr) {
|
|
98
|
+
const errors = [];
|
|
99
|
+
const stack = [expr];
|
|
100
|
+
while (stack.length > 0) {
|
|
101
|
+
const node = stack.pop();
|
|
102
|
+
if (!node) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (node instanceof FunctionCall) {
|
|
106
|
+
if (node.functionName.lexeme.toLowerCase() === "format") {
|
|
107
|
+
const error = validateSingleFormatCall(node);
|
|
108
|
+
if (error) {
|
|
109
|
+
errors.push(error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Push args for further processing (to find nested format calls)
|
|
113
|
+
for (const arg of node.args) {
|
|
114
|
+
stack.push(arg);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (node instanceof Binary) {
|
|
118
|
+
stack.push(node.left, node.right);
|
|
119
|
+
}
|
|
120
|
+
else if (node instanceof Unary) {
|
|
121
|
+
stack.push(node.expr);
|
|
122
|
+
}
|
|
123
|
+
else if (node instanceof Logical) {
|
|
124
|
+
for (const arg of node.args) {
|
|
125
|
+
stack.push(arg);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else if (node instanceof Grouping) {
|
|
129
|
+
stack.push(node.group);
|
|
130
|
+
}
|
|
131
|
+
else if (node instanceof IndexAccess) {
|
|
132
|
+
stack.push(node.expr, node.index);
|
|
133
|
+
}
|
|
134
|
+
// Literal, ContextAccess - no children to process
|
|
135
|
+
}
|
|
136
|
+
return errors;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Validates a single format() function call.
|
|
140
|
+
*
|
|
141
|
+
* @param fc The FunctionCall AST node
|
|
142
|
+
* @returns Validation error if found, undefined if valid
|
|
143
|
+
*/
|
|
144
|
+
function validateSingleFormatCall(fc) {
|
|
145
|
+
// Must have at least one argument (the format string)
|
|
146
|
+
if (fc.args.length < 1) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
// First argument must be a string literal
|
|
150
|
+
const firstArg = fc.args[0];
|
|
151
|
+
if (!(firstArg instanceof Literal) || firstArg.literal.kind !== Kind.String) {
|
|
152
|
+
return undefined; // Can't validate dynamic format strings
|
|
153
|
+
}
|
|
154
|
+
const formatString = firstArg.literal.coerceString();
|
|
155
|
+
const numArgs = fc.args.length - 1; // Subtract 1 for format string itself
|
|
156
|
+
const { valid, maxArgIndex } = validateFormatString(formatString);
|
|
157
|
+
if (!valid) {
|
|
158
|
+
return {
|
|
159
|
+
type: "invalid-syntax",
|
|
160
|
+
message: "Format string has invalid syntax (missing closing brace, unescaped braces, or invalid placeholder)"
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (maxArgIndex >= numArgs) {
|
|
164
|
+
return {
|
|
165
|
+
type: "arg-count-mismatch",
|
|
166
|
+
expected: maxArgIndex + 1,
|
|
167
|
+
provided: numArgs
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=validate-format-string.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-format-string.js","sourceRoot":"","sources":["../src/validate-format-string.ts"],"names":[],"mappings":"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"}
|
package/dist/validate.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FeatureFlags } from "@actions/expressions";
|
|
1
2
|
import { FileProvider } from "@actions/workflow-parser/workflows/file-provider";
|
|
2
3
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
3
4
|
import { Diagnostic } from "vscode-languageserver-types";
|
|
@@ -9,6 +10,7 @@ export type ValidationConfig = {
|
|
|
9
10
|
contextProviderConfig?: ContextProviderConfig;
|
|
10
11
|
actionsMetadataProvider?: ActionsMetadataProvider;
|
|
11
12
|
fileProvider?: FileProvider;
|
|
13
|
+
featureFlags?: FeatureFlags;
|
|
12
14
|
};
|
|
13
15
|
export type ActionsMetadataProvider = {
|
|
14
16
|
fetchActionMetadata(action: ActionReference): Promise<ActionMetadata | undefined>;
|
package/dist/validate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAsB,MAAM,sBAAsB,CAAC;AAYvE,OAAO,EAAC,YAAY,EAAC,MAAM,kDAAkD,CAAC;AAC9E,OAAO,EAAC,YAAY,EAAC,MAAM,oCAAoC,CAAC;AAChE,OAAO,EAAC,UAAU,EAA0B,MAAM,6BAA6B,CAAC;AAChF,OAAO,EAAC,cAAc,EAAE,eAAe,EAAC,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAC,qBAAqB,EAAC,MAAM,+BAA+B,CAAC;AAcpE,OAAO,EAAC,mBAAmB,EAAoB,MAAM,6BAA6B,CAAC;AAMnF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;IAC9C,uBAAuB,CAAC,EAAE,uBAAuB,CAAC;IAClD,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;CACnF,CAAC;AAEF;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,YAAY,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAI3G"}
|
package/dist/validate.js
CHANGED
|
@@ -20,6 +20,7 @@ import { mapRange } from "./utils/range.js";
|
|
|
20
20
|
import { getOrConvertWorkflowTemplate, getOrParseWorkflow } from "./utils/workflow-cache.js";
|
|
21
21
|
import { validateActionReference } from "./validate-action-reference.js";
|
|
22
22
|
import { validateAction } from "./validate-action.js";
|
|
23
|
+
import { validateFormatCalls } from "./validate-format-string.js";
|
|
23
24
|
import { ValueProviderKind } from "./value-providers/config.js";
|
|
24
25
|
import { defaultValueProviders } from "./value-providers/default.js";
|
|
25
26
|
const CRON_SCHEDULE_DOCS_URL = "https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#onschedule";
|
|
@@ -58,7 +59,7 @@ async function validateWorkflow(textDocument, config) {
|
|
|
58
59
|
errorPolicy: ErrorPolicy.TryConversion
|
|
59
60
|
});
|
|
60
61
|
// Validate expressions and value providers
|
|
61
|
-
await additionalValidations(diagnostics, textDocument.uri, template, result.value, config);
|
|
62
|
+
await additionalValidations(diagnostics, textDocument.uri, template, result.value, config, config?.featureFlags);
|
|
62
63
|
}
|
|
63
64
|
// For now map parser errors directly to diagnostics
|
|
64
65
|
for (const error of result.context.errors.getErrors()) {
|
|
@@ -74,8 +75,8 @@ async function validateWorkflow(textDocument, config) {
|
|
|
74
75
|
}
|
|
75
76
|
return diagnostics;
|
|
76
77
|
}
|
|
77
|
-
async function additionalValidations(diagnostics, documentUri, template, root, config) {
|
|
78
|
-
for (const [parent, token, key] of TemplateToken.traverse(root)) {
|
|
78
|
+
async function additionalValidations(diagnostics, documentUri, template, root, config, featureFlags) {
|
|
79
|
+
for (const [parent, token, key, ancestors] of TemplateToken.traverse(root)) {
|
|
79
80
|
// If the token is a value in a pair, use the key definition for validation
|
|
80
81
|
// If the token has a parent (map, sequence, etc), use this definition for validation
|
|
81
82
|
const validationToken = key || parent || token;
|
|
@@ -84,7 +85,11 @@ async function additionalValidations(diagnostics, documentUri, template, root, c
|
|
|
84
85
|
if (isBasicExpression(token) && token.range) {
|
|
85
86
|
await validateExpression(diagnostics, token, validationToken.definitionInfo?.allowedContext || [], config?.contextProviderConfig, getProviderContext(documentUri, template, root, token.range), key?.definition?.key);
|
|
86
87
|
}
|
|
87
|
-
//
|
|
88
|
+
// Validate block scalar chomping for expressions and strings
|
|
89
|
+
if (featureFlags?.isEnabled("blockScalarChompingWarning")) {
|
|
90
|
+
validateBlockScalarChomping(diagnostics, token, parent, key, ancestors);
|
|
91
|
+
}
|
|
92
|
+
// `if` conditions allow omitting ${{ }}, so validate strings in these fields as expressions
|
|
88
93
|
const definitionKey = token.definition?.key;
|
|
89
94
|
if (isString(token) &&
|
|
90
95
|
token.range &&
|
|
@@ -95,7 +100,7 @@ async function additionalValidations(diagnostics, documentUri, template, root, c
|
|
|
95
100
|
// Ensure the condition has a status function, wrapping if needed
|
|
96
101
|
const finalCondition = ensureStatusFunction(condition, token.definitionInfo);
|
|
97
102
|
// Create a BasicExpressionToken for validation
|
|
98
|
-
const expressionToken = new BasicExpressionToken(token.file, token.range, finalCondition, token.definitionInfo, undefined, token.source);
|
|
103
|
+
const expressionToken = new BasicExpressionToken(token.file, token.range, finalCondition, token.definitionInfo, undefined, token.source, undefined, token.blockScalarHeader);
|
|
99
104
|
await validateExpression(diagnostics, expressionToken, validationToken.definitionInfo?.allowedContext || [], config?.contextProviderConfig, getProviderContext(documentUri, template, root, token.range));
|
|
100
105
|
}
|
|
101
106
|
}
|
|
@@ -562,6 +567,26 @@ async function validateExpression(diagnostics, token, allowedContext, contextPro
|
|
|
562
567
|
// Ignore any error here, we should've caught this earlier in the parsing process
|
|
563
568
|
continue;
|
|
564
569
|
}
|
|
570
|
+
// Validate format() function calls
|
|
571
|
+
const formatErrors = validateFormatCalls(expr);
|
|
572
|
+
for (const formatError of formatErrors) {
|
|
573
|
+
if (formatError.type === "invalid-syntax") {
|
|
574
|
+
diagnostics.push({
|
|
575
|
+
message: `Invalid format string: ${formatError.message}`,
|
|
576
|
+
range: mapRange(expression.range),
|
|
577
|
+
severity: DiagnosticSeverity.Error,
|
|
578
|
+
code: "invalid-format-string"
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
else if (formatError.type === "arg-count-mismatch") {
|
|
582
|
+
diagnostics.push({
|
|
583
|
+
message: `Format string references argument {${formatError.expected - 1}} but only ${formatError.provided} argument(s) provided`,
|
|
584
|
+
range: mapRange(expression.range),
|
|
585
|
+
severity: DiagnosticSeverity.Error,
|
|
586
|
+
code: "format-arg-count-mismatch"
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
565
590
|
const context = await getWorkflowExpressionContext(namedContexts, contextProviderConfig, workflowContext, Mode.Validation);
|
|
566
591
|
const e = new ValidationEvaluator(expr, wrapDictionary(context), validatorFunctions);
|
|
567
592
|
e.validate();
|
|
@@ -631,4 +656,78 @@ function getStaticConcurrencyGroup(token) {
|
|
|
631
656
|
}
|
|
632
657
|
return undefined;
|
|
633
658
|
}
|
|
659
|
+
/**
|
|
660
|
+
* Validates YAML block scalar chomping.
|
|
661
|
+
*
|
|
662
|
+
* Block scalars (| and >) implicitly add a trailing newline by default ("clip" chomping).
|
|
663
|
+
* This is often unintended by the workflow author and can cause unexpected behavior.
|
|
664
|
+
* This function warns on certain fields when clip chomping is used (implicit trailing newline)
|
|
665
|
+
* and suggests they explicitly use strip (|-) or keep (|+) to clarify intent.
|
|
666
|
+
*
|
|
667
|
+
* Only specific fields are validated - those where trailing newlines may cause
|
|
668
|
+
* issues but aren't automatically trimmed server-side. For example env, inputs, outputs, etc.
|
|
669
|
+
*
|
|
670
|
+
* Skipped fields:
|
|
671
|
+
* - run: Multi-line scripts commonly have trailing newlines
|
|
672
|
+
* - Fields trimmed server-side: name, uses, shell, if, etc.
|
|
673
|
+
*/
|
|
674
|
+
function validateBlockScalarChomping(diagnostics, token, parent, key, ancestors) {
|
|
675
|
+
// Not an expression or string?
|
|
676
|
+
if (!isBasicExpression(token) && !isString(token)) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
// Not a block scalar?
|
|
680
|
+
const header = token.blockScalarHeader;
|
|
681
|
+
if (!header) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
// Not "clip" chomp style?
|
|
685
|
+
if (header.includes("+") || header.includes("-")) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
// Check if we should warn
|
|
689
|
+
let shouldWarn = false;
|
|
690
|
+
const parentDefinitionName = parent?.definition?.key;
|
|
691
|
+
const tokenDefinitionName = token.definition?.key;
|
|
692
|
+
const keyName = key && isString(key) ? key.value : undefined;
|
|
693
|
+
if (parentDefinitionName &&
|
|
694
|
+
[
|
|
695
|
+
"workflow-env",
|
|
696
|
+
"job-env",
|
|
697
|
+
"step-env",
|
|
698
|
+
"container-env",
|
|
699
|
+
"step-with",
|
|
700
|
+
"job-outputs",
|
|
701
|
+
"workflow-job-with",
|
|
702
|
+
"workflow-job-secrets"
|
|
703
|
+
].includes(parentDefinitionName)) {
|
|
704
|
+
// env, with, outputs, or secrets fields
|
|
705
|
+
shouldWarn = true;
|
|
706
|
+
}
|
|
707
|
+
else if (ancestors.some(ancestor => {
|
|
708
|
+
const ancestorKey = ancestor.definition?.key;
|
|
709
|
+
return ancestorKey === "matrix" || ancestorKey === "matrix-filter" || ancestorKey === "matrix-filter-item";
|
|
710
|
+
})) {
|
|
711
|
+
// Matrix values (vectors, include, exclude)
|
|
712
|
+
shouldWarn = true;
|
|
713
|
+
}
|
|
714
|
+
else if (tokenDefinitionName && ["workflow-concurrency", "job-concurrency"].includes(tokenDefinitionName)) {
|
|
715
|
+
// Concurrency shorthand
|
|
716
|
+
shouldWarn = true;
|
|
717
|
+
}
|
|
718
|
+
else if (keyName === "group" && parentDefinitionName === "concurrency-mapping") {
|
|
719
|
+
// Concurrency group field
|
|
720
|
+
shouldWarn = true;
|
|
721
|
+
}
|
|
722
|
+
if (!shouldWarn) {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const blockIndicator = header.startsWith("|") ? "|" : ">";
|
|
726
|
+
diagnostics.push({
|
|
727
|
+
message: `Block scalar '${blockIndicator}' implicitly adds a trailing newline that may be unintentional. Use '${blockIndicator}-' to remove it, or '${blockIndicator}+' to explicitly keep it.`,
|
|
728
|
+
range: mapRange(token.range),
|
|
729
|
+
severity: DiagnosticSeverity.Warning,
|
|
730
|
+
code: "block-scalar-chomping"
|
|
731
|
+
});
|
|
732
|
+
}
|
|
634
733
|
//# sourceMappingURL=validate.js.map
|