@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.
Files changed (59) hide show
  1. package/dist/cjs/index.cjs +2314 -0
  2. package/dist/cjs/index.cjs.map +1 -0
  3. package/dist/index.legacy-esm.js +2249 -0
  4. package/dist/index.mjs +2249 -0
  5. package/dist/index.mjs.map +1 -0
  6. package/package.json +40 -0
  7. package/src/__tests__/__snapshots__/service.test.ts.snap +213 -0
  8. package/src/__tests__/service.test.ts +298 -0
  9. package/src/constants.ts +38 -0
  10. package/src/index.ts +490 -0
  11. package/src/parser/__tests__/parse.test.ts +18 -0
  12. package/src/parser/document.ts +456 -0
  13. package/src/parser/edits.ts +31 -0
  14. package/src/parser/index.ts +38 -0
  15. package/src/parser/jsonParseErrors.ts +69 -0
  16. package/src/parser/types.ts +314 -0
  17. package/src/parser/utils.ts +94 -0
  18. package/src/plugins/__tests__/asset-wrapper-array-plugin.test.ts +112 -0
  19. package/src/plugins/__tests__/binding-schema-plugin.test.ts +62 -0
  20. package/src/plugins/__tests__/duplicate-id-plugin.test.ts +195 -0
  21. package/src/plugins/__tests__/missing-asset-wrapper-plugin.test.ts +190 -0
  22. package/src/plugins/__tests__/nav-state-plugin.test.ts +136 -0
  23. package/src/plugins/__tests__/view-node-plugin.test.ts +154 -0
  24. package/src/plugins/asset-wrapper-array-plugin.ts +123 -0
  25. package/src/plugins/binding-schema-plugin.ts +289 -0
  26. package/src/plugins/duplicate-id-plugin.ts +158 -0
  27. package/src/plugins/missing-asset-wrapper-plugin.ts +96 -0
  28. package/src/plugins/nav-state-plugin.ts +139 -0
  29. package/src/plugins/view-node-plugin.ts +225 -0
  30. package/src/plugins/xlr-plugin.ts +371 -0
  31. package/src/types.ts +119 -0
  32. package/src/utils.ts +143 -0
  33. package/src/xlr/__tests__/__snapshots__/transform.test.ts.snap +390 -0
  34. package/src/xlr/__tests__/transform.test.ts +108 -0
  35. package/src/xlr/index.ts +3 -0
  36. package/src/xlr/registry.ts +99 -0
  37. package/src/xlr/service.ts +190 -0
  38. package/src/xlr/transforms.ts +169 -0
  39. package/types/constants.d.ts +7 -0
  40. package/types/index.d.ts +69 -0
  41. package/types/parser/document.d.ts +25 -0
  42. package/types/parser/edits.d.ts +10 -0
  43. package/types/parser/index.d.ts +16 -0
  44. package/types/parser/jsonParseErrors.d.ts +27 -0
  45. package/types/parser/types.d.ts +188 -0
  46. package/types/parser/utils.d.ts +26 -0
  47. package/types/plugins/asset-wrapper-array-plugin.d.ts +9 -0
  48. package/types/plugins/binding-schema-plugin.d.ts +15 -0
  49. package/types/plugins/duplicate-id-plugin.d.ts +7 -0
  50. package/types/plugins/missing-asset-wrapper-plugin.d.ts +9 -0
  51. package/types/plugins/nav-state-plugin.d.ts +9 -0
  52. package/types/plugins/view-node-plugin.d.ts +9 -0
  53. package/types/plugins/xlr-plugin.d.ts +7 -0
  54. package/types/types.d.ts +81 -0
  55. package/types/utils.d.ts +24 -0
  56. package/types/xlr/index.d.ts +4 -0
  57. package/types/xlr/registry.d.ts +17 -0
  58. package/types/xlr/service.d.ts +22 -0
  59. 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
+ }