@n8n/eslint-plugin-community-nodes 0.11.0 → 0.12.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 (35) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/README.md +3 -0
  3. package/dist/plugin.d.ts +18 -0
  4. package/dist/plugin.d.ts.map +1 -1
  5. package/dist/plugin.js +6 -0
  6. package/dist/plugin.js.map +1 -1
  7. package/dist/rules/index.d.ts +3 -0
  8. package/dist/rules/index.d.ts.map +1 -1
  9. package/dist/rules/index.js +6 -0
  10. package/dist/rules/index.js.map +1 -1
  11. package/dist/rules/missing-paired-item.d.ts +16 -0
  12. package/dist/rules/missing-paired-item.d.ts.map +1 -0
  13. package/dist/rules/missing-paired-item.js +121 -0
  14. package/dist/rules/missing-paired-item.js.map +1 -0
  15. package/dist/rules/no-forbidden-lifecycle-scripts.d.ts +2 -0
  16. package/dist/rules/no-forbidden-lifecycle-scripts.d.ts.map +1 -0
  17. package/dist/rules/no-forbidden-lifecycle-scripts.js +59 -0
  18. package/dist/rules/no-forbidden-lifecycle-scripts.js.map +1 -0
  19. package/dist/rules/require-continue-on-fail.d.ts +2 -0
  20. package/dist/rules/require-continue-on-fail.d.ts.map +1 -0
  21. package/dist/rules/require-continue-on-fail.js +76 -0
  22. package/dist/rules/require-continue-on-fail.js.map +1 -0
  23. package/docs/rules/missing-paired-item.md +70 -0
  24. package/docs/rules/no-forbidden-lifecycle-scripts.md +46 -0
  25. package/docs/rules/require-continue-on-fail.md +56 -0
  26. package/package.json +2 -2
  27. package/src/plugin.ts +6 -0
  28. package/src/rules/index.ts +6 -0
  29. package/src/rules/missing-paired-item.test.ts +229 -0
  30. package/src/rules/missing-paired-item.ts +149 -0
  31. package/src/rules/no-forbidden-lifecycle-scripts.test.ts +103 -0
  32. package/src/rules/no-forbidden-lifecycle-scripts.ts +69 -0
  33. package/src/rules/require-continue-on-fail.test.ts +129 -0
  34. package/src/rules/require-continue-on-fail.ts +88 -0
  35. package/tsconfig.build.tsbuildinfo +1 -0
