@n8n/eslint-plugin-community-nodes 0.31.0 → 0.32.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 (38) hide show
  1. package/README.md +1 -0
  2. package/dist/plugin.d.ts +6 -0
  3. package/dist/plugin.d.ts.map +1 -1
  4. package/dist/plugin.js +2 -0
  5. package/dist/plugin.js.map +1 -1
  6. package/dist/rules/index.d.ts +1 -0
  7. package/dist/rules/index.d.ts.map +1 -1
  8. package/dist/rules/index.js +2 -0
  9. package/dist/rules/index.js.map +1 -1
  10. package/dist/rules/no-credential-reuse.d.ts.map +1 -1
  11. package/dist/rules/no-credential-reuse.js +4 -6
  12. package/dist/rules/no-credential-reuse.js.map +1 -1
  13. package/dist/rules/no-emoji-in-options.js +4 -4
  14. package/dist/rules/no-emoji-in-options.js.map +1 -1
  15. package/dist/rules/no-unsafe-connection-type-cast.d.ts +3 -0
  16. package/dist/rules/no-unsafe-connection-type-cast.d.ts.map +1 -0
  17. package/dist/rules/no-unsafe-connection-type-cast.js +104 -0
  18. package/dist/rules/no-unsafe-connection-type-cast.js.map +1 -0
  19. package/dist/rules/valid-credential-references.d.ts.map +1 -1
  20. package/dist/rules/valid-credential-references.js +4 -6
  21. package/dist/rules/valid-credential-references.js.map +1 -1
  22. package/dist/utils/ast-utils.d.ts +6 -0
  23. package/dist/utils/ast-utils.d.ts.map +1 -1
  24. package/dist/utils/ast-utils.js +50 -0
  25. package/dist/utils/ast-utils.js.map +1 -1
  26. package/docs/rules/no-unsafe-connection-type-cast.md +58 -0
  27. package/package.json +5 -5
  28. package/src/plugin.ts +2 -0
  29. package/src/rules/index.ts +2 -0
  30. package/src/rules/no-credential-reuse.test.ts +54 -0
  31. package/src/rules/no-credential-reuse.ts +4 -8
  32. package/src/rules/no-emoji-in-options.test.ts +77 -0
  33. package/src/rules/no-emoji-in-options.ts +4 -4
  34. package/src/rules/no-unsafe-connection-type-cast.test.ts +306 -0
  35. package/src/rules/no-unsafe-connection-type-cast.ts +125 -0
  36. package/src/rules/valid-credential-references.test.ts +75 -0
  37. package/src/rules/valid-credential-references.ts +4 -8
  38. package/src/utils/ast-utils.ts +67 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@n8n/eslint-plugin-community-nodes",
3
3
  "type": "module",
4
- "version": "0.31.0",
4
+ "version": "0.32.0",
5
5
  "main": "./dist/plugin.js",
6
6
  "types": "./dist/plugin.d.ts",
7
7
  "exports": {
@@ -16,7 +16,10 @@
16
16
  "fastest-levenshtein": "1.0.16"
17
17
  },
