@n8n/eslint-plugin-community-nodes 0.3.0 → 0.5.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 (114) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/README.md +60 -0
  3. package/dist/plugin.d.ts +144 -28
  4. package/dist/plugin.d.ts.map +1 -1
  5. package/dist/plugin.js +28 -25
  6. package/dist/plugin.js.map +1 -1
  7. package/dist/rules/credential-documentation-url.d.ts +7 -0
  8. package/dist/rules/credential-documentation-url.d.ts.map +1 -0
  9. package/dist/rules/credential-documentation-url.js +100 -0
  10. package/dist/rules/credential-documentation-url.js.map +1 -0
  11. package/dist/rules/credential-password-field.d.ts +1 -2
  12. package/dist/rules/credential-password-field.d.ts.map +1 -1
  13. package/dist/rules/credential-password-field.js +25 -14
  14. package/dist/rules/credential-password-field.js.map +1 -1
  15. package/dist/rules/credential-test-required.d.ts +1 -2
  16. package/dist/rules/credential-test-required.d.ts.map +1 -1
  17. package/dist/rules/credential-test-required.js +43 -5
  18. package/dist/rules/credential-test-required.js.map +1 -1
  19. package/dist/rules/icon-validation.d.ts +1 -2
  20. package/dist/rules/icon-validation.d.ts.map +1 -1
  21. package/dist/rules/icon-validation.js +81 -15
  22. package/dist/rules/icon-validation.js.map +1 -1
  23. package/dist/rules/index.d.ts +9 -5
  24. package/dist/rules/index.d.ts.map +1 -1
  25. package/dist/rules/index.js +7 -5
  26. package/dist/rules/index.js.map +1 -1
  27. package/dist/rules/no-credential-reuse.d.ts +1 -2
  28. package/dist/rules/no-credential-reuse.d.ts.map +1 -1
  29. package/dist/rules/no-credential-reuse.js +33 -4
  30. package/dist/rules/no-credential-reuse.js.map +1 -1
  31. package/dist/rules/no-deprecated-workflow-functions.d.ts +1 -2
  32. package/dist/rules/no-deprecated-workflow-functions.d.ts.map +1 -1
  33. package/dist/rules/no-deprecated-workflow-functions.js +38 -10
  34. package/dist/rules/no-deprecated-workflow-functions.js.map +1 -1
  35. package/dist/rules/no-restricted-globals.d.ts +2 -2
  36. package/dist/rules/no-restricted-globals.d.ts.map +1 -1
  37. package/dist/rules/no-restricted-globals.js +5 -3
  38. package/dist/rules/no-restricted-globals.js.map +1 -1
  39. package/dist/rules/no-restricted-imports.d.ts +1 -2
  40. package/dist/rules/no-restricted-imports.d.ts.map +1 -1
  41. package/dist/rules/no-restricted-imports.js +3 -3
  42. package/dist/rules/no-restricted-imports.js.map +1 -1
  43. package/dist/rules/node-usable-as-tool.d.ts +1 -2
  44. package/dist/rules/node-usable-as-tool.d.ts.map +1 -1
  45. package/dist/rules/node-usable-as-tool.js +6 -5
  46. package/dist/rules/node-usable-as-tool.js.map +1 -1
  47. package/dist/rules/package-name-convention.d.ts +1 -2
  48. package/dist/rules/package-name-convention.d.ts.map +1 -1
  49. package/dist/rules/package-name-convention.js +38 -2
  50. package/dist/rules/package-name-convention.js.map +1 -1
  51. package/dist/rules/resource-operation-pattern.d.ts +1 -2
  52. package/dist/rules/resource-operation-pattern.d.ts.map +1 -1
  53. package/dist/rules/resource-operation-pattern.js +9 -7
  54. package/dist/rules/resource-operation-pattern.js.map +1 -1
  55. package/dist/utils/ast-utils.d.ts +2 -1
  56. package/dist/utils/ast-utils.d.ts.map +1 -1
  57. package/dist/utils/ast-utils.js +37 -19
  58. package/dist/utils/ast-utils.js.map +1 -1
  59. package/dist/utils/file-utils.d.ts +14 -0
  60. package/dist/utils/file-utils.d.ts.map +1 -1
  61. package/dist/utils/file-utils.js +85 -18
  62. package/dist/utils/file-utils.js.map +1 -1
  63. package/dist/utils/index.d.ts +1 -0
  64. package/dist/utils/index.d.ts.map +1 -1
  65. package/dist/utils/index.js +1 -0
  66. package/dist/utils/index.js.map +1 -1
  67. package/dist/utils/rule-creator.d.ts +3 -0
  68. package/dist/utils/rule-creator.d.ts.map +1 -0
  69. package/dist/utils/rule-creator.js +5 -0
  70. package/dist/utils/rule-creator.js.map +1 -0
  71. package/docs/rules/credential-documentation-url.md +94 -0
  72. package/docs/rules/credential-password-field.md +45 -0
  73. package/docs/rules/credential-test-required.md +58 -0
  74. package/docs/rules/icon-validation.md +67 -0
  75. package/docs/rules/no-credential-reuse.md +82 -0
  76. package/docs/rules/no-deprecated-workflow-functions.md +61 -0
  77. package/docs/rules/no-restricted-globals.md +44 -0
  78. package/docs/rules/no-restricted-imports.md +47 -0
  79. package/docs/rules/node-usable-as-tool.md +43 -0
  80. package/docs/rules/package-name-convention.md +52 -0
  81. package/docs/rules/resource-operation-pattern.md +84 -0
  82. package/eslint.config.mjs +27 -0
  83. package/package.json +25 -4
  84. package/src/plugin.ts +30 -26
  85. package/src/rules/credential-documentation-url.test.ts +306 -0
  86. package/src/rules/credential-documentation-url.ts +129 -0
  87. package/src/rules/credential-password-field.test.ts +1 -0
  88. package/src/rules/credential-password-field.ts +34 -16
  89. package/src/rules/credential-test-required.test.ts +84 -57
  90. package/src/rules/credential-test-required.ts +51 -5
  91. package/src/rules/icon-validation.test.ts +97 -14
  92. package/src/rules/icon-validation.ts +95 -14
  93. package/src/rules/index.ts +8 -5
  94. package/src/rules/no-credential-reuse.test.ts +306 -58
  95. package/src/rules/no-credential-reuse.ts +43 -3
  96. package/src/rules/no-deprecated-workflow-functions.test.ts +70 -0
  97. package/src/rules/no-deprecated-workflow-functions.ts +44 -10
  98. package/src/rules/no-restricted-globals.test.ts +1 -0
  99. package/src/rules/no-restricted-globals.ts +6 -3
  100. package/src/rules/no-restricted-imports.test.ts +1 -0
  101. package/src/rules/no-restricted-imports.ts +8 -3
  102. package/src/rules/node-usable-as-tool.test.ts +1 -0
  103. package/src/rules/node-usable-as-tool.ts +8 -6
  104. package/src/rules/package-name-convention.test.ts +82 -5
  105. package/src/rules/package-name-convention.ts +46 -2
  106. package/src/rules/resource-operation-pattern.test.ts +1 -0
  107. package/src/rules/resource-operation-pattern.ts +13 -6
  108. package/src/utils/ast-utils.ts +47 -19
  109. package/src/utils/file-utils.ts +108 -18
  110. package/src/utils/index.ts +1 -0
  111. package/src/utils/rule-creator.ts +6 -0
  112. package/tsconfig.build.json +4 -0
  113. package/tsconfig.eslint.json +5 -0
  114. package/tsconfig.json +1 -2