@@ -0,0 +1,70 @@
1
+ # Require pairedItem on INodeExecutionData objects in execute() methods to preserve item linking (`@n8n/community-nodes/missing-paired-item`)
2
+
3
+ 💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
4
+
5
+ <!-- end auto-generated rule header -->
6
+
7
+ ## Rule Details
8
+
9
+ Every `INodeExecutionData` object returned from `execute()` should include a `pairedItem` property. Without it, downstream nodes cannot trace data lineage and expressions like `$('NodeName').item` will silently fail.
10
+
11
+ The rule detects object literals with a `json` property but no `pairedItem` inside `execute()` methods of `INodeType` classes.
12
+
13
+ ## Examples
14
+
15
+ ### Incorrect
16
+
17
+ ```typescript
18
+ class MyNode implements INodeType {
19
+ async execute() {
20
+ const items = this.getInputData();
21
+ // Missing pairedItem
22
+ return [items.map((item) => ({ json: item.json }))];
23
+ }
24
+ }
25
+ ```
26
+
27
+ ```typescript
28
+ class MyNode implements INodeType {
29
+ async execute() {
30
+ const returnData: INodeExecutionData[] = [];
31
+ // Missing pairedItem
32
+ returnData.push({ json: { result: true } });
33
+ return [returnData];
34
+ }
35
+ }
36
+ ```
37
+
38
+ ### Correct
39
+
40
+ ```typescript
41
+ class MyNode implements INodeType {
42
+ async execute() {
43
+ const items = this.getInputData();
44
+ return [items.map((item, index) => ({ json: item.json, pairedItem: { item: index } }))];
45
+ }
46
+ }
47
+ ```
48
+
49
+ ```typescript
50
+ class MyNode implements INodeType {
51
+ async execute() {
52
+ const returnData: INodeExecutionData[] = [];
53
+ returnData.push({ json: { result: true }, pairedItem: { item: 0 } });
54
+ return [returnData];
55
+ }
56
+ }
57
+ ```
58
+
59
+ ## When to Disable
60
+
61
+ If your node intentionally does not support item linking (e.g. it aggregates all input items into a single output), you can suppress this rule:
62
+
63
+ ```typescript
64
+ // eslint-disable-next-line @n8n/community-nodes/missing-paired-item
65
+ returnData.push({ json: aggregatedResult });
66
+ ```
67
+
68
+ ## Further Reading
69
+
70
+ - [n8n Paired Items Documentation](https://docs.n8n.io/integrations/creating-nodes/build/reference/paired-items/)
@@ -0,0 +1,46 @@
1
+ # Ban lifecycle scripts (prepare, preinstall, postinstall, etc.) in community node packages (`@n8n/community-nodes/no-forbidden-lifecycle-scripts`)
2
+
3
+ 💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
4
+
5
+ <!-- end auto-generated rule header -->
6
+
7
+ ## Rule Details
8
+
9
+ npm lifecycle scripts (`prepare`, `preinstall`, `install`, `postinstall`, `prepublish`, `preprepare`, `postprepare`) run automatically — without user confirmation — during `npm install`. In the context of n8n community nodes, this means arbitrary code executes on the n8n instance the moment a community node is installed.
10
+
11
+ n8n community nodes are distributed as pre-built npm packages. Unlike regular npm libraries, there is no legitimate reason for a community node to hook into install-time lifecycle events — the package should already contain compiled code ready to use. A `prepare` or `postinstall` script in a community node is either a misconfiguration (the author forgot to remove a build step meant for development) or a supply-chain attack vector.
12
+
13
+ ## Examples
14
+
15
+ ### Incorrect
16
+
17
+ ```json
18
+ {
19
+ "name": "n8n-nodes-example",
20
+ "scripts": {
21
+ "prepare": "npm run build"
22
+ }
23
+ }
24
+ ```
25
+
26
+ ```json
27
+ {
28
+ "name": "n8n-nodes-example",
29
+ "scripts": {
30
+ "build": "tsc",
31
+ "postinstall": "node setup.js"
32
+ }
33
+ }
34
+ ```
35
+
36
+ ### Correct
37
+
38
+ ```json
39
+ {
40
+ "name": "n8n-nodes-example",
41
+ "scripts": {
42
+ "build": "tsc",
43
+ "test": "jest"
44
+ }
45
+ }
46
+ ```
@@ -0,0 +1,56 @@
1
+ # Require continueOnFail() handling in execute() methods of node classes (`@n8n/community-nodes/require-continue-on-fail`)
2
+
3
+ 💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
4
+
5
+ <!-- end auto-generated rule header -->
6
+
7
+ ## Rule Details
8
+
9
+ Ensures that `execute()` methods in node classes include a `this.continueOnFail()` check. Without this, a single item error will abort the entire workflow instead of allowing execution to continue past the failing item.
10
+
11
+ ## Examples
12
+
13
+ ### ❌ Incorrect
14
+
15
+ ```typescript
16
+ export class MyNode implements INodeType {
17
+ description: INodeTypeDescription = { /* ... */ };
18
+
19
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
20
+ const items = this.getInputData();
21
+ const returnData: INodeExecutionData[] = [];
22
+ for (let i = 0; i < items.length; i++) {
23
+ // No error handling — one bad item kills the whole workflow
24
+ const result = await someApiCall(items[i]);
25
+ returnData.push({ json: result });
26
+ }
27
+ return [returnData];
28
+ }
29
+ }
30
+ ```
31
+
32
+ ### ✅ Correct
33
+
34
+ ```typescript
35
+ export class MyNode implements INodeType {
36
+ description: INodeTypeDescription = { /* ... */ };
37
+
38
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
39
+ const items = this.getInputData();
40
+ const returnData: INodeExecutionData[] = [];
41
+ for (let i = 0; i < items.length; i++) {
42
+ try {
43
+ const result = await someApiCall(items[i]);
44
+ returnData.push({ json: result });
45
+ } catch (error) {
46
+ if (this.continueOnFail()) {
47
+ returnData.push({ json: { error: error.message }, pairedItem: { item: i } });
48
+ continue;
49
+ }
50
+ throw error;
51
+ }
52
+ }
53
+ return [returnData];
54
+ }
55
+ }
56
+ ```
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.11.0",
4
+ "version": "0.12.0",
5
5
  "main": "./dist/plugin.js",
6
6
  "types": "./dist/plugin.d.ts",
7
7
  "exports": {
@@ -24,7 +24,7 @@
24
24
  "vitest": "^4.1.1",
25
25
  "@n8n/typescript-config": "1.4.0",
26
26
  "@n8n/vitest-config": "1.9.0",
27
- "n8n-workflow": "2.16.0"
27
+ "n8n-workflow": "2.17.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "eslint": ">= 9",
package/src/plugin.ts CHANGED
@@ -29,6 +29,7 @@ const configs = {
29
29
  '@n8n/community-nodes/package-name-convention': 'error',
30
30
  '@n8n/community-nodes/credential-test-required': 'error',
31
31
  '@n8n/community-nodes/no-credential-reuse': 'error',
32
+ '@n8n/community-nodes/no-forbidden-lifecycle-scripts': 'error',
32
33
  '@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
33
34
  '@n8n/community-nodes/icon-validation': 'error',
34
35
  '@n8n/community-nodes/options-sorted-alphabetically': 'warn',
@@ -37,6 +38,8 @@ const configs = {
37
38
  '@n8n/community-nodes/node-class-description-icon-missing': 'error',
38
39
  '@n8n/community-nodes/cred-class-field-icon-missing': 'error',
39
40
  '@n8n/community-nodes/node-connection-type-literal': 'error',
41
+ '@n8n/community-nodes/missing-paired-item': 'error',
42
+ '@n8n/community-nodes/require-continue-on-fail': 'error',
40
43
  },
41
44
  },
42
45
  recommendedWithoutN8nCloudSupport: {
@@ -52,6 +55,7 @@ const configs = {
52
55
  '@n8n/community-nodes/package-name-convention': 'error',
53
56
  '@n8n/community-nodes/credential-test-required': 'error',
54
57
  '@n8n/community-nodes/no-credential-reuse': 'error',
58
+ '@n8n/community-nodes/no-forbidden-lifecycle-scripts': 'error',
55
59
  '@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
56
60
  '@n8n/community-nodes/icon-validation': 'error',
57
61
  '@n8n/community-nodes/options-sorted-alphabetically': 'warn',
@@ -60,6 +64,8 @@ const configs = {
60
64
  '@n8n/community-nodes/node-class-description-icon-missing': 'error',
61
65
  '@n8n/community-nodes/cred-class-field-icon-missing': 'error',
62
66
  '@n8n/community-nodes/node-connection-type-literal': 'error',
67
+ '@n8n/community-nodes/missing-paired-item': 'error',
68
+ '@n8n/community-nodes/require-continue-on-fail': 'error',
63
69
  },
64
70
  },
65
71
  } satisfies Record<string, Linter.Config>;
@@ -6,8 +6,10 @@ import { CredentialDocumentationUrlRule } from './credential-documentation-url.j
6
6
  import { CredentialPasswordFieldRule } from './credential-password-field.js';
7
7
  import { CredentialTestRequiredRule } from './credential-test-required.js';
8
8
  import { IconValidationRule } from './icon-validation.js';
9
+ import { MissingPairedItemRule } from './missing-paired-item.js';
9
10
  import { NoCredentialReuseRule } from './no-credential-reuse.js';
10
11
  import { NoDeprecatedWorkflowFunctionsRule } from './no-deprecated-workflow-functions.js';
12
+ import { NoForbiddenLifecycleScriptsRule } from './no-forbidden-lifecycle-scripts.js';
11
13
  import { NoHttpRequestWithManualAuthRule } from './no-http-request-with-manual-auth.js';
12
14
  import { NoRestrictedGlobalsRule } from './no-restricted-globals.js';
13
15
  import { NoRestrictedImportsRule } from './no-restricted-imports.js';
@@ -16,6 +18,7 @@ import { NodeConnectionTypeLiteralRule } from './node-connection-type-literal.js
16
18
  import { NodeUsableAsToolRule } from './node-usable-as-tool.js';
17
19
  import { OptionsSortedAlphabeticallyRule } from './options-sorted-alphabetically.js';
18
20
  import { PackageNameConventionRule } from './package-name-convention.js';
21
+ import { RequireContinueOnFailRule } from './require-continue-on-fail.js';
19
22
  import { ResourceOperationPatternRule } from './resource-operation-pattern.js';
20
23
 
21
24
  export const rules = {
@@ -29,6 +32,7 @@ export const rules = {
29
32
  'package-name-convention': PackageNameConventionRule,
30
33
  'credential-test-required': CredentialTestRequiredRule,
31
34
  'no-credential-reuse': NoCredentialReuseRule,
35
+ 'no-forbidden-lifecycle-scripts': NoForbiddenLifecycleScriptsRule,
32
36
  'no-http-request-with-manual-auth': NoHttpRequestWithManualAuthRule,
33
37
  'icon-validation': IconValidationRule,
34
38
  'resource-operation-pattern': ResourceOperationPatternRule,
@@ -36,4 +40,6 @@ export const rules = {
36
40
  'node-class-description-icon-missing': NodeClassDescriptionIconMissingRule,
37
41
  'cred-class-field-icon-missing': CredClassFieldIconMissingRule,
38
42
  'node-connection-type-literal': NodeConnectionTypeLiteralRule,
43
+ 'missing-paired-item': MissingPairedItemRule,
44
+ 'require-continue-on-fail': RequireContinueOnFailRule,
39
45
  } satisfies Record<string, AnyRuleModule>;
@@ -0,0 +1,229 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { MissingPairedItemRule } from './missing-paired-item.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ ruleTester.run('missing-paired-item', MissingPairedItemRule, {
8
+ valid: [
9
+ {
10
+ name: 'object with json and pairedItem in execute()',
11
+ filename: 'MyNode.node.ts',
12
+ code: `
13
+ class MyNode implements INodeType {
14
+ description = {};
15
+ async execute() {
16
+ return [[{ json: { id: 1 }, pairedItem: { item: 0 } }]];
17
+ }
18
+ }`,
19
+ },
20
+ {
21
+ name: 'object with json and pairedItem as number shorthand',
22
+ filename: 'MyNode.node.ts',
23
+ code: `
24
+ class MyNode implements INodeType {
25
+ description = {};
26
+ async execute() {
27
+ return [[{ json: { id: 1 }, pairedItem: 0 }]];
28
+ }
29
+ }`,
30
+ },
31
+ {
32
+ name: 'non-node-type class is ignored',
33
+ filename: 'MyNode.node.ts',
34
+ code: `
35
+ class Helper {
36
+ async execute() {
37
+ return [[{ json: { id: 1 } }]];
38
+ }
39
+ }`,
40
+ },
41
+ {
42
+ name: 'non-.node.ts file is ignored',
43
+ filename: 'utils.ts',
44
+ code: `
45
+ class MyNode implements INodeType {
46
+ description = {};
47
+ async execute() {
48
+ return [[{ json: { id: 1 } }]];
49
+ }
50
+ }`,
51
+ },
52
+ {
53
+ name: 'object outside execute() is ignored',
54
+ filename: 'MyNode.node.ts',
55
+ code: `
56
+ class MyNode implements INodeType {
57
+ description = {};
58
+ async someHelper() {
59
+ return { json: { id: 1 } };
60
+ }
61
+ }`,
62
+ },
63
+ {
64
+ name: 'object without json property is ignored',
65
+ filename: 'MyNode.node.ts',
66
+ code: `
67
+ class MyNode implements INodeType {
68
+ description = {};
69
+ async execute() {
70
+ const options = { url: 'https://api.example.com', method: 'GET' };
71
+ return [[{ json: options, pairedItem: { item: 0 } }]];
72
+ }
73
+ }`,
74
+ },
75
+ {
76
+ name: 'object with unrecognized keys is not INodeExecutionData',
77
+ filename: 'MyNode.node.ts',
78
+ code: `
79
+ class MyNode implements INodeType {
80
+ description = {};
81
+ async execute() {
82
+ const config = { json: true, indent: 2, spaces: 4 };
83
+ return this.process(config);
84
+ }
85
+ }`,
86
+ },
87
+ {
88
+ name: 'class extending Node base class with pairedItem',
89
+ filename: 'MyNode.node.ts',
90
+ code: `
91
+ class MyNode extends Node {
92
+ description = {};
93
+ async execute() {
94
+ return [[{ json: { id: 1 }, pairedItem: { item: 0 } }]];
95
+ }
96
+ }`,
97
+ },
98
+ {
99
+ name: 'spread element in object is allowed',
100
+ filename: 'MyNode.node.ts',
101
+ code: `
102
+ class MyNode implements INodeType {
103
+ description = {};
104
+ async execute() {
105
+ return [[{ ...baseItem, json: { id: 1 } }]];
106
+ }
107
+ }`,
108
+ },
109
+ {
110
+ name: 'object inside constructExecutionMetaData is allowed',
111
+ filename: 'MyNode.node.ts',
112
+ code: `
113
+ class MyNode implements INodeType {
114
+ description = {};
115
+ async execute() {
116
+ const executionData = this.helpers.constructExecutionMetaData(
117
+ [{ json: { success: true } }],
118
+ { itemData: { item: i } },
119
+ );
120
+ return [executionData];
121
+ }
122
+ }`,
123
+ },
124
+ {
125
+ name: 'object with computed property key is not INodeExecutionData',
126
+ filename: 'MyNode.node.ts',
127
+ code: `
128
+ class MyNode implements INodeType {
129
+ description = {};
130
+ async execute() {
131
+ const obj = { json: data, [dynamicKey]: value };
132
+ return [[obj]];
133
+ }
134
+ }`,
135
+ },
136
+ ],
137
+ invalid: [
138
+ {
139
+ name: 'missing pairedItem in return statement',
140
+ filename: 'MyNode.node.ts',
141
+ code: `
142
+ class MyNode implements INodeType {
143
+ description = {};
144
+ async execute() {
145
+ return [[{ json: { id: 1 } }]];
146
+ }
147
+ }`,
148
+ errors: [{ messageId: 'missingPairedItem' }],
149
+ },
150
+ {
151
+ name: 'missing pairedItem in .map() callback',
152
+ filename: 'MyNode.node.ts',
153
+ code: `
154
+ class MyNode implements INodeType {
155
+ description = {};
156
+ async execute() {
157
+ const items = this.getInputData();
158
+ return [items.map((item, index) => ({ json: item.json }))];
159
+ }
160
+ }`,
161
+ errors: [{ messageId: 'missingPairedItem' }],
162
+ },
163
+ {
164
+ name: 'missing pairedItem in .push() call',
165
+ filename: 'MyNode.node.ts',
166
+ code: `
167
+ class MyNode implements INodeType {
168
+ description = {};
169
+ async execute() {
170
+ const returnData = [];
171
+ returnData.push({ json: { result: true } });
172
+ return [returnData];
173
+ }
174
+ }`,
175
+ errors: [{ messageId: 'missingPairedItem' }],
176
+ },
177
+ {
178
+ name: 'multiple objects missing pairedItem',
179
+ filename: 'MyNode.node.ts',
180
+ code: `
181
+ class MyNode implements INodeType {
182
+ description = {};
183
+ async execute() {
184
+ const returnData = [];
185
+ returnData.push({ json: { a: 1 } });
186
+ returnData.push({ json: { b: 2 } });
187
+ return [returnData];
188
+ }
189
+ }`,
190
+ errors: [{ messageId: 'missingPairedItem' }, { messageId: 'missingPairedItem' }],
191
+ },
192
+ {
193
+ name: 'object with json and binary but no pairedItem',
194
+ filename: 'MyNode.node.ts',
195
+ code: `
196
+ class MyNode implements INodeType {
197
+ description = {};
198
+ async execute() {
199
+ return [[{ json: { id: 1 }, binary: { data: binaryData } }]];
200
+ }
201
+ }`,
202
+ errors: [{ messageId: 'missingPairedItem' }],
203
+ },
204
+ {
205
+ name: 'object with json and executionStatus but no pairedItem',
206
+ filename: 'MyNode.node.ts',
207
+ code: `
208
+ class MyNode implements INodeType {
209
+ description = {};
210
+ async execute() {
211
+ return [[{ json: { id: 1 }, executionStatus: 'success' }]];
212
+ }
213
+ }`,
214
+ errors: [{ messageId: 'missingPairedItem' }],
215
+ },
216
+ {
217
+ name: 'class extending Node base class missing pairedItem',
218
+ filename: 'MyNode.node.ts',
219
+ code: `
220
+ class MyNode extends Node {
221
+ description = {};
222
+ async execute() {
223
+ return [[{ json: { id: 1 } }]];
224
+ }
225
+ }`,
226
+ errors: [{ messageId: 'missingPairedItem' }],
227
+ },
228
+ ],
229
+ });
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Flags object literals with a `json` property but no `pairedItem` property
3
+ * inside `execute()` methods of `INodeType` classes.
4
+ *
5
+ * Missing `pairedItem` breaks downstream item-referencing expressions like
6
+ * `$('NodeName').item`. This rule catches the three most common patterns:
7
+ *
8
+ * - Object literals in `.map()` callbacks
9
+ * - Object literals passed to `.push()` calls
10
+ * - Object literals in return statements (typically `return [[{ json }]]`)
11
+ *
12
+ * Only flags object literals directly — variable references are skipped since
13
+ * their shape cannot be determined without type resolution.
14
+ */
15
+
16
+ import type { TSESTree } from '@typescript-eslint/utils';
17
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
18
+
19
+ import {
20
+ createRule,
21
+ findObjectProperty,
22
+ isFileType,
23
+ isNodeTypeClass,
24
+ isThisHelpersMethodCall,
25
+ } from '../utils/index.js';
26
+
27
+ /**
28
+ * Checks whether the object is inside an array argument to
29
+ * `this.helpers.constructExecutionMetaData()`, which adds pairedItem
30
+ * via the second argument's `itemData` property.
31
+ */
32
+ function isInsideConstructExecutionMetaData(node: TSESTree.ObjectExpression): boolean {
33
+ // Pattern: constructExecutionMetaData([{ json: ... }], { itemData: ... })
34
+ // Walk up: ObjectExpression -> ArrayExpression -> CallExpression
35
+ const parent = node.parent;
36
+ if (parent?.type !== AST_NODE_TYPES.ArrayExpression) return false;
37
+
38
+ const grandparent = parent.parent;
39
+ if (grandparent?.type !== AST_NODE_TYPES.CallExpression) return false;
40
+
41
+ // Check it's the first argument
42
+ if (grandparent.arguments[0] !== parent) return false;
43
+
44
+ return isThisHelpersMethodCall(grandparent, 'constructExecutionMetaData');
45
+ }
46
+
47
+ export const MissingPairedItemRule = createRule({
48
+ name: 'missing-paired-item',
49
+ meta: {
50
+ type: 'problem',
51
+ docs: {
52
+ description:
53
+ 'Require pairedItem on INodeExecutionData objects in execute() methods to preserve item linking.',
54
+ },
55
+ messages: {
56
+ missingPairedItem:
57
+ 'Missing pairedItem on INodeExecutionData object. Add `pairedItem: { item: index }` to preserve item linking. See https://docs.n8n.io/integrations/creating-nodes/build/reference/paired-items/',
58
+ },
59
+ schema: [],
60
+ hasSuggestions: false,
61
+ },
62
+ defaultOptions: [],
63
+ create(context) {
64
+ if (!isFileType(context.filename, '.node.ts')) {
65
+ return {};
66
+ }
67
+
68
+ let inNodeTypeClass = false;
69
+ let inExecuteMethod = false;
70
+
71
+ return {
72
+ ClassDeclaration(node) {
73
+ if (isNodeTypeClass(node)) {
74
+ inNodeTypeClass = true;
75
+ }
76
+ },
77
+
78
+ 'ClassDeclaration:exit'() {
79
+ inNodeTypeClass = false;
80
+ inExecuteMethod = false;
81
+ },
82
+
83
+ MethodDefinition(node: TSESTree.MethodDefinition) {
84
+ if (
85
+ inNodeTypeClass &&
86
+ node.key.type === AST_NODE_TYPES.Identifier &&
87
+ node.key.name === 'execute'
88
+ ) {
89
+ inExecuteMethod = true;
90
+ }
91
+ },
92
+
93
+ 'MethodDefinition:exit'(node: TSESTree.MethodDefinition) {
94
+ if (
95
+ inExecuteMethod &&
96
+ node.key.type === AST_NODE_TYPES.Identifier &&
97
+ node.key.name === 'execute'
98
+ ) {
99
+ inExecuteMethod = false;
100
+ }
101
+ },
102
+
103
+ ObjectExpression(node: TSESTree.ObjectExpression) {
104
+ if (!inExecuteMethod) return;
105
+
106
+ const hasJson = findObjectProperty(node, 'json') !== null;
107
+ if (!hasJson) return;
108
+
109
+ const hasPairedItem = findObjectProperty(node, 'pairedItem') !== null;
110
+ if (hasPairedItem) return;
111
+
112
+ // Skip if inside constructExecutionMetaData() — it adds pairedItem via itemData
113
+ if (isInsideConstructExecutionMetaData(node)) return;
114
+
115
+ // Skip if the object contains spread elements — they may already provide pairedItem
116
+ const hasSpread = node.properties.some(
117
+ (prop) => prop.type === AST_NODE_TYPES.SpreadElement,
118
+ );
119
+ if (hasSpread) return;
120
+
121
+ // Only flag if this looks like an INodeExecutionData object literal —
122
+ // must have `json` and optionally `binary`/`error`, nothing unexpected.
123
+ // Objects with many unrelated keys are likely not INodeExecutionData.
124
+ const knownKeys = new Set([
125
+ 'json',
126
+ 'binary',
127
+ 'error',
128
+ 'pairedItem',
129
+ 'executionStatus',
130
+ 'metadata',
131
+ 'evaluationData',
132
+ 'redaction',
133
+ 'sendMessage',
134
+ 'index',
135
+ ]);
136
+ const allPropertiesKnown = node.properties.every(
137
+ (prop) =>
138
+ prop.type === AST_NODE_TYPES.Property &&
139
+ prop.key.type === AST_NODE_TYPES.Identifier &&
140
+ knownKeys.has(prop.key.name),
141
+ );
142
+
143
+ if (!allPropertiesKnown) return;
144
+
145
+ context.report({ node, messageId: 'missingPairedItem' });
146
+ },
147
+ };
148
+ },
149
+ });