@player-tools/json-language-service 0.2.2--canary.20.454
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/README.md +8 -0
- package/dist/index.cjs.js +2519 -0
- package/dist/index.d.ts +449 -0
- package/dist/index.esm.js +2458 -0
- package/package.json +43 -0
- package/src/constants.ts +40 -0
- package/src/index.ts +433 -0
- package/src/parser/document.ts +456 -0
- package/src/parser/edits.ts +31 -0
- package/src/parser/index.ts +39 -0
- package/src/parser/jsonParseErrors.ts +69 -0
- package/src/parser/types.ts +312 -0
- package/src/parser/utils.ts +94 -0
- package/src/plugins/asset-wrapper-array-plugin.ts +119 -0
- package/src/plugins/binding-schema-plugin.ts +289 -0
- package/src/plugins/duplicate-id-plugin.ts +118 -0
- package/src/plugins/legacy-action-plugin.ts +79 -0
- package/src/plugins/legacy-template-plugin.ts +125 -0
- package/src/plugins/missing-asset-wrapper-plugin.ts +96 -0
- package/src/plugins/nav-state-plugin.ts +139 -0
- package/src/plugins/view-node-plugin.ts +225 -0
- package/src/plugins/xlr-plugin.ts +348 -0
- package/src/types.ts +120 -0
- package/src/utils.ts +141 -0
- package/src/xlr/index.ts +3 -0
- package/src/xlr/registry.ts +99 -0
- package/src/xlr/service.ts +191 -0
- package/src/xlr/transforms.ts +168 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import type { Location } from 'vscode-languageserver-types';
|
|
2
|
+
import { CompletionItemKind } from 'vscode-languageserver-types';
|
|
3
|
+
import type { NodeType } from '@player-tools/xlr';
|
|
4
|
+
import type { PlayerLanguageService, PlayerLanguageServicePlugin } from '..';
|
|
5
|
+
import type {
|
|
6
|
+
DocumentContext,
|
|
7
|
+
EnhancedDocumentContextWithPosition,
|
|
8
|
+
} from '../types';
|
|
9
|
+
import { getLSLocationOfNode, getProperty, isValueCompletion } from '../utils';
|
|
10
|
+
import type { PropertyASTNode, StringASTNode } from '../parser';
|
|
11
|
+
import { getContentNode } from '../parser';
|
|
12
|
+
|
|
13
|
+
interface SchemaInfo {
|
|
14
|
+
/** mapping of binding to schema path */
|
|
15
|
+
bindingToSchemaType: Map<
|
|
16
|
+
string,
|
|
17
|
+
{
|
|
18
|
+
/** the binding */
|
|
19
|
+
binding: string;
|
|
20
|
+
|
|
21
|
+
/** the type name */
|
|
22
|
+
typeName: string;
|
|
23
|
+
|
|
24
|
+
/** the name of the key */
|
|
25
|
+
key: string;
|
|
26
|
+
}
|
|
27
|
+
>;
|
|
28
|
+
|
|
29
|
+
/** JSON AST mapping of type to the node */
|
|
30
|
+
typeToNode: Map<
|
|
31
|
+
string,
|
|
32
|
+
{
|
|
33
|
+
/** the type */
|
|
34
|
+
type: string;
|
|
35
|
+
|
|
36
|
+
/** the json node */
|
|
37
|
+
typeNode: PropertyASTNode;
|
|
38
|
+
}
|
|
39
|
+
>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** parse the document for the schema info */
|
|
43
|
+
function getBindingInfo(ctx: DocumentContext): SchemaInfo {
|
|
44
|
+
const info: SchemaInfo = {
|
|
45
|
+
bindingToSchemaType: new Map(),
|
|
46
|
+
typeToNode: new Map(),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (ctx.PlayerContent.root.type !== 'content') {
|
|
50
|
+
return info;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const schemaRoot = ctx.PlayerContent.root.properties?.find(
|
|
54
|
+
(child) => child.keyNode.value === 'schema'
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (!schemaRoot || schemaRoot.valueNode?.type !== 'object') {
|
|
58
|
+
return info;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const schemaTypeQueue: Array<{
|
|
62
|
+
/** the current path */
|
|
63
|
+
currentPath: string;
|
|
64
|
+
|
|
65
|
+
/** the next type to visit */
|
|
66
|
+
typeToVisit: string;
|
|
67
|
+
|
|
68
|
+
/** list of visited types (to prevent loops) */
|
|
69
|
+
visited: Set<string>;
|
|
70
|
+
}> = [
|
|
71
|
+
{
|
|
72
|
+
currentPath: '',
|
|
73
|
+
typeToVisit: 'ROOT',
|
|
74
|
+
visited: new Set(),
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
while (schemaTypeQueue.length > 0) {
|
|
79
|
+
const next = schemaTypeQueue.shift();
|
|
80
|
+
if (!next) {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (next.visited.has(next.typeToVisit)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const visited = new Set(...next.visited, next.typeToVisit);
|
|
89
|
+
const { currentPath, typeToVisit } = next;
|
|
90
|
+
|
|
91
|
+
const typeNode = schemaRoot.valueNode.properties.find(
|
|
92
|
+
(child) => child.keyNode.value === typeToVisit
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (!typeNode || typeNode.valueNode?.type !== 'object') {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
info.typeToNode.set(typeToVisit, { type: typeToVisit, typeNode });
|
|
100
|
+
|
|
101
|
+
typeNode.valueNode.properties.forEach((prop) => {
|
|
102
|
+
// PropName is the path
|
|
103
|
+
// { type: TYPE } is the next nested type
|
|
104
|
+
|
|
105
|
+
const nextPath = [currentPath, prop.keyNode.value].join(
|
|
106
|
+
currentPath === '' ? '' : '.'
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
info.bindingToSchemaType.set(nextPath, {
|
|
110
|
+
binding: nextPath,
|
|
111
|
+
typeName: typeToVisit,
|
|
112
|
+
key: prop.keyNode.value,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (prop.valueNode?.type === 'object') {
|
|
116
|
+
const nestedTypeName = prop.valueNode.properties.find(
|
|
117
|
+
(c) => c.keyNode.value === 'type'
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (nestedTypeName && nestedTypeName.valueNode?.type === 'string') {
|
|
121
|
+
schemaTypeQueue.push({
|
|
122
|
+
currentPath: nextPath,
|
|
123
|
+
typeToVisit: nestedTypeName.valueNode.value,
|
|
124
|
+
visited,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return info;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Checks to see if there is a Binding ref node somewhere at this level
|
|
136
|
+
*
|
|
137
|
+
* - @param nodes Array of nodes to check for an AssetWrapper ref
|
|
138
|
+
*/
|
|
139
|
+
const checkTypesForBinding = (nodes: Array<NodeType>): boolean => {
|
|
140
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
141
|
+
const node = nodes[i];
|
|
142
|
+
if (node.type === 'string' && node.name === 'Binding') return true;
|
|
143
|
+
if (node.type === 'or') return checkTypesForBinding(node.or);
|
|
144
|
+
if (node.type === 'and') return checkTypesForBinding(node.and);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return false;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** check if the property is of type Binding */
|
|
151
|
+
function isBindingPropertyAssignment(
|
|
152
|
+
ctx: EnhancedDocumentContextWithPosition
|
|
153
|
+
): boolean {
|
|
154
|
+
if (ctx.node.type !== 'string' || ctx.node.parent?.type !== 'property') {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (checkTypesForBinding(ctx.XLR?.nodes ?? [])) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** find where in the document the type def is located */
|
|
166
|
+
function getLocationForBindingTypeDefinition(
|
|
167
|
+
ctx: EnhancedDocumentContextWithPosition,
|
|
168
|
+
schemaInfo: SchemaInfo
|
|
169
|
+
): Location | undefined {
|
|
170
|
+
if (!isBindingPropertyAssignment(ctx)) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const existingBindingValue = (ctx.node as StringASTNode).value;
|
|
175
|
+
const info = schemaInfo.bindingToSchemaType.get(existingBindingValue);
|
|
176
|
+
|
|
177
|
+
if (!info) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const nodeLocation = schemaInfo.typeToNode.get(info.typeName);
|
|
182
|
+
|
|
183
|
+
if (!nodeLocation || nodeLocation.typeNode.valueNode?.type !== 'object') {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const prop = getProperty(nodeLocation.typeNode.valueNode, info.key);
|
|
188
|
+
|
|
189
|
+
if (!prop) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return getLSLocationOfNode(ctx.document, prop);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** find where the schema is for a type */
|
|
197
|
+
function getLocationForSchemaType(
|
|
198
|
+
ctx: EnhancedDocumentContextWithPosition,
|
|
199
|
+
schemaInfo: SchemaInfo
|
|
200
|
+
): Location | undefined {
|
|
201
|
+
if (isValueCompletion(ctx.node)) {
|
|
202
|
+
// See if we're the "type" prop of a schema lookup
|
|
203
|
+
|
|
204
|
+
if (
|
|
205
|
+
ctx.node.parent?.type === 'property' &&
|
|
206
|
+
ctx.node.type === 'string' &&
|
|
207
|
+
ctx.node.parent.keyNode.value === 'type'
|
|
208
|
+
) {
|
|
209
|
+
const typeName = ctx.node.value;
|
|
210
|
+
const node = schemaInfo.typeToNode.get(typeName);
|
|
211
|
+
|
|
212
|
+
if (!node) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const schemaPropNode = getContentNode(ctx.node)?.properties.find(
|
|
217
|
+
(p) => p.keyNode.value === 'schema'
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (schemaPropNode?.valueNode?.type !== 'object') {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const schemaTypeNode = schemaPropNode.valueNode.properties.find(
|
|
225
|
+
(p) => p.keyNode.value === typeName
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
if (schemaTypeNode !== node.typeNode) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return getLSLocationOfNode(ctx.document, node.typeNode);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
*
|
|
239
|
+
* Adds completions for:
|
|
240
|
+
* - any `Binding` type from TS
|
|
241
|
+
* - "type" and "key" for non-defined types
|
|
242
|
+
*
|
|
243
|
+
* Adds definitions for:
|
|
244
|
+
* - any `Binding` reference to the schema def
|
|
245
|
+
*/
|
|
246
|
+
export class SchemaInfoPlugin implements PlayerLanguageServicePlugin {
|
|
247
|
+
name = 'view-node';
|
|
248
|
+
|
|
249
|
+
apply(service: PlayerLanguageService) {
|
|
250
|
+
let schemaInfo: SchemaInfo | undefined;
|
|
251
|
+
|
|
252
|
+
service.hooks.onDocumentUpdate.tap(this.name, (ctx) => {
|
|
253
|
+
schemaInfo = getBindingInfo(ctx);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
service.hooks.complete.tap(this.name, async (ctx, completionCtx) => {
|
|
257
|
+
// Is this a `binding` type
|
|
258
|
+
if (!isBindingPropertyAssignment(ctx)) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const existingBindingValue = (ctx.node as StringASTNode).value;
|
|
263
|
+
|
|
264
|
+
const bindings = Array.from(
|
|
265
|
+
schemaInfo?.bindingToSchemaType.keys() ?? []
|
|
266
|
+
).filter((k) => k.startsWith(existingBindingValue));
|
|
267
|
+
|
|
268
|
+
bindings.forEach((b) => {
|
|
269
|
+
completionCtx.addCompletionItem({
|
|
270
|
+
kind: CompletionItemKind.Value,
|
|
271
|
+
label: b.substring(existingBindingValue.length),
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// get bindings from schema
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
service.hooks.definition.tap(this.name, (ctx) => {
|
|
279
|
+
if (!schemaInfo) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
getLocationForSchemaType(ctx, schemaInfo) ||
|
|
285
|
+
getLocationForBindingTypeDefinition(ctx, schemaInfo)
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { DiagnosticSeverity } from 'vscode-languageserver-types';
|
|
2
|
+
import type { AssetASTNode, ASTNode, ViewASTNode } from '../parser';
|
|
3
|
+
import { getViewNode, replaceString } from '../parser';
|
|
4
|
+
import type { PlayerLanguageService, PlayerLanguageServicePlugin } from '..';
|
|
5
|
+
import type { Violation, ValidationContext, ASTVisitor } from '../types';
|
|
6
|
+
|
|
7
|
+
/** Create an id for the node given it's path */
|
|
8
|
+
const generateID = (node?: ASTNode): string => {
|
|
9
|
+
if (!node || node.type === 'view') {
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const prefix = generateID(node.parent);
|
|
14
|
+
let current = '';
|
|
15
|
+
|
|
16
|
+
if (node.type === 'property') {
|
|
17
|
+
current = node.keyNode.value;
|
|
18
|
+
} else if (node.type === 'asset' && node.assetType?.valueNode?.value) {
|
|
19
|
+
current = node.assetType.valueNode?.value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return [prefix, current].filter(Boolean).join('-');
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Create a duplicate id violation for the given node */
|
|
26
|
+
const createViolation = (node: AssetASTNode): Violation | undefined => {
|
|
27
|
+
const valueNode = node.id?.valueNode;
|
|
28
|
+
|
|
29
|
+
if (!valueNode) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
node: valueNode,
|
|
35
|
+
severity: DiagnosticSeverity.Error,
|
|
36
|
+
message: `The id '${node.id?.valueNode?.value}' is already in use in this view.`,
|
|
37
|
+
fix: () => {
|
|
38
|
+
return {
|
|
39
|
+
name: 'Generate new ID',
|
|
40
|
+
edit: replaceString(valueNode, `"${generateID(node)}"`),
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** visit each of the assets in a view and check for duplicate ids */
|
|
47
|
+
const createValidationVisitor = (ctx: ValidationContext): ASTVisitor => {
|
|
48
|
+
const viewInfo = new Map<
|
|
49
|
+
ViewASTNode | AssetASTNode,
|
|
50
|
+
Map<
|
|
51
|
+
string,
|
|
52
|
+
{
|
|
53
|
+
/** the original asset node */
|
|
54
|
+
original: AssetASTNode;
|
|
55
|
+
|
|
56
|
+
/** if we've accounted for this node already */
|
|
57
|
+
handled: boolean;
|
|
58
|
+
}
|
|
59
|
+
>
|
|
60
|
+
>();
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
AssetNode: (assetNode) => {
|
|
64
|
+
const view = getViewNode(assetNode);
|
|
65
|
+
if (!view) {
|
|
66
|
+
// not sure how you can get here
|
|
67
|
+
throw new Error(
|
|
68
|
+
'Asset found but not within a view. Something is wrong'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const assetID = assetNode.id;
|
|
73
|
+
|
|
74
|
+
if (!assetID || !assetID.valueNode?.value) {
|
|
75
|
+
// Can't check for dupe ids if the asset doesn't have one
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const id = assetID.valueNode?.value;
|
|
80
|
+
if (!viewInfo.has(view)) {
|
|
81
|
+
viewInfo.set(view, new Map());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const assetIDMap = viewInfo.get(view);
|
|
85
|
+
const idInfo = assetIDMap?.get(id);
|
|
86
|
+
|
|
87
|
+
if (idInfo) {
|
|
88
|
+
if (!idInfo.handled) {
|
|
89
|
+
const origViolation = createViolation(idInfo.original);
|
|
90
|
+
if (origViolation) {
|
|
91
|
+
ctx.addViolation(origViolation);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
idInfo.handled = true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const assetViolation = createViolation(assetNode);
|
|
98
|
+
if (assetViolation) {
|
|
99
|
+
ctx.addViolation(assetViolation);
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
// not claimed yet so this is the first
|
|
103
|
+
assetIDMap?.set(id, { original: assetNode, handled: false });
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** The plugin to enable duplicate id checking/fixing */
|
|
110
|
+
export class DuplicateIDPlugin implements PlayerLanguageServicePlugin {
|
|
111
|
+
name = 'duplicate-id';
|
|
112
|
+
|
|
113
|
+
apply(service: PlayerLanguageService) {
|
|
114
|
+
service.hooks.validate.tap(this.name, async (ctx, validation) => {
|
|
115
|
+
validation.useASTVisitor(createValidationVisitor(validation));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { DiagnosticSeverity } from 'vscode-languageserver-types';
|
|
2
|
+
import type { PlayerLanguageService, PlayerLanguageServicePlugin } from '..';
|
|
3
|
+
import type { ASTNode } from '../parser';
|
|
4
|
+
import { getNodeValue } from '../parser';
|
|
5
|
+
import type { ASTVisitor, ValidationContext } from '../types';
|
|
6
|
+
|
|
7
|
+
/** Create an AST visitor for checking the legacy action */
|
|
8
|
+
function createRuleVisitor(context: ValidationContext): ASTVisitor {
|
|
9
|
+
/** Check if a node is using the action asset or not */
|
|
10
|
+
const checkForLegacyAction = (node: ASTNode) => {
|
|
11
|
+
if (node.type === 'asset') {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (
|
|
16
|
+
node.type === 'object' &&
|
|
17
|
+
!node.properties.some(
|
|
18
|
+
(p) =>
|
|
19
|
+
p.keyNode.value === 'asset' ||
|
|
20
|
+
p.keyNode.value === 'dynamicSwitch' ||
|
|
21
|
+
p.keyNode.value === 'staticSwitch'
|
|
22
|
+
)
|
|
23
|
+
) {
|
|
24
|
+
context.addViolation({
|
|
25
|
+
message: 'Migrate to an action-asset',
|
|
26
|
+
node,
|
|
27
|
+
severity: DiagnosticSeverity.Warning,
|
|
28
|
+
fix: () => {
|
|
29
|
+
const newActionAsset = {
|
|
30
|
+
asset: {
|
|
31
|
+
type: 'action',
|
|
32
|
+
...getNodeValue(node),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
name: 'Convert to Asset',
|
|
38
|
+
edit: {
|
|
39
|
+
type: 'replace',
|
|
40
|
+
node,
|
|
41
|
+
value: JSON.stringify(newActionAsset, null, 2),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
ViewNode: (viewNode) => {
|
|
51
|
+
// Check for an `actions` array of non-assets
|
|
52
|
+
|
|
53
|
+
const actionsProp = viewNode.properties.find(
|
|
54
|
+
(p) => p.keyNode.value === 'actions'
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (!actionsProp || actionsProp.valueNode?.type !== 'array') {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Go through the array and add a violation/fix for anything that's not an asset
|
|
62
|
+
|
|
63
|
+
actionsProp.valueNode.children.forEach((action) => {
|
|
64
|
+
checkForLegacyAction(action);
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A plugin that validates and corrects the usage of non-asset actions in a view */
|
|
71
|
+
export class LegacyActionPlugin implements PlayerLanguageServicePlugin {
|
|
72
|
+
name = 'legacy-action';
|
|
73
|
+
|
|
74
|
+
apply(service: PlayerLanguageService) {
|
|
75
|
+
service.hooks.validate.tap(this.name, async (ctx, validationContext) => {
|
|
76
|
+
validationContext.useASTVisitor(createRuleVisitor(validationContext));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { addLast, omit, set } from 'timm';
|
|
2
|
+
import { DiagnosticSeverity } from 'vscode-languageserver-types';
|
|
3
|
+
import type { PlayerLanguageService, PlayerLanguageServicePlugin } from '..';
|
|
4
|
+
import type { AssetASTNode, ObjectASTNode, ViewASTNode } from '../parser';
|
|
5
|
+
import { getNodeValue } from '../parser';
|
|
6
|
+
import type {
|
|
7
|
+
ASTVisitor,
|
|
8
|
+
DocumentContext,
|
|
9
|
+
ValidationContext,
|
|
10
|
+
Violation,
|
|
11
|
+
} from '../types';
|
|
12
|
+
import { formatLikeNode } from '../utils';
|
|
13
|
+
|
|
14
|
+
/** Create a visitor for handling old template syntax */
|
|
15
|
+
function createRuleVisitor(
|
|
16
|
+
context: ValidationContext,
|
|
17
|
+
docInfo: DocumentContext
|
|
18
|
+
): ASTVisitor {
|
|
19
|
+
/** Check a node for any signs of a legacy-template */
|
|
20
|
+
const checkForLegacyTemplate = (
|
|
21
|
+
node: ObjectASTNode | AssetASTNode | ViewASTNode
|
|
22
|
+
) => {
|
|
23
|
+
// check if it has any of the 3 props we care about
|
|
24
|
+
|
|
25
|
+
const templateDataProp = node.properties.find(
|
|
26
|
+
(p) => p.keyNode.value === 'templateData'
|
|
27
|
+
);
|
|
28
|
+
const templateValueProp = node.properties.find(
|
|
29
|
+
(p) => p.keyNode.value === 'template'
|
|
30
|
+
);
|
|
31
|
+
const templateOutputProp = node.properties.find(
|
|
32
|
+
(p) => p.keyNode.value === 'templateOutput'
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// If we don't have any of those props, or we just have the`template` prop and it points to an array, skip it all, we good.
|
|
36
|
+
if (
|
|
37
|
+
!templateDataProp &&
|
|
38
|
+
!templateOutputProp &&
|
|
39
|
+
(!templateValueProp || templateValueProp.valueNode?.type === 'array')
|
|
40
|
+
) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const templateViolation: Omit<Violation, 'node'> = {
|
|
45
|
+
severity: DiagnosticSeverity.Error,
|
|
46
|
+
message: `Migrate to the template[] syntax.`,
|
|
47
|
+
fix: () => {
|
|
48
|
+
// Create the new template object;
|
|
49
|
+
const path = [
|
|
50
|
+
'template',
|
|
51
|
+
templateValueProp?.valueNode?.type === 'array'
|
|
52
|
+
? templateValueProp.valueNode.children.length
|
|
53
|
+
: 0,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const newTemplateObj = {
|
|
57
|
+
value:
|
|
58
|
+
templateValueProp?.valueNode?.type !== 'array' &&
|
|
59
|
+
templateValueProp?.valueNode
|
|
60
|
+
? getNodeValue(templateValueProp?.valueNode)
|
|
61
|
+
: {},
|
|
62
|
+
output: templateOutputProp?.valueNode?.jsonNode.value ?? '',
|
|
63
|
+
data: templateDataProp?.valueNode?.jsonNode.value ?? '',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const oldValue = getNodeValue(node);
|
|
67
|
+
let newValue = omit(oldValue, 'templateData');
|
|
68
|
+
newValue = omit(newValue, 'templateOutput');
|
|
69
|
+
|
|
70
|
+
if (templateValueProp?.valueNode?.type !== 'array') {
|
|
71
|
+
newValue = omit(newValue, 'template');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
newValue = set(
|
|
75
|
+
newValue,
|
|
76
|
+
'template',
|
|
77
|
+
addLast(newValue.template ?? [], newTemplateObj)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
edit: {
|
|
82
|
+
type: 'replace',
|
|
83
|
+
path,
|
|
84
|
+
node,
|
|
85
|
+
value: formatLikeNode(docInfo.document, node, newValue),
|
|
86
|
+
},
|
|
87
|
+
name: 'Convert to template[]',
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (templateDataProp) {
|
|
93
|
+
context.addViolation({
|
|
94
|
+
...templateViolation,
|
|
95
|
+
node: templateDataProp,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (templateOutputProp) {
|
|
100
|
+
context.addViolation({
|
|
101
|
+
...templateViolation,
|
|
102
|
+
node: templateOutputProp,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
ViewNode: checkForLegacyTemplate,
|
|
109
|
+
AssetNode: checkForLegacyTemplate,
|
|
110
|
+
ObjectNode: checkForLegacyTemplate,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A plugin that handles the old legacy template syntax */
|
|
115
|
+
export class LegacyTemplatePlugin implements PlayerLanguageServicePlugin {
|
|
116
|
+
name = 'legacy-template';
|
|
117
|
+
|
|
118
|
+
apply(service: PlayerLanguageService) {
|
|
119
|
+
service.hooks.validate.tap(this.name, async (ctx, validationContext) => {
|
|
120
|
+
validationContext.useASTVisitor(
|
|
121
|
+
createRuleVisitor(validationContext, ctx)
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { DiagnosticSeverity } from 'vscode-languageserver-types';
|
|
2
|
+
import type { PlayerLanguageService, PlayerLanguageServicePlugin } from '..';
|
|
3
|
+
import type { ASTNode, ObjectASTNode } from '../parser';
|
|
4
|
+
import {
|
|
5
|
+
getNodeValue,
|
|
6
|
+
isKeyNode,
|
|
7
|
+
isObjectNode,
|
|
8
|
+
isPropertyNode,
|
|
9
|
+
} from '../parser';
|
|
10
|
+
import { formatLikeNode } from '../utils';
|
|
11
|
+
|
|
12
|
+
/** Get the JSON object that the validation targets */
|
|
13
|
+
const getObjectTarget = (node?: ASTNode): ObjectASTNode | undefined => {
|
|
14
|
+
if (isObjectNode(node)) {
|
|
15
|
+
return node;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (
|
|
19
|
+
isKeyNode(node) &&
|
|
20
|
+
isPropertyNode(node.parent) &&
|
|
21
|
+
isObjectNode(node.parent.valueNode)
|
|
22
|
+
) {
|
|
23
|
+
return node.parent.valueNode;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A plugin to help identify and fix the issue of forgetting the "asset" wrapper object
|
|
29
|
+
*/
|
|
30
|
+
export class MissingAssetWrapperPlugin implements PlayerLanguageServicePlugin {
|
|
31
|
+
name = 'missing-asset-wrapper';
|
|
32
|
+
|
|
33
|
+
apply(languageService: PlayerLanguageService): void {
|
|
34
|
+
languageService.hooks.onValidateEnd.tap(
|
|
35
|
+
this.name,
|
|
36
|
+
(diagnostics, { addFixableViolation, documentContext }) => {
|
|
37
|
+
// Just be naive here
|
|
38
|
+
// If there's an error for "expected asset" + an unexpected `id` and `type`, replace that with our own
|
|
39
|
+
|
|
40
|
+
let filteredDiags = diagnostics;
|
|
41
|
+
|
|
42
|
+
const expectedAssetDiags = diagnostics.filter(
|
|
43
|
+
(d) =>
|
|
44
|
+
d.message.includes(
|
|
45
|
+
"Does not match any of the expected types for type: 'AssetWrapperOrSwitch'"
|
|
46
|
+
) || d.message.startsWith('Expected property: asset')
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expectedAssetDiags.forEach((d) => {
|
|
50
|
+
const originalNode = documentContext.PlayerContent.getNodeFromOffset(
|
|
51
|
+
documentContext.document.offsetAt(d.range.start)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const objectNode = getObjectTarget(originalNode);
|
|
55
|
+
|
|
56
|
+
if (objectNode && originalNode) {
|
|
57
|
+
// This 'expected property' diag is for the key of a property, where the value is the stubbed out asset
|
|
58
|
+
// Check for diags for keys in that nested object
|
|
59
|
+
|
|
60
|
+
// Now group the other diagnostics that are for unexpected props underneath that object
|
|
61
|
+
// We'll suppress these for now since they are bound to be wrong until they're wrapped in an asset
|
|
62
|
+
const associatedDiags = filteredDiags.filter((nestedDiag) => {
|
|
63
|
+
const diagNode = documentContext.PlayerContent.getNodeFromOffset(
|
|
64
|
+
documentContext.document.offsetAt(nestedDiag.range.start)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return objectNode.properties.some((p) => p.keyNode === diagNode);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
addFixableViolation(d, {
|
|
71
|
+
node: originalNode,
|
|
72
|
+
message: d.message,
|
|
73
|
+
severity: d.severity ?? DiagnosticSeverity.Error,
|
|
74
|
+
fix: () => ({
|
|
75
|
+
name: `Wrap in "asset"`,
|
|
76
|
+
edit: {
|
|
77
|
+
type: 'replace',
|
|
78
|
+
node: objectNode,
|
|
79
|
+
value: formatLikeNode(documentContext.document, objectNode, {
|
|
80
|
+
asset: getNodeValue(objectNode),
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
filteredDiags = filteredDiags.filter(
|
|
87
|
+
(filteredD) => !associatedDiags.includes(filteredD)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return filteredDiags;
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|