@player-tools/json-language-service 0.13.0-next.3 → 0.13.0-next.5
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/cjs/index.cjs +440 -35
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.legacy-esm.js +430 -26
- package/dist/index.mjs +430 -26
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/__snapshots__/service.test.ts.snap +5 -15
- package/src/__tests__/service.test.ts +1 -28
- package/src/constants.ts +2 -0
- package/src/plugins/__tests__/asset-wrapper-array-plugin.test.ts +2 -2
- package/src/plugins/__tests__/missing-asset-wrapper-plugin.test.ts +2 -2
- package/src/plugins/__tests__/schema-validation-plugin.test.ts +1923 -0
- package/src/plugins/schema-validation-plugin.ts +508 -0
- package/src/plugins/xlr-plugin.ts +2 -21
- package/src/utils.ts +20 -0
- package/src/xlr/registry.ts +6 -1
- package/types/plugins/schema-validation-plugin.d.ts +12 -0
- package/types/plugins/xlr-plugin.d.ts +7 -0
- package/types/utils.d.ts +3 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import { DiagnosticSeverity } from "vscode-languageserver-types";
|
|
2
|
+
import type { PlayerLanguageService, PlayerLanguageServicePlugin } from "..";
|
|
3
|
+
import type { ValidationContext, ASTVisitor } from "../types";
|
|
4
|
+
import type { ContentASTNode, ObjectASTNode, StringASTNode } from "../parser";
|
|
5
|
+
import { getProperty } from "../utils";
|
|
6
|
+
import type { ValidationMessage, XLRSDK } from "@player-tools/xlr-sdk";
|
|
7
|
+
import { NamedType, ObjectType, OrType, RefType } from "@player-tools/xlr";
|
|
8
|
+
import { translateSeverity } from "./xlr-plugin";
|
|
9
|
+
import { isObjectType, isPrimitiveTypeNode } from "@player-tools/xlr-utils";
|
|
10
|
+
|
|
11
|
+
function formatErrorMessage(message: string): string {
|
|
12
|
+
return `Schema Validation Error: ${message}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function makeValidationRefObject(
|
|
16
|
+
baseObject: ObjectType,
|
|
17
|
+
validationFunction: ObjectType,
|
|
18
|
+
): ObjectType {
|
|
19
|
+
return {
|
|
20
|
+
...baseObject,
|
|
21
|
+
properties: {
|
|
22
|
+
...baseObject.properties,
|
|
23
|
+
...validationFunction.properties,
|
|
24
|
+
},
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
} as ObjectType;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validate that all claimed validations are registered and pass the correct props
|
|
31
|
+
*/
|
|
32
|
+
function validateSchemaValidations(
|
|
33
|
+
validationNode: ObjectASTNode,
|
|
34
|
+
sdk: XLRSDK,
|
|
35
|
+
validationContext: ValidationContext,
|
|
36
|
+
) {
|
|
37
|
+
const claimedValidator = getProperty(validationNode, "type");
|
|
38
|
+
if (!claimedValidator) {
|
|
39
|
+
validationContext.addViolation({
|
|
40
|
+
node: validationNode,
|
|
41
|
+
message: formatErrorMessage('Validation object missing "type" property'),
|
|
42
|
+
severity: DiagnosticSeverity.Error,
|
|
43
|
+
});
|
|
44
|
+
} else if (claimedValidator.valueNode?.type !== "string") {
|
|
45
|
+
validationContext.addViolation({
|
|
46
|
+
node: claimedValidator.valueNode ?? validationNode,
|
|
47
|
+
message: formatErrorMessage("Validation type must be a string"),
|
|
48
|
+
severity: DiagnosticSeverity.Error,
|
|
49
|
+
});
|
|
50
|
+
} else {
|
|
51
|
+
const validationXLR = sdk.getType(claimedValidator.valueNode.value, {
|
|
52
|
+
getRawType: true,
|
|
53
|
+
}) as NamedType<RefType>;
|
|
54
|
+
if (!validationXLR) {
|
|
55
|
+
validationContext.addViolation({
|
|
56
|
+
node: validationNode,
|
|
57
|
+
message: formatErrorMessage(
|
|
58
|
+
`Validation Function ${claimedValidator} is not a registered validator`,
|
|
59
|
+
),
|
|
60
|
+
severity: DiagnosticSeverity.Error,
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
const valRef = sdk.getType("Validation.Reference", {
|
|
64
|
+
getRawType: true,
|
|
65
|
+
}) as NamedType<ObjectType> | undefined;
|
|
66
|
+
if (valRef) {
|
|
67
|
+
let validationIssues: ValidationMessage[];
|
|
68
|
+
const validatorFunctionProps = validationXLR.genericArguments?.[0] as
|
|
69
|
+
| ObjectType
|
|
70
|
+
| OrType
|
|
71
|
+
| undefined;
|
|
72
|
+
|
|
73
|
+
if (!validatorFunctionProps || isObjectType(validatorFunctionProps)) {
|
|
74
|
+
validationIssues = sdk.validateByType(
|
|
75
|
+
makeValidationRefObject(
|
|
76
|
+
valRef,
|
|
77
|
+
validatorFunctionProps ?? ({} as ObjectType),
|
|
78
|
+
),
|
|
79
|
+
validationNode.jsonNode,
|
|
80
|
+
);
|
|
81
|
+
validationIssues.forEach((issue) => {
|
|
82
|
+
validationContext.addViolation({
|
|
83
|
+
node: validationNode,
|
|
84
|
+
message: formatErrorMessage(issue.message),
|
|
85
|
+
severity: translateSeverity(issue.severity),
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
// need to make sure that only one of the arg groups is right
|
|
90
|
+
const validationResults = validatorFunctionProps.or
|
|
91
|
+
.map((node) => {
|
|
92
|
+
if (isObjectType(node)) {
|
|
93
|
+
return sdk.validateByType(
|
|
94
|
+
makeValidationRefObject(valRef, node),
|
|
95
|
+
validationNode.jsonNode,
|
|
96
|
+
);
|
|
97
|
+
} else {
|
|
98
|
+
validationIssues.forEach((issue) => {
|
|
99
|
+
validationContext.addViolation({
|
|
100
|
+
node: validationNode,
|
|
101
|
+
message: formatErrorMessage(
|
|
102
|
+
`Internal Error - Validation function ${validationXLR.name} type argument is not an object`,
|
|
103
|
+
),
|
|
104
|
+
severity: DiagnosticSeverity.Error,
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
.filter((o) => o !== null && Array.isArray(o) && o.length === 0);
|
|
111
|
+
|
|
112
|
+
if (validationResults.length !== 1) {
|
|
113
|
+
validationContext.addViolation({
|
|
114
|
+
node: validationNode,
|
|
115
|
+
message: formatErrorMessage(
|
|
116
|
+
`Validation function invalid function parameters for type ${validationXLR.name}`,
|
|
117
|
+
),
|
|
118
|
+
severity: DiagnosticSeverity.Error,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
validationContext.addViolation({
|
|
124
|
+
node: validationNode,
|
|
125
|
+
message: formatErrorMessage(
|
|
126
|
+
"Validation.Reference from @player-ui/types is not loaded into SDK",
|
|
127
|
+
),
|
|
128
|
+
severity: DiagnosticSeverity.Error,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Validate that the format function is registered and passes the correct props
|
|
137
|
+
*/
|
|
138
|
+
function validateSchemaFormat(
|
|
139
|
+
formatNode: ObjectASTNode,
|
|
140
|
+
sdk: XLRSDK,
|
|
141
|
+
validationContext: ValidationContext,
|
|
142
|
+
) {
|
|
143
|
+
const claimedFormatter = getProperty(formatNode, "type");
|
|
144
|
+
if (!claimedFormatter) {
|
|
145
|
+
validationContext.addViolation({
|
|
146
|
+
node: formatNode,
|
|
147
|
+
message: formatErrorMessage('Format object missing "type" property'),
|
|
148
|
+
severity: DiagnosticSeverity.Error,
|
|
149
|
+
});
|
|
150
|
+
} else if (claimedFormatter.valueNode?.type !== "string") {
|
|
151
|
+
validationContext.addViolation({
|
|
152
|
+
node: claimedFormatter.valueNode ?? claimedFormatter,
|
|
153
|
+
message: formatErrorMessage("Format type must be a string"),
|
|
154
|
+
severity: DiagnosticSeverity.Error,
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
const formatterXLR = sdk.getType(claimedFormatter.valueNode.value, {
|
|
158
|
+
getRawType: true,
|
|
159
|
+
}) as RefType;
|
|
160
|
+
if (!formatterXLR) {
|
|
161
|
+
validationContext.addViolation({
|
|
162
|
+
node: formatNode,
|
|
163
|
+
message: formatErrorMessage(
|
|
164
|
+
`Formatter ${claimedFormatter} is not a registered formatter`,
|
|
165
|
+
),
|
|
166
|
+
severity: DiagnosticSeverity.Error,
|
|
167
|
+
});
|
|
168
|
+
} else if (
|
|
169
|
+
formatterXLR.genericArguments &&
|
|
170
|
+
formatterXLR.genericArguments.length === 3
|
|
171
|
+
) {
|
|
172
|
+
const otherArgsXLR = formatterXLR.genericArguments[2] as ObjectType;
|
|
173
|
+
const validationIssues = sdk.validateByType(
|
|
174
|
+
{
|
|
175
|
+
...otherArgsXLR,
|
|
176
|
+
properties: {
|
|
177
|
+
...otherArgsXLR.properties,
|
|
178
|
+
type: {
|
|
179
|
+
required: true,
|
|
180
|
+
node: {
|
|
181
|
+
type: "string",
|
|
182
|
+
const: claimedFormatter.valueNode.value,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
formatNode.jsonNode,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
validationIssues.forEach((issue) => {
|
|
191
|
+
validationContext.addViolation({
|
|
192
|
+
node: formatNode,
|
|
193
|
+
message: formatErrorMessage(issue.message),
|
|
194
|
+
severity: translateSeverity(issue.severity),
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Collects all type names defined at the top level of the schema (ROOT and
|
|
203
|
+
* any custom type names). Used to validate that "type" references either
|
|
204
|
+
* point to a schema type or an XLR-loaded type.
|
|
205
|
+
*/
|
|
206
|
+
function getSchemaTypeNames(schemaObj: ObjectASTNode): Set<string> {
|
|
207
|
+
const names = new Set<string>();
|
|
208
|
+
for (const prop of schemaObj.properties) {
|
|
209
|
+
const key = prop.keyNode?.value;
|
|
210
|
+
if (typeof key === "string") {
|
|
211
|
+
names.add(key);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return names;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Validates that a Schema.DataType object has the proper structure
|
|
219
|
+
*/
|
|
220
|
+
function validateDataTypeStructure(
|
|
221
|
+
dataTypeNode: ObjectASTNode,
|
|
222
|
+
claimedDataType: NamedType<ObjectType>,
|
|
223
|
+
sdk: XLRSDK,
|
|
224
|
+
validationContext: ValidationContext,
|
|
225
|
+
): void {
|
|
226
|
+
// Basic Structural Tests
|
|
227
|
+
const validationProp = getProperty(dataTypeNode, "validation");
|
|
228
|
+
if (validationProp?.valueNode && validationProp.valueNode.type !== "array") {
|
|
229
|
+
validationContext.addViolation({
|
|
230
|
+
node: validationProp.valueNode,
|
|
231
|
+
message: formatErrorMessage(
|
|
232
|
+
'Schema.DataType "validation" must be an array.',
|
|
233
|
+
),
|
|
234
|
+
severity: DiagnosticSeverity.Error,
|
|
235
|
+
});
|
|
236
|
+
} else if (validationProp?.valueNode) {
|
|
237
|
+
validationProp.valueNode.children?.forEach((valRef) => {
|
|
238
|
+
if (valRef && valRef.type === "object") {
|
|
239
|
+
validateSchemaValidations(valRef, sdk, validationContext);
|
|
240
|
+
} else {
|
|
241
|
+
validationContext.addViolation({
|
|
242
|
+
node: validationProp.valueNode ?? dataTypeNode,
|
|
243
|
+
message: formatErrorMessage(
|
|
244
|
+
'Schema.DataType "validation" must be an object.',
|
|
245
|
+
),
|
|
246
|
+
severity: DiagnosticSeverity.Error,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const formatProp = getProperty(dataTypeNode, "format");
|
|
253
|
+
if (formatProp?.valueNode?.type === "object") {
|
|
254
|
+
validateSchemaFormat(formatProp.valueNode, sdk, validationContext);
|
|
255
|
+
} else {
|
|
256
|
+
if (formatProp) {
|
|
257
|
+
validationContext.addViolation({
|
|
258
|
+
node: formatProp?.valueNode ?? dataTypeNode,
|
|
259
|
+
message: formatErrorMessage(
|
|
260
|
+
'Schema.DataType "format" must be an object.',
|
|
261
|
+
),
|
|
262
|
+
severity: DiagnosticSeverity.Error,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check if default value conforms to the expected value
|
|
268
|
+
const defaultNode = claimedDataType.properties?.["default"]?.node;
|
|
269
|
+
const defaultProp = getProperty(dataTypeNode, "default");
|
|
270
|
+
if (defaultNode && defaultProp?.valueNode) {
|
|
271
|
+
if (isPrimitiveTypeNode(defaultNode)) {
|
|
272
|
+
if (defaultProp.valueNode.type !== defaultNode.type) {
|
|
273
|
+
validationContext.addViolation({
|
|
274
|
+
node: defaultProp.valueNode,
|
|
275
|
+
message: formatErrorMessage(
|
|
276
|
+
`Default value doesn't match the expected type of ${defaultNode.type} for type ${claimedDataType.name}`,
|
|
277
|
+
),
|
|
278
|
+
severity: DiagnosticSeverity.Error,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
} else if (defaultNode.type === "or") {
|
|
282
|
+
if (!defaultNode.or.some((n) => n.type === defaultProp.valueNode?.type)) {
|
|
283
|
+
validationContext.addViolation({
|
|
284
|
+
node: defaultProp.valueNode,
|
|
285
|
+
message: formatErrorMessage(
|
|
286
|
+
`Default value doesn't match any of the expected types ${defaultNode.or.map((t) => t.type).join(", ")} for type ${claimedDataType.name}`,
|
|
287
|
+
),
|
|
288
|
+
severity: DiagnosticSeverity.Error,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
validationContext.addViolation({
|
|
293
|
+
node: defaultProp.valueNode,
|
|
294
|
+
message: formatErrorMessage(
|
|
295
|
+
`Unknown default node type ${defaultNode.type}`,
|
|
296
|
+
),
|
|
297
|
+
severity: DiagnosticSeverity.Error,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// RecordType/ArrayType Checks
|
|
303
|
+
const isArrayProp = getProperty(dataTypeNode, "isArray");
|
|
304
|
+
const isRecordProp = getProperty(dataTypeNode, "isRecord");
|
|
305
|
+
if (isArrayProp?.valueNode && isArrayProp.valueNode.type !== "boolean") {
|
|
306
|
+
validationContext.addViolation({
|
|
307
|
+
node: isArrayProp.valueNode,
|
|
308
|
+
message: formatErrorMessage(
|
|
309
|
+
'Schema.DataType "isArray" must be a boolean.',
|
|
310
|
+
),
|
|
311
|
+
severity: DiagnosticSeverity.Error,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (isRecordProp?.valueNode && isRecordProp.valueNode.type !== "boolean") {
|
|
315
|
+
validationContext.addViolation({
|
|
316
|
+
node: isRecordProp.valueNode,
|
|
317
|
+
message: formatErrorMessage(
|
|
318
|
+
'Schema.DataType "isRecord" must be a boolean.',
|
|
319
|
+
),
|
|
320
|
+
severity: DiagnosticSeverity.Error,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (
|
|
325
|
+
isArrayProp?.valueNode &&
|
|
326
|
+
isRecordProp?.valueNode &&
|
|
327
|
+
(isArrayProp.valueNode as { value?: boolean }).value === true &&
|
|
328
|
+
(isRecordProp.valueNode as { value?: boolean }).value === true
|
|
329
|
+
) {
|
|
330
|
+
validationContext.addViolation({
|
|
331
|
+
node: dataTypeNode,
|
|
332
|
+
message: formatErrorMessage(
|
|
333
|
+
'Schema.DataType cannot have both "isArray" and "isRecord" true.',
|
|
334
|
+
),
|
|
335
|
+
severity: DiagnosticSeverity.Error,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Validates a single schema node (e.g. ROOT or a custom type): each property
|
|
342
|
+
* must be an object with a "type" field (Schema.DataType), full structure
|
|
343
|
+
* validation, known type reference (schema or XLR), and when the type is an
|
|
344
|
+
* XLR type, validates the DataType object against the XLR definition via the SDK.
|
|
345
|
+
*/
|
|
346
|
+
function validateSchemaNode(
|
|
347
|
+
node: ObjectASTNode,
|
|
348
|
+
schemaTypeNames: Set<string>,
|
|
349
|
+
sdk: XLRSDK,
|
|
350
|
+
validationContext: ValidationContext,
|
|
351
|
+
): void {
|
|
352
|
+
for (const prop of node.properties) {
|
|
353
|
+
const valueNode = prop.valueNode;
|
|
354
|
+
if (!(valueNode && valueNode.type === "object")) {
|
|
355
|
+
if (valueNode) {
|
|
356
|
+
validationContext.addViolation({
|
|
357
|
+
node: valueNode,
|
|
358
|
+
message: formatErrorMessage(
|
|
359
|
+
`Schema property "${prop.keyNode.value}" must be an object (Schema.DataType) with a "type" field.`,
|
|
360
|
+
),
|
|
361
|
+
severity: DiagnosticSeverity.Error,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const dataTypeNode = valueNode as ObjectASTNode;
|
|
368
|
+
const typeProp = getProperty(dataTypeNode, "type");
|
|
369
|
+
if (!typeProp) {
|
|
370
|
+
validationContext.addViolation({
|
|
371
|
+
node: valueNode,
|
|
372
|
+
message: formatErrorMessage(
|
|
373
|
+
'Schema.DataType must have a "type" property (reference to schema or XLR type).',
|
|
374
|
+
),
|
|
375
|
+
severity: DiagnosticSeverity.Error,
|
|
376
|
+
});
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const typeValueNode = typeProp.valueNode;
|
|
381
|
+
if (!typeValueNode || typeValueNode.type !== "string") {
|
|
382
|
+
validationContext.addViolation({
|
|
383
|
+
node: typeValueNode ?? typeProp,
|
|
384
|
+
message: formatErrorMessage(
|
|
385
|
+
'Schema "type" must be a string (schema type name or XLR type name).',
|
|
386
|
+
),
|
|
387
|
+
severity: DiagnosticSeverity.Error,
|
|
388
|
+
});
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const typeName = (typeValueNode as StringASTNode).value;
|
|
393
|
+
const isSchemaType = schemaTypeNames.has(typeName);
|
|
394
|
+
const XLRType = sdk.getType(typeName, { getRawType: true });
|
|
395
|
+
|
|
396
|
+
if (!isSchemaType && !XLRType) {
|
|
397
|
+
validationContext.addViolation({
|
|
398
|
+
node: typeValueNode,
|
|
399
|
+
message: formatErrorMessage(
|
|
400
|
+
`Unknown schema type "${typeName}". Type must be a schema type (key in this schema) or an XLR type loaded in the SDK.`,
|
|
401
|
+
),
|
|
402
|
+
severity: DiagnosticSeverity.Error,
|
|
403
|
+
});
|
|
404
|
+
} else if (XLRType) {
|
|
405
|
+
/** Full DataType structure per @player-ui/types */
|
|
406
|
+
validateDataTypeStructure(
|
|
407
|
+
dataTypeNode,
|
|
408
|
+
XLRType as NamedType<ObjectType>,
|
|
409
|
+
sdk,
|
|
410
|
+
validationContext,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Validates the Flow's schema property: structure per Schema.Schema,
|
|
418
|
+
* type references, full DataType structure, and XLR shape when type is an XLR type.
|
|
419
|
+
*/
|
|
420
|
+
function validateFlowSchema(
|
|
421
|
+
contentNode: ContentASTNode,
|
|
422
|
+
sdk: XLRSDK,
|
|
423
|
+
validationContext: ValidationContext,
|
|
424
|
+
): void {
|
|
425
|
+
const schemaProp = getProperty(contentNode, "schema");
|
|
426
|
+
if (!schemaProp?.valueNode) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const schemaValue = schemaProp.valueNode;
|
|
431
|
+
if (schemaValue.type !== "object") {
|
|
432
|
+
validationContext.addViolation({
|
|
433
|
+
node: schemaValue,
|
|
434
|
+
message: formatErrorMessage(
|
|
435
|
+
'Flow "schema" must be an object with at least a "ROOT" key.',
|
|
436
|
+
),
|
|
437
|
+
severity: DiagnosticSeverity.Error,
|
|
438
|
+
});
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const schemaObj = schemaValue as ObjectASTNode;
|
|
443
|
+
const hasRoot = schemaObj.properties.some((p) => p.keyNode.value === "ROOT");
|
|
444
|
+
|
|
445
|
+
if (!hasRoot) {
|
|
446
|
+
validationContext.addViolation({
|
|
447
|
+
node: schemaValue,
|
|
448
|
+
message: formatErrorMessage('Schema must have a "ROOT" key.'),
|
|
449
|
+
severity: DiagnosticSeverity.Error,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const schemaTypeNames = getSchemaTypeNames(schemaObj);
|
|
454
|
+
|
|
455
|
+
for (const prop of schemaObj.properties) {
|
|
456
|
+
const nodeValue = prop.valueNode;
|
|
457
|
+
if (!nodeValue || nodeValue.type !== "object") {
|
|
458
|
+
if (nodeValue) {
|
|
459
|
+
validationContext.addViolation({
|
|
460
|
+
node: nodeValue,
|
|
461
|
+
message: formatErrorMessage(
|
|
462
|
+
`Schema node "${prop.keyNode.value}" must be an object.`,
|
|
463
|
+
),
|
|
464
|
+
severity: DiagnosticSeverity.Error,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
validateSchemaNode(
|
|
471
|
+
nodeValue as ObjectASTNode,
|
|
472
|
+
schemaTypeNames,
|
|
473
|
+
sdk,
|
|
474
|
+
validationContext,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Plugin that registers schema validation with the Player Language Service.
|
|
481
|
+
*/
|
|
482
|
+
export class SchemaValidationPlugin implements PlayerLanguageServicePlugin {
|
|
483
|
+
name = "schema-validation";
|
|
484
|
+
|
|
485
|
+
/** Resolved when CommonTypes have been loaded into the XLR SDK (once per plugin apply) */
|
|
486
|
+
private commonTypesLoaded: Promise<void> | null = null;
|
|
487
|
+
|
|
488
|
+
apply(service: PlayerLanguageService): void {
|
|
489
|
+
service.hooks.validate.tap(this.name, async (_ctx, validationContext) => {
|
|
490
|
+
await this.commonTypesLoaded;
|
|
491
|
+
validationContext.useASTVisitor(
|
|
492
|
+
this.createValidationVisitor(service, validationContext),
|
|
493
|
+
);
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private createValidationVisitor(
|
|
498
|
+
service: PlayerLanguageService,
|
|
499
|
+
validationContext: ValidationContext,
|
|
500
|
+
): ASTVisitor {
|
|
501
|
+
const sdk = service.XLRService.XLRSDK;
|
|
502
|
+
return {
|
|
503
|
+
ContentNode: (contentNode) => {
|
|
504
|
+
validateFlowSchema(contentNode, sdk, validationContext);
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
}
|
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
PlayerLanguageServicePlugin,
|
|
15
15
|
ValidationContext,
|
|
16
16
|
} from "..";
|
|
17
|
-
import { mapFlowStateToType } from "../utils";
|
|
17
|
+
import { findErrorNode, mapFlowStateToType } from "../utils";
|
|
18
18
|
import type { ASTNode, ObjectASTNode } from "../parser";
|
|
19
19
|
import type { EnhancedDocumentContextWithPosition } from "../types";
|
|
20
20
|
|
|
@@ -22,30 +22,11 @@ function isError(issue: ValidationMessage): boolean {
|
|
|
22
22
|
return issue.severity === DiagnosticSeverity.Error;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/** BFS search to find a JSONC node in children of some AST Node */
|
|
26
|
-
const findErrorNode = (rootNode: ASTNode, nodeToFind: Node): ASTNode => {
|
|
27
|
-
const children: Array<ASTNode> = [rootNode];
|
|
28
|
-
|
|
29
|
-
while (children.length > 0) {
|
|
30
|
-
const child = children.pop() as ASTNode;
|
|
31
|
-
if (child.jsonNode === nodeToFind) {
|
|
32
|
-
return child;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (child.children) {
|
|
36
|
-
children.push(...child.children);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// if the node can't be found return the original
|
|
41
|
-
return rootNode;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
25
|
/**
|
|
45
26
|
* Translates an SDK severity level to an LSP severity level
|
|
46
27
|
* Relies on both levels having the values associated to the underlying levels
|
|
47
28
|
*/
|
|
48
|
-
const translateSeverity = (
|
|
29
|
+
export const translateSeverity = (
|
|
49
30
|
severity: ValidationSeverity,
|
|
50
31
|
): DiagnosticSeverity => {
|
|
51
32
|
return severity as DiagnosticSeverity;
|
package/src/utils.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ObjectASTNode,
|
|
8
8
|
PropertyASTNode,
|
|
9
9
|
} from "./parser";
|
|
10
|
+
import type { Node } from "jsonc-parser";
|
|
10
11
|
import type { ASTVisitor } from "./types";
|
|
11
12
|
|
|
12
13
|
export const typeToVisitorMap: Record<ASTNode["type"], keyof ASTVisitor> = {
|
|
@@ -141,3 +142,22 @@ export function mapFlowStateToType(
|
|
|
141
142
|
|
|
142
143
|
return flowXLR;
|
|
143
144
|
}
|
|
145
|
+
|
|
146
|
+
/** BFS search to find a JSONC node in children of some AST Node */
|
|
147
|
+
export const findErrorNode = (rootNode: ASTNode, nodeToFind: Node): ASTNode => {
|
|
148
|
+
const children: Array<ASTNode> = [rootNode];
|
|
149
|
+
|
|
150
|
+
while (children.length > 0) {
|
|
151
|
+
const child = children.pop() as ASTNode;
|
|
152
|
+
if (child.jsonNode === nodeToFind) {
|
|
153
|
+
return child;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (child.children) {
|
|
157
|
+
children.push(...child.children);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// if the node can't be found return the original
|
|
162
|
+
return rootNode;
|
|
163
|
+
};
|
package/src/xlr/registry.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { Filters, TypeMetadata } from "@player-tools/xlr-sdk";
|
|
|
2
2
|
import { BasicXLRRegistry } from "@player-tools/xlr-sdk";
|
|
3
3
|
import type { NamedType, NodeType } from "@player-tools/xlr";
|
|
4
4
|
|
|
5
|
+
const SINGLE_INSTANCE_CAPABILITIES = ["DataTypes", "Formatters", "Validators"];
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Player specific implementation of a XLRs Registry
|
|
7
9
|
*/
|
|
@@ -47,7 +49,10 @@ export class PlayerXLRRegistry extends BasicXLRRegistry {
|
|
|
47
49
|
registeredName = type.extends.genericArguments[0].const;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
if (
|
|
52
|
+
if (
|
|
53
|
+
this.registrationMap.has(registeredName) &&
|
|
54
|
+
!SINGLE_INSTANCE_CAPABILITIES.includes(capability)
|
|
55
|
+
) {
|
|
51
56
|
const current = this.registrationMap.get(registeredName) as
|
|
52
57
|
| string
|
|
53
58
|
| string[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PlayerLanguageService, PlayerLanguageServicePlugin } from "..";
|
|
2
|
+
/**
|
|
3
|
+
* Plugin that registers schema validation with the Player Language Service.
|
|
4
|
+
*/
|
|
5
|
+
export declare class SchemaValidationPlugin implements PlayerLanguageServicePlugin {
|
|
6
|
+
name: string;
|
|
7
|
+
/** Resolved when CommonTypes have been loaded into the XLR SDK (once per plugin apply) */
|
|
8
|
+
private commonTypesLoaded;
|
|
9
|
+
apply(service: PlayerLanguageService): void;
|
|
10
|
+
private createValidationVisitor;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=schema-validation-plugin.d.ts.map
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { ValidationSeverity } from "@player-tools/xlr-sdk";
|
|
2
|
+
import { DiagnosticSeverity } from "vscode-languageserver-types";
|
|
1
3
|
import type { PlayerLanguageService, PlayerLanguageServicePlugin } from "..";
|
|
4
|
+
/**
|
|
5
|
+
* Translates an SDK severity level to an LSP severity level
|
|
6
|
+
* Relies on both levels having the values associated to the underlying levels
|
|
7
|
+
*/
|
|
8
|
+
export declare const translateSeverity: (severity: ValidationSeverity) => DiagnosticSeverity;
|
|
2
9
|
/** The plugin to enable duplicate id checking/fixing */
|
|
3
10
|
export declare class XLRPlugin implements PlayerLanguageServicePlugin {
|
|
4
11
|
name: string;
|
package/types/utils.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Range, Location } from "vscode-languageserver-types";
|
|
2
2
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
3
3
|
import type { ASTNode, PlayerContent, PropertyASTNode } from "./parser";
|
|
4
|
+
import type { Node } from "jsonc-parser";
|
|
4
5
|
import type { ASTVisitor } from "./types";
|
|
5
6
|
export declare const typeToVisitorMap: Record<ASTNode["type"], keyof ASTVisitor>;
|
|
6
7
|
/** Check to see if the source range contains the target one */
|
|
@@ -21,4 +22,6 @@ export declare function getLSLocationOfNode(document: TextDocument, node: ASTNod
|
|
|
21
22
|
export declare function formatLikeNode(document: TextDocument, originalNode: ASTNode, replacement: Record<string, unknown>): string;
|
|
22
23
|
/** Maps the string identifying the FlowType to the named type */
|
|
23
24
|
export declare function mapFlowStateToType(flowType: string | undefined): string | undefined;
|
|
25
|
+
/** BFS search to find a JSONC node in children of some AST Node */
|
|
26
|
+
export declare const findErrorNode: (rootNode: ASTNode, nodeToFind: Node) => ASTNode;
|
|
24
27
|
//# sourceMappingURL=utils.d.ts.map
|