@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.
- package/dist/rules/no-silent-error-swallowing.d.ts.map +1 -1
- package/dist/rules/no-silent-error-swallowing.js +2 -12
- package/dist/rules/webhook-lifecycle-complete.d.ts.map +1 -1
- package/dist/rules/webhook-lifecycle-complete.js +3 -11
- package/dist/utils/ast-utils.d.ts +6 -0
- package/dist/utils/ast-utils.d.ts.map +1 -1
- package/dist/utils/ast-utils.js +22 -5
- package/package.json +2 -2
- package/src/rules/no-silent-error-swallowing.test.ts +26 -0
- package/src/rules/no-silent-error-swallowing.ts +2 -9
- package/src/rules/webhook-lifecycle-complete.test.ts +42 -0
- package/src/rules/webhook-lifecycle-complete.ts +3 -12
- package/src/utils/ast-utils.ts +29 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-silent-error-swallowing.d.ts","sourceRoot":"","sources":["../../src/rules/no-silent-error-swallowing.ts"],"names":[],"mappings":"
|
|
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 =
|
|
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":"
|
|
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
|
|
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
|
|
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":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAezD,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,CAAC,gBAAgB,GAAG,OAAO,CAUxE;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,QAAQ,CAAC,gBAAgB,GAAG,OAAO,CAE9E;AAED,sGAAsG;AACtG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,CAAC,gBAAgB,GAAG,OAAO,CAE3E;AAWD;;;;GAIG;AACH,wBAAgB,aAAa,CAC5B,IAAI,EAAE,QAAQ,CAAC,gBAAgB,EAC/B,gBAAgB,EAAE,QAAQ,CAAC,gBAAgB,GACzC,OAAO,CAET;AAED,wBAAgB,iBAAiB,CAChC,IAAI,EAAE,QAAQ,CAAC,gBAAgB,EAC/B,YAAY,EAAE,MAAM,GAClB,QAAQ,CAAC,kBAAkB,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"ast-utils.d.ts","sourceRoot":"","sources":["../../src/utils/ast-utils.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/ast-utils.js
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
|
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;
|
package/src/utils/ast-utils.ts
CHANGED
|
@@ -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
|
|
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
|
}
|