@hero-design/eslint-plugin 8.12.0-rc.0 → 8.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,412 @@
1
+ /**
2
+ * @fileoverview Hero Design's eslint plugin
3
+ * @author Thong Quach
4
+ */
5
+ 'use strict';
6
+
7
+ //------------------------------------------------------------------------------
8
+ // Requirements
9
+ //------------------------------------------------------------------------------
10
+
11
+ const requireIndex = require('requireindex');
12
+
13
+ //------------------------------------------------------------------------------
14
+ // Plugin Definition
15
+ //------------------------------------------------------------------------------
16
+
17
+ const themeKeysMap = [
18
+ {
19
+ old: 'colors.globalPrimary',
20
+ new: 'colors.onDefaultGlobalSurface',
21
+ },
22
+ {
23
+ old: 'colors.globalPrimaryLight',
24
+ new: 'colors.globalPrimaryLight',
25
+ },
26
+ {
27
+ old: 'colors.globalPrimaryBackground',
28
+ new: 'colors.defaultGlobalSurface',
29
+ },
30
+ {
31
+ old: 'colors.primaryLight',
32
+ new: 'colors.secondary',
33
+ },
34
+ {
35
+ old: 'colors.primaryDark',
36
+ new: 'colors.primary',
37
+ },
38
+ {
39
+ old: 'colors.primaryBackground',
40
+ new: 'colors.highlightedSurface',
41
+ },
42
+ {
43
+ old: 'colors.primaryBackgroundDark',
44
+ new: 'colors.mutedOnDefaultGlobalSurface',
45
+ },
46
+ {
47
+ old: 'colors.secondaryLight',
48
+ new: 'colors.secondaryLight',
49
+ },
50
+ {
51
+ old: 'colors.secondaryBackground',
52
+ new: 'colors.secondaryBackground',
53
+ },
54
+ {
55
+ old: 'colors.infoMediumLight',
56
+ new: 'colors.mutedInfo',
57
+ },
58
+ {
59
+ old: 'colors.infoLight',
60
+ new: 'colors.mutedInfo',
61
+ },
62
+ {
63
+ old: 'colors.infoBackground',
64
+ new: 'colors.infoSurface',
65
+ },
66
+ {
67
+ old: 'colors.successLight',
68
+ new: 'colors.mutedSuccess',
69
+ },
70
+ {
71
+ old: 'colors.successDark',
72
+ new: 'colors.success',
73
+ },
74
+ {
75
+ old: 'colors.successBackground',
76
+ new: 'colors.successSurface',
77
+ },
78
+ {
79
+ old: 'colors.danger',
80
+ new: 'colors.error',
81
+ },
82
+ {
83
+ old: 'colors.dangerMediumLight',
84
+ new: 'colors.mutedError',
85
+ },
86
+ {
87
+ old: 'colors.dangerLight',
88
+ new: 'colors.mutedError',
89
+ },
90
+ {
91
+ old: 'colors.dangerBackground',
92
+ new: 'colors.errorSurface',
93
+ },
94
+ {
95
+ old: 'colors.warningLight',
96
+ new: 'colors.mutedWarning',
97
+ },
98
+ {
99
+ old: 'colors.warningDark',
100
+ new: 'colors.warning',
101
+ },
102
+ {
103
+ old: 'colors.warningBackground',
104
+ new: 'colors.warningSurface',
105
+ },
106
+ {
107
+ old: 'colors.platformBackground',
108
+ new: 'colors.defaultGlobalSurface',
109
+ },
110
+ {
111
+ old: 'colors.backgroundLight',
112
+ new: 'colors.neutralGlobalSurface',
113
+ },
114
+ {
115
+ old: 'colors.backgroundDark',
116
+ new: 'colors.darkGlobalSurface',
117
+ },
118
+ {
119
+ old: 'colors.text',
120
+ new: 'colors.onDefaultGlobalSurface',
121
+ },
122
+ {
123
+ old: 'colors.subduedText',
124
+ new: 'colors.mutedOnDefaultGlobalSurface',
125
+ },
126
+ {
127
+ old: 'colors.disabledText',
128
+ new: 'colors.inactiveOnDefaultGlobalSurface',
129
+ },
130
+ {
131
+ old: 'colors.disabledLightText',
132
+ new: 'colors.disabledOnDefaultGlobalSurface',
133
+ },
134
+ {
135
+ old: 'colors.invertedText',
136
+ new: 'colors.onDarkGlobalSurface',
137
+ },
138
+ {
139
+ old: 'colors.outline',
140
+ new: 'colors.secondaryOutline',
141
+ },
142
+ {
143
+ old: 'colors.archivedLight',
144
+ new: 'colors.disabledOnDefaultGlobalSurface',
145
+ },
146
+ {
147
+ old: 'colors.archivedDark',
148
+ new: 'colors.archived',
149
+ },
150
+ {
151
+ old: 'colors.archivedBackground',
152
+ new: 'colors.archivedSurface',
153
+ },
154
+ {
155
+ old: 'colors.black',
156
+ new: 'colors.onDefaultGlobalSurface',
157
+ },
158
+ {
159
+ old: 'colors.inactiveBackground',
160
+ new: 'colors.inactiveOnDefaultGlobalSurface',
161
+ },
162
+ {
163
+ old: 'colors.shadow',
164
+ new: 'colors.secondaryOutline',
165
+ },
166
+ {
167
+ old: 'colors.mutedGlobalPrimary',
168
+ new: 'colors.mutedOnDefaultGlobalSurface',
169
+ },
170
+ {
171
+ old: 'colors.onGlobalPrimary',
172
+ new: 'colors.onDefaultGlobalSurface',
173
+ },
174
+ {
175
+ old: 'colors.globalSecondary',
176
+ new: 'colors.secondary',
177
+ },
178
+ {
179
+ old: 'colors.globalPrimaryOutline',
180
+ new: 'colors.primaryOutline',
181
+ },
182
+ {
183
+ old: 'colors.globalSecondaryOutline',
184
+ new: 'colors.secondaryOutline',
185
+ },
186
+ {
187
+ old: 'colors.mutedPrimary',
188
+ new: 'colors.mutedPrimary',
189
+ },
190
+ {
191
+ old: 'colors.highlightedSecondarySurface',
192
+ new: 'colors.highlightedSecondarySurface',
193
+ },
194
+ {
195
+ old: 'colors.mutedSecondary',
196
+ new: 'colors.mutedSecondary',
197
+ },
198
+ {
199
+ old: 'colors.disabledSecondary',
200
+ new: 'colors.disabledSecondary',
201
+ },
202
+ {
203
+ old: 'colors.lightHighlightedSurface',
204
+ new: 'colors.highlightedSurface',
205
+ },
206
+ ];
207
+ const deprecatedThemeColors = themeKeysMap.map((c) => c.old.split('.')[1]);
208
+ const boxColorProps = [
209
+ 'backgroundColor',
210
+ 'bgColor',
211
+ 'borderColor',
212
+ 'borderTopColor',
213
+ 'borderBottomColor',
214
+ 'borderStartColor',
215
+ 'borderEndColor',
216
+ 'borderLeftColor',
217
+ 'borderRightColor',
218
+ ];
219
+ const deprecatedBoxPropValues = boxColorProps.map((prop) => ({
220
+ name: prop,
221
+ values: deprecatedThemeColors,
222
+ }));
223
+
224
+ // import all rules in lib/rules
225
+ module.exports = {
226
+ rules: requireIndex(__dirname + '/rules'),
227
+ configs: {
228
+ internalRn: {
229
+ plugins: ['@hero-design'],
230
+ rules: {
231
+ '@hero-design/no-deprecated-theme-key': [
232
+ 'error',
233
+ {
234
+ keys: themeKeysMap,
235
+ },
236
+ ],
237
+ '@hero-design/no-deprecated-component-prop': [
238
+ 'error',
239
+ {
240
+ package: '@hero-design/rn',
241
+ components: [
242
+ { name: 'Card', props: ['variant'] },
243
+ { name: 'Switch', props: ['size'] },
244
+ {
245
+ name: 'Select',
246
+ props: ['onDimiss', 'numberOfLines', 'inputProps.required'],
247
+ },
248
+ {
249
+ name: 'Select.Multi',
250
+ props: ['onDimiss', 'numberOfLines', 'inputProps.required'],
251
+ },
252
+ { name: 'Toast.Provider', props: ['position'] },
253
+ ],
254
+ },
255
+ ],
256
+ '@hero-design/no-deprecated-component-prop-value': [
257
+ 'error',
258
+ {
259
+ package: '@hero-design/rn',
260
+ components: [
261
+ {
262
+ name: 'Tag',
263
+ props: [{ name: 'intent', values: ['default'] }],
264
+ },
265
+ {
266
+ name: 'Button',
267
+ props: [{ name: 'variant', values: ['basic-transparent'] }],
268
+ },
269
+ {
270
+ name: 'Button.Icon',
271
+ props: [
272
+ {
273
+ name: 'icon',
274
+ values: [
275
+ 'carat-down-small',
276
+ 'carat-down',
277
+ 'carat-left-small',
278
+ 'carat-left',
279
+ 'carat-right-small',
280
+ 'carat-right',
281
+ 'carat-up-small',
282
+ 'carat-up',
283
+ ],
284
+ },
285
+ ],
286
+ },
287
+ {
288
+ name: 'Button.Utility',
289
+ props: [
290
+ {
291
+ name: 'icon',
292
+ values: [
293
+ 'carat-down-small',
294
+ 'carat-down',
295
+ 'carat-left-small',
296
+ 'carat-left',
297
+ 'carat-right-small',
298
+ 'carat-right',
299
+ 'carat-up-small',
300
+ 'carat-up',
301
+ ],
302
+ },
303
+ ],
304
+ },
305
+ {
306
+ name: 'Icon',
307
+ props: [
308
+ {
309
+ name: 'icon',
310
+ values: [
311
+ 'carat-down-small',
312
+ 'carat-down',
313
+ 'carat-left-small',
314
+ 'carat-left',
315
+ 'carat-right-small',
316
+ 'carat-right',
317
+ 'carat-up-small',
318
+ 'carat-up',
319
+ ],
320
+ },
321
+ ],
322
+ },
323
+ {
324
+ name: 'Box',
325
+ props: deprecatedBoxPropValues,
326
+ },
327
+ ],
328
+ },
329
+ ],
330
+ },
331
+ },
332
+ recommendedRn: {
333
+ plugins: ['@hero-design'],
334
+ extends: ['plugin:@hero-design/internalRn'],
335
+ rules: {
336
+ '@hero-design/not-recommended-import': [
337
+ 'warn',
338
+ {
339
+ package: '@hero-design/rn',
340
+ imports: [
341
+ {
342
+ name: 'swagSystemPalette',
343
+ message: 'Please use colors from useTheme hook instead.',
344
+ },
345
+ {
346
+ name: 'workSystemPalette',
347
+ message: 'Please use colors from useTheme hook instead.',
348
+ },
349
+ {
350
+ name: 'jobsSystemPalette',
351
+ message: 'Please use colors from useTheme hook instead.',
352
+ },
353
+ {
354
+ name: 'walletSystemPalette',
355
+ message: 'Please use colors from useTheme hook instead.',
356
+ },
357
+ {
358
+ name: 'eBensSystemPalette',
359
+ message: 'Please use colors from useTheme hook instead.',
360
+ },
361
+ ],
362
+ },
363
+ {
364
+ package: '@hero-design/colors',
365
+ imports: [
366
+ {
367
+ name: 'default',
368
+ message: 'Please use colors from useTheme hook instead.',
369
+ },
370
+ {
371
+ name: 'defaultWebPalette',
372
+ message: 'Please use colors from useTheme hook instead.',
373
+ },
374
+ {
375
+ name: 'defaultMobilePalette',
376
+ message: 'Please use colors from useTheme hook instead.',
377
+ },
378
+ {
379
+ name: 'mixColor',
380
+ message: 'Please use colors from useTheme hook instead.',
381
+ },
382
+ {
383
+ name: 'colorScales',
384
+ message: 'Please use colors from useTheme hook instead.',
385
+ },
386
+ {
387
+ name: 'eBensPalette',
388
+ message: 'Please use colors from useTheme hook instead.',
389
+ },
390
+ {
391
+ name: 'jobsPalette',
392
+ message: 'Please use colors from useTheme hook instead.',
393
+ },
394
+ {
395
+ name: 'swagPalette',
396
+ message: 'Please use colors from useTheme hook instead.',
397
+ },
398
+ {
399
+ name: 'walletPalette',
400
+ message: 'Please use colors from useTheme hook instead.',
401
+ },
402
+ {
403
+ name: 'workPalette',
404
+ message: 'Please use colors from useTheme hook instead.',
405
+ },
406
+ ],
407
+ },
408
+ ],
409
+ },
410
+ },
411
+ },
412
+ };
@@ -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,165 @@
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.map((p) => p.split('.')),
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[0] === atb.name.name
134
+ );
135
+
136
+ if (deprecatedProp === undefined) return;
137
+ if (deprecatedProp.length === 1) {
138
+ context.report({
139
+ node: atb,
140
+ messageId: 'deprecatedProp',
141
+ data: { prop: deprecatedProp.join('.') },
142
+ });
143
+ }
144
+
145
+ if (deprecatedProp.length === 2) {
146
+ if (atb.value.type !== 'JSXExpressionContainer') return;
147
+ if (atb.value.expression.type !== 'ObjectExpression') return;
148
+
149
+ let nestedDeprecatedProp = atb.value.expression.properties.find(
150
+ (p) => p.key.name === deprecatedProp[1]
151
+ );
152
+
153
+ if (nestedDeprecatedProp !== undefined) {
154
+ context.report({
155
+ node: nestedDeprecatedProp,
156
+ messageId: 'deprecatedProp',
157
+ data: { prop: deprecatedProp.join('.') },
158
+ });
159
+ }
160
+ }
161
+ });
162
+ },
163
+ };
164
+ },
165
+ };
@@ -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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hero-design/eslint-plugin",
3
- "version": "8.12.0-rc.0",
3
+ "version": "8.12.0",
4
4
  "description": "Hero Design's eslint plugin",
5
5
  "keywords": [
6
6
  "eslint",
@@ -26,7 +26,7 @@
26
26
  "eslint-plugin-eslint-plugin": "^5.0.0",
27
27
  "eslint-plugin-node": "^11.1.0",
28
28
  "jest": "^27.3.1",
29
- "prettier-config-hd": "8.12.0-rc.0"
29
+ "prettier-config-hd": "8.12.0"
30
30
  },
31
31
  "engines": {
32
32
  "node": "^14.17.0 || ^16.0.0 || >= 18.0.0"