@n8n/eslint-plugin-community-nodes 0.31.0 → 0.33.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/README.md +2 -1
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +2 -0
- package/dist/rules/ai-node-package-json.d.ts +3 -1
- package/dist/rules/ai-node-package-json.d.ts.map +1 -1
- package/dist/rules/cred-class-field-icon-missing.d.ts +3 -1
- package/dist/rules/cred-class-field-icon-missing.d.ts.map +1 -1
- package/dist/rules/cred-class-oauth2-naming.d.ts +3 -1
- package/dist/rules/cred-class-oauth2-naming.d.ts.map +1 -1
- package/dist/rules/credential-documentation-url.d.ts +3 -1
- package/dist/rules/credential-documentation-url.d.ts.map +1 -1
- package/dist/rules/credential-password-field.d.ts +3 -1
- package/dist/rules/credential-password-field.d.ts.map +1 -1
- package/dist/rules/credential-password-field.js +1 -2
- package/dist/rules/credential-test-required.d.ts +3 -1
- package/dist/rules/credential-test-required.d.ts.map +1 -1
- package/dist/rules/credential-unnecessary-password.d.ts +3 -1
- package/dist/rules/credential-unnecessary-password.d.ts.map +1 -1
- package/dist/rules/credential-unnecessary-password.js +1 -2
- package/dist/rules/icon-prefer-themed-variants.d.ts +3 -1
- package/dist/rules/icon-prefer-themed-variants.d.ts.map +1 -1
- package/dist/rules/icon-validation.d.ts +3 -1
- package/dist/rules/icon-validation.d.ts.map +1 -1
- package/dist/rules/icon-validation.js +5 -15
- package/dist/rules/index.d.ts +132 -43
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +2 -0
- package/dist/rules/missing-paired-item.d.ts +3 -1
- package/dist/rules/missing-paired-item.d.ts.map +1 -1
- package/dist/rules/n8n-object-validation.d.ts +3 -1
- package/dist/rules/n8n-object-validation.d.ts.map +1 -1
- package/dist/rules/no-builder-hint-leakage.d.ts +3 -1
- package/dist/rules/no-builder-hint-leakage.d.ts.map +1 -1
- package/dist/rules/no-credential-reuse.d.ts +3 -1
- package/dist/rules/no-credential-reuse.d.ts.map +1 -1
- package/dist/rules/no-credential-reuse.js +4 -6
- package/dist/rules/no-dangerous-functions.d.ts +3 -1
- package/dist/rules/no-dangerous-functions.d.ts.map +1 -1
- package/dist/rules/no-dead-files.d.ts +3 -1
- package/dist/rules/no-dead-files.d.ts.map +1 -1
- package/dist/rules/no-deprecated-workflow-functions.d.ts +3 -1
- package/dist/rules/no-deprecated-workflow-functions.d.ts.map +1 -1
- package/dist/rules/no-emoji-in-options.d.ts +3 -1
- package/dist/rules/no-emoji-in-options.d.ts.map +1 -1
- package/dist/rules/no-emoji-in-options.js +4 -4
- package/dist/rules/no-forbidden-lifecycle-scripts.d.ts +3 -1
- package/dist/rules/no-forbidden-lifecycle-scripts.d.ts.map +1 -1
- package/dist/rules/no-forbidden-lifecycle-scripts.js +1 -1
- package/dist/rules/no-hardcoded-secrets.d.ts +3 -1
- package/dist/rules/no-hardcoded-secrets.d.ts.map +1 -1
- package/dist/rules/no-http-request-with-manual-auth.d.ts +3 -1
- package/dist/rules/no-http-request-with-manual-auth.d.ts.map +1 -1
- package/dist/rules/no-overrides-field.d.ts +3 -1
- package/dist/rules/no-overrides-field.d.ts.map +1 -1
- package/dist/rules/no-restricted-globals.d.ts +3 -1
- package/dist/rules/no-restricted-globals.d.ts.map +1 -1
- package/dist/rules/no-restricted-imports.d.ts +3 -1
- package/dist/rules/no-restricted-imports.d.ts.map +1 -1
- package/dist/rules/no-runtime-dependencies.d.ts +3 -1
- package/dist/rules/no-runtime-dependencies.d.ts.map +1 -1
- package/dist/rules/no-silent-error-swallowing.d.ts +3 -1
- package/dist/rules/no-silent-error-swallowing.d.ts.map +1 -1
- package/dist/rules/no-template-placeholders.d.ts +3 -1
- package/dist/rules/no-template-placeholders.d.ts.map +1 -1
- package/dist/rules/no-unsafe-connection-type-cast.d.ts +5 -0
- package/dist/rules/no-unsafe-connection-type-cast.d.ts.map +1 -0
- package/dist/rules/no-unsafe-connection-type-cast.js +104 -0
- package/dist/rules/node-class-description-icon-missing.d.ts +3 -1
- package/dist/rules/node-class-description-icon-missing.d.ts.map +1 -1
- package/dist/rules/node-class-description-icon-missing.js +1 -2
- package/dist/rules/node-connection-type-literal.d.ts +3 -1
- package/dist/rules/node-connection-type-literal.d.ts.map +1 -1
- package/dist/rules/node-registration-complete.d.ts +3 -1
- package/dist/rules/node-registration-complete.d.ts.map +1 -1
- package/dist/rules/node-usable-as-tool.d.ts +3 -1
- package/dist/rules/node-usable-as-tool.d.ts.map +1 -1
- package/dist/rules/package-name-convention.d.ts +3 -1
- package/dist/rules/package-name-convention.d.ts.map +1 -1
- package/dist/rules/require-files-array.d.ts +3 -1
- package/dist/rules/require-files-array.d.ts.map +1 -1
- package/dist/rules/require-homepage.d.ts +3 -1
- package/dist/rules/require-homepage.d.ts.map +1 -1
- package/dist/rules/require-node-api-error.d.ts +3 -1
- package/dist/rules/require-node-api-error.d.ts.map +1 -1
- package/dist/rules/require-node-description-fields.d.ts +3 -1
- package/dist/rules/require-node-description-fields.d.ts.map +1 -1
- package/dist/rules/require-node-description-fields.js +1 -2
- package/dist/rules/require-version.d.ts +3 -1
- package/dist/rules/require-version.d.ts.map +1 -1
- package/dist/rules/resource-operation-pattern.d.ts +3 -1
- package/dist/rules/resource-operation-pattern.d.ts.map +1 -1
- package/dist/rules/resource-operation-pattern.js +3 -4
- package/dist/rules/trigger-node-conventions.d.ts +3 -1
- package/dist/rules/trigger-node-conventions.d.ts.map +1 -1
- package/dist/rules/valid-author.d.ts +3 -1
- package/dist/rules/valid-author.d.ts.map +1 -1
- package/dist/rules/valid-author.js +1 -1
- package/dist/rules/valid-credential-references.d.ts +3 -1
- package/dist/rules/valid-credential-references.d.ts.map +1 -1
- package/dist/rules/valid-credential-references.js +4 -6
- package/dist/rules/valid-description.d.ts +3 -1
- package/dist/rules/valid-description.d.ts.map +1 -1
- package/dist/rules/valid-peer-dependencies.d.ts +3 -1
- package/dist/rules/valid-peer-dependencies.d.ts.map +1 -1
- package/dist/rules/webhook-lifecycle-complete.d.ts +3 -1
- package/dist/rules/webhook-lifecycle-complete.d.ts.map +1 -1
- 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 +50 -0
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +1 -2
- package/dist/utils/rule-creator.d.ts +3 -1
- package/dist/utils/rule-creator.d.ts.map +1 -1
- package/docs/rules/icon-prefer-themed-variants.md +2 -2
- package/docs/rules/icon-validation.md +11 -11
- package/docs/rules/no-unsafe-connection-type-cast.md +58 -0
- package/eslint.config.mjs +2 -3
- package/package.json +8 -8
- package/src/plugin.ts +2 -0
- package/src/rules/credential-password-field.ts +1 -4
- package/src/rules/credential-unnecessary-password.ts +1 -4
- package/src/rules/icon-validation.test.ts +19 -21
- package/src/rules/icon-validation.ts +5 -20
- package/src/rules/index.ts +2 -0
- package/src/rules/no-credential-reuse.test.ts +54 -0
- package/src/rules/no-credential-reuse.ts +4 -8
- package/src/rules/no-emoji-in-options.test.ts +77 -0
- package/src/rules/no-emoji-in-options.ts +4 -4
- package/src/rules/no-forbidden-lifecycle-scripts.ts +1 -1
- package/src/rules/no-unsafe-connection-type-cast.test.ts +306 -0
- package/src/rules/no-unsafe-connection-type-cast.ts +125 -0
- package/src/rules/node-class-description-icon-missing.ts +1 -4
- package/src/rules/require-node-description-fields.ts +1 -4
- package/src/rules/resource-operation-pattern.ts +3 -6
- package/src/rules/valid-author.ts +1 -1
- package/src/rules/valid-credential-references.test.ts +75 -0
- package/src/rules/valid-credential-references.ts +4 -8
- package/src/utils/ast-utils.ts +67 -0
- package/src/utils/file-utils.ts +3 -6
- package/dist/plugin.js.map +0 -1
- package/dist/rules/ai-node-package-json.js.map +0 -1
- package/dist/rules/cred-class-field-icon-missing.js.map +0 -1
- package/dist/rules/cred-class-oauth2-naming.js.map +0 -1
- package/dist/rules/credential-documentation-url.js.map +0 -1
- package/dist/rules/credential-password-field.js.map +0 -1
- package/dist/rules/credential-test-required.js.map +0 -1
- package/dist/rules/credential-unnecessary-password.js.map +0 -1
- package/dist/rules/icon-prefer-themed-variants.js.map +0 -1
- package/dist/rules/icon-validation.js.map +0 -1
- package/dist/rules/index.js.map +0 -1
- package/dist/rules/missing-paired-item.js.map +0 -1
- package/dist/rules/n8n-object-validation.js.map +0 -1
- package/dist/rules/no-builder-hint-leakage.js.map +0 -1
- package/dist/rules/no-credential-reuse.js.map +0 -1
- package/dist/rules/no-dangerous-functions.js.map +0 -1
- package/dist/rules/no-dead-files.js.map +0 -1
- package/dist/rules/no-deprecated-workflow-functions.js.map +0 -1
- package/dist/rules/no-emoji-in-options.js.map +0 -1
- package/dist/rules/no-forbidden-lifecycle-scripts.js.map +0 -1
- package/dist/rules/no-hardcoded-secrets.js.map +0 -1
- package/dist/rules/no-http-request-with-manual-auth.js.map +0 -1
- package/dist/rules/no-overrides-field.js.map +0 -1
- package/dist/rules/no-restricted-globals.js.map +0 -1
- package/dist/rules/no-restricted-imports.js.map +0 -1
- package/dist/rules/no-runtime-dependencies.js.map +0 -1
- package/dist/rules/no-silent-error-swallowing.js.map +0 -1
- package/dist/rules/no-template-placeholders.js.map +0 -1
- package/dist/rules/node-class-description-icon-missing.js.map +0 -1
- package/dist/rules/node-connection-type-literal.js.map +0 -1
- package/dist/rules/node-registration-complete.js.map +0 -1
- package/dist/rules/node-usable-as-tool.js.map +0 -1
- package/dist/rules/package-name-convention.js.map +0 -1
- package/dist/rules/require-files-array.js.map +0 -1
- package/dist/rules/require-homepage.js.map +0 -1
- package/dist/rules/require-node-api-error.js.map +0 -1
- package/dist/rules/require-node-description-fields.js.map +0 -1
- package/dist/rules/require-version.js.map +0 -1
- package/dist/rules/resource-operation-pattern.js.map +0 -1
- package/dist/rules/trigger-node-conventions.js.map +0 -1
- package/dist/rules/valid-author.js.map +0 -1
- package/dist/rules/valid-credential-references.js.map +0 -1
- package/dist/rules/valid-description.js.map +0 -1
- package/dist/rules/valid-peer-dependencies.js.map +0 -1
- package/dist/rules/webhook-lifecycle-complete.js.map +0 -1
- package/dist/utils/ast-utils.js.map +0 -1
- package/dist/utils/constants.js.map +0 -1
- package/dist/utils/credential-fields.js.map +0 -1
- package/dist/utils/file-utils.js.map +0 -1
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/rule-creator.js.map +0 -1
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
1
|
import type { ReportSuggestionArray } from '@typescript-eslint/utils/ts-eslint';
|
|
3
2
|
|
|
4
3
|
import {
|
|
5
4
|
isNodeTypeClass,
|
|
6
|
-
|
|
5
|
+
findNodeDescriptionObject,
|
|
7
6
|
findArrayLiteralProperty,
|
|
8
7
|
extractCredentialNameFromArray,
|
|
9
8
|
findPackageJson,
|
|
@@ -59,15 +58,12 @@ export const NoCredentialReuseRule = createRule({
|
|
|
59
58
|
return;
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
!descriptionProperty?.value ||
|
|
65
|
-
descriptionProperty.value.type !== TSESTree.AST_NODE_TYPES.ObjectExpression
|
|
66
|
-
) {
|
|
61
|
+
const description = findNodeDescriptionObject(node);
|
|
62
|
+
if (!description) {
|
|
67
63
|
return;
|
|
68
64
|
}
|
|
69
65
|
|
|
70
|
-
const credentialsArray = findArrayLiteralProperty(
|
|
66
|
+
const credentialsArray = findArrayLiteralProperty(description, 'credentials');
|
|
71
67
|
if (!credentialsArray) {
|
|
72
68
|
return;
|
|
73
69
|
}
|
|
@@ -16,6 +16,27 @@ function createNodeCode(body: string): string {
|
|
|
16
16
|
`;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Versioned node layout from the node-building docs: the version class assigns
|
|
21
|
+
* `description` in its constructor instead of as a property initializer.
|
|
22
|
+
*/
|
|
23
|
+
function createVersionedNodeCode(body: string): string {
|
|
24
|
+
return `
|
|
25
|
+
import type { INodeType, INodeTypeBaseDescription, INodeTypeDescription } from 'n8n-workflow';
|
|
26
|
+
|
|
27
|
+
export class TestNodeV1 implements INodeType {
|
|
28
|
+
description: INodeTypeDescription;
|
|
29
|
+
|
|
30
|
+
constructor(baseDescription: INodeTypeBaseDescription) {
|
|
31
|
+
this.description = {
|
|
32
|
+
...baseDescription,
|
|
33
|
+
${body}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
ruleTester.run('no-emoji-in-options', NoEmojiInOptionsRule, {
|
|
20
41
|
valid: [
|
|
21
42
|
{
|
|
@@ -75,6 +96,62 @@ ruleTester.run('no-emoji-in-options', NoEmojiInOptionsRule, {
|
|
|
75
96
|
},
|
|
76
97
|
],
|
|
77
98
|
invalid: [
|
|
99
|
+
{
|
|
100
|
+
name: 'emoji in option name of a versioned node assigning description in its constructor',
|
|
101
|
+
filename: '/tmp/v1/TestNodeV1.node.ts',
|
|
102
|
+
code: createVersionedNodeCode(`
|
|
103
|
+
version: 1,
|
|
104
|
+
properties: [
|
|
105
|
+
{
|
|
106
|
+
displayName: 'Operation',
|
|
107
|
+
name: 'operation',
|
|
108
|
+
type: 'options',
|
|
109
|
+
options: [
|
|
110
|
+
{ name: '✅ Create', value: 'create' },
|
|
111
|
+
{ name: 'Delete', value: 'delete' },
|
|
112
|
+
],
|
|
113
|
+
default: 'create',
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
`),
|
|
117
|
+
errors: [{ messageId: 'emojiInOption', data: { key: 'name', emoji: '✅' } }],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'emoji in a description that is cast with `as`',
|
|
121
|
+
filename: '/tmp/TestNode.node.ts',
|
|
122
|
+
code: `
|
|
123
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
124
|
+
|
|
125
|
+
export class TestNode implements INodeType {
|
|
126
|
+
description = {
|
|
127
|
+
displayName: '🚀 Rocket Node',
|
|
128
|
+
name: 'testNode',
|
|
129
|
+
properties: [],
|
|
130
|
+
} as INodeTypeDescription;
|
|
131
|
+
}
|
|
132
|
+
`,
|
|
133
|
+
errors: [{ messageId: 'emojiInOption', data: { key: 'displayName', emoji: '🚀' } }],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'emoji in a constructor-assigned description that is cast with `as`',
|
|
137
|
+
filename: '/tmp/v1/TestNodeV1.node.ts',
|
|
138
|
+
code: `
|
|
139
|
+
import type { INodeType, INodeTypeBaseDescription, INodeTypeDescription } from 'n8n-workflow';
|
|
140
|
+
|
|
141
|
+
export class TestNodeV1 implements INodeType {
|
|
142
|
+
description: INodeTypeDescription;
|
|
143
|
+
|
|
144
|
+
constructor(baseDescription: INodeTypeBaseDescription) {
|
|
145
|
+
this.description = {
|
|
146
|
+
...baseDescription,
|
|
147
|
+
displayName: '🚀 Rocket Node',
|
|
148
|
+
properties: [],
|
|
149
|
+
} as INodeTypeDescription;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
`,
|
|
153
|
+
errors: [{ messageId: 'emojiInOption', data: { key: 'displayName', emoji: '🚀' } }],
|
|
154
|
+
},
|
|
78
155
|
{
|
|
79
156
|
name: 'emoji in node displayName',
|
|
80
157
|
filename: '/tmp/TestNode.node.ts',
|
|
@@ -3,7 +3,7 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
isNodeTypeClass,
|
|
6
|
-
|
|
6
|
+
findNodeDescriptionObject,
|
|
7
7
|
getStringLiteralValue,
|
|
8
8
|
isFileType,
|
|
9
9
|
createRule,
|
|
@@ -93,12 +93,12 @@ export const NoEmojiInOptionsRule = createRule({
|
|
|
93
93
|
return;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
const
|
|
97
|
-
if (!
|
|
96
|
+
const description = findNodeDescriptionObject(node);
|
|
97
|
+
if (!description) {
|
|
98
98
|
return;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
traverse(
|
|
101
|
+
traverse(description);
|
|
102
102
|
},
|
|
103
103
|
};
|
|
104
104
|
},
|
|
@@ -41,7 +41,7 @@ export const NoForbiddenLifecycleScriptsRule = createRule({
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
const scriptsProp = findJsonProperty(node, 'scripts');
|
|
44
|
-
if (
|
|
44
|
+
if (scriptsProp?.value.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { NoUnsafeConnectionTypeCastRule } from './no-unsafe-connection-type-cast.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
function createNodeCode(inputs: string, outputs: string): string {
|
|
8
|
+
return `
|
|
9
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
10
|
+
import { NodeConnectionTypes } from 'n8n-workflow';
|
|
11
|
+
|
|
12
|
+
export class TestNode implements INodeType {
|
|
13
|
+
description: INodeTypeDescription = {
|
|
14
|
+
displayName: 'Test Node',
|
|
15
|
+
name: 'testNode',
|
|
16
|
+
group: ['input'],
|
|
17
|
+
version: 1,
|
|
18
|
+
description: 'A test node',
|
|
19
|
+
defaults: { name: 'Test Node' },
|
|
20
|
+
inputs: ${inputs},
|
|
21
|
+
outputs: ${outputs},
|
|
22
|
+
properties: [],
|
|
23
|
+
};
|
|
24
|
+
}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Lets a test spell the connection key itself, e.g. `'inputs'` or `['inputs']`. */
|
|
28
|
+
function createNodeCodeWithProperty(property: string): string {
|
|
29
|
+
return `
|
|
30
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
31
|
+
import { NodeConnectionTypes } from 'n8n-workflow';
|
|
32
|
+
|
|
33
|
+
export class TestNode implements INodeType {
|
|
34
|
+
description: INodeTypeDescription = {
|
|
35
|
+
displayName: 'Test Node',
|
|
36
|
+
name: 'testNode',
|
|
37
|
+
group: ['input'],
|
|
38
|
+
version: 1,
|
|
39
|
+
description: 'A test node',
|
|
40
|
+
defaults: { name: 'Test Node' },
|
|
41
|
+
${property},
|
|
42
|
+
outputs: [NodeConnectionTypes.Main],
|
|
43
|
+
properties: [],
|
|
44
|
+
};
|
|
45
|
+
}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const MAIN = '[NodeConnectionTypes.Main]';
|
|
49
|
+
|
|
50
|
+
ruleTester.run('no-unsafe-connection-type-cast', NoUnsafeConnectionTypeCastRule, {
|
|
51
|
+
valid: [
|
|
52
|
+
{
|
|
53
|
+
name: 'class that does not implement INodeType',
|
|
54
|
+
code: 'export class RegularClass {\n\tsomeProperty = "value";\n}',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'node without casts',
|
|
58
|
+
code: createNodeCode(MAIN, MAIN),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'node with empty inputs and outputs',
|
|
62
|
+
code: createNodeCode('[]', '[]'),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'dynamic inputs as a template literal expression',
|
|
66
|
+
code: createNodeCode('`={{ (() => [NodeConnectionTypes.Main])() }}`', MAIN),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'as const narrows rather than erases',
|
|
70
|
+
code: createNodeCode(`${MAIN} as const`, MAIN),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'satisfies keeps the type checked',
|
|
74
|
+
code: createNodeCode(`${MAIN} satisfies NodeConnectionType[]`, MAIN),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'a named type assertion is still checked structurally',
|
|
78
|
+
code: createNodeCode(`${MAIN} as NodeConnectionType[]`, MAIN),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'casts elsewhere in the description are untouched',
|
|
82
|
+
code: createNodeCode(MAIN, MAIN).replace('version: 1,', 'version: 1 as never,'),
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
invalid: [
|
|
86
|
+
{
|
|
87
|
+
name: 'as never on inputs',
|
|
88
|
+
code: createNodeCode("['main'] as never", MAIN),
|
|
89
|
+
errors: [
|
|
90
|
+
{
|
|
91
|
+
messageId: 'erasingCast',
|
|
92
|
+
data: { typeName: 'never', property: 'inputs' },
|
|
93
|
+
suggestions: [
|
|
94
|
+
{
|
|
95
|
+
messageId: 'removeCast',
|
|
96
|
+
output: createNodeCode("['main']", MAIN),
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'as never on outputs',
|
|
104
|
+
code: createNodeCode(MAIN, "['main'] as never"),
|
|
105
|
+
errors: [
|
|
106
|
+
{
|
|
107
|
+
messageId: 'erasingCast',
|
|
108
|
+
data: { typeName: 'never', property: 'outputs' },
|
|
109
|
+
suggestions: [
|
|
110
|
+
{
|
|
111
|
+
messageId: 'removeCast',
|
|
112
|
+
output: createNodeCode(MAIN, "['main']"),
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'as never on both inputs and outputs',
|
|
120
|
+
code: createNodeCode("['main'] as never", "['main'] as never"),
|
|
121
|
+
errors: [
|
|
122
|
+
{
|
|
123
|
+
messageId: 'erasingCast',
|
|
124
|
+
data: { typeName: 'never', property: 'inputs' },
|
|
125
|
+
suggestions: [
|
|
126
|
+
{
|
|
127
|
+
messageId: 'removeCast',
|
|
128
|
+
output: createNodeCode("['main']", "['main'] as never"),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
messageId: 'erasingCast',
|
|
134
|
+
data: { typeName: 'never', property: 'outputs' },
|
|
135
|
+
suggestions: [
|
|
136
|
+
{
|
|
137
|
+
messageId: 'removeCast',
|
|
138
|
+
output: createNodeCode("['main'] as never", "['main']"),
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'as any',
|
|
146
|
+
code: createNodeCode("['main'] as any", MAIN),
|
|
147
|
+
errors: [
|
|
148
|
+
{
|
|
149
|
+
messageId: 'erasingCast',
|
|
150
|
+
data: { typeName: 'any', property: 'inputs' },
|
|
151
|
+
suggestions: [
|
|
152
|
+
{
|
|
153
|
+
messageId: 'removeCast',
|
|
154
|
+
output: createNodeCode("['main']", MAIN),
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'as unknown',
|
|
162
|
+
code: createNodeCode("['main'] as unknown", MAIN),
|
|
163
|
+
errors: [
|
|
164
|
+
{
|
|
165
|
+
messageId: 'erasingCast',
|
|
166
|
+
data: { typeName: 'unknown', property: 'inputs' },
|
|
167
|
+
suggestions: [
|
|
168
|
+
{
|
|
169
|
+
messageId: 'removeCast',
|
|
170
|
+
output: createNodeCode("['main']", MAIN),
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'double cast through unknown removes the whole chain',
|
|
178
|
+
code: createNodeCode("['main'] as unknown as NodeConnectionType[]", MAIN),
|
|
179
|
+
errors: [
|
|
180
|
+
{
|
|
181
|
+
messageId: 'erasingCast',
|
|
182
|
+
data: { typeName: 'unknown', property: 'inputs' },
|
|
183
|
+
suggestions: [
|
|
184
|
+
{
|
|
185
|
+
messageId: 'removeCast',
|
|
186
|
+
output: createNodeCode("['main']", MAIN),
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: 'angle-bracket assertion',
|
|
194
|
+
code: createNodeCode("<never>['main']", MAIN),
|
|
195
|
+
errors: [
|
|
196
|
+
{
|
|
197
|
+
messageId: 'erasingCast',
|
|
198
|
+
data: { typeName: 'never', property: 'inputs' },
|
|
199
|
+
suggestions: [
|
|
200
|
+
{
|
|
201
|
+
messageId: 'removeCast',
|
|
202
|
+
output: createNodeCode("['main']", MAIN),
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: 'erasing cast hidden behind an outer satisfies',
|
|
210
|
+
code: createNodeCode("['main'] as never satisfies NodeConnectionType[]", MAIN),
|
|
211
|
+
errors: [
|
|
212
|
+
{
|
|
213
|
+
messageId: 'erasingCast',
|
|
214
|
+
data: { typeName: 'never', property: 'inputs' },
|
|
215
|
+
suggestions: [
|
|
216
|
+
{
|
|
217
|
+
messageId: 'removeCast',
|
|
218
|
+
output: createNodeCode("['main'] satisfies NodeConnectionType[]", MAIN),
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: 'erasing cast wrapped around an inner satisfies',
|
|
226
|
+
code: createNodeCode("['main'] satisfies NodeConnectionType[] as never", MAIN),
|
|
227
|
+
errors: [
|
|
228
|
+
{
|
|
229
|
+
messageId: 'erasingCast',
|
|
230
|
+
data: { typeName: 'never', property: 'inputs' },
|
|
231
|
+
suggestions: [
|
|
232
|
+
{
|
|
233
|
+
messageId: 'removeCast',
|
|
234
|
+
output: createNodeCode("['main'] satisfies NodeConnectionType[]", MAIN),
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'cast nested under satisfies, with a second cast outside it',
|
|
242
|
+
code: createNodeCode("['main'] as never satisfies NodeConnectionType[] as unknown", MAIN),
|
|
243
|
+
errors: [
|
|
244
|
+
{
|
|
245
|
+
messageId: 'erasingCast',
|
|
246
|
+
data: { typeName: 'unknown', property: 'inputs' },
|
|
247
|
+
suggestions: [
|
|
248
|
+
{
|
|
249
|
+
messageId: 'removeCast',
|
|
250
|
+
// Both casts go, the satisfies between them stays.
|
|
251
|
+
output: createNodeCode("['main'] satisfies NodeConnectionType[]", MAIN),
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: 'quoted connection key',
|
|
259
|
+
code: createNodeCodeWithProperty("'inputs': ['main'] as never"),
|
|
260
|
+
errors: [
|
|
261
|
+
{
|
|
262
|
+
messageId: 'erasingCast',
|
|
263
|
+
data: { typeName: 'never', property: 'inputs' },
|
|
264
|
+
suggestions: [
|
|
265
|
+
{
|
|
266
|
+
messageId: 'removeCast',
|
|
267
|
+
output: createNodeCodeWithProperty("'inputs': ['main']"),
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: 'computed connection key',
|
|
275
|
+
code: createNodeCodeWithProperty("['inputs']: ['main'] as never"),
|
|
276
|
+
errors: [
|
|
277
|
+
{
|
|
278
|
+
messageId: 'erasingCast',
|
|
279
|
+
data: { typeName: 'never', property: 'inputs' },
|
|
280
|
+
suggestions: [
|
|
281
|
+
{
|
|
282
|
+
messageId: 'removeCast',
|
|
283
|
+
output: createNodeCodeWithProperty("['inputs']: ['main']"),
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: 'cast hiding a malformed declaration',
|
|
291
|
+
code: createNodeCode("'not-an-array' as never", MAIN),
|
|
292
|
+
errors: [
|
|
293
|
+
{
|
|
294
|
+
messageId: 'erasingCast',
|
|
295
|
+
data: { typeName: 'never', property: 'inputs' },
|
|
296
|
+
suggestions: [
|
|
297
|
+
{
|
|
298
|
+
messageId: 'removeCast',
|
|
299
|
+
output: createNodeCode("'not-an-array'", MAIN),
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
+
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
isNodeTypeClass,
|
|
6
|
+
findClassProperty,
|
|
7
|
+
findObjectProperty,
|
|
8
|
+
findJsonProperty,
|
|
9
|
+
createRule,
|
|
10
|
+
} from '../utils/index.js';
|
|
11
|
+
|
|
12
|
+
const ERASING_KEYWORDS: Partial<Record<AST_NODE_TYPES, string>> = {
|
|
13
|
+
[AST_NODE_TYPES.TSNeverKeyword]: 'never',
|
|
14
|
+
[AST_NODE_TYPES.TSAnyKeyword]: 'any',
|
|
15
|
+
[AST_NODE_TYPES.TSUnknownKeyword]: 'unknown',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type Cast = TSESTree.TSAsExpression | TSESTree.TSTypeAssertion;
|
|
19
|
+
|
|
20
|
+
function isCast(node: TSESTree.Node): node is Cast {
|
|
21
|
+
return (
|
|
22
|
+
node.type === AST_NODE_TYPES.TSAsExpression || node.type === AST_NODE_TYPES.TSTypeAssertion
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Wrapper = Cast | TSESTree.TSSatisfiesExpression;
|
|
27
|
+
|
|
28
|
+
function isWrapper(node: TSESTree.Node): node is Wrapper {
|
|
29
|
+
return isCast(node) || node.type === AST_NODE_TYPES.TSSatisfiesExpression;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Walks a wrapper chain such as `x as unknown as string[]` or
|
|
34
|
+
* `x as never satisfies string[]`, outermost first. `satisfies` is traversed
|
|
35
|
+
* but never collected: it checks the value instead of erasing its type, so it
|
|
36
|
+
* can hide a cast without being one.
|
|
37
|
+
*/
|
|
38
|
+
function collectCasts(node: TSESTree.Node): Cast[] {
|
|
39
|
+
const casts: Cast[] = [];
|
|
40
|
+
let current = node;
|
|
41
|
+
while (isWrapper(current)) {
|
|
42
|
+
if (isCast(current)) casts.push(current);
|
|
43
|
+
current = current.expression;
|
|
44
|
+
}
|
|
45
|
+
return casts;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Removes the cast syntax itself and nothing else, so every other part of the
|
|
50
|
+
* chain survives: a `satisfies` at any depth, and the expression's own text.
|
|
51
|
+
*/
|
|
52
|
+
function removeCastSyntax(fixer: TSESLint.RuleFixer, cast: Cast): TSESLint.RuleFix {
|
|
53
|
+
return cast.type === AST_NODE_TYPES.TSTypeAssertion
|
|
54
|
+
? // `<never>expr`: drop the leading `<never>`
|
|
55
|
+
fixer.removeRange([cast.range[0], cast.expression.range[0]])
|
|
56
|
+
: // `expr as never`: drop the trailing ` as never`
|
|
57
|
+
fixer.removeRange([cast.expression.range[1], cast.range[1]]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const NoUnsafeConnectionTypeCastRule = createRule({
|
|
61
|
+
name: 'no-unsafe-connection-type-cast',
|
|
62
|
+
meta: {
|
|
63
|
+
type: 'problem',
|
|
64
|
+
docs: {
|
|
65
|
+
description:
|
|
66
|
+
'Disallow type-erasing casts (`as never`, `as any`, `as unknown`) on node description `inputs`/`outputs`',
|
|
67
|
+
},
|
|
68
|
+
messages: {
|
|
69
|
+
erasingCast:
|
|
70
|
+
'Remove the `as {{typeName}}` cast on "{{property}}". It disables type checking, so a malformed connection declaration passes both TypeScript and lint.',
|
|
71
|
+
removeCast: 'Remove the cast',
|
|
72
|
+
},
|
|
73
|
+
hasSuggestions: true,
|
|
74
|
+
schema: [],
|
|
75
|
+
},
|
|
76
|
+
defaultOptions: [],
|
|
77
|
+
create(context) {
|
|
78
|
+
function checkProperty(value: TSESTree.Node, property: 'inputs' | 'outputs') {
|
|
79
|
+
const casts = collectCasts(value);
|
|
80
|
+
const [outermost] = casts;
|
|
81
|
+
if (!outermost) return;
|
|
82
|
+
|
|
83
|
+
// A chain is unsafe if any link erases the type, e.g. the `unknown`
|
|
84
|
+
// in `as unknown as string[]` is what defeats the checker.
|
|
85
|
+
const erasing = casts.find((cast) => ERASING_KEYWORDS[cast.typeAnnotation.type]);
|
|
86
|
+
if (!erasing) return;
|
|
87
|
+
|
|
88
|
+
// Report the outermost cast rather than the whole property value, so a
|
|
89
|
+
// `satisfies` wrapped around it stays put when the suggestion is applied.
|
|
90
|
+
context.report({
|
|
91
|
+
node: outermost,
|
|
92
|
+
messageId: 'erasingCast',
|
|
93
|
+
data: {
|
|
94
|
+
typeName: ERASING_KEYWORDS[erasing.typeAnnotation.type],
|
|
95
|
+
property,
|
|
96
|
+
},
|
|
97
|
+
suggest: [
|
|
98
|
+
{
|
|
99
|
+
messageId: 'removeCast',
|
|
100
|
+
fix: (fixer) => casts.map((cast) => removeCastSyntax(fixer, cast)),
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
ClassDeclaration(node) {
|
|
108
|
+
if (!isNodeTypeClass(node)) return;
|
|
109
|
+
|
|
110
|
+
const descriptionProperty = findClassProperty(node, 'description');
|
|
111
|
+
if (!descriptionProperty) return;
|
|
112
|
+
|
|
113
|
+
const descriptionValue = descriptionProperty.value;
|
|
114
|
+
if (descriptionValue?.type !== AST_NODE_TYPES.ObjectExpression) return;
|
|
115
|
+
|
|
116
|
+
for (const prop of ['inputs', 'outputs'] as const) {
|
|
117
|
+
const property =
|
|
118
|
+
findObjectProperty(descriptionValue, prop) ?? findJsonProperty(descriptionValue, prop);
|
|
119
|
+
if (!property) continue;
|
|
120
|
+
checkProperty(property.value, prop);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
});
|
|
@@ -41,10 +41,7 @@ export const NodeClassDescriptionIconMissingRule = createRule({
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
const descriptionProperty = findClassProperty(node, 'description');
|
|
44
|
-
if (
|
|
45
|
-
!descriptionProperty?.value ||
|
|
46
|
-
descriptionProperty.value.type !== TSESTree.AST_NODE_TYPES.ObjectExpression
|
|
47
|
-
) {
|
|
44
|
+
if (descriptionProperty?.value?.type !== TSESTree.AST_NODE_TYPES.ObjectExpression) {
|
|
48
45
|
return;
|
|
49
46
|
}
|
|
50
47
|
|
|
@@ -41,10 +41,7 @@ export const RequireNodeDescriptionFieldsRule = createRule({
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
const descriptionProperty = findClassProperty(node, 'description');
|
|
44
|
-
if (
|
|
45
|
-
!descriptionProperty?.value ||
|
|
46
|
-
descriptionProperty.value.type !== TSESTree.AST_NODE_TYPES.ObjectExpression
|
|
47
|
-
) {
|
|
44
|
+
if (descriptionProperty?.value?.type !== TSESTree.AST_NODE_TYPES.ObjectExpression) {
|
|
48
45
|
return;
|
|
49
46
|
}
|
|
50
47
|
|
|
@@ -30,15 +30,12 @@ export const ResourceOperationPatternRule = createRule({
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const analyzeNodeDescription = (descriptionValue: TSESTree.Expression | null): void => {
|
|
33
|
-
if (
|
|
33
|
+
if (descriptionValue?.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const propertiesProperty = findObjectProperty(descriptionValue, 'properties');
|
|
38
|
-
if (
|
|
39
|
-
!propertiesProperty?.value ||
|
|
40
|
-
propertiesProperty.value.type !== AST_NODE_TYPES.ArrayExpression
|
|
41
|
-
) {
|
|
38
|
+
if (propertiesProperty?.value?.type !== AST_NODE_TYPES.ArrayExpression) {
|
|
42
39
|
return;
|
|
43
40
|
}
|
|
44
41
|
|
|
@@ -48,7 +45,7 @@ export const ResourceOperationPatternRule = createRule({
|
|
|
48
45
|
let operationNode: TSESTree.Node | null = null;
|
|
49
46
|
|
|
50
47
|
for (const property of propertiesArray.elements) {
|
|
51
|
-
if (
|
|
48
|
+
if (property?.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
52
49
|
continue;
|
|
53
50
|
}
|
|
54
51
|
|
|
@@ -22,7 +22,7 @@ function parsePersonString(value: string): { name: string; email: string } {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function isNonEmptyStringLiteral(node: TSESTree.Property | null): boolean {
|
|
25
|
-
if (
|
|
25
|
+
if (node?.value.type !== AST_NODE_TYPES.Literal) {
|
|
26
26
|
return false;
|
|
27
27
|
}
|
|
28
28
|
const { value } = node.value;
|