@xano/xanoscript-language-server 12.17.0 → 12.19.0
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/cache/documentCache.js +12 -5
- package/language-server-worker.js +6 -0
- package/lexer/mcp.js +26 -1
- package/lexer/policy.js +30 -0
- package/lexer/tokens.js +6 -0
- package/onCompletion/contentAssist.js +6 -0
- package/onCompletion/onCompletion.js +33 -3
- package/onCompletion/policyCompletion.js +339 -0
- package/onHover/PolicyMessageProvider.js +142 -0
- package/onHover/functions.md +43 -0
- package/onHover/onHover.js +2 -0
- package/package.json +3 -1
- package/parser/functions/mcp/mcpOauthFn.js +140 -0
- package/parser/functions/mcp/register.js +10 -0
- package/parser/functions/mcpFn.js +2 -1
- package/parser/mcp_server_parser.js +48 -1
- package/parser/multidoc.js +45 -13
- package/parser/parser.js +52 -0
- package/parser/policy/catalogue.js +100 -0
- package/parser/policy/catalogueSnapshot.js +7075 -0
- package/parser/policy/policyDocs.js +76 -0
- package/parser/policy/policyLiteral.js +227 -0
- package/parser/policy/validatePolicyRule.js +323 -0
- package/parser/policy_parser.js +463 -0
- package/parser/register.js +12 -0
- package/server.js +6 -0
- package/utils.js +4 -2
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import { CommentToken } from "../lexer/comment.js";
|
|
2
|
+
import { EqualToken, LCurly, LSquare, Minus, RCurly } from "../lexer/control.js";
|
|
3
|
+
import {
|
|
4
|
+
FloatLiteral,
|
|
5
|
+
IntegerLiteral,
|
|
6
|
+
SingleQuotedStringLiteral,
|
|
7
|
+
StringLiteral,
|
|
8
|
+
} from "../lexer/literal.js";
|
|
9
|
+
import { PolicyToken } from "../lexer/policy.js";
|
|
10
|
+
import { DotToken, Identifier, NewlineToken } from "../lexer/tokens.js";
|
|
11
|
+
import { getPolicyCatalogue } from "./policy/catalogue.js";
|
|
12
|
+
import { policyStringValue, readPolicyLiteral } from "./policy/policyLiteral.js";
|
|
13
|
+
import { didYouMean, validatePolicyRule } from "./policy/validatePolicyRule.js";
|
|
14
|
+
|
|
15
|
+
// Grammar for a `policy <key> { ... }` document.
|
|
16
|
+
//
|
|
17
|
+
// The language server knows the block SHAPE, as it knows every other object type's:
|
|
18
|
+
//
|
|
19
|
+
// policy <key> {
|
|
20
|
+
// <field> = <literal> # one per line
|
|
21
|
+
// rule { <field> = <literal> } # repeating; `check` names a catalogue item, `params` its params
|
|
22
|
+
// }
|
|
23
|
+
//
|
|
24
|
+
// with the comment, named-rule and literal-value rules of cloud-client's Script.php. It knows no
|
|
25
|
+
// field: which fields a policy and a rule may set, their types, value sets and defaults, the retired
|
|
26
|
+
// fields and reserved blocks, the key pattern and when a rule is required all come from the
|
|
27
|
+
// catalogue's `document` (helper/mvp/policy/Document.php), and every check from its `items`. With no
|
|
28
|
+
// catalogue loaded, only the shape is checked. See parser/policy/catalogue.js.
|
|
29
|
+
|
|
30
|
+
/** The platform's refusal of a named rule, after its `rule[<i>]: ` prefix. */
|
|
31
|
+
const RULE_NAMED =
|
|
32
|
+
'A rule cannot be named. Write "rule {" — rules are identified by position (KEY.R1, KEY.R2…).';
|
|
33
|
+
|
|
34
|
+
/** The platform's refusal of a comment inside a policy block, naming the comment's line. */
|
|
35
|
+
const COMMENT_REFUSED = (kind, line) =>
|
|
36
|
+
`line ${line}: policy files cannot contain "${kind}" comments. Put the explanation in the policy's statement, rationale or narrative.`;
|
|
37
|
+
|
|
38
|
+
/** `sprintf` for the platform's `%s` / `%d` sentences; the value is inserted as it is. */
|
|
39
|
+
const fill = (message, value) => String(message).replace(/%[sd]/, () => String(value));
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* `<key>` after `policy`: a quoted string, or letters, digits, ".", "_" and "-" written together,
|
|
43
|
+
* which the lexer splits into several tokens (`AUTH-001` is `AUTH`, `-`, `001`).
|
|
44
|
+
* @param {import('./base_parser.js').XanoBaseParser} $
|
|
45
|
+
*/
|
|
46
|
+
export function policyKeyFn($) {
|
|
47
|
+
return (tokens) => {
|
|
48
|
+
const add = (token) => $.ACTION(() => tokens.push(token));
|
|
49
|
+
$.OR([
|
|
50
|
+
{ ALT: () => add($.CONSUME(StringLiteral)) },
|
|
51
|
+
{ ALT: () => add($.CONSUME(SingleQuotedStringLiteral)) },
|
|
52
|
+
{
|
|
53
|
+
ALT: () => {
|
|
54
|
+
$.AT_LEAST_ONE(() => {
|
|
55
|
+
add(
|
|
56
|
+
$.OR1([
|
|
57
|
+
{ ALT: () => $.CONSUME(Identifier) },
|
|
58
|
+
{ ALT: () => $.CONSUME(IntegerLiteral) },
|
|
59
|
+
{ ALT: () => $.CONSUME(FloatLiteral) },
|
|
60
|
+
{ ALT: () => $.CONSUME(Minus) },
|
|
61
|
+
{ ALT: () => $.CONSUME(DotToken) },
|
|
62
|
+
])
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* `name = value`, in a policy or a rule. Any value expression parses here; what the field may hold
|
|
73
|
+
* (a literal of its declared type) is checked once the block is read.
|
|
74
|
+
* @param {import('./base_parser.js').XanoBaseParser} $
|
|
75
|
+
*/
|
|
76
|
+
export function policyFieldFn($) {
|
|
77
|
+
return (fields) => {
|
|
78
|
+
const keyToken = $.CONSUME(Identifier);
|
|
79
|
+
$.CONSUME(EqualToken); // "="
|
|
80
|
+
const first = $.ACTION(() => $.LA(1));
|
|
81
|
+
$.SUBRULE($.schemaFn, { ARGS: [keyToken, "[expression]"] });
|
|
82
|
+
$.ACTION(() => {
|
|
83
|
+
fields.push({ name: keyToken.image, keyToken, first, last: $.LA(0) });
|
|
84
|
+
const next = $.LA(1);
|
|
85
|
+
if (next.tokenType !== NewlineToken && next.image !== "}" && next.startLine === $.LA(0).endLine) {
|
|
86
|
+
$.addInvalidValueError(next, "Policy fields must be separated by a newline.");
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* `{ ... }` of a rule (or of a block that is not one, read the same way so the rest parses): fields
|
|
94
|
+
* only, one per line.
|
|
95
|
+
* @param {import('./base_parser.js').XanoBaseParser} $
|
|
96
|
+
*/
|
|
97
|
+
export function policyBlockBodyFn($) {
|
|
98
|
+
return (block) => {
|
|
99
|
+
$.CONSUME(LCurly); // "{"
|
|
100
|
+
// Native blocks also allow compact, same-line fields.
|
|
101
|
+
$.MANY(() => {
|
|
102
|
+
$.OR([
|
|
103
|
+
{ ALT: () => $.CONSUME(NewlineToken) },
|
|
104
|
+
{
|
|
105
|
+
ALT: () => {
|
|
106
|
+
const comment = $.CONSUME(CommentToken);
|
|
107
|
+
$.ACTION(() => block.comments.push(comment));
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{ ALT: () => $.SUBRULE($.policyFieldFn, { ARGS: [block?.fields] }) },
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
$.CONSUME(RCurly); // "}"
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* `rule { ... }`, and anything else written as `<word> [name] { ... }` inside a policy (`guard`,
|
|
119
|
+
* `test`, a misplaced `remediation { }`): read alike, told apart when the policy is checked. A name
|
|
120
|
+
* is read the way a key is (a quoted string, or a bare run such as `1`, `a.b` or `x-2`), since the
|
|
121
|
+
* platform refuses each of them as a name.
|
|
122
|
+
* @param {import('./base_parser.js').XanoBaseParser} $
|
|
123
|
+
*/
|
|
124
|
+
export function policyBlockFn($) {
|
|
125
|
+
return (blocks) => {
|
|
126
|
+
const block = { token: null, nameToken: null, fields: [], comments: [] };
|
|
127
|
+
const nameTokens = [];
|
|
128
|
+
block.token = $.CONSUME(Identifier); // "rule"
|
|
129
|
+
$.OPTION(() => $.SUBRULE($.policyKeyFn, { ARGS: [nameTokens] }));
|
|
130
|
+
$.ACTION(() => {
|
|
131
|
+
block.nameToken = nameTokens[0] ?? null;
|
|
132
|
+
});
|
|
133
|
+
$.SUBRULE($.policyBlockBodyFn, { ARGS: [block] });
|
|
134
|
+
$.ACTION(() => blocks.push(block));
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {import('./base_parser.js').XanoBaseParser} $
|
|
140
|
+
*/
|
|
141
|
+
export function policyDeclaration($) {
|
|
142
|
+
return () => {
|
|
143
|
+
const fields = [];
|
|
144
|
+
const blocks = [];
|
|
145
|
+
const comments = [];
|
|
146
|
+
|
|
147
|
+
$.sectionStack.push("policyDeclaration");
|
|
148
|
+
// Comments before the block are formatting; inside it they are refused.
|
|
149
|
+
$.MANY(() => $.OR([
|
|
150
|
+
{ ALT: () => $.CONSUME(NewlineToken) },
|
|
151
|
+
{ ALT: () => $.CONSUME(CommentToken) },
|
|
152
|
+
]));
|
|
153
|
+
const policyToken = $.CONSUME(PolicyToken); // "policy"
|
|
154
|
+
const keyTokens = [];
|
|
155
|
+
$.SUBRULE($.policyKeyFn, { ARGS: [keyTokens] });
|
|
156
|
+
$.CONSUME(LCurly); // "{"
|
|
157
|
+
$.MANY1(() => {
|
|
158
|
+
$.OR1([
|
|
159
|
+
{ ALT: () => $.CONSUME1(NewlineToken) },
|
|
160
|
+
{
|
|
161
|
+
ALT: () => {
|
|
162
|
+
const comment = $.CONSUME1(CommentToken);
|
|
163
|
+
$.ACTION(() => comments.push(comment));
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{ ALT: () => $.SUBRULE($.policyFieldFn, { ARGS: [fields] }) },
|
|
167
|
+
{ ALT: () => $.SUBRULE($.policyBlockFn, { ARGS: [blocks] }) },
|
|
168
|
+
]);
|
|
169
|
+
});
|
|
170
|
+
$.CONSUME(RCurly); // "}"
|
|
171
|
+
$.MANY2(() => $.OR2([
|
|
172
|
+
{ ALT: () => $.CONSUME2(NewlineToken) },
|
|
173
|
+
{ ALT: () => $.CONSUME2(CommentToken) },
|
|
174
|
+
]));
|
|
175
|
+
|
|
176
|
+
$.ACTION(() =>
|
|
177
|
+
checkPolicy($, { policyToken, keyTokens, fields, blocks, comments })
|
|
178
|
+
);
|
|
179
|
+
$.sectionStack.pop();
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The comment starting at `tokens[i]`: an own-line `//` (a comment token), or a `//` or `/*` the lexer
|
|
185
|
+
* read as divisions because something precedes it on the line. With `slashOnly`, only the latter.
|
|
186
|
+
*/
|
|
187
|
+
function commentAt(tokens, i, slashOnly = false) {
|
|
188
|
+
const token = tokens[i];
|
|
189
|
+
if (!token) return null;
|
|
190
|
+
if (token.tokenType === CommentToken) return slashOnly ? null : { token, kind: "//" };
|
|
191
|
+
const next = tokens[i + 1];
|
|
192
|
+
if (token.image === "/" && next && token.endOffset + 1 === next.startOffset && (next.image === "/" || next.image === "*")) {
|
|
193
|
+
return { token, kind: next.image === "/" ? "//" : "/* */" };
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* A comment the grammar cannot place (inside `params` or a list, after a value on the same line, or
|
|
200
|
+
* `/* *\/`) stops the parse; say what the platform says about it, at the comment, instead of
|
|
201
|
+
* "Expecting }".
|
|
202
|
+
* @param {import('./base_parser.js').XanoBaseParser} parser after `policyDeclaration()`
|
|
203
|
+
*/
|
|
204
|
+
export function explainPolicySyntaxErrors(parser) {
|
|
205
|
+
const tokens = parser.input;
|
|
206
|
+
for (const error of parser.errors) {
|
|
207
|
+
let at = tokens.indexOf(error.token);
|
|
208
|
+
if (at < 0) continue;
|
|
209
|
+
// A `{` or `[` the parse could not read past: a comment may be the first thing inside it.
|
|
210
|
+
if (tokens[at].tokenType === LCurly || tokens[at].tokenType === LSquare) {
|
|
211
|
+
let next = at + 1;
|
|
212
|
+
while (tokens[next]?.tokenType === NewlineToken) next++;
|
|
213
|
+
if (commentAt(tokens, next)) at = next;
|
|
214
|
+
}
|
|
215
|
+
// The parse stops at the comment, or at its second character after reading a `/` as a division.
|
|
216
|
+
const comment = commentAt(tokens, at) ?? commentAt(tokens, at - 1, true);
|
|
217
|
+
if (comment) {
|
|
218
|
+
error.token = comment.token;
|
|
219
|
+
error.message = COMMENT_REFUSED(comment.kind, comment.token.startLine);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Report through the parser. What the block's shape decides is an error. What the catalogue decides
|
|
226
|
+
* is an error against the instance's own (live) catalogue, and a dated warning against the snapshot
|
|
227
|
+
* shipped with the language server, which may be older than the instance; a finding that mirrors
|
|
228
|
+
* platform logic beyond the published schema (`advisory`) is a warning either way.
|
|
229
|
+
*/
|
|
230
|
+
function reporter($, catalogue) {
|
|
231
|
+
const error = (token, message) => $.addInvalidValueError(token, message);
|
|
232
|
+
const warning = (token, message) => $.addWarning(message, token);
|
|
233
|
+
const dated = catalogue.generatedAt ? ` of ${catalogue.generatedAt}` : "";
|
|
234
|
+
return {
|
|
235
|
+
error,
|
|
236
|
+
catalogue: (token, message, advisory = false) => {
|
|
237
|
+
if (catalogue.source === "live" && !advisory) {
|
|
238
|
+
error(token, message);
|
|
239
|
+
} else if (catalogue.source === "snapshot") {
|
|
240
|
+
warning(token, `${message} (checked against the language server's check catalogue${dated}; your instance decides on save)`);
|
|
241
|
+
} else {
|
|
242
|
+
warning(token, message);
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** A field's value as literals with their tokens, or null when it has none. */
|
|
249
|
+
function valueOf(input, field) {
|
|
250
|
+
if (!field?.first || !field?.last) return null;
|
|
251
|
+
return readPolicyLiteral(input, input.indexOf(field.first), input.indexOf(field.last));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Whether a field's literal value has the type the document declares (`string`, `string[]`,
|
|
256
|
+
* `bool`, `object`) and one of its `values`; the sentence when not.
|
|
257
|
+
*/
|
|
258
|
+
function fieldProblem(name, node, spec) {
|
|
259
|
+
const type = spec?.type ?? "string";
|
|
260
|
+
if (type === "string[]") {
|
|
261
|
+
if (node.kind !== "array" || node.items.some((item) => item?.kind !== "string")) {
|
|
262
|
+
return { token: node.token, message: `${name} must be a list of strings.` };
|
|
263
|
+
}
|
|
264
|
+
const max = spec.max_length;
|
|
265
|
+
const long = max ? node.items.findIndex((item) => [...item.value.trim()].length > max) : -1;
|
|
266
|
+
if (long >= 0) {
|
|
267
|
+
return { token: node.items[long].token, message: `${name}[${long}] must be ${max} characters or fewer.` };
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
if (type === "bool") {
|
|
272
|
+
return node.kind === "boolean" ? null : { token: node.token, message: `${name} must be true or false.` };
|
|
273
|
+
}
|
|
274
|
+
if (type === "object") {
|
|
275
|
+
return node.kind === "object" ? null : { token: node.token, message: `${name} must be an object.` };
|
|
276
|
+
}
|
|
277
|
+
if (node.kind !== "string") {
|
|
278
|
+
return { token: node.token, message: `${name} must be a string.` };
|
|
279
|
+
}
|
|
280
|
+
if (spec?.values?.length && !spec.values.includes(node.value.trim())) {
|
|
281
|
+
return {
|
|
282
|
+
token: node.token,
|
|
283
|
+
message: `${name} must be one of: ${spec.values.join(", ")} (got "${node.value.trim()}").`,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* A rule's params: an object of literal values (Script::literal; a name written twice is already
|
|
291
|
+
* reported by the object grammar). The catalogue then says what the object may hold.
|
|
292
|
+
*/
|
|
293
|
+
function checkParamsShape(node, report) {
|
|
294
|
+
if (!node) return;
|
|
295
|
+
// The native transform normalizes null and an empty list to empty params.
|
|
296
|
+
if (node.kind === "null" || (node.kind === "array" && node.items.length === 0)) return;
|
|
297
|
+
if (node.kind !== "object") {
|
|
298
|
+
report.error(
|
|
299
|
+
node.token,
|
|
300
|
+
node.kind === "expression"
|
|
301
|
+
? "Policy parameters must be literal values."
|
|
302
|
+
: 'params must be an object, e.g. params = { tags: ["pci"] }.'
|
|
303
|
+
);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const visit = (value) => {
|
|
307
|
+
if (!value) return;
|
|
308
|
+
if (value.kind === "expression") {
|
|
309
|
+
report.error(value.token, "Policy parameters must be literal values.");
|
|
310
|
+
} else if (value.kind === "object") {
|
|
311
|
+
value.entries.forEach((entry) => visit(entry.value));
|
|
312
|
+
} else if (value.kind === "array") {
|
|
313
|
+
value.items.forEach(visit);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
visit(node);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* One block's fields (a policy's or a rule's): each once, a literal, and, when the catalogue
|
|
321
|
+
* describes the document, one it declares, of its type. `retired` and `where` shape the sentences.
|
|
322
|
+
* @returns {Object<string, {field: Object, node: Object|null}>} the fields by name
|
|
323
|
+
*/
|
|
324
|
+
function checkFields(fields, { input, report, specs, retired, where, extra = [], index }) {
|
|
325
|
+
const out = Object.create(null);
|
|
326
|
+
const names = specs ? [...Object.keys(specs), ...extra] : [];
|
|
327
|
+
for (const field of fields) {
|
|
328
|
+
const { name, keyToken } = field;
|
|
329
|
+
if (retired && Object.hasOwn(retired, name)) {
|
|
330
|
+
report.catalogue(keyToken, fill(retired[name], index));
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (specs && !Object.hasOwn(specs, name)) {
|
|
334
|
+
report.catalogue(
|
|
335
|
+
keyToken,
|
|
336
|
+
`${where}: unknown field "${name}". ${didYouMean(name, names)}${where === "policy" ? "Policy" : "Rule"} fields: ${names.join(", ")}.`
|
|
337
|
+
);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (name in out) {
|
|
341
|
+
report.error(keyToken, `${where}: "${name}" is set more than once.`);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const node = valueOf(input, field);
|
|
345
|
+
out[name] = { field, node };
|
|
346
|
+
// `params` is the check's own object; checkParamsShape and the catalogue read it.
|
|
347
|
+
if (!node || name === "params") continue;
|
|
348
|
+
if (node.kind === "expression") {
|
|
349
|
+
report.error(node.token, `${name} must be a literal value.`);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const problem = specs ? fieldProblem(name, node, specs[name]) : null;
|
|
353
|
+
if (problem) report.catalogue(problem.token, problem.message);
|
|
354
|
+
}
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Everything the platform refuses that the block decides, then each rule against the catalogue.
|
|
360
|
+
*/
|
|
361
|
+
function checkPolicy($, { policyToken, keyTokens, fields, blocks, comments }) {
|
|
362
|
+
const catalogue = getPolicyCatalogue();
|
|
363
|
+
const document = catalogue.document;
|
|
364
|
+
const report = reporter($, catalogue);
|
|
365
|
+
const input = $.input;
|
|
366
|
+
|
|
367
|
+
for (const comment of comments) report.error(comment, COMMENT_REFUSED("//", comment.startLine));
|
|
368
|
+
|
|
369
|
+
// The key, as the platform reads it: the text from its first token to its last.
|
|
370
|
+
const keyToken = keyTokens[0] ?? policyToken;
|
|
371
|
+
const contiguous = keyTokens.every(
|
|
372
|
+
(token, i) => i === 0 || keyTokens[i - 1].endOffset + 1 === token.startOffset
|
|
373
|
+
);
|
|
374
|
+
const key =
|
|
375
|
+
keyTokens.length === 1 && /^["']/.test(keyTokens[0].image)
|
|
376
|
+
? policyStringValue(keyTokens[0].image).trim()
|
|
377
|
+
: contiguous
|
|
378
|
+
? keyTokens.map((token) => token.image).join("")
|
|
379
|
+
: `${keyTokens[0].image}…${keyTokens[keyTokens.length - 1].image}`;
|
|
380
|
+
if (document?.key?.pattern) {
|
|
381
|
+
let pattern = null;
|
|
382
|
+
try {
|
|
383
|
+
pattern = new RegExp(document.key.pattern);
|
|
384
|
+
} catch {
|
|
385
|
+
pattern = null;
|
|
386
|
+
}
|
|
387
|
+
if (pattern && !pattern.test(key)) report.catalogue(keyToken, fill(document.key.message, key));
|
|
388
|
+
} else if (!contiguous) {
|
|
389
|
+
report.error(keyToken, `Policy key "${key}" must be written without spaces.`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const values = checkFields(fields, {
|
|
393
|
+
input,
|
|
394
|
+
report,
|
|
395
|
+
specs: document?.fields,
|
|
396
|
+
retired: document?.retired?.policy,
|
|
397
|
+
where: "policy",
|
|
398
|
+
extra: ["rule"],
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Blocks: rules, and blocks a policy does not have.
|
|
402
|
+
const reserved = document?.reserved_blocks;
|
|
403
|
+
let ruleIndex = 0;
|
|
404
|
+
for (const block of blocks) {
|
|
405
|
+
const word = block.token.image;
|
|
406
|
+
for (const comment of block.comments) report.error(comment, COMMENT_REFUSED("//", comment.startLine));
|
|
407
|
+
|
|
408
|
+
if (word !== "rule") {
|
|
409
|
+
if (reserved?.names?.includes(word)) {
|
|
410
|
+
report.catalogue(block.token, fill(reserved.message, word));
|
|
411
|
+
} else if (document?.retired?.policy && Object.hasOwn(document.retired.policy, word)) {
|
|
412
|
+
report.catalogue(block.token, document.retired.policy[word]);
|
|
413
|
+
} else {
|
|
414
|
+
report.error(block.token, `policy: unknown block "${word}". ${didYouMean(word, ["rule"])}Policies support rule blocks only.`);
|
|
415
|
+
}
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const index = ruleIndex++;
|
|
420
|
+
if (block.nameToken) report.error(block.nameToken, `rule[${index}]: ${RULE_NAMED}`);
|
|
421
|
+
|
|
422
|
+
const rule = checkFields(block.fields, {
|
|
423
|
+
input,
|
|
424
|
+
report,
|
|
425
|
+
specs: document?.rule,
|
|
426
|
+
retired: document?.retired?.rule,
|
|
427
|
+
where: `rule[${index}]`,
|
|
428
|
+
index,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
checkParamsShape(rule.params?.node, report);
|
|
432
|
+
if (!rule.check) {
|
|
433
|
+
if (document?.rule?.check?.required) {
|
|
434
|
+
report.catalogue(block.token, `rule[${index}]: "check" is required. Name a check from the catalogue: check = "<check id>".`);
|
|
435
|
+
}
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const checkNode = rule.check.node;
|
|
440
|
+
const problems = validatePolicyRule(
|
|
441
|
+
{
|
|
442
|
+
checkId: checkNode?.kind === "string" ? checkNode.value.trim() : null,
|
|
443
|
+
checkToken: checkNode?.token ?? null,
|
|
444
|
+
paramsToken: rule.params?.field.keyToken ?? null,
|
|
445
|
+
params: rule.params?.node ?? null,
|
|
446
|
+
ruleToken: block.token,
|
|
447
|
+
},
|
|
448
|
+
catalogue
|
|
449
|
+
);
|
|
450
|
+
for (const problem of problems) report.catalogue(problem.token, problem.message, problem.advisory);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// A policy with no rule: allowed only for the field values the document names.
|
|
454
|
+
const required = document?.rules_required;
|
|
455
|
+
if (required && ruleIndex === 0) {
|
|
456
|
+
const allowed = Object.entries(required.unless ?? {}).some(([name, value]) => {
|
|
457
|
+
const node = values[name]?.node;
|
|
458
|
+
const inForce = node && node.kind !== "expression" ? node.value : document.fields?.[name]?.default;
|
|
459
|
+
return inForce === value;
|
|
460
|
+
});
|
|
461
|
+
if (!allowed) report.catalogue(keyToken, required.message);
|
|
462
|
+
}
|
|
463
|
+
}
|
package/parser/register.js
CHANGED
|
@@ -31,6 +31,13 @@ import {
|
|
|
31
31
|
microserviceVolumeClause,
|
|
32
32
|
} from "./microservice_parser.js";
|
|
33
33
|
import { middlewareDeclaration } from "./middleware_parser.js";
|
|
34
|
+
import {
|
|
35
|
+
policyBlockBodyFn,
|
|
36
|
+
policyBlockFn,
|
|
37
|
+
policyDeclaration,
|
|
38
|
+
policyFieldFn,
|
|
39
|
+
policyKeyFn,
|
|
40
|
+
} from "./policy_parser.js";
|
|
34
41
|
import { queryDeclaration } from "./query_parser.js";
|
|
35
42
|
import { realtimeChannelDeclaration } from "./realtime_channel_parser.js";
|
|
36
43
|
import { realtimeServerDeclaration } from "./realtime_server_parser.js";
|
|
@@ -126,6 +133,11 @@ export const register = ($) => {
|
|
|
126
133
|
"microserviceChartClause",
|
|
127
134
|
microserviceChartClause($)
|
|
128
135
|
);
|
|
136
|
+
$.policyKeyFn = $.RULE("policyKeyFn", policyKeyFn($));
|
|
137
|
+
$.policyFieldFn = $.RULE("policyFieldFn", policyFieldFn($));
|
|
138
|
+
$.policyBlockBodyFn = $.RULE("policyBlockBodyFn", policyBlockBodyFn($));
|
|
139
|
+
$.policyBlockFn = $.RULE("policyBlockFn", policyBlockFn($));
|
|
140
|
+
$.policyDeclaration = $.RULE("policyDeclaration", policyDeclaration($));
|
|
129
141
|
$.middlewareDeclaration = $.RULE(
|
|
130
142
|
"middlewareDeclaration",
|
|
131
143
|
middlewareDeclaration($)
|
package/server.js
CHANGED
|
@@ -17,6 +17,8 @@ import { onHover } from "./onHover/onHover.js";
|
|
|
17
17
|
import { onSemanticCheck } from "./onSemanticCheck/onSemanticCheck.js";
|
|
18
18
|
import { TOKEN_TYPES } from "./onSemanticCheck/tokens.js";
|
|
19
19
|
import { xanoscriptParser } from "./parser/parser.js";
|
|
20
|
+
import { setPolicyCatalogue } from "./parser/policy/catalogue.js";
|
|
21
|
+
import policyCatalogueSnapshot from "./parser/policy/catalogueSnapshot.js";
|
|
20
22
|
import { getSchemeFromContent } from "./utils.js";
|
|
21
23
|
import { workspaceIndex } from "./workspace/workspaceIndex.js";
|
|
22
24
|
|
|
@@ -34,6 +36,10 @@ const FEATURES = {
|
|
|
34
36
|
variableValidation: true, // variable usage validation
|
|
35
37
|
};
|
|
36
38
|
|
|
39
|
+
// Policy rules are checked against the check catalogue this package was built with (see
|
|
40
|
+
// scripts/gen-policy-catalogue.mjs); what it finds is a warning, since the instance may be newer.
|
|
41
|
+
setPolicyCatalogue(policyCatalogueSnapshot, { source: "snapshot" });
|
|
42
|
+
|
|
37
43
|
// Create a connection to the VS Code client
|
|
38
44
|
const connection = createConnection(ProposedFeatures.all);
|
|
39
45
|
|
package/utils.js
CHANGED
|
@@ -32,6 +32,7 @@ const schemeByFirstWord = {
|
|
|
32
32
|
message: "message",
|
|
33
33
|
microservice: "microservice",
|
|
34
34
|
middleware: "middleware",
|
|
35
|
+
policy: "policy",
|
|
35
36
|
prompt: "prompt",
|
|
36
37
|
query: "api",
|
|
37
38
|
realtime_server: "realtime_server",
|
|
@@ -72,8 +73,9 @@ const typeByScheme = {
|
|
|
72
73
|
// All other schemes map 1:1 (addon, agent, task, etc.)
|
|
73
74
|
};
|
|
74
75
|
|
|
75
|
-
// Extends firstWordRegex to also capture the name (quoted or bare identifier
|
|
76
|
-
|
|
76
|
+
// Extends firstWordRegex to also capture the name (quoted or bare identifier; a bare policy key
|
|
77
|
+
// may also hold "." and "-", e.g. `policy AUTH-001 {`)
|
|
78
|
+
const objectInfoRegex = /^(?:\s|\/\/[^\n]*\n)*(\w+)\s+(?:"([^"]+)"|(\w[\w.-]*))/;
|
|
77
79
|
|
|
78
80
|
/**
|
|
79
81
|
* Extract the object type and name from XanoScript file content.
|