@@ -1,35 +1,27 @@
1
1
  import { RuleTester } from '@typescript-eslint/rule-tester';
2
- import { NoCredentialReuseRule } from './no-credential-reuse.js';
3
2
  import { vi } from 'vitest';
4
- import * as fs from 'node:fs';
5
3
 
6
- const ruleTester = new RuleTester();
4
+ import { NoCredentialReuseRule } from './no-credential-reuse.js';
5
+ import * as fileUtils from '../utils/file-utils.js';
6
+
7
+ vi.mock('../utils/file-utils.js', async () => {
8
+ const actual = await vi.importActual('../utils/file-utils.js');
9
+ return {
10
+ ...actual,
11
+ readPackageJsonCredentials: vi.fn(),
12
+ findPackageJson: vi.fn(),
13
+ };
14
+ });
7
15
 
8
- // Mock fs functions
9
- vi.mock('node:fs', () => ({
10
- readFileSync: vi.fn(),
11
- existsSync: vi.fn(),
12
- }));
16
+ const mockReadPackageJsonCredentials = vi.mocked(fileUtils.readPackageJsonCredentials);
17
+ const mockFindPackageJson = vi.mocked(fileUtils.findPackageJson);
13
18
 
14
- const mockReadFileSync = vi.mocked(fs.readFileSync);
15
- const mockExistsSync = vi.mocked(fs.existsSync);
19
+ const ruleTester = new RuleTester();
16
20
 
