@n8n/eslint-plugin-community-nodes 0.33.0 → 0.34.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.
@@ -1 +1 @@
1
- {"version":3,"file":"no-silent-error-swallowing.d.ts","sourceRoot":"","sources":["../../src/rules/no-silent-error-swallowing.ts"],"names":[],"mappings":"AA6CA,eAAO,MAAM,2BAA2B;;CAiFtC,CAAC"}
1
+ {"version":3,"file":"no-silent-error-swallowing.d.ts","sourceRoot":"","sources":["../../src/rules/no-silent-error-swallowing.ts"],"names":[],"mappings":"AAsCA,eAAO,MAAM,2BAA2B;;CAiFtC,CAAC"}
@@ -1,16 +1,6 @@
1
1
  import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
- import { createRule, findClassProperty, isNodeTypeClass, WEBHOOK_LIFECYCLE_METHODS, } from '../utils/index.js';
2
+ import { createRule, findClassProperty, getPropertyKeyName, isNodeTypeClass, WEBHOOK_LIFECYCLE_METHODS, } from '../utils/index.js';
3
3
  const LIFECYCLE_METHODS = WEBHOOK_LIFECYCLE_METHODS;
4
- /** Returns the static name of a non-computed object property key, or null. */
5
- function getPropertyName(property) {
6
- if (property.computed)
7
- return null;
8
- if (property.key.type === AST_NODE_TYPES.Identifier)
9
- return property.key.name;
10
- if (property.key.type === AST_NODE_TYPES.Literal)
11
- return String(property.key.value);
12
- return null;
13
- }
14
4
  function isFunctionNode(node) {
15
5
  return (node.type === AST_NODE_TYPES.FunctionDeclaration ||
16
6
  node.type === AST_NODE_TYPES.FunctionExpression ||
@@ -70,7 +60,7 @@ export const NoSilentErrorSwallowingRule = createRule({
70
60
  for (const methodProperty of groupProperty.value.properties) {
71
61
  if (methodProperty.type !== AST_NODE_TYPES.Property)
72
62
  continue;
73
- const methodName = getPropertyName(methodProperty);
63
+ const methodName = getPropertyKeyName(methodProperty);
74
64
  if (methodName === null || !LIFECYCLE_METHODS.includes(methodName))
75
65
  continue;
76
66
  const fn = methodProperty.value;
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-lifecycle-complete.d.ts","sourceRoot":"","sources":["../../src/rules/webhook-lifecycle-complete.ts"],"names":[],"mappings":"AA8CA,eAAO,MAAM,4BAA4B;;CAkFvC,CAAC"}
1
+ {"version":3,"file":"webhook-lifecycle-complete.d.ts","sourceRoot":"","sources":["../../src/rules/webhook-lifecycle-complete.ts"],"names":[],"mappings":"AA0CA,eAAO,MAAM,4BAA4B;;CA6EvC,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
- import { createRule, findClassProperty, findObjectProperty, isNodeTypeClass, WEBHOOK_LIFECYCLE_METHODS, } from '../utils/index.js';
2
+ import { createRule, findClassProperty, findObjectProperty, getPropertyKeyName, isNodeTypeClass, WEBHOOK_LIFECYCLE_METHODS, } from '../utils/index.js';
3
3
  /**
4
4
  * Returns true if the description declares webhook endpoints, indicating the
5
5
  * node is a webhook-based trigger that needs a complete lifecycle.
@@ -17,11 +17,7 @@ function hasWebhooksDeclared(descriptionValue) {
17
17
  function isMethodProperty(property, name) {
18
18
  if (property.type !== AST_NODE_TYPES.Property)
19
19
  return false;
20
- if (property.computed)
21
- return false;
22
- const keyMatches = (property.key.type === AST_NODE_TYPES.Identifier && property.key.name === name) ||
23
- (property.key.type === AST_NODE_TYPES.Literal && property.key.value === name);
24
- if (!keyMatches)
20
+ if (getPropertyKeyName(property) !== name)
25
21
  return false;
26
22
  return (property.value.type === AST_NODE_TYPES.FunctionExpression ||
27
23
  property.value.type === AST_NODE_TYPES.ArrowFunctionExpression);
@@ -81,11 +77,7 @@ export const WebhookLifecycleCompleteRule = createRule({
81
77
  continue;
82
78
  if (groupProperty.value.type !== AST_NODE_TYPES.ObjectExpression)
83
79
  continue;
84
- const groupName = groupProperty.key.type === AST_NODE_TYPES.Identifier
85
- ? groupProperty.key.name
86
- : groupProperty.key.type === AST_NODE_TYPES.Literal
87
- ? String(groupProperty.key.value)
88
- : 'default';
80
+ const groupName = getPropertyKeyName(groupProperty) ?? 'default';
89
81
  const missing = findMissingMethods(groupProperty.value);
90
82
  if (missing.length === 0)
91
83
  continue;
@@ -9,6 +9,12 @@ export declare function isTriggerNodeClass(node: TSESTree.ClassDeclaration): boo
9
9
  * `group` values.
10
10
  */
11
11
  export declare function isTriggerNode(node: TSESTree.ClassDeclaration, descriptionValue: TSESTree.ObjectExpression): boolean;
12
+ /**
13
+ * Returns the name a property key stands for, or null when it is only known at
14
+ * runtime. `delete`, `'delete'` and `['delete']` all name the same property, so
15
+ * how the key is written must not decide whether a rule sees it.
16
+ */
17
+ export declare function getPropertyKeyName(property: TSESTree.Property | TSESTree.PropertyDefinition | TSESTree.MethodDefinition): string | null;
12
18
  export declare function findClassProperty(node: TSESTree.ClassDeclaration, propertyName: string): TSESTree.PropertyDefinition | null;
13
19
  /**
14
20
  * Returns the `description` object literal of a node class. Versioned nodes
@@ -1 +1 @@
1
- {"version":3,"file":"ast-utils.d.ts","sourceRoot":"","sources":["../../src/utils/ast-utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ast-utils.d.ts","sourceRoot":"","sources":["../../src/utils/ast-utils.ts"],"names":[],"mappings":"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"}
@@ -33,10 +33,29 @@ function hasTriggerGroup(descriptionValue) {
33
33
  export function isTriggerNode(node, descriptionValue) {
34
34
  return hasTriggerGroup(descriptionValue) || isTriggerNodeClass(node);
35
35
  }
36
+ /**
37
+ * Returns the name a property key stands for, or null when it is only known at
38
+ * runtime. `delete`, `'delete'` and `['delete']` all name the same property, so
39
+ * how the key is written must not decide whether a rule sees it.
40
+ */
41
+ export function getPropertyKeyName(property) {
42
+ const { key } = property;
43
+ if (key.type === AST_NODE_TYPES.Literal) {
44
+ return key.value === null ? null : String(key.value);
45
+ }
46
+ // A computed key still names one property when it is a template without
47
+ // substitutions; written plainly, an identifier is the name itself.
48
+ if (key.type === AST_NODE_TYPES.TemplateLiteral) {
49
+ return key.expressions.length === 0 ? (key.quasis[0]?.value.cooked ?? null) : null;
50
+ }
51
+ if (key.type === AST_NODE_TYPES.Identifier) {
52
+ return property.computed ? null : key.name;
53
+ }
54
+ return null;
55
+ }
36
56
  export function findClassProperty(node, propertyName) {
37
57
  const property = node.body.body.find((member) => member.type === AST_NODE_TYPES.PropertyDefinition &&
38
- member.key?.type === AST_NODE_TYPES.Identifier &&
39
- member.key.name === propertyName);
58
+ getPropertyKeyName(member) === propertyName);
40
59
  return property?.type === AST_NODE_TYPES.PropertyDefinition ? property : null;
41
60
  }
42
61
  /**
@@ -90,9 +109,7 @@ export function findNodeDescriptionObject(node) {
90
109
  return findConstructorAssignedDescription(node);
91
110
  }
92
111
  export function findObjectProperty(obj, propertyName) {
93
- const property = obj.properties.find((prop) => prop.type === AST_NODE_TYPES.Property &&
94
- prop.key.type === AST_NODE_TYPES.Identifier &&
95
- prop.key.name === propertyName);
112
+ const property = obj.properties.find((prop) => prop.type === AST_NODE_TYPES.Property && getPropertyKeyName(prop) === propertyName);
96
113
  return property?.type === AST_NODE_TYPES.Property ? property : null;
97
114
  }
98
115
  export function getLiteralValue(node) {
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.33.0",
4
+ "version": "0.34.0",
5
5
  "main": "./dist/plugin.js",
6
6
  "types": "./dist/plugin.d.ts",
7
7
  "exports": {
@@ -19,7 +19,7 @@
19
19
  "@n8n/typescript-config": "1.12.0",
20
20
  "@n8n/vitest-config": "1.22.0",
21
21
  "@types/node": "24.10.1",
22
- "n8n-workflow": "2.40.0",
22
+ "n8n-workflow": "2.41.0",
23
23
  "@typescript-eslint/rule-tester": "8.70.0",
24
24
  "eslint-doc-generator": "^2.2.2",
25
25
  "eslint-plugin-eslint-plugin": "^7.0.0",
@@ -174,6 +174,32 @@ export class RegularClass {
174
174
  }`),
175
175
  errors: [{ messageId: 'silentReturn', data: { method: 'delete' } }],
176
176
  },
177
+ {
178
+ name: 'lifecycle method written with a computed literal key',
179
+ code: createTriggerNode(`{
180
+ default: {
181
+ ['delete']: async function (this: IHookFunctions): Promise<boolean> {
182
+ try {
183
+ return await this.helpers.httpRequest({ url: 'https://example.com' });
184
+ } catch (error) {}
185
+ },
186
+ },
187
+ }`),
188
+ errors: [{ messageId: 'emptyCatch', data: { method: 'delete' } }],
189
+ },
190
+ {
191
+ name: 'lifecycle group written with a quoted key',
192
+ code: createTriggerNode(`{
193
+ 'default': {
194
+ async delete(this: IHookFunctions): Promise<boolean> {
195
+ try {
196
+ return await this.helpers.httpRequest({ url: 'https://example.com' });
197
+ } catch (error) {}
198
+ },
199
+ },
200
+ }`),
201
+ errors: [{ messageId: 'emptyCatch', data: { method: 'delete' } }],
202
+ },
177
203
  {
178
204
  name: 'catch block with a bare return',
179
205
  code: createTriggerNode(`{
@@ -3,20 +3,13 @@ import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils';
3
3
  import {
4
4
  createRule,
5
5
  findClassProperty,
6
+ getPropertyKeyName,
6
7
  isNodeTypeClass,
7
8
  WEBHOOK_LIFECYCLE_METHODS,
8
9
  } from '../utils/index.js';
9
10
 
10
11
  const LIFECYCLE_METHODS: readonly string[] = WEBHOOK_LIFECYCLE_METHODS;
11
12
 
12
- /** Returns the static name of a non-computed object property key, or null. */
13
- function getPropertyName(property: TSESTree.Property): string | null {
14
- if (property.computed) return null;
15
- if (property.key.type === AST_NODE_TYPES.Identifier) return property.key.name;
16
- if (property.key.type === AST_NODE_TYPES.Literal) return String(property.key.value);
17
- return null;
18
- }
19
-
20
13
  function isFunctionNode(node: TSESTree.Node): boolean {
21
14
  return (
22
15
  node.type === AST_NODE_TYPES.FunctionDeclaration ||
@@ -81,7 +74,7 @@ export const NoSilentErrorSwallowingRule = createRule({
81
74
  for (const methodProperty of groupProperty.value.properties) {
82
75
  if (methodProperty.type !== AST_NODE_TYPES.Property) continue;
83
76
 
84
- const methodName = getPropertyName(methodProperty);
77
+ const methodName = getPropertyKeyName(methodProperty);
85
78
  if (methodName === null || !LIFECYCLE_METHODS.includes(methodName)) continue;
86
79
 
87
80
  const fn = methodProperty.value;
@@ -88,6 +88,30 @@ export class RegularClass {
88
88
  }`,
89
89
  }),
90
90
  },
91
+ {
92
+ name: 'lifecycle methods written with computed literal keys',
93
+ code: createTriggerNode({
94
+ webhookMethods: `{
95
+ default: {
96
+ ['checkExists']: async () => true,
97
+ ['create']: async () => true,
98
+ [\`delete\`]: async () => true,
99
+ },
100
+ }`,
101
+ }),
102
+ },
103
+ {
104
+ name: 'lifecycle group and methods written with quoted keys',
105
+ code: createTriggerNode({
106
+ webhookMethods: `{
107
+ 'default': {
108
+ 'checkExists': async () => true,
109
+ 'create': async () => true,
110
+ 'delete': async () => true,
111
+ },
112
+ }`,
113
+ }),
114
+ },
91
115
  {
92
116
  name: 'webhook lifecycle defined via arrow functions',
93
117
  code: createTriggerNode({
@@ -189,6 +213,24 @@ export class RegularClass {
189
213
  }),
190
214
  errors: [{ messageId: 'missingWebhookMethods' }],
191
215
  },
216
+ {
217
+ name: 'lifecycle method behind a key only known at runtime',
218
+ code: createTriggerNode({
219
+ webhookMethods: `{
220
+ default: {
221
+ async checkExists() { return true; },
222
+ async create() { return true; },
223
+ [methodName]: async () => true,
224
+ },
225
+ }`,
226
+ }),
227
+ errors: [
228
+ {
229
+ messageId: 'missingLifecycleMethod',
230
+ data: { group: 'default', missing: '`delete`' },
231
+ },
232
+ ],
233
+ },
192
234
  {
193
235
  name: 'multiple webhook groups each missing methods',
194
236
  code: createTriggerNode({
@@ -4,6 +4,7 @@ import {
4
4
  createRule,
5
5
  findClassProperty,
6
6
  findObjectProperty,
7
+ getPropertyKeyName,
7
8
  isNodeTypeClass,
8
9
  WEBHOOK_LIFECYCLE_METHODS,
9
10
  type WebhookLifecycleMethod,
@@ -25,12 +26,7 @@ function hasWebhooksDeclared(descriptionValue: TSESTree.ObjectExpression): boole
25
26
  /** Returns true when the property defines a (possibly async) method named `name`. */
26
27
  function isMethodProperty(property: TSESTree.ObjectLiteralElement, name: string): boolean {
27
28
  if (property.type !== AST_NODE_TYPES.Property) return false;
28
- if (property.computed) return false;
29
-
30
- const keyMatches =
31
- (property.key.type === AST_NODE_TYPES.Identifier && property.key.name === name) ||
32
- (property.key.type === AST_NODE_TYPES.Literal && property.key.value === name);
33
- if (!keyMatches) return false;
29
+ if (getPropertyKeyName(property) !== name) return false;
34
30
 
35
31
  return (
36
32
  property.value.type === AST_NODE_TYPES.FunctionExpression ||
@@ -104,12 +100,7 @@ export const WebhookLifecycleCompleteRule = createRule({
104
100
  if (groupProperty.type !== AST_NODE_TYPES.Property) continue;
105
101
  if (groupProperty.value.type !== AST_NODE_TYPES.ObjectExpression) continue;
106
102
 
107
- const groupName =
108
- groupProperty.key.type === AST_NODE_TYPES.Identifier
109
- ? groupProperty.key.name
110
- : groupProperty.key.type === AST_NODE_TYPES.Literal
111
- ? String(groupProperty.key.value)
112
- : 'default';
103
+ const groupName = getPropertyKeyName(groupProperty) ?? 'default';
113
104
 
114
105
  const missing = findMissingMethods(groupProperty.value);
115
106
  if (missing.length === 0) continue;
@@ -55,6 +55,33 @@ export function isTriggerNode(
55
55
  return hasTriggerGroup(descriptionValue) || isTriggerNodeClass(node);
56
56
  }
57
57
 
58
+ /**
59
+ * Returns the name a property key stands for, or null when it is only known at
60
+ * runtime. `delete`, `'delete'` and `['delete']` all name the same property, so
61
+ * how the key is written must not decide whether a rule sees it.
62
+ */
63
+ export function getPropertyKeyName(
64
+ property: TSESTree.Property | TSESTree.PropertyDefinition | TSESTree.MethodDefinition,
65
+ ): string | null {
66
+ const { key } = property;
67
+
68
+ if (key.type === AST_NODE_TYPES.Literal) {
69
+ return key.value === null ? null : String(key.value);
70
+ }
71
+
72
+ // A computed key still names one property when it is a template without
73
+ // substitutions; written plainly, an identifier is the name itself.
74
+ if (key.type === AST_NODE_TYPES.TemplateLiteral) {
75
+ return key.expressions.length === 0 ? (key.quasis[0]?.value.cooked ?? null) : null;
76
+ }
77
+
78
+ if (key.type === AST_NODE_TYPES.Identifier) {
79
+ return property.computed ? null : key.name;
80
+ }
81
+
82
+ return null;
83
+ }
84
+
58
85
  export function findClassProperty(
59
86
  node: TSESTree.ClassDeclaration,
60
87
  propertyName: string,
@@ -62,8 +89,7 @@ export function findClassProperty(
62
89
  const property = node.body.body.find(
63
90
  (member) =>
64
91
  member.type === AST_NODE_TYPES.PropertyDefinition &&
65
- member.key?.type === AST_NODE_TYPES.Identifier &&
66
- member.key.name === propertyName,
92
+ getPropertyKeyName(member) === propertyName,
67
93
  );
68
94
  return property?.type === AST_NODE_TYPES.PropertyDefinition ? property : null;
69
95
  }
@@ -140,10 +166,7 @@ export function findObjectProperty(
140
166
  propertyName: string,
141
167
  ): TSESTree.Property | null {
142
168
  const property = obj.properties.find(
143
- (prop) =>
144
- prop.type === AST_NODE_TYPES.Property &&
145
- prop.key.type === AST_NODE_TYPES.Identifier &&
146
- prop.key.name === propertyName,
169
+ (prop) => prop.type === AST_NODE_TYPES.Property && getPropertyKeyName(prop) === propertyName,
147
170
  );
148
171
  return property?.type === AST_NODE_TYPES.Property ? property : null;
149
172
  }