Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add no-deprecated-props rule #156

Merged
merged 9 commits into from
Apr 22, 2024
5 changes: 5 additions & 0 deletions .changeset/tidy-moons-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eslint-plugin-primer-react': minor
---

Add no-deprecated-props rule
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ ESLint rules for Primer React
- [a11y-tooltip-interactive-trigger](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/a11y-tooltip-interactive-trigger.md)
- [a11y-explicit-heading](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/a11y-explicit-heading.md)
- [new-css-color-vars](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/new-css-color-vars.md)
- [no-deprecated-props](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/no-deprecated-props.md)
48 changes: 48 additions & 0 deletions docs/rules/no-deprecated-props.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
## Rule Details

This rule enforces to use the recommended API (`ActionList.GroupHeading`) component over the deprecated prop (`title` prop on `ActionList.Group`) for ActionList component.

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

```jsx
/* eslint primer-react/no-deprecated-props: "error" */
import {ActionList} from '@primer/react'

const App = () => (
<ActionList>
<ActionList.Group title="Group heading">
<ActionList.Item>Item 1</ActionList.Item>
</ActionList.Group>
</ActionList>
)
```

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

```jsx
/* eslint primer-react/no-deprecated-props: "error" */
import {ActionList} from '@primer/react'

const App = () => (
<ActionList>
<ActionList.Group>
<ActionList.GroupHeading as="h2">Group heading</ActionList.GroupHeading>
<ActionList.Item>Item 1</ActionList.Item>
</ActionList.Group>
</ActionList>
)
```