17
21
  const nodeFilePath = '/tmp/TestNode.node.ts';
18
22
 
19
- function createCredentialClass(name: string, displayName: string): string {
20
- return `
21
- import type { ICredentialType, INodeProperties } from 'n8n-workflow';
22
-
23
- export class ${name.charAt(0).toUpperCase() + name.slice(1)} implements ICredentialType {
24
- name = '${name}';
25
- displayName = '${displayName}';
26
- properties: INodeProperties[] = [];
27
- }
28
- `;
29
- }
30
-
31
23
  function createNodeCode(
32
- credentials: (string | { name: string; required?: boolean })[] = [],
24
+ credentials: Array<string | { name: string; required?: boolean }> = [],
33
25
  ): string {
34
26
  const credentialsArray =
35
27
  credentials.length > 0
@@ -66,6 +58,45 @@ export class TestNode implements INodeType {
66
58
  }`;
67
59
  }
68
60
 
61
+ // Helper function to create expected outputs with double quotes (matching rule fix behavior)
62
+ function createExpectedNodeCode(
63
+ credentials: Array<string | { name: string; required?: boolean }> = [],
64
+ ): string {
65
+ const credentialsArray =
66
+ credentials.length > 0
67
+ ? credentials
68
+ .map((cred) => {
69
+ if (typeof cred === 'string') {
70
+ return `"${cred}"`;
71
+ } else {
72
+ const required =
73
+ cred.required !== undefined ? `,\n\t\t\t\trequired: ${cred.required}` : '';
74
+ return `{\n\t\t\t\tname: "${cred.name}"${required},\n\t\t\t}`;
75
+ }
76
+ })
77
+ .join(',\n\t\t\t')
78
+ : '';
79
+
80
+ const credentialsProperty =
81
+ credentials.length > 0 ? `credentials: [\n\t\t\t${credentialsArray}\n\t\t],` : '';
82
+
83
+ return `
84
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
85
+
86
+ export class TestNode implements INodeType {
87
+ description: INodeTypeDescription = {
88
+ displayName: 'Test Node',
89
+ name: 'testNode',
90
+ group: ['output'],
91
+ version: 1,
92
+ inputs: ['main'],
93
+ outputs: ['main'],
94
+ ${credentialsProperty}
95
+ properties: [],
96
+ };
97
+ }`;
98
+ }
99
+
69
100
  // Helper function to create non-node class
70
101
  function createNonNodeClass(): string {
71
102
  return `
@@ -90,41 +121,10 @@ export class NotANode {
90
121
  }
91
122
 
92
123
  function setupMockFileSystem() {
93
- const packageJson = {
94
- name: 'test-package',
95
- n8n: {
96
- credentials: [
97
- 'dist/credentials/MyApi.credentials.js',
98
- 'dist/credentials/AnotherApi.credentials.js',
99
- ],
100
- },
101
- };
102
-
103
- const myApiCredential = createCredentialClass('myApiCredential', 'My API');
104
- const anotherApiCredential = createCredentialClass('anotherApiCredential', 'Another API');
105
-
106
- mockExistsSync.mockImplementation((path: fs.PathLike) => {
107
- const pathStr = path.toString();
108
- return (
109
- pathStr.includes('package.json') ||
110
- pathStr.includes('MyApi.credentials.ts') ||
111
- pathStr.includes('AnotherApi.credentials.ts')
112
- );
113
- });
114
-
115
- mockReadFileSync.mockImplementation((path: any): string => {
116
- const pathStr = path.toString();
117
- if (pathStr.includes('package.json')) {
118
- return JSON.stringify(packageJson, null, 2);
119
- }
120
- if (pathStr.includes('MyApi.credentials.ts')) {
121
- return myApiCredential;
122
- }
123
- if (pathStr.includes('AnotherApi.credentials.ts')) {
124
- return anotherApiCredential;
125
- }
126
- throw new Error(`File not found: ${pathStr}`);
127
- });
124
+ mockFindPackageJson.mockReturnValue('/tmp/package.json');
125
+ mockReadPackageJsonCredentials.mockReturnValue(
126
+ new Set(['myApiCredential', 'anotherApiCredential']),
127
+ );
128
128
  }
129
129
 
130
130
  setupMockFileSystem();
@@ -171,6 +171,18 @@ ruleTester.run('no-credential-reuse', NoCredentialReuseRule, {
171
171
  {
172
172
  messageId: 'credentialNotInPackage',
173
173
  data: { credentialName: 'ExternalApi' },
174
+ suggestions: [
175
+ {
176
+ messageId: 'useAvailable',
177
+ data: { suggestedName: 'myApiCredential' },
178
+ output: createExpectedNodeCode([{ name: 'myApiCredential', required: true }]),
179
+ },
180
+ {
181
+ messageId: 'useAvailable',
182
+ data: { suggestedName: 'anotherApiCredential' },
183
+ output: createExpectedNodeCode([{ name: 'anotherApiCredential', required: true }]),
184
+ },
185
+ ],
174
186
  },
175
187
  ],
176
188
  },
@@ -182,6 +194,18 @@ ruleTester.run('no-credential-reuse', NoCredentialReuseRule, {
182
194
  {
183
195
  messageId: 'credentialNotInPackage',
184
196
  data: { credentialName: 'ExternalApi' },
197
+ suggestions: [
198
+ {
199
+ messageId: 'useAvailable',
200
+ data: { suggestedName: 'myApiCredential' },
201
+ output: createExpectedNodeCode(['myApiCredential']),
202
+ },
203
+ {
204
+ messageId: 'useAvailable',
205
+ data: { suggestedName: 'anotherApiCredential' },
206
+ output: createExpectedNodeCode(['anotherApiCredential']),
207
+ },
208
+ ],
185
209
  },
186
210
  ],
187
211
  },
@@ -197,10 +221,118 @@ ruleTester.run('no-credential-reuse', NoCredentialReuseRule, {
197
221
  {
198
222
  messageId: 'credentialNotInPackage',
199
223
  data: { credentialName: 'ExternalApi' },
224
+ suggestions: [
225
+ {
226
+ messageId: 'useAvailable',
227
+ data: { suggestedName: 'myApiCredential' },
228
+ output: `
229
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
230
+
231
+ export class TestNode implements INodeType {
232
+ description: INodeTypeDescription = {
233
+ displayName: 'Test Node',
234
+ name: 'testNode',
235
+ group: ['output'],
236
+ version: 1,
237
+ inputs: ['main'],
238
+ outputs: ['main'],
239
+ credentials: [
240
+ 'myApiCredential',
241
+ {
242
+ name: "myApiCredential",
243
+ required: true,
244
+ },
245
+ 'AnotherExternalApi'
246
+ ],
247
+ properties: [],
248
+ };
249
+ }`,
250
+ },
251
+ {
252
+ messageId: 'useAvailable',
253
+ data: { suggestedName: 'anotherApiCredential' },
254
+ output: `
255
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
256
+
257
+ export class TestNode implements INodeType {
258
+ description: INodeTypeDescription = {
259
+ displayName: 'Test Node',
260
+ name: 'testNode',
261
+ group: ['output'],
262
+ version: 1,
263
+ inputs: ['main'],
264
+ outputs: ['main'],
265
+ credentials: [
266
+ 'myApiCredential',
267
+ {
268
+ name: "anotherApiCredential",
269
+ required: true,
270
+ },
271
+ 'AnotherExternalApi'
272
+ ],
273
+ properties: [],
274
+ };
275
+ }`,
276
+ },
277
+ ],
200
278
  },
201
279
  {
202
280
  messageId: 'credentialNotInPackage',
203
281
  data: { credentialName: 'AnotherExternalApi' },
282
+ suggestions: [
283
+ {
284
+ messageId: 'useAvailable',
285
+ data: { suggestedName: 'myApiCredential' },
286
+ output: `
287
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
288
+
289
+ export class TestNode implements INodeType {
290
+ description: INodeTypeDescription = {
291
+ displayName: 'Test Node',
292
+ name: 'testNode',
293
+ group: ['output'],
294
+ version: 1,
295
+ inputs: ['main'],
296
+ outputs: ['main'],
297
+ credentials: [
298
+ 'myApiCredential',
299
+ {
300
+ name: 'ExternalApi',
301
+ required: true,
302
+ },
303
+ "myApiCredential"
304
+ ],
305
+ properties: [],
306
+ };
307
+ }`,
308
+ },
309
+ {
310
+ messageId: 'useAvailable',
311
+ data: { suggestedName: 'anotherApiCredential' },
312
+ output: `
313
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
314
+
315
+ export class TestNode implements INodeType {
316
+ description: INodeTypeDescription = {
317
+ displayName: 'Test Node',
318
+ name: 'testNode',
319
+ group: ['output'],
320
+ version: 1,
321
+ inputs: ['main'],
322
+ outputs: ['main'],
323
+ credentials: [
324
+ 'myApiCredential',
325
+ {
326
+ name: 'ExternalApi',
327
+ required: true,
328
+ },
329
+ "anotherApiCredential"
330
+ ],
331
+ properties: [],
332
+ };
333
+ }`,
334
+ },
335
+ ],
204
336
  },
205
337
  ],
206
338
  },
@@ -215,10 +347,126 @@ ruleTester.run('no-credential-reuse', NoCredentialReuseRule, {
215
347
  {
216
348
  messageId: 'credentialNotInPackage',
217
349
  data: { credentialName: 'ExternalApi1' },
350
+ suggestions: [
351
+ {
352
+ messageId: 'useAvailable',
353
+ data: { suggestedName: 'myApiCredential' },
354
+ output: `
355
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
356
+
357
+ export class TestNode implements INodeType {
358
+ description: INodeTypeDescription = {
359
+ displayName: 'Test Node',
360
+ name: 'testNode',
361
+ group: ['output'],
362
+ version: 1,
363
+ inputs: ['main'],
364
+ outputs: ['main'],
365
+ credentials: [
366
+ {
367
+ name: "myApiCredential",
368
+ required: true,
369
+ },
370
+ {
371
+ name: 'ExternalApi2',
372
+ required: false,
373
+ }
374
+ ],
375
+ properties: [],
376
+ };
377
+ }`,
378
+ },
379
+ {
380
+ messageId: 'useAvailable',
381
+ data: { suggestedName: 'anotherApiCredential' },
382
+ output: `
383
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
384
+
385
+ export class TestNode implements INodeType {
386
+ description: INodeTypeDescription = {
387
+ displayName: 'Test Node',
388
+ name: 'testNode',
389
+ group: ['output'],
390
+ version: 1,
391
+ inputs: ['main'],
392
+ outputs: ['main'],
393
+ credentials: [
394
+ {
395
+ name: "anotherApiCredential",
396
+ required: true,
397
+ },
398
+ {
399
+ name: 'ExternalApi2',
400
+ required: false,
401
+ }
402
+ ],
403
+ properties: [],
404
+ };
405
+ }`,
406
+ },
407
+ ],
218
408
  },
219
409
  {
220
410
  messageId: 'credentialNotInPackage',
221
411
  data: { credentialName: 'ExternalApi2' },
412
+ suggestions: [
413
+ {
414
+ messageId: 'useAvailable',
415
+ data: { suggestedName: 'myApiCredential' },
416
+ output: `
417
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
418
+
419
+ export class TestNode implements INodeType {
420
+ description: INodeTypeDescription = {
421
+ displayName: 'Test Node',
422
+ name: 'testNode',
423
+ group: ['output'],
424
+ version: 1,
425
+ inputs: ['main'],
426
+ outputs: ['main'],
427
+ credentials: [
428
+ {
429
+ name: 'ExternalApi1',
430
+ required: true,
431
+ },
432
+ {
433
+ name: "myApiCredential",
434
+ required: false,
435
+ }
436
+ ],
437
+ properties: [],
438
+ };
439
+ }`,
440
+ },
441
+ {
442
+ messageId: 'useAvailable',
443
+ data: { suggestedName: 'anotherApiCredential' },
444
+ output: `
445
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
446
+
447
+ export class TestNode implements INodeType {
448
+ description: INodeTypeDescription = {
449
+ displayName: 'Test Node',
450
+ name: 'testNode',
451
+ group: ['output'],
452
+ version: 1,
453
+ inputs: ['main'],
454
+ outputs: ['main'],
455
+ credentials: [
456
+ {
457
+ name: 'ExternalApi1',
458
+ required: true,
459
+ },
460
+ {
461
+ name: "anotherApiCredential",
462
+ required: false,
463
+ }
464
+ ],
465
+ properties: [],
466
+ };
467
+ }`,
468
+ },
469
+ ],
222
470
  },
223
471
  ],
224
472
  },
@@ -1,4 +1,6 @@
1
- import { ESLintUtils } from '@typescript-eslint/utils';
1
+ import { TSESTree } from '@typescript-eslint/types';
2
+ import type { ReportSuggestionArray } from '@typescript-eslint/utils/ts-eslint';
3
+
2
4
  import {
3
5
  isNodeTypeClass,
4
6
  findClassProperty,
@@ -7,9 +9,12 @@ import {
7
9
  findPackageJson,
8
10
  readPackageJsonCredentials,
9
11
  isFileType,
12
+ findSimilarStrings,
13
+ createRule,
10
14
  } from '../utils/index.js';
11
15
 
12
- export const NoCredentialReuseRule = ESLintUtils.RuleCreator.withoutDocs({
16
+ export const NoCredentialReuseRule = createRule({
17
+ name: 'no-credential-reuse',
13
18
  meta: {
14
19
  type: 'problem',
15
20
  docs: {
@@ -17,10 +22,13 @@ export const NoCredentialReuseRule = ESLintUtils.RuleCreator.withoutDocs({
17
22
  'Prevent credential re-use security issues by ensuring nodes only reference credentials from the same package',
18
23
  },
19
24
  messages: {
25
+ didYouMean: "Did you mean '{{ suggestedName }}'?",
26
+ useAvailable: "Use available credential '{{ suggestedName }}'",
20
27
  credentialNotInPackage:
21
28
  'SECURITY: Node references credential "{{ credentialName }}" which is not defined in this package. This creates a security risk as it attempts to reuse credentials from other packages. Nodes can only use credentials from the same package as listed in package.json n8n.credentials field.',
22
29
  },
23
30
  schema: [],
31
+ hasSuggestions: true,
24
32
  },
25
33
  defaultOptions: [],
26
34
  create(context) {
@@ -52,7 +60,10 @@ export const NoCredentialReuseRule = ESLintUtils.RuleCreator.withoutDocs({
52
60
  }
53
61
 
54
62
  const descriptionProperty = findClassProperty(node, 'description');
55
- if (!descriptionProperty?.value || descriptionProperty.value.type !== 'ObjectExpression') {
63
+ if (
64
+ !descriptionProperty?.value ||
65
+ descriptionProperty.value.type !== TSESTree.AST_NODE_TYPES.ObjectExpression
66
+ ) {
56
67
  return;
57
68
  }
58
69
 
@@ -66,12 +77,41 @@ export const NoCredentialReuseRule = ESLintUtils.RuleCreator.withoutDocs({
66
77
  credentialsArray.elements.forEach((element) => {
67
78
  const credentialInfo = extractCredentialNameFromArray(element);
68
79
  if (credentialInfo && !allowedCredentials.has(credentialInfo.name)) {
80
+ const similarCredentials = findSimilarStrings(credentialInfo.name, allowedCredentials);
81
+ const suggestions: ReportSuggestionArray<
82
+ 'didYouMean' | 'useAvailable' | 'credentialNotInPackage'
83
+ > = [];
84
+
85
+ for (const similarName of similarCredentials) {
86
+ suggestions.push({
87
+ messageId: 'didYouMean',
88
+ data: { suggestedName: similarName },
89
+ fix(fixer) {
90
+ return fixer.replaceText(credentialInfo.node, `"${similarName}"`);
91
+ },
92
+ });
93
+ }
94
+
95
+ if (suggestions.length === 0 && allowedCredentials.size > 0) {
96
+ const availableCredentials = Array.from(allowedCredentials).slice(0, 3);
97
+ for (const availableName of availableCredentials) {
98
+ suggestions.push({
99
+ messageId: 'useAvailable',
100
+ data: { suggestedName: availableName },
101
+ fix(fixer) {
102
+ return fixer.replaceText(credentialInfo.node, `"${availableName}"`);
103
+ },
104
+ });
105
+ }
106
+ }
107
+
69
108
  context.report({
70
109
  node: credentialInfo.node,
71
110
  messageId: 'credentialNotInPackage',
72
111
  data: {
73
112
  credentialName: credentialInfo.name,
74
113
  },
114
+ suggest: suggestions,
75
115
  });
76
116
  }
77
117
  });
@@ -1,4 +1,5 @@
1
1
  import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
2
3
  import { NoDeprecatedWorkflowFunctionsRule } from './no-deprecated-workflow-functions.js';
3
4
 
4
5
  const ruleTester = new RuleTester();
@@ -60,6 +61,16 @@ const response3 = await this.helpers.requestOAuth2.call(this, 'google', options)
60
61
  {
61
62
  messageId: 'deprecatedRequestFunction',
62
63
  data: { functionName: 'request', replacement: 'httpRequest' },
64
+ suggestions: [
65
+ {
66
+ messageId: 'suggestReplaceFunction',
67
+ data: { functionName: 'request', replacement: 'httpRequest' },
68
+ output: `
69
+ const response1 = await this.helpers.httpRequest('https://example.com/1');
70
+ const response2 = await this.helpers.requestWithAuthentication.call(this, 'oauth', options);
71
+ const response3 = await this.helpers.requestOAuth2.call(this, 'google', options);`,
72
+ },
73
+ ],
63
74
  },
64
75
  {
65
76
  messageId: 'deprecatedRequestFunction',
@@ -67,10 +78,33 @@ const response3 = await this.helpers.requestOAuth2.call(this, 'google', options)
67
78
  functionName: 'requestWithAuthentication',
68
79
  replacement: 'httpRequestWithAuthentication',
69
80
  },
81
+ suggestions: [
82
+ {
83
+ messageId: 'suggestReplaceFunction',
84
+ data: {
85
+ functionName: 'requestWithAuthentication',
86
+ replacement: 'httpRequestWithAuthentication',
87
+ },
88
+ output: `
89
+ const response1 = await this.helpers.request('https://example.com/1');
90
+ const response2 = await this.helpers.httpRequestWithAuthentication.call(this, 'oauth', options);
91
+ const response3 = await this.helpers.requestOAuth2.call(this, 'google', options);`,
92
+ },
93
+ ],
70
94
  },
71
95
  {
72
96
  messageId: 'deprecatedRequestFunction',
73
97
  data: { functionName: 'requestOAuth2', replacement: 'httpRequestWithAuthentication' },
98
+ suggestions: [
99
+ {
100
+ messageId: 'suggestReplaceFunction',
101
+ data: { functionName: 'requestOAuth2', replacement: 'httpRequestWithAuthentication' },
102
+ output: `
103
+ const response1 = await this.helpers.request('https://example.com/1');
104
+ const response2 = await this.helpers.requestWithAuthentication.call(this, 'oauth', options);
105
+ const response3 = await this.helpers.httpRequestWithAuthentication.call(this, 'google', options);`,
106
+ },
107
+ ],
74
108
  },
75
109
  ],
76
110
  },
@@ -86,14 +120,50 @@ function makeRequest(options: IRequestOptions): Promise<any> {
86
120
  {
87
121
  messageId: 'deprecatedType',
88
122
  data: { typeName: 'IRequestOptions', replacement: 'IHttpRequestOptions' },
123
+ suggestions: [
124
+ {
125
+ messageId: 'suggestReplaceType',
126
+ data: { typeName: 'IRequestOptions', replacement: 'IHttpRequestOptions' },
127
+ output: `
128
+ import { IHttpRequestOptions } from 'n8n-workflow';
129
+
130
+ function makeRequest(options: IRequestOptions): Promise<any> {
131
+ return this.helpers.request(options);
132
+ }`,
133
+ },
134
+ ],
89
135
  },
90
136
  {
91
137
  messageId: 'deprecatedType',
92
138
  data: { typeName: 'IRequestOptions', replacement: 'IHttpRequestOptions' },
139
+ suggestions: [
140
+ {
141
+ messageId: 'suggestReplaceType',
142
+ data: { typeName: 'IRequestOptions', replacement: 'IHttpRequestOptions' },
143
+ output: `
144
+ import { IRequestOptions } from 'n8n-workflow';
145
+
146
+ function makeRequest(options: IHttpRequestOptions): Promise<any> {
147
+ return this.helpers.request(options);
148
+ }`,
149
+ },
150
+ ],
93
151
  },
94
152
  {
95
153
  messageId: 'deprecatedRequestFunction',
96
154
  data: { functionName: 'request', replacement: 'httpRequest' },
155
+ suggestions: [
156
+ {
157
+ messageId: 'suggestReplaceFunction',
158
+ data: { functionName: 'request', replacement: 'httpRequest' },
159
+ output: `
160
+ import { IRequestOptions } from 'n8n-workflow';
161
+
162
+ function makeRequest(options: IRequestOptions): Promise<any> {
163
+ return this.helpers.httpRequest(options);
164
+ }`,
165
+ },
166
+ ],
97
167
  },
98
168
  ],
99
169
  },