@hero-design/eslint-plugin 7.27.1

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.
@@ -0,0 +1,189 @@
1
+ /**
2
+ * @fileoverview Disallow deprecated component prop's values
3
+ * @author Thong Quach
4
+ */
5
+ 'use strict';
6
+
7
+ //------------------------------------------------------------------------------
8
+ // Rule Definition
9
+ //------------------------------------------------------------------------------
10
+
11
+ /** @type {import('eslint').Rule.RuleModule} */
12
+ module.exports = {
13
+ meta: {
14
+ type: 'problem',
15
+ docs: {
16
+ description: "Disallow deprecated component prop's values",
17
+ recommended: false,
18
+ url: null, // URL to the documentation page for this rule
19
+ },
20
+ fixable: null, // Or `code` or `whitespace`
21
+ schema: [
22
+ {
23
+ type: 'object',
24
+ properties: {
25
+ package: { type: 'string' },
26
+ components: {
27
+ type: 'array',
28
+ items: {
29
+ type: 'object',
30
+ properties: {
31
+ name: { type: 'string' },
32
+ props: {
33
+ type: 'array',
34
+ items: {
35
+ type: 'object',
36
+ properties: {
37
+ name: { type: 'string' },
38
+ values: {
39
+ type: 'array',
40
+ },
41
+ },
42
+ },
43
+ },
44
+ },
45
+ },
46
+ },
47
+ },
48
+ additionalProperties: false,
49
+ required: ['package', 'components'],
50
+ },
51
+ ],
52
+ messages: {
53
+ deprecatedValue:
54
+ 'The value "{{ value }}" of "{{ prop }}" prop has been deprecated.',
55
+ },
56
+ },
57
+
58
+ create(context) {
59
+ const components = context.options[0].components.map((c) => ({
60
+ props: c.props.map((p) => p.name),
61
+ values: c.props.map((p) => p.values),
62
+ identifiers: c.name.split('.'),
63
+ }));
64
+ let importedComponents = [];
65
+
66
+ const getDeprecatedComponent = (node) => {
67
+ return importedComponents.find((c) => {
68
+ const { identifiers } = c;
69
+ const elementName = node.name;
70
+
71
+ // <Radio />
72
+ if (elementName.type === 'JSXIdentifier' && identifiers.length === 1) {
73
+ return elementName.name === identifiers[0];
74
+ }
75
+
76
+ if (elementName.type === 'JSXMemberExpression') {
77
+ // <Radio.Group />
78
+ if (identifiers.length === 2) {
79
+ return (
80
+ elementName.object.name === identifiers[0] &&
81
+ elementName.property.name === identifiers[1]
82
+ );
83
+ }
84
+
85
+ // <HD.Radio.Group />
86
+ if (identifiers.length === 3) {
87
+ return (
88
+ elementName.object.object.name === identifiers[0] &&
89
+ elementName.object.property.name === identifiers[1] &&
90
+ elementName.property.name === identifiers[2]
91
+ );
92
+ }
93
+ }
94
+
95
+ return false;
96
+ });
97
+ };
98
+
99
+ return {
100
+ ImportDeclaration(node) {
101
+ if (node.source.value !== context.options[0].package) return;
102
+
103
+ // import * as HD from "@hero-design/rn";
104
+ if (node.specifiers[0].type === 'ImportNamespaceSpecifier') {
105
+ const localPackageName = node.specifiers[0].local.name;
106
+
107
+ importedComponents = components.map((c) => ({
108
+ ...c,
109
+ identifiers: [localPackageName].concat(c.identifiers),
110
+ }));
111
+
112
+ return;
113
+ }
114
+
115
+ // import { Card, Box } from "@hero-design/rn";
116
+ // import { Card as HDCard, Box } from "hero-design/rn";
117
+ node.specifiers.forEach((spec) => {
118
+ if (spec.type !== 'ImportSpecifier') return;
119
+
120
+ const importingComponents = components
121
+ .filter((c) => spec.imported.name === c.identifiers[0])
122
+ .map((c) => ({
123
+ ...c,
124
+ identifiers: [spec.local.name].concat(c.identifiers.slice(1)),
125
+ }));
126
+
127
+ importedComponents.push(...importingComponents);
128
+ });
129
+ },
130
+
131
+ JSXOpeningElement(node) {
132
+ if (importedComponents.length === 0) return;
133
+
134
+ const deprecatedComponent = getDeprecatedComponent(node);
135
+ if (deprecatedComponent === undefined) return;
136
+
137
+ node.attributes.forEach((atb) => {
138
+ if (atb.type !== 'JSXAttribute') return;
139
+
140
+ const deprecatedPropIndex = deprecatedComponent.props.findIndex(
141
+ (p) => p === atb.name.name
142
+ );
143
+ if (deprecatedPropIndex < 0) return;
144
+
145
+ const deprecatedValues =
146
+ deprecatedComponent.values[deprecatedPropIndex];
147
+ const currValue = atb.value;
148
+
149
+ if (currValue.type === 'Literal') {
150
+ const deprecatedValue = deprecatedValues.find(
151
+ (v) => v === currValue.value
152
+ );
153
+
154
+ if (deprecatedValue !== undefined) {
155
+ context.report({
156
+ node: atb.value,
157
+ messageId: 'deprecatedValue',
158
+ data: {
159
+ prop: deprecatedComponent.props[deprecatedPropIndex],
160
+ value: deprecatedValue,
161
+ },
162
+ });
163
+ }
164
+ }
165
+
166
+ if (
167
+ currValue.type === 'JSXExpressionContainer' &&
168
+ currValue.expression.type === 'Literal'
169
+ ) {
170
+ const deprecatedValue = deprecatedValues.find(
171
+ (v) => v === currValue.expression.value
172
+ );
173
+
174
+ if (deprecatedValue !== undefined) {
175
+ context.report({
176
+ node: atb.value,
177
+ messageId: 'deprecatedValue',
178
+ data: {
179
+ prop: deprecatedComponent.props[deprecatedPropIndex],
180
+ value: deprecatedValue,
181
+ },
182
+ });
183
+ }
184
+ }
185
+ });
186
+ },
187
+ };
188
+ },
189
+ };
@@ -0,0 +1,146 @@
1
+ /**
2
+ * @fileoverview Disallow deprecated component props
3
+ * @author Thong Quach
4
+ */
5
+ 'use strict';
6
+
7
+ //------------------------------------------------------------------------------
8
+ // Rule Definition
9
+ //------------------------------------------------------------------------------
10
+
11
+ /** @type {import('eslint').Rule.RuleModule} */
12
+ module.exports = {
13
+ meta: {
14
+ type: 'problem',
15
+ docs: {
16
+ description: 'Disallow deprecated component props',
17
+ recommended: false,
18
+ url: null, // URL to the documentation page for this rule
19
+ },
20
+ fixable: null, // Or `code` or `whitespace`
21
+ schema: [
22
+ {
23
+ type: 'object',
24
+ properties: {
25
+ package: { type: 'string' },
26
+ components: {
27
+ type: 'array',
28
+ items: {
29
+ type: 'object',
30
+ properties: {
31
+ name: { type: 'string' },
32
+ props: {
33
+ type: 'array',
34
+ items: {
35
+ type: 'string',
36
+ },
37
+ },
38
+ },
39
+ },
40
+ },
41
+ },
42
+ additionalProperties: false,
43
+ required: ['package', 'components'],
44
+ },
45
+ ],
46
+ messages: {
47
+ deprecatedProp: 'The prop "{{ prop }}" has been deprecated.',
48
+ },
49
+ },
50
+
51
+ create(context) {
52
+ const components = context.options[0].components.map((c) => ({
53
+ props: c.props,
54
+ identifiers: c.name.split('.'),
55
+ }));
56
+ let importedComponents = [];
57
+
58
+ const getDeprecatedComponent = (node) => {
59
+ return importedComponents.find((c) => {
60
+ const { identifiers } = c;
61
+ const elementName = node.name;
62
+
63
+ // <Radio />
64
+ if (elementName.type === 'JSXIdentifier' && identifiers.length === 1) {
65
+ return elementName.name === identifiers[0];
66
+ }
67
+
68
+ if (elementName.type === 'JSXMemberExpression') {
69
+ // <Radio.Group />
70
+ if (identifiers.length === 2) {
71
+ return (
72
+ elementName.object.name === identifiers[0] &&
73
+ elementName.property.name === identifiers[1]
74
+ );
75
+ }
76
+
77
+ // <HD.Radio.Group />
78
+ if (identifiers.length === 3) {
79
+ return (
80
+ elementName.object.object.name === identifiers[0] &&
81
+ elementName.object.property.name === identifiers[1] &&
82
+ elementName.property.name === identifiers[2]
83
+ );
84
+ }
85
+ }
86
+
87
+ return false;
88
+ });
89
+ };
90
+
91
+ return {
92
+ ImportDeclaration(node) {
93
+ if (node.source.value !== context.options[0].package) return;
94
+
95
+ // import * as HD from "@hero-design/rn";
96
+ if (node.specifiers[0].type === 'ImportNamespaceSpecifier') {
97
+ const localPackageName = node.specifiers[0].local.name;
98
+
99
+ importedComponents = components.map((c) => ({
100
+ ...c,
101
+ identifiers: [localPackageName].concat(c.identifiers),
102
+ }));
103
+
104
+ return;
105
+ }
106
+
107
+ // import { Card, Box } from "@hero-design/rn";
108
+ // import { Card as HDCard, Box } from "hero-design/rn";
109
+ node.specifiers.forEach((spec) => {
110
+ if (spec.type !== 'ImportSpecifier') return;
111
+
112
+ const importingComponents = components
113
+ .filter((c) => spec.imported.name === c.identifiers[0])
114
+ .map((c) => ({
115
+ ...c,
116
+ identifiers: [spec.local.name].concat(c.identifiers.slice(1)),
117
+ }));
118
+
119
+ importedComponents.push(...importingComponents);
120
+ });
121
+ },
122
+
123
+ JSXOpeningElement(node) {
124
+ if (importedComponents.length === 0) return;
125
+
126
+ const deprecatedComponent = getDeprecatedComponent(node);
127
+ if (deprecatedComponent === undefined) return;
128
+
129
+ node.attributes.forEach((atb) => {
130
+ if (atb.type !== 'JSXAttribute') return;
131
+
132
+ const deprecatedProp = deprecatedComponent.props.find(
133
+ (p) => p === atb.name.name
134
+ );
135
+ if (deprecatedProp !== undefined) {
136
+ context.report({
137
+ node: atb,
138
+ messageId: 'deprecatedProp',
139
+ data: { prop: deprecatedProp },
140
+ });
141
+ }
142
+ });
143
+ },
144
+ };
145
+ },
146
+ };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @fileoverview Disallow deprecated theme keys
3
+ * @author Thong Quach
4
+ */
5
+ 'use strict';
6
+
7
+ //------------------------------------------------------------------------------
8
+ // Rule Definition
9
+ //------------------------------------------------------------------------------
10
+
11
+ /** @type {import('eslint').Rule.RuleModule} */
12
+ module.exports = {
13
+ meta: {
14
+ type: 'problem', // `problem`, `suggestion`, or `layout`
15
+ docs: {
16
+ description: 'Disallow deprecated theme keys',
17
+ recommended: false,
18
+ url: null, // URL to the documentation page for this rule
19
+ },
20
+ fixable: 'code', // Or `code` or `whitespace`
21
+ schema: [
22
+ {
23
+ type: 'object',
24
+ properties: {
25
+ keys: {
26
+ type: 'array',
27
+ items: {
28
+ type: 'object',
29
+ properties: {
30
+ old: { type: 'string' },
31
+ new: { type: 'string' },
32
+ },
33
+ },
34
+ },
35
+ },
36
+ additionalProperties: false,
37
+ required: ['keys'],
38
+ },
39
+ ],
40
+ messages: {
41
+ deprecatedThemeKey: 'The key "{{ key }}" in theme has been deprecated.',
42
+ },
43
+ },
44
+
45
+ create(context) {
46
+ const oldKeys = context.options[0].keys.map((k) => k.old);
47
+ const newKeys = context.options[0].keys.map((k) => k.new);
48
+
49
+ const findPath = (node, identifiers) => {
50
+ const property = node.property;
51
+
52
+ if (
53
+ property != null &&
54
+ property.type === 'Identifier' &&
55
+ property.name != null
56
+ ) {
57
+ return findPath(node.parent, [...identifiers, property.name]);
58
+ }
59
+
60
+ return identifiers.join('.');
61
+ };
62
+
63
+ return {
64
+ MemberExpression(node) {
65
+ const object = node.object;
66
+ if (object == null) return;
67
+
68
+ if (object.type === 'Identifier' && object.name === 'theme') {
69
+ const path = findPath(node, []);
70
+ const deprecatedKeyIndex = oldKeys.findIndex((k) => k === path);
71
+
72
+ if (deprecatedKeyIndex >= 0) {
73
+ const reportingNode =
74
+ node.parent.type === 'MemberExpression' ? node.parent : node;
75
+
76
+ context.report({
77
+ node: reportingNode,
78
+ messageId: 'deprecatedThemeKey',
79
+ data: { key: oldKeys[deprecatedKeyIndex] },
80
+ fix: function (fixer) {
81
+ return fixer.replaceText(
82
+ reportingNode,
83
+ `theme.${newKeys[deprecatedKeyIndex]}`
84
+ );
85
+ },
86
+ });
87
+ }
88
+ }
89
+ },
90
+ };
91
+ },
92
+ };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @fileoverview Disallow not recommended imports
3
+ * @author Thong Quach
4
+ */
5
+ 'use strict';
6
+
7
+ //------------------------------------------------------------------------------
8
+ // Rule Definition
9
+ //------------------------------------------------------------------------------
10
+
11
+ /** @type {import('eslint').Rule.RuleModule} */
12
+ module.exports = {
13
+ meta: {
14
+ type: 'problem',
15
+ docs: {
16
+ description: 'Disallow not recommended imports ',
17
+ recommended: false,
18
+ url: null, // URL to the documentation page for this rule
19
+ },
20
+ fixable: null, // Or `code` or `whitespace`
21
+ schema: {
22
+ type: 'array',
23
+ items: {
24
+ type: 'object',
25
+ properties: {
26
+ package: { type: 'string' },
27
+ imports: {
28
+ type: 'array',
29
+ items: {
30
+ type: 'object',
31
+ properties: {
32
+ name: { type: 'string' },
33
+ message: { type: 'string' },
34
+ },
35
+ },
36
+ },
37
+ },
38
+ additionalProperties: false,
39
+ required: ['package', 'imports'],
40
+ },
41
+ },
42
+ messages: {
43
+ notRecommendedImport:
44
+ 'Importing "{{ import }}" from "{{ package }}" is not recommended. {{ message }}',
45
+ },
46
+ },
47
+
48
+ create(context) {
49
+ const packages = context.options.map((opt) => opt.package);
50
+ const imports = context.options.map((opt) => opt.imports);
51
+
52
+ return {
53
+ ImportDeclaration(node) {
54
+ const packageIndex = packages.findIndex((p) => p === node.source.value);
55
+ if (packageIndex < 0) return;
56
+
57
+ node.specifiers.forEach((spec) => {
58
+ if (spec.type === 'ImportDefaultSpecifier') {
59
+ const defaultImport = imports[packageIndex].find(
60
+ (imp) => imp.name === 'default'
61
+ );
62
+
63
+ if (defaultImport != null) {
64
+ context.report({
65
+ node: spec,
66
+ messageId: 'notRecommendedImport',
67
+ data: {
68
+ import: defaultImport.name,
69
+ package: packages[packageIndex],
70
+ message: defaultImport.message,
71
+ },
72
+ });
73
+ }
74
+ }
75
+
76
+ if (spec.type !== 'ImportSpecifier') return;
77
+
78
+ const foundImport = imports[packageIndex].find(
79
+ (imp) => imp.name === spec.imported.name
80
+ );
81
+
82
+ if (foundImport != null) {
83
+ context.report({
84
+ node: spec,
85
+ messageId: 'notRecommendedImport',
86
+ data: {
87
+ import: foundImport.name,
88
+ package: packages[packageIndex],
89
+ message: foundImport.message,
90
+ },
91
+ });
92
+ }
93
+ });
94
+ },
95
+ };
96
+ },
97
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@hero-design/eslint-plugin",
3
+ "version": "7.27.1",
4
+ "description": "Hero Design's eslint plugin",
5
+ "keywords": [
6
+ "eslint",
7
+ "eslintplugin",
8
+ "eslint-plugin"
9
+ ],
10
+ "author": "Thong Quach",
11
+ "main": "./lib/index.js",
12
+ "exports": "./lib/index.js",
13
+ "prettier": "prettier-config-hd",
14
+ "scripts": {
15
+ "lint": "eslint .",
16
+ "test": "jest --runInBand",
17
+ "test:watch": "jest --runInBand --watch",
18
+ "test:ci": "jest --runInBand --logHeapUsage",
19
+ "publish:npm": "yarn publish --access public"
20
+ },
21
+ "dependencies": {
22
+ "requireindex": "^1.2.0"
23
+ },
24
+ "devDependencies": {
25
+ "eslint": "^8.10.0",
26
+ "eslint-plugin-eslint-plugin": "^5.0.0",
27
+ "eslint-plugin-node": "^11.1.0",
28
+ "jest": "^27.3.1",
29
+ "prettier-config-hd": "7.27.1"
30
+ },
31
+ "engines": {
32
+ "node": "^14.17.0 || ^16.0.0 || >= 18.0.0"
33
+ },
34
+ "peerDependencies": {
35
+ "eslint": ">=7"
36
+ },
37
+ "license": "ISC"
38
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @fileoverview Disallow deprecated component prop's values
3
+ * @author Thong Quach
4
+ */
5
+ 'use strict';
6
+
7
+ //------------------------------------------------------------------------------
8
+ // Requirements
9
+ //------------------------------------------------------------------------------
10
+
11
+ const rule = require('../../../lib/rules/no-deprecated-component-prop-value'),
12
+ RuleTester = require('eslint').RuleTester;
13
+
14
+ //------------------------------------------------------------------------------
15
+ // Tests
16
+ //------------------------------------------------------------------------------
17
+
18
+ const config = {
19
+ options: [
20
+ {
21
+ package: '@hero-design/rn',
22
+ components: [
23
+ {
24
+ name: 'Tag',
25
+ props: [{ name: 'intent', values: ['default'] }],
26
+ },
27
+ {
28
+ name: 'Alert',
29
+ props: [{ name: 'variant', values: ['default'] }],
30
+ },
31
+ {
32
+ name: 'Button',
33
+ props: [{ name: 'variant', values: ['basic-transparent'] }],
34
+ },
35
+ {
36
+ name: 'Icon',
37
+ props: [
38
+ {
39
+ name: 'icon',
40
+ values: [
41
+ 'carat-down-small',
42
+ 'carat-down',
43
+ 'carat-left-small',
44
+ 'carat-left',
45
+ 'carat-right-small',
46
+ 'carat-right',
47
+ 'carat-up-small',
48
+ 'carat-up',
49
+ ],
50
+ },
51
+ ],
52
+ },
53
+ {
54
+ name: 'Select',
55
+ props: [{ name: 'numberOfLines', values: [1] }],
56
+ },
57
+ {
58
+ name: 'Select.Multi',
59
+ props: [{ name: 'numberOfLines', values: [1] }],
60
+ },
61
+ ],
62
+ },
63
+ ],
64
+ parserOptions: {
65
+ sourceType: 'module',
66
+ ecmaVersion: 6,
67
+ ecmaFeatures: { jsx: true },
68
+ },
69
+ };
70
+
71
+ const ruleTester = new RuleTester();
72
+ ruleTester.run('no-deprecated-component-prop-value', rule, {
73
+ valid: [
74
+ {
75
+ code: '<Box intent="default" {...props} />',
76
+ },
77
+ {
78
+ code: 'import HD from "@hero-design/rn"; <Tag intent="default" {...props} />',
79
+ },
80
+ {
81
+ code: 'import { Tag, Box } from "@hero-design/rn"; <Tag intent="primary" />',
82
+ },
83
+ {
84
+ code: 'import { Tag, Box } from "ant-design"; <Tag intent="default" />',
85
+ },
86
+ {
87
+ code: 'import { Tag as HDTag, Box } from "@hero-design/rn"; <Tag intent="default" />',
88
+ },
89
+ {
90
+ code: 'import { Select } from "@hero-design/rn"; <Select.Multi numberOfLines={2} />',
91
+ },
92
+ {
93
+ code: 'import * as HD from "@hero-design/rn"; <Card variant="basic" />',
94
+ },
95
+ {
96
+ code: 'import * as HD from "@hero-design/rn"; <HD.Select.Multi numberOfLines={2} />',
97
+ },
98
+ ].map((test) => ({
99
+ ...test,
100
+ ...config,
101
+ })),
102
+
103
+ invalid: [
104
+ {
105
+ code: 'import { Tag, Box } from "@hero-design/rn"; <Tag intent="default" />',
106
+ errors: [{ messageId: 'deprecatedValue' }],
107
+ },
108
+ {
109
+ code: 'import { Tag as HDTag, Box } from "@hero-design/rn"; <HDTag intent="default" />',
110
+ errors: [{ messageId: 'deprecatedValue' }],
111
+ },
112
+ {
113
+ code: 'import { Button as HDButton, Icon } from "@hero-design/rn"; <><HDButton variant="basic-transparent" /><Icon icon="carat-down" /></>',
114
+ errors: [
115
+ { messageId: 'deprecatedValue' },
116
+ { messageId: 'deprecatedValue' },
117
+ ],
118
+ },
119
+ {
120
+ code: 'import { Select } from "@hero-design/rn"; <Select numberOfLines={1} />',
121
+ errors: [{ messageId: 'deprecatedValue' }],
122
+ },
123
+ {
124
+ code: 'import { Select } from "@hero-design/rn"; <><Select numberOfLines={1} /><Select.Multi numberOfLines={1} /></>',
125
+ errors: [
126
+ { messageId: 'deprecatedValue' },
127
+ { messageId: 'deprecatedValue' },
128
+ ],
129
+ },
130
+ {
131
+ code: 'import * as HD from "@hero-design/rn"; <HD.Select numberOfLines={1} />',
132
+ errors: [{ messageId: 'deprecatedValue' }],
133
+ },
134
+ {
135
+ code: 'import * as HD from "@hero-design/rn"; <HD.Select.Multi numberOfLines={1} />',
136
+ errors: [{ messageId: 'deprecatedValue' }],
137
+ },
138
+ ].map((test) => ({
139
+ ...test,
140
+ ...config,
141
+ })),
142
+ });