```jsx
/* eslint primer-react/no-deprecated-props: "error" */
import {ActionList} from '@primer/react'

const App = () => (
<ActionList role="lisbox">
<ActionList.Group>
<ActionList.GroupHeading>Group heading</ActionList.GroupHeading>
<ActionList.Item>Item 1</ActionList.Item>
</ActionList.Group>
</ActionList>
)
```
1 change: 1 addition & 0 deletions src/configs/recommended.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module.exports = {
'primer-react/new-color-css-vars': 'error',
'primer-react/a11y-explicit-heading': 'error',
'primer-react/new-color-css-vars-have-fallback': 'error',
'primer-react/no-deprecated-props': 'warn',
},
settings: {
github: {
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module.exports = {
'new-color-css-vars': require('./rules/new-color-css-vars'),
'a11y-explicit-heading': require('./rules/a11y-explicit-heading'),
'new-color-css-vars-have-fallback': require('./rules/new-color-css-vars-have-fallback'),
'no-deprecated-props': require('./rules/no-deprecated-props'),
},
configs: {
recommended: require('./configs/recommended'),
Expand Down
121 changes: 121 additions & 0 deletions src/rules/__tests__/no-deprecated-props.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
'use strict'

const {RuleTester} = require('eslint')
const rule = require('../no-deprecated-props')

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

ruleTester.run('no-deprecated-props', rule, {
valid: [
`import {ActionList} from '@primer/react';
<ActionList>
<ActionList.Group>
<ActionList.GroupHeading as="h3">Group heading 1</ActionList.GroupHeading>
<ActionList.Item>Item</ActionList.Item>
</ActionList.Group>
<ActionList.Group>
<ActionList.GroupHeading as="h3">Group heading 2</ActionList.GroupHeading>
<ActionList.Item>Item 2</ActionList.Item>
</ActionList.Group>
</ActionList>`,
`import {ActionList} from '@primer/react';
<ActionList>
<ActionList.Group>
<ActionList.GroupHeading>Group heading 1</ActionList.GroupHeading>
<ActionList.Item>Item</ActionList.Item>
</ActionList.Group>
<ActionList.Group>
<ActionList.GroupHeading>Group heading 2</ActionList.GroupHeading>
<ActionList.Item>Item 2</ActionList.Item>
</ActionList.Group>
</ActionList>`,
`import {ActionList} from '@primer/react';
<ActionList>
<ActionList.Group>
<ActionList.GroupHeading as="h3">Group heading</ActionList.GroupHeading>
<ActionList.Item>Item</ActionList.Item>
</ActionList.Group>
<ActionList.Item>Item 2</ActionList.Item>
</ActionList>`,
`import {ActionList} from '@primer/react';
<ActionList role="listbox">
<ActionList.Group>
<ActionList.GroupHeading>Group heading</ActionList.GroupHeading>
<ActionList.Item>Item</ActionList.Item>
</ActionList.Group>
<ActionList.Item>Item 2</ActionList.Item>
</ActionList>`,
`import {ActionList} from '@primer/react';
<ActionList role="menu">
<ActionList.Item>Item</ActionList.Item>
<ActionList.Group>
<ActionList.GroupHeading>Group heading</ActionList.GroupHeading>
<ActionList.Item>Group item</ActionList.Item>
</ActionList.Group>
</ActionList>`,
],
invalid: [
{
code: `<ActionList.Group title="Group heading 1"></ActionList.Group>`,
output: `<ActionList.Group><ActionList.GroupHeading>Group heading 1</ActionList.GroupHeading></ActionList.Group>`,
errors: [
{
messageId: 'titlePropDeprecated',
},
],
},
{
code: `<ActionList.Group title="Group heading 1" sx={{padding: 2}}></ActionList.Group>`,
output: `<ActionList.Group sx={{padding: 2}}><ActionList.GroupHeading>Group heading 1</ActionList.GroupHeading></ActionList.Group>`,
errors: [
{
messageId: 'titlePropDeprecated',
},
],
},
{
code: `<ActionList.Group variant="filled" title="Group heading 1"></ActionList.Group>`,
output: `<ActionList.Group variant="filled"><ActionList.GroupHeading>Group heading 1</ActionList.GroupHeading></ActionList.Group>`,
errors: [
{
messageId: 'titlePropDeprecated',
},
],
},
{
code: `<ActionList.Group title={titleVariable}></ActionList.Group>`,
output: `<ActionList.Group><ActionList.GroupHeading>{titleVariable}</ActionList.GroupHeading></ActionList.Group>`,
errors: [
{
messageId: 'titlePropDeprecated',
},
],
},
{
code: `<ActionList.Group title={'Title'}></ActionList.Group>`,
output: `<ActionList.Group><ActionList.GroupHeading>{'Title'}</ActionList.GroupHeading></ActionList.Group>`,
errors: [
{
messageId: 'titlePropDeprecated',
},
],
},
{
code: `<ActionList.Group title={condition ? 'Title' : undefined}></ActionList.Group>`,
output: `<ActionList.Group><ActionList.GroupHeading>{condition ? 'Title' : undefined}</ActionList.GroupHeading></ActionList.Group>`,
errors: [
{
messageId: 'titlePropDeprecated',
},
],
},
],
})
62 changes: 62 additions & 0 deletions src/rules/no-deprecated-props.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use strict'
const {getJSXOpeningElementAttribute} = require('../utils/get-jsx-opening-element-attribute')
const {getJSXOpeningElementName} = require('../utils/get-jsx-opening-element-name')

/**
* @type {import('eslint').Rule.RuleModule}
*/
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'Avoid using deprecated `title` prop on `ActionList.Group` component. Use `ActionList.GroupHeading` instead.',
recommended: true,
url: 'https://primer.style/components/action-list/react/beta#actionlistgroupheading',
},
fixable: 'code',
schema: [],
messages: {
titlePropDeprecated: 'The `title` prop is deprecated. Please use `ActionList.GroupHeading` instead.',
},
},
create(context) {
return {
JSXOpeningElement(node) {
const openingElName = getJSXOpeningElementName(node)
if (openingElName !== 'ActionList.Group') {
return
}
const title = getJSXOpeningElementAttribute(node, 'title')
let groupTitle = ''
if (title !== undefined) {
context.report({
node,
messageId: 'titlePropDeprecated',
fix(fixer) {
// Group title is a string literal i.e. title="title"
if (title.value.type === 'Literal') {
groupTitle = title.value.value
// Group title is a JSX expression i.e. title={title}
} else if (title.value.type === 'JSXExpressionContainer') {
groupTitle = context.sourceCode.getText(title.value)
} else {
// we don't provide fix for cases where the title prop is not a string literal or JSX expression
return []
}
const start = title.range[0]
const end = title.range[1]
return [
fixer.removeRange([start - 1, end]), // remove the space before the title as well
fixer.insertTextAfterRange(
[node.range[1], node.range[1]],
`<ActionList.GroupHeading>${groupTitle}</ActionList.GroupHeading>`,
),
]
},
})
}
},
}
},
}