Ajout de promotion et de commande
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Yannick Croissant
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
+423
@@ -0,0 +1,423 @@
|
||||
# `eslint-plugin-react` <sup>[![Version Badge][npm-version-svg]][package-url]</sup>
|
||||
|
||||
===================
|
||||
|
||||
[![github actions][actions-image]][actions-url]
|
||||
[![Maintenance Status][status-image]][status-url]
|
||||
[![NPM version][npm-image]][npm-url]
|
||||
[![Tidelift][tidelift-image]][tidelift-url]
|
||||
|
||||
React specific linting rules for `eslint`
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
npm install eslint eslint-plugin-react --save-dev
|
||||
```
|
||||
|
||||
It is also possible to install ESLint globally rather than locally (using `npm install -g eslint`). However, this is not recommended, and any plugins or shareable configs that you use must be installed locally in either case.
|
||||
|
||||
## Configuration (legacy: `.eslintrc*`) <a id="configuration"></a>
|
||||
|
||||
Use [our preset](#recommended) to get reasonable defaults:
|
||||
|
||||
```json
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended"
|
||||
]
|
||||
```
|
||||
|
||||
If you are using the [new JSX transform from React 17](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#removing-unused-react-imports), extend [`react/jsx-runtime`](https://github.com/jsx-eslint/eslint-plugin-react/blob/c8917b0885094b5e4cc2a6f613f7fb6f16fe932e/index.js#L163-L176) in your eslint config (add `"plugin:react/jsx-runtime"` to `"extends"`) to disable the relevant rules.
|
||||
|
||||
You should also specify settings that will be shared across all the plugin rules. ([More about eslint shared settings](https://eslint.org/docs/latest/use/configure/configuration-files#configuring-shared-settings))
|
||||
|
||||
```json5
|
||||
{
|
||||
"settings": {
|
||||
"react": {
|
||||
"createClass": "createReactClass", // Regex for Component Factory to use,
|
||||
// default to "createReactClass"
|
||||
"pragma": "React", // Pragma to use, default to "React"
|
||||
"fragment": "Fragment", // Fragment to use (may be a property of <pragma>), default to "Fragment"
|
||||
"version": "detect", // React version. "detect" automatically picks the version you have installed.
|
||||
// You can also use `16.0`, `16.3`, etc, if you want to override the detected value.
|
||||
// Defaults to the "defaultVersion" setting and warns if missing, and to "detect" in the future
|
||||
"defaultVersion": "", // Default React version to use when the version you have installed cannot be detected.
|
||||
// If not provided, defaults to the latest React version.
|
||||
"flowVersion": "0.53" // Flow version
|
||||
},
|
||||
"propWrapperFunctions": [
|
||||
// The names of any function used to wrap propTypes, e.g. `forbidExtraProps`. If this isn't set, any propTypes wrapped in a function will be skipped.
|
||||
"forbidExtraProps",
|
||||
{"property": "freeze", "object": "Object"},
|
||||
{"property": "myFavoriteWrapper"},
|
||||
// for rules that check exact prop wrappers
|
||||
{"property": "forbidExtraProps", "exact": true}
|
||||
],
|
||||
"componentWrapperFunctions": [
|
||||
// The name of any function used to wrap components, e.g. Mobx `observer` function. If this isn't set, components wrapped by these functions will be skipped.
|
||||
"observer", // `property`
|
||||
{"property": "styled"}, // `object` is optional
|
||||
{"property": "observer", "object": "Mobx"},
|
||||
{"property": "observer", "object": "<pragma>"} // sets `object` to whatever value `settings.react.pragma` is set to
|
||||
],
|
||||
"formComponents": [
|
||||
// Components used as alternatives to <form> for forms, eg. <Form endpoint={ url } />
|
||||
"CustomForm",
|
||||
{"name": "SimpleForm", "formAttribute": "endpoint"},
|
||||
{"name": "Form", "formAttribute": ["registerEndpoint", "loginEndpoint"]}, // allows specifying multiple properties if necessary
|
||||
],
|
||||
"linkComponents": [
|
||||
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
|
||||
"Hyperlink",
|
||||
{"name": "MyLink", "linkAttribute": "to"},
|
||||
{"name": "Link", "linkAttribute": ["to", "href"]}, // allows specifying multiple properties if necessary
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you do not use a preset you will need to specify individual rules and add extra configuration.
|
||||
|
||||
Add "react" to the plugins section.
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": [
|
||||
"react"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Enable JSX support.
|
||||
|
||||
With `eslint` 2+
|
||||
|
||||
```json
|
||||
{
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Enable the rules that you would like to use.
|
||||
|
||||
```json
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
}
|
||||
```
|
||||
|
||||
### Shareable configs
|
||||
|
||||
#### Recommended
|
||||
|
||||
This plugin exports a `recommended` configuration that enforces React good practices.
|
||||
|
||||
To enable this configuration use the `extends` property in your `.eslintrc` config file:
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": ["eslint:recommended", "plugin:react/recommended"]
|
||||
}
|
||||
```
|
||||
|
||||
See [`eslint` documentation](https://eslint.org/docs/user-guide/configuring/configuration-files#extending-configuration-files) for more information about extending configuration files.
|
||||
|
||||
#### All
|
||||
|
||||
This plugin also exports an `all` configuration that includes every available rule.
|
||||
This pairs well with the `eslint:all` rule.
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"extends": ["eslint:all", "plugin:react/all"]
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: These configurations will import `eslint-plugin-react` and enable JSX in [parser options](https://eslint.org/docs/user-guide/configuring/language-options#specifying-parser-options).
|
||||
|
||||
## Configuration (new: `eslint.config.js`)
|
||||
|
||||
From [`v8.21.0`](https://github.com/eslint/eslint/releases/tag/v8.21.0), eslint announced a new config system.
|
||||
In the new system, `.eslintrc*` is no longer used. `eslint.config.js` would be the default config file name.
|
||||
In eslint `v8`, the legacy system (`.eslintrc*`) would still be supported, while in eslint `v9`, only the new system would be supported.
|
||||
|
||||
And from [`v8.23.0`](https://github.com/eslint/eslint/releases/tag/v8.23.0), eslint CLI starts to look up `eslint.config.js`.
|
||||
**So, if your eslint is `>=8.23.0`, you're 100% ready to use the new config system.**
|
||||
|
||||
You might want to check out the official blog posts,
|
||||
|
||||
- <https://eslint.org/blog/2022/08/new-config-system-part-1/>
|
||||
- <https://eslint.org/blog/2022/08/new-config-system-part-2/>
|
||||
- <https://eslint.org/blog/2022/08/new-config-system-part-3/>
|
||||
|
||||
and the [official docs](https://eslint.org/docs/latest/user-guide/configuring/configuration-files-new).
|
||||
|
||||
### Plugin
|
||||
|
||||
The default export of `eslint-plugin-react` is a plugin object.
|
||||
|
||||
```js
|
||||
const react = require('eslint-plugin-react');
|
||||
const globals = require('globals');
|
||||
|
||||
module.exports = [
|
||||
…
|
||||
{
|
||||
files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'],
|
||||
plugins: {
|
||||
react,
|
||||
},
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// ... any rules you want
|
||||
'react/jsx-uses-react': 'error',
|
||||
'react/jsx-uses-vars': 'error',
|
||||
},
|
||||
// ... others are omitted for brevity
|
||||
},
|
||||
…
|
||||
];
|
||||
```
|
||||
|
||||
### Configuring shared settings
|
||||
|
||||
Refer to the [official docs](https://eslint.org/docs/latest/user-guide/configuring/configuration-files-new#configuring-shared-settings).
|
||||
|
||||
The schema of the `settings.react` object would be identical to that of what's already described above in the legacy config section.
|
||||
|
||||
<!-- markdownlint-disable-next-line no-duplicate-heading -->
|
||||
### Flat Configs
|
||||
|
||||
This plugin exports 3 flat configs:
|
||||
|
||||
- `flat.all`
|
||||
- `flat.recommended`
|
||||
- `flat['jsx-runtime']`
|
||||
|
||||
The flat configs are available via the root plugin import. They will configure the plugin under the `react/` namespace and enable JSX in [`languageOptions.parserOptions`](https://eslint.org/docs/latest/use/configure/language-options#specifying-parser-options).
|
||||
|
||||
```js
|
||||
const reactPlugin = require('eslint-plugin-react');
|
||||
|
||||
module.exports = [
|
||||
…
|
||||
reactPlugin.configs.flat.recommended, // This is not a plugin object, but a shareable config object
|
||||
reactPlugin.configs.flat['jsx-runtime'], // Add this if you are using React 17+
|
||||
…
|
||||
];
|
||||
```
|
||||
|
||||
You can of course add/override some properties.
|
||||
|
||||
**Note**: Our shareable configs does not preconfigure `files` or [`languageOptions.globals`](https://eslint.org/docs/latest/user-guide/configuring/configuration-files-new#configuration-objects).
|
||||
For most of the cases, you probably want to configure some properties by yourself.
|
||||
|
||||
```js
|
||||
const reactPlugin = require('eslint-plugin-react');
|
||||
const globals = require('globals');
|
||||
|
||||
module.exports = [
|
||||
…
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
|
||||
...reactPlugin.configs.flat.recommended,
|
||||
languageOptions: {
|
||||
...reactPlugin.configs.flat.recommended.languageOptions,
|
||||
globals: {
|
||||
...globals.serviceworker,
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
},
|
||||
…
|
||||
];
|
||||
```
|
||||
|
||||
The above example is same as the example below, as the new config system is based on chaining.
|
||||
|
||||
```js
|
||||
const reactPlugin = require('eslint-plugin-react');
|
||||
const globals = require('globals');
|
||||
|
||||
module.exports = [
|
||||
…
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
|
||||
...reactPlugin.configs.flat.recommended,
|
||||
},
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.serviceworker,
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
},
|
||||
…
|
||||
];
|
||||
```
|
||||
|
||||
## List of supported rules
|
||||
|
||||
<!-- begin auto-generated rules list -->
|
||||
|
||||
💼 [Configurations](https://github.com/jsx-eslint/eslint-plugin-react/#shareable-configs) enabled in.\
|
||||
🚫 [Configurations](https://github.com/jsx-eslint/eslint-plugin-react/#shareable-configs) disabled in.\
|
||||
🏃 Set in the `jsx-runtime` [configuration](https://github.com/jsx-eslint/eslint-plugin-react/#shareable-configs).\
|
||||
☑️ Set in the `recommended` [configuration](https://github.com/jsx-eslint/eslint-plugin-react/#shareable-configs).\
|
||||
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
|
||||
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).\
|
||||
❌ Deprecated.
|
||||
|
||||
| Name | Description | 💼 | 🚫 | 🔧 | 💡 | ❌ |
|
||||
| :------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------- | :- | :- | :- | :- | :- |
|
||||
| [boolean-prop-naming](docs/rules/boolean-prop-naming.md) | Enforces consistent naming for boolean props | | | | | |
|
||||
| [button-has-type](docs/rules/button-has-type.md) | Disallow usage of `button` elements without an explicit `type` attribute | | | | | |
|
||||
| [checked-requires-onchange-or-readonly](docs/rules/checked-requires-onchange-or-readonly.md) | Enforce using `onChange` or `readonly` attribute when `checked` is used | | | | | |
|
||||
| [default-props-match-prop-types](docs/rules/default-props-match-prop-types.md) | Enforce all defaultProps have a corresponding non-required PropType | | | | | |
|
||||
| [destructuring-assignment](docs/rules/destructuring-assignment.md) | Enforce consistent usage of destructuring assignment of props, state, and context | | | 🔧 | | |
|
||||
| [display-name](docs/rules/display-name.md) | Disallow missing displayName in a React component definition | ☑️ | | | | |
|
||||
| [forbid-component-props](docs/rules/forbid-component-props.md) | Disallow certain props on components | | | | | |
|
||||
| [forbid-dom-props](docs/rules/forbid-dom-props.md) | Disallow certain props on DOM Nodes | | | | | |
|
||||
| [forbid-elements](docs/rules/forbid-elements.md) | Disallow certain elements | | | | | |
|
||||
| [forbid-foreign-prop-types](docs/rules/forbid-foreign-prop-types.md) | Disallow using another component's propTypes | | | | | |
|
||||
| [forbid-prop-types](docs/rules/forbid-prop-types.md) | Disallow certain propTypes | | | | | |
|
||||
| [forward-ref-uses-ref](docs/rules/forward-ref-uses-ref.md) | Require all forwardRef components include a ref parameter | | | | 💡 | |
|
||||
| [function-component-definition](docs/rules/function-component-definition.md) | Enforce a specific function type for function components | | | 🔧 | | |
|
||||
| [hook-use-state](docs/rules/hook-use-state.md) | Ensure destructuring and symmetric naming of useState hook value and setter variables | | | | 💡 | |
|
||||
| [iframe-missing-sandbox](docs/rules/iframe-missing-sandbox.md) | Enforce sandbox attribute on iframe elements | | | | | |
|
||||
| [jsx-boolean-value](docs/rules/jsx-boolean-value.md) | Enforce boolean attributes notation in JSX | | | 🔧 | | |
|
||||
| [jsx-child-element-spacing](docs/rules/jsx-child-element-spacing.md) | Enforce or disallow spaces inside of curly braces in JSX attributes and expressions | | | | | |
|
||||
| [jsx-closing-bracket-location](docs/rules/jsx-closing-bracket-location.md) | Enforce closing bracket location in JSX | | | 🔧 | | |
|
||||
| [jsx-closing-tag-location](docs/rules/jsx-closing-tag-location.md) | Enforce closing tag location for multiline JSX | | | 🔧 | | |
|
||||
| [jsx-curly-brace-presence](docs/rules/jsx-curly-brace-presence.md) | Disallow unnecessary JSX expressions when literals alone are sufficient or enforce JSX expressions on literals in JSX children or attributes | | | 🔧 | | |
|
||||
| [jsx-curly-newline](docs/rules/jsx-curly-newline.md) | Enforce consistent linebreaks in curly braces in JSX attributes and expressions | | | 🔧 | | |
|
||||
| [jsx-curly-spacing](docs/rules/jsx-curly-spacing.md) | Enforce or disallow spaces inside of curly braces in JSX attributes and expressions | | | 🔧 | | |
|
||||
| [jsx-equals-spacing](docs/rules/jsx-equals-spacing.md) | Enforce or disallow spaces around equal signs in JSX attributes | | | 🔧 | | |
|
||||
| [jsx-filename-extension](docs/rules/jsx-filename-extension.md) | Disallow file extensions that may contain JSX | | | | | |
|
||||
| [jsx-first-prop-new-line](docs/rules/jsx-first-prop-new-line.md) | Enforce proper position of the first property in JSX | | | 🔧 | | |
|
||||
| [jsx-fragments](docs/rules/jsx-fragments.md) | Enforce shorthand or standard form for React fragments | | | 🔧 | | |
|
||||
| [jsx-handler-names](docs/rules/jsx-handler-names.md) | Enforce event handler naming conventions in JSX | | | | | |
|
||||
| [jsx-indent](docs/rules/jsx-indent.md) | Enforce JSX indentation | | | 🔧 | | |
|
||||
| [jsx-indent-props](docs/rules/jsx-indent-props.md) | Enforce props indentation in JSX | | | 🔧 | | |
|
||||
| [jsx-key](docs/rules/jsx-key.md) | Disallow missing `key` props in iterators/collection literals | ☑️ | | | | |
|
||||
| [jsx-max-depth](docs/rules/jsx-max-depth.md) | Enforce JSX maximum depth | | | | | |
|
||||
| [jsx-max-props-per-line](docs/rules/jsx-max-props-per-line.md) | Enforce maximum of props on a single line in JSX | | | 🔧 | | |
|
||||
| [jsx-newline](docs/rules/jsx-newline.md) | Require or prevent a new line after jsx elements and expressions. | | | 🔧 | | |
|
||||
| [jsx-no-bind](docs/rules/jsx-no-bind.md) | Disallow `.bind()` or arrow functions in JSX props | | | | | |
|
||||
| [jsx-no-comment-textnodes](docs/rules/jsx-no-comment-textnodes.md) | Disallow comments from being inserted as text nodes | ☑️ | | | | |
|
||||
| [jsx-no-constructed-context-values](docs/rules/jsx-no-constructed-context-values.md) | Disallows JSX context provider values from taking values that will cause needless rerenders | | | | | |
|
||||
| [jsx-no-duplicate-props](docs/rules/jsx-no-duplicate-props.md) | Disallow duplicate properties in JSX | ☑️ | | | | |
|
||||
| [jsx-no-leaked-render](docs/rules/jsx-no-leaked-render.md) | Disallow problematic leaked values from being rendered | | | 🔧 | | |
|
||||
| [jsx-no-literals](docs/rules/jsx-no-literals.md) | Disallow usage of string literals in JSX | | | | | |
|
||||
| [jsx-no-script-url](docs/rules/jsx-no-script-url.md) | Disallow usage of `javascript:` URLs | | | | | |
|
||||
| [jsx-no-target-blank](docs/rules/jsx-no-target-blank.md) | Disallow `target="_blank"` attribute without `rel="noreferrer"` | ☑️ | | 🔧 | | |
|
||||
| [jsx-no-undef](docs/rules/jsx-no-undef.md) | Disallow undeclared variables in JSX | ☑️ | | | | |
|
||||
| [jsx-no-useless-fragment](docs/rules/jsx-no-useless-fragment.md) | Disallow unnecessary fragments | | | 🔧 | | |
|
||||
| [jsx-one-expression-per-line](docs/rules/jsx-one-expression-per-line.md) | Require one JSX element per line | | | 🔧 | | |
|
||||
| [jsx-pascal-case](docs/rules/jsx-pascal-case.md) | Enforce PascalCase for user-defined JSX components | | | | | |
|
||||
| [jsx-props-no-multi-spaces](docs/rules/jsx-props-no-multi-spaces.md) | Disallow multiple spaces between inline JSX props | | | 🔧 | | |
|
||||
| [jsx-props-no-spread-multi](docs/rules/jsx-props-no-spread-multi.md) | Disallow JSX prop spreading the same identifier multiple times | | | | | |
|
||||
| [jsx-props-no-spreading](docs/rules/jsx-props-no-spreading.md) | Disallow JSX prop spreading | | | | | |
|
||||
| [jsx-sort-default-props](docs/rules/jsx-sort-default-props.md) | Enforce defaultProps declarations alphabetical sorting | | | | | ❌ |
|
||||
| [jsx-sort-props](docs/rules/jsx-sort-props.md) | Enforce props alphabetical sorting | | | 🔧 | | |
|
||||
| [jsx-space-before-closing](docs/rules/jsx-space-before-closing.md) | Enforce spacing before closing bracket in JSX | | | 🔧 | | ❌ |
|
||||
| [jsx-tag-spacing](docs/rules/jsx-tag-spacing.md) | Enforce whitespace in and around the JSX opening and closing brackets | | | 🔧 | | |
|
||||
| [jsx-uses-react](docs/rules/jsx-uses-react.md) | Disallow React to be incorrectly marked as unused | ☑️ | 🏃 | | | |
|
||||
| [jsx-uses-vars](docs/rules/jsx-uses-vars.md) | Disallow variables used in JSX to be incorrectly marked as unused | ☑️ | | | | |
|
||||
| [jsx-wrap-multilines](docs/rules/jsx-wrap-multilines.md) | Disallow missing parentheses around multiline JSX | | | 🔧 | | |
|
||||
| [no-access-state-in-setstate](docs/rules/no-access-state-in-setstate.md) | Disallow when this.state is accessed within setState | | | | | |
|
||||
| [no-adjacent-inline-elements](docs/rules/no-adjacent-inline-elements.md) | Disallow adjacent inline elements not separated by whitespace. | | | | | |
|
||||
| [no-array-index-key](docs/rules/no-array-index-key.md) | Disallow usage of Array index in keys | | | | | |
|
||||
| [no-arrow-function-lifecycle](docs/rules/no-arrow-function-lifecycle.md) | Lifecycle methods should be methods on the prototype, not class fields | | | 🔧 | | |
|
||||
| [no-children-prop](docs/rules/no-children-prop.md) | Disallow passing of children as props | ☑️ | | | | |
|
||||
| [no-danger](docs/rules/no-danger.md) | Disallow usage of dangerous JSX properties | | | | | |
|
||||
| [no-danger-with-children](docs/rules/no-danger-with-children.md) | Disallow when a DOM element is using both children and dangerouslySetInnerHTML | ☑️ | | | | |
|
||||
| [no-deprecated](docs/rules/no-deprecated.md) | Disallow usage of deprecated methods | ☑️ | | | | |
|
||||
| [no-did-mount-set-state](docs/rules/no-did-mount-set-state.md) | Disallow usage of setState in componentDidMount | | | | | |
|
||||
| [no-did-update-set-state](docs/rules/no-did-update-set-state.md) | Disallow usage of setState in componentDidUpdate | | | | | |
|
||||
| [no-direct-mutation-state](docs/rules/no-direct-mutation-state.md) | Disallow direct mutation of this.state | ☑️ | | | | |
|
||||
| [no-find-dom-node](docs/rules/no-find-dom-node.md) | Disallow usage of findDOMNode | ☑️ | | | | |
|
||||
| [no-invalid-html-attribute](docs/rules/no-invalid-html-attribute.md) | Disallow usage of invalid attributes | | | | 💡 | |
|
||||
| [no-is-mounted](docs/rules/no-is-mounted.md) | Disallow usage of isMounted | ☑️ | | | | |
|
||||
| [no-multi-comp](docs/rules/no-multi-comp.md) | Disallow multiple component definition per file | | | | | |
|
||||
| [no-namespace](docs/rules/no-namespace.md) | Enforce that namespaces are not used in React elements | | | | | |
|
||||
| [no-object-type-as-default-prop](docs/rules/no-object-type-as-default-prop.md) | Disallow usage of referential-type variables as default param in functional component | | | | | |
|
||||
| [no-redundant-should-component-update](docs/rules/no-redundant-should-component-update.md) | Disallow usage of shouldComponentUpdate when extending React.PureComponent | | | | | |
|
||||
| [no-render-return-value](docs/rules/no-render-return-value.md) | Disallow usage of the return value of ReactDOM.render | ☑️ | | | | |
|
||||
| [no-set-state](docs/rules/no-set-state.md) | Disallow usage of setState | | | | | |
|
||||
| [no-string-refs](docs/rules/no-string-refs.md) | Disallow using string references | ☑️ | | | | |
|
||||
| [no-this-in-sfc](docs/rules/no-this-in-sfc.md) | Disallow `this` from being used in stateless functional components | | | | | |
|
||||
| [no-typos](docs/rules/no-typos.md) | Disallow common typos | | | | | |
|
||||
| [no-unescaped-entities](docs/rules/no-unescaped-entities.md) | Disallow unescaped HTML entities from appearing in markup | ☑️ | | | 💡 | |
|
||||
| [no-unknown-property](docs/rules/no-unknown-property.md) | Disallow usage of unknown DOM property | ☑️ | | 🔧 | | |
|
||||
| [no-unsafe](docs/rules/no-unsafe.md) | Disallow usage of unsafe lifecycle methods | | ☑️ | | | |
|
||||
| [no-unstable-nested-components](docs/rules/no-unstable-nested-components.md) | Disallow creating unstable components inside components | | | | | |
|
||||
| [no-unused-class-component-methods](docs/rules/no-unused-class-component-methods.md) | Disallow declaring unused methods of component class | | | | | |
|
||||
| [no-unused-prop-types](docs/rules/no-unused-prop-types.md) | Disallow definitions of unused propTypes | | | | | |
|
||||
| [no-unused-state](docs/rules/no-unused-state.md) | Disallow definitions of unused state | | | | | |
|
||||
| [no-will-update-set-state](docs/rules/no-will-update-set-state.md) | Disallow usage of setState in componentWillUpdate | | | | | |
|
||||
| [prefer-es6-class](docs/rules/prefer-es6-class.md) | Enforce ES5 or ES6 class for React Components | | | | | |
|
||||
| [prefer-exact-props](docs/rules/prefer-exact-props.md) | Prefer exact proptype definitions | | | | | |
|
||||
| [prefer-read-only-props](docs/rules/prefer-read-only-props.md) | Enforce that props are read-only | | | 🔧 | | |
|
||||
| [prefer-stateless-function](docs/rules/prefer-stateless-function.md) | Enforce stateless components to be written as a pure function | | | | | |
|
||||
| [prop-types](docs/rules/prop-types.md) | Disallow missing props validation in a React component definition | ☑️ | | | | |
|
||||
| [react-in-jsx-scope](docs/rules/react-in-jsx-scope.md) | Disallow missing React when using JSX | ☑️ | 🏃 | | | |
|
||||
| [require-default-props](docs/rules/require-default-props.md) | Enforce a defaultProps definition for every prop that is not a required prop | | | | | |
|
||||
| [require-optimization](docs/rules/require-optimization.md) | Enforce React components to have a shouldComponentUpdate method | | | | | |
|
||||
| [require-render-return](docs/rules/require-render-return.md) | Enforce ES5 or ES6 class for returning value in render function | ☑️ | | | | |
|
||||
| [self-closing-comp](docs/rules/self-closing-comp.md) | Disallow extra closing tags for components without children | | | 🔧 | | |
|
||||
| [sort-comp](docs/rules/sort-comp.md) | Enforce component methods order | | | | | |
|
||||
| [sort-default-props](docs/rules/sort-default-props.md) | Enforce defaultProps declarations alphabetical sorting | | | | | |
|
||||
| [sort-prop-types](docs/rules/sort-prop-types.md) | Enforce propTypes declarations alphabetical sorting | | | 🔧 | | |
|
||||
| [state-in-constructor](docs/rules/state-in-constructor.md) | Enforce class component state initialization style | | | | | |
|
||||
| [static-property-placement](docs/rules/static-property-placement.md) | Enforces where React component static properties should be positioned. | | | | | |
|
||||
| [style-prop-object](docs/rules/style-prop-object.md) | Enforce style prop value is an object | | | | | |
|
||||
| [void-dom-elements-no-children](docs/rules/void-dom-elements-no-children.md) | Disallow void DOM elements (e.g. `<img />`, `<br />`) from receiving children | | | | | |
|
||||
|
||||
<!-- end auto-generated rules list -->
|
||||
|
||||
## Other useful plugins
|
||||
|
||||
- Rules of Hooks: [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/master/packages/eslint-plugin-react-hooks)
|
||||
- JSX accessibility: [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y)
|
||||
- React Native: [eslint-plugin-react-native](https://github.com/Intellicode/eslint-plugin-react-native)
|
||||
|
||||
## License
|
||||
|
||||
`eslint-plugin-react` is licensed under the [MIT License](https://opensource.org/licenses/mit-license.php).
|
||||
|
||||
[npm-url]: https://npmjs.org/package/eslint-plugin-react
|
||||
[npm-image]: https://img.shields.io/npm/v/eslint-plugin-react.svg
|
||||
|
||||
[status-url]: https://github.com/jsx-eslint/eslint-plugin-react/pulse
|
||||
[status-image]: https://img.shields.io/github/last-commit/jsx-eslint/eslint-plugin-react.svg
|
||||
|
||||
[tidelift-url]: https://tidelift.com/subscription/pkg/npm-eslint-plugin-react?utm_source=npm-eslint-plugin-react&utm_medium=referral&utm_campaign=readme
|
||||
[tidelift-image]: https://tidelift.com/badges/package/npm/eslint-plugin-react?style=flat
|
||||
|
||||
[package-url]: https://npmjs.org/package/eslint-plugin-react
|
||||
[npm-version-svg]: https://versionbadg.es/jsx-eslint/eslint-plugin-react.svg
|
||||
|
||||
[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/jsx-eslint/eslint-plugin-react
|
||||
[actions-url]: https://github.com/jsx-eslint/eslint-plugin-react/actions
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const plugin = require('..');
|
||||
|
||||
const legacyConfig = plugin.configs.all;
|
||||
|
||||
module.exports = {
|
||||
plugins: { react: plugin },
|
||||
rules: legacyConfig.rules,
|
||||
languageOptions: { parserOptions: legacyConfig.parserOptions },
|
||||
};
|
||||
|
||||
Object.defineProperty(module.exports, 'languageOptions', { enumerable: false });
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const plugin = require('..');
|
||||
|
||||
const legacyConfig = plugin.configs['jsx-runtime'];
|
||||
|
||||
module.exports = {
|
||||
plugins: { react: plugin },
|
||||
rules: legacyConfig.rules,
|
||||
languageOptions: { parserOptions: legacyConfig.parserOptions },
|
||||
};
|
||||
|
||||
Object.defineProperty(module.exports, 'languageOptions', { enumerable: false });
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const plugin = require('..');
|
||||
|
||||
const legacyConfig = plugin.configs.recommended;
|
||||
|
||||
module.exports = {
|
||||
plugins: { react: plugin },
|
||||
rules: legacyConfig.rules,
|
||||
languageOptions: { parserOptions: legacyConfig.parserOptions },
|
||||
};
|
||||
|
||||
Object.defineProperty(module.exports, 'languageOptions', { enumerable: false });
|
||||
+187
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":";AAiGA,8LAA8L;AAE9L,sJAAsJ;AACtJ,sBADW;IAAE,eAAe,EAAE,OAAO,eAAe,CAAC;IAAC,KAAK,EAAE,OAAO,QAAQ,CAAC;IAAC,OAAO,EAAE,OAAO,OAAO,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;KAAE,CAAA;CAAC,CAKhJ;;;;AAhFF,uCAAuC;AACvC,+BADW,OAAO,CAAC,OAAO,QAAQ,CAAC,CAC2C;AApB9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAAwC;AAgCxC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAuDmB,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC;EAGhD;;aAEuB;QAAE,KAAK,EAAE,OAAO,MAAM,CAAA;KAAE;WAAS,OAAO,QAAQ,EAAE,MAAM,CAAC,WAAW;qBAAmB;QAAE,aAAa,EAAE,OAAO,QAAQ,EAAE,MAAM,CAAC,aAAa,CAAA;KAAE"}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
'use strict';
|
||||
|
||||
const fromEntries = require('object.fromentries');
|
||||
const entries = require('object.entries');
|
||||
|
||||
const allRules = require('./lib/rules');
|
||||
|
||||
function filterRules(rules, predicate) {
|
||||
return fromEntries(entries(rules).filter((entry) => predicate(entry[1])));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} rules - rules object mapping rule name to rule module
|
||||
* @returns {Record<string, SEVERITY_ERROR | 'error'>}
|
||||
*/
|
||||
function configureAsError(rules) {
|
||||
return fromEntries(Object.keys(rules).map((key) => [`react/${key}`, 2]));
|
||||
}
|
||||
|
||||
/** @type {Partial<typeof allRules>} */
|
||||
const activeRules = filterRules(allRules, (rule) => !rule.meta.deprecated);
|
||||
/** @type {Record<keyof typeof activeRules, 2 | 'error'>} */
|
||||
const activeRulesConfig = configureAsError(activeRules);
|
||||
|
||||
/** @type {Partial<typeof allRules>} */
|
||||
const deprecatedRules = filterRules(allRules, (rule) => rule.meta.deprecated);
|
||||
|
||||
/** @type {['react']} */
|
||||
// for legacy config system
|
||||
const plugins = [
|
||||
'react',
|
||||
];
|
||||
|
||||
// TODO: with TS 4.5+, inline this
|
||||
const SEVERITY_ERROR = /** @type {2} */ (2);
|
||||
const SEVERITY_OFF = /** @type {0} */ (0);
|
||||
|
||||
const configs = {
|
||||
recommended: {
|
||||
plugins,
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'react/display-name': SEVERITY_ERROR,
|
||||
'react/jsx-key': SEVERITY_ERROR,
|
||||
'react/jsx-no-comment-textnodes': SEVERITY_ERROR,
|
||||
'react/jsx-no-duplicate-props': SEVERITY_ERROR,
|
||||
'react/jsx-no-target-blank': SEVERITY_ERROR,
|
||||
'react/jsx-no-undef': SEVERITY_ERROR,
|
||||
'react/jsx-uses-react': SEVERITY_ERROR,
|
||||
'react/jsx-uses-vars': SEVERITY_ERROR,
|
||||
'react/no-children-prop': SEVERITY_ERROR,
|
||||
'react/no-danger-with-children': SEVERITY_ERROR,
|
||||
'react/no-deprecated': SEVERITY_ERROR,
|
||||
'react/no-direct-mutation-state': SEVERITY_ERROR,
|
||||
'react/no-find-dom-node': SEVERITY_ERROR,
|
||||
'react/no-is-mounted': SEVERITY_ERROR,
|
||||
'react/no-render-return-value': SEVERITY_ERROR,
|
||||
'react/no-string-refs': SEVERITY_ERROR,
|
||||
'react/no-unescaped-entities': SEVERITY_ERROR,
|
||||
'react/no-unknown-property': SEVERITY_ERROR,
|
||||
'react/no-unsafe': SEVERITY_OFF,
|
||||
'react/prop-types': SEVERITY_ERROR,
|
||||
'react/react-in-jsx-scope': SEVERITY_ERROR,
|
||||
'react/require-render-return': SEVERITY_ERROR,
|
||||
},
|
||||
},
|
||||
all: {
|
||||
plugins,
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
rules: activeRulesConfig,
|
||||
},
|
||||
'jsx-runtime': {
|
||||
plugins,
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
jsxPragma: null, // for @typescript/eslint-parser
|
||||
},
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': SEVERITY_OFF,
|
||||
'react/jsx-uses-react': SEVERITY_OFF,
|
||||
},
|
||||
},
|
||||
flat: /** @type {Record<string, ReactFlatConfig>} */ ({
|
||||
__proto__: null,
|
||||
}),
|
||||
};
|
||||
|
||||
/** @typedef {{ plugins: { react: typeof plugin }, rules: import('eslint').Linter.RulesRecord, languageOptions: { parserOptions: import('eslint').Linter.ParserOptions } }} ReactFlatConfig */
|
||||
|
||||
/** @type {{ deprecatedRules: typeof deprecatedRules, rules: typeof allRules, configs: typeof configs & { flat: Record<string, ReactFlatConfig> }}} */
|
||||
const plugin = {
|
||||
deprecatedRules,
|
||||
rules: allRules,
|
||||
configs,
|
||||
};
|
||||
|
||||
Object.assign(configs.flat, {
|
||||
recommended: {
|
||||
plugins: { react: plugin },
|
||||
rules: configs.recommended.rules,
|
||||
languageOptions: { parserOptions: configs.recommended.parserOptions },
|
||||
},
|
||||
all: {
|
||||
plugins: { react: plugin },
|
||||
rules: configs.all.rules,
|
||||
languageOptions: { parserOptions: configs.all.parserOptions },
|
||||
},
|
||||
'jsx-runtime': {
|
||||
plugins: { react: plugin },
|
||||
rules: configs['jsx-runtime'].rules,
|
||||
languageOptions: { parserOptions: configs['jsx-runtime'].parserOptions },
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = plugin;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare const _exports: import('eslint').Rule.RuleModule;
|
||||
export = _exports;
|
||||
//# sourceMappingURL=boolean-prop-naming.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"boolean-prop-naming.d.ts","sourceRoot":"","sources":["boolean-prop-naming.js"],"names":[],"mappings":"wBAyCW,OAAO,QAAQ,EAAE,IAAI,CAAC,UAAU"}
|
||||
+426
@@ -0,0 +1,426 @@
|
||||
/**
|
||||
* @fileoverview Enforces consistent naming for boolean props
|
||||
* @author Ev Haus
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const flatMap = require('array.prototype.flatmap');
|
||||
const values = require('object.values');
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const propsUtil = require('../util/props');
|
||||
const astUtil = require('../util/ast');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const propWrapperUtil = require('../util/propWrapper');
|
||||
const report = require('../util/report');
|
||||
const eslintUtil = require('../util/eslint');
|
||||
|
||||
const getSourceCode = eslintUtil.getSourceCode;
|
||||
const getText = eslintUtil.getText;
|
||||
|
||||
/**
|
||||
* Checks if prop is nested
|
||||
* @param {Object} prop Property object, single prop type declaration
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function nestedPropTypes(prop) {
|
||||
return (
|
||||
prop.type === 'Property'
|
||||
&& astUtil.isCallExpression(prop.value)
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
patternMismatch: 'Prop name `{{propName}}` doesn’t match rule `{{pattern}}`',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
category: 'Stylistic Issues',
|
||||
description: 'Enforces consistent naming for boolean props',
|
||||
recommended: false,
|
||||
url: docsUrl('boolean-prop-naming'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
propTypeNames: {
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
minItems: 1,
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
},
|
||||
rule: {
|
||||
default: '^(is|has)[A-Z]([A-Za-z0-9]?)+',
|
||||
minLength: 1,
|
||||
type: 'string',
|
||||
},
|
||||
message: {
|
||||
minLength: 1,
|
||||
type: 'string',
|
||||
},
|
||||
validateNested: {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
type: 'object',
|
||||
}],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components, utils) => {
|
||||
const config = context.options[0] || {};
|
||||
const rule = config.rule ? new RegExp(config.rule) : null;
|
||||
const propTypeNames = config.propTypeNames || ['bool'];
|
||||
|
||||
// Remembers all Flowtype object definitions
|
||||
const objectTypeAnnotations = new Map();
|
||||
|
||||
/**
|
||||
* Returns the prop key to ensure we handle the following cases:
|
||||
* propTypes: {
|
||||
* full: React.PropTypes.bool,
|
||||
* short: PropTypes.bool,
|
||||
* direct: bool,
|
||||
* required: PropTypes.bool.isRequired
|
||||
* }
|
||||
* @param {Object} node The node we're getting the name of
|
||||
* @returns {string | null}
|
||||
*/
|
||||
function getPropKey(node) {
|
||||
// Check for `ExperimentalSpreadProperty` (eslint 3/4) and `SpreadElement` (eslint 5)
|
||||
// so we can skip validation of those fields.
|
||||
// Otherwise it will look for `node.value.property` which doesn't exist and breaks eslint.
|
||||
if (node.type === 'ExperimentalSpreadProperty' || node.type === 'SpreadElement') {
|
||||
return null;
|
||||
}
|
||||
if (node.value && node.value.property) {
|
||||
const name = node.value.property.name;
|
||||
if (name === 'isRequired') {
|
||||
if (node.value.object && node.value.object.property) {
|
||||
return node.value.object.property.name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
if (node.value && node.value.type === 'Identifier') {
|
||||
return node.value.name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the given node (prop)
|
||||
* @param {Object} node The node we're getting the name of
|
||||
* @returns {string}
|
||||
*/
|
||||
function getPropName(node) {
|
||||
// Due to this bug https://github.com/babel/babel-eslint/issues/307
|
||||
// we can't get the name of the Flow object key name. So we have
|
||||
// to hack around it for now.
|
||||
if (node.type === 'ObjectTypeProperty') {
|
||||
return getSourceCode(context).getFirstToken(node).value;
|
||||
}
|
||||
|
||||
return node.key.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if prop is declared in flow way
|
||||
* @param {Object} prop Property object, single prop type declaration
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function flowCheck(prop) {
|
||||
return (
|
||||
prop.type === 'ObjectTypeProperty'
|
||||
&& prop.value.type === 'BooleanTypeAnnotation'
|
||||
&& rule.test(getPropName(prop)) === false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if prop is declared in regular way
|
||||
* @param {Object} prop Property object, single prop type declaration
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function regularCheck(prop) {
|
||||
const propKey = getPropKey(prop);
|
||||
return (
|
||||
propKey
|
||||
&& propTypeNames.indexOf(propKey) >= 0
|
||||
&& rule.test(getPropName(prop)) === false
|
||||
);
|
||||
}
|
||||
|
||||
function tsCheck(prop) {
|
||||
if (prop.type !== 'TSPropertySignature') return false;
|
||||
const typeAnnotation = (prop.typeAnnotation || {}).typeAnnotation;
|
||||
return (
|
||||
typeAnnotation
|
||||
&& typeAnnotation.type === 'TSBooleanKeyword'
|
||||
&& rule.test(getPropName(prop)) === false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs recursive check on all proptypes
|
||||
* @param {Array} proptypes A list of Property object (for each proptype defined)
|
||||
* @param {Function} addInvalidProp callback to run for each error
|
||||
*/
|
||||
function runCheck(proptypes, addInvalidProp) {
|
||||
if (proptypes) {
|
||||
proptypes.forEach((prop) => {
|
||||
if (config.validateNested && nestedPropTypes(prop)) {
|
||||
runCheck(prop.value.arguments[0].properties, addInvalidProp);
|
||||
return;
|
||||
}
|
||||
if (flowCheck(prop) || regularCheck(prop) || tsCheck(prop)) {
|
||||
addInvalidProp(prop);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks and mark props with invalid naming
|
||||
* @param {Object} node The component node we're testing
|
||||
* @param {Array} proptypes A list of Property object (for each proptype defined)
|
||||
*/
|
||||
function validatePropNaming(node, proptypes) {
|
||||
const component = components.get(node) || node;
|
||||
const invalidProps = component.invalidProps || [];
|
||||
|
||||
runCheck(proptypes, (prop) => {
|
||||
invalidProps.push(prop);
|
||||
});
|
||||
|
||||
components.set(node, {
|
||||
invalidProps,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports invalid prop naming
|
||||
* @param {Object} component The component to process
|
||||
*/
|
||||
function reportInvalidNaming(component) {
|
||||
component.invalidProps.forEach((propNode) => {
|
||||
const propName = getPropName(propNode);
|
||||
report(context, config.message || messages.patternMismatch, !config.message && 'patternMismatch', {
|
||||
node: propNode,
|
||||
data: {
|
||||
component: propName,
|
||||
propName,
|
||||
pattern: config.rule,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkPropWrapperArguments(node, args) {
|
||||
if (!node || !Array.isArray(args)) {
|
||||
return;
|
||||
}
|
||||
args.filter((arg) => arg.type === 'ObjectExpression').forEach((object) => validatePropNaming(node, object.properties));
|
||||
}
|
||||
|
||||
function getComponentTypeAnnotation(component) {
|
||||
// If this is a functional component that uses a global type, check it
|
||||
if (
|
||||
(component.node.type === 'FunctionDeclaration' || component.node.type === 'ArrowFunctionExpression')
|
||||
&& component.node.params
|
||||
&& component.node.params.length > 0
|
||||
&& component.node.params[0].typeAnnotation
|
||||
) {
|
||||
return component.node.params[0].typeAnnotation.typeAnnotation;
|
||||
}
|
||||
|
||||
if (
|
||||
!component.node.parent
|
||||
|| component.node.parent.type !== 'VariableDeclarator'
|
||||
|| !component.node.parent.id
|
||||
|| component.node.parent.id.type !== 'Identifier'
|
||||
|| !component.node.parent.id.typeAnnotation
|
||||
|| !component.node.parent.id.typeAnnotation.typeAnnotation
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const annotationTypeArguments = propsUtil.getTypeArguments(
|
||||
component.node.parent.id.typeAnnotation.typeAnnotation
|
||||
);
|
||||
if (
|
||||
annotationTypeArguments && (
|
||||
annotationTypeArguments.type === 'TSTypeParameterInstantiation'
|
||||
|| annotationTypeArguments.type === 'TypeParameterInstantiation'
|
||||
)
|
||||
) {
|
||||
return annotationTypeArguments.params.find(
|
||||
(param) => param.type === 'TSTypeReference' || param.type === 'GenericTypeAnnotation'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function findAllTypeAnnotations(identifier, node) {
|
||||
if (node.type === 'TSTypeLiteral' || node.type === 'ObjectTypeAnnotation' || node.type === 'TSInterfaceBody') {
|
||||
const currentNode = [].concat(
|
||||
objectTypeAnnotations.get(identifier.name) || [],
|
||||
node
|
||||
);
|
||||
objectTypeAnnotations.set(identifier.name, currentNode);
|
||||
} else if (
|
||||
node.type === 'TSParenthesizedType'
|
||||
&& (
|
||||
node.typeAnnotation.type === 'TSIntersectionType'
|
||||
|| node.typeAnnotation.type === 'TSUnionType'
|
||||
)
|
||||
) {
|
||||
node.typeAnnotation.types.forEach((type) => {
|
||||
findAllTypeAnnotations(identifier, type);
|
||||
});
|
||||
} else if (
|
||||
node.type === 'TSIntersectionType'
|
||||
|| node.type === 'TSUnionType'
|
||||
|| node.type === 'IntersectionTypeAnnotation'
|
||||
|| node.type === 'UnionTypeAnnotation'
|
||||
) {
|
||||
node.types.forEach((type) => {
|
||||
findAllTypeAnnotations(identifier, type);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
'ClassProperty, PropertyDefinition'(node) {
|
||||
if (!rule || !propsUtil.isPropTypesDeclaration(node)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
node.value
|
||||
&& astUtil.isCallExpression(node.value)
|
||||
&& propWrapperUtil.isPropWrapperFunction(
|
||||
context,
|
||||
getText(context, node.value.callee)
|
||||
)
|
||||
) {
|
||||
checkPropWrapperArguments(node, node.value.arguments);
|
||||
}
|
||||
if (node.value && node.value.properties) {
|
||||
validatePropNaming(node, node.value.properties);
|
||||
}
|
||||
if (node.typeAnnotation && node.typeAnnotation.typeAnnotation) {
|
||||
validatePropNaming(node, node.typeAnnotation.typeAnnotation.properties);
|
||||
}
|
||||
},
|
||||
|
||||
MemberExpression(node) {
|
||||
if (!rule || !propsUtil.isPropTypesDeclaration(node)) {
|
||||
return;
|
||||
}
|
||||
const component = utils.getRelatedComponent(node);
|
||||
if (!component || !node.parent.right) {
|
||||
return;
|
||||
}
|
||||
const right = node.parent.right;
|
||||
if (
|
||||
astUtil.isCallExpression(right)
|
||||
&& propWrapperUtil.isPropWrapperFunction(
|
||||
context,
|
||||
getText(context, right.callee)
|
||||
)
|
||||
) {
|
||||
checkPropWrapperArguments(component.node, right.arguments);
|
||||
return;
|
||||
}
|
||||
validatePropNaming(component.node, node.parent.right.properties);
|
||||
},
|
||||
|
||||
ObjectExpression(node) {
|
||||
if (!rule) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Search for the proptypes declaration
|
||||
node.properties.forEach((property) => {
|
||||
if (!propsUtil.isPropTypesDeclaration(property)) {
|
||||
return;
|
||||
}
|
||||
validatePropNaming(node, property.value.properties);
|
||||
});
|
||||
},
|
||||
|
||||
TypeAlias(node) {
|
||||
findAllTypeAnnotations(node.id, node.right);
|
||||
},
|
||||
|
||||
TSTypeAliasDeclaration(node) {
|
||||
findAllTypeAnnotations(node.id, node.typeAnnotation);
|
||||
},
|
||||
|
||||
TSInterfaceDeclaration(node) {
|
||||
findAllTypeAnnotations(node.id, node.body);
|
||||
},
|
||||
|
||||
// eslint-disable-next-line object-shorthand
|
||||
'Program:exit'() {
|
||||
if (!rule) {
|
||||
return;
|
||||
}
|
||||
|
||||
values(components.list()).forEach((component) => {
|
||||
const annotation = getComponentTypeAnnotation(component);
|
||||
|
||||
if (annotation) {
|
||||
let propType;
|
||||
if (annotation.type === 'GenericTypeAnnotation') {
|
||||
propType = objectTypeAnnotations.get(annotation.id.name);
|
||||
} else if (annotation.type === 'ObjectTypeAnnotation' || annotation.type === 'TSTypeLiteral') {
|
||||
propType = annotation;
|
||||
} else if (annotation.type === 'TSTypeReference') {
|
||||
propType = objectTypeAnnotations.get(annotation.typeName.name);
|
||||
} else if (annotation.type === 'TSIntersectionType') {
|
||||
propType = flatMap(annotation.types, (type) => (
|
||||
type.type === 'TSTypeReference'
|
||||
? objectTypeAnnotations.get(type.typeName.name)
|
||||
: type
|
||||
));
|
||||
}
|
||||
|
||||
if (propType) {
|
||||
[].concat(propType).filter(Boolean).forEach((prop) => {
|
||||
validatePropNaming(
|
||||
component.node,
|
||||
prop.properties || prop.members || prop.body
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (component.invalidProps && component.invalidProps.length > 0) {
|
||||
reportInvalidNaming(component);
|
||||
}
|
||||
});
|
||||
|
||||
// Reset cache
|
||||
objectTypeAnnotations.clear();
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare const _exports: import('eslint').Rule.RuleModule;
|
||||
export = _exports;
|
||||
//# sourceMappingURL=button-has-type.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"button-has-type.d.ts","sourceRoot":"","sources":["button-has-type.js"],"names":[],"mappings":"wBA8BW,OAAO,QAAQ,EAAE,IAAI,CAAC,UAAU"}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* @fileoverview Forbid "button" element without an explicit "type" attribute
|
||||
* @author Filipp Riabchun
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const getProp = require('jsx-ast-utils/getProp');
|
||||
const getLiteralPropValue = require('jsx-ast-utils/getLiteralPropValue');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const isCreateElement = require('../util/isCreateElement');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const optionDefaults = {
|
||||
button: true,
|
||||
submit: true,
|
||||
reset: true,
|
||||
};
|
||||
|
||||
const messages = {
|
||||
missingType: 'Missing an explicit type attribute for button',
|
||||
complexType: 'The button type attribute must be specified by a static string or a trivial ternary expression',
|
||||
invalidValue: '"{{value}}" is an invalid value for button type attribute',
|
||||
forbiddenValue: '"{{value}}" is an invalid value for button type attribute',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow usage of `button` elements without an explicit `type` attribute',
|
||||
category: 'Possible Errors',
|
||||
recommended: false,
|
||||
url: docsUrl('button-has-type'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
button: {
|
||||
default: optionDefaults.button,
|
||||
type: 'boolean',
|
||||
},
|
||||
submit: {
|
||||
default: optionDefaults.submit,
|
||||
type: 'boolean',
|
||||
},
|
||||
reset: {
|
||||
default: optionDefaults.reset,
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = Object.assign({}, optionDefaults, context.options[0]);
|
||||
|
||||
function reportMissing(node) {
|
||||
report(context, messages.missingType, 'missingType', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
|
||||
function reportComplex(node) {
|
||||
report(context, messages.complexType, 'complexType', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
|
||||
function checkValue(node, value) {
|
||||
if (!(value in configuration)) {
|
||||
report(context, messages.invalidValue, 'invalidValue', {
|
||||
node,
|
||||
data: {
|
||||
value,
|
||||
},
|
||||
});
|
||||
} else if (!configuration[value]) {
|
||||
report(context, messages.forbiddenValue, 'forbiddenValue', {
|
||||
node,
|
||||
data: {
|
||||
value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function checkExpression(node, expression) {
|
||||
switch (expression.type) {
|
||||
case 'Literal':
|
||||
checkValue(node, expression.value);
|
||||
return;
|
||||
case 'TemplateLiteral':
|
||||
if (expression.expressions.length === 0) {
|
||||
checkValue(node, expression.quasis[0].value.raw);
|
||||
} else {
|
||||
reportComplex(expression);
|
||||
}
|
||||
return;
|
||||
case 'ConditionalExpression':
|
||||
checkExpression(node, expression.consequent);
|
||||
checkExpression(node, expression.alternate);
|
||||
return;
|
||||
default:
|
||||
reportComplex(expression);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
JSXElement(node) {
|
||||
if (node.openingElement.name.name !== 'button') {
|
||||
return;
|
||||
}
|
||||
|
||||
const typeProp = getProp(node.openingElement.attributes, 'type');
|
||||
|
||||
if (!typeProp) {
|
||||
reportMissing(node);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeProp.value && typeProp.value.type === 'JSXExpressionContainer') {
|
||||
checkExpression(node, typeProp.value.expression);
|
||||
return;
|
||||
}
|
||||
|
||||
const propValue = getLiteralPropValue(typeProp);
|
||||
checkValue(node, propValue);
|
||||
},
|
||||
CallExpression(node) {
|
||||
if (!isCreateElement(context, node) || node.arguments.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.arguments[0].type !== 'Literal' || node.arguments[0].value !== 'button') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!node.arguments[1] || node.arguments[1].type !== 'ObjectExpression') {
|
||||
reportMissing(node);
|
||||
return;
|
||||
}
|
||||
|
||||
const props = node.arguments[1].properties;
|
||||
const typeProp = props.find((prop) => (
|
||||
'key' in prop
|
||||
&& prop.key
|
||||
&& 'name' in prop.key
|
||||
&& prop.key.name === 'type'
|
||||
));
|
||||
|
||||
if (!typeProp) {
|
||||
reportMissing(node);
|
||||
return;
|
||||
}
|
||||
|
||||
checkExpression(node, 'value' in typeProp ? typeProp.value : undefined);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Generated
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
declare const _exports: import('eslint').Rule.RuleModule;
|
||||
export = _exports;
|
||||
//# sourceMappingURL=checked-requires-onchange-or-readonly.d.ts.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"checked-requires-onchange-or-readonly.d.ts","sourceRoot":"","sources":["checked-requires-onchange-or-readonly.js"],"names":[],"mappings":"wBA2CW,OAAO,QAAQ,EAAE,IAAI,CAAC,UAAU"}
|
||||
Generated
Vendored
+142
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @fileoverview Enforce the use of the 'onChange' or 'readonly' attribute when 'checked' is used'
|
||||
* @author Jaesoekjjang
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const ASTUtils = require('jsx-ast-utils');
|
||||
const flatMap = require('array.prototype.flatmap');
|
||||
const isCreateElement = require('../util/isCreateElement');
|
||||
const report = require('../util/report');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
|
||||
const messages = {
|
||||
missingProperty: '`checked` should be used with either `onChange` or `readOnly`.',
|
||||
exclusiveCheckedAttribute: 'Use either `checked` or `defaultChecked`, but not both.',
|
||||
};
|
||||
|
||||
const targetPropSet = new Set(['checked', 'onChange', 'readOnly', 'defaultChecked']);
|
||||
|
||||
const defaultOptions = {
|
||||
ignoreMissingProperties: false,
|
||||
ignoreExclusiveCheckedAttribute: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {object[]} properties
|
||||
* @param {string} keyName
|
||||
* @returns {Set<string>}
|
||||
*/
|
||||
function extractTargetProps(properties, keyName) {
|
||||
return new Set(
|
||||
flatMap(
|
||||
properties,
|
||||
(prop) => (
|
||||
prop[keyName] && targetPropSet.has(prop[keyName].name)
|
||||
? [prop[keyName].name]
|
||||
: []
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce using `onChange` or `readonly` attribute when `checked` is used',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('checked-requires-onchange-or-readonly'),
|
||||
},
|
||||
messages,
|
||||
schema: [{
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
ignoreMissingProperties: {
|
||||
type: 'boolean',
|
||||
},
|
||||
ignoreExclusiveCheckedAttribute: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
create(context) {
|
||||
const options = Object.assign({}, defaultOptions, context.options[0]);
|
||||
|
||||
function reportMissingProperty(node) {
|
||||
report(
|
||||
context,
|
||||
messages.missingProperty,
|
||||
'missingProperty',
|
||||
{ node }
|
||||
);
|
||||
}
|
||||
|
||||
function reportExclusiveCheckedAttribute(node) {
|
||||
report(
|
||||
context,
|
||||
messages.exclusiveCheckedAttribute,
|
||||
'exclusiveCheckedAttribute',
|
||||
{ node }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node
|
||||
* @param {Set<string>} propSet
|
||||
* @returns {void}
|
||||
*/
|
||||
const checkAttributesAndReport = (node, propSet) => {
|
||||
if (!propSet.has('checked')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.ignoreExclusiveCheckedAttribute && propSet.has('defaultChecked')) {
|
||||
reportExclusiveCheckedAttribute(node);
|
||||
}
|
||||
|
||||
if (
|
||||
!options.ignoreMissingProperties
|
||||
&& !(propSet.has('onChange') || propSet.has('readOnly'))
|
||||
) {
|
||||
reportMissingProperty(node);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
if (ASTUtils.elementType(node) !== 'input') {
|
||||
return;
|
||||
}
|
||||
|
||||
const propSet = extractTargetProps(node.attributes, 'name');
|
||||
checkAttributesAndReport(node, propSet);
|
||||
},
|
||||
CallExpression(node) {
|
||||
if (!isCreateElement(context, node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstArg = node.arguments[0];
|
||||
const secondArg = node.arguments[1];
|
||||
if (
|
||||
!firstArg
|
||||
|| firstArg.type !== 'Literal'
|
||||
|| firstArg.value !== 'input'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!secondArg || secondArg.type !== 'ObjectExpression') {
|
||||
return;
|
||||
}
|
||||
|
||||
const propSet = extractTargetProps(secondArg.properties, 'key');
|
||||
checkAttributesAndReport(node, propSet);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare const _exports: import('eslint').Rule.RuleModule;
|
||||
export = _exports;
|
||||
//# sourceMappingURL=default-props-match-prop-types.d.ts.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"default-props-match-prop-types.d.ts","sourceRoot":"","sources":["default-props-match-prop-types.js"],"names":[],"mappings":"wBAuBW,OAAO,QAAQ,EAAE,IAAI,CAAC,UAAU"}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* @fileOverview Enforce all defaultProps are defined in propTypes
|
||||
* @author Vitor Balocco
|
||||
* @author Roy Sutton
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const values = require('object.values');
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
requiredHasDefault: 'defaultProp "{{name}}" defined for isRequired propType.',
|
||||
defaultHasNoType: 'defaultProp "{{name}}" has no corresponding propTypes declaration.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce all defaultProps have a corresponding non-required PropType',
|
||||
category: 'Best Practices',
|
||||
url: docsUrl('default-props-match-prop-types'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowRequiredDefaults: {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components) => {
|
||||
const configuration = context.options[0] || {};
|
||||
const allowRequiredDefaults = configuration.allowRequiredDefaults || false;
|
||||
|
||||
/**
|
||||
* Reports all defaultProps passed in that don't have an appropriate propTypes counterpart.
|
||||
* @param {Object[]} propTypes Array of propTypes to check.
|
||||
* @param {Object} defaultProps Object of defaultProps to check. Keys are the props names.
|
||||
* @return {void}
|
||||
*/
|
||||
function reportInvalidDefaultProps(propTypes, defaultProps) {
|
||||
// If this defaultProps is "unresolved" or the propTypes is undefined, then we should ignore
|
||||
// this component and not report any errors for it, to avoid false-positives with e.g.
|
||||
// external defaultProps/propTypes declarations or spread operators.
|
||||
if (defaultProps === 'unresolved' || !propTypes || Object.keys(propTypes).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(defaultProps).forEach((defaultPropName) => {
|
||||
const defaultProp = defaultProps[defaultPropName];
|
||||
const prop = propTypes[defaultPropName];
|
||||
|
||||
if (prop && (allowRequiredDefaults || !prop.isRequired)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prop) {
|
||||
report(context, messages.requiredHasDefault, 'requiredHasDefault', {
|
||||
node: defaultProp.node,
|
||||
data: {
|
||||
name: defaultPropName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
report(context, messages.defaultHasNoType, 'defaultHasNoType', {
|
||||
node: defaultProp.node,
|
||||
data: {
|
||||
name: defaultPropName,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public API
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
'Program:exit'() {
|
||||
// If no defaultProps could be found, we don't report anything.
|
||||
values(components.list())
|
||||
.filter((component) => component.defaultProps)
|
||||
.forEach((component) => {
|
||||
reportInvalidDefaultProps(
|
||||
component.declaredPropTypes,
|
||||
component.defaultProps || {}
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare const _exports: import('eslint').Rule.RuleModule;
|
||||
export = _exports;
|
||||
//# sourceMappingURL=destructuring-assignment.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"destructuring-assignment.d.ts","sourceRoot":"","sources":["destructuring-assignment.js"],"names":[],"mappings":"wBA2DW,OAAO,QAAQ,EAAE,IAAI,CAAC,UAAU"}
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* @fileoverview Enforce consistent usage of destructuring assignment of props, state, and context.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const eslintUtil = require('../util/eslint');
|
||||
const isAssignmentLHS = require('../util/ast').isAssignmentLHS;
|
||||
const report = require('../util/report');
|
||||
|
||||
const getScope = eslintUtil.getScope;
|
||||
const getText = eslintUtil.getText;
|
||||
|
||||
const DEFAULT_OPTION = 'always';
|
||||
|
||||
function createSFCParams() {
|
||||
const queue = [];
|
||||
|
||||
return {
|
||||
push(params) {
|
||||
queue.unshift(params);
|
||||
},
|
||||
pop() {
|
||||
queue.shift();
|
||||
},
|
||||
propsName() {
|
||||
const found = queue.find((params) => {
|
||||
const props = params[0];
|
||||
return props && !props.destructuring && props.name;
|
||||
});
|
||||
return found && found[0] && found[0].name;
|
||||
},
|
||||
contextName() {
|
||||
const found = queue.find((params) => {
|
||||
const context = params[1];
|
||||
return context && !context.destructuring && context.name;
|
||||
});
|
||||
return found && found[1] && found[1].name;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function evalParams(params) {
|
||||
return params.map((param) => ({
|
||||
destructuring: param.type === 'ObjectPattern',
|
||||
name: param.type === 'Identifier' && param.name,
|
||||
}));
|
||||
}
|
||||
|
||||
const messages = {
|
||||
noDestructPropsInSFCArg: 'Must never use destructuring props assignment in SFC argument',
|
||||
noDestructContextInSFCArg: 'Must never use destructuring context assignment in SFC argument',
|
||||
noDestructAssignment: 'Must never use destructuring {{type}} assignment',
|
||||
useDestructAssignment: 'Must use destructuring {{type}} assignment',
|
||||
destructureInSignature: 'Must destructure props in the function signature.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce consistent usage of destructuring assignment of props, state, and context',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('destructuring-assignment'),
|
||||
},
|
||||
fixable: 'code',
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'string',
|
||||
enum: [
|
||||
'always',
|
||||
'never',
|
||||
],
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
ignoreClassFields: {
|
||||
type: 'boolean',
|
||||
},
|
||||
destructureInSignature: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'always',
|
||||
'ignore',
|
||||
],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components, utils) => {
|
||||
const configuration = context.options[0] || DEFAULT_OPTION;
|
||||
const ignoreClassFields = (context.options[1] && (context.options[1].ignoreClassFields === true)) || false;
|
||||
const destructureInSignature = (context.options[1] && context.options[1].destructureInSignature) || 'ignore';
|
||||
const sfcParams = createSFCParams();
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node We expect either an ArrowFunctionExpression,
|
||||
* FunctionDeclaration, or FunctionExpression
|
||||
*/
|
||||
function handleStatelessComponent(node) {
|
||||
const params = evalParams(node.params);
|
||||
|
||||
const SFCComponent = components.get(getScope(context, node).block);
|
||||
if (!SFCComponent) {
|
||||
return;
|
||||
}
|
||||
sfcParams.push(params);
|
||||
|
||||
if (params[0] && params[0].destructuring && components.get(node) && configuration === 'never') {
|
||||
report(context, messages.noDestructPropsInSFCArg, 'noDestructPropsInSFCArg', {
|
||||
node,
|
||||
});
|
||||
} else if (params[1] && params[1].destructuring && components.get(node) && configuration === 'never') {
|
||||
report(context, messages.noDestructContextInSFCArg, 'noDestructContextInSFCArg', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleStatelessComponentExit(node) {
|
||||
const SFCComponent = components.get(getScope(context, node).block);
|
||||
if (SFCComponent) {
|
||||
sfcParams.pop();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSFCUsage(node) {
|
||||
const propsName = sfcParams.propsName();
|
||||
const contextName = sfcParams.contextName();
|
||||
// props.aProp || context.aProp
|
||||
const isPropUsed = (
|
||||
(propsName && node.object.name === propsName)
|
||||
|| (contextName && node.object.name === contextName)
|
||||
)
|
||||
&& !isAssignmentLHS(node);
|
||||
if (isPropUsed && configuration === 'always' && !node.optional) {
|
||||
report(context, messages.useDestructAssignment, 'useDestructAssignment', {
|
||||
node,
|
||||
data: {
|
||||
type: node.object.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isInClassProperty(node) {
|
||||
let curNode = node.parent;
|
||||
while (curNode) {
|
||||
if (curNode.type === 'ClassProperty' || curNode.type === 'PropertyDefinition') {
|
||||
return true;
|
||||
}
|
||||
curNode = curNode.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleClassUsage(node) {
|
||||
// this.props.Aprop || this.context.aProp || this.state.aState
|
||||
const isPropUsed = (
|
||||
node.object.type === 'MemberExpression' && node.object.object.type === 'ThisExpression'
|
||||
&& (node.object.property.name === 'props' || node.object.property.name === 'context' || node.object.property.name === 'state')
|
||||
&& !isAssignmentLHS(node)
|
||||
);
|
||||
|
||||
if (
|
||||
isPropUsed && configuration === 'always'
|
||||
&& !(ignoreClassFields && isInClassProperty(node))
|
||||
) {
|
||||
report(context, messages.useDestructAssignment, 'useDestructAssignment', {
|
||||
node,
|
||||
data: {
|
||||
type: node.object.property.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// valid-jsdoc cannot read function types
|
||||
// eslint-disable-next-line valid-jsdoc
|
||||
/**
|
||||
* Find a parent that satisfy the given predicate
|
||||
* @param {ASTNode} node
|
||||
* @param {(node: ASTNode) => boolean} predicate
|
||||
* @returns {ASTNode | undefined}
|
||||
*/
|
||||
function findParent(node, predicate) {
|
||||
let n = node;
|
||||
while (n) {
|
||||
if (predicate(n)) {
|
||||
return n;
|
||||
}
|
||||
n = n.parent;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
FunctionDeclaration: handleStatelessComponent,
|
||||
|
||||
ArrowFunctionExpression: handleStatelessComponent,
|
||||
|
||||
FunctionExpression: handleStatelessComponent,
|
||||
|
||||
'FunctionDeclaration:exit': handleStatelessComponentExit,
|
||||
|
||||
'ArrowFunctionExpression:exit': handleStatelessComponentExit,
|
||||
|
||||
'FunctionExpression:exit': handleStatelessComponentExit,
|
||||
|
||||
MemberExpression(node) {
|
||||
const SFCComponent = utils.getParentStatelessComponent(node);
|
||||
if (SFCComponent) {
|
||||
handleSFCUsage(node);
|
||||
}
|
||||
|
||||
const classComponent = utils.getParentComponent(node);
|
||||
if (classComponent) {
|
||||
handleClassUsage(node);
|
||||
}
|
||||
},
|
||||
|
||||
TSQualifiedName(node) {
|
||||
if (configuration !== 'always') {
|
||||
return;
|
||||
}
|
||||
// handle `typeof props.a.b`
|
||||
if (node.left.type === 'Identifier'
|
||||
&& node.left.name === sfcParams.propsName()
|
||||
&& findParent(node, (n) => n.type === 'TSTypeQuery')
|
||||
&& utils.getParentStatelessComponent(node)
|
||||
) {
|
||||
report(context, messages.useDestructAssignment, 'useDestructAssignment', {
|
||||
node,
|
||||
data: {
|
||||
type: 'props',
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
VariableDeclarator(node) {
|
||||
const classComponent = utils.getParentComponent(node);
|
||||
const SFCComponent = components.get(getScope(context, node).block);
|
||||
|
||||
const destructuring = (node.init && node.id && node.id.type === 'ObjectPattern');
|
||||
// let {foo} = props;
|
||||
const destructuringSFC = destructuring && (node.init.name === 'props' || node.init.name === 'context');
|
||||
// let {foo} = this.props;
|
||||
const destructuringClass = destructuring && node.init.object && node.init.object.type === 'ThisExpression' && (
|
||||
node.init.property.name === 'props' || node.init.property.name === 'context' || node.init.property.name === 'state'
|
||||
);
|
||||
|
||||
if (SFCComponent && destructuringSFC && configuration === 'never') {
|
||||
report(context, messages.noDestructAssignment, 'noDestructAssignment', {
|
||||
node,
|
||||
data: {
|
||||
type: node.init.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
classComponent && destructuringClass && configuration === 'never'
|
||||
&& !(ignoreClassFields && (node.parent.type === 'ClassProperty' || node.parent.type === 'PropertyDefinition'))
|
||||
) {
|
||||
report(context, messages.noDestructAssignment, 'noDestructAssignment', {
|
||||
node,
|
||||
data: {
|
||||
type: node.init.property.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
SFCComponent
|
||||
&& destructuringSFC
|
||||
&& configuration === 'always'
|
||||
&& destructureInSignature === 'always'
|
||||
&& node.init.name === 'props'
|
||||
) {
|
||||
const scopeSetProps = getScope(context, node).set.get('props');
|
||||
const propsRefs = scopeSetProps && scopeSetProps.references;
|
||||
if (!propsRefs) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if props is used elsewhere
|
||||
if (propsRefs.length > 1) {
|
||||
return;
|
||||
}
|
||||
report(context, messages.destructureInSignature, 'destructureInSignature', {
|
||||
node,
|
||||
fix(fixer) {
|
||||
const param = SFCComponent.node.params[0];
|
||||
if (!param) {
|
||||
return;
|
||||
}
|
||||
const replaceRange = [
|
||||
param.range[0],
|
||||
param.typeAnnotation ? param.typeAnnotation.range[0] : param.range[1],
|
||||
];
|
||||
return [
|
||||
fixer.replaceTextRange(replaceRange, getText(context, node.id)),
|
||||
fixer.remove(node.parent),
|
||||
];
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare const _exports: import('eslint').Rule.RuleModule;
|
||||
export = _exports;
|
||||
//# sourceMappingURL=display-name.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"display-name.d.ts","sourceRoot":"","sources":["display-name.js"],"names":[],"mappings":"wBA6BW,OAAO,QAAQ,EAAE,IAAI,CAAC,UAAU"}
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* @fileoverview Prevent missing displayName in a React component definition
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const values = require('object.values');
|
||||
const filter = require('es-iterator-helpers/Iterator.prototype.filter');
|
||||
const forEach = require('es-iterator-helpers/Iterator.prototype.forEach');
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const isCreateContext = require('../util/isCreateContext');
|
||||
const astUtil = require('../util/ast');
|
||||
const componentUtil = require('../util/componentUtil');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const testReactVersion = require('../util/version').testReactVersion;
|
||||
const propsUtil = require('../util/props');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noDisplayName: 'Component definition is missing display name',
|
||||
noContextDisplayName: 'Context definition is missing display name',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow missing displayName in a React component definition',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
url: docsUrl('display-name'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
ignoreTranspilerName: {
|
||||
type: 'boolean',
|
||||
},
|
||||
checkContextObjects: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components, utils) => {
|
||||
const config = context.options[0] || {};
|
||||
const ignoreTranspilerName = config.ignoreTranspilerName || false;
|
||||
const checkContextObjects = (config.checkContextObjects || false) && testReactVersion(context, '>= 16.3.0');
|
||||
|
||||
const contextObjects = new Map();
|
||||
|
||||
/**
|
||||
* Mark a prop type as declared
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
*/
|
||||
function markDisplayNameAsDeclared(node) {
|
||||
components.set(node, {
|
||||
hasDisplayName: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if React.forwardRef is nested inside React.memo
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {boolean} True if React.forwardRef is nested inside React.memo, false if not.
|
||||
*/
|
||||
function isNestedMemo(node) {
|
||||
return astUtil.isCallExpression(node)
|
||||
&& node.arguments
|
||||
&& astUtil.isCallExpression(node.arguments[0])
|
||||
&& utils.isPragmaComponentWrapper(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports missing display name for a given component
|
||||
* @param {Object} component The component to process
|
||||
*/
|
||||
function reportMissingDisplayName(component) {
|
||||
if (
|
||||
testReactVersion(context, '^0.14.10 || ^15.7.0 || >= 16.12.0')
|
||||
&& isNestedMemo(component.node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
report(context, messages.noDisplayName, 'noDisplayName', {
|
||||
node: component.node,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports missing display name for a given context object
|
||||
* @param {Object} contextObj The context object to process
|
||||
*/
|
||||
function reportMissingContextDisplayName(contextObj) {
|
||||
report(context, messages.noContextDisplayName, 'noContextDisplayName', {
|
||||
node: contextObj.node,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the component have a name set by the transpiler
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {boolean} True if component has a name, false if not.
|
||||
*/
|
||||
function hasTranspilerName(node) {
|
||||
const namedObjectAssignment = (
|
||||
node.type === 'ObjectExpression'
|
||||
&& node.parent
|
||||
&& node.parent.parent
|
||||
&& node.parent.parent.type === 'AssignmentExpression'
|
||||
&& (
|
||||
!node.parent.parent.left.object
|
||||
|| node.parent.parent.left.object.name !== 'module'
|
||||
|| node.parent.parent.left.property.name !== 'exports'
|
||||
)
|
||||
);
|
||||
const namedObjectDeclaration = (
|
||||
node.type === 'ObjectExpression'
|
||||
&& node.parent
|
||||
&& node.parent.parent
|
||||
&& node.parent.parent.type === 'VariableDeclarator'
|
||||
);
|
||||
const namedClass = (
|
||||
(node.type === 'ClassDeclaration' || node.type === 'ClassExpression')
|
||||
&& node.id
|
||||
&& !!node.id.name
|
||||
);
|
||||
|
||||
const namedFunctionDeclaration = (
|
||||
(node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression')
|
||||
&& node.id
|
||||
&& !!node.id.name
|
||||
);
|
||||
|
||||
const namedFunctionExpression = (
|
||||
astUtil.isFunctionLikeExpression(node)
|
||||
&& node.parent
|
||||
&& (node.parent.type === 'VariableDeclarator' || node.parent.type === 'Property' || node.parent.method === true)
|
||||
&& (!node.parent.parent || !componentUtil.isES5Component(node.parent.parent, context))
|
||||
);
|
||||
|
||||
if (
|
||||
namedObjectAssignment || namedObjectDeclaration
|
||||
|| namedClass
|
||||
|| namedFunctionDeclaration || namedFunctionExpression
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
ExpressionStatement(node) {
|
||||
if (checkContextObjects && isCreateContext(node)) {
|
||||
contextObjects.set(node.expression.left.name, { node, hasDisplayName: false });
|
||||
}
|
||||
},
|
||||
VariableDeclarator(node) {
|
||||
if (checkContextObjects && isCreateContext(node)) {
|
||||
contextObjects.set(node.id.name, { node, hasDisplayName: false });
|
||||
}
|
||||
},
|
||||
'ClassProperty, PropertyDefinition'(node) {
|
||||
if (!propsUtil.isDisplayNameDeclaration(node)) {
|
||||
return;
|
||||
}
|
||||
markDisplayNameAsDeclared(node);
|
||||
},
|
||||
|
||||
MemberExpression(node) {
|
||||
if (!propsUtil.isDisplayNameDeclaration(node.property)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
checkContextObjects
|
||||
&& node.object
|
||||
&& node.object.name
|
||||
&& contextObjects.has(node.object.name)
|
||||
) {
|
||||
contextObjects.get(node.object.name).hasDisplayName = true;
|
||||
}
|
||||
const component = utils.getRelatedComponent(node);
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
markDisplayNameAsDeclared(astUtil.unwrapTSAsExpression(component.node));
|
||||
},
|
||||
|
||||
'FunctionExpression, FunctionDeclaration, ArrowFunctionExpression'(node) {
|
||||
if (ignoreTranspilerName || !hasTranspilerName(node)) {
|
||||
return;
|
||||
}
|
||||
if (components.get(node)) {
|
||||
markDisplayNameAsDeclared(node);
|
||||
}
|
||||
},
|
||||
|
||||
MethodDefinition(node) {
|
||||
if (!propsUtil.isDisplayNameDeclaration(node.key)) {
|
||||
return;
|
||||
}
|
||||
markDisplayNameAsDeclared(node);
|
||||
},
|
||||
|
||||
'ClassExpression, ClassDeclaration'(node) {
|
||||
if (ignoreTranspilerName || !hasTranspilerName(node)) {
|
||||
return;
|
||||
}
|
||||
markDisplayNameAsDeclared(node);
|
||||
},
|
||||
|
||||
ObjectExpression(node) {
|
||||
if (!componentUtil.isES5Component(node, context)) {
|
||||
return;
|
||||
}
|
||||
if (ignoreTranspilerName || !hasTranspilerName(node)) {
|
||||
// Search for the displayName declaration
|
||||
node.properties.forEach((property) => {
|
||||
if (!property.key || !propsUtil.isDisplayNameDeclaration(property.key)) {
|
||||
return;
|
||||
}
|
||||
markDisplayNameAsDeclared(node);
|
||||
});
|
||||
return;
|
||||
}
|
||||
markDisplayNameAsDeclared(node);
|
||||
},
|
||||
|
||||
CallExpression(node) {
|
||||
if (!utils.isPragmaComponentWrapper(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.arguments.length > 0 && astUtil.isFunctionLikeExpression(node.arguments[0])) {
|
||||
// Skip over React.forwardRef declarations that are embedded within
|
||||
// a React.memo i.e. React.memo(React.forwardRef(/* ... */))
|
||||
// This means that we raise a single error for the call to React.memo
|
||||
// instead of one for React.memo and one for React.forwardRef
|
||||
const isWrappedInAnotherPragma = utils.getPragmaComponentWrapper(node);
|
||||
if (
|
||||
!isWrappedInAnotherPragma
|
||||
&& (ignoreTranspilerName || !hasTranspilerName(node.arguments[0]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (components.get(node)) {
|
||||
markDisplayNameAsDeclared(node);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'Program:exit'() {
|
||||
const list = components.list();
|
||||
// Report missing display name for all components
|
||||
values(list).filter((component) => !component.hasDisplayName).forEach((component) => {
|
||||
reportMissingDisplayName(component);
|
||||
});
|
||||
if (checkContextObjects) {
|
||||
// Report missing display name for all context objects
|
||||
forEach(
|
||||
filter(contextObjects.values(), (v) => !v.hasDisplayName),
|
||||
(contextObj) => reportMissingContextDisplayName(contextObj)
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare const _exports: import('eslint').Rule.RuleModule;
|
||||
export = _exports;
|
||||
//# sourceMappingURL=forbid-component-props.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"forbid-component-props.d.ts","sourceRoot":"","sources":["forbid-component-props.js"],"names":[],"mappings":"wBAyBW,OAAO,QAAQ,EAAE,IAAI,CAAC,UAAU"}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* @fileoverview Forbid certain props on components
|
||||
* @author Joe Lencioni
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const minimatch = require('minimatch');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const DEFAULTS = ['className', 'style'];
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
propIsForbidden: 'Prop "{{prop}}" is forbidden on Components',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow certain props on components',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('forbid-component-props'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
forbid: {
|
||||
type: 'array',
|
||||
items: {
|
||||
anyOf: [
|
||||
{ type: 'string' },
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
propName: { type: 'string' },
|
||||
allowedFor: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
allowedForPatterns: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
message: { type: 'string' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
propName: { type: 'string' },
|
||||
disallowedFor: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
minItems: 1,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
disallowedForPatterns: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
minItems: 1,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
message: { type: 'string' },
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['disallowedFor'] },
|
||||
{ required: ['disallowedForPatterns'] },
|
||||
],
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
propNamePattern: { type: 'string' },
|
||||
allowedFor: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
allowedForPatterns: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
message: { type: 'string' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
propNamePattern: { type: 'string' },
|
||||
disallowedFor: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
minItems: 1,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
disallowedForPatterns: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
minItems: 1,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
message: { type: 'string' },
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['disallowedFor'] },
|
||||
{ required: ['disallowedForPatterns'] },
|
||||
],
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const forbid = new Map((configuration.forbid || DEFAULTS).map((value) => {
|
||||
const propName = typeof value === 'string' ? value : value.propName;
|
||||
const propPattern = value.propNamePattern;
|
||||
const prop = propName || propPattern;
|
||||
const options = {
|
||||
allowList: [].concat(value.allowedFor || []),
|
||||
allowPatternList: [].concat(value.allowedForPatterns || []),
|
||||
disallowList: [].concat(value.disallowedFor || []),
|
||||
disallowPatternList: [].concat(value.disallowedForPatterns || []),
|
||||
message: typeof value === 'string' ? null : value.message,
|
||||
isPattern: !!value.propNamePattern,
|
||||
};
|
||||
return [prop, options];
|
||||
}));
|
||||
|
||||
function getPropOptions(prop) {
|
||||
// Get config options having pattern
|
||||
const propNamePatternArray = Array.from(forbid.entries()).filter((propEntry) => propEntry[1].isPattern);
|
||||
// Match current prop with pattern options, return if matched
|
||||
const propNamePattern = propNamePatternArray.find((propPatternVal) => minimatch(prop, propPatternVal[0]));
|
||||
// Get options for matched propNamePattern
|
||||
const propNamePatternOptions = propNamePattern && propNamePattern[1];
|
||||
|
||||
const options = forbid.get(prop) || propNamePatternOptions;
|
||||
return options;
|
||||
}
|
||||
|
||||
function isForbidden(prop, tagName) {
|
||||
const options = getPropOptions(prop);
|
||||
if (!options) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function checkIsTagForbiddenByAllowOptions() {
|
||||
if (options.allowList.indexOf(tagName) !== -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.allowPatternList.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return options.allowPatternList.every(
|
||||
(pattern) => !minimatch(tagName, pattern)
|
||||
);
|
||||
}
|
||||
|
||||
function checkIsTagForbiddenByDisallowOptions() {
|
||||
if (options.disallowList.indexOf(tagName) !== -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (options.disallowPatternList.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return options.disallowPatternList.some(
|
||||
(pattern) => minimatch(tagName, pattern)
|
||||
);
|
||||
}
|
||||
|
||||
const hasDisallowOptions = options.disallowList.length > 0 || options.disallowPatternList.length > 0;
|
||||
|
||||
// disallowList should have a least one item (schema configuration)
|
||||
const isTagForbidden = hasDisallowOptions
|
||||
? checkIsTagForbiddenByDisallowOptions()
|
||||
: checkIsTagForbiddenByAllowOptions();
|
||||
|
||||
// if the tagName is undefined (`<this.something>`), we assume it's a forbidden element
|
||||
return typeof tagName === 'undefined' || isTagForbidden;
|
||||
}
|
||||
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
const parentName = node.parent.name;
|
||||
// Extract a component name when using a "namespace", e.g. `<AntdLayout.Content />`.
|
||||
const tag = parentName.name || `${parentName.object.name}.${parentName.property.name}`;
|
||||
const componentName = parentName.name || parentName.property.name;
|
||||
if (componentName && typeof componentName[0] === 'string' && componentName[0] !== componentName[0].toUpperCase()) {
|
||||
// This is a DOM node, not a Component, so exit.
|
||||
return;
|
||||
}
|
||||
|
||||
const prop = node.name.name;
|
||||
|
||||
if (!isForbidden(prop, tag)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const customMessage = getPropOptions(prop).message;
|
||||
|
||||
report(context, customMessage || messages.propIsForbidden, !customMessage && 'propIsForbidden', {
|
||||
node,
|
||||
data: {
|
||||
prop,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare const _exports: import('eslint').Rule.RuleModule;
|
||||
export = _exports;
|
||||
//# sourceMappingURL=forbid-dom-props.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"forbid-dom-props.d.ts","sourceRoot":"","sources":["forbid-dom-props.js"],"names":[],"mappings":"wBAuCW,OAAO,QAAQ,EAAE,IAAI,CAAC,UAAU"}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* @fileoverview Forbid certain props on DOM Nodes
|
||||
* @author David Vázquez
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const DEFAULTS = [];
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {Map<string, object>} forbidMap // { disallowList: null | string[], message: null | string }
|
||||
* @param {string} prop
|
||||
* @param {string} tagName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isForbidden(forbidMap, prop, tagName) {
|
||||
const options = forbidMap.get(prop);
|
||||
return options && (
|
||||
typeof tagName === 'undefined'
|
||||
|| !options.disallowList
|
||||
|| options.disallowList.indexOf(tagName) !== -1
|
||||
);
|
||||
}
|
||||
|
||||
const messages = {
|
||||
propIsForbidden: 'Prop "{{prop}}" is forbidden on DOM Nodes',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow certain props on DOM Nodes',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('forbid-dom-props'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
forbid: {
|
||||
type: 'array',
|
||||
items: {
|
||||
anyOf: [{
|
||||
type: 'string',
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
propName: {
|
||||
type: 'string',
|
||||
},
|
||||
disallowedFor: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
}],
|
||||
minLength: 1,
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const forbid = new Map((configuration.forbid || DEFAULTS).map((value) => {
|
||||
const propName = typeof value === 'string' ? value : value.propName;
|
||||
return [propName, {
|
||||
disallowList: typeof value === 'string' ? null : (value.disallowedFor || null),
|
||||
message: typeof value === 'string' ? null : value.message,
|
||||
}];
|
||||
}));
|
||||
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
const tag = node.parent.name.name;
|
||||
if (!(tag && typeof tag === 'string' && tag[0] !== tag[0].toUpperCase())) {
|
||||
// This is a Component, not a DOM node, so exit.
|
||||
return;
|
||||
}
|
||||
|
||||
const prop = node.name.name;
|
||||
|
||||
if (!isForbidden(forbid, prop, tag)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const customMessage = forbid.get(prop).message;
|
||||
|
||||
report(context, customMessage || messages.propIsForbidden, !customMessage && 'propIsForbidden', {
|
||||
node,
|
||||
data: {
|
||||
prop,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare const _exports: import('eslint').Rule.RuleModule;
|
||||
export = _exports;
|
||||
//# sourceMappingURL=forbid-elements.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"forbid-elements.d.ts","sourceRoot":"","sources":["forbid-elements.js"],"names":[],"mappings":"wBAsBW,OAAO,QAAQ,EAAE,IAAI,CAAC,UAAU"}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* @fileoverview Forbid certain elements
|
||||
* @author Kenneth Chung
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const has = require('hasown');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const getText = require('../util/eslint').getText;
|
||||
const isCreateElement = require('../util/isCreateElement');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
forbiddenElement: '<{{element}}> is forbidden',
|
||||
forbiddenElement_message: '<{{element}}> is forbidden, {{message}}',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow certain elements',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('forbid-elements'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
forbid: {
|
||||
type: 'array',
|
||||
items: {
|
||||
anyOf: [
|
||||
{ type: 'string' },
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
element: { type: 'string' },
|
||||
message: { type: 'string' },
|
||||
},
|
||||
required: ['element'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const forbidConfiguration = configuration.forbid || [];
|
||||
|
||||
/** @type {Record<string, { element: string, message?: string }>} */
|
||||
const indexedForbidConfigs = {};
|
||||
|
||||
forbidConfiguration.forEach((item) => {
|
||||
if (typeof item === 'string') {
|
||||
indexedForbidConfigs[item] = { element: item };
|
||||
} else {
|
||||
indexedForbidConfigs[item.element] = item;
|
||||
}
|
||||
});
|
||||
|
||||
function reportIfForbidden(element, node) {
|
||||
if (has(indexedForbidConfigs, element)) {
|
||||
const message = indexedForbidConfigs[element].message;
|
||||
|
||||
report(
|
||||
context,
|
||||
message ? messages.forbiddenElement_message : messages.forbiddenElement,
|
||||
message ? 'forbiddenElement_message' : 'forbiddenElement',
|
||||
{
|
||||
node,
|
||||
data: {
|
||||
element,
|
||||
message,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
reportIfForbidden(getText(context, node.name), node.name);
|
||||
},
|
||||
|
||||
CallExpression(node) {
|
||||
if (!isCreateElement(context, node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const argument = node.arguments[0];
|
||||
if (!argument) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (argument.type === 'Identifier' && /^[A-Z_]/.test(argument.name)) {
|
||||
reportIfForbidden(argument.name, argument);
|
||||
} else if (argument.type === 'Literal' && /^[a-z][^.]*$/.test(String(argument.value))) {
|
||||
reportIfForbidden(argument.value, argument);
|
||||
} else if (argument.type === 'MemberExpression') {
|
||||
reportIfForbidden(getText(context, argument), argument);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare const _exports: import('eslint').Rule.RuleModule;
|
||||
export = _exports;
|
||||
//# sourceMappingURL=forbid-foreign-prop-types.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"forbid-foreign-prop-types.d.ts","sourceRoot":"","sources":["forbid-foreign-prop-types.js"],"names":[],"mappings":"wBAeW,OAAO,QAAQ,EAAE,IAAI,CAAC,UAAU"}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* @fileoverview Forbid using another component's propTypes
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const ast = require('../util/ast');
|
||||
const report = require('../util/report');
|
||||
|
||||
const messages = {
|
||||
forbiddenPropType: 'Using propTypes from another component is not safe because they may be removed in production builds',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow using another component\'s propTypes',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('forbid-foreign-prop-types'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowInPropTypes: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const config = context.options[0] || {};
|
||||
const allowInPropTypes = config.allowInPropTypes || false;
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
function findParentAssignmentExpression(node) {
|
||||
let parent = node.parent;
|
||||
|
||||
while (parent && parent.type !== 'Program') {
|
||||
if (parent.type === 'AssignmentExpression') {
|
||||
return parent;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findParentClassProperty(node) {
|
||||
let parent = node.parent;
|
||||
|
||||
while (parent && parent.type !== 'Program') {
|
||||
if (parent.type === 'ClassProperty' || parent.type === 'PropertyDefinition') {
|
||||
return parent;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isAllowedAssignment(node) {
|
||||
if (!allowInPropTypes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const assignmentExpression = findParentAssignmentExpression(node);
|
||||
|
||||
if (
|
||||
assignmentExpression
|
||||
&& assignmentExpression.left
|
||||
&& assignmentExpression.left.property
|
||||
&& assignmentExpression.left.property.name === 'propTypes'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const classProperty = findParentClassProperty(node);
|
||||
|
||||
if (
|
||||
classProperty
|
||||
&& classProperty.key
|
||||
&& classProperty.key.name === 'propTypes'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
if (
|
||||
(node.property
|
||||
&& (
|
||||
!node.computed
|
||||
&& node.property.type === 'Identifier'
|
||||
&& node.property.name === 'propTypes'
|
||||
&& !ast.isAssignmentLHS(node)
|
||||
&& !isAllowedAssignment(node)
|
||||
)) || (
|
||||
// @ts-expect-error: The JSXText type is not present in the estree type definitions
|
||||
(node.property.type === 'Literal' || node.property.type === 'JSXText')
|
||||
&& 'value' in node.property
|
||||
&& node.property.value === 'propTypes'
|
||||
&& !ast.isAssignmentLHS(node)
|
||||
&& !isAllowedAssignment(node)
|
||||
)
|
||||
) {
|
||||
report(context, messages.forbiddenPropType, 'forbiddenPropType', {
|
||||
node: node.property,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
ObjectPattern(node) {
|
||||
const propTypesNode = node.properties.find((property) => (
|
||||
property.type === 'Property'
|
||||
&& 'name' in property.key
|
||||
&& property.key.name === 'propTypes'
|
||||
));
|
||||
|
||||
if (propTypesNode) {
|
||||
report(context, messages.forbiddenPropType, 'forbiddenPropType', {
|
||||
node: propTypesNode,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare const _exports: import('eslint').Rule.RuleModule;
|
||||
export = _exports;
|
||||
//# sourceMappingURL=forbid-prop-types.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"forbid-prop-types.d.ts","sourceRoot":"","sources":["forbid-prop-types.js"],"names":[],"mappings":"wBA4BW,OAAO,QAAQ,EAAE,IAAI,CAAC,UAAU"}
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* @fileoverview Forbid certain propTypes
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const variableUtil = require('../util/variable');
|
||||
const propsUtil = require('../util/props');
|
||||
const astUtil = require('../util/ast');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const propWrapperUtil = require('../util/propWrapper');
|
||||
const report = require('../util/report');
|
||||
const getText = require('../util/eslint').getText;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const DEFAULTS = ['any', 'array', 'object'];
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
forbiddenPropType: 'Prop type "{{target}}" is forbidden',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow certain propTypes',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('forbid-prop-types'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
forbid: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
checkContextTypes: {
|
||||
type: 'boolean',
|
||||
},
|
||||
checkChildContextTypes: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: true,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const checkContextTypes = configuration.checkContextTypes || false;
|
||||
const checkChildContextTypes = configuration.checkChildContextTypes || false;
|
||||
let propTypesPackageName = null;
|
||||
let reactPackageName = null;
|
||||
let isForeignPropTypesPackage = false;
|
||||
|
||||
function isPropTypesPackage(node) {
|
||||
return (
|
||||
node.type === 'Identifier'
|
||||
&& (
|
||||
node.name === null
|
||||
|| node.name === propTypesPackageName
|
||||
|| !isForeignPropTypesPackage
|
||||
)
|
||||
) || (
|
||||
node.type === 'MemberExpression'
|
||||
&& (
|
||||
node.object.name === null
|
||||
|| node.object.name === reactPackageName
|
||||
|| !isForeignPropTypesPackage
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function isForbidden(type) {
|
||||
const forbid = configuration.forbid || DEFAULTS;
|
||||
return forbid.indexOf(type) >= 0;
|
||||
}
|
||||
|
||||
function reportIfForbidden(type, declaration, target) {
|
||||
if (isForbidden(type)) {
|
||||
report(context, messages.forbiddenPropType, 'forbiddenPropType', {
|
||||
node: declaration,
|
||||
data: {
|
||||
target,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function shouldCheckContextTypes(node) {
|
||||
if (checkContextTypes && propsUtil.isContextTypesDeclaration(node)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldCheckChildContextTypes(node) {
|
||||
if (checkChildContextTypes && propsUtil.isChildContextTypesDeclaration(node)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if propTypes declarations are forbidden
|
||||
* @param {Array} declarations The array of AST nodes being checked.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkProperties(declarations) {
|
||||
if (declarations) {
|
||||
declarations.forEach((declaration) => {
|
||||
if (declaration.type !== 'Property') {
|
||||
return;
|
||||
}
|
||||
let target;
|
||||
let value = declaration.value;
|
||||
if (
|
||||
value.type === 'MemberExpression'
|
||||
&& value.property
|
||||
&& value.property.name
|
||||
&& value.property.name === 'isRequired'
|
||||
) {
|
||||
value = value.object;
|
||||
}
|
||||
if (astUtil.isCallExpression(value)) {
|
||||
if (!isPropTypesPackage(value.callee)) {
|
||||
return;
|
||||
}
|
||||
value.arguments.forEach((arg) => {
|
||||
const name = arg.type === 'MemberExpression' ? arg.property.name : arg.name;
|
||||
reportIfForbidden(name, declaration, name);
|
||||
});
|
||||
value = value.callee;
|
||||
}
|
||||
if (!isPropTypesPackage(value)) {
|
||||
return;
|
||||
}
|
||||
if (value.property) {
|
||||
target = value.property.name;
|
||||
} else if (value.type === 'Identifier') {
|
||||
target = value.name;
|
||||
}
|
||||
reportIfForbidden(target, declaration, target);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function checkNode(node) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'ObjectExpression') {
|
||||
checkProperties(node.properties);
|
||||
} else if (node.type === 'Identifier') {
|
||||
const propTypesObject = variableUtil.findVariableByName(context, node, node.name);
|
||||
if (propTypesObject && propTypesObject.properties) {
|
||||
checkProperties(propTypesObject.properties);
|
||||
}
|
||||
} else if (astUtil.isCallExpression(node)) {
|
||||
const innerNode = node.arguments && node.arguments[0];
|
||||
if (
|
||||
propWrapperUtil.isPropWrapperFunction(context, getText(context, node.callee))
|
||||
&& innerNode
|
||||
) {
|
||||
checkNode(innerNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
if (node.source && node.source.value === 'prop-types') { // import PropType from "prop-types"
|
||||
if (node.specifiers.length > 0) {
|
||||
propTypesPackageName = node.specifiers[0].local.name;
|
||||
}
|
||||
} else if (node.source && node.source.value === 'react') { // import { PropTypes } from "react"
|
||||
if (node.specifiers.length > 0) {
|
||||
reactPackageName = node.specifiers[0].local.name; // guard against accidental anonymous `import "react"`
|
||||
}
|
||||
if (node.specifiers.length >= 1) {
|
||||
const propTypesSpecifier = node.specifiers.find((specifier) => (
|
||||
'imported' in specifier
|
||||
&& specifier.imported
|
||||
&& 'name' in specifier.imported
|
||||
&& specifier.imported.name === 'PropTypes'
|
||||
));
|
||||
if (propTypesSpecifier) {
|
||||
propTypesPackageName = propTypesSpecifier.local.name;
|
||||
}
|
||||
}
|
||||
} else { // package is not imported from "react" or "prop-types"
|
||||
// eslint-disable-next-line no-lonely-if
|
||||
if (node.specifiers.some((x) => x.local.name === 'PropTypes')) { // assert: node.specifiers.length > 1
|
||||
isForeignPropTypesPackage = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'ClassProperty, PropertyDefinition'(node) {
|
||||
if (
|
||||
!propsUtil.isPropTypesDeclaration(node)
|
||||
&& !isPropTypesPackage(node)
|
||||
&& !shouldCheckContextTypes(node)
|
||||
&& !shouldCheckChildContextTypes(node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
checkNode(node.value);
|
||||
},
|
||||
|
||||
MemberExpression(node) {
|
||||
if (
|
||||
!propsUtil.isPropTypesDeclaration(node)
|
||||
&& !isPropTypesPackage(node)
|
||||
&& !shouldCheckContextTypes(node)
|
||||
&& !shouldCheckChildContextTypes(node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkNode('right' in node.parent && node.parent.right);
|
||||
},
|
||||
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.callee.type === 'MemberExpression'
|
||||
&& node.callee.object
|
||||
&& !isPropTypesPackage(node.callee.object)
|
||||
&& !propsUtil.isPropTypesDeclaration(node.callee)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
node.arguments.length > 0
|
||||
&& (
|
||||
('name' in node.callee && node.callee.name === 'shape')
|
||||
|| astUtil.getPropertyName(node.callee) === 'shape'
|
||||
)
|
||||
) {
|
||||
checkProperties('properties' in node.arguments[0] && node.arguments[0].properties);
|
||||
}
|
||||
},
|
||||
|
||||
MethodDefinition(node) {
|
||||
if (
|
||||
!propsUtil.isPropTypesDeclaration(node)
|
||||
&& !isPropTypesPackage(node)
|
||||
&& !shouldCheckContextTypes(node)
|
||||
&& !shouldCheckChildContextTypes(node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const returnStatement = astUtil.findReturnStatement(node);
|
||||
|
||||
if (returnStatement && returnStatement.argument) {
|
||||
checkNode(returnStatement.argument);
|
||||
}
|
||||
},
|
||||
|
||||
ObjectExpression(node) {
|
||||
node.properties.forEach((property) => {
|
||||
if (!('key' in property) || !property.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!propsUtil.isPropTypesDeclaration(property)
|
||||
&& !isPropTypesPackage(property)
|
||||
&& !shouldCheckContextTypes(property)
|
||||
&& !shouldCheckChildContextTypes(property)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (property.value.type === 'ObjectExpression') {
|
||||
checkProperties(property.value.properties);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
};
|
||||
},
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare const _exports: import('eslint').Rule.RuleModule;
|
||||
export = _exports;
|
||||
//# sourceMappingURL=forward-ref-uses-ref.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"forward-ref-uses-ref.d.ts","sourceRoot":"","sources":["forward-ref-uses-ref.js"],"names":[],"mappings":"wBA2CW,OAAO,QAAQ,EAAE,IAAI,CAAC,UAAU"}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @fileoverview Require all forwardRef components include a ref parameter
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const isParenthesized = require('../util/ast').isParenthesized;
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
const getMessageData = require('../util/message');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node
|
||||
* @returns {boolean} If the node represents the identifier `forwardRef`.
|
||||
*/
|
||||
function isForwardRefIdentifier(node) {
|
||||
return node.type === 'Identifier' && node.name === 'forwardRef';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node
|
||||
* @returns {boolean} If the node represents a function call `forwardRef()` or `React.forwardRef()`.
|
||||
*/
|
||||
function isForwardRefCall(node) {
|
||||
return (
|
||||
node.type === 'CallExpression'
|
||||
&& (
|
||||
isForwardRefIdentifier(node.callee)
|
||||
|| (node.callee.type === 'MemberExpression' && isForwardRefIdentifier(node.callee.property))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const messages = {
|
||||
missingRefParameter: 'forwardRef is used with this component but no ref parameter is set',
|
||||
addRefParameter: 'Add a ref parameter',
|
||||
removeForwardRef: 'Remove forwardRef wrapper',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Require all forwardRef components include a ref parameter',
|
||||
category: 'Possible Errors',
|
||||
recommended: false,
|
||||
url: docsUrl('forward-ref-uses-ref'),
|
||||
},
|
||||
messages,
|
||||
schema: [],
|
||||
type: 'suggestion',
|
||||
hasSuggestions: true,
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
return {
|
||||
'FunctionExpression, ArrowFunctionExpression'(node) {
|
||||
if (!isForwardRefCall(node.parent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.params.length === 1) {
|
||||
report(context, messages.missingRefParameter, 'missingRefParameter', {
|
||||
node,
|
||||
suggest: [
|
||||
Object.assign(
|
||||
getMessageData('addRefParameter', messages.addRefParameter),
|
||||
{
|
||||
fix(fixer) {
|
||||
const param = node.params[0];
|
||||
// If using shorthand arrow function syntax, add parentheses around the new parameter pair
|
||||
const shouldAddParentheses = node.type === 'ArrowFunctionExpression' && !isParenthesized(context, param);
|
||||
return [].concat(
|
||||
shouldAddParentheses ? fixer.insertTextBefore(param, '(') : [],
|
||||
fixer.insertTextAfter(param, `, ref${shouldAddParentheses ? ')' : ''}`)
|
||||
);
|
||||
},
|
||||
}
|
||||
),
|
||||
Object.assign(
|
||||
getMessageData('removeForwardRef', messages.removeForwardRef),
|
||||
{
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(node.parent, sourceCode.getText(node));
|
||||
},
|
||||
}
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare const _exports: import('eslint').Rule.RuleModule;
|
||||
export = _exports;
|
||||
//# sourceMappingURL=function-component-definition.d.ts.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"function-component-definition.d.ts","sourceRoot":"","sources":["function-component-definition.js"],"names":[],"mappings":"wBAqHW,OAAO,QAAQ,EAAE,IAAI,CAAC,UAAU"}
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* @fileoverview Standardize the way function component get defined
|
||||
* @author Stefan Wullems
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const arrayIncludes = require('array-includes');
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const reportC = require('../util/report');
|
||||
const getText = require('../util/eslint').getText;
|
||||
const propsUtil = require('../util/props');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
function buildFunction(template, parts) {
|
||||
return Object.keys(parts).reduce(
|
||||
(acc, key) => acc.replace(`{${key}}`, () => parts[key] || ''),
|
||||
template
|
||||
);
|
||||
}
|
||||
|
||||
const NAMED_FUNCTION_TEMPLATES = {
|
||||
'function-declaration': 'function {name}{typeParams}({params}){returnType} {body}',
|
||||
'arrow-function': '{varType} {name}{typeAnnotation} = {typeParams}({params}){returnType} => {body}',
|
||||
'function-expression': '{varType} {name}{typeAnnotation} = function{typeParams}({params}){returnType} {body}',
|
||||
};
|
||||
|
||||
const UNNAMED_FUNCTION_TEMPLATES = {
|
||||
'function-expression': 'function{typeParams}({params}){returnType} {body}',
|
||||
'arrow-function': '{typeParams}({params}){returnType} => {body}',
|
||||
};
|
||||
|
||||
function hasOneUnconstrainedTypeParam(node) {
|
||||
const nodeTypeArguments = propsUtil.getTypeArguments(node);
|
||||
|
||||
return nodeTypeArguments
|
||||
&& nodeTypeArguments.params
|
||||
&& nodeTypeArguments.params.length === 1
|
||||
&& !nodeTypeArguments.params[0].constraint;
|
||||
}
|
||||
|
||||
function hasName(node) {
|
||||
return (
|
||||
node.type === 'FunctionDeclaration'
|
||||
|| node.parent.type === 'VariableDeclarator'
|
||||
);
|
||||
}
|
||||
|
||||
function getNodeText(prop, source) {
|
||||
if (!prop) return null;
|
||||
return source.slice(prop.range[0], prop.range[1]);
|
||||
}
|
||||
|
||||
function getName(node) {
|
||||
if (node.type === 'FunctionDeclaration') {
|
||||
return node.id.name;
|
||||
}
|
||||
|
||||
if (
|
||||
node.type === 'ArrowFunctionExpression'
|
||||
|| node.type === 'FunctionExpression'
|
||||
) {
|
||||
return hasName(node) && node.parent.id.name;
|
||||
}
|
||||
}
|
||||
|
||||
function getParams(node, source) {
|
||||
if (node.params.length === 0) return null;
|
||||
return source.slice(
|
||||
node.params[0].range[0],
|
||||
node.params[node.params.length - 1].range[1]
|
||||
);
|
||||
}
|
||||
|
||||
function getBody(node, source) {
|
||||
const range = node.body.range;
|
||||
|
||||
if (node.body.type !== 'BlockStatement') {
|
||||
return ['{', ` return ${source.slice(range[0], range[1])}`, '}'].join('\n');
|
||||
}
|
||||
|
||||
return source.slice(range[0], range[1]);
|
||||
}
|
||||
|
||||
function getTypeAnnotation(node, source) {
|
||||
if (!hasName(node) || node.type === 'FunctionDeclaration') return;
|
||||
|
||||
if (
|
||||
node.type === 'ArrowFunctionExpression'
|
||||
|| node.type === 'FunctionExpression'
|
||||
) {
|
||||
return getNodeText(node.parent.id.typeAnnotation, source);
|
||||
}
|
||||
}
|
||||
|
||||
function isUnfixableBecauseOfExport(node) {
|
||||
return (
|
||||
node.type === 'FunctionDeclaration'
|
||||
&& node.parent
|
||||
&& node.parent.type === 'ExportDefaultDeclaration'
|
||||
);
|
||||
}
|
||||
|
||||
function isFunctionExpressionWithName(node) {
|
||||
return node.type === 'FunctionExpression' && node.id && node.id.name;
|
||||
}
|
||||
|
||||
const messages = {
|
||||
'function-declaration': 'Function component is not a function declaration',
|
||||
'function-expression': 'Function component is not a function expression',
|
||||
'arrow-function': 'Function component is not an arrow function',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce a specific function type for function components',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('function-component-definition'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
namedComponents: {
|
||||
anyOf: [
|
||||
{
|
||||
enum: [
|
||||
'function-declaration',
|
||||
'arrow-function',
|
||||
'function-expression',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'function-declaration',
|
||||
'arrow-function',
|
||||
'function-expression',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
unnamedComponents: {
|
||||
anyOf: [
|
||||
{ enum: ['arrow-function', 'function-expression'] },
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
enum: ['arrow-function', 'function-expression'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components) => {
|
||||
const configuration = context.options[0] || {};
|
||||
let fileVarType = 'var';
|
||||
|
||||
const namedConfig = [].concat(
|
||||
configuration.namedComponents || 'function-declaration'
|
||||
);
|
||||
const unnamedConfig = [].concat(
|
||||
configuration.unnamedComponents || 'function-expression'
|
||||
);
|
||||
|
||||
function getFixer(node, options) {
|
||||
const source = getText(context);
|
||||
|
||||
const typeAnnotation = getTypeAnnotation(node, source);
|
||||
|
||||
if (options.type === 'function-declaration' && typeAnnotation) {
|
||||
return;
|
||||
}
|
||||
if (options.type === 'arrow-function' && hasOneUnconstrainedTypeParam(node)) {
|
||||
return;
|
||||
}
|
||||
if (isUnfixableBecauseOfExport(node)) return;
|
||||
if (isFunctionExpressionWithName(node)) return;
|
||||
let varType = fileVarType;
|
||||
if (
|
||||
(node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression')
|
||||
&& node.parent.type === 'VariableDeclarator'
|
||||
) {
|
||||
varType = node.parent.parent.kind;
|
||||
}
|
||||
|
||||
const nodeTypeArguments = propsUtil.getTypeArguments(node);
|
||||
return (fixer) => fixer.replaceTextRange(
|
||||
options.range,
|
||||
buildFunction(options.template, {
|
||||
typeAnnotation,
|
||||
typeParams: getNodeText(nodeTypeArguments, source),
|
||||
params: getParams(node, source),
|
||||
returnType: getNodeText(node.returnType, source),
|
||||
body: getBody(node, source),
|
||||
name: getName(node),
|
||||
varType,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function report(node, options) {
|
||||
reportC(context, messages[options.messageId], options.messageId, {
|
||||
node,
|
||||
fix: getFixer(node, options.fixerOptions),
|
||||
});
|
||||
}
|
||||
|
||||
function validate(node, functionType) {
|
||||
if (!components.get(node)) return;
|
||||
|
||||
if (node.parent && node.parent.type === 'Property') return;
|
||||
|
||||
if (hasName(node) && !arrayIncludes(namedConfig, functionType)) {
|
||||
report(node, {
|
||||
messageId: namedConfig[0],
|
||||
fixerOptions: {
|
||||
type: namedConfig[0],
|
||||
template: NAMED_FUNCTION_TEMPLATES[namedConfig[0]],
|
||||
range:
|
||||
node.type === 'FunctionDeclaration'
|
||||
? node.range
|
||||
: node.parent.parent.range,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!hasName(node) && !arrayIncludes(unnamedConfig, functionType)) {
|
||||
report(node, {
|
||||
messageId: unnamedConfig[0],
|
||||
fixerOptions: {
|
||||
type: unnamedConfig[0],
|
||||
template: UNNAMED_FUNCTION_TEMPLATES[unnamedConfig[0]],
|
||||
range: node.range,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
const validatePairs = [];
|
||||
let hasES6OrJsx = false;
|
||||
return {
|
||||
FunctionDeclaration(node) {
|
||||
validatePairs.push([node, 'function-declaration']);
|
||||
},
|
||||
ArrowFunctionExpression(node) {
|
||||
validatePairs.push([node, 'arrow-function']);
|
||||
},
|
||||
FunctionExpression(node) {
|
||||
validatePairs.push([node, 'function-expression']);
|
||||
},
|
||||
VariableDeclaration(node) {
|
||||
hasES6OrJsx = hasES6OrJsx || node.kind === 'const' || node.kind === 'let';
|
||||
},
|
||||
'Program:exit'() {
|
||||
if (hasES6OrJsx) fileVarType = 'const';
|
||||
validatePairs.forEach((pair) => validate(pair[0], pair[1]));
|
||||
},
|
||||
'ImportDeclaration, ExportNamedDeclaration, ExportDefaultDeclaration, ExportAllDeclaration, ExportSpecifier, ExportDefaultSpecifier, JSXElement, TSExportAssignment, TSImportEqualsDeclaration'() {
|
||||
hasES6OrJsx = true;
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare const _exports: import('eslint').Rule.RuleModule;
|
||||
export = _exports;
|
||||
//# sourceMappingURL=hook-use-state.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"hook-use-state.d.ts","sourceRoot":"","sources":["hook-use-state.js"],"names":[],"mappings":"wBA4BW,OAAO,QAAQ,EAAE,IAAI,CAAC,UAAU"}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* @fileoverview Ensure symmetric naming of useState hook value and setter variables
|
||||
* @author Duncan Beevers
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
const getMessageData = require('../util/message');
|
||||
const getText = require('../util/eslint').getText;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
function isNodeDestructuring(node) {
|
||||
return node && (node.type === 'ArrayPattern' || node.type === 'ObjectPattern');
|
||||
}
|
||||
|
||||
const messages = {
|
||||
useStateErrorMessage: 'useState call is not destructured into value + setter pair',
|
||||
useStateErrorMessageOrAddOption: 'useState call is not destructured into value + setter pair (you can allow destructuring by enabling "allowDestructuredState" option)',
|
||||
suggestPair: 'Destructure useState call into value + setter pair',
|
||||
suggestMemo: 'Replace useState call with useMemo',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Ensure destructuring and symmetric naming of useState hook value and setter variables',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('hook-use-state'),
|
||||
},
|
||||
messages,
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowDestructuredState: {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
type: 'suggestion',
|
||||
hasSuggestions: true,
|
||||
},
|
||||
|
||||
create: Components.detect((context, components, util) => {
|
||||
const configuration = context.options[0] || {};
|
||||
const allowDestructuredState = configuration.allowDestructuredState || false;
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
const isImmediateReturn = node.parent
|
||||
&& node.parent.type === 'ReturnStatement';
|
||||
|
||||
if (isImmediateReturn || !util.isReactHookCall(node, ['useState'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isDestructuringDeclarator = node.parent
|
||||
&& node.parent.type === 'VariableDeclarator'
|
||||
&& node.parent.id.type === 'ArrayPattern';
|
||||
|
||||
if (!isDestructuringDeclarator) {
|
||||
report(
|
||||
context,
|
||||
messages.useStateErrorMessage,
|
||||
'useStateErrorMessage',
|
||||
{
|
||||
node,
|
||||
suggest: false,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const variableNodes = node.parent.id.elements;
|
||||
const valueVariable = variableNodes[0];
|
||||
const setterVariable = variableNodes[1];
|
||||
const isOnlyValueDestructuring = isNodeDestructuring(valueVariable) && !isNodeDestructuring(setterVariable);
|
||||
|
||||
if (allowDestructuredState && isOnlyValueDestructuring) {
|
||||
return;
|
||||
}
|
||||
|
||||
const valueVariableName = valueVariable
|
||||
? valueVariable.name
|
||||
: undefined;
|
||||
|
||||
const setterVariableName = setterVariable
|
||||
? setterVariable.name
|
||||
: undefined;
|
||||
|
||||
const caseCandidateMatch = valueVariableName ? valueVariableName.match(/(^[a-z]+)(.*)/) : undefined;
|
||||
const upperCaseCandidatePrefix = caseCandidateMatch ? caseCandidateMatch[1] : undefined;
|
||||
const caseCandidateSuffix = caseCandidateMatch ? caseCandidateMatch[2] : undefined;
|
||||
const expectedSetterVariableNames = upperCaseCandidatePrefix ? [
|
||||
`set${upperCaseCandidatePrefix.charAt(0).toUpperCase()}${upperCaseCandidatePrefix.slice(1)}${caseCandidateSuffix}`,
|
||||
`set${upperCaseCandidatePrefix.toUpperCase()}${caseCandidateSuffix}`,
|
||||
] : [];
|
||||
|
||||
const isSymmetricGetterSetterPair = valueVariable
|
||||
&& setterVariable
|
||||
&& expectedSetterVariableNames.indexOf(setterVariableName) !== -1
|
||||
&& variableNodes.length === 2;
|
||||
|
||||
if (!isSymmetricGetterSetterPair) {
|
||||
const suggestions = [
|
||||
Object.assign(
|
||||
getMessageData('suggestPair', messages.suggestPair),
|
||||
{
|
||||
fix(fixer) {
|
||||
if (expectedSetterVariableNames.length > 0) {
|
||||
return fixer.replaceTextRange(
|
||||
node.parent.id.range,
|
||||
`[${valueVariableName}, ${expectedSetterVariableNames[0]}]`
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
];
|
||||
|
||||
const defaultReactImports = components.getDefaultReactImports();
|
||||
const defaultReactImportSpecifier = defaultReactImports
|
||||
? defaultReactImports[0]
|
||||
: undefined;
|
||||
|
||||
const defaultReactImportName = defaultReactImportSpecifier
|
||||
? defaultReactImportSpecifier.local.name
|
||||
: undefined;
|
||||
|
||||
const namedReactImports = components.getNamedReactImports();
|
||||
const useStateReactImportSpecifier = namedReactImports
|
||||
? namedReactImports.find((specifier) => specifier.imported.name === 'useState')
|
||||
: undefined;
|
||||
|
||||
const isSingleGetter = valueVariable && variableNodes.length === 1;
|
||||
const isUseStateCalledWithSingleArgument = node.arguments.length === 1;
|
||||
if (isSingleGetter && isUseStateCalledWithSingleArgument) {
|
||||
const useMemoReactImportSpecifier = namedReactImports
|
||||
&& namedReactImports.find((specifier) => specifier.imported.name === 'useMemo');
|
||||
|
||||
let useMemoCode;
|
||||
if (useMemoReactImportSpecifier) {
|
||||
useMemoCode = useMemoReactImportSpecifier.local.name;
|
||||
} else if (defaultReactImportName) {
|
||||
useMemoCode = `${defaultReactImportName}.useMemo`;
|
||||
} else {
|
||||
useMemoCode = 'useMemo';
|
||||
}
|
||||
|
||||
suggestions.unshift(Object.assign(
|
||||
getMessageData('suggestMemo', messages.suggestMemo),
|
||||
{
|
||||
fix: (fixer) => [
|
||||
// Add useMemo import, if necessary
|
||||
useStateReactImportSpecifier
|
||||
&& (!useMemoReactImportSpecifier || defaultReactImportName)
|
||||
&& fixer.insertTextAfter(useStateReactImportSpecifier, ', useMemo'),
|
||||
// Convert single-value destructure to simple assignment
|
||||
fixer.replaceTextRange(node.parent.id.range, valueVariableName),
|
||||
// Convert useState call to useMemo + arrow function + dependency array
|
||||
fixer.replaceTextRange(
|
||||
node.range,
|
||||
`${useMemoCode}(() => ${getText(context, node.arguments[0])}, [])`
|
||||
),
|
||||
].filter(Boolean),
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
if (isOnlyValueDestructuring) {
|
||||
report(
|
||||
context,
|
||||
messages.useStateErrorMessageOrAddOption,
|
||||
'useStateErrorMessageOrAddOption',
|
||||
{
|
||||
node: node.parent.id,
|
||||
suggest: false,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
report(
|
||||
context,
|
||||
messages.useStateErrorMessage,
|
||||
'useStateErrorMessage',
|
||||
{
|
||||
node: node.parent.id,
|
||||
suggest: suggestions,
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user