Skip to content

Commit e2cab87

Browse files
authored
feat: add use-deprecated-from-deprecated (#204)
* feat: add use-deprecated-from-deprecated * chore: add changeset
1 parent 657d04b commit e2cab87

File tree

5 files changed

+227
-0
lines changed

5 files changed

+227
-0
lines changed

.changeset/dry-tips-burn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-primer-react': minor
3+
---
4+
5+
Add use-deprecated-from-deprecated rule
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Use Deprecated from Deprecated
2+
3+
## Rule Details
4+
5+
This rule enforces the usage of deprecated imports from `@primer/react/deprecated`.
6+
7+
👎 Examples of **incorrect** code for this rule
8+
9+
```jsx
10+
import {Dialog} from '@primer/react'
11+
12+
function ExampleComponent() {
13+
return <Dialog>{/* ... */}</Dialog>
14+
}
15+
```
16+
17+
👍 Examples of **correct** code for this rule:
18+
19+
```jsx
20+
import {Dialog} from '@primer/react/deprecated'
21+
22+
function ExampleComponent() {
23+
return <Dialog>{/* ... */}</Dialog>
24+
}
25+
```

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module.exports = {
1010
'a11y-link-in-text-block': require('./rules/a11y-link-in-text-block'),
1111
'a11y-remove-disable-tooltip': require('./rules/a11y-remove-disable-tooltip'),
1212
'a11y-use-next-tooltip': require('./rules/a11y-use-next-tooltip'),
13+
'use-deprecated-from-deprecated': require('./rules/use-deprecated-from-deprecated'),
1314
},
1415
configs: {
1516
recommended: require('./configs/recommended'),
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
'use strict'
2+
3+
const {RuleTester} = require('eslint')
4+
const rule = require('../use-deprecated-from-deprecated')
5+
6+
const ruleTester = new RuleTester({
7+
parserOptions: {
8+
ecmaVersion: 'latest',
9+
sourceType: 'module',
10+
ecmaFeatures: {
11+
jsx: true,
12+
},
13+
},
14+
})
15+
16+
ruleTester.run('use-deprecated-from-deprecated', rule, {
17+
valid: [],
18+
invalid: [
19+
// Single deprecated import
20+
{
21+
code: `import {Tooltip} from '@primer/react'`,
22+
output: `import {Tooltip} from '@primer/react/deprecated'`,
23+
errors: ['Import deprecated components from @primer/react/deprecated'],
24+
},
25+
26+
// Single deprecated import with existing deprecated entrypoint
27+
{
28+
code: `import {Tooltip} from '@primer/react'
29+
import {Dialog} from '@primer/react/deprecated'`,
30+
output: `\nimport {Dialog, Tooltip} from '@primer/react/deprecated'`,
31+
errors: ['Import deprecated components from @primer/react/deprecated'],
32+
},
33+
34+
// Multiple deprecated imports
35+
{
36+
code: `import {Dialog, Tooltip} from '@primer/react'`,
37+
output: `import {Dialog, Tooltip} from '@primer/react/deprecated'`,
38+
errors: ['Import deprecated components from @primer/react/deprecated'],
39+
},
40+
41+
// Mixed deprecated and non-deprecated imports
42+
{
43+
code: `import {Button, Tooltip} from '@primer/react'`,
44+
output: `import {Button, } from '@primer/react'
45+
import {Tooltip} from '@primer/react/deprecated'`,
46+
errors: ['Import deprecated components from @primer/react/deprecated'],
47+
},
48+
49+
// Mixed deprecated and non-deprecated imports with existing deprecated
50+
{
51+
code: `import {Button, Tooltip} from '@primer/react'
52+
import {Dialog} from '@primer/react/deprecated'`,
53+
output: `import {Button, } from '@primer/react'
54+
import {Dialog, Tooltip} from '@primer/react/deprecated'`,
55+
errors: ['Import deprecated components from @primer/react/deprecated'],
56+
},
57+
58+
// Multiple mixed deprecated and non-deprecated imports
59+
{
60+
code: `import {Button, Dialog, Tooltip} from '@primer/react'`,
61+
output: `import {Button, } from '@primer/react'
62+
import {Dialog, Tooltip} from '@primer/react/deprecated'`,
63+
errors: ['Import deprecated components from @primer/react/deprecated'],
64+
},
65+
],
66+
})
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
'use strict'
2+
3+
const url = require('../url')
4+
5+
const components = [
6+
{
7+
identifier: 'Dialog',
8+
entrypoint: '@primer/react',
9+
},
10+
{
11+
identifier: 'Octicon',
12+
entrypoint: '@primer/react',
13+
},
14+
{
15+
identifier: 'Pagehead',
16+
entrypoint: '@primer/react',
17+
},
18+
{
19+
identifier: 'TabNav',
20+
entrypoint: '@primer/react',
21+
},
22+
{
23+
identifier: 'Tooltip',
24+
entrypoint: '@primer/react',
25+
},
26+
]
27+
28+
const entrypoints = new Map()
29+
30+
for (const component of components) {
31+
if (!entrypoints.has(component.entrypoint)) {
32+
entrypoints.set(component.entrypoint, new Set())
33+
}
34+
entrypoints.get(component.entrypoint).add(component.identifier)
35+
}
36+
37+
/**
38+
* @type {import('eslint').Rule.RuleModule}
39+
*/
40+
module.exports = {
41+
meta: {
42+
type: 'problem',
43+
docs: {
44+
description: 'Use deprecated components from the `@primer/react/deprecated` entrypoint',
45+
recommended: true,
46+
url: url(module),
47+
},
48+
fixable: true,
49+
schema: [],
50+
},
51+
create(context) {
52+
const sourceCode = context.getSourceCode()
53+
54+
return {
55+
ImportDeclaration(node) {
56+
if (!entrypoints.has(node.source.value)) {
57+
return
58+
}
59+
60+
const entrypoint = entrypoints.get(node.source.value)
61+
const deprecated = node.specifiers.filter(specifier => {
62+
return entrypoint.has(specifier.imported.name)
63+
})
64+
65+
if (deprecated.length === 0) {
66+
return
67+
}
68+
69+
const deprecatedEntrypoint = node.parent.body.find(node => {
70+
if (node.type !== 'ImportDeclaration') {
71+
return false
72+
}
73+
74+
return node.source.value === '@primer/react/deprecated'
75+
})
76+
77+
// All imports are deprecated
78+
if (deprecated.length === node.specifiers.length) {
79+
context.report({
80+
node,
81+
message: 'Import deprecated components from @primer/react/deprecated',
82+
*fix(fixer) {
83+
if (deprecatedEntrypoint) {
84+
const lastSpecifier = deprecatedEntrypoint.specifiers[deprecatedEntrypoint.specifiers.length - 1]
85+
86+
yield fixer.remove(node)
87+
yield fixer.insertTextAfter(
88+
lastSpecifier,
89+
`, ${node.specifiers.map(specifier => specifier.imported.name).join(', ')}`,
90+
)
91+
} else {
92+
yield fixer.replaceText(node.source, `'@primer/react/deprecated'`)
93+
}
94+
},
95+
})
96+
} else {
97+
// There is a mix of deprecated and non-deprecated imports
98+
context.report({
99+
node,
100+
message: 'Import deprecated components from @primer/react/deprecated',
101+
*fix(fixer) {
102+
for (const specifier of deprecated) {
103+
yield fixer.remove(specifier)
104+
const comma = sourceCode.getTokenAfter(specifier)
105+
if (comma.value === ',') {
106+
yield fixer.remove(comma)
107+
}
108+
}
109+
110+
if (deprecatedEntrypoint) {
111+
const lastSpecifier = deprecatedEntrypoint.specifiers[deprecatedEntrypoint.specifiers.length - 1]
112+
yield fixer.insertTextAfter(
113+
lastSpecifier,
114+
`, ${deprecated.map(specifier => specifier.imported.name).join(', ')}`,
115+
)
116+
} else {
117+
yield fixer.insertTextAfter(
118+
node,
119+
`\nimport {${deprecated
120+
.map(specifier => specifier.imported.name)
121+
.join(', ')}} from '@primer/react/deprecated'`,
122+
)
123+
}
124+
},
125+
})
126+
}
127+
},
128+
}
129+
},
130+
}

0 commit comments

Comments
 (0)