18
18
  "devDependencies": {
19
+ "@n8n/typescript-config": "1.11.0",
20
+ "@n8n/vitest-config": "1.21.0",
19
21
  "@types/node": "24.10.1",
22
+ "n8n-workflow": "2.39.0",
20
23
  "@typescript-eslint/rule-tester": "^8.35.0",
21
24
  "eslint-doc-generator": "^2.2.2",
22
25
  "eslint-plugin-eslint-plugin": "^7.0.0",
@@ -24,10 +27,7 @@
24
27
  "@typescript/native": "npm:typescript@7.0.2",
25
28
  "typescript": "npm:@typescript/typescript6@6.0.2",
26
29
  "vite": "^8.0.2",
27
- "vitest": "^4.1.9",
28
- "@n8n/typescript-config": "1.11.0",
29
- "n8n-workflow": "2.38.0",
30
- "@n8n/vitest-config": "1.21.0"
30
+ "vitest": "^4.1.9"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "eslint": "9.29.0",
package/src/plugin.ts CHANGED
@@ -41,6 +41,7 @@ const configs = {
41
41
  '@n8n/community-nodes/no-runtime-dependencies': 'error',
42
42
  '@n8n/community-nodes/no-silent-error-swallowing': 'error',
43
43
  '@n8n/community-nodes/no-template-placeholders': 'error',
44
+ '@n8n/community-nodes/no-unsafe-connection-type-cast': 'error',
44
45
  '@n8n/community-nodes/icon-validation': 'error',
45
46
  '@n8n/community-nodes/icon-prefer-themed-variants': 'warn',
46
47
  '@n8n/community-nodes/resource-operation-pattern': 'warn',
@@ -89,6 +90,7 @@ const configs = {
89
90
  '@n8n/community-nodes/no-runtime-dependencies': 'error',
90
91
  '@n8n/community-nodes/no-silent-error-swallowing': 'error',
91
92
  '@n8n/community-nodes/no-template-placeholders': 'error',
93
+ '@n8n/community-nodes/no-unsafe-connection-type-cast': 'error',
92
94
  '@n8n/community-nodes/icon-validation': 'error',
93
95
  '@n8n/community-nodes/icon-prefer-themed-variants': 'warn',
94
96
  '@n8n/community-nodes/credential-documentation-url': 'error',
@@ -26,6 +26,7 @@ import { NoRestrictedImportsRule } from './no-restricted-imports.js';
26
26
  import { NoRuntimeDependenciesRule } from './no-runtime-dependencies.js';
27
27
  import { NoSilentErrorSwallowingRule } from './no-silent-error-swallowing.js';
28
28
  import { NoTemplatePlaceholdersRule } from './no-template-placeholders.js';
29
+ import { NoUnsafeConnectionTypeCastRule } from './no-unsafe-connection-type-cast.js';
29
30
  import { NodeClassDescriptionIconMissingRule } from './node-class-description-icon-missing.js';
30
31
  import { NodeConnectionTypeLiteralRule } from './node-connection-type-literal.js';
31
32
  import { NodeRegistrationCompleteRule } from './node-registration-complete.js';
@@ -65,6 +66,7 @@ export const rules = {
65
66
  'no-runtime-dependencies': NoRuntimeDependenciesRule,
66
67
  'no-silent-error-swallowing': NoSilentErrorSwallowingRule,
67
68
  'no-template-placeholders': NoTemplatePlaceholdersRule,
69
+ 'no-unsafe-connection-type-cast': NoUnsafeConnectionTypeCastRule,
68
70
  'icon-validation': IconValidationRule,
69
71
  'icon-prefer-themed-variants': IconPreferThemedVariantsRule,
70
72
  'resource-operation-pattern': ResourceOperationPatternRule,
@@ -19,6 +19,7 @@ const mockFindPackageJson = vi.mocked(fileUtils.findPackageJson);
19
19
  const ruleTester = new RuleTester();
20
20
 
21
21
  const nodeFilePath = '/tmp/TestNode.node.ts';
22
+ const versionedNodeFilePath = '/tmp/v1/TestNodeV1.node.ts';
22
23
 
23
24
  function createNodeCode(
24
25
  credentials: Array<string | { name: string; required?: boolean }> = [],
@@ -97,6 +98,36 @@ export class TestNode implements INodeType {
97
98
  }`;
98
99
  }
99
100
 
101
+ /**
102
+ * Versioned node layout from the node-building docs: each version class
103
+ * implements `INodeType` and assigns `description` in its constructor, merging
104
+ * the `baseDescription` that the `VersionedNodeType` entry file passes in.
105
+ */
106
+ function createVersionedNodeCode(credentialNameLiteral: string): string {
107
+ return `
108
+ import type { INodeType, INodeTypeBaseDescription, INodeTypeDescription } from 'n8n-workflow';
109
+
110
+ export class TestNodeV1 implements INodeType {
111
+ description: INodeTypeDescription;
112
+
113
+ constructor(baseDescription: INodeTypeBaseDescription) {
114
+ this.description = {
115
+ ...baseDescription,
116
+ version: 1,
117
+ inputs: ['main'],
118
+ outputs: ['main'],
119
+ credentials: [
120
+ {
121
+ name: ${credentialNameLiteral},
122
+ required: true,
123
+ },
124
+ ],
125
+ properties: [],
126
+ };
127
+ }
128
+ }`;
129
+ }
130
+
100
131
  // Helper function to create non-node class
101
132
  function createNonNodeClass(): string {
102
133
  return `
@@ -163,6 +194,29 @@ ruleTester.run('no-credential-reuse', NoCredentialReuseRule, {
163
194
  },
164
195
  ],
165
196
  invalid: [
197
+ {
198
+ name: 'SECURITY: versioned node assigning description in its constructor uses credential not in package',
199
+ filename: versionedNodeFilePath,
200
+ code: createVersionedNodeCode("'ExternalApi'"),
201
+ errors: [
202
+ {
203
+ messageId: 'credentialNotInPackage',
204
+ data: { credentialName: 'ExternalApi' },
205
+ suggestions: [
206
+ {
207
+ messageId: 'useAvailable',
208
+ data: { suggestedName: 'myApiCredential' },
209
+ output: createVersionedNodeCode('"myApiCredential"'),
210
+ },
211
+ {
212
+ messageId: 'useAvailable',
213
+ data: { suggestedName: 'anotherApiCredential' },
214
+ output: createVersionedNodeCode('"anotherApiCredential"'),
215
+ },
216
+ ],
217
+ },
218
+ ],
219
+ },
166
220
  {
167
221
  name: 'SECURITY: node using credential not in package (object form)',
168
222
  filename: nodeFilePath,
@@ -1,9 +1,8 @@
1
- import { TSESTree } from '@typescript-eslint/utils';
2
1
  import type { ReportSuggestionArray } from '@typescript-eslint/utils/ts-eslint';
3
2
 
4
3
  import {
5
4
  isNodeTypeClass,
6
- findClassProperty,
5
+ findNodeDescriptionObject,
7
6
  findArrayLiteralProperty,
8
7
  extractCredentialNameFromArray,
9
8
  findPackageJson,
@@ -59,15 +58,12 @@ export const NoCredentialReuseRule = createRule({
59
58
  return;
60
59
  }
61
60
 
62
- const descriptionProperty = findClassProperty(node, 'description');
63
- if (
64
- !descriptionProperty?.value ||
65
- descriptionProperty.value.type !== TSESTree.AST_NODE_TYPES.ObjectExpression
66
- ) {
61
+ const description = findNodeDescriptionObject(node);
62
+ if (!description) {
67
63
  return;
68
64
  }
69
65
 
70
- const credentialsArray = findArrayLiteralProperty(descriptionProperty.value, 'credentials');
66
+ const credentialsArray = findArrayLiteralProperty(description, 'credentials');
71
67
  if (!credentialsArray) {
72
68
  return;
73
69
  }
@@ -16,6 +16,27 @@ function createNodeCode(body: string): string {
16
16
  `;
17
17
  }
18
18
 
19
+ /**
20
+ * Versioned node layout from the node-building docs: the version class assigns
21
+ * `description` in its constructor instead of as a property initializer.
22
+ */
23
+ function createVersionedNodeCode(body: string): string {
24
+ return `
25
+ import type { INodeType, INodeTypeBaseDescription, INodeTypeDescription } from 'n8n-workflow';
26
+
27
+ export class TestNodeV1 implements INodeType {
28
+ description: INodeTypeDescription;
29
+
30
+ constructor(baseDescription: INodeTypeBaseDescription) {
31
+ this.description = {
32
+ ...baseDescription,
33
+ ${body}
34
+ };
35
+ }
36
+ }
37
+ `;
38
+ }
39
+
19
40
  ruleTester.run('no-emoji-in-options', NoEmojiInOptionsRule, {
20
41
  valid: [
21
42
  {
@@ -75,6 +96,62 @@ ruleTester.run('no-emoji-in-options', NoEmojiInOptionsRule, {
75
96
  },
76
97
  ],
77
98
  invalid: [
99
+ {
100
+ name: 'emoji in option name of a versioned node assigning description in its constructor',
101
+ filename: '/tmp/v1/TestNodeV1.node.ts',
102
+ code: createVersionedNodeCode(`
103
+ version: 1,
104
+ properties: [
105
+ {
106
+ displayName: 'Operation',
107
+ name: 'operation',
108
+ type: 'options',
109
+ options: [
110
+ { name: '✅ Create', value: 'create' },
111
+ { name: 'Delete', value: 'delete' },
112
+ ],
113
+ default: 'create',
114
+ },
115
+ ],
116
+ `),
117
+ errors: [{ messageId: 'emojiInOption', data: { key: 'name', emoji: '✅' } }],
118
+ },
119
+ {
120
+ name: 'emoji in a description that is cast with `as`',
121
+ filename: '/tmp/TestNode.node.ts',
122
+ code: `
123
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
124
+
125
+ export class TestNode implements INodeType {
126
+ description = {
127
+ displayName: '🚀 Rocket Node',
128
+ name: 'testNode',
129
+ properties: [],
130
+ } as INodeTypeDescription;
131
+ }
132
+ `,
133
+ errors: [{ messageId: 'emojiInOption', data: { key: 'displayName', emoji: '🚀' } }],
134
+ },
135
+ {
136
+ name: 'emoji in a constructor-assigned description that is cast with `as`',
137
+ filename: '/tmp/v1/TestNodeV1.node.ts',
138
+ code: `
139
+ import type { INodeType, INodeTypeBaseDescription, INodeTypeDescription } from 'n8n-workflow';
140
+
141
+ export class TestNodeV1 implements INodeType {
142
+ description: INodeTypeDescription;
143
+
144
+ constructor(baseDescription: INodeTypeBaseDescription) {
145
+ this.description = {
146
+ ...baseDescription,
147
+ displayName: '🚀 Rocket Node',
148
+ properties: [],
149
+ } as INodeTypeDescription;
150
+ }
151
+ }
152
+ `,
153
+ errors: [{ messageId: 'emojiInOption', data: { key: 'displayName', emoji: '🚀' } }],
154
+ },
78
155
  {
79
156
  name: 'emoji in node displayName',
80
157
  filename: '/tmp/TestNode.node.ts',
@@ -3,7 +3,7 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
3
3
 
4
4
  import {
5
5
  isNodeTypeClass,
6
- findClassProperty,
6
+ findNodeDescriptionObject,
7
7
  getStringLiteralValue,
8
8
  isFileType,
9
9
  createRule,
@@ -93,12 +93,12 @@ export const NoEmojiInOptionsRule = createRule({
93
93
  return;
94
94
  }
95
95
 
96
- const descriptionProperty = findClassProperty(node, 'description');
97
- if (!descriptionProperty?.value) {
96
+ const description = findNodeDescriptionObject(node);
97
+ if (!description) {
98
98
  return;
99
99
  }
100
100
 
101
- traverse(descriptionProperty.value);
101
+ traverse(description);
102
102
  },
103
103
  };
104
104
  },
@@ -0,0 +1,306 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { NoUnsafeConnectionTypeCastRule } from './no-unsafe-connection-type-cast.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ function createNodeCode(inputs: string, outputs: string): string {
8
+ return `
9
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
10
+ import { NodeConnectionTypes } from 'n8n-workflow';
11
+
12
+ export class TestNode implements INodeType {
13
+ description: INodeTypeDescription = {
14
+ displayName: 'Test Node',
15
+ name: 'testNode',
16
+ group: ['input'],
17
+ version: 1,
18
+ description: 'A test node',
19
+ defaults: { name: 'Test Node' },
20
+ inputs: ${inputs},
21
+ outputs: ${outputs},
22
+ properties: [],
23
+ };
24
+ }`;
25
+ }
26
+
27
+ /** Lets a test spell the connection key itself, e.g. `'inputs'` or `['inputs']`. */
28
+ function createNodeCodeWithProperty(property: string): string {
29
+ return `
30
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
31
+ import { NodeConnectionTypes } from 'n8n-workflow';
32
+
33
+ export class TestNode implements INodeType {
34
+ description: INodeTypeDescription = {
35
+ displayName: 'Test Node',
36
+ name: 'testNode',
37
+ group: ['input'],
38
+ version: 1,
39
+ description: 'A test node',
40
+ defaults: { name: 'Test Node' },
41
+ ${property},
42
+ outputs: [NodeConnectionTypes.Main],
43
+ properties: [],
44
+ };
45
+ }`;
46
+ }
47
+
48
+ const MAIN = '[NodeConnectionTypes.Main]';
49
+
50
+ ruleTester.run('no-unsafe-connection-type-cast', NoUnsafeConnectionTypeCastRule, {
51
+ valid: [
52
+ {
53
+ name: 'class that does not implement INodeType',
54
+ code: 'export class RegularClass {\n\tsomeProperty = "value";\n}',
55
+ },
56
+ {
57
+ name: 'node without casts',
58
+ code: createNodeCode(MAIN, MAIN),
59
+ },
60
+ {
61
+ name: 'node with empty inputs and outputs',
62
+ code: createNodeCode('[]', '[]'),
63
+ },
64
+ {
65
+ name: 'dynamic inputs as a template literal expression',
66
+ code: createNodeCode('`={{ (() => [NodeConnectionTypes.Main])() }}`', MAIN),
67
+ },
68
+ {
69
+ name: 'as const narrows rather than erases',
70
+ code: createNodeCode(`${MAIN} as const`, MAIN),
71
+ },
72
+ {
73
+ name: 'satisfies keeps the type checked',
74
+ code: createNodeCode(`${MAIN} satisfies NodeConnectionType[]`, MAIN),
75
+ },
76
+ {
77
+ name: 'a named type assertion is still checked structurally',
78
+ code: createNodeCode(`${MAIN} as NodeConnectionType[]`, MAIN),
79
+ },
80
+ {
81
+ name: 'casts elsewhere in the description are untouched',
82
+ code: createNodeCode(MAIN, MAIN).replace('version: 1,', 'version: 1 as never,'),
83
+ },
84
+ ],
85
+ invalid: [
86
+ {
87
+ name: 'as never on inputs',
88
+ code: createNodeCode("['main'] as never", MAIN),
89
+ errors: [
90
+ {
91
+ messageId: 'erasingCast',
92
+ data: { typeName: 'never', property: 'inputs' },
93
+ suggestions: [
94
+ {
95
+ messageId: 'removeCast',
96
+ output: createNodeCode("['main']", MAIN),
97
+ },
98
+ ],
99
+ },
100
+ ],
101
+ },
102
+ {
103
+ name: 'as never on outputs',
104
+ code: createNodeCode(MAIN, "['main'] as never"),
105
+ errors: [
106
+ {
107
+ messageId: 'erasingCast',
108
+ data: { typeName: 'never', property: 'outputs' },
109
+ suggestions: [
110
+ {
111
+ messageId: 'removeCast',
112
+ output: createNodeCode(MAIN, "['main']"),
113
+ },
114
+ ],
115
+ },
116
+ ],
117
+ },
118
+ {
119
+ name: 'as never on both inputs and outputs',
120
+ code: createNodeCode("['main'] as never", "['main'] as never"),
121
+ errors: [
122
+ {
123
+ messageId: 'erasingCast',
124
+ data: { typeName: 'never', property: 'inputs' },
125
+ suggestions: [
126
+ {
127
+ messageId: 'removeCast',
128
+ output: createNodeCode("['main']", "['main'] as never"),
129
+ },
130
+ ],
131
+ },
132
+ {
133
+ messageId: 'erasingCast',
134
+ data: { typeName: 'never', property: 'outputs' },
135
+ suggestions: [
136
+ {
137
+ messageId: 'removeCast',
138
+ output: createNodeCode("['main'] as never", "['main']"),
139
+ },
140
+ ],
141
+ },
142
+ ],
143
+ },
144
+ {
145
+ name: 'as any',
146
+ code: createNodeCode("['main'] as any", MAIN),
147
+ errors: [
148
+ {
149
+ messageId: 'erasingCast',
150
+ data: { typeName: 'any', property: 'inputs' },
151
+ suggestions: [
152
+ {
153
+ messageId: 'removeCast',
154
+ output: createNodeCode("['main']", MAIN),
155
+ },
156
+ ],
157
+ },
158
+ ],
159
+ },
160
+ {
161
+ name: 'as unknown',
162
+ code: createNodeCode("['main'] as unknown", MAIN),
163
+ errors: [
164
+ {
165
+ messageId: 'erasingCast',
166
+ data: { typeName: 'unknown', property: 'inputs' },
167
+ suggestions: [
168
+ {
169
+ messageId: 'removeCast',
170
+ output: createNodeCode("['main']", MAIN),
171
+ },
172
+ ],
173
+ },
174
+ ],
175
+ },
176
+ {
177
+ name: 'double cast through unknown removes the whole chain',
178
+ code: createNodeCode("['main'] as unknown as NodeConnectionType[]", MAIN),
179
+ errors: [
180
+ {
181
+ messageId: 'erasingCast',
182
+ data: { typeName: 'unknown', property: 'inputs' },
183
+ suggestions: [
184
+ {
185
+ messageId: 'removeCast',
186
+ output: createNodeCode("['main']", MAIN),
187
+ },
188
+ ],
189
+ },
190
+ ],
191
+ },
192
+ {
193
+ name: 'angle-bracket assertion',
194
+ code: createNodeCode("<never>['main']", MAIN),
195
+ errors: [
196
+ {
197
+ messageId: 'erasingCast',
198
+ data: { typeName: 'never', property: 'inputs' },
199
+ suggestions: [
200
+ {
201
+ messageId: 'removeCast',
202
+ output: createNodeCode("['main']", MAIN),
203
+ },
204
+ ],
205
+ },
206
+ ],
207
+ },
208
+ {
209
+ name: 'erasing cast hidden behind an outer satisfies',
210
+ code: createNodeCode("['main'] as never satisfies NodeConnectionType[]", MAIN),
211
+ errors: [
212
+ {
213
+ messageId: 'erasingCast',
214
+ data: { typeName: 'never', property: 'inputs' },
215
+ suggestions: [
216
+ {
217
+ messageId: 'removeCast',
218
+ output: createNodeCode("['main'] satisfies NodeConnectionType[]", MAIN),
219
+ },
220
+ ],
221
+ },
222
+ ],
223
+ },
224
+ {
225
+ name: 'erasing cast wrapped around an inner satisfies',
226
+ code: createNodeCode("['main'] satisfies NodeConnectionType[] as never", MAIN),
227
+ errors: [
228
+ {
229
+ messageId: 'erasingCast',
230
+ data: { typeName: 'never', property: 'inputs' },
231
+ suggestions: [
232
+ {
233
+ messageId: 'removeCast',
234
+ output: createNodeCode("['main'] satisfies NodeConnectionType[]", MAIN),
235
+ },
236
+ ],
237
+ },
238
+ ],
239
+ },
240
+ {
241
+ name: 'cast nested under satisfies, with a second cast outside it',
242
+ code: createNodeCode("['main'] as never satisfies NodeConnectionType[] as unknown", MAIN),
243
+ errors: [
244
+ {
245
+ messageId: 'erasingCast',
246
+ data: { typeName: 'unknown', property: 'inputs' },
247
+ suggestions: [
248
+ {
249
+ messageId: 'removeCast',
250
+ // Both casts go, the satisfies between them stays.
251
+ output: createNodeCode("['main'] satisfies NodeConnectionType[]", MAIN),
252
+ },
253
+ ],
254
+ },
255
+ ],
256
+ },
257
+ {
258
+ name: 'quoted connection key',
259
+ code: createNodeCodeWithProperty("'inputs': ['main'] as never"),
260
+ errors: [
261
+ {
262
+ messageId: 'erasingCast',
263
+ data: { typeName: 'never', property: 'inputs' },
264
+ suggestions: [
265
+ {
266
+ messageId: 'removeCast',
267
+ output: createNodeCodeWithProperty("'inputs': ['main']"),
268
+ },
269
+ ],
270
+ },
271
+ ],
272
+ },
273
+ {
274
+ name: 'computed connection key',
275
+ code: createNodeCodeWithProperty("['inputs']: ['main'] as never"),
276
+ errors: [
277
+ {
278
+ messageId: 'erasingCast',
279
+ data: { typeName: 'never', property: 'inputs' },
280
+ suggestions: [
281
+ {
282
+ messageId: 'removeCast',
283
+ output: createNodeCodeWithProperty("['inputs']: ['main']"),
284
+ },
285
+ ],
286
+ },
287
+ ],
288
+ },
289
+ {
290
+ name: 'cast hiding a malformed declaration',
291
+ code: createNodeCode("'not-an-array' as never", MAIN),
292
+ errors: [
293
+ {
294
+ messageId: 'erasingCast',
295
+ data: { typeName: 'never', property: 'inputs' },
296
+ suggestions: [
297
+ {
298
+ messageId: 'removeCast',
299
+ output: createNodeCode("'not-an-array'", MAIN),
300
+ },
301
+ ],
302
+ },
303
+ ],
304
+ },
305
+ ],
306
+ });