Skip to content

Commit

Permalink
feat: add use-deprecated-from-deprecated (#204)
Browse files Browse the repository at this point in the history
* feat: add use-deprecated-from-deprecated

* chore: add changeset
  • Loading branch information
joshblack authored Aug 16, 2024
1 parent 657d04b commit e2cab87
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/dry-tips-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eslint-plugin-primer-react': minor
---

Add use-deprecated-from-deprecated rule
25 changes: 25 additions & 0 deletions docs/rules/use-deprecated-from-deprecated.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Use Deprecated from Deprecated

## Rule Details

This rule enforces the usage of deprecated imports from `@primer/react/deprecated`.

👎 Examples of **incorrect** code for this rule

```jsx
import {Dialog} from '@primer/react'

function ExampleComponent() {
return <Dialog>{/* ... */}</Dialog>
}
```

👍 Examples of **correct** code for this rule:

```jsx
import {Dialog} from '@primer/react/deprecated'

function ExampleComponent() {
return <Dialog>{/* ... */}</Dialog>
}
```
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module.exports = {
'a11y-link-in-text-block': require('./rules/a11y-link-in-text-block'),
'a11y-remove-disable-tooltip': require('./rules/a11y-remove-disable-tooltip'),
'a11y-use-next-tooltip': require('./rules/a11y-use-next-tooltip'),
'use-deprecated-from-deprecated': require('./rules/use-deprecated-from-deprecated'),
},
configs: {
recommended: require('./configs/recommended'),
Expand Down
66 changes: 66 additions & 0 deletions src/rules/__tests__/use-deprecated-from-deprecated.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use strict'

const {RuleTester} = require('eslint')
const rule = require('../use-deprecated-from-deprecated')

const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
})

ruleTester.run('use-deprecated-from-deprecated', rule, {
valid: [],
invalid: [
// Single deprecated import
{
code: `import {Tooltip} from '@primer/react'`,
output: `import {Tooltip} from '@primer/react/deprecated'`,
errors: ['Import deprecated components from @primer/react/deprecated'],
},

// Single deprecated import with existing deprecated entrypoint
{
code: `import {Tooltip} from '@primer/react'
import {Dialog} from '@primer/react/deprecated'`,
output: `\nimport {Dialog, Tooltip} from '@primer/react/deprecated'`,
errors: ['Import deprecated components from @primer/react/deprecated'],
},

// Multiple deprecated imports
{
code: `import {Dialog, Tooltip} from '@primer/react'`,
output: `import {Dialog, Tooltip} from '@primer/react/deprecated'`,
errors: ['Import deprecated components from @primer/react/deprecated'],
},

// Mixed deprecated and non-deprecated imports
{
code: `import {Button, Tooltip} from '@primer/react'`,
output: `import {Button, } from '@primer/react'
import {Tooltip} from '@primer/react/deprecated'`,
errors: ['Import deprecated components from @primer/react/deprecated'],
},

// Mixed deprecated and non-deprecated imports with existing deprecated
{
code: `import {Button, Tooltip} from '@primer/react'
import {Dialog} from '@primer/react/deprecated'`,
output: `import {Button, } from '@primer/react'
import {Dialog, Tooltip} from '@primer/react/deprecated'`,
errors: ['Import deprecated components from @primer/react/deprecated'],
},

// Multiple mixed deprecated and non-deprecated imports
{
code: `import {Button, Dialog, Tooltip} from '@primer/react'`,
output: `import {Button, } from '@primer/react'
import {Dialog, Tooltip} from '@primer/react/deprecated'`,
errors: ['Import deprecated components from @primer/react/deprecated'],
},
],
})
130 changes: 130 additions & 0 deletions src/rules/use-deprecated-from-deprecated.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use strict'

const url = require('../url')

const components = [
{
identifier: 'Dialog',
entrypoint: '@primer/react',
},
{
identifier: 'Octicon',
entrypoint: '@primer/react',
},
{
identifier: 'Pagehead',
entrypoint: '@primer/react',
},
{
identifier: 'TabNav',
entrypoint: '@primer/react',
},
{
identifier: 'Tooltip',
entrypoint: '@primer/react',
},
]

const entrypoints = new Map()

for (const component of components) {
if (!entrypoints.has(component.entrypoint)) {
entrypoints.set(component.entrypoint, new Set())
}
entrypoints.get(component.entrypoint).add(component.identifier)
}

/**
* @type {import('eslint').Rule.RuleModule}
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Use deprecated components from the `@primer/react/deprecated` entrypoint',
recommended: true,
url: url(module),
},
fixable: true,
schema: [],
},
create(context) {
const sourceCode = context.getSourceCode()

return {
ImportDeclaration(node) {
if (!entrypoints.has(node.source.value)) {
return
}

const entrypoint = entrypoints.get(node.source.value)
const deprecated = node.specifiers.filter(specifier => {
return entrypoint.has(specifier.imported.name)
})

if (deprecated.length === 0) {
return
}

const deprecatedEntrypoint = node.parent.body.find(node => {
if (node.type !== 'ImportDeclaration') {
return false
}

return node.source.value === '@primer/react/deprecated'
})

// All imports are deprecated
if (deprecated.length === node.specifiers.length) {
context.report({
node,
message: 'Import deprecated components from @primer/react/deprecated',
*fix(fixer) {
if (deprecatedEntrypoint) {
const lastSpecifier = deprecatedEntrypoint.specifiers[deprecatedEntrypoint.specifiers.length - 1]

yield fixer.remove(node)
yield fixer.insertTextAfter(
lastSpecifier,
`, ${node.specifiers.map(specifier => specifier.imported.name).join(', ')}`,
)
} else {
yield fixer.replaceText(node.source, `'@primer/react/deprecated'`)
}
},
})
} else {
// There is a mix of deprecated and non-deprecated imports
context.report({
node,
message: 'Import deprecated components from @primer/react/deprecated',
*fix(fixer) {
for (const specifier of deprecated) {
yield fixer.remove(specifier)
const comma = sourceCode.getTokenAfter(specifier)
if (comma.value === ',') {
yield fixer.remove(comma)
}
}

if (deprecatedEntrypoint) {
const lastSpecifier = deprecatedEntrypoint.specifiers[deprecatedEntrypoint.specifiers.length - 1]
yield fixer.insertTextAfter(
lastSpecifier,
`, ${deprecated.map(specifier => specifier.imported.name).join(', ')}`,
)
} else {
yield fixer.insertTextAfter(
node,
`\nimport {${deprecated
.map(specifier => specifier.imported.name)
.join(', ')}} from '@primer/react/deprecated'`,
)
}
},
})
}
},
}
},
}

0 comments on commit e2cab87

Please sign in to comment.