@player-lang/json-language-service 0.0.2-next.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/dist/cjs/index.cjs +2314 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/index.legacy-esm.js +2249 -0
- package/dist/index.mjs +2249 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +40 -0
- package/src/__tests__/__snapshots__/service.test.ts.snap +213 -0
- package/src/__tests__/service.test.ts +298 -0
- package/src/constants.ts +38 -0
- package/src/index.ts +490 -0
- package/src/parser/__tests__/parse.test.ts +18 -0
- package/src/parser/document.ts +456 -0
- package/src/parser/edits.ts +31 -0
- package/src/parser/index.ts +38 -0
- package/src/parser/jsonParseErrors.ts +69 -0
- package/src/parser/types.ts +314 -0
- package/src/parser/utils.ts +94 -0
- package/src/plugins/__tests__/asset-wrapper-array-plugin.test.ts +112 -0
- package/src/plugins/__tests__/binding-schema-plugin.test.ts +62 -0
- package/src/plugins/__tests__/duplicate-id-plugin.test.ts +195 -0
- package/src/plugins/__tests__/missing-asset-wrapper-plugin.test.ts +190 -0
- package/src/plugins/__tests__/nav-state-plugin.test.ts +136 -0
- package/src/plugins/__tests__/view-node-plugin.test.ts +154 -0
- package/src/plugins/asset-wrapper-array-plugin.ts +123 -0
- package/src/plugins/binding-schema-plugin.ts +289 -0
- package/src/plugins/duplicate-id-plugin.ts +158 -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 +371 -0
- package/src/types.ts +119 -0
- package/src/utils.ts +143 -0
- package/src/xlr/__tests__/__snapshots__/transform.test.ts.snap +390 -0
- package/src/xlr/__tests__/transform.test.ts +108 -0
- package/src/xlr/index.ts +3 -0
- package/src/xlr/registry.ts +99 -0
- package/src/xlr/service.ts +190 -0
- package/src/xlr/transforms.ts +169 -0
- package/types/constants.d.ts +7 -0
- package/types/index.d.ts +69 -0
- package/types/parser/document.d.ts +25 -0
- package/types/parser/edits.d.ts +10 -0
- package/types/parser/index.d.ts +16 -0
- package/types/parser/jsonParseErrors.d.ts +27 -0
- package/types/parser/types.d.ts +188 -0
- package/types/parser/utils.d.ts +26 -0
- package/types/plugins/asset-wrapper-array-plugin.d.ts +9 -0
- package/types/plugins/binding-schema-plugin.d.ts +15 -0
- package/types/plugins/duplicate-id-plugin.d.ts +7 -0
- package/types/plugins/missing-asset-wrapper-plugin.d.ts +9 -0
- package/types/plugins/nav-state-plugin.d.ts +9 -0
- package/types/plugins/view-node-plugin.d.ts +9 -0
- package/types/plugins/xlr-plugin.d.ts +7 -0
- package/types/types.d.ts +81 -0
- package/types/utils.d.ts +24 -0
- package/types/xlr/index.d.ts +4 -0
- package/types/xlr/registry.d.ts +17 -0
- package/types/xlr/service.d.ts +22 -0
- package/types/xlr/transforms.d.ts +18 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { NodeType } from "@xlr-lib/xlr";
|
|
2
|
+
import { DiagnosticSeverity } from "vscode-languageserver-types";
|
|
3
|
+
import type { PlayerLanguageService, PlayerLanguageServicePlugin } from "..";
|
|
4
|
+
import type { ASTNode, StringASTNode } from "../parser";
|
|
5
|
+
import { getNodeValue } from "../parser";
|
|
6
|
+
import { formatLikeNode } from "../utils";
|
|
7
|
+
|
|
8
|
+
/** Check if the node is defined within a view */
|
|
9
|
+
const isInView = (node: ASTNode): boolean => {
|
|
10
|
+
if (node.type === "view") {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!node.parent) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return isInView(node.parent);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Checks to see if there is an AssetWrapper ref node somewhere at this level
|
|
23
|
+
*
|
|
24
|
+
* - @param nodes Array of nodes to check for an AssetWrapper ref
|
|
25
|
+
*/
|
|
26
|
+
const checkTypesForAssetWrapper = (nodes: Array<NodeType>): boolean => {
|
|
27
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
28
|
+
const node = nodes[i];
|
|
29
|
+
if (node.type === "object" && node.title?.includes("AssetWrapper")) {
|
|
30
|
+
return true;
|
|
31
|
+
} else if (node.type === "or") {
|
|
32
|
+
return checkTypesForAssetWrapper(node.or);
|
|
33
|
+
} else if (node.type === "and") {
|
|
34
|
+
return checkTypesForAssetWrapper(node.and);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return false;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Checks to see if the array's parent property is a switch statement
|
|
43
|
+
*/
|
|
44
|
+
const checkSwitchCase = (node: StringASTNode): boolean => {
|
|
45
|
+
return node.value === "staticSwitch" || node.value === "dynamicSwitch";
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Looks for an array where there _should_ be an AssetWrapper
|
|
50
|
+
*/
|
|
51
|
+
export class AssetWrapperArrayPlugin implements PlayerLanguageServicePlugin {
|
|
52
|
+
name = "asset-wrapper-to-array";
|
|
53
|
+
|
|
54
|
+
apply(service: PlayerLanguageService): void {
|
|
55
|
+
service.hooks.validate.tap(
|
|
56
|
+
this.name,
|
|
57
|
+
async (documentInfo, validationContext) => {
|
|
58
|
+
validationContext.useASTVisitor({
|
|
59
|
+
ArrayNode: async (arrayNode) => {
|
|
60
|
+
if (!isInView(arrayNode)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const xlrInfo = service.XLRService.getTypeInfoAtPosition(arrayNode);
|
|
65
|
+
if (!xlrInfo) return;
|
|
66
|
+
|
|
67
|
+
const isAssetWrapper = checkTypesForAssetWrapper(xlrInfo.nodes);
|
|
68
|
+
|
|
69
|
+
const parentNode = arrayNode.parent;
|
|
70
|
+
|
|
71
|
+
if (parentNode?.type !== "property") {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const targetLabel = parentNode.keyNode;
|
|
76
|
+
|
|
77
|
+
// manual check because switch types have arrays of asset wrappers but don't extend asset wrapper
|
|
78
|
+
const isSwitchCase = checkSwitchCase(targetLabel);
|
|
79
|
+
|
|
80
|
+
if (isAssetWrapper && !isSwitchCase) {
|
|
81
|
+
// This is an array node that _should_ be an asset wrapper.
|
|
82
|
+
// Convert it to a collection
|
|
83
|
+
|
|
84
|
+
let newAsset = {
|
|
85
|
+
asset: {
|
|
86
|
+
id: "",
|
|
87
|
+
type: "collection",
|
|
88
|
+
values: getNodeValue(arrayNode),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (arrayNode.children.length === 1) {
|
|
93
|
+
newAsset = getNodeValue(arrayNode.children[0]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
validationContext.addViolation({
|
|
97
|
+
node: targetLabel,
|
|
98
|
+
severity: DiagnosticSeverity.Error,
|
|
99
|
+
message: `Implicit Array -> "collection" assets is not supported.`,
|
|
100
|
+
fix: () => {
|
|
101
|
+
return {
|
|
102
|
+
name: `Convert to ${
|
|
103
|
+
arrayNode.children.length > 0 ? "collection" : "asset"
|
|
104
|
+
}`,
|
|
105
|
+
edit: {
|
|
106
|
+
type: "replace",
|
|
107
|
+
node: arrayNode,
|
|
108
|
+
value: formatLikeNode(
|
|
109
|
+
documentInfo.document,
|
|
110
|
+
arrayNode,
|
|
111
|
+
newAsset,
|
|
112
|
+
),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import type { Location } from "vscode-languageserver-types";
|
|
2
|
+
import { CompletionItemKind } from "vscode-languageserver-types";
|
|
3
|
+
import type { NodeType } from "@xlr-lib/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): void {
|
|
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,158 @@
|
|
|
1
|
+
import { DiagnosticSeverity } from "vscode-languageserver-types";
|
|
2
|
+
import type { AssetASTNode, ASTNode, ViewASTNode } from "../parser";
|
|
3
|
+
import { getViewNode, isPropertyNode, replaceString } from "../parser";
|
|
4
|
+
import type { PlayerLanguageService, PlayerLanguageServicePlugin } from "..";
|
|
5
|
+
import type { Violation, ValidationContext, ASTVisitor } from "../types";
|
|
6
|
+
|
|
7
|
+
/** Recurse up tree from node to find how many parents are templates */
|
|
8
|
+
const checkParentTemplate = (node: ASTNode, depth = 0) => {
|
|
9
|
+
if (node.parent) {
|
|
10
|
+
if (
|
|
11
|
+
isPropertyNode(node.parent) &&
|
|
12
|
+
node.parent.keyNode.value === "template"
|
|
13
|
+
) {
|
|
14
|
+
// Increase the template count each time it finds a nested template
|
|
15
|
+
|
|
16
|
+
depth += 1;
|
|
17
|
+
|
|
18
|
+
return checkParentTemplate(node.parent, depth);
|
|
19
|
+
}
|
|
20
|
+
return checkParentTemplate(node.parent, depth);
|
|
21
|
+
}
|
|
22
|
+
return depth;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Create an id for the node given it's path */
|
|
26
|
+
const generateID = (node?: ASTNode): string => {
|
|
27
|
+
if (!node || node.type === "view") {
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const prefix = generateID(node.parent);
|
|
32
|
+
let current = "";
|
|
33
|
+
|
|
34
|
+
if (node.type === "property") {
|
|
35
|
+
current = node.keyNode.value;
|
|
36
|
+
} else if (node.type === "asset" && node.assetType?.valueNode?.value) {
|
|
37
|
+
current = node.assetType.valueNode?.value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return [prefix, current].filter(Boolean).join("-");
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Create a duplicate id violation for the given node */
|
|
44
|
+
const createViolation = (node: AssetASTNode): Violation | undefined => {
|
|
45
|
+
const valueNode = node.id?.valueNode;
|
|
46
|
+
|
|
47
|
+
if (!valueNode) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
node: valueNode,
|
|
53
|
+
severity: DiagnosticSeverity.Error,
|
|
54
|
+
message: `The id "${node.id?.valueNode?.value}" is already in use in this view.`,
|
|
55
|
+
fix: () => {
|
|
56
|
+
return {
|
|
57
|
+
name: "Generate new ID",
|
|
58
|
+
edit: replaceString(valueNode, `"${generateID(node)}"`),
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** visit each of the assets in a view and check for duplicate ids */
|
|
65
|
+
const createValidationVisitor = (ctx: ValidationContext): ASTVisitor => {
|
|
66
|
+
const viewInfo = new Map<
|
|
67
|
+
ViewASTNode | AssetASTNode,
|
|
68
|
+
Map<
|
|
69
|
+
string,
|
|
70
|
+
{
|
|
71
|
+
/** the original asset node */
|
|
72
|
+
original: AssetASTNode;
|
|
73
|
+
|
|
74
|
+
/** if we've accounted for this node already */
|
|
75
|
+
handled: boolean;
|
|
76
|
+
}
|
|
77
|
+
>
|
|
78
|
+
>();
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
AssetNode: (assetNode) => {
|
|
82
|
+
const view = getViewNode(assetNode);
|
|
83
|
+
if (!view) {
|
|
84
|
+
// not sure how you can get here
|
|
85
|
+
throw new Error(
|
|
86
|
+
"Asset found but not within a view. Something is wrong",
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const assetID = assetNode.id;
|
|
91
|
+
|
|
92
|
+
if (!assetID || !assetID.valueNode?.value) {
|
|
93
|
+
// Can"t check for dupe ids if the asset doesn"t have one
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const id = assetID.valueNode?.value;
|
|
98
|
+
if (!viewInfo.has(view)) {
|
|
99
|
+
viewInfo.set(view, new Map());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const assetIDMap = viewInfo.get(view);
|
|
103
|
+
const idInfo = assetIDMap?.get(id);
|
|
104
|
+
|
|
105
|
+
const templateDepth = checkParentTemplate(assetNode);
|
|
106
|
+
|
|
107
|
+
if (templateDepth > 0) {
|
|
108
|
+
const expectedIndexElements = [];
|
|
109
|
+
for (let i = 0; i < templateDepth; i++) {
|
|
110
|
+
expectedIndexElements.push(`_index${i === 0 ? "" : i}_`);
|
|
111
|
+
}
|
|
112
|
+
const missingIndexSegments = expectedIndexElements.filter(
|
|
113
|
+
(e) => !id.includes(e),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (missingIndexSegments.length !== 0) {
|
|
117
|
+
ctx.addViolation({
|
|
118
|
+
node: assetNode,
|
|
119
|
+
severity: DiagnosticSeverity.Error,
|
|
120
|
+
message: `The id for this templated elements is missing the following index segments: ${missingIndexSegments.join(
|
|
121
|
+
", ",
|
|
122
|
+
)}`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (idInfo) {
|
|
128
|
+
if (!idInfo.handled) {
|
|
129
|
+
const origViolation = createViolation(idInfo.original);
|
|
130
|
+
if (origViolation) {
|
|
131
|
+
ctx.addViolation(origViolation);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
idInfo.handled = true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const assetViolation = createViolation(assetNode);
|
|
138
|
+
if (assetViolation) {
|
|
139
|
+
ctx.addViolation(assetViolation);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
// not claimed yet so this is the first
|
|
143
|
+
assetIDMap?.set(id, { original: assetNode, handled: false });
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** The plugin to enable duplicate id checking/fixing */
|
|
150
|
+
export class DuplicateIDPlugin implements PlayerLanguageServicePlugin {
|
|
151
|
+
name = "duplicate-id";
|
|
152
|
+
|
|
153
|
+
apply(service: PlayerLanguageService): void {
|
|
154
|
+
service.hooks.validate.tap(this.name, async (ctx, validation) => {
|
|
155
|
+
validation.useASTVisitor(createValidationVisitor(validation));
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -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
|
+
}
|