@n8n/eslint-plugin-community-nodes 0.24.0 → 0.25.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/.turbo/turbo-build.log +1 -1
- package/README.md +39 -52
- package/dist/plugin.d.ts +0 -78
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +0 -26
- package/dist/plugin.js.map +1 -1
- package/dist/rules/index.d.ts +0 -13
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +0 -26
- package/dist/rules/index.js.map +1 -1
- package/dist/typecheck.tsbuildinfo +1 -1
- package/docs/rules/no-forbidden-lifecycle-scripts.md +1 -1
- package/package.json +4 -4
- package/src/plugin.ts +0 -26
- package/src/rules/index.ts +0 -26
- package/dist/rules/cred-class-name-field-conventions.d.ts +0 -2
- package/dist/rules/cred-class-name-field-conventions.d.ts.map +0 -1
- package/dist/rules/cred-class-name-field-conventions.js +0 -89
- package/dist/rules/cred-class-name-field-conventions.js.map +0 -1
- package/dist/rules/cred-class-name-suffix.d.ts +0 -2
- package/dist/rules/cred-class-name-suffix.d.ts.map +0 -1
- package/dist/rules/cred-class-name-suffix.js +0 -53
- package/dist/rules/cred-class-name-suffix.js.map +0 -1
- package/dist/rules/cred-filename-against-convention.d.ts +0 -2
- package/dist/rules/cred-filename-against-convention.d.ts.map +0 -1
- package/dist/rules/cred-filename-against-convention.js +0 -42
- package/dist/rules/cred-filename-against-convention.js.map +0 -1
- package/dist/rules/no-asterisk-in-option-names.d.ts +0 -2
- package/dist/rules/no-asterisk-in-option-names.d.ts.map +0 -1
- package/dist/rules/no-asterisk-in-option-names.js +0 -90
- package/dist/rules/no-asterisk-in-option-names.js.map +0 -1
- package/dist/rules/no-duplicate-param-options.d.ts +0 -2
- package/dist/rules/no-duplicate-param-options.d.ts.map +0 -1
- package/dist/rules/no-duplicate-param-options.js +0 -97
- package/dist/rules/no-duplicate-param-options.js.map +0 -1
- package/dist/rules/node-class-description-name-camelcase.d.ts +0 -2
- package/dist/rules/node-class-description-name-camelcase.d.ts.map +0 -1
- package/dist/rules/node-class-description-name-camelcase.js +0 -92
- package/dist/rules/node-class-description-name-camelcase.js.map +0 -1
- package/dist/rules/node-filename-against-convention.d.ts +0 -2
- package/dist/rules/node-filename-against-convention.d.ts.map +0 -1
- package/dist/rules/node-filename-against-convention.js +0 -61
- package/dist/rules/node-filename-against-convention.js.map +0 -1
- package/dist/rules/node-operation-error-itemindex.d.ts +0 -12
- package/dist/rules/node-operation-error-itemindex.d.ts.map +0 -1
- package/dist/rules/node-operation-error-itemindex.js +0 -184
- package/dist/rules/node-operation-error-itemindex.js.map +0 -1
- package/dist/rules/options-sorted-alphabetically.d.ts +0 -2
- package/dist/rules/options-sorted-alphabetically.d.ts.map +0 -1
- package/dist/rules/options-sorted-alphabetically.js +0 -95
- package/dist/rules/options-sorted-alphabetically.js.map +0 -1
- package/dist/rules/require-community-node-keyword.d.ts +0 -2
- package/dist/rules/require-community-node-keyword.d.ts.map +0 -1
- package/dist/rules/require-community-node-keyword.js +0 -65
- package/dist/rules/require-community-node-keyword.js.map +0 -1
- package/dist/rules/require-continue-on-fail.d.ts +0 -2
- package/dist/rules/require-continue-on-fail.d.ts.map +0 -1
- package/dist/rules/require-continue-on-fail.js +0 -76
- package/dist/rules/require-continue-on-fail.js.map +0 -1
- package/dist/rules/require-mit-license.d.ts +0 -2
- package/dist/rules/require-mit-license.d.ts.map +0 -1
- package/dist/rules/require-mit-license.js +0 -57
- package/dist/rules/require-mit-license.js.map +0 -1
- package/dist/rules/require-param-default.d.ts +0 -15
- package/dist/rules/require-param-default.d.ts.map +0 -1
- package/dist/rules/require-param-default.js +0 -75
- package/dist/rules/require-param-default.js.map +0 -1
- package/docs/rules/cred-class-name-field-conventions.md +0 -38
- package/docs/rules/cred-class-name-suffix.md +0 -46
- package/docs/rules/cred-filename-against-convention.md +0 -42
- package/docs/rules/no-asterisk-in-option-names.md +0 -61
- package/docs/rules/no-duplicate-param-options.md +0 -46
- package/docs/rules/node-class-description-name-camelcase.md +0 -41
- package/docs/rules/node-filename-against-convention.md +0 -50
- package/docs/rules/node-operation-error-itemindex.md +0 -81
- package/docs/rules/options-sorted-alphabetically.md +0 -63
- package/docs/rules/require-community-node-keyword.md +0 -45
- package/docs/rules/require-continue-on-fail.md +0 -56
- package/docs/rules/require-mit-license.md +0 -38
- package/docs/rules/require-param-default.md +0 -60
- package/src/rules/cred-class-name-field-conventions.test.ts +0 -121
- package/src/rules/cred-class-name-field-conventions.ts +0 -102
- package/src/rules/cred-class-name-suffix.test.ts +0 -74
- package/src/rules/cred-class-name-suffix.ts +0 -57
- package/src/rules/cred-filename-against-convention.test.ts +0 -72
- package/src/rules/cred-filename-against-convention.ts +0 -48
- package/src/rules/no-asterisk-in-option-names.test.ts +0 -185
- package/src/rules/no-asterisk-in-option-names.ts +0 -111
- package/src/rules/no-duplicate-param-options.test.ts +0 -125
- package/src/rules/no-duplicate-param-options.ts +0 -125
- package/src/rules/node-class-description-name-camelcase.test.ts +0 -121
- package/src/rules/node-class-description-name-camelcase.ts +0 -114
- package/src/rules/node-filename-against-convention.test.ts +0 -115
- package/src/rules/node-filename-against-convention.ts +0 -76
- package/src/rules/node-operation-error-itemindex.test.ts +0 -280
- package/src/rules/node-operation-error-itemindex.ts +0 -223
- package/src/rules/options-sorted-alphabetically.test.ts +0 -468
- package/src/rules/options-sorted-alphabetically.ts +0 -129
- package/src/rules/require-community-node-keyword.test.ts +0 -82
- package/src/rules/require-community-node-keyword.ts +0 -78
- package/src/rules/require-continue-on-fail.test.ts +0 -129
- package/src/rules/require-continue-on-fail.ts +0 -88
- package/src/rules/require-mit-license.test.ts +0 -67
- package/src/rules/require-mit-license.ts +0 -65
- package/src/rules/require-param-default.test.ts +0 -227
- package/src/rules/require-param-default.ts +0 -89
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import * as path from 'node:path';
|
|
2
|
-
|
|
3
|
-
import { isCredentialTypeClass, isFileType, createRule } from '../utils/index.js';
|
|
4
|
-
|
|
5
|
-
export const CredFilenameAgainstConventionRule = createRule({
|
|
6
|
-
name: 'cred-filename-against-convention',
|
|
7
|
-
meta: {
|
|
8
|
-
type: 'problem',
|
|
9
|
-
docs: {
|
|
10
|
-
description: 'Credential filename must match the credential class name',
|
|
11
|
-
},
|
|
12
|
-
messages: {
|
|
13
|
-
renameFile:
|
|
14
|
-
'Credential filename must match the class name "{{className}}". Rename file to "{{expected}}".',
|
|
15
|
-
},
|
|
16
|
-
schema: [],
|
|
17
|
-
},
|
|
18
|
-
defaultOptions: [],
|
|
19
|
-
create(context) {
|
|
20
|
-
if (!isFileType(context.filename, '.credentials.ts')) {
|
|
21
|
-
return {};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
ClassDeclaration(node) {
|
|
26
|
-
if (!isCredentialTypeClass(node)) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const classNameNode = node.id;
|
|
31
|
-
if (!classNameNode) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const className = classNameNode.name;
|
|
36
|
-
const actualBaseName = path.basename(context.filename, '.credentials.ts');
|
|
37
|
-
|
|
38
|
-
if (actualBaseName !== className) {
|
|
39
|
-
context.report({
|
|
40
|
-
node: classNameNode,
|
|
41
|
-
messageId: 'renameFile',
|
|
42
|
-
data: { className, expected: `${className}.credentials.ts` },
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
},
|
|
48
|
-
});
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
-
|
|
3
|
-
import { NoAsteriskInOptionNamesRule } from './no-asterisk-in-option-names.js';
|
|
4
|
-
|
|
5
|
-
const ruleTester = new RuleTester();
|
|
6
|
-
|
|
7
|
-
function createNodeCode(body: string): string {
|
|
8
|
-
return `
|
|
9
|
-
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
10
|
-
|
|
11
|
-
export class TestNode implements INodeType {
|
|
12
|
-
description: INodeTypeDescription = {
|
|
13
|
-
${body}
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
ruleTester.run('no-asterisk-in-option-names', NoAsteriskInOptionNamesRule, {
|
|
20
|
-
valid: [
|
|
21
|
-
{
|
|
22
|
-
name: 'class that does not implement INodeType',
|
|
23
|
-
filename: '/tmp/TestNode.node.ts',
|
|
24
|
-
code: `
|
|
25
|
-
export class NotANode {
|
|
26
|
-
description = {
|
|
27
|
-
properties: [
|
|
28
|
-
{ options: [{ name: '* All', value: 'all' }] },
|
|
29
|
-
],
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
`,
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
name: 'non .node.ts file is ignored',
|
|
36
|
-
filename: '/tmp/helper.ts',
|
|
37
|
-
code: `
|
|
38
|
-
export class TestNode {
|
|
39
|
-
description = {
|
|
40
|
-
properties: [
|
|
41
|
-
{ options: [{ name: '* All', value: 'all' }] },
|
|
42
|
-
],
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
`,
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
name: 'option names without asterisk',
|
|
49
|
-
filename: '/tmp/TestNode.node.ts',
|
|
50
|
-
code: createNodeCode(`
|
|
51
|
-
displayName: 'Test Node',
|
|
52
|
-
name: 'testNode',
|
|
53
|
-
properties: [
|
|
54
|
-
{
|
|
55
|
-
displayName: 'Operation',
|
|
56
|
-
name: 'operation',
|
|
57
|
-
type: 'options',
|
|
58
|
-
options: [
|
|
59
|
-
{ name: 'Create', value: 'create' },
|
|
60
|
-
{ name: '[All]', value: 'all' },
|
|
61
|
-
],
|
|
62
|
-
default: 'create',
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
`),
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
name: 'asterisk in displayName outside options is ignored',
|
|
69
|
-
filename: '/tmp/TestNode.node.ts',
|
|
70
|
-
code: createNodeCode(`
|
|
71
|
-
displayName: 'Test * Node',
|
|
72
|
-
name: 'testNode',
|
|
73
|
-
properties: [
|
|
74
|
-
{ displayName: 'Field *', name: 'field', type: 'string', default: '' },
|
|
75
|
-
],
|
|
76
|
-
`),
|
|
77
|
-
},
|
|
78
|
-
],
|
|
79
|
-
invalid: [
|
|
80
|
-
{
|
|
81
|
-
name: 'asterisk in option name within options array',
|
|
82
|
-
filename: '/tmp/TestNode.node.ts',
|
|
83
|
-
code: createNodeCode(`
|
|
84
|
-
displayName: 'Test Node',
|
|
85
|
-
name: 'testNode',
|
|
86
|
-
properties: [
|
|
87
|
-
{
|
|
88
|
-
displayName: 'Resource',
|
|
89
|
-
name: 'resource',
|
|
90
|
-
type: 'options',
|
|
91
|
-
options: [
|
|
92
|
-
{ name: '* All', value: 'all' },
|
|
93
|
-
{ name: 'User', value: 'user' },
|
|
94
|
-
],
|
|
95
|
-
default: 'all',
|
|
96
|
-
},
|
|
97
|
-
],
|
|
98
|
-
`),
|
|
99
|
-
errors: [
|
|
100
|
-
{
|
|
101
|
-
messageId: 'asteriskInOptionName',
|
|
102
|
-
data: { name: '* All' },
|
|
103
|
-
suggestions: [
|
|
104
|
-
{
|
|
105
|
-
messageId: 'replaceAsterisk',
|
|
106
|
-
output: createNodeCode(`
|
|
107
|
-
displayName: 'Test Node',
|
|
108
|
-
name: 'testNode',
|
|
109
|
-
properties: [
|
|
110
|
-
{
|
|
111
|
-
displayName: 'Resource',
|
|
112
|
-
name: 'resource',
|
|
113
|
-
type: 'options',
|
|
114
|
-
options: [
|
|
115
|
-
{ name: '[All] All', value: 'all' },
|
|
116
|
-
{ name: 'User', value: 'user' },
|
|
117
|
-
],
|
|
118
|
-
default: 'all',
|
|
119
|
-
},
|
|
120
|
-
],
|
|
121
|
-
`),
|
|
122
|
-
},
|
|
123
|
-
],
|
|
124
|
-
},
|
|
125
|
-
],
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
name: 'asterisk in nested collection options',
|
|
129
|
-
filename: '/tmp/TestNode.node.ts',
|
|
130
|
-
code: createNodeCode(`
|
|
131
|
-
displayName: 'Test Node',
|
|
132
|
-
name: 'testNode',
|
|
133
|
-
properties: [
|
|
134
|
-
{
|
|
135
|
-
displayName: 'Filters',
|
|
136
|
-
name: 'filters',
|
|
137
|
-
type: 'collection',
|
|
138
|
-
default: {},
|
|
139
|
-
options: [
|
|
140
|
-
{
|
|
141
|
-
displayName: 'Status',
|
|
142
|
-
name: 'status',
|
|
143
|
-
type: 'options',
|
|
144
|
-
options: [{ name: '*', value: 'any' }],
|
|
145
|
-
default: 'any',
|
|
146
|
-
},
|
|
147
|
-
],
|
|
148
|
-
},
|
|
149
|
-
],
|
|
150
|
-
`),
|
|
151
|
-
errors: [
|
|
152
|
-
{
|
|
153
|
-
messageId: 'asteriskInOptionName',
|
|
154
|
-
data: { name: '*' },
|
|
155
|
-
suggestions: [
|
|
156
|
-
{
|
|
157
|
-
messageId: 'replaceAsterisk',
|
|
158
|
-
output: createNodeCode(`
|
|
159
|
-
displayName: 'Test Node',
|
|
160
|
-
name: 'testNode',
|
|
161
|
-
properties: [
|
|
162
|
-
{
|
|
163
|
-
displayName: 'Filters',
|
|
164
|
-
name: 'filters',
|
|
165
|
-
type: 'collection',
|
|
166
|
-
default: {},
|
|
167
|
-
options: [
|
|
168
|
-
{
|
|
169
|
-
displayName: 'Status',
|
|
170
|
-
name: 'status',
|
|
171
|
-
type: 'options',
|
|
172
|
-
options: [{ name: '[All]', value: 'any' }],
|
|
173
|
-
default: 'any',
|
|
174
|
-
},
|
|
175
|
-
],
|
|
176
|
-
},
|
|
177
|
-
],
|
|
178
|
-
`),
|
|
179
|
-
},
|
|
180
|
-
],
|
|
181
|
-
},
|
|
182
|
-
],
|
|
183
|
-
},
|
|
184
|
-
],
|
|
185
|
-
});
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
-
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
isNodeTypeClass,
|
|
6
|
-
findClassProperty,
|
|
7
|
-
findObjectProperty,
|
|
8
|
-
getStringLiteralValue,
|
|
9
|
-
isFileType,
|
|
10
|
-
createRule,
|
|
11
|
-
} from '../utils/index.js';
|
|
12
|
-
|
|
13
|
-
export const NoAsteriskInOptionNamesRule = createRule({
|
|
14
|
-
name: 'no-asterisk-in-option-names',
|
|
15
|
-
meta: {
|
|
16
|
-
type: 'suggestion',
|
|
17
|
-
docs: {
|
|
18
|
-
description: 'Disallow asterisk characters in node option name values',
|
|
19
|
-
},
|
|
20
|
-
hasSuggestions: true,
|
|
21
|
-
messages: {
|
|
22
|
-
asteriskInOptionName:
|
|
23
|
-
'Option name "{{ name }}" contains "*", which renders ambiguously in the n8n UI. Use bracketed notation like "[All]" instead.',
|
|
24
|
-
replaceAsterisk: 'Replace "*" with "[All]".',
|
|
25
|
-
},
|
|
26
|
-
schema: [],
|
|
27
|
-
},
|
|
28
|
-
defaultOptions: [],
|
|
29
|
-
create(context) {
|
|
30
|
-
if (!isFileType(context.filename, '.node.ts')) {
|
|
31
|
-
return {};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Check every object that is a direct element of an `options` array.
|
|
35
|
-
const checkOptionsArray = (optionsArray: TSESTree.ArrayExpression): void => {
|
|
36
|
-
for (const option of optionsArray.elements) {
|
|
37
|
-
if (!option || option.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const valueNode = findObjectProperty(option, 'name')?.value;
|
|
42
|
-
if (!valueNode) {
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const name = getStringLiteralValue(valueNode);
|
|
47
|
-
if (!name?.includes('*')) {
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
context.report({
|
|
52
|
-
node: valueNode,
|
|
53
|
-
messageId: 'asteriskInOptionName',
|
|
54
|
-
data: { name },
|
|
55
|
-
suggest: [
|
|
56
|
-
{
|
|
57
|
-
messageId: 'replaceAsterisk',
|
|
58
|
-
fix(fixer) {
|
|
59
|
-
const quote = context.sourceCode.getText(valueNode).at(0) ?? "'";
|
|
60
|
-
return fixer.replaceText(
|
|
61
|
-
valueNode,
|
|
62
|
-
`${quote}${name.replaceAll('*', '[All]')}${quote}`,
|
|
63
|
-
);
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const traverse = (node: TSESTree.Node): void => {
|
|
72
|
-
if (node.type === AST_NODE_TYPES.ObjectExpression) {
|
|
73
|
-
const optionsProperty = findObjectProperty(node, 'options');
|
|
74
|
-
if (optionsProperty?.value.type === AST_NODE_TYPES.ArrayExpression) {
|
|
75
|
-
checkOptionsArray(optionsProperty.value);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
for (const key in node) {
|
|
80
|
-
if (key === 'parent') {
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
const child = node[key as keyof TSESTree.Node] as unknown;
|
|
84
|
-
if (Array.isArray(child)) {
|
|
85
|
-
for (const item of child) {
|
|
86
|
-
if (item && typeof item === 'object' && 'type' in item) {
|
|
87
|
-
traverse(item as TSESTree.Node);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
} else if (child && typeof child === 'object' && 'type' in child) {
|
|
91
|
-
traverse(child as TSESTree.Node);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
ClassDeclaration(node) {
|
|
98
|
-
if (!isNodeTypeClass(node)) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const descriptionProperty = findClassProperty(node, 'description');
|
|
103
|
-
if (!descriptionProperty?.value) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
traverse(descriptionProperty.value);
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
},
|
|
111
|
-
});
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
-
|
|
3
|
-
import { NoDuplicateParamOptionsRule } from './no-duplicate-param-options.js';
|
|
4
|
-
|
|
5
|
-
const ruleTester = new RuleTester();
|
|
6
|
-
|
|
7
|
-
function nodeCode(options: string, type = 'options'): string {
|
|
8
|
-
return `
|
|
9
|
-
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
10
|
-
|
|
11
|
-
export class TestNode implements INodeType {
|
|
12
|
-
description: INodeTypeDescription = {
|
|
13
|
-
displayName: 'Test Node',
|
|
14
|
-
name: 'testNode',
|
|
15
|
-
properties: [
|
|
16
|
-
{
|
|
17
|
-
displayName: 'Resource',
|
|
18
|
-
name: 'resource',
|
|
19
|
-
type: '${type}',
|
|
20
|
-
options: [${options}],
|
|
21
|
-
default: '',
|
|
22
|
-
},
|
|
23
|
-
],
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
ruleTester.run('no-duplicate-param-options', NoDuplicateParamOptionsRule, {
|
|
30
|
-
valid: [
|
|
31
|
-
{
|
|
32
|
-
name: 'non-INodeType class is skipped',
|
|
33
|
-
filename: '/tmp/TestNode.node.ts',
|
|
34
|
-
code: 'export class Foo {}',
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
name: 'unique names and values',
|
|
38
|
-
filename: '/tmp/TestNode.node.ts',
|
|
39
|
-
code: nodeCode(`
|
|
40
|
-
{ name: 'Create', value: 'create' },
|
|
41
|
-
{ name: 'Delete', value: 'delete' },
|
|
42
|
-
`),
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
name: 'unique numeric values',
|
|
46
|
-
filename: '/tmp/TestNode.node.ts',
|
|
47
|
-
code: nodeCode(`
|
|
48
|
-
{ name: 'One', value: 1 },
|
|
49
|
-
{ name: 'Two', value: 2 },
|
|
50
|
-
`),
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
name: 'multiOptions with unique options',
|
|
54
|
-
filename: '/tmp/TestNode.node.ts',
|
|
55
|
-
code: nodeCode(
|
|
56
|
-
`
|
|
57
|
-
{ name: 'A', value: 'a' },
|
|
58
|
-
{ name: 'B', value: 'b' },
|
|
59
|
-
`,
|
|
60
|
-
'multiOptions',
|
|
61
|
-
),
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
name: 'non-node.ts file is skipped',
|
|
65
|
-
filename: '/tmp/TestNode.ts',
|
|
66
|
-
code: nodeCode(`
|
|
67
|
-
{ name: 'Dup', value: 'a' },
|
|
68
|
-
{ name: 'Dup', value: 'b' },
|
|
69
|
-
`),
|
|
70
|
-
},
|
|
71
|
-
],
|
|
72
|
-
invalid: [
|
|
73
|
-
{
|
|
74
|
-
name: 'duplicate name',
|
|
75
|
-
filename: '/tmp/TestNode.node.ts',
|
|
76
|
-
code: nodeCode(`
|
|
77
|
-
{ name: 'Create', value: 'create' },
|
|
78
|
-
{ name: 'Create', value: 'create2' },
|
|
79
|
-
`),
|
|
80
|
-
errors: [{ messageId: 'duplicateName', data: { value: 'Create', displayName: 'Resource' } }],
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
name: 'duplicate value',
|
|
84
|
-
filename: '/tmp/TestNode.node.ts',
|
|
85
|
-
code: nodeCode(`
|
|
86
|
-
{ name: 'Create', value: 'create' },
|
|
87
|
-
{ name: 'Make', value: 'create' },
|
|
88
|
-
`),
|
|
89
|
-
errors: [{ messageId: 'duplicateValue', data: { value: 'create', displayName: 'Resource' } }],
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
name: 'duplicate numeric value',
|
|
93
|
-
filename: '/tmp/TestNode.node.ts',
|
|
94
|
-
code: nodeCode(`
|
|
95
|
-
{ name: 'One', value: 1 },
|
|
96
|
-
{ name: 'Uno', value: 1 },
|
|
97
|
-
`),
|
|
98
|
-
errors: [{ messageId: 'duplicateValue', data: { value: '1', displayName: 'Resource' } }],
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
name: 'duplicate name and value reported separately',
|
|
102
|
-
filename: '/tmp/TestNode.node.ts',
|
|
103
|
-
code: nodeCode(`
|
|
104
|
-
{ name: 'Create', value: 'create' },
|
|
105
|
-
{ name: 'Create', value: 'create' },
|
|
106
|
-
`),
|
|
107
|
-
errors: [
|
|
108
|
-
{ messageId: 'duplicateName', data: { value: 'Create', displayName: 'Resource' } },
|
|
109
|
-
{ messageId: 'duplicateValue', data: { value: 'create', displayName: 'Resource' } },
|
|
110
|
-
],
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
name: 'duplicate in multiOptions',
|
|
114
|
-
filename: '/tmp/TestNode.node.ts',
|
|
115
|
-
code: nodeCode(
|
|
116
|
-
`
|
|
117
|
-
{ name: 'A', value: 'a' },
|
|
118
|
-
{ name: 'A', value: 'b' },
|
|
119
|
-
`,
|
|
120
|
-
'multiOptions',
|
|
121
|
-
),
|
|
122
|
-
errors: [{ messageId: 'duplicateName', data: { value: 'A', displayName: 'Resource' } }],
|
|
123
|
-
},
|
|
124
|
-
],
|
|
125
|
-
});
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
-
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
isNodeTypeClass,
|
|
6
|
-
findClassProperty,
|
|
7
|
-
findObjectProperty,
|
|
8
|
-
getStringLiteralValue,
|
|
9
|
-
getLiteralValue,
|
|
10
|
-
isFileType,
|
|
11
|
-
createRule,
|
|
12
|
-
} from '../utils/index.js';
|
|
13
|
-
|
|
14
|
-
export const NoDuplicateParamOptionsRule = createRule({
|
|
15
|
-
name: 'no-duplicate-param-options',
|
|
16
|
-
meta: {
|
|
17
|
-
type: 'problem',
|
|
18
|
-
docs: {
|
|
19
|
-
description: 'Disallow duplicate option names or values within a single node parameter',
|
|
20
|
-
},
|
|
21
|
-
messages: {
|
|
22
|
-
duplicateName: 'Duplicate option name "{{ value }}" in parameter "{{ displayName }}".',
|
|
23
|
-
duplicateValue: 'Duplicate option value "{{ value }}" in parameter "{{ displayName }}".',
|
|
24
|
-
},
|
|
25
|
-
schema: [],
|
|
26
|
-
},
|
|
27
|
-
defaultOptions: [],
|
|
28
|
-
create(context) {
|
|
29
|
-
if (!isFileType(context.filename, '.node.ts')) {
|
|
30
|
-
return {};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const checkProperty = (property: TSESTree.ObjectExpression): void => {
|
|
34
|
-
const typeProperty = findObjectProperty(property, 'type');
|
|
35
|
-
const type = typeProperty ? getStringLiteralValue(typeProperty.value) : null;
|
|
36
|
-
if (type !== 'options' && type !== 'multiOptions') {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const optionsProperty = findObjectProperty(property, 'options');
|
|
41
|
-
if (
|
|
42
|
-
!optionsProperty?.value ||
|
|
43
|
-
optionsProperty.value.type !== AST_NODE_TYPES.ArrayExpression
|
|
44
|
-
) {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const displayNameProperty = findObjectProperty(property, 'displayName');
|
|
49
|
-
const displayName =
|
|
50
|
-
(displayNameProperty ? getStringLiteralValue(displayNameProperty.value) : null) ??
|
|
51
|
-
'unknown';
|
|
52
|
-
|
|
53
|
-
const seenNames = new Set<string>();
|
|
54
|
-
const seenValues = new Set<string | number | boolean>();
|
|
55
|
-
|
|
56
|
-
for (const option of optionsProperty.value.elements) {
|
|
57
|
-
if (!option || option.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const nameProperty = findObjectProperty(option, 'name');
|
|
62
|
-
const name = nameProperty ? getStringLiteralValue(nameProperty.value) : null;
|
|
63
|
-
if (name !== null) {
|
|
64
|
-
if (seenNames.has(name)) {
|
|
65
|
-
context.report({
|
|
66
|
-
node: nameProperty!.value,
|
|
67
|
-
messageId: 'duplicateName',
|
|
68
|
-
data: { value: name, displayName },
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
seenNames.add(name);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const valueProperty = findObjectProperty(option, 'value');
|
|
75
|
-
const value = valueProperty ? getLiteralValue(valueProperty.value) : null;
|
|
76
|
-
if (value !== null) {
|
|
77
|
-
if (seenValues.has(value)) {
|
|
78
|
-
context.report({
|
|
79
|
-
node: valueProperty!.value,
|
|
80
|
-
messageId: 'duplicateValue',
|
|
81
|
-
data: { value: String(value), displayName },
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
seenValues.add(value);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const analyzeNodeDescription = (descriptionValue: TSESTree.Expression | null): void => {
|
|
90
|
-
if (!descriptionValue || descriptionValue.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const propertiesProperty = findObjectProperty(descriptionValue, 'properties');
|
|
95
|
-
if (
|
|
96
|
-
!propertiesProperty?.value ||
|
|
97
|
-
propertiesProperty.value.type !== AST_NODE_TYPES.ArrayExpression
|
|
98
|
-
) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
for (const property of propertiesProperty.value.elements) {
|
|
103
|
-
if (!property || property.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
checkProperty(property);
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
ClassDeclaration(node) {
|
|
112
|
-
if (!isNodeTypeClass(node)) {
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const descriptionProperty = findClassProperty(node, 'description');
|
|
117
|
-
if (!descriptionProperty) {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
analyzeNodeDescription(descriptionProperty.value);
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
},
|
|
125
|
-
});
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
-
|
|
3
|
-
import { NodeClassDescriptionNameCamelCaseRule } from './node-class-description-name-camelcase.js';
|
|
4
|
-
|
|
5
|
-
const ruleTester = new RuleTester();
|
|
6
|
-
|
|
7
|
-
function createNodeCode(name: string): string {
|
|
8
|
-
return `
|
|
9
|
-
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
10
|
-
|
|
11
|
-
export class TestNode implements INodeType {
|
|
12
|
-
description: INodeTypeDescription = {
|
|
13
|
-
displayName: 'Test Node',
|
|
14
|
-
name: '${name}',
|
|
15
|
-
group: ['input'],
|
|
16
|
-
version: 1,
|
|
17
|
-
description: 'A test node',
|
|
18
|
-
defaults: { name: 'Test Node' },
|
|
19
|
-
inputs: [],
|
|
20
|
-
outputs: [],
|
|
21
|
-
properties: [],
|
|
22
|
-
};
|
|
23
|
-
}`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function createNodeCodeWithoutName(): string {
|
|
27
|
-
return `
|
|
28
|
-
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
29
|
-
|
|
30
|
-
export class TestNode implements INodeType {
|
|
31
|
-
description: INodeTypeDescription = {
|
|
32
|
-
displayName: 'Test Node',
|
|
33
|
-
group: ['input'],
|
|
34
|
-
version: 1,
|
|
35
|
-
description: 'A test node',
|
|
36
|
-
defaults: { name: 'Test Node' },
|
|
37
|
-
inputs: [],
|
|
38
|
-
outputs: [],
|
|
39
|
-
properties: [],
|
|
40
|
-
};
|
|
41
|
-
}`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function createRegularClass(): string {
|
|
45
|
-
return `
|
|
46
|
-
export class SomeHelper {
|
|
47
|
-
name = 'My Helper';
|
|
48
|
-
}`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
ruleTester.run('node-class-description-name-camelcase', NodeClassDescriptionNameCamelCaseRule, {
|
|
52
|
-
valid: [
|
|
53
|
-
{
|
|
54
|
-
name: 'single lowercase word',
|
|
55
|
-
filename: '/tmp/Github.node.ts',
|
|
56
|
-
code: createNodeCode('github'),
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: 'camelCase multi-word name',
|
|
60
|
-
filename: '/tmp/GoogleSheets.node.ts',
|
|
61
|
-
code: createNodeCode('googleSheets'),
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
name: 'camelCase name with digits',
|
|
65
|
-
filename: '/tmp/Oauth2.node.ts',
|
|
66
|
-
code: createNodeCode('myNode2'),
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
name: 'class not implementing INodeType is ignored',
|
|
70
|
-
filename: '/tmp/Github.node.ts',
|
|
71
|
-
code: createRegularClass(),
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
name: 'non-.node.ts file is ignored',
|
|
75
|
-
filename: '/tmp/Github.ts',
|
|
76
|
-
code: createNodeCode('My Node'),
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
name: 'missing description.name is ignored',
|
|
80
|
-
filename: '/tmp/Github.node.ts',
|
|
81
|
-
code: createNodeCodeWithoutName(),
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
invalid: [
|
|
85
|
-
{
|
|
86
|
-
name: 'PascalCase first letter is lowercased',
|
|
87
|
-
filename: '/tmp/Github.node.ts',
|
|
88
|
-
code: createNodeCode('MyNode'),
|
|
89
|
-
errors: [{ messageId: 'notCamelCase', data: { value: 'MyNode' } }],
|
|
90
|
-
output: createNodeCode('myNode'),
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
name: 'spaces are removed and camelCased',
|
|
94
|
-
filename: '/tmp/Github.node.ts',
|
|
95
|
-
code: createNodeCode('My Node'),
|
|
96
|
-
errors: [{ messageId: 'notCamelCase', data: { value: 'My Node' } }],
|
|
97
|
-
output: createNodeCode('myNode'),
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
name: 'hyphen separators are removed and camelCased',
|
|
101
|
-
filename: '/tmp/Github.node.ts',
|
|
102
|
-
code: createNodeCode('my-node'),
|
|
103
|
-
errors: [{ messageId: 'notCamelCase', data: { value: 'my-node' } }],
|
|
104
|
-
output: createNodeCode('myNode'),
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
name: 'underscore separators are removed and camelCased',
|
|
108
|
-
filename: '/tmp/Github.node.ts',
|
|
109
|
-
code: createNodeCode('my_cool_node'),
|
|
110
|
-
errors: [{ messageId: 'notCamelCase', data: { value: 'my_cool_node' } }],
|
|
111
|
-
output: createNodeCode('myCoolNode'),
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
name: 'names starting with a digit are reported without an autofix',
|
|
115
|
-
filename: '/tmp/Github.node.ts',
|
|
116
|
-
code: createNodeCode('123node'),
|
|
117
|
-
errors: [{ messageId: 'notCamelCase', data: { value: '123node' } }],
|
|
118
|
-
output: null,
|
|
119
|
-
},
|
|
120
|
-
],
|
|
121
|
-
});
|