paradiego

This commit is contained in:
2024-09-18 13:34:19 -03:00
commit 3f0e204289
12510 changed files with 1486101 additions and 0 deletions

22
node_modules/eslint-plugin-react/LICENSE generated vendored Normal file
View File

@@ -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.

422
node_modules/eslint-plugin-react/README.md generated vendored Normal file
View File

@@ -0,0 +1,422 @@
# `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/user-guide/configuring/configuration-files#adding-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
];
```
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
node_modules/eslint-plugin-react/configs/all.js generated vendored Normal file
View File

@@ -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 });

View File

@@ -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 });

View File

@@ -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 });

109
node_modules/eslint-plugin-react/index.js generated vendored Normal file
View File

@@ -0,0 +1,109 @@
'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, 2>}
*/
function configureAsError(rules) {
return fromEntries(Object.keys(rules).map((key) => [`react/${key}`, 2]));
}
const activeRules = filterRules(allRules, (rule) => !rule.meta.deprecated);
const activeRulesConfig = configureAsError(activeRules);
const deprecatedRules = filterRules(allRules, (rule) => rule.meta.deprecated);
// for legacy config system
const plugins = [
'react',
];
const plugin = {
deprecatedRules,
rules: allRules,
configs: {
recommended: {
plugins,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
rules: {
'react/display-name': 2,
'react/jsx-key': 2,
'react/jsx-no-comment-textnodes': 2,
'react/jsx-no-duplicate-props': 2,
'react/jsx-no-target-blank': 2,
'react/jsx-no-undef': 2,
'react/jsx-uses-react': 2,
'react/jsx-uses-vars': 2,
'react/no-children-prop': 2,
'react/no-danger-with-children': 2,
'react/no-deprecated': 2,
'react/no-direct-mutation-state': 2,
'react/no-find-dom-node': 2,
'react/no-is-mounted': 2,
'react/no-render-return-value': 2,
'react/no-string-refs': 2,
'react/no-unescaped-entities': 2,
'react/no-unknown-property': 2,
'react/no-unsafe': 0,
'react/prop-types': 2,
'react/react-in-jsx-scope': 2,
'react/require-render-return': 2,
},
},
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': 0,
'react/jsx-uses-react': 0,
},
},
},
};
plugin.configs.flat = {
recommended: {
plugins: { react: plugin },
rules: plugin.configs.recommended.rules,
languageOptions: { parserOptions: plugin.configs.recommended.parserOptions },
},
all: {
plugins: { react: plugin },
rules: plugin.configs.all.rules,
languageOptions: { parserOptions: plugin.configs.all.parserOptions },
},
'jsx-runtime': {
plugins: { react: plugin },
rules: plugin.configs['jsx-runtime'].rules,
languageOptions: { parserOptions: plugin.configs['jsx-runtime'].parserOptions },
},
};
module.exports = plugin;

View File

@@ -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}}` doesnt 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();
},
};
}),
};

View File

@@ -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);
},
};
},
};

View File

@@ -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);
},
};
},
};

View File

@@ -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 || {}
);
});
},
};
}),
};

View File

@@ -0,0 +1,285 @@
/**
* @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,
},
});
}
}
return {
FunctionDeclaration: handleStatelessComponent,
ArrowFunctionExpression: handleStatelessComponent,
FunctionExpression: handleStatelessComponent,
'FunctionDeclaration:exit': handleStatelessComponentExit,
'ArrowFunctionExpression:exit': handleStatelessComponentExit,
'FunctionExpression:exit': handleStatelessComponentExit,
MemberExpression(node) {
let scope = getScope(context, node);
let SFCComponent = components.get(scope.block);
while (!SFCComponent && scope.upper && scope.upper !== scope) {
SFCComponent = components.get(scope.upper.block);
scope = scope.upper;
}
if (SFCComponent) {
handleSFCUsage(node);
}
const classComponent = utils.getParentComponent(node);
if (classComponent) {
handleClassUsage(node);
}
},
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),
];
},
});
}
},
};
}),
};

View File

@@ -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)
);
}
},
};
}),
};

View File

@@ -0,0 +1,180 @@
/**
* @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' },
},
message: { type: 'string' },
},
additionalProperties: false,
},
{
type: 'object',
properties: {
propName: { type: 'string' },
disallowedFor: {
type: 'array',
uniqueItems: true,
minItems: 1,
items: { type: 'string' },
},
message: { type: 'string' },
},
required: ['disallowedFor'],
additionalProperties: false,
},
{
type: 'object',
properties: {
propNamePattern: { type: 'string' },
allowedFor: {
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' },
},
message: { type: 'string' },
},
required: ['disallowedFor'],
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: typeof value === 'string' ? [] : (value.allowedFor || []),
disallowList: typeof value === 'string' ? [] : (value.disallowedFor || []),
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;
}
// disallowList should have a least one item (schema configuration)
const isTagForbidden = options.disallowList.length > 0
? options.disallowList.indexOf(tagName) !== -1
: options.allowList.indexOf(tagName) === -1;
// 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,
},
});
},
};
},
};

View File

@@ -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,
},
});
},
};
},
};

View File

@@ -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);
}
},
};
},
};

View File

@@ -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,
});
}
},
};
},
};

View File

@@ -0,0 +1,297 @@
/**
* @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
&& 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);
}
});
},
};
},
};

View File

@@ -0,0 +1,99 @@
/**
* @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',
};
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));
},
}
),
],
});
}
},
};
},
};

View File

@@ -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;
},
};
}),
};

View File

@@ -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,
}
);
}
},
};
}),
};

View File

@@ -0,0 +1,143 @@
/**
* @fileoverview TBD
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const isCreateElement = require('../util/isCreateElement');
const report = require('../util/report');
const messages = {
attributeMissing: 'An iframe element is missing a sandbox attribute',
invalidValue: 'An iframe element defines a sandbox attribute with invalid value "{{ value }}"',
invalidCombination: 'An iframe element defines a sandbox attribute with both allow-scripts and allow-same-origin which is invalid',
};
const ALLOWED_VALUES = [
// From https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox
'',
'allow-downloads-without-user-activation',
'allow-downloads',
'allow-forms',
'allow-modals',
'allow-orientation-lock',
'allow-pointer-lock',
'allow-popups',
'allow-popups-to-escape-sandbox',
'allow-presentation',
'allow-same-origin',
'allow-scripts',
'allow-storage-access-by-user-activation',
'allow-top-navigation',
'allow-top-navigation-by-user-activation',
];
function validateSandboxAttribute(context, node, attribute) {
if (typeof attribute !== 'string') {
// Only string literals are supported for now
return;
}
const values = attribute.split(' ');
let allowScripts = false;
let allowSameOrigin = false;
values.forEach((attributeValue) => {
const trimmedAttributeValue = attributeValue.trim();
if (ALLOWED_VALUES.indexOf(trimmedAttributeValue) === -1) {
report(context, messages.invalidValue, 'invalidValue', {
node,
data: {
value: trimmedAttributeValue,
},
});
}
if (trimmedAttributeValue === 'allow-scripts') {
allowScripts = true;
}
if (trimmedAttributeValue === 'allow-same-origin') {
allowSameOrigin = true;
}
});
if (allowScripts && allowSameOrigin) {
report(context, messages.invalidCombination, 'invalidCombination', {
node,
});
}
}
function checkAttributes(context, node) {
let sandboxAttributeFound = false;
node.attributes.forEach((attribute) => {
if (attribute.type === 'JSXAttribute'
&& attribute.name
&& attribute.name.type === 'JSXIdentifier'
&& attribute.name.name === 'sandbox'
) {
sandboxAttributeFound = true;
if (
attribute.value
&& attribute.value.type === 'Literal'
&& attribute.value.value
) {
validateSandboxAttribute(context, node, attribute.value.value);
}
}
});
if (!sandboxAttributeFound) {
report(context, messages.attributeMissing, 'attributeMissing', {
node,
});
}
}
function checkProps(context, node) {
let sandboxAttributeFound = false;
if (node.arguments.length > 1) {
const props = node.arguments[1];
const sandboxProp = props.properties && props.properties.find((x) => x.type === 'Property' && x.key.name === 'sandbox');
if (sandboxProp) {
sandboxAttributeFound = true;
if (sandboxProp.value && sandboxProp.value.type === 'Literal' && sandboxProp.value.value) {
validateSandboxAttribute(context, node, sandboxProp.value.value);
}
}
}
if (!sandboxAttributeFound) {
report(context, messages.attributeMissing, 'attributeMissing', {
node,
});
}
}
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce sandbox attribute on iframe elements',
category: 'Best Practices',
recommended: false,
url: docsUrl('iframe-missing-sandbox'),
},
schema: [],
messages,
},
create(context) {
return {
'JSXOpeningElement[name.name="iframe"]'(node) {
checkAttributes(context, node);
},
CallExpression(node) {
if (isCreateElement(context, node) && node.arguments && node.arguments.length > 0) {
const tag = node.arguments[0];
if (tag.type === 'Literal' && tag.value === 'iframe') {
checkProps(context, node);
}
}
},
};
},
};

110
node_modules/eslint-plugin-react/lib/rules/index.js generated vendored Normal file
View File

@@ -0,0 +1,110 @@
'use strict';
/* eslint global-require: 0 */
/** @satisfies {Record<string, import('eslint').Rule.RuleModule>} */
module.exports = {
'boolean-prop-naming': require('./boolean-prop-naming'),
'button-has-type': require('./button-has-type'),
'checked-requires-onchange-or-readonly': require('./checked-requires-onchange-or-readonly'),
'default-props-match-prop-types': require('./default-props-match-prop-types'),
'destructuring-assignment': require('./destructuring-assignment'),
'display-name': require('./display-name'),
'forbid-component-props': require('./forbid-component-props'),
'forbid-dom-props': require('./forbid-dom-props'),
'forbid-elements': require('./forbid-elements'),
'forbid-foreign-prop-types': require('./forbid-foreign-prop-types'),
'forbid-prop-types': require('./forbid-prop-types'),
'forward-ref-uses-ref': require('./forward-ref-uses-ref'),
'function-component-definition': require('./function-component-definition'),
'hook-use-state': require('./hook-use-state'),
'iframe-missing-sandbox': require('./iframe-missing-sandbox'),
'jsx-boolean-value': require('./jsx-boolean-value'),
'jsx-child-element-spacing': require('./jsx-child-element-spacing'),
'jsx-closing-bracket-location': require('./jsx-closing-bracket-location'),
'jsx-closing-tag-location': require('./jsx-closing-tag-location'),
'jsx-curly-spacing': require('./jsx-curly-spacing'),
'jsx-curly-newline': require('./jsx-curly-newline'),
'jsx-equals-spacing': require('./jsx-equals-spacing'),
'jsx-filename-extension': require('./jsx-filename-extension'),
'jsx-first-prop-new-line': require('./jsx-first-prop-new-line'),
'jsx-handler-names': require('./jsx-handler-names'),
'jsx-indent': require('./jsx-indent'),
'jsx-indent-props': require('./jsx-indent-props'),
'jsx-key': require('./jsx-key'),
'jsx-max-depth': require('./jsx-max-depth'),
'jsx-max-props-per-line': require('./jsx-max-props-per-line'),
'jsx-newline': require('./jsx-newline'),
'jsx-no-bind': require('./jsx-no-bind'),
'jsx-no-comment-textnodes': require('./jsx-no-comment-textnodes'),
'jsx-no-constructed-context-values': require('./jsx-no-constructed-context-values'),
'jsx-no-duplicate-props': require('./jsx-no-duplicate-props'),
'jsx-no-leaked-render': require('./jsx-no-leaked-render'),
'jsx-no-literals': require('./jsx-no-literals'),
'jsx-no-script-url': require('./jsx-no-script-url'),
'jsx-no-target-blank': require('./jsx-no-target-blank'),
'jsx-no-useless-fragment': require('./jsx-no-useless-fragment'),
'jsx-one-expression-per-line': require('./jsx-one-expression-per-line'),
'jsx-no-undef': require('./jsx-no-undef'),
'jsx-curly-brace-presence': require('./jsx-curly-brace-presence'),
'jsx-pascal-case': require('./jsx-pascal-case'),
'jsx-fragments': require('./jsx-fragments'),
'jsx-props-no-multi-spaces': require('./jsx-props-no-multi-spaces'),
'jsx-props-no-spreading': require('./jsx-props-no-spreading'),
'jsx-props-no-spread-multi': require('./jsx-props-no-spread-multi'),
'jsx-sort-default-props': require('./jsx-sort-default-props'),
'jsx-sort-props': require('./jsx-sort-props'),
'jsx-space-before-closing': require('./jsx-space-before-closing'),
'jsx-tag-spacing': require('./jsx-tag-spacing'),
'jsx-uses-react': require('./jsx-uses-react'),
'jsx-uses-vars': require('./jsx-uses-vars'),
'jsx-wrap-multilines': require('./jsx-wrap-multilines'),
'no-invalid-html-attribute': require('./no-invalid-html-attribute'),
'no-access-state-in-setstate': require('./no-access-state-in-setstate'),
'no-adjacent-inline-elements': require('./no-adjacent-inline-elements'),
'no-array-index-key': require('./no-array-index-key'),
'no-arrow-function-lifecycle': require('./no-arrow-function-lifecycle'),
'no-children-prop': require('./no-children-prop'),
'no-danger': require('./no-danger'),
'no-danger-with-children': require('./no-danger-with-children'),
'no-deprecated': require('./no-deprecated'),
'no-did-mount-set-state': require('./no-did-mount-set-state'),
'no-did-update-set-state': require('./no-did-update-set-state'),
'no-direct-mutation-state': require('./no-direct-mutation-state'),
'no-find-dom-node': require('./no-find-dom-node'),
'no-is-mounted': require('./no-is-mounted'),
'no-multi-comp': require('./no-multi-comp'),
'no-namespace': require('./no-namespace'),
'no-set-state': require('./no-set-state'),
'no-string-refs': require('./no-string-refs'),
'no-redundant-should-component-update': require('./no-redundant-should-component-update'),
'no-render-return-value': require('./no-render-return-value'),
'no-this-in-sfc': require('./no-this-in-sfc'),
'no-typos': require('./no-typos'),
'no-unescaped-entities': require('./no-unescaped-entities'),
'no-unknown-property': require('./no-unknown-property'),
'no-unsafe': require('./no-unsafe'),
'no-unstable-nested-components': require('./no-unstable-nested-components'),
'no-unused-class-component-methods': require('./no-unused-class-component-methods'),
'no-unused-prop-types': require('./no-unused-prop-types'),
'no-unused-state': require('./no-unused-state'),
'no-object-type-as-default-prop': require('./no-object-type-as-default-prop'),
'no-will-update-set-state': require('./no-will-update-set-state'),
'prefer-es6-class': require('./prefer-es6-class'),
'prefer-exact-props': require('./prefer-exact-props'),
'prefer-read-only-props': require('./prefer-read-only-props'),
'prefer-stateless-function': require('./prefer-stateless-function'),
'prop-types': require('./prop-types'),
'react-in-jsx-scope': require('./react-in-jsx-scope'),
'require-default-props': require('./require-default-props'),
'require-optimization': require('./require-optimization'),
'require-render-return': require('./require-render-return'),
'self-closing-comp': require('./self-closing-comp'),
'sort-comp': require('./sort-comp'),
'sort-default-props': require('./sort-default-props'),
'sort-prop-types': require('./sort-prop-types'),
'state-in-constructor': require('./state-in-constructor'),
'static-property-placement': require('./static-property-placement'),
'style-prop-object': require('./style-prop-object'),
'void-dom-elements-no-children': require('./void-dom-elements-no-children'),
};

View File

@@ -0,0 +1,169 @@
/**
* @fileoverview Enforce boolean attributes notation in JSX
* @author Yannick Croissant
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const exceptionsSchema = {
type: 'array',
items: { type: 'string', minLength: 1 },
uniqueItems: true,
};
const ALWAYS = 'always';
const NEVER = 'never';
/**
* @param {string} configuration
* @param {Set<string>} exceptions
* @param {string} propName
* @returns {boolean} propName
*/
function isAlways(configuration, exceptions, propName) {
const isException = exceptions.has(propName);
if (configuration === ALWAYS) {
return !isException;
}
return isException;
}
/**
* @param {string} configuration
* @param {Set<string>} exceptions
* @param {string} propName
* @returns {boolean} propName
*/
function isNever(configuration, exceptions, propName) {
const isException = exceptions.has(propName);
if (configuration === NEVER) {
return !isException;
}
return isException;
}
const messages = {
omitBoolean: 'Value must be omitted for boolean attribute `{{propName}}`',
setBoolean: 'Value must be set for boolean attribute `{{propName}}`',
omitPropAndBoolean: 'Value must be omitted for `false` attribute: `{{propName}}`',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce boolean attributes notation in JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-boolean-value'),
},
fixable: 'code',
messages,
schema: {
anyOf: [{
type: 'array',
items: [{ enum: [ALWAYS, NEVER] }],
additionalItems: false,
}, {
type: 'array',
items: [{
enum: [ALWAYS],
}, {
type: 'object',
additionalProperties: false,
properties: {
[NEVER]: exceptionsSchema,
assumeUndefinedIsFalse: {
type: 'boolean',
},
},
}],
additionalItems: false,
}, {
type: 'array',
items: [{
enum: [NEVER],
}, {
type: 'object',
additionalProperties: false,
properties: {
[ALWAYS]: exceptionsSchema,
assumeUndefinedIsFalse: {
type: 'boolean',
},
},
}],
additionalItems: false,
}],
},
},
create(context) {
const configuration = context.options[0] || NEVER;
const configObject = context.options[1] || {};
const exceptions = new Set((configuration === ALWAYS ? configObject[NEVER] : configObject[ALWAYS]) || []);
return {
JSXAttribute(node) {
const propName = node.name && node.name.name;
const value = node.value;
if (
isAlways(configuration, exceptions, propName)
&& value === null
) {
const messageId = 'setBoolean';
const data = { propName };
report(context, messages[messageId], messageId, {
node,
data,
fix(fixer) {
return fixer.insertTextAfter(node, '={true}');
},
});
}
if (
isNever(configuration, exceptions, propName)
&& value
&& value.type === 'JSXExpressionContainer'
&& value.expression.value === true
) {
const messageId = 'omitBoolean';
const data = { propName };
report(context, messages[messageId], messageId, {
node,
data,
fix(fixer) {
return fixer.removeRange([node.name.range[1], value.range[1]]);
},
});
}
if (
isNever(configuration, exceptions, propName)
&& configObject.assumeUndefinedIsFalse
&& value
&& value.type === 'JSXExpressionContainer'
&& value.expression.value === false
) {
const messageId = 'omitPropAndBoolean';
const data = { propName };
report(context, messages[messageId], messageId, {
node,
data,
fix(fixer) {
return fixer.removeRange([node.name.range[0], value.range[1]]);
},
});
}
},
};
},
};

View File

@@ -0,0 +1,117 @@
'use strict';
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
// This list is taken from https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements
// Note: 'br' is not included because whitespace around br tags is inconsequential to the rendered output
const INLINE_ELEMENTS = new Set([
'a',
'abbr',
'acronym',
'b',
'bdo',
'big',
'button',
'cite',
'code',
'dfn',
'em',
'i',
'img',
'input',
'kbd',
'label',
'map',
'object',
'q',
'samp',
'script',
'select',
'small',
'span',
'strong',
'sub',
'sup',
'textarea',
'tt',
'var',
]);
const messages = {
spacingAfterPrev: 'Ambiguous spacing after previous element {{element}}',
spacingBeforeNext: 'Ambiguous spacing before next element {{element}}',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce or disallow spaces inside of curly braces in JSX attributes and expressions',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-child-element-spacing'),
},
fixable: null,
messages,
schema: [],
},
create(context) {
const TEXT_FOLLOWING_ELEMENT_PATTERN = /^\s*\n\s*\S/;
const TEXT_PRECEDING_ELEMENT_PATTERN = /\S\s*\n\s*$/;
const elementName = (node) => (
node.openingElement
&& node.openingElement.name
&& node.openingElement.name.type === 'JSXIdentifier'
&& node.openingElement.name.name
);
const isInlineElement = (node) => (
node.type === 'JSXElement'
&& INLINE_ELEMENTS.has(elementName(node))
);
const handleJSX = (node) => {
let lastChild = null;
let child = null;
(node.children.concat([null])).forEach((nextChild) => {
if (
(lastChild || nextChild)
&& (!lastChild || isInlineElement(lastChild))
&& (child && (child.type === 'Literal' || child.type === 'JSXText'))
&& (!nextChild || isInlineElement(nextChild))
&& true
) {
if (lastChild && child.value.match(TEXT_FOLLOWING_ELEMENT_PATTERN)) {
report(context, messages.spacingAfterPrev, 'spacingAfterPrev', {
node: lastChild,
loc: lastChild.loc.end,
data: {
element: elementName(lastChild),
},
});
} else if (nextChild && child.value.match(TEXT_PRECEDING_ELEMENT_PATTERN)) {
report(context, messages.spacingBeforeNext, 'spacingBeforeNext', {
node: nextChild,
loc: nextChild.loc.start,
data: {
element: elementName(nextChild),
},
});
}
}
lastChild = child;
child = nextChild;
});
};
return {
JSXElement: handleJSX,
JSXFragment: handleJSX,
};
},
};

View File

@@ -0,0 +1,315 @@
/**
* @fileoverview Validate closing bracket location in JSX
* @author Yannick Croissant
*/
'use strict';
const has = require('hasown');
const repeat = require('string.prototype.repeat');
const docsUrl = require('../util/docsUrl');
const getSourceCode = require('../util/eslint').getSourceCode;
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
bracketLocation: 'The closing bracket must be {{location}}{{details}}',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce closing bracket location in JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-closing-bracket-location'),
},
fixable: 'code',
messages,
schema: [{
anyOf: [
{
enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned'],
},
{
type: 'object',
properties: {
location: {
enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned'],
},
},
additionalProperties: false,
}, {
type: 'object',
properties: {
nonEmpty: {
enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned', false],
},
selfClosing: {
enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned', false],
},
},
additionalProperties: false,
},
],
}],
},
create(context) {
const MESSAGE_LOCATION = {
'after-props': 'placed after the last prop',
'after-tag': 'placed after the opening tag',
'props-aligned': 'aligned with the last prop',
'tag-aligned': 'aligned with the opening tag',
'line-aligned': 'aligned with the line containing the opening tag',
};
const DEFAULT_LOCATION = 'tag-aligned';
const config = context.options[0];
const options = {
nonEmpty: DEFAULT_LOCATION,
selfClosing: DEFAULT_LOCATION,
};
if (typeof config === 'string') {
// simple shorthand [1, 'something']
options.nonEmpty = config;
options.selfClosing = config;
} else if (typeof config === 'object') {
// [1, {location: 'something'}] (back-compat)
if (has(config, 'location')) {
options.nonEmpty = config.location;
options.selfClosing = config.location;
}
// [1, {nonEmpty: 'something'}]
if (has(config, 'nonEmpty')) {
options.nonEmpty = config.nonEmpty;
}
// [1, {selfClosing: 'something'}]
if (has(config, 'selfClosing')) {
options.selfClosing = config.selfClosing;
}
}
/**
* Get expected location for the closing bracket
* @param {Object} tokens Locations of the opening bracket, closing bracket and last prop
* @return {string} Expected location for the closing bracket
*/
function getExpectedLocation(tokens) {
let location;
// Is always after the opening tag if there is no props
if (typeof tokens.lastProp === 'undefined') {
location = 'after-tag';
// Is always after the last prop if this one is on the same line as the opening bracket
} else if (tokens.opening.line === tokens.lastProp.lastLine) {
location = 'after-props';
// Else use configuration dependent on selfClosing property
} else {
location = tokens.selfClosing ? options.selfClosing : options.nonEmpty;
}
return location;
}
/**
* Get the correct 0-indexed column for the closing bracket, given the
* expected location.
* @param {Object} tokens Locations of the opening bracket, closing bracket and last prop
* @param {string} expectedLocation Expected location for the closing bracket
* @return {?Number} The correct column for the closing bracket, or null
*/
function getCorrectColumn(tokens, expectedLocation) {
switch (expectedLocation) {
case 'props-aligned':
return tokens.lastProp.column;
case 'tag-aligned':
return tokens.opening.column;
case 'line-aligned':
return tokens.openingStartOfLine.column;
default:
return null;
}
}
/**
* Check if the closing bracket is correctly located
* @param {Object} tokens Locations of the opening bracket, closing bracket and last prop
* @param {string} expectedLocation Expected location for the closing bracket
* @return {boolean} True if the closing bracket is correctly located, false if not
*/
function hasCorrectLocation(tokens, expectedLocation) {
switch (expectedLocation) {
case 'after-tag':
return tokens.tag.line === tokens.closing.line;
case 'after-props':
return tokens.lastProp.lastLine === tokens.closing.line;
case 'props-aligned':
case 'tag-aligned':
case 'line-aligned': {
const correctColumn = getCorrectColumn(tokens, expectedLocation);
return correctColumn === tokens.closing.column;
}
default:
return true;
}
}
/**
* Get the characters used for indentation on the line to be matched
* @param {Object} tokens Locations of the opening bracket, closing bracket and last prop
* @param {string} expectedLocation Expected location for the closing bracket
* @param {number} [correctColumn] Expected column for the closing bracket. Default to 0
* @return {string} The characters used for indentation
*/
function getIndentation(tokens, expectedLocation, correctColumn) {
const newColumn = correctColumn || 0;
let indentation;
let spaces = '';
switch (expectedLocation) {
case 'props-aligned':
indentation = /^\s*/.exec(getSourceCode(context).lines[tokens.lastProp.firstLine - 1])[0];
break;
case 'tag-aligned':
case 'line-aligned':
indentation = /^\s*/.exec(getSourceCode(context).lines[tokens.opening.line - 1])[0];
break;
default:
indentation = '';
}
if (indentation.length + 1 < newColumn) {
// Non-whitespace characters were included in the column offset
spaces = repeat(' ', +correctColumn - indentation.length);
}
return indentation + spaces;
}
/**
* Get the locations of the opening bracket, closing bracket, last prop, and
* start of opening line.
* @param {ASTNode} node The node to check
* @return {Object} Locations of the opening bracket, closing bracket, last
* prop and start of opening line.
*/
function getTokensLocations(node) {
const sourceCode = getSourceCode(context);
const opening = sourceCode.getFirstToken(node).loc.start;
const closing = sourceCode.getLastTokens(node, node.selfClosing ? 2 : 1)[0].loc.start;
const tag = sourceCode.getFirstToken(node.name).loc.start;
let lastProp;
if (node.attributes.length) {
lastProp = node.attributes[node.attributes.length - 1];
lastProp = {
column: sourceCode.getFirstToken(lastProp).loc.start.column,
firstLine: sourceCode.getFirstToken(lastProp).loc.start.line,
lastLine: sourceCode.getLastToken(lastProp).loc.end.line,
};
}
const openingLine = sourceCode.lines[opening.line - 1];
const closingLine = sourceCode.lines[closing.line - 1];
const isTab = {
openTab: /^\t/.test(openingLine),
closeTab: /^\t/.test(closingLine),
};
const openingStartOfLine = {
column: /^\s*/.exec(openingLine)[0].length,
line: opening.line,
};
return {
isTab,
tag,
opening,
closing,
lastProp,
selfClosing: node.selfClosing,
openingStartOfLine,
};
}
/**
* Get an unique ID for a given JSXOpeningElement
*
* @param {ASTNode} node The AST node being checked.
* @returns {string} Unique ID (based on its range)
*/
function getOpeningElementId(node) {
return node.range.join(':');
}
const lastAttributeNode = {};
return {
JSXAttribute(node) {
lastAttributeNode[getOpeningElementId(node.parent)] = node;
},
JSXSpreadAttribute(node) {
lastAttributeNode[getOpeningElementId(node.parent)] = node;
},
'JSXOpeningElement:exit'(node) {
const attributeNode = lastAttributeNode[getOpeningElementId(node)];
const cachedLastAttributeEndPos = attributeNode ? attributeNode.range[1] : null;
let expectedNextLine;
const tokens = getTokensLocations(node);
const expectedLocation = getExpectedLocation(tokens);
let usingSameIndentation = true;
if (expectedLocation === 'tag-aligned') {
usingSameIndentation = tokens.isTab.openTab === tokens.isTab.closeTab;
}
if (hasCorrectLocation(tokens, expectedLocation) && usingSameIndentation) {
return;
}
const data = {
location: MESSAGE_LOCATION[expectedLocation],
details: '',
};
const correctColumn = getCorrectColumn(tokens, expectedLocation);
if (correctColumn !== null) {
expectedNextLine = tokens.lastProp
&& (tokens.lastProp.lastLine === tokens.closing.line);
data.details = ` (expected column ${correctColumn + 1}${expectedNextLine ? ' on the next line)' : ')'}`;
}
report(context, messages.bracketLocation, 'bracketLocation', {
node,
loc: tokens.closing,
data,
fix(fixer) {
const closingTag = tokens.selfClosing ? '/>' : '>';
switch (expectedLocation) {
case 'after-tag':
if (cachedLastAttributeEndPos) {
return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]],
(expectedNextLine ? '\n' : '') + closingTag);
}
return fixer.replaceTextRange([node.name.range[1], node.range[1]],
(expectedNextLine ? '\n' : ' ') + closingTag);
case 'after-props':
return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]],
(expectedNextLine ? '\n' : '') + closingTag);
case 'props-aligned':
case 'tag-aligned':
case 'line-aligned':
return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]],
`\n${getIndentation(tokens, expectedLocation, correctColumn)}${closingTag}`);
default:
return true;
}
},
});
},
};
},
};

View File

@@ -0,0 +1,142 @@
/**
* @fileoverview Validate closing tag location in JSX
* @author Ross Solomon
*/
'use strict';
const repeat = require('string.prototype.repeat');
const has = require('hasown');
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
const getSourceCode = require('../util/eslint').getSourceCode;
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
onOwnLine: 'Closing tag of a multiline JSX expression must be on its own line.',
matchIndent: 'Expected closing tag to match indentation of opening.',
alignWithOpening: 'Expected closing tag to be aligned with the line containing the opening tag',
};
const defaultOption = 'tag-aligned';
const optionMessageMap = {
'tag-aligned': 'matchIndent',
'line-aligned': 'alignWithOpening',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce closing tag location for multiline JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-closing-tag-location'),
},
fixable: 'whitespace',
messages,
schema: [{
anyOf: [
{
enum: ['tag-aligned', 'line-aligned'],
},
{
type: 'object',
properties: {
location: {
enum: ['tag-aligned', 'line-aligned'],
},
},
additionalProperties: false,
},
],
}],
},
create(context) {
const config = context.options[0];
let option = defaultOption;
if (typeof config === 'string') {
option = config;
} else if (typeof config === 'object') {
if (has(config, 'location')) {
option = config.location;
}
}
function getIndentation(openingStartOfLine, opening) {
if (option === 'line-aligned') return openingStartOfLine.column;
if (option === 'tag-aligned') return opening.loc.start.column;
}
function handleClosingElement(node) {
if (!node.parent) {
return;
}
const sourceCode = getSourceCode(context);
const opening = node.parent.openingElement || node.parent.openingFragment;
const openingLoc = sourceCode.getFirstToken(opening).loc.start;
const openingLine = sourceCode.lines[openingLoc.line - 1];
const openingStartOfLine = {
column: /^\s*/.exec(openingLine)[0].length,
line: openingLoc.line,
};
if (opening.loc.start.line === node.loc.start.line) {
return;
}
if (
opening.loc.start.column === node.loc.start.column
&& option === 'tag-aligned'
) {
return;
}
if (
openingStartOfLine.column === node.loc.start.column
&& option === 'line-aligned'
) {
return;
}
const messageId = astUtil.isNodeFirstInLine(context, node)
? optionMessageMap[option]
: 'onOwnLine';
report(context, messages[messageId], messageId, {
node,
loc: node.loc,
fix(fixer) {
const indent = repeat(
' ',
getIndentation(openingStartOfLine, opening)
);
if (astUtil.isNodeFirstInLine(context, node)) {
return fixer.replaceTextRange(
[node.range[0] - node.loc.start.column, node.range[0]],
indent
);
}
return fixer.insertTextBefore(node, `\n${indent}`);
},
});
}
return {
JSXClosingElement: handleClosingElement,
JSXClosingFragment: handleClosingElement,
};
},
};

View File

@@ -0,0 +1,421 @@
/**
* @fileoverview Enforce curly braces or disallow unnecessary curly brace in JSX
* @author Jacky Ho
* @author Simon Lydell
*/
'use strict';
const arrayIncludes = require('array-includes');
const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');
const report = require('../util/report');
const eslintUtil = require('../util/eslint');
const getSourceCode = eslintUtil.getSourceCode;
const getText = eslintUtil.getText;
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const OPTION_ALWAYS = 'always';
const OPTION_NEVER = 'never';
const OPTION_IGNORE = 'ignore';
const OPTION_VALUES = [
OPTION_ALWAYS,
OPTION_NEVER,
OPTION_IGNORE,
];
const DEFAULT_CONFIG = { props: OPTION_NEVER, children: OPTION_NEVER, propElementValues: OPTION_IGNORE };
const HTML_ENTITY_REGEX = () => /&[A-Za-z\d#]+;/g;
function containsLineTerminators(rawStringValue) {
return /[\n\r\u2028\u2029]/.test(rawStringValue);
}
function containsBackslash(rawStringValue) {
return arrayIncludes(rawStringValue, '\\');
}
function containsHTMLEntity(rawStringValue) {
return HTML_ENTITY_REGEX().test(rawStringValue);
}
function containsOnlyHtmlEntities(rawStringValue) {
return rawStringValue.replace(HTML_ENTITY_REGEX(), '').trim() === '';
}
function containsDisallowedJSXTextChars(rawStringValue) {
return /[{<>}]/.test(rawStringValue);
}
function containsQuoteCharacters(value) {
return /['"]/.test(value);
}
function containsMultilineComment(value) {
return /\/\*/.test(value);
}
function escapeDoubleQuotes(rawStringValue) {
return rawStringValue.replace(/\\"/g, '"').replace(/"/g, '\\"');
}
function escapeBackslashes(rawStringValue) {
return rawStringValue.replace(/\\/g, '\\\\');
}
function needToEscapeCharacterForJSX(raw, node) {
return (
containsBackslash(raw)
|| containsHTMLEntity(raw)
|| (node.parent.type !== 'JSXAttribute' && containsDisallowedJSXTextChars(raw))
);
}
function containsWhitespaceExpression(child) {
if (child.type === 'JSXExpressionContainer') {
const value = child.expression.value;
return value ? jsxUtil.isWhiteSpaces(value) : false;
}
return false;
}
function isLineBreak(text) {
return containsLineTerminators(text) && text.trim() === '';
}
function wrapNonHTMLEntities(text) {
const HTML_ENTITY = '<HTML_ENTITY>';
const withCurlyBraces = text.split(HTML_ENTITY_REGEX()).map((word) => (
word === '' ? '' : `{${JSON.stringify(word)}}`
)).join(HTML_ENTITY);
const htmlEntities = text.match(HTML_ENTITY_REGEX());
return htmlEntities.reduce((acc, htmlEntity) => (
acc.replace(HTML_ENTITY, htmlEntity)
), withCurlyBraces);
}
function wrapWithCurlyBraces(rawText) {
if (!containsLineTerminators(rawText)) {
return `{${JSON.stringify(rawText)}}`;
}
return rawText.split('\n').map((line) => {
if (line.trim() === '') {
return line;
}
const firstCharIndex = line.search(/[^\s]/);
const leftWhitespace = line.slice(0, firstCharIndex);
const text = line.slice(firstCharIndex);
if (containsHTMLEntity(line)) {
return `${leftWhitespace}${wrapNonHTMLEntities(text)}`;
}
return `${leftWhitespace}{${JSON.stringify(text)}}`;
}).join('\n');
}
function isWhiteSpaceLiteral(node) {
return node.type && node.type === 'Literal' && node.value && jsxUtil.isWhiteSpaces(node.value);
}
function isStringWithTrailingWhiteSpaces(value) {
return /^\s|\s$/.test(value);
}
function isLiteralWithTrailingWhiteSpaces(node) {
return node.type && node.type === 'Literal' && node.value && isStringWithTrailingWhiteSpaces(node.value);
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
unnecessaryCurly: 'Curly braces are unnecessary here.',
missingCurly: 'Need to wrap this literal in a JSX expression.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow unnecessary JSX expressions when literals alone are sufficient or enforce JSX expressions on literals in JSX children or attributes',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-curly-brace-presence'),
},
fixable: 'code',
messages,
schema: [
{
anyOf: [
{
type: 'object',
properties: {
props: { enum: OPTION_VALUES },
children: { enum: OPTION_VALUES },
propElementValues: { enum: OPTION_VALUES },
},
additionalProperties: false,
},
{
enum: OPTION_VALUES,
},
],
},
],
},
create(context) {
const ruleOptions = context.options[0];
const userConfig = typeof ruleOptions === 'string'
? { props: ruleOptions, children: ruleOptions, propElementValues: OPTION_IGNORE }
: Object.assign({}, DEFAULT_CONFIG, ruleOptions);
/**
* Report and fix an unnecessary curly brace violation on a node
* @param {ASTNode} JSXExpressionNode - The AST node with an unnecessary JSX expression
*/
function reportUnnecessaryCurly(JSXExpressionNode) {
report(context, messages.unnecessaryCurly, 'unnecessaryCurly', {
node: JSXExpressionNode,
fix(fixer) {
const expression = JSXExpressionNode.expression;
let textToReplace;
if (jsxUtil.isJSX(expression)) {
textToReplace = getText(context, expression);
} else {
const expressionType = expression && expression.type;
const parentType = JSXExpressionNode.parent.type;
if (parentType === 'JSXAttribute') {
if (expressionType !== 'TemplateLiteral' && /["]/.test(expression.raw.slice(1, -1))) {
textToReplace = expression.raw;
} else {
textToReplace = `"${expressionType === 'TemplateLiteral'
? expression.quasis[0].value.raw
: expression.raw.slice(1, -1)
}"`;
}
} else if (jsxUtil.isJSX(expression)) {
textToReplace = getText(context, expression);
} else {
textToReplace = expressionType === 'TemplateLiteral'
? expression.quasis[0].value.cooked : expression.value;
}
}
return fixer.replaceText(JSXExpressionNode, textToReplace);
},
});
}
function reportMissingCurly(literalNode) {
report(context, messages.missingCurly, 'missingCurly', {
node: literalNode,
fix(fixer) {
if (jsxUtil.isJSX(literalNode)) {
return fixer.replaceText(literalNode, `{${getText(context, literalNode)}}`);
}
// If a HTML entity name is found, bail out because it can be fixed
// by either using the real character or the unicode equivalent.
// If it contains any line terminator character, bail out as well.
if (
containsOnlyHtmlEntities(literalNode.raw)
|| (literalNode.parent.type === 'JSXAttribute' && containsLineTerminators(literalNode.raw))
|| isLineBreak(literalNode.raw)
) {
return null;
}
const expression = literalNode.parent.type === 'JSXAttribute'
? `{"${escapeDoubleQuotes(escapeBackslashes(
literalNode.raw.slice(1, -1)
))}"}`
: wrapWithCurlyBraces(literalNode.raw);
return fixer.replaceText(literalNode, expression);
},
});
}
// Bail out if there is any character that needs to be escaped in JSX
// because escaping decreases readability and the original code may be more
// readable anyway or intentional for other specific reasons
function lintUnnecessaryCurly(JSXExpressionNode) {
const expression = JSXExpressionNode.expression;
const expressionType = expression.type;
const sourceCode = getSourceCode(context);
// Curly braces containing comments are necessary
if (sourceCode.getCommentsInside && sourceCode.getCommentsInside(JSXExpressionNode).length > 0) {
return;
}
if (
(expressionType === 'Literal' || expressionType === 'JSXText')
&& typeof expression.value === 'string'
&& (
(JSXExpressionNode.parent.type === 'JSXAttribute' && !isWhiteSpaceLiteral(expression))
|| !isLiteralWithTrailingWhiteSpaces(expression)
)
&& !containsMultilineComment(expression.value)
&& !needToEscapeCharacterForJSX(expression.raw, JSXExpressionNode) && (
jsxUtil.isJSX(JSXExpressionNode.parent)
|| (!containsQuoteCharacters(expression.value) || typeof expression.value === 'string')
)
) {
reportUnnecessaryCurly(JSXExpressionNode);
} else if (
expressionType === 'TemplateLiteral'
&& expression.expressions.length === 0
&& expression.quasis[0].value.raw.indexOf('\n') === -1
&& !isStringWithTrailingWhiteSpaces(expression.quasis[0].value.raw)
&& !needToEscapeCharacterForJSX(expression.quasis[0].value.raw, JSXExpressionNode)
&& !containsQuoteCharacters(expression.quasis[0].value.cooked)
) {
reportUnnecessaryCurly(JSXExpressionNode);
} else if (jsxUtil.isJSX(expression)) {
reportUnnecessaryCurly(JSXExpressionNode);
}
}
function areRuleConditionsSatisfied(parent, config, ruleCondition) {
return (
parent.type === 'JSXAttribute'
&& typeof config.props === 'string'
&& config.props === ruleCondition
) || (
jsxUtil.isJSX(parent)
&& typeof config.children === 'string'
&& config.children === ruleCondition
);
}
function getAdjacentSiblings(node, children) {
for (let i = 1; i < children.length - 1; i++) {
const child = children[i];
if (node === child) {
return [children[i - 1], children[i + 1]];
}
}
if (node === children[0] && children[1]) {
return [children[1]];
}
if (node === children[children.length - 1] && children[children.length - 2]) {
return [children[children.length - 2]];
}
return [];
}
function hasAdjacentJsxExpressionContainers(node, children) {
if (!children) {
return false;
}
const childrenExcludingWhitespaceLiteral = children.filter((child) => !isWhiteSpaceLiteral(child));
const adjSiblings = getAdjacentSiblings(node, childrenExcludingWhitespaceLiteral);
return adjSiblings.some((x) => x.type && x.type === 'JSXExpressionContainer');
}
function hasAdjacentJsx(node, children) {
if (!children) {
return false;
}
const childrenExcludingWhitespaceLiteral = children.filter((child) => !isWhiteSpaceLiteral(child));
const adjSiblings = getAdjacentSiblings(node, childrenExcludingWhitespaceLiteral);
return adjSiblings.some((x) => x.type && arrayIncludes(['JSXExpressionContainer', 'JSXElement'], x.type));
}
function shouldCheckForUnnecessaryCurly(node, config) {
const parent = node.parent;
// Bail out if the parent is a JSXAttribute & its contents aren't
// StringLiteral or TemplateLiteral since e.g
// <App prop1={<CustomEl />} prop2={<CustomEl>...</CustomEl>} />
if (
parent.type && parent.type === 'JSXAttribute'
&& (node.expression && node.expression.type
&& node.expression.type !== 'Literal'
&& node.expression.type !== 'StringLiteral'
&& node.expression.type !== 'TemplateLiteral')
) {
return false;
}
// If there are adjacent `JsxExpressionContainer` then there is no need,
// to check for unnecessary curly braces.
if (jsxUtil.isJSX(parent) && hasAdjacentJsxExpressionContainers(node, parent.children)) {
return false;
}
if (containsWhitespaceExpression(node) && hasAdjacentJsx(node, parent.children)) {
return false;
}
if (
parent.children
&& parent.children.length === 1
&& containsWhitespaceExpression(node)
) {
return false;
}
return areRuleConditionsSatisfied(parent, config, OPTION_NEVER);
}
function shouldCheckForMissingCurly(node, config) {
if (jsxUtil.isJSX(node)) {
return config.propElementValues !== OPTION_IGNORE;
}
if (
isLineBreak(node.raw)
|| containsOnlyHtmlEntities(node.raw)
) {
return false;
}
const parent = node.parent;
if (
parent.children
&& parent.children.length === 1
&& containsWhitespaceExpression(parent.children[0])
) {
return false;
}
return areRuleConditionsSatisfied(parent, config, OPTION_ALWAYS);
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
'JSXAttribute > JSXExpressionContainer > JSXElement'(node) {
if (userConfig.propElementValues === OPTION_NEVER) {
reportUnnecessaryCurly(node.parent);
}
},
JSXExpressionContainer(node) {
if (shouldCheckForUnnecessaryCurly(node, userConfig)) {
lintUnnecessaryCurly(node);
}
},
'JSXAttribute > JSXElement, Literal, JSXText'(node) {
if (shouldCheckForMissingCurly(node, userConfig)) {
reportMissingCurly(node);
}
},
};
},
};

View File

@@ -0,0 +1,187 @@
/**
* @fileoverview enforce consistent line breaks inside jsx curly
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const eslintUtil = require('../util/eslint');
const report = require('../util/report');
const getSourceCode = eslintUtil.getSourceCode;
const getText = eslintUtil.getText;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function getNormalizedOption(context) {
const rawOption = context.options[0] || 'consistent';
if (rawOption === 'consistent') {
return {
multiline: 'consistent',
singleline: 'consistent',
};
}
if (rawOption === 'never') {
return {
multiline: 'forbid',
singleline: 'forbid',
};
}
return {
multiline: rawOption.multiline || 'consistent',
singleline: rawOption.singleline || 'consistent',
};
}
const messages = {
expectedBefore: 'Expected newline before \'}\'.',
expectedAfter: 'Expected newline after \'{\'.',
unexpectedBefore: 'Unexpected newline before \'}\'.',
unexpectedAfter: 'Unexpected newline after \'{\'.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'Enforce consistent linebreaks in curly braces in JSX attributes and expressions',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-curly-newline'),
},
fixable: 'whitespace',
schema: [
{
anyOf: [
{
enum: ['consistent', 'never'],
},
{
type: 'object',
properties: {
singleline: { enum: ['consistent', 'require', 'forbid'] },
multiline: { enum: ['consistent', 'require', 'forbid'] },
},
additionalProperties: false,
},
],
},
],
messages,
},
create(context) {
const sourceCode = getSourceCode(context);
const option = getNormalizedOption(context);
// ----------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------
/**
* Determines whether two adjacent tokens are on the same line.
* @param {Object} left - The left token object.
* @param {Object} right - The right token object.
* @returns {boolean} Whether or not the tokens are on the same line.
*/
function isTokenOnSameLine(left, right) {
return left.loc.end.line === right.loc.start.line;
}
/**
* Determines whether there should be newlines inside curlys
* @param {ASTNode} expression The expression contained in the curlys
* @param {boolean} hasLeftNewline `true` if the left curly has a newline in the current code.
* @returns {boolean} `true` if there should be newlines inside the function curlys
*/
function shouldHaveNewlines(expression, hasLeftNewline) {
const isMultiline = expression.loc.start.line !== expression.loc.end.line;
switch (isMultiline ? option.multiline : option.singleline) {
case 'forbid': return false;
case 'require': return true;
case 'consistent':
default: return hasLeftNewline;
}
}
/**
* Validates curlys
* @param {Object} curlys An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
* @param {ASTNode} expression The expression inside the curly
* @returns {void}
*/
function validateCurlys(curlys, expression) {
const leftCurly = curlys.leftCurly;
const rightCurly = curlys.rightCurly;
const tokenAfterLeftCurly = sourceCode.getTokenAfter(leftCurly);
const tokenBeforeRightCurly = sourceCode.getTokenBefore(rightCurly);
const hasLeftNewline = !isTokenOnSameLine(leftCurly, tokenAfterLeftCurly);
const hasRightNewline = !isTokenOnSameLine(tokenBeforeRightCurly, rightCurly);
const needsNewlines = shouldHaveNewlines(expression, hasLeftNewline);
if (hasLeftNewline && !needsNewlines) {
report(context, messages.unexpectedAfter, 'unexpectedAfter', {
node: leftCurly,
fix(fixer) {
return getText(context)
.slice(leftCurly.range[1], tokenAfterLeftCurly.range[0])
.trim()
? null // If there is a comment between the { and the first element, don't do a fix.
: fixer.removeRange([leftCurly.range[1], tokenAfterLeftCurly.range[0]]);
},
});
} else if (!hasLeftNewline && needsNewlines) {
report(context, messages.expectedAfter, 'expectedAfter', {
node: leftCurly,
fix: (fixer) => fixer.insertTextAfter(leftCurly, '\n'),
});
}
if (hasRightNewline && !needsNewlines) {
report(context, messages.unexpectedBefore, 'unexpectedBefore', {
node: rightCurly,
fix(fixer) {
return getText(context)
.slice(tokenBeforeRightCurly.range[1], rightCurly.range[0])
.trim()
? null // If there is a comment between the last element and the }, don't do a fix.
: fixer.removeRange([
tokenBeforeRightCurly.range[1],
rightCurly.range[0],
]);
},
});
} else if (!hasRightNewline && needsNewlines) {
report(context, messages.expectedBefore, 'expectedBefore', {
node: rightCurly,
fix: (fixer) => fixer.insertTextBefore(rightCurly, '\n'),
});
}
}
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return {
JSXExpressionContainer(node) {
const curlyTokens = {
leftCurly: sourceCode.getFirstToken(node),
rightCurly: sourceCode.getLastToken(node),
};
validateCurlys(curlyTokens, node.expression);
},
};
},
};

View File

@@ -0,0 +1,432 @@
/**
* @fileoverview Enforce or disallow spaces inside of curly braces in JSX attributes.
* @author Jamund Ferguson
* @author Brandyn Bennett
* @author Michael Ficarra
* @author Vignesh Anand
* @author Jamund Ferguson
* @author Yannick Croissant
* @author Erik Wendel
*/
'use strict';
const has = require('hasown');
const docsUrl = require('../util/docsUrl');
const getSourceCode = require('../util/eslint').getSourceCode;
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const SPACING = {
always: 'always',
never: 'never',
};
const SPACING_VALUES = [SPACING.always, SPACING.never];
const messages = {
noNewlineAfter: 'There should be no newline after \'{{token}}\'',
noNewlineBefore: 'There should be no newline before \'{{token}}\'',
noSpaceAfter: 'There should be no space after \'{{token}}\'',
noSpaceBefore: 'There should be no space before \'{{token}}\'',
spaceNeededAfter: 'A space is required after \'{{token}}\'',
spaceNeededBefore: 'A space is required before \'{{token}}\'',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce or disallow spaces inside of curly braces in JSX attributes and expressions',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-curly-spacing'),
},
fixable: 'code',
messages,
schema: {
definitions: {
basicConfig: {
type: 'object',
properties: {
when: {
enum: SPACING_VALUES,
},
allowMultiline: {
type: 'boolean',
},
spacing: {
type: 'object',
properties: {
objectLiterals: {
enum: SPACING_VALUES,
},
},
},
},
},
basicConfigOrBoolean: {
anyOf: [{
$ref: '#/definitions/basicConfig',
}, {
type: 'boolean',
}],
},
},
type: 'array',
items: [{
anyOf: [{
allOf: [{
$ref: '#/definitions/basicConfig',
}, {
type: 'object',
properties: {
attributes: {
$ref: '#/definitions/basicConfigOrBoolean',
},
children: {
$ref: '#/definitions/basicConfigOrBoolean',
},
},
}],
}, {
enum: SPACING_VALUES,
}],
}, {
type: 'object',
properties: {
allowMultiline: {
type: 'boolean',
},
spacing: {
type: 'object',
properties: {
objectLiterals: {
enum: SPACING_VALUES,
},
},
},
},
additionalProperties: false,
}],
},
},
create(context) {
function normalizeConfig(configOrTrue, defaults, lastPass) {
const config = configOrTrue === true ? {} : configOrTrue;
const when = config.when || defaults.when;
const allowMultiline = has(config, 'allowMultiline') ? config.allowMultiline : defaults.allowMultiline;
const spacing = config.spacing || {};
let objectLiteralSpaces = spacing.objectLiterals || defaults.objectLiteralSpaces;
if (lastPass) {
// On the final pass assign the values that should be derived from others if they are still undefined
objectLiteralSpaces = objectLiteralSpaces || when;
}
return {
when,
allowMultiline,
objectLiteralSpaces,
};
}
const DEFAULT_WHEN = SPACING.never;
const DEFAULT_ALLOW_MULTILINE = true;
const DEFAULT_ATTRIBUTES = true;
const DEFAULT_CHILDREN = false;
let originalConfig = context.options[0] || {};
if (SPACING_VALUES.indexOf(originalConfig) !== -1) {
originalConfig = Object.assign({ when: context.options[0] }, context.options[1]);
}
const defaultConfig = normalizeConfig(originalConfig, {
when: DEFAULT_WHEN,
allowMultiline: DEFAULT_ALLOW_MULTILINE,
});
const attributes = has(originalConfig, 'attributes') ? originalConfig.attributes : DEFAULT_ATTRIBUTES;
const attributesConfig = attributes ? normalizeConfig(attributes, defaultConfig, true) : null;
const children = has(originalConfig, 'children') ? originalConfig.children : DEFAULT_CHILDREN;
const childrenConfig = children ? normalizeConfig(children, defaultConfig, true) : null;
// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------
/**
* Determines whether two adjacent tokens have a newline between them.
* @param {Object} left - The left token object.
* @param {Object} right - The right token object.
* @returns {boolean} Whether or not there is a newline between the tokens.
*/
function isMultiline(left, right) {
return left.loc.end.line !== right.loc.start.line;
}
/**
* Trims text of whitespace between two ranges
* @param {Fixer} fixer - the eslint fixer object
* @param {number} fromLoc - the start location
* @param {number} toLoc - the end location
* @param {string} mode - either 'start' or 'end'
* @param {string=} spacing - a spacing value that will optionally add a space to the removed text
* @returns {Object|*|{range, text}}
*/
function fixByTrimmingWhitespace(fixer, fromLoc, toLoc, mode, spacing) {
let replacementText = getSourceCode(context).text.slice(fromLoc, toLoc);
if (mode === 'start') {
replacementText = replacementText.replace(/^\s+/gm, '');
} else {
replacementText = replacementText.replace(/\s+$/gm, '');
}
if (spacing === SPACING.always) {
if (mode === 'start') {
replacementText += ' ';
} else {
replacementText = ` ${replacementText}`;
}
}
return fixer.replaceTextRange([fromLoc, toLoc], replacementText);
}
/**
* Reports that there shouldn't be a newline after the first token
* @param {ASTNode} node - The node to report in the event of an error.
* @param {Token} token - The token to use for the report.
* @param {string} spacing
* @returns {void}
*/
function reportNoBeginningNewline(node, token, spacing) {
report(context, messages.noNewlineAfter, 'noNewlineAfter', {
node,
loc: token.loc.start,
data: {
token: token.value,
},
fix(fixer) {
const nextToken = getSourceCode(context).getTokenAfter(token);
return fixByTrimmingWhitespace(fixer, token.range[1], nextToken.range[0], 'start', spacing);
},
});
}
/**
* Reports that there shouldn't be a newline before the last token
* @param {ASTNode} node - The node to report in the event of an error.
* @param {Token} token - The token to use for the report.
* @param {string} spacing
* @returns {void}
*/
function reportNoEndingNewline(node, token, spacing) {
report(context, messages.noNewlineBefore, 'noNewlineBefore', {
node,
loc: token.loc.start,
data: {
token: token.value,
},
fix(fixer) {
const previousToken = getSourceCode(context).getTokenBefore(token);
return fixByTrimmingWhitespace(fixer, previousToken.range[1], token.range[0], 'end', spacing);
},
});
}
/**
* Reports that there shouldn't be a space after the first token
* @param {ASTNode} node - The node to report in the event of an error.
* @param {Token} token - The token to use for the report.
* @returns {void}
*/
function reportNoBeginningSpace(node, token) {
report(context, messages.noSpaceAfter, 'noSpaceAfter', {
node,
loc: token.loc.start,
data: {
token: token.value,
},
fix(fixer) {
const sourceCode = getSourceCode(context);
const nextToken = sourceCode.getTokenAfter(token);
let nextComment;
// eslint >=4.x
if (sourceCode.getCommentsAfter) {
nextComment = sourceCode.getCommentsAfter(token);
// eslint 3.x
} else {
const potentialComment = sourceCode.getTokenAfter(token, { includeComments: true });
nextComment = nextToken === potentialComment ? [] : [potentialComment];
}
// Take comments into consideration to narrow the fix range to what is actually affected. (See #1414)
if (nextComment.length > 0) {
return fixByTrimmingWhitespace(fixer, token.range[1], Math.min(nextToken.range[0], nextComment[0].range[0]), 'start');
}
return fixByTrimmingWhitespace(fixer, token.range[1], nextToken.range[0], 'start');
},
});
}
/**
* Reports that there shouldn't be a space before the last token
* @param {ASTNode} node - The node to report in the event of an error.
* @param {Token} token - The token to use for the report.
* @returns {void}
*/
function reportNoEndingSpace(node, token) {
report(context, messages.noSpaceBefore, 'noSpaceBefore', {
node,
loc: token.loc.start,
data: {
token: token.value,
},
fix(fixer) {
const sourceCode = getSourceCode(context);
const previousToken = sourceCode.getTokenBefore(token);
let previousComment;
// eslint >=4.x
if (sourceCode.getCommentsBefore) {
previousComment = sourceCode.getCommentsBefore(token);
// eslint 3.x
} else {
const potentialComment = sourceCode.getTokenBefore(token, { includeComments: true });
previousComment = previousToken === potentialComment ? [] : [potentialComment];
}
// Take comments into consideration to narrow the fix range to what is actually affected. (See #1414)
if (previousComment.length > 0) {
return fixByTrimmingWhitespace(fixer, Math.max(previousToken.range[1], previousComment[0].range[1]), token.range[0], 'end');
}
return fixByTrimmingWhitespace(fixer, previousToken.range[1], token.range[0], 'end');
},
});
}
/**
* Reports that there should be a space after the first token
* @param {ASTNode} node - The node to report in the event of an error.
* @param {Token} token - The token to use for the report.
* @returns {void}
*/
function reportRequiredBeginningSpace(node, token) {
report(context, messages.spaceNeededAfter, 'spaceNeededAfter', {
node,
loc: token.loc.start,
data: {
token: token.value,
},
fix(fixer) {
return fixer.insertTextAfter(token, ' ');
},
});
}
/**
* Reports that there should be a space before the last token
* @param {ASTNode} node - The node to report in the event of an error.
* @param {Token} token - The token to use for the report.
* @returns {void}
*/
function reportRequiredEndingSpace(node, token) {
report(context, messages.spaceNeededBefore, 'spaceNeededBefore', {
node,
loc: token.loc.start,
data: {
token: token.value,
},
fix(fixer) {
return fixer.insertTextBefore(token, ' ');
},
});
}
/**
* Determines if spacing in curly braces is valid.
* @param {ASTNode} node The AST node to check.
* @returns {void}
*/
function validateBraceSpacing(node) {
let config;
switch (node.parent.type) {
case 'JSXAttribute':
case 'JSXOpeningElement':
config = attributesConfig;
break;
case 'JSXElement':
case 'JSXFragment':
config = childrenConfig;
break;
default:
return;
}
if (config === null) {
return;
}
const sourceCode = getSourceCode(context);
const first = sourceCode.getFirstToken(node);
const last = sourceCode.getLastToken(node);
let second = sourceCode.getTokenAfter(first, { includeComments: true });
let penultimate = sourceCode.getTokenBefore(last, { includeComments: true });
if (!second) {
second = sourceCode.getTokenAfter(first);
const leadingComments = sourceCode.getNodeByRangeIndex(second.range[0]).leadingComments;
second = leadingComments ? leadingComments[0] : second;
}
if (!penultimate) {
penultimate = sourceCode.getTokenBefore(last);
const trailingComments = sourceCode.getNodeByRangeIndex(penultimate.range[0]).trailingComments;
penultimate = trailingComments ? trailingComments[trailingComments.length - 1] : penultimate;
}
const isObjectLiteral = first.value === second.value;
const spacing = isObjectLiteral ? config.objectLiteralSpaces : config.when;
if (spacing === SPACING.always) {
if (!sourceCode.isSpaceBetweenTokens(first, second)) {
reportRequiredBeginningSpace(node, first);
} else if (!config.allowMultiline && isMultiline(first, second)) {
reportNoBeginningNewline(node, first, spacing);
}
if (!sourceCode.isSpaceBetweenTokens(penultimate, last)) {
reportRequiredEndingSpace(node, last);
} else if (!config.allowMultiline && isMultiline(penultimate, last)) {
reportNoEndingNewline(node, last, spacing);
}
} else if (spacing === SPACING.never) {
if (isMultiline(first, second)) {
if (!config.allowMultiline) {
reportNoBeginningNewline(node, first, spacing);
}
} else if (sourceCode.isSpaceBetweenTokens(first, second)) {
reportNoBeginningSpace(node, first);
}
if (isMultiline(penultimate, last)) {
if (!config.allowMultiline) {
reportNoEndingNewline(node, last, spacing);
}
} else if (sourceCode.isSpaceBetweenTokens(penultimate, last)) {
reportNoEndingSpace(node, last);
}
}
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
JSXExpressionContainer: validateBraceSpacing,
JSXSpreadAttribute: validateBraceSpacing,
};
},
};

View File

@@ -0,0 +1,112 @@
/**
* @fileoverview Disallow or enforce spaces around equal signs in JSX attributes.
* @author ryym
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const getSourceCode = require('../util/eslint').getSourceCode;
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
noSpaceBefore: 'There should be no space before \'=\'',
noSpaceAfter: 'There should be no space after \'=\'',
needSpaceBefore: 'A space is required before \'=\'',
needSpaceAfter: 'A space is required after \'=\'',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce or disallow spaces around equal signs in JSX attributes',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-equals-spacing'),
},
fixable: 'code',
messages,
schema: [{
enum: ['always', 'never'],
}],
},
create(context) {
const config = context.options[0] || 'never';
/**
* Determines a given attribute node has an equal sign.
* @param {ASTNode} attrNode - The attribute node.
* @returns {boolean} Whether or not the attriute node has an equal sign.
*/
function hasEqual(attrNode) {
return attrNode.type !== 'JSXSpreadAttribute' && attrNode.value !== null;
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
JSXOpeningElement(node) {
node.attributes.forEach((attrNode) => {
if (!hasEqual(attrNode)) {
return;
}
const sourceCode = getSourceCode(context);
const equalToken = sourceCode.getTokenAfter(attrNode.name);
const spacedBefore = sourceCode.isSpaceBetweenTokens(attrNode.name, equalToken);
const spacedAfter = sourceCode.isSpaceBetweenTokens(equalToken, attrNode.value);
if (config === 'never') {
if (spacedBefore) {
report(context, messages.noSpaceBefore, 'noSpaceBefore', {
node: attrNode,
loc: equalToken.loc.start,
fix(fixer) {
return fixer.removeRange([attrNode.name.range[1], equalToken.range[0]]);
},
});
}
if (spacedAfter) {
report(context, messages.noSpaceAfter, 'noSpaceAfter', {
node: attrNode,
loc: equalToken.loc.start,
fix(fixer) {
return fixer.removeRange([equalToken.range[1], attrNode.value.range[0]]);
},
});
}
} else if (config === 'always') {
if (!spacedBefore) {
report(context, messages.needSpaceBefore, 'needSpaceBefore', {
node: attrNode,
loc: equalToken.loc.start,
fix(fixer) {
return fixer.insertTextBefore(equalToken, ' ');
},
});
}
if (!spacedAfter) {
report(context, messages.needSpaceAfter, 'needSpaceAfter', {
node: attrNode,
loc: equalToken.loc.start,
fix(fixer) {
return fixer.insertTextAfter(equalToken, ' ');
},
});
}
}
});
},
};
},
};

View File

@@ -0,0 +1,120 @@
/**
* @fileoverview Restrict file extensions that may contain JSX
* @author Joe Lencioni
*/
'use strict';
const path = require('path');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const DEFAULTS = {
allow: 'always',
extensions: ['.jsx'],
ignoreFilesWithoutCode: false,
};
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
noJSXWithExtension: 'JSX not allowed in files with extension \'{{ext}}\'',
extensionOnlyForJSX: 'Only files containing JSX may use the extension \'{{ext}}\'',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow file extensions that may contain JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-filename-extension'),
},
messages,
schema: [{
type: 'object',
properties: {
allow: {
enum: ['always', 'as-needed'],
},
extensions: {
type: 'array',
items: {
type: 'string',
},
},
ignoreFilesWithoutCode: {
type: 'boolean',
},
},
additionalProperties: false,
}],
},
create(context) {
const filename = context.getFilename();
let jsxNode;
if (filename === '<text>') {
// No need to traverse any nodes.
return {};
}
const allow = (context.options[0] && context.options[0].allow) || DEFAULTS.allow;
const allowedExtensions = (context.options[0] && context.options[0].extensions) || DEFAULTS.extensions;
const ignoreFilesWithoutCode = (context.options[0] && context.options[0].ignoreFilesWithoutCode)
|| DEFAULTS.ignoreFilesWithoutCode;
const isAllowedExtension = allowedExtensions.some((extension) => filename.slice(-extension.length) === extension);
function handleJSX(node) {
if (!jsxNode) {
jsxNode = node;
}
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
JSXElement: handleJSX,
JSXFragment: handleJSX,
'Program:exit'(node) {
if (jsxNode) {
if (!isAllowedExtension) {
report(context, messages.noJSXWithExtension, 'noJSXWithExtension', {
node: jsxNode,
data: {
ext: path.extname(filename),
},
});
}
return;
}
if (isAllowedExtension && allow === 'as-needed') {
if (ignoreFilesWithoutCode && node.body.length === 0) {
return;
}
report(context, messages.extensionOnlyForJSX, 'extensionOnlyForJSX', {
node,
data: {
ext: path.extname(filename),
},
});
}
},
};
},
};

View File

@@ -0,0 +1,83 @@
/**
* @fileoverview Ensure proper position of the first property in JSX
* @author Joachim Seminck
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
const propsUtil = require('../util/props');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
propOnNewLine: 'Property should be placed on a new line',
propOnSameLine: 'Property should be placed on the same line as the component declaration',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce proper position of the first property in JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-first-prop-new-line'),
},
fixable: 'code',
messages,
schema: [{
enum: ['always', 'never', 'multiline', 'multiline-multiprop', 'multiprop'],
}],
},
create(context) {
const configuration = context.options[0] || 'multiline-multiprop';
function isMultilineJSX(jsxNode) {
return jsxNode.loc.start.line < jsxNode.loc.end.line;
}
return {
JSXOpeningElement(node) {
if (
(configuration === 'multiline' && isMultilineJSX(node))
|| (configuration === 'multiline-multiprop' && isMultilineJSX(node) && node.attributes.length > 1)
|| (configuration === 'multiprop' && node.attributes.length > 1)
|| (configuration === 'always')
) {
node.attributes.some((decl) => {
if (decl.loc.start.line === node.loc.start.line) {
report(context, messages.propOnNewLine, 'propOnNewLine', {
node: decl,
fix(fixer) {
const nodeTypeArguments = propsUtil.getTypeArguments(node);
return fixer.replaceTextRange([(nodeTypeArguments || node.name).range[1], decl.range[0]], '\n');
},
});
}
return true;
});
} else if (
(configuration === 'never' && node.attributes.length > 0)
|| (configuration === 'multiprop' && isMultilineJSX(node) && node.attributes.length <= 1)
) {
const firstNode = node.attributes[0];
if (node.loc.start.line < firstNode.loc.start.line) {
report(context, messages.propOnSameLine, 'propOnSameLine', {
node: firstNode,
fix(fixer) {
return fixer.replaceTextRange([node.name.range[1], firstNode.range[0]], ' ');
},
});
}
}
},
};
},
};

View File

@@ -0,0 +1,208 @@
/**
* @fileoverview Enforce shorthand or standard form for React fragments.
* @author Alex Zherdev
*/
'use strict';
const elementType = require('jsx-ast-utils/elementType');
const pragmaUtil = require('../util/pragma');
const variableUtil = require('../util/variable');
const testReactVersion = require('../util/version').testReactVersion;
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
const getText = require('../util/eslint').getText;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function replaceNode(source, node, text) {
return `${source.slice(0, node.range[0])}${text}${source.slice(node.range[1])}`;
}
const messages = {
fragmentsNotSupported: 'Fragments are only supported starting from React v16.2. Please disable the `react/jsx-fragments` rule in `eslint` settings or upgrade your version of React.',
preferPragma: 'Prefer {{react}}.{{fragment}} over fragment shorthand',
preferFragment: 'Prefer fragment shorthand over {{react}}.{{fragment}}',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce shorthand or standard form for React fragments',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-fragments'),
},
fixable: 'code',
messages,
schema: [{
enum: ['syntax', 'element'],
}],
},
create(context) {
const configuration = context.options[0] || 'syntax';
const reactPragma = pragmaUtil.getFromContext(context);
const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
const openFragShort = '<>';
const closeFragShort = '</>';
const openFragLong = `<${reactPragma}.${fragmentPragma}>`;
const closeFragLong = `</${reactPragma}.${fragmentPragma}>`;
function reportOnReactVersion(node) {
if (!testReactVersion(context, '>= 16.2.0')) {
report(context, messages.fragmentsNotSupported, 'fragmentsNotSupported', {
node,
});
return true;
}
return false;
}
function getFixerToLong(jsxFragment) {
if (!jsxFragment.closingFragment || !jsxFragment.openingFragment) {
// the old TS parser crashes here
// TODO: FIXME: can we fake these two descriptors?
return null;
}
return function fix(fixer) {
let source = getText(context);
source = replaceNode(source, jsxFragment.closingFragment, closeFragLong);
source = replaceNode(source, jsxFragment.openingFragment, openFragLong);
const lengthDiff = openFragLong.length - getText(context, jsxFragment.openingFragment).length
+ closeFragLong.length - getText(context, jsxFragment.closingFragment).length;
const range = jsxFragment.range;
return fixer.replaceTextRange(range, source.slice(range[0], range[1] + lengthDiff));
};
}
function getFixerToShort(jsxElement) {
return function fix(fixer) {
let source = getText(context);
let lengthDiff;
if (jsxElement.closingElement) {
source = replaceNode(source, jsxElement.closingElement, closeFragShort);
source = replaceNode(source, jsxElement.openingElement, openFragShort);
lengthDiff = getText(context, jsxElement.openingElement).length - openFragShort.length
+ getText(context, jsxElement.closingElement).length - closeFragShort.length;
} else {
source = replaceNode(source, jsxElement.openingElement, `${openFragShort}${closeFragShort}`);
lengthDiff = getText(context, jsxElement.openingElement).length - openFragShort.length
- closeFragShort.length;
}
const range = jsxElement.range;
return fixer.replaceTextRange(range, source.slice(range[0], range[1] - lengthDiff));
};
}
function refersToReactFragment(node, name) {
const variableInit = variableUtil.findVariableByName(context, node, name);
if (!variableInit) {
return false;
}
// const { Fragment } = React;
if (variableInit.type === 'Identifier' && variableInit.name === reactPragma) {
return true;
}
// const Fragment = React.Fragment;
if (
variableInit.type === 'MemberExpression'
&& variableInit.object.type === 'Identifier'
&& variableInit.object.name === reactPragma
&& variableInit.property.type === 'Identifier'
&& variableInit.property.name === fragmentPragma
) {
return true;
}
// const { Fragment } = require('react');
if (
variableInit.callee
&& variableInit.callee.name === 'require'
&& variableInit.arguments
&& variableInit.arguments[0]
&& variableInit.arguments[0].value === 'react'
) {
return true;
}
return false;
}
const jsxElements = [];
const fragmentNames = new Set([`${reactPragma}.${fragmentPragma}`]);
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
JSXElement(node) {
jsxElements.push(node);
},
JSXFragment(node) {
if (reportOnReactVersion(node)) {
return;
}
if (configuration === 'element') {
report(context, messages.preferPragma, 'preferPragma', {
node,
data: {
react: reactPragma,
fragment: fragmentPragma,
},
fix: getFixerToLong(node),
});
}
},
ImportDeclaration(node) {
if (node.source && node.source.value === 'react') {
node.specifiers.forEach((spec) => {
if ('imported' in spec && spec.imported && spec.imported.name === fragmentPragma) {
if (spec.local) {
fragmentNames.add(spec.local.name);
}
}
});
}
},
'Program:exit'() {
jsxElements.forEach((node) => {
const openingEl = node.openingElement;
const elName = elementType(openingEl);
if (fragmentNames.has(elName) || refersToReactFragment(node, elName)) {
if (reportOnReactVersion(node)) {
return;
}
const attrs = openingEl.attributes;
if (configuration === 'syntax' && !(attrs && attrs.length > 0)) {
report(context, messages.preferFragment, 'preferFragment', {
node,
data: {
react: reactPragma,
fragment: fragmentPragma,
},
fix: getFixerToShort(node),
});
}
}
});
},
};
},
};

View File

@@ -0,0 +1,209 @@
/**
* @fileoverview Enforce event handler naming conventions in JSX
* @author Jake Marsh
*/
'use strict';
const minimatch = require('minimatch');
const docsUrl = require('../util/docsUrl');
const getText = require('../util/eslint').getText;
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
badHandlerName: 'Handler function for {{propKey}} prop key must be a camelCase name beginning with \'{{handlerPrefix}}\' only',
badPropKey: 'Prop key for {{propValue}} must begin with \'{{handlerPropPrefix}}\'',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce event handler naming conventions in JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-handler-names'),
},
messages,
schema: [{
anyOf: [
{
type: 'object',
properties: {
eventHandlerPrefix: { type: 'string' },
eventHandlerPropPrefix: { type: 'string' },
checkLocalVariables: { type: 'boolean' },
checkInlineFunction: { type: 'boolean' },
ignoreComponentNames: {
type: 'array',
uniqueItems: true,
items: { type: 'string' },
},
},
additionalProperties: false,
}, {
type: 'object',
properties: {
eventHandlerPrefix: { type: 'string' },
eventHandlerPropPrefix: {
type: 'boolean',
enum: [false],
},
checkLocalVariables: { type: 'boolean' },
checkInlineFunction: { type: 'boolean' },
ignoreComponentNames: {
type: 'array',
uniqueItems: true,
items: { type: 'string' },
},
},
additionalProperties: false,
}, {
type: 'object',
properties: {
eventHandlerPrefix: {
type: 'boolean',
enum: [false],
},
eventHandlerPropPrefix: { type: 'string' },
checkLocalVariables: { type: 'boolean' },
checkInlineFunction: { type: 'boolean' },
ignoreComponentNames: {
type: 'array',
uniqueItems: true,
items: { type: 'string' },
},
},
additionalProperties: false,
}, {
type: 'object',
properties: {
checkLocalVariables: { type: 'boolean' },
},
additionalProperties: false,
}, {
type: 'object',
properties: {
checkInlineFunction: { type: 'boolean' },
},
additionalProperties: false,
},
{
type: 'object',
properties: {
ignoreComponentNames: {
type: 'array',
uniqueItems: true,
items: { type: 'string' },
},
},
},
],
}],
},
create(context) {
function isPrefixDisabled(prefix) {
return prefix === false;
}
function isInlineHandler(node) {
return node.value.expression.type === 'ArrowFunctionExpression';
}
const configuration = context.options[0] || {};
const eventHandlerPrefix = isPrefixDisabled(configuration.eventHandlerPrefix)
? null
: configuration.eventHandlerPrefix || 'handle';
const eventHandlerPropPrefix = isPrefixDisabled(configuration.eventHandlerPropPrefix)
? null
: configuration.eventHandlerPropPrefix || 'on';
const EVENT_HANDLER_REGEX = !eventHandlerPrefix
? null
: new RegExp(`^((props\\.${eventHandlerPropPrefix || ''})|((.*\\.)?${eventHandlerPrefix}))[0-9]*[A-Z].*$`);
const PROP_EVENT_HANDLER_REGEX = !eventHandlerPropPrefix
? null
: new RegExp(`^(${eventHandlerPropPrefix}[A-Z].*|ref)$`);
const checkLocal = !!configuration.checkLocalVariables;
const checkInlineFunction = !!configuration.checkInlineFunction;
const ignoreComponentNames = configuration.ignoreComponentNames || [];
return {
JSXAttribute(node) {
const componentName = node.parent.name.name;
const isComponentNameIgnored = ignoreComponentNames.some((ignoredComponentNamePattern) => {
const isIgnored = minimatch(componentName, ignoredComponentNamePattern);
return isIgnored;
});
if (
!node.value
|| !node.value.expression
|| (!checkInlineFunction && isInlineHandler(node))
|| (
!checkLocal
&& (isInlineHandler(node)
? !node.value.expression.body.callee || !node.value.expression.body.callee.object
: !node.value.expression.object
)
)
|| isComponentNameIgnored
) {
return;
}
const propKey = typeof node.name === 'object' ? node.name.name : node.name;
const expression = node.value.expression;
const propValue = getText(
context,
checkInlineFunction && isInlineHandler(node) ? expression.body.callee : expression
).replace(/\s*/g, '').replace(/^this\.|.*::/, '');
if (propKey === 'ref') {
return;
}
const propIsEventHandler = PROP_EVENT_HANDLER_REGEX && PROP_EVENT_HANDLER_REGEX.test(propKey);
const propFnIsNamedCorrectly = EVENT_HANDLER_REGEX && EVENT_HANDLER_REGEX.test(propValue);
if (
propIsEventHandler
&& propFnIsNamedCorrectly !== null
&& !propFnIsNamedCorrectly
) {
report(context, messages.badHandlerName, 'badHandlerName', {
node,
data: {
propKey,
handlerPrefix: eventHandlerPrefix,
},
});
} else if (
propFnIsNamedCorrectly
&& propIsEventHandler !== null
&& !propIsEventHandler
) {
report(context, messages.badPropKey, 'badPropKey', {
node,
data: {
propValue,
handlerPropPrefix: eventHandlerPropPrefix,
},
});
}
},
};
},
};

View File

@@ -0,0 +1,218 @@
/**
* @fileoverview Validate props indentation in JSX
* @author Yannick Croissant
* This rule has been ported and modified from eslint and nodeca.
* @author Vitaly Puzrin
* @author Gyandeep Singh
* @copyright 2015 Vitaly Puzrin. All rights reserved.
* @copyright 2015 Gyandeep Singh. All rights reserved.
Copyright (C) 2014 by Vitaly Puzrin
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.
*/
'use strict';
const repeat = require('string.prototype.repeat');
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
const getText = require('../util/eslint').getText;
const reportC = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
wrongIndent: 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce props indentation in JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-indent-props'),
},
fixable: 'code',
messages,
schema: [{
anyOf: [{
enum: ['tab', 'first'],
}, {
type: 'integer',
}, {
type: 'object',
properties: {
indentMode: {
anyOf: [{
enum: ['tab', 'first'],
}, {
type: 'integer',
}],
},
ignoreTernaryOperator: {
type: 'boolean',
},
},
}],
}],
},
create(context) {
const extraColumnStart = 0;
let indentType = 'space';
/** @type {number|'first'} */
let indentSize = 4;
const line = {
isUsingOperator: false,
currentOperator: false,
};
let ignoreTernaryOperator = false;
if (context.options.length) {
const isConfigObject = typeof context.options[0] === 'object';
const indentMode = isConfigObject
? context.options[0].indentMode
: context.options[0];
if (indentMode === 'first') {
indentSize = 'first';
indentType = 'space';
} else if (indentMode === 'tab') {
indentSize = 1;
indentType = 'tab';
} else if (typeof indentMode === 'number') {
indentSize = indentMode;
indentType = 'space';
}
if (isConfigObject && context.options[0].ignoreTernaryOperator) {
ignoreTernaryOperator = true;
}
}
/**
* Reports a given indent violation and properly pluralizes the message
* @param {ASTNode} node Node violating the indent rule
* @param {number} needed Expected indentation character count
* @param {number} gotten Indentation character count in the actual node/code
*/
function report(node, needed, gotten) {
const msgContext = {
needed,
type: indentType,
characters: needed === 1 ? 'character' : 'characters',
gotten,
};
reportC(context, messages.wrongIndent, 'wrongIndent', {
node,
data: msgContext,
fix(fixer) {
return fixer.replaceTextRange([node.range[0] - node.loc.start.column, node.range[0]],
repeat(indentType === 'space' ? ' ' : '\t', needed)
);
},
});
}
/**
* Get node indent
* @param {ASTNode} node Node to examine
* @return {number} Indent
*/
function getNodeIndent(node) {
let src = getText(context, node, node.loc.start.column + extraColumnStart);
const lines = src.split('\n');
src = lines[0];
let regExp;
if (indentType === 'space') {
regExp = /^[ ]+/;
} else {
regExp = /^[\t]+/;
}
const indent = regExp.exec(src);
const useOperator = /^([ ]|[\t])*[:]/.test(src) || /^([ ]|[\t])*[?]/.test(src);
const useBracket = /[<]/.test(src);
line.currentOperator = false;
if (useOperator) {
line.isUsingOperator = true;
line.currentOperator = true;
} else if (useBracket) {
line.isUsingOperator = false;
}
return indent ? indent[0].length : 0;
}
/**
* Check indent for nodes list
* @param {ASTNode[]} nodes list of node objects
* @param {number} indent needed indent
*/
function checkNodesIndent(nodes, indent) {
let nestedIndent = indent;
nodes.forEach((node) => {
const nodeIndent = getNodeIndent(node);
if (
line.isUsingOperator
&& !line.currentOperator
&& indentSize !== 'first'
&& !ignoreTernaryOperator
) {
nestedIndent += indentSize;
line.isUsingOperator = false;
}
if (
node.type !== 'ArrayExpression' && node.type !== 'ObjectExpression'
&& nodeIndent !== nestedIndent && astUtil.isNodeFirstInLine(context, node)
) {
report(node, nestedIndent, nodeIndent);
}
});
}
return {
JSXOpeningElement(node) {
if (!node.attributes.length) {
return;
}
let propIndent;
if (indentSize === 'first') {
const firstPropNode = node.attributes[0];
propIndent = firstPropNode.loc.start.column;
} else {
const elementIndent = getNodeIndent(node);
propIndent = elementIndent + indentSize;
}
checkNodesIndent(node.attributes, propIndent);
},
};
},
};

View File

@@ -0,0 +1,447 @@
/**
* @fileoverview Validate JSX indentation
* @author Yannick Croissant
* This rule has been ported and modified from eslint and nodeca.
* @author Vitaly Puzrin
* @author Gyandeep Singh
* @copyright 2015 Vitaly Puzrin. All rights reserved.
* @copyright 2015 Gyandeep Singh. All rights reserved.
Copyright (C) 2014 by Vitaly Puzrin
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.
*/
'use strict';
const matchAll = require('string.prototype.matchall');
const repeat = require('string.prototype.repeat');
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
const reportC = require('../util/report');
const jsxUtil = require('../util/jsx');
const eslintUtil = require('../util/eslint');
const getSourceCode = eslintUtil.getSourceCode;
const getText = eslintUtil.getText;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
wrongIndent: 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce JSX indentation',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-indent'),
},
fixable: 'whitespace',
messages,
schema: [{
anyOf: [{
enum: ['tab'],
}, {
type: 'integer',
}],
}, {
type: 'object',
properties: {
checkAttributes: {
type: 'boolean',
},
indentLogicalExpressions: {
type: 'boolean',
},
},
additionalProperties: false,
}],
},
create(context) {
const extraColumnStart = 0;
let indentType = 'space';
let indentSize = 4;
if (context.options.length) {
if (context.options[0] === 'tab') {
indentSize = 1;
indentType = 'tab';
} else if (typeof context.options[0] === 'number') {
indentSize = context.options[0];
indentType = 'space';
}
}
const indentChar = indentType === 'space' ? ' ' : '\t';
const options = context.options[1] || {};
const checkAttributes = options.checkAttributes || false;
const indentLogicalExpressions = options.indentLogicalExpressions || false;
/**
* Responsible for fixing the indentation issue fix
* @param {ASTNode} node Node violating the indent rule
* @param {number} needed Expected indentation character count
* @returns {Function} function to be executed by the fixer
* @private
*/
function getFixerFunction(node, needed) {
const indent = repeat(indentChar, needed);
if (node.type === 'JSXText' || node.type === 'Literal') {
return function fix(fixer) {
const regExp = /\n[\t ]*(\S)/g;
const fixedText = node.raw.replace(regExp, (match, p1) => `\n${indent}${p1}`);
return fixer.replaceText(node, fixedText);
};
}
if (node.type === 'ReturnStatement') {
const raw = getText(context, node);
const lines = raw.split('\n');
if (lines.length > 1) {
return function fix(fixer) {
const lastLineStart = raw.lastIndexOf('\n');
const lastLine = raw.slice(lastLineStart).replace(/^\n[\t ]*(\S)/, (match, p1) => `\n${indent}${p1}`);
return fixer.replaceTextRange(
[node.range[0] + lastLineStart, node.range[1]],
lastLine
);
};
}
}
return function fix(fixer) {
return fixer.replaceTextRange(
[node.range[0] - node.loc.start.column, node.range[0]],
indent
);
};
}
/**
* Reports a given indent violation and properly pluralizes the message
* @param {ASTNode} node Node violating the indent rule
* @param {number} needed Expected indentation character count
* @param {number} gotten Indentation character count in the actual node/code
* @param {Object} [loc] Error line and column location
*/
function report(node, needed, gotten, loc) {
const msgContext = {
needed,
type: indentType,
characters: needed === 1 ? 'character' : 'characters',
gotten,
};
reportC(context, messages.wrongIndent, 'wrongIndent', Object.assign({
node,
data: msgContext,
fix: getFixerFunction(node, needed),
}, loc && { loc }));
}
/**
* Get node indent
* @param {ASTNode} node Node to examine
* @param {boolean} [byLastLine] get indent of node's last line
* @param {boolean} [excludeCommas] skip comma on start of line
* @return {number} Indent
*/
function getNodeIndent(node, byLastLine, excludeCommas) {
let src = getText(context, node, node.loc.start.column + extraColumnStart);
const lines = src.split('\n');
if (byLastLine) {
src = lines[lines.length - 1];
} else {
src = lines[0];
}
const skip = excludeCommas ? ',' : '';
let regExp;
if (indentType === 'space') {
regExp = new RegExp(`^[ ${skip}]+`);
} else {
regExp = new RegExp(`^[\t${skip}]+`);
}
const indent = regExp.exec(src);
return indent ? indent[0].length : 0;
}
/**
* Check if the node is the right member of a logical expression
* @param {ASTNode} node The node to check
* @return {boolean} true if its the case, false if not
*/
function isRightInLogicalExp(node) {
return (
node.parent
&& node.parent.parent
&& node.parent.parent.type === 'LogicalExpression'
&& node.parent.parent.right === node.parent
&& !indentLogicalExpressions
);
}
/**
* Check if the node is the alternate member of a conditional expression
* @param {ASTNode} node The node to check
* @return {boolean} true if its the case, false if not
*/
function isAlternateInConditionalExp(node) {
return (
node.parent
&& node.parent.parent
&& node.parent.parent.type === 'ConditionalExpression'
&& node.parent.parent.alternate === node.parent
&& getSourceCode(context).getTokenBefore(node).value !== '('
);
}
/**
* Check if the node is within a DoExpression block but not the first expression (which need to be indented)
* @param {ASTNode} node The node to check
* @return {boolean} true if its the case, false if not
*/
function isSecondOrSubsequentExpWithinDoExp(node) {
/*
It returns true when node.parent.parent.parent.parent matches:
DoExpression({
...,
body: BlockStatement({
...,
body: [
..., // 1-n times
ExpressionStatement({
...,
expression: JSXElement({
...,
openingElement: JSXOpeningElement() // the node
})
}),
... // 0-n times
]
})
})
except:
DoExpression({
...,
body: BlockStatement({
...,
body: [
ExpressionStatement({
...,
expression: JSXElement({
...,
openingElement: JSXOpeningElement() // the node
})
}),
... // 0-n times
]
})
})
*/
const isInExpStmt = (
node.parent
&& node.parent.parent
&& node.parent.parent.type === 'ExpressionStatement'
);
if (!isInExpStmt) {
return false;
}
const expStmt = node.parent.parent;
const isInBlockStmtWithinDoExp = (
expStmt.parent
&& expStmt.parent.type === 'BlockStatement'
&& expStmt.parent.parent
&& expStmt.parent.parent.type === 'DoExpression'
);
if (!isInBlockStmtWithinDoExp) {
return false;
}
const blockStmt = expStmt.parent;
const blockStmtFirstExp = blockStmt.body[0];
return !(blockStmtFirstExp === expStmt);
}
/**
* Check indent for nodes list
* @param {ASTNode} node The node to check
* @param {number} indent needed indent
* @param {boolean} [excludeCommas] skip comma on start of line
*/
function checkNodesIndent(node, indent, excludeCommas) {
const nodeIndent = getNodeIndent(node, false, excludeCommas);
const isCorrectRightInLogicalExp = isRightInLogicalExp(node) && (nodeIndent - indent) === indentSize;
const isCorrectAlternateInCondExp = isAlternateInConditionalExp(node) && (nodeIndent - indent) === 0;
if (
nodeIndent !== indent
&& astUtil.isNodeFirstInLine(context, node)
&& !isCorrectRightInLogicalExp
&& !isCorrectAlternateInCondExp
) {
report(node, indent, nodeIndent);
}
}
/**
* Check indent for Literal Node or JSXText Node
* @param {ASTNode} node The node to check
* @param {number} indent needed indent
*/
function checkLiteralNodeIndent(node, indent) {
const value = node.value;
const regExp = indentType === 'space' ? /\n( *)[\t ]*\S/g : /\n(\t*)[\t ]*\S/g;
const nodeIndentsPerLine = Array.from(
matchAll(String(value), regExp),
(match) => (match[1] ? match[1].length : 0)
);
const hasFirstInLineNode = nodeIndentsPerLine.length > 0;
if (
hasFirstInLineNode
&& !nodeIndentsPerLine.every((actualIndent) => actualIndent === indent)
) {
nodeIndentsPerLine.forEach((nodeIndent) => {
report(node, indent, nodeIndent);
});
}
}
function handleOpeningElement(node) {
const sourceCode = getSourceCode(context);
let prevToken = sourceCode.getTokenBefore(node);
if (!prevToken) {
return;
}
// Use the parent in a list or an array
if (prevToken.type === 'JSXText' || ((prevToken.type === 'Punctuator') && prevToken.value === ',')) {
prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
prevToken = prevToken.type === 'Literal' || prevToken.type === 'JSXText' ? prevToken.parent : prevToken;
// Use the first non-punctuator token in a conditional expression
} else if (prevToken.type === 'Punctuator' && prevToken.value === ':') {
do {
prevToken = sourceCode.getTokenBefore(prevToken);
} while (prevToken.type === 'Punctuator' && prevToken.value !== '/');
prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
while (prevToken.parent && prevToken.parent.type !== 'ConditionalExpression') {
prevToken = prevToken.parent;
}
}
prevToken = prevToken.type === 'JSXExpressionContainer' ? prevToken.expression : prevToken;
const parentElementIndent = getNodeIndent(prevToken);
const indent = (
prevToken.loc.start.line === node.loc.start.line
|| isRightInLogicalExp(node)
|| isAlternateInConditionalExp(node)
|| isSecondOrSubsequentExpWithinDoExp(node)
) ? 0 : indentSize;
checkNodesIndent(node, parentElementIndent + indent);
}
function handleClosingElement(node) {
if (!node.parent) {
return;
}
const peerElementIndent = getNodeIndent(node.parent.openingElement || node.parent.openingFragment);
checkNodesIndent(node, peerElementIndent);
}
function handleAttribute(node) {
if (!checkAttributes || (!node.value || node.value.type !== 'JSXExpressionContainer')) {
return;
}
const nameIndent = getNodeIndent(node.name);
const lastToken = getSourceCode(context).getLastToken(node.value);
const firstInLine = astUtil.getFirstNodeInLine(context, lastToken);
const indent = node.name.loc.start.line === firstInLine.loc.start.line ? 0 : nameIndent;
checkNodesIndent(firstInLine, indent);
}
function handleLiteral(node) {
if (!node.parent) {
return;
}
if (node.parent.type !== 'JSXElement' && node.parent.type !== 'JSXFragment') {
return;
}
const parentNodeIndent = getNodeIndent(node.parent);
checkLiteralNodeIndent(node, parentNodeIndent + indentSize);
}
return {
JSXOpeningElement: handleOpeningElement,
JSXOpeningFragment: handleOpeningElement,
JSXClosingElement: handleClosingElement,
JSXClosingFragment: handleClosingElement,
JSXAttribute: handleAttribute,
JSXExpressionContainer(node) {
if (!node.parent) {
return;
}
const parentNodeIndent = getNodeIndent(node.parent);
checkNodesIndent(node, parentNodeIndent + indentSize);
},
Literal: handleLiteral,
JSXText: handleLiteral,
ReturnStatement(node) {
if (
!node.parent
|| !jsxUtil.isJSX(node.argument)
) {
return;
}
let fn = node.parent;
while (fn && fn.type !== 'FunctionDeclaration' && fn.type !== 'FunctionExpression') {
fn = fn.parent;
}
if (
!fn
|| !jsxUtil.isReturningJSX(context, node, true)
) {
return;
}
const openingIndent = getNodeIndent(node);
const closingIndent = getNodeIndent(node, true);
if (openingIndent !== closingIndent) {
report(node, openingIndent, closingIndent);
}
},
};
},
};

303
node_modules/eslint-plugin-react/lib/rules/jsx-key.js generated vendored Normal file
View File

@@ -0,0 +1,303 @@
/**
* @fileoverview Report missing `key` props in iterators/collection literals.
* @author Ben Mosher
*/
'use strict';
const hasProp = require('jsx-ast-utils/hasProp');
const propName = require('jsx-ast-utils/propName');
const values = require('object.values');
const docsUrl = require('../util/docsUrl');
const pragmaUtil = require('../util/pragma');
const report = require('../util/report');
const astUtil = require('../util/ast');
const getText = require('../util/eslint').getText;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const defaultOptions = {
checkFragmentShorthand: false,
checkKeyMustBeforeSpread: false,
warnOnDuplicates: false,
};
const messages = {
missingIterKey: 'Missing "key" prop for element in iterator',
missingIterKeyUsePrag: 'Missing "key" prop for element in iterator. Shorthand fragment syntax does not support providing keys. Use {{reactPrag}}.{{fragPrag}} instead',
missingArrayKey: 'Missing "key" prop for element in array',
missingArrayKeyUsePrag: 'Missing "key" prop for element in array. Shorthand fragment syntax does not support providing keys. Use {{reactPrag}}.{{fragPrag}} instead',
keyBeforeSpread: '`key` prop must be placed before any `{...spread}, to avoid conflicting with Reacts new JSX transform: https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html`',
nonUniqueKeys: '`key` prop must be unique',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow missing `key` props in iterators/collection literals',
category: 'Possible Errors',
recommended: true,
url: docsUrl('jsx-key'),
},
messages,
schema: [{
type: 'object',
properties: {
checkFragmentShorthand: {
type: 'boolean',
default: defaultOptions.checkFragmentShorthand,
},
checkKeyMustBeforeSpread: {
type: 'boolean',
default: defaultOptions.checkKeyMustBeforeSpread,
},
warnOnDuplicates: {
type: 'boolean',
default: defaultOptions.warnOnDuplicates,
},
},
additionalProperties: false,
}],
},
create(context) {
const options = Object.assign({}, defaultOptions, context.options[0]);
const checkFragmentShorthand = options.checkFragmentShorthand;
const checkKeyMustBeforeSpread = options.checkKeyMustBeforeSpread;
const warnOnDuplicates = options.warnOnDuplicates;
const reactPragma = pragmaUtil.getFromContext(context);
const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
function isKeyAfterSpread(attributes) {
let hasFoundSpread = false;
return attributes.some((attribute) => {
if (attribute.type === 'JSXSpreadAttribute') {
hasFoundSpread = true;
return false;
}
if (attribute.type !== 'JSXAttribute') {
return false;
}
return hasFoundSpread && propName(attribute) === 'key';
});
}
function checkIteratorElement(node) {
if (node.type === 'JSXElement') {
if (!hasProp(node.openingElement.attributes, 'key')) {
report(context, messages.missingIterKey, 'missingIterKey', { node });
} else {
const attrs = node.openingElement.attributes;
if (checkKeyMustBeforeSpread && isKeyAfterSpread(attrs)) {
report(context, messages.keyBeforeSpread, 'keyBeforeSpread', { node });
}
}
} else if (checkFragmentShorthand && node.type === 'JSXFragment') {
report(context, messages.missingIterKeyUsePrag, 'missingIterKeyUsePrag', {
node,
data: {
reactPrag: reactPragma,
fragPrag: fragmentPragma,
},
});
}
}
function getReturnStatements(node) {
const returnStatements = arguments[1] || [];
if (node.type === 'IfStatement') {
if (node.consequent) {
getReturnStatements(node.consequent, returnStatements);
}
if (node.alternate) {
getReturnStatements(node.alternate, returnStatements);
}
} else if (node.type === 'ReturnStatement') {
returnStatements.push(node);
} else if (Array.isArray(node.body)) {
node.body.forEach((item) => {
if (item.type === 'IfStatement') {
getReturnStatements(item, returnStatements);
}
if (item.type === 'ReturnStatement') {
returnStatements.push(item);
}
});
}
return returnStatements;
}
/**
* Checks if the given node is a function expression or arrow function,
* and checks if there is a missing key prop in return statement's arguments
* @param {ASTNode} node
*/
function checkFunctionsBlockStatement(node) {
if (astUtil.isFunctionLikeExpression(node)) {
if (node.body.type === 'BlockStatement') {
getReturnStatements(node.body)
.filter((returnStatement) => returnStatement && returnStatement.argument)
.forEach((returnStatement) => {
checkIteratorElement(returnStatement.argument);
});
}
}
}
/**
* Checks if the given node is an arrow function that has an JSX Element or JSX Fragment in its body,
* and the JSX is missing a key prop
* @param {ASTNode} node
*/
function checkArrowFunctionWithJSX(node) {
const isArrFn = node && node.type === 'ArrowFunctionExpression';
const shouldCheckNode = (n) => n && (n.type === 'JSXElement' || n.type === 'JSXFragment');
if (isArrFn && shouldCheckNode(node.body)) {
checkIteratorElement(node.body);
}
if (node.body.type === 'ConditionalExpression') {
if (shouldCheckNode(node.body.consequent)) {
checkIteratorElement(node.body.consequent);
}
if (shouldCheckNode(node.body.alternate)) {
checkIteratorElement(node.body.alternate);
}
} else if (node.body.type === 'LogicalExpression' && shouldCheckNode(node.body.right)) {
checkIteratorElement(node.body.right);
}
}
const childrenToArraySelector = `:matches(
CallExpression
[callee.object.object.name=${reactPragma}]
[callee.object.property.name=Children]
[callee.property.name=toArray],
CallExpression
[callee.object.name=Children]
[callee.property.name=toArray]
)`.replace(/\s/g, '');
let isWithinChildrenToArray = false;
const seen = new WeakSet();
return {
[childrenToArraySelector]() {
isWithinChildrenToArray = true;
},
[`${childrenToArraySelector}:exit`]() {
isWithinChildrenToArray = false;
},
'ArrayExpression, JSXElement > JSXElement'(node) {
if (isWithinChildrenToArray) {
return;
}
const jsx = (node.type === 'ArrayExpression' ? node.elements : node.parent.children).filter((x) => x && x.type === 'JSXElement');
if (jsx.length === 0) {
return;
}
const map = {};
jsx.forEach((element) => {
const attrs = element.openingElement.attributes;
const keys = attrs.filter((x) => x.name && x.name.name === 'key');
if (keys.length === 0) {
if (node.type === 'ArrayExpression') {
report(context, messages.missingArrayKey, 'missingArrayKey', {
node: element,
});
}
} else {
keys.forEach((attr) => {
const value = getText(context, attr.value);
if (!map[value]) { map[value] = []; }
map[value].push(attr);
if (checkKeyMustBeforeSpread && isKeyAfterSpread(attrs)) {
report(context, messages.keyBeforeSpread, 'keyBeforeSpread', {
node: node.type === 'ArrayExpression' ? node : node.parent,
});
}
});
}
});
if (warnOnDuplicates) {
values(map).filter((v) => v.length > 1).forEach((v) => {
v.forEach((n) => {
if (!seen.has(n)) {
seen.add(n);
report(context, messages.nonUniqueKeys, 'nonUniqueKeys', {
node: n,
});
}
});
});
}
},
JSXFragment(node) {
if (!checkFragmentShorthand || isWithinChildrenToArray) {
return;
}
if (node.parent.type === 'ArrayExpression') {
report(context, messages.missingArrayKeyUsePrag, 'missingArrayKeyUsePrag', {
node,
data: {
reactPrag: reactPragma,
fragPrag: fragmentPragma,
},
});
}
},
// Array.prototype.map
// eslint-disable-next-line no-multi-str
'CallExpression[callee.type="MemberExpression"][callee.property.name="map"],\
CallExpression[callee.type="OptionalMemberExpression"][callee.property.name="map"],\
OptionalCallExpression[callee.type="MemberExpression"][callee.property.name="map"],\
OptionalCallExpression[callee.type="OptionalMemberExpression"][callee.property.name="map"]'(node) {
if (isWithinChildrenToArray) {
return;
}
const fn = node.arguments.length > 0 && node.arguments[0];
if (!fn || !astUtil.isFunctionLikeExpression(fn)) {
return;
}
checkArrowFunctionWithJSX(fn);
checkFunctionsBlockStatement(fn);
},
// Array.from
'CallExpression[callee.type="MemberExpression"][callee.property.name="from"]'(node) {
if (isWithinChildrenToArray) {
return;
}
const fn = node.arguments.length > 1 && node.arguments[1];
if (!astUtil.isFunctionLikeExpression(fn)) {
return;
}
checkArrowFunctionWithJSX(fn);
checkFunctionsBlockStatement(fn);
},
};
},
};

View File

@@ -0,0 +1,162 @@
/**
* @fileoverview Validate JSX maximum depth
* @author Chris<wfsr@foxmail.com>
*/
'use strict';
const has = require('hasown');
const includes = require('array-includes');
const variableUtil = require('../util/variable');
const jsxUtil = require('../util/jsx');
const docsUrl = require('../util/docsUrl');
const reportC = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
wrongDepth: 'Expected the depth of nested jsx elements to be <= {{needed}}, but found {{found}}.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce JSX maximum depth',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-max-depth'),
},
messages,
schema: [
{
type: 'object',
properties: {
max: {
type: 'integer',
minimum: 0,
},
},
additionalProperties: false,
},
],
},
create(context) {
const DEFAULT_DEPTH = 2;
const option = context.options[0] || {};
const maxDepth = has(option, 'max') ? option.max : DEFAULT_DEPTH;
function isExpression(node) {
return node.type === 'JSXExpressionContainer';
}
function hasJSX(node) {
return jsxUtil.isJSX(node) || (isExpression(node) && jsxUtil.isJSX(node.expression));
}
function isLeaf(node) {
const children = node.children;
return !children || children.length === 0 || !children.some(hasJSX);
}
function getDepth(node) {
let count = 0;
while (jsxUtil.isJSX(node.parent) || isExpression(node.parent)) {
node = node.parent;
if (jsxUtil.isJSX(node)) {
count += 1;
}
}
return count;
}
function report(node, depth) {
reportC(context, messages.wrongDepth, 'wrongDepth', {
node,
data: {
found: depth,
needed: maxDepth,
},
});
}
function findJSXElementOrFragment(startNode, name, previousReferences) {
function find(refs, prevRefs) {
for (let i = refs.length - 1; i >= 0; i--) {
if (typeof refs[i].writeExpr !== 'undefined') {
const writeExpr = refs[i].writeExpr;
return (jsxUtil.isJSX(writeExpr)
&& writeExpr)
|| ((writeExpr && writeExpr.type === 'Identifier')
&& findJSXElementOrFragment(startNode, writeExpr.name, prevRefs));
}
}
return null;
}
const variable = variableUtil.getVariableFromContext(context, startNode, name);
if (variable && variable.references) {
const containDuplicates = previousReferences.some((ref) => includes(variable.references, ref));
// Prevent getting stuck in circular references
if (containDuplicates) {
return false;
}
return find(variable.references, previousReferences.concat(variable.references));
}
return false;
}
function checkDescendant(baseDepth, children) {
baseDepth += 1;
(children || []).filter((node) => hasJSX(node)).forEach((node) => {
if (baseDepth > maxDepth) {
report(node, baseDepth);
} else if (!isLeaf(node)) {
checkDescendant(baseDepth, node.children);
}
});
}
function handleJSX(node) {
if (!isLeaf(node)) {
return;
}
const depth = getDepth(node);
if (depth > maxDepth) {
report(node, depth);
}
}
return {
JSXElement: handleJSX,
JSXFragment: handleJSX,
JSXExpressionContainer(node) {
if (node.expression.type !== 'Identifier') {
return;
}
const element = findJSXElementOrFragment(node, node.expression.name, []);
if (element) {
const baseDepth = getDepth(node);
checkDescendant(baseDepth, element.children);
}
},
};
},
};

View File

@@ -0,0 +1,155 @@
/**
* @fileoverview Limit maximum of props on a single line in JSX
* @author Yannick Croissant
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const getText = require('../util/eslint').getText;
const report = require('../util/report');
function getPropName(context, propNode) {
if (propNode.type === 'JSXSpreadAttribute') {
return getText(context, propNode.argument);
}
return propNode.name.name;
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
newLine: 'Prop `{{prop}}` must be placed on a new line',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce maximum of props on a single line in JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-max-props-per-line'),
},
fixable: 'code',
messages,
schema: [{
anyOf: [{
type: 'object',
properties: {
maximum: {
type: 'object',
properties: {
single: {
type: 'integer',
minimum: 1,
},
multi: {
type: 'integer',
minimum: 1,
},
},
},
},
additionalProperties: false,
}, {
type: 'object',
properties: {
maximum: {
type: 'number',
minimum: 1,
},
when: {
type: 'string',
enum: ['always', 'multiline'],
},
},
additionalProperties: false,
}],
}],
},
create(context) {
const configuration = context.options[0] || {};
const maximum = configuration.maximum || 1;
const maxConfig = typeof maximum === 'number'
? {
single: configuration.when === 'multiline' ? Infinity : maximum,
multi: maximum,
}
: {
single: maximum.single || Infinity,
multi: maximum.multi || Infinity,
};
function generateFixFunction(line, max) {
const output = [];
const front = line[0].range[0];
const back = line[line.length - 1].range[1];
for (let i = 0; i < line.length; i += max) {
const nodes = line.slice(i, i + max);
output.push(nodes.reduce((prev, curr) => {
if (prev === '') {
return getText(context, curr);
}
return `${prev} ${getText(context, curr)}`;
}, ''));
}
const code = output.join('\n');
return function fix(fixer) {
return fixer.replaceTextRange([front, back], code);
};
}
return {
JSXOpeningElement(node) {
if (!node.attributes.length) {
return;
}
const isSingleLineTag = node.loc.start.line === node.loc.end.line;
if ((isSingleLineTag ? maxConfig.single : maxConfig.multi) === Infinity) {
return;
}
const firstProp = node.attributes[0];
const linePartitionedProps = [[firstProp]];
node.attributes.reduce((last, decl) => {
if (last.loc.end.line === decl.loc.start.line) {
linePartitionedProps[linePartitionedProps.length - 1].push(decl);
} else {
linePartitionedProps.push([decl]);
}
return decl;
});
linePartitionedProps.forEach((propsInLine) => {
const maxPropsCountPerLine = isSingleLineTag && propsInLine[0].loc.start.line === node.loc.start.line
? maxConfig.single
: maxConfig.multi;
if (propsInLine.length > maxPropsCountPerLine) {
const name = getPropName(context, propsInLine[maxPropsCountPerLine]);
report(context, messages.newLine, 'newLine', {
node: propsInLine[maxPropsCountPerLine],
data: {
prop: name,
},
fix: generateFixFunction(propsInLine, maxPropsCountPerLine),
});
}
});
},
};
},
};

View File

@@ -0,0 +1,166 @@
/**
* @fileoverview Require or prevent a new line after jsx elements and expressions.
* @author Johnny Zabala
* @author Joseph Stiles
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const getText = require('../util/eslint').getText;
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
require: 'JSX element should start in a new line',
prevent: 'JSX element should not start in a new line',
allowMultilines: 'Multiline JSX elements should start in a new line',
};
function isMultilined(node) {
return node && node.loc.start.line !== node.loc.end.line;
}
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Require or prevent a new line after jsx elements and expressions.',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-newline'),
},
fixable: 'code',
messages,
schema: [
{
type: 'object',
properties: {
prevent: {
default: false,
type: 'boolean',
},
allowMultilines: {
default: false,
type: 'boolean',
},
},
additionalProperties: false,
if: {
properties: {
allowMultilines: {
const: true,
},
},
},
then: {
properties: {
prevent: {
const: true,
},
},
required: [
'prevent',
],
},
},
],
},
create(context) {
const jsxElementParents = new Set();
function isBlockCommentInCurlyBraces(element) {
const elementRawValue = getText(context, element);
return /^\s*{\/\*/.test(elementRawValue);
}
function isNonBlockComment(element) {
return !isBlockCommentInCurlyBraces(element) && (element.type === 'JSXElement' || element.type === 'JSXExpressionContainer');
}
return {
'Program:exit'() {
jsxElementParents.forEach((parent) => {
parent.children.forEach((element, index, elements) => {
if (element.type === 'JSXElement' || element.type === 'JSXExpressionContainer') {
const configuration = context.options[0] || {};
const prevent = configuration.prevent || false;
const allowMultilines = configuration.allowMultilines || false;
const firstAdjacentSibling = elements[index + 1];
const secondAdjacentSibling = elements[index + 2];
const hasSibling = firstAdjacentSibling
&& secondAdjacentSibling
&& (firstAdjacentSibling.type === 'Literal' || firstAdjacentSibling.type === 'JSXText');
if (!hasSibling) return;
// Check adjacent sibling has the proper amount of newlines
const isWithoutNewLine = !/\n\s*\n/.test(firstAdjacentSibling.value);
if (isBlockCommentInCurlyBraces(element)) return;
if (
allowMultilines
&& (
isMultilined(element)
|| isMultilined(elements.slice(index + 2).find(isNonBlockComment))
)
) {
if (!isWithoutNewLine) return;
const regex = /(\n)(?!.*\1)/g;
const replacement = '\n\n';
const messageId = 'allowMultilines';
report(context, messages[messageId], messageId, {
node: secondAdjacentSibling,
fix(fixer) {
return fixer.replaceText(
firstAdjacentSibling,
getText(context, firstAdjacentSibling).replace(regex, replacement)
);
},
});
return;
}
if (isWithoutNewLine === prevent) return;
const messageId = prevent
? 'prevent'
: 'require';
const regex = prevent
? /(\n\n)(?!.*\1)/g
: /(\n)(?!.*\1)/g;
const replacement = prevent
? '\n'
: '\n\n';
report(context, messages[messageId], messageId, {
node: secondAdjacentSibling,
fix(fixer) {
return fixer.replaceText(
firstAdjacentSibling,
// double or remove the last newline
getText(context, firstAdjacentSibling).replace(regex, replacement)
);
},
});
}
});
});
},
':matches(JSXElement, JSXFragment) > :matches(JSXElement, JSXExpressionContainer)': (node) => {
jsxElementParents.add(node.parent);
},
};
},
};

View File

@@ -0,0 +1,209 @@
/**
* @fileoverview Prevents usage of Function.prototype.bind and arrow functions
* in React component props.
* @author Daniel Lo Nigro <dan.cx>
* @author Jacky Ho
*/
'use strict';
const propName = require('jsx-ast-utils/propName');
const docsUrl = require('../util/docsUrl');
const astUtil = require('../util/ast');
const jsxUtil = require('../util/jsx');
const report = require('../util/report');
const getAncestors = require('../util/eslint').getAncestors;
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
const messages = {
bindCall: 'JSX props should not use .bind()',
arrowFunc: 'JSX props should not use arrow functions',
bindExpression: 'JSX props should not use ::',
func: 'JSX props should not use functions',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow `.bind()` or arrow functions in JSX props',
category: 'Best Practices',
recommended: false,
url: docsUrl('jsx-no-bind'),
},
messages,
schema: [{
type: 'object',
properties: {
allowArrowFunctions: {
default: false,
type: 'boolean',
},
allowBind: {
default: false,
type: 'boolean',
},
allowFunctions: {
default: false,
type: 'boolean',
},
ignoreRefs: {
default: false,
type: 'boolean',
},
ignoreDOMComponents: {
default: false,
type: 'boolean',
},
},
additionalProperties: false,
}],
},
create(context) {
const configuration = context.options[0] || {};
// Keep track of all the variable names pointing to a bind call,
// bind expression or an arrow function in different block statements
const blockVariableNameSets = {};
/**
* @param {string | number} blockStart
*/
function setBlockVariableNameSet(blockStart) {
blockVariableNameSets[blockStart] = {
arrowFunc: new Set(),
bindCall: new Set(),
bindExpression: new Set(),
func: new Set(),
};
}
function getNodeViolationType(node) {
if (
!configuration.allowBind
&& astUtil.isCallExpression(node)
&& node.callee.type === 'MemberExpression'
&& node.callee.property.type === 'Identifier'
&& node.callee.property.name === 'bind'
) {
return 'bindCall';
}
if (node.type === 'ConditionalExpression') {
return getNodeViolationType(node.test)
|| getNodeViolationType(node.consequent)
|| getNodeViolationType(node.alternate);
}
if (!configuration.allowArrowFunctions && node.type === 'ArrowFunctionExpression') {
return 'arrowFunc';
}
if (
!configuration.allowFunctions
&& (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration')
) {
return 'func';
}
if (!configuration.allowBind && node.type === 'BindExpression') {
return 'bindExpression';
}
return null;
}
/**
* @param {string | number} violationType
* @param {unknown} variableName
* @param {string | number} blockStart
*/
function addVariableNameToSet(violationType, variableName, blockStart) {
blockVariableNameSets[blockStart][violationType].add(variableName);
}
function getBlockStatementAncestors(node) {
return getAncestors(context, node).filter(
(ancestor) => ancestor.type === 'BlockStatement'
).reverse();
}
function reportVariableViolation(node, name, blockStart) {
const blockSets = blockVariableNameSets[blockStart];
const violationTypes = Object.keys(blockSets);
return violationTypes.find((type) => {
if (blockSets[type].has(name)) {
report(context, messages[type], type, {
node,
});
return true;
}
return false;
});
}
function findVariableViolation(node, name) {
getBlockStatementAncestors(node).find(
(block) => reportVariableViolation(node, name, block.range[0])
);
}
return {
BlockStatement(node) {
setBlockVariableNameSet(node.range[0]);
},
FunctionDeclaration(node) {
const blockAncestors = getBlockStatementAncestors(node);
const variableViolationType = getNodeViolationType(node);
if (blockAncestors.length > 0 && variableViolationType) {
addVariableNameToSet(variableViolationType, node.id.name, blockAncestors[0].range[0]);
}
},
VariableDeclarator(node) {
if (!node.init) {
return;
}
const blockAncestors = getBlockStatementAncestors(node);
const variableViolationType = getNodeViolationType(node.init);
if (
blockAncestors.length > 0
&& variableViolationType
&& 'kind' in node.parent
&& node.parent.kind === 'const' // only support const right now
) {
addVariableNameToSet(variableViolationType, 'name' in node.id ? node.id.name : undefined, blockAncestors[0].range[0]);
}
},
JSXAttribute(node) {
const isRef = configuration.ignoreRefs && propName(node) === 'ref';
if (isRef || !node.value || !node.value.expression) {
return;
}
const isDOMComponent = jsxUtil.isDOMComponent(node.parent);
if (configuration.ignoreDOMComponents && isDOMComponent) {
return;
}
const valueNode = node.value.expression;
const valueNodeType = valueNode.type;
const nodeViolationType = getNodeViolationType(valueNode);
if (valueNodeType === 'Identifier') {
findVariableViolation(node, valueNode.name);
} else if (nodeViolationType) {
report(context, messages[nodeViolationType], nodeViolationType, {
node,
});
}
},
};
},
};

View File

@@ -0,0 +1,71 @@
/**
* @fileoverview Comments inside children section of tag should be placed inside braces.
* @author Ben Vinegar
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const getText = require('../util/eslint').getText;
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
putCommentInBraces: 'Comments inside children section of tag should be placed inside braces',
};
/**
* @param {Context} context
* @param {ASTNode} node
* @returns {void}
*/
function checkText(context, node) {
// since babel-eslint has the wrong node.raw, we'll get the source text
const rawValue = getText(context, node);
if (/^\s*\/(\/|\*)/m.test(rawValue)) {
// inside component, e.g. <div>literal</div>
if (
node.parent.type !== 'JSXAttribute'
&& node.parent.type !== 'JSXExpressionContainer'
&& node.parent.type.indexOf('JSX') !== -1
) {
report(context, messages.putCommentInBraces, 'putCommentInBraces', {
node,
});
}
}
}
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow comments from being inserted as text nodes',
category: 'Possible Errors',
recommended: true,
url: docsUrl('jsx-no-comment-textnodes'),
},
messages,
schema: [],
},
create(context) {
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
Literal(node) {
checkText(context, node);
},
JSXText(node) {
checkText(context, node);
},
};
},
};

View File

@@ -0,0 +1,227 @@
/**
* @fileoverview Prevents jsx context provider values from taking values that
* will cause needless rerenders.
* @author Dylan Oshima
*/
'use strict';
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const getScope = require('../util/eslint').getScope;
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
// Recursively checks if an element is a construction.
// A construction is a variable that changes identity every render.
function isConstruction(node, callScope) {
switch (node.type) {
case 'Literal':
if (node.regex != null) {
return { type: 'regular expression', node };
}
return null;
case 'Identifier': {
const variableScoping = callScope.set.get(node.name);
if (variableScoping == null || variableScoping.defs == null) {
// If it's not in scope, we don't care.
return null; // Handled
}
// Gets the last variable identity
const variableDefs = variableScoping.defs;
const def = variableDefs[variableDefs.length - 1];
if (def != null
&& def.type !== 'Variable'
&& def.type !== 'FunctionName'
) {
// Parameter or an unusual pattern. Bail out.
return null; // Unhandled
}
if (def.node.type === 'FunctionDeclaration') {
return { type: 'function declaration', node: def.node, usage: node };
}
const init = def.node.init;
if (init == null) {
return null;
}
const initConstruction = isConstruction(init, callScope);
if (initConstruction == null) {
return null;
}
return {
type: initConstruction.type,
node: initConstruction.node,
usage: node,
};
}
case 'ObjectExpression':
// Any object initialized inline will create a new identity
return { type: 'object', node };
case 'ArrayExpression':
return { type: 'array', node };
case 'ArrowFunctionExpression':
case 'FunctionExpression':
// Functions that are initialized inline will have a new identity
return { type: 'function expression', node };
case 'ClassExpression':
return { type: 'class expression', node };
case 'NewExpression':
// `const a = new SomeClass();` is a construction
return { type: 'new expression', node };
case 'ConditionalExpression':
return (isConstruction(node.consequent, callScope)
|| isConstruction(node.alternate, callScope)
);
case 'LogicalExpression':
return (isConstruction(node.left, callScope)
|| isConstruction(node.right, callScope)
);
case 'MemberExpression': {
const objConstruction = isConstruction(node.object, callScope);
if (objConstruction == null) {
return null;
}
return {
type: objConstruction.type,
node: objConstruction.node,
usage: node.object,
};
}
case 'JSXFragment':
return { type: 'JSX fragment', node };
case 'JSXElement':
return { type: 'JSX element', node };
case 'AssignmentExpression': {
const construct = isConstruction(node.right, callScope);
if (construct != null) {
return {
type: 'assignment expression',
node: construct.node,
usage: node,
};
}
return null;
}
case 'TypeCastExpression':
case 'TSAsExpression':
return isConstruction(node.expression, callScope);
default:
return null;
}
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
withIdentifierMsg: "The '{{variableName}}' {{type}} (at line {{nodeLine}}) passed as the value prop to the Context provider (at line {{usageLine}}) changes every render. To fix this consider wrapping it in a useMemo hook.",
withIdentifierMsgFunc: "The '{{variableName}}' {{type}} (at line {{nodeLine}}) passed as the value prop to the Context provider (at line {{usageLine}}) changes every render. To fix this consider wrapping it in a useCallback hook.",
defaultMsg: 'The {{type}} passed as the value prop to the Context provider (at line {{nodeLine}}) changes every render. To fix this consider wrapping it in a useMemo hook.',
defaultMsgFunc: 'The {{type}} passed as the value prop to the Context provider (at line {{nodeLine}}) changes every render. To fix this consider wrapping it in a useCallback hook.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallows JSX context provider values from taking values that will cause needless rerenders',
category: 'Best Practices',
recommended: false,
url: docsUrl('jsx-no-constructed-context-values'),
},
messages,
schema: false,
},
// eslint-disable-next-line arrow-body-style
create: Components.detect((context, components, utils) => {
return {
JSXOpeningElement(node) {
const openingElementName = node.name;
if (openingElementName.type !== 'JSXMemberExpression') {
// Has no member
return;
}
const isJsxContext = openingElementName.property.name === 'Provider';
if (!isJsxContext) {
// Member is not Provider
return;
}
// Contexts can take in more than just a value prop
// so we need to iterate through all of them
const jsxValueAttribute = node.attributes.find(
(attribute) => attribute.type === 'JSXAttribute' && attribute.name.name === 'value'
);
if (jsxValueAttribute == null) {
// No value prop was passed
return;
}
const valueNode = jsxValueAttribute.value;
if (!valueNode) {
// attribute is a boolean shorthand
return;
}
if (valueNode.type !== 'JSXExpressionContainer') {
// value could be a literal
return;
}
const valueExpression = valueNode.expression;
const invocationScope = getScope(context, node);
// Check if the value prop is a construction
const constructInfo = isConstruction(valueExpression, invocationScope);
if (constructInfo == null) {
return;
}
if (!utils.getParentComponent(node)) {
return;
}
// Report found error
const constructType = constructInfo.type;
const constructNode = constructInfo.node;
const constructUsage = constructInfo.usage;
const data = {
type: constructType, nodeLine: constructNode.loc.start.line,
};
let messageId = 'defaultMsg';
// Variable passed to value prop
if (constructUsage != null) {
messageId = 'withIdentifierMsg';
data.usageLine = constructUsage.loc.start.line;
data.variableName = constructUsage.name;
}
// Type of expression
if (
constructType === 'function expression'
|| constructType === 'function declaration'
) {
messageId += 'Func';
}
report(context, messages[messageId], messageId, {
node: constructNode,
data,
});
},
};
}),
};

View File

@@ -0,0 +1,77 @@
/**
* @fileoverview Enforce no duplicate props
* @author Markus Ånöstam
*/
'use strict';
const has = require('hasown');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
noDuplicateProps: 'No duplicate props allowed',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow duplicate properties in JSX',
category: 'Possible Errors',
recommended: true,
url: docsUrl('jsx-no-duplicate-props'),
},
messages,
schema: [{
type: 'object',
properties: {
ignoreCase: {
type: 'boolean',
},
},
additionalProperties: false,
}],
},
create(context) {
const configuration = context.options[0] || {};
const ignoreCase = configuration.ignoreCase || false;
return {
JSXOpeningElement(node) {
const props = {};
node.attributes.forEach((decl) => {
if (decl.type === 'JSXSpreadAttribute') {
return;
}
let name = decl.name.name;
if (typeof name !== 'string') {
return;
}
if (ignoreCase) {
name = name.toLowerCase();
}
if (has(props, name)) {
report(context, messages.noDuplicateProps, 'noDuplicateProps', {
node: decl,
});
} else {
props[name] = 1;
}
});
},
};
},
};

View File

@@ -0,0 +1,204 @@
/**
* @fileoverview Prevent problematic leaked values from being rendered
* @author Mario Beltrán
*/
'use strict';
const find = require('es-iterator-helpers/Iterator.prototype.find');
const from = require('es-iterator-helpers/Iterator.from');
const getText = require('../util/eslint').getText;
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
const variableUtil = require('../util/variable');
const testReactVersion = require('../util/version').testReactVersion;
const isParenthesized = require('../util/ast').isParenthesized;
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
const messages = {
noPotentialLeakedRender: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes',
};
const COERCE_STRATEGY = 'coerce';
const TERNARY_STRATEGY = 'ternary';
const DEFAULT_VALID_STRATEGIES = [TERNARY_STRATEGY, COERCE_STRATEGY];
const COERCE_VALID_LEFT_SIDE_EXPRESSIONS = ['UnaryExpression', 'BinaryExpression', 'CallExpression'];
const TERNARY_INVALID_ALTERNATE_VALUES = [undefined, null, false];
function trimLeftNode(node) {
// Remove double unary expression (boolean coercion), so we avoid trimming valid negations
if (node.type === 'UnaryExpression' && node.argument.type === 'UnaryExpression') {
return trimLeftNode(node.argument.argument);
}
return node;
}
function getIsCoerceValidNestedLogicalExpression(node) {
if (node.type === 'LogicalExpression') {
return getIsCoerceValidNestedLogicalExpression(node.left) && getIsCoerceValidNestedLogicalExpression(node.right);
}
return COERCE_VALID_LEFT_SIDE_EXPRESSIONS.some((validExpression) => validExpression === node.type);
}
function extractExpressionBetweenLogicalAnds(node) {
if (node.type !== 'LogicalExpression') return [node];
if (node.operator !== '&&') return [node];
return [].concat(
extractExpressionBetweenLogicalAnds(node.left),
extractExpressionBetweenLogicalAnds(node.right)
);
}
function ruleFixer(context, fixStrategy, fixer, reportedNode, leftNode, rightNode) {
const rightSideText = getText(context, rightNode);
if (fixStrategy === COERCE_STRATEGY) {
const expressions = extractExpressionBetweenLogicalAnds(leftNode);
const newText = expressions.map((node) => {
let nodeText = getText(context, node);
if (isParenthesized(context, node)) {
nodeText = `(${nodeText})`;
}
if (node.parent && node.parent.type === 'ConditionalExpression' && node.parent.consequent.value === false) {
return `${getIsCoerceValidNestedLogicalExpression(node) ? '' : '!'}${nodeText}`;
}
return `${getIsCoerceValidNestedLogicalExpression(node) ? '' : '!!'}${nodeText}`;
}).join(' && ');
if (rightNode.parent && rightNode.parent.type === 'ConditionalExpression' && rightNode.parent.consequent.value === false) {
const consequentVal = rightNode.parent.consequent.raw || rightNode.parent.consequent.name;
const alternateVal = rightNode.parent.alternate.raw || rightNode.parent.alternate.name;
if (rightNode.parent.test && rightNode.parent.test.type === 'LogicalExpression') {
return fixer.replaceText(reportedNode, `${newText} ? ${consequentVal} : ${alternateVal}`);
}
return fixer.replaceText(reportedNode, `${newText} && ${alternateVal}`);
}
if (rightNode.type === 'ConditionalExpression' || rightNode.type === 'LogicalExpression') {
return fixer.replaceText(reportedNode, `${newText} && (${rightSideText})`);
}
if (rightNode.type === 'JSXElement') {
const rightSideTextLines = rightSideText.split('\n');
if (rightSideTextLines.length > 1) {
const rightSideTextLastLine = rightSideTextLines[rightSideTextLines.length - 1];
const indentSpacesStart = ' '.repeat(rightSideTextLastLine.search(/\S/));
const indentSpacesClose = ' '.repeat(rightSideTextLastLine.search(/\S/) - 2);
return fixer.replaceText(reportedNode, `${newText} && (\n${indentSpacesStart}${rightSideText}\n${indentSpacesClose})`);
}
}
if (rightNode.type === 'Literal') {
return null;
}
return fixer.replaceText(reportedNode, `${newText} && ${rightSideText}`);
}
if (fixStrategy === TERNARY_STRATEGY) {
let leftSideText = getText(context, trimLeftNode(leftNode));
if (isParenthesized(context, leftNode)) {
leftSideText = `(${leftSideText})`;
}
return fixer.replaceText(reportedNode, `${leftSideText} ? ${rightSideText} : null`);
}
throw new TypeError('Invalid value for "validStrategies" option');
}
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow problematic leaked values from being rendered',
category: 'Possible Errors',
recommended: false,
url: docsUrl('jsx-no-leaked-render'),
},
messages,
fixable: 'code',
schema: [
{
type: 'object',
properties: {
validStrategies: {
type: 'array',
items: {
enum: [
TERNARY_STRATEGY,
COERCE_STRATEGY,
],
},
uniqueItems: true,
default: DEFAULT_VALID_STRATEGIES,
},
},
additionalProperties: false,
},
],
},
create(context) {
const config = context.options[0] || {};
const validStrategies = new Set(config.validStrategies || DEFAULT_VALID_STRATEGIES);
const fixStrategy = find(from(validStrategies), () => true);
return {
'JSXExpressionContainer > LogicalExpression[operator="&&"]'(node) {
const leftSide = node.left;
const isCoerceValidLeftSide = COERCE_VALID_LEFT_SIDE_EXPRESSIONS
.some((validExpression) => validExpression === leftSide.type);
if (validStrategies.has(COERCE_STRATEGY)) {
if (isCoerceValidLeftSide || getIsCoerceValidNestedLogicalExpression(leftSide)) {
return;
}
const leftSideVar = variableUtil.getVariableFromContext(context, node, leftSide.name);
if (leftSideVar) {
const leftSideValue = leftSideVar.defs
&& leftSideVar.defs.length
&& leftSideVar.defs[0].node.init
&& leftSideVar.defs[0].node.init.value;
if (typeof leftSideValue === 'boolean') {
return;
}
}
}
if (testReactVersion(context, '>= 18') && leftSide.type === 'Literal' && leftSide.value === '') {
return;
}
report(context, messages.noPotentialLeakedRender, 'noPotentialLeakedRender', {
node,
fix(fixer) {
return ruleFixer(context, fixStrategy, fixer, node, leftSide, node.right);
},
});
},
'JSXExpressionContainer > ConditionalExpression'(node) {
if (validStrategies.has(TERNARY_STRATEGY)) {
return;
}
const isValidTernaryAlternate = TERNARY_INVALID_ALTERNATE_VALUES.indexOf(node.alternate.value) === -1;
const isJSXElementAlternate = node.alternate.type === 'JSXElement';
if (isValidTernaryAlternate || isJSXElementAlternate) {
return;
}
report(context, messages.noPotentialLeakedRender, 'noPotentialLeakedRender', {
node,
fix(fixer) {
return ruleFixer(context, fixStrategy, fixer, node, node.test, node.consequent);
},
});
},
};
},
};

View File

@@ -0,0 +1,555 @@
/**
* @fileoverview Prevent using string literals in React component definition
* @author Caleb Morris
* @author David Buchan-Swanson
*/
'use strict';
const iterFrom = require('es-iterator-helpers/Iterator.from');
const map = require('es-iterator-helpers/Iterator.prototype.map');
const some = require('es-iterator-helpers/Iterator.prototype.some');
const flatMap = require('es-iterator-helpers/Iterator.prototype.flatMap');
const fromEntries = require('object.fromentries');
const entries = require('object.entries');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
const getText = require('../util/eslint').getText;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
/**
* @param {unknown} value
* @returns {string | unknown}
*/
function trimIfString(value) {
return typeof value === 'string' ? value.trim() : value;
}
const reOverridableElement = /^[A-Z][\w.]*$/;
const reIsWhiteSpace = /^[\s]+$/;
const jsxElementTypes = new Set(['JSXElement', 'JSXFragment']);
const standardJSXNodeParentTypes = new Set(['JSXAttribute', 'JSXElement', 'JSXExpressionContainer', 'JSXFragment']);
const messages = {
invalidPropValue: 'Invalid prop value: "{{text}}"',
invalidPropValueInElement: 'Invalid prop value: "{{text}}" in {{element}}',
noStringsInAttributes: 'Strings not allowed in attributes: "{{text}}"',
noStringsInAttributesInElement: 'Strings not allowed in attributes: "{{text}}" in {{element}}',
noStringsInJSX: 'Strings not allowed in JSX files: "{{text}}"',
noStringsInJSXInElement: 'Strings not allowed in JSX files: "{{text}}" in {{element}}',
literalNotInJSXExpression: 'Missing JSX expression container around literal string: "{{text}}"',
literalNotInJSXExpressionInElement: 'Missing JSX expression container around literal string: "{{text}}" in {{element}}',
};
/** @type {Exclude<import('eslint').Rule.RuleModule['meta']['schema'], unknown[]>['properties']} */
const commonPropertiesSchema = {
noStrings: {
type: 'boolean',
},
allowedStrings: {
type: 'array',
uniqueItems: true,
items: {
type: 'string',
},
},
ignoreProps: {
type: 'boolean',
},
noAttributeStrings: {
type: 'boolean',
},
};
/**
* @typedef RawElementConfigProperties
* @property {boolean} [noStrings]
* @property {string[]} [allowedStrings]
* @property {boolean} [ignoreProps]
* @property {boolean} [noAttributeStrings]
*
* @typedef RawOverrideConfigProperties
* @property {boolean} [allowElement]
* @property {boolean} [applyToNestedElements=true]
*
* @typedef {RawElementConfigProperties} RawElementConfig
* @typedef {RawElementConfigProperties & RawElementConfigProperties} RawOverrideConfig
*
* @typedef RawElementOverrides
* @property {Record<string, RawOverrideConfig>} [elementOverrides]
*
* @typedef {RawElementConfig & RawElementOverrides} RawConfig
*
* ----------------------------------------------------------------------
*
* @typedef ElementConfigType
* @property {'element'} type
*
* @typedef ElementConfigProperties
* @property {boolean} noStrings
* @property {Set<string>} allowedStrings
* @property {boolean} ignoreProps
* @property {boolean} noAttributeStrings
*
* @typedef OverrideConfigProperties
* @property {'override'} type
* @property {string} name
* @property {boolean} allowElement
* @property {boolean} applyToNestedElements
*
* @typedef {ElementConfigType & ElementConfigProperties} ElementConfig
* @typedef {OverrideConfigProperties & ElementConfigProperties} OverrideConfig
*
* @typedef ElementOverrides
* @property {Record<string, OverrideConfig>} elementOverrides
*
* @typedef {ElementConfig & ElementOverrides} Config
* @typedef {Config | OverrideConfig} ResolvedConfig
*/
/**
* Normalizes the element portion of the config
* @param {RawConfig} config
* @returns {ElementConfig}
*/
function normalizeElementConfig(config) {
return {
type: 'element',
noStrings: !!config.noStrings,
allowedStrings: config.allowedStrings
? new Set(map(iterFrom(config.allowedStrings), trimIfString))
: new Set(),
ignoreProps: !!config.ignoreProps,
noAttributeStrings: !!config.noAttributeStrings,
};
}
/**
* Normalizes the config and applies default values to all config options
* @param {RawConfig} config
* @returns {Config}
*/
function normalizeConfig(config) {
/** @type {Config} */
const normalizedConfig = Object.assign(normalizeElementConfig(config), {
elementOverrides: {},
});
if (config.elementOverrides) {
normalizedConfig.elementOverrides = fromEntries(
flatMap(
iterFrom(entries(config.elementOverrides)),
(entry) => {
const elementName = entry[0];
const rawElementConfig = entry[1];
if (!reOverridableElement.test(elementName)) {
return [];
}
return [[
elementName,
Object.assign(normalizeElementConfig(rawElementConfig), {
type: 'override',
name: elementName,
allowElement: !!rawElementConfig.allowElement,
applyToNestedElements: typeof rawElementConfig.applyToNestedElements === 'undefined' || !!rawElementConfig.applyToNestedElements,
}),
]];
}
)
);
}
return normalizedConfig;
}
const elementOverrides = {
type: 'object',
patternProperties: {
[reOverridableElement.source]: {
type: 'object',
properties: Object.assign(
{ applyToNestedElements: { type: 'boolean' } },
commonPropertiesSchema
),
},
},
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow usage of string literals in JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-no-literals'),
},
messages,
schema: [{
type: 'object',
properties: Object.assign(
{ elementOverrides },
commonPropertiesSchema
),
additionalProperties: false,
}],
},
create(context) {
/** @type {RawConfig} */
const rawConfig = (context.options.length && context.options[0]) || {};
const config = normalizeConfig(rawConfig);
const hasElementOverrides = Object.keys(config.elementOverrides).length > 0;
/** @type {Map<string, string>} */
const renamedImportMap = new Map();
/**
* Determines if the given expression is a require statement. Supports
* nested MemberExpresions. ie `require('foo').nested.property`
* @param {ASTNode} node
* @returns {boolean}
*/
function isRequireStatement(node) {
if (node.type === 'CallExpression') {
if (node.callee.type === 'Identifier') {
return node.callee.name === 'require';
}
}
if (node.type === 'MemberExpression') {
return isRequireStatement(node.object);
}
return false;
}
/** @typedef {{ name: string, compoundName?: string }} ElementNameFragment */
/**
* Gets the name of the given JSX element. Supports nested
* JSXMemeberExpressions. ie `<Namesapce.Component.SubComponent />`
* @param {ASTNode} node
* @returns {ElementNameFragment | undefined}
*/
function getJSXElementName(node) {
if (node.openingElement.name.type === 'JSXIdentifier') {
const name = node.openingElement.name.name;
return {
name: renamedImportMap.get(name) || name,
compoundName: undefined,
};
}
/** @type {string[]} */
const nameFragments = [];
if (node.openingElement.name.type === 'JSXMemberExpression') {
/** @type {ASTNode} */
let current = node.openingElement.name;
while (current.type === 'JSXMemberExpression') {
if (current.property.type === 'JSXIdentifier') {
nameFragments.unshift(current.property.name);
}
current = current.object;
}
if (current.type === 'JSXIdentifier') {
nameFragments.unshift(current.name);
const rootFragment = nameFragments[0];
if (rootFragment) {
const rootFragmentRenamed = renamedImportMap.get(rootFragment);
if (rootFragmentRenamed) {
nameFragments[0] = rootFragmentRenamed;
}
}
const nameFragment = nameFragments[nameFragments.length - 1];
if (nameFragment) {
return {
name: nameFragment,
compoundName: nameFragments.join('.'),
};
}
}
}
}
/**
* Gets all JSXElement ancestor nodes for the given node
* @param {ASTNode} node
* @returns {ASTNode[]}
*/
function getJSXElementAncestors(node) {
/** @type {ASTNode[]} */
const ancestors = [];
let current = node;
while (current) {
if (current.type === 'JSXElement') {
ancestors.push(current);
}
current = current.parent;
}
return ancestors;
}
/**
* @param {ASTNode} node
* @returns {ASTNode}
*/
function getParentIgnoringBinaryExpressions(node) {
let current = node;
while (current.parent.type === 'BinaryExpression') {
current = current.parent;
}
return current.parent;
}
/**
* @param {ASTNode} node
* @returns {{ parent: ASTNode, grandParent: ASTNode }}
*/
function getParentAndGrandParent(node) {
const parent = getParentIgnoringBinaryExpressions(node);
return {
parent,
grandParent: parent.parent,
};
}
/**
* @param {ASTNode} node
* @returns {boolean}
*/
function hasJSXElementParentOrGrandParent(node) {
const ancestors = getParentAndGrandParent(node);
return some(iterFrom([ancestors.parent, ancestors.grandParent]), (parent) => jsxElementTypes.has(parent.type));
}
/**
* Determines whether a given node's value and its immediate parent are
* viable text nodes that can/should be reported on
* @param {ASTNode} node
* @param {ResolvedConfig} resolvedConfig
* @returns {boolean}
*/
function isViableTextNode(node, resolvedConfig) {
const textValues = iterFrom([trimIfString(node.raw), trimIfString(node.value)]);
if (some(textValues, (value) => resolvedConfig.allowedStrings.has(value))) {
return false;
}
const parent = getParentIgnoringBinaryExpressions(node);
let isStandardJSXNode = false;
if (typeof node.value === 'string' && !reIsWhiteSpace.test(node.value) && standardJSXNodeParentTypes.has(parent.type)) {
if (resolvedConfig.noAttributeStrings) {
isStandardJSXNode = parent.type === 'JSXAttribute' || parent.type === 'JSXElement';
} else {
isStandardJSXNode = parent.type !== 'JSXAttribute';
}
}
if (resolvedConfig.noStrings) {
return isStandardJSXNode;
}
return isStandardJSXNode && parent.type !== 'JSXExpressionContainer';
}
/**
* Gets an override config for a given node. For any given node, we also
* need to traverse the ancestor tree to determine if an ancestor's config
* will also apply to the current node.
* @param {ASTNode} node
* @returns {OverrideConfig | undefined}
*/
function getOverrideConfig(node) {
if (!hasElementOverrides) {
return;
}
const allAncestorElements = getJSXElementAncestors(node);
if (!allAncestorElements.length) {
return;
}
for (const ancestorElement of allAncestorElements) {
const isClosestJSXAncestor = ancestorElement === allAncestorElements[0];
const ancestor = getJSXElementName(ancestorElement);
if (ancestor) {
if (ancestor.name) {
const ancestorElements = config.elementOverrides[ancestor.name];
const ancestorConfig = ancestor.compoundName
? config.elementOverrides[ancestor.compoundName] || ancestorElements
: ancestorElements;
if (ancestorConfig) {
if (isClosestJSXAncestor || ancestorConfig.applyToNestedElements) {
return ancestorConfig;
}
}
}
}
}
}
/**
* @param {ResolvedConfig} resolvedConfig
* @returns {boolean}
*/
function shouldAllowElement(resolvedConfig) {
return resolvedConfig.type === 'override' && 'allowElement' in resolvedConfig && !!resolvedConfig.allowElement;
}
/**
* @param {boolean} ancestorIsJSXElement
* @param {ResolvedConfig} resolvedConfig
* @returns {string}
*/
function defaultMessageId(ancestorIsJSXElement, resolvedConfig) {
if (resolvedConfig.noAttributeStrings && !ancestorIsJSXElement) {
return resolvedConfig.type === 'override' ? 'noStringsInAttributesInElement' : 'noStringsInAttributes';
}
if (resolvedConfig.noStrings) {
return resolvedConfig.type === 'override' ? 'noStringsInJSXInElement' : 'noStringsInJSX';
}
return resolvedConfig.type === 'override' ? 'literalNotInJSXExpressionInElement' : 'literalNotInJSXExpression';
}
/**
* @param {ASTNode} node
* @param {string} messageId
* @param {ResolvedConfig} resolvedConfig
*/
function reportLiteralNode(node, messageId, resolvedConfig) {
report(context, messages[messageId], messageId, {
node,
data: {
text: getText(context, node).trim(),
element: resolvedConfig.type === 'override' && 'name' in resolvedConfig ? resolvedConfig.name : undefined,
},
});
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return Object.assign(hasElementOverrides ? {
// Get renamed import local names mapped to their imported name
ImportDeclaration(node) {
node.specifiers
.filter((s) => s.type === 'ImportSpecifier')
.forEach((specifier) => {
renamedImportMap.set(
(specifier.local || specifier.imported).name,
specifier.imported.name
);
});
},
// Get renamed destructured local names mapped to their imported name
VariableDeclaration(node) {
node.declarations
.filter((d) => (
d.type === 'VariableDeclarator'
&& isRequireStatement(d.init)
&& d.id.type === 'ObjectPattern'
))
.forEach((declaration) => {
declaration.id.properties
.filter((property) => (
property.type === 'Property'
&& property.key.type === 'Identifier'
&& property.value.type === 'Identifier'
))
.forEach((property) => {
renamedImportMap.set(property.value.name, property.key.name);
});
});
},
} : false, {
Literal(node) {
const resolvedConfig = getOverrideConfig(node) || config;
const hasJSXParentOrGrandParent = hasJSXElementParentOrGrandParent(node);
if (hasJSXParentOrGrandParent && shouldAllowElement(resolvedConfig)) {
return;
}
if (isViableTextNode(node, resolvedConfig)) {
if (hasJSXParentOrGrandParent || !config.ignoreProps) {
reportLiteralNode(node, defaultMessageId(hasJSXParentOrGrandParent, resolvedConfig), resolvedConfig);
}
}
},
JSXAttribute(node) {
const isLiteralString = node.value && node.value.type === 'Literal'
&& typeof node.value.value === 'string';
const isStringLiteral = node.value && node.value.type === 'StringLiteral';
if (isLiteralString || isStringLiteral) {
const resolvedConfig = getOverrideConfig(node) || config;
if (
resolvedConfig.noStrings
&& !resolvedConfig.ignoreProps
&& !resolvedConfig.allowedStrings.has(node.value.value)
) {
const messageId = resolvedConfig.type === 'override' ? 'invalidPropValueInElement' : 'invalidPropValue';
reportLiteralNode(node, messageId, resolvedConfig);
}
}
},
JSXText(node) {
const resolvedConfig = getOverrideConfig(node) || config;
if (shouldAllowElement(resolvedConfig)) {
return;
}
if (isViableTextNode(node, resolvedConfig)) {
const hasJSXParendOrGrantParent = hasJSXElementParentOrGrandParent(node);
reportLiteralNode(node, defaultMessageId(hasJSXParendOrGrantParent, resolvedConfig), resolvedConfig);
}
},
TemplateLiteral(node) {
const ancestors = getParentAndGrandParent(node);
const isParentJSXExpressionCont = ancestors.parent.type === 'JSXExpressionContainer';
const isParentJSXElement = ancestors.grandParent.type === 'JSXElement';
if (isParentJSXExpressionCont) {
const resolvedConfig = getOverrideConfig(node) || config;
if (
resolvedConfig.noStrings
&& (isParentJSXElement || !resolvedConfig.ignoreProps)
) {
reportLiteralNode(node, defaultMessageId(isParentJSXElement, resolvedConfig), resolvedConfig);
}
}
},
});
},
};

View File

@@ -0,0 +1,143 @@
/**
* @fileoverview Prevent usage of `javascript:` URLs
* @author Sergei Startsev
*/
'use strict';
const includes = require('array-includes');
const docsUrl = require('../util/docsUrl');
const linkComponentsUtil = require('../util/linkComponents');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
// https://github.com/facebook/react/blob/d0ebde77f6d1232cefc0da184d731943d78e86f2/packages/react-dom/src/shared/sanitizeURL.js#L30
/* eslint-disable-next-line max-len, no-control-regex */
const isJavaScriptProtocol = /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
function hasJavaScriptProtocol(attr) {
return attr.value && attr.value.type === 'Literal'
&& isJavaScriptProtocol.test(attr.value.value);
}
function shouldVerifyProp(node, config) {
const name = node.name && node.name.name;
const parentName = node.parent.name && node.parent.name.name;
if (!name || !parentName || !config.has(parentName)) return false;
const attributes = config.get(parentName);
return includes(attributes, name);
}
function parseLegacyOption(config, option) {
option.forEach((opt) => {
config.set(opt.name, opt.props);
});
}
const messages = {
noScriptURL: 'A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow usage of `javascript:` URLs',
category: 'Best Practices',
recommended: false,
url: docsUrl('jsx-no-script-url'),
},
messages,
schema: {
anyOf: [
{
type: 'array',
items: [
{
type: 'array',
uniqueItems: true,
items: {
type: 'object',
properties: {
name: {
type: 'string',
},
props: {
type: 'array',
items: {
type: 'string',
uniqueItems: true,
},
},
},
required: ['name', 'props'],
additionalProperties: false,
},
},
{
type: 'object',
properties: {
includeFromSettings: {
type: 'boolean',
},
},
additionalItems: false,
},
],
additionalItems: false,
},
{
type: 'array',
items: [
{
type: 'object',
properties: {
includeFromSettings: {
type: 'boolean',
},
},
additionalItems: false,
},
],
additionalItems: false,
},
],
},
},
create(context) {
const options = context.options;
const hasLegacyOption = Array.isArray(options[0]);
const legacyOptions = hasLegacyOption ? options[0] : [];
// eslint-disable-next-line no-nested-ternary
const objectOption = (hasLegacyOption && options.length > 1)
? options[1]
: (options.length > 0
? options[0]
: {
includeFromSettings: false,
}
);
const includeFromSettings = objectOption.includeFromSettings;
const linkComponents = linkComponentsUtil.getLinkComponents(includeFromSettings ? context : {});
parseLegacyOption(linkComponents, legacyOptions);
return {
JSXAttribute(node) {
if (shouldVerifyProp(node, linkComponents) && hasJavaScriptProtocol(node)) {
report(context, messages.noScriptURL, 'noScriptURL', {
node,
});
}
},
};
},
};

View File

@@ -0,0 +1,285 @@
/**
* @fileoverview Forbid target='_blank' attribute
* @author Kevin Miller
*/
'use strict';
const includes = require('array-includes');
const docsUrl = require('../util/docsUrl');
const linkComponentsUtil = require('../util/linkComponents');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function findLastIndex(arr, condition) {
for (let i = arr.length - 1; i >= 0; i -= 1) {
if (condition(arr[i])) {
return i;
}
}
return -1;
}
function attributeValuePossiblyBlank(attribute) {
if (!attribute || !attribute.value) {
return false;
}
const value = attribute.value;
if (value.type === 'Literal') {
return typeof value.value === 'string' && value.value.toLowerCase() === '_blank';
}
if (value.type === 'JSXExpressionContainer') {
const expr = value.expression;
if (expr.type === 'Literal') {
return typeof expr.value === 'string' && expr.value.toLowerCase() === '_blank';
}
if (expr.type === 'ConditionalExpression') {
if (expr.alternate.type === 'Literal' && expr.alternate.value && expr.alternate.value.toLowerCase() === '_blank') {
return true;
}
if (expr.consequent.type === 'Literal' && expr.consequent.value && expr.consequent.value.toLowerCase() === '_blank') {
return true;
}
}
}
return false;
}
function hasExternalLink(node, linkAttributes, warnOnSpreadAttributes, spreadAttributeIndex) {
const linkIndex = findLastIndex(node.attributes, (attr) => attr.name && includes(linkAttributes, attr.name.name));
const foundExternalLink = linkIndex !== -1 && ((attr) => attr.value && attr.value.type === 'Literal' && /^(?:\w+:|\/\/)/.test(attr.value.value))(
node.attributes[linkIndex]);
return foundExternalLink || (warnOnSpreadAttributes && linkIndex < spreadAttributeIndex);
}
function hasDynamicLink(node, linkAttributes) {
const dynamicLinkIndex = findLastIndex(node.attributes, (attr) => attr.name
&& includes(linkAttributes, attr.name.name)
&& attr.value
&& attr.value.type === 'JSXExpressionContainer');
if (dynamicLinkIndex !== -1) {
return true;
}
}
/**
* Get the string(s) from a value
* @param {ASTNode} value The AST node being checked.
* @param {ASTNode} targetValue The AST node being checked.
* @returns {string | string[] | null} The string value, or null if not a string.
*/
function getStringFromValue(value, targetValue) {
if (value) {
if (value.type === 'Literal') {
return value.value;
}
if (value.type === 'JSXExpressionContainer') {
if (value.expression.type === 'TemplateLiteral') {
return value.expression.quasis[0].value.cooked;
}
const expr = value.expression;
if (expr && expr.type === 'ConditionalExpression') {
const relValues = [expr.consequent.value, expr.alternate.value];
if (targetValue.type === 'JSXExpressionContainer' && targetValue.expression && targetValue.expression.type === 'ConditionalExpression') {
const targetTestCond = targetValue.expression.test.name;
const relTestCond = value.expression.test.name;
if (targetTestCond === relTestCond) {
const targetBlankIndex = [targetValue.expression.consequent.value, targetValue.expression.alternate.value].indexOf('_blank');
return relValues[targetBlankIndex];
}
}
return relValues;
}
return expr.value;
}
}
return null;
}
function hasSecureRel(node, allowReferrer, warnOnSpreadAttributes, spreadAttributeIndex) {
const relIndex = findLastIndex(node.attributes, (attr) => (attr.type === 'JSXAttribute' && attr.name.name === 'rel'));
const targetIndex = findLastIndex(node.attributes, (attr) => (attr.type === 'JSXAttribute' && attr.name.name === 'target'));
if (relIndex === -1 || (warnOnSpreadAttributes && relIndex < spreadAttributeIndex)) {
return false;
}
const relAttribute = node.attributes[relIndex];
const targetAttributeValue = node.attributes[targetIndex] && node.attributes[targetIndex].value;
const value = getStringFromValue(relAttribute.value, targetAttributeValue);
return [].concat(value).every((item) => {
const tags = typeof item === 'string' ? item.toLowerCase().split(' ') : false;
const noreferrer = tags && tags.indexOf('noreferrer') >= 0;
if (noreferrer) {
return true;
}
const noopener = tags && tags.indexOf('noopener') >= 0;
return allowReferrer && noopener;
});
}
const messages = {
noTargetBlankWithoutNoreferrer: 'Using target="_blank" without rel="noreferrer" (which implies rel="noopener") is a security risk in older browsers: see https://mathiasbynens.github.io/rel-noopener/#recommendations',
noTargetBlankWithoutNoopener: 'Using target="_blank" without rel="noreferrer" or rel="noopener" (the former implies the latter and is preferred due to wider support) is a security risk: see https://mathiasbynens.github.io/rel-noopener/#recommendations',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
fixable: 'code',
docs: {
description: 'Disallow `target="_blank"` attribute without `rel="noreferrer"`',
category: 'Best Practices',
recommended: true,
url: docsUrl('jsx-no-target-blank'),
},
messages,
schema: [{
type: 'object',
properties: {
allowReferrer: {
type: 'boolean',
},
enforceDynamicLinks: {
enum: ['always', 'never'],
},
warnOnSpreadAttributes: {
type: 'boolean',
},
links: {
type: 'boolean',
default: true,
},
forms: {
type: 'boolean',
default: false,
},
},
additionalProperties: false,
}],
},
create(context) {
const configuration = Object.assign(
{
allowReferrer: false,
warnOnSpreadAttributes: false,
links: true,
forms: false,
},
context.options[0]
);
const allowReferrer = configuration.allowReferrer;
const warnOnSpreadAttributes = configuration.warnOnSpreadAttributes;
const enforceDynamicLinks = configuration.enforceDynamicLinks || 'always';
const linkComponents = linkComponentsUtil.getLinkComponents(context);
const formComponents = linkComponentsUtil.getFormComponents(context);
return {
JSXOpeningElement(node) {
const targetIndex = findLastIndex(node.attributes, (attr) => attr.name && attr.name.name === 'target');
const spreadAttributeIndex = findLastIndex(node.attributes, (attr) => (attr.type === 'JSXSpreadAttribute'));
if (linkComponents.has(node.name.name)) {
if (!attributeValuePossiblyBlank(node.attributes[targetIndex])) {
const hasSpread = spreadAttributeIndex >= 0;
if (warnOnSpreadAttributes && hasSpread) {
// continue to check below
} else if ((hasSpread && targetIndex < spreadAttributeIndex) || !hasSpread || !warnOnSpreadAttributes) {
return;
}
}
const linkAttributes = linkComponents.get(node.name.name);
const hasDangerousLink = hasExternalLink(node, linkAttributes, warnOnSpreadAttributes, spreadAttributeIndex)
|| (enforceDynamicLinks === 'always' && hasDynamicLink(node, linkAttributes));
if (hasDangerousLink && !hasSecureRel(node, allowReferrer, warnOnSpreadAttributes, spreadAttributeIndex)) {
const messageId = allowReferrer ? 'noTargetBlankWithoutNoopener' : 'noTargetBlankWithoutNoreferrer';
const relValue = allowReferrer ? 'noopener' : 'noreferrer';
report(context, messages[messageId], messageId, {
node,
fix(fixer) {
// eslint 5 uses `node.attributes`; eslint 6+ uses `node.parent.attributes`
const nodeWithAttrs = node.parent.attributes ? node.parent : node;
// eslint 5 does not provide a `name` property on JSXSpreadElements
const relAttribute = nodeWithAttrs.attributes.find((attr) => attr.name && attr.name.name === 'rel');
if (targetIndex < spreadAttributeIndex || (spreadAttributeIndex >= 0 && !relAttribute)) {
return null;
}
if (!relAttribute) {
return fixer.insertTextAfter(nodeWithAttrs.attributes.slice(-1)[0], ` rel="${relValue}"`);
}
if (!relAttribute.value) {
return fixer.insertTextAfter(relAttribute, `="${relValue}"`);
}
if (relAttribute.value.type === 'Literal') {
const parts = relAttribute.value.value
.split('noreferrer')
.filter(Boolean);
return fixer.replaceText(relAttribute.value, `"${parts.concat('noreferrer').join(' ')}"`);
}
if (relAttribute.value.type === 'JSXExpressionContainer') {
if (relAttribute.value.expression.type === 'Literal') {
if (typeof relAttribute.value.expression.value === 'string') {
const parts = relAttribute.value.expression.value
.split('noreferrer')
.filter(Boolean);
return fixer.replaceText(relAttribute.value.expression, `"${parts.concat('noreferrer').join(' ')}"`);
}
// for undefined, boolean, number, symbol, bigint, and null
return fixer.replaceText(relAttribute.value, '"noreferrer"');
}
}
return null;
},
});
}
}
if (formComponents.has(node.name.name)) {
if (!attributeValuePossiblyBlank(node.attributes[targetIndex])) {
const hasSpread = spreadAttributeIndex >= 0;
if (warnOnSpreadAttributes && hasSpread) {
// continue to check below
} else if (
(hasSpread && targetIndex < spreadAttributeIndex)
|| !hasSpread
|| !warnOnSpreadAttributes
) {
return;
}
}
if (!configuration.forms || hasSecureRel(node)) {
return;
}
const formAttributes = formComponents.get(node.name.name);
if (
hasExternalLink(node, formAttributes)
|| (enforceDynamicLinks === 'always' && hasDynamicLink(node, formAttributes))
) {
const messageId = allowReferrer ? 'noTargetBlankWithoutNoopener' : 'noTargetBlankWithoutNoreferrer';
report(context, messages[messageId], messageId, {
node,
});
}
}
},
};
},
};

View File

@@ -0,0 +1,117 @@
/**
* @fileoverview Disallow undeclared variables in JSX
* @author Yannick Croissant
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const eslintUtil = require('../util/eslint');
const jsxUtil = require('../util/jsx');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
undefined: '\'{{identifier}}\' is not defined.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow undeclared variables in JSX',
category: 'Possible Errors',
recommended: true,
url: docsUrl('jsx-no-undef'),
},
messages,
schema: [{
type: 'object',
properties: {
allowGlobals: {
type: 'boolean',
},
},
additionalProperties: false,
}],
},
create(context) {
const config = context.options[0] || {};
const allowGlobals = config.allowGlobals || false;
/**
* Compare an identifier with the variables declared in the scope
* @param {ASTNode} node - Identifier or JSXIdentifier node
* @returns {void}
*/
function checkIdentifierInJSX(node) {
let scope = eslintUtil.getScope(context, node);
const sourceCode = eslintUtil.getSourceCode(context);
const sourceType = sourceCode.ast.sourceType;
const scopeUpperBound = !allowGlobals && sourceType === 'module' ? 'module' : 'global';
let variables = scope.variables;
let i;
let len;
// Ignore 'this' keyword (also maked as JSXIdentifier when used in JSX)
if (node.name === 'this') {
return;
}
while (scope.type !== scopeUpperBound && scope.type !== 'global') {
scope = scope.upper;
variables = scope.variables.concat(variables);
}
if (scope.childScopes.length) {
variables = scope.childScopes[0].variables.concat(variables);
// Temporary fix for babel-eslint
if (scope.childScopes[0].childScopes.length) {
variables = scope.childScopes[0].childScopes[0].variables.concat(variables);
}
}
for (i = 0, len = variables.length; i < len; i++) {
if (variables[i].name === node.name) {
return;
}
}
report(context, messages.undefined, 'undefined', {
node,
data: {
identifier: node.name,
},
});
}
return {
JSXOpeningElement(node) {
switch (node.name.type) {
case 'JSXIdentifier':
if (jsxUtil.isDOMComponent(node)) {
return;
}
node = node.name;
break;
case 'JSXMemberExpression':
node = node.name;
do {
node = node.object;
} while (node && node.type !== 'JSXIdentifier');
break;
case 'JSXNamespacedName':
return;
default:
break;
}
checkIdentifierInJSX(node);
},
};
},
};

View File

@@ -0,0 +1,259 @@
/**
* @fileoverview Disallow useless fragments
*/
'use strict';
const arrayIncludes = require('array-includes');
const pragmaUtil = require('../util/pragma');
const astUtil = require('../util/ast');
const jsxUtil = require('../util/jsx');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
const getText = require('../util/eslint').getText;
function isJSXText(node) {
return !!node && (node.type === 'JSXText' || node.type === 'Literal');
}
/**
* @param {string} text
* @returns {boolean}
*/
function isOnlyWhitespace(text) {
return text.trim().length === 0;
}
/**
* @param {ASTNode} node
* @returns {boolean}
*/
function isNonspaceJSXTextOrJSXCurly(node) {
return (isJSXText(node) && !isOnlyWhitespace(node.raw)) || node.type === 'JSXExpressionContainer';
}
/**
* Somehow fragment like this is useful: <Foo content={<>ee eeee eeee ...</>} />
* @param {ASTNode} node
* @returns {boolean}
*/
function isFragmentWithOnlyTextAndIsNotChild(node) {
return node.children.length === 1
&& isJSXText(node.children[0])
&& !(node.parent.type === 'JSXElement' || node.parent.type === 'JSXFragment');
}
/**
* @param {string} text
* @returns {string}
*/
function trimLikeReact(text) {
const leadingSpaces = /^\s*/.exec(text)[0];
const trailingSpaces = /\s*$/.exec(text)[0];
const start = arrayIncludes(leadingSpaces, '\n') ? leadingSpaces.length : 0;
const end = arrayIncludes(trailingSpaces, '\n') ? text.length - trailingSpaces.length : text.length;
return text.slice(start, end);
}
/**
* Test if node is like `<Fragment key={_}>_</Fragment>`
* @param {JSXElement} node
* @returns {boolean}
*/
function isKeyedElement(node) {
return node.type === 'JSXElement'
&& node.openingElement.attributes
&& node.openingElement.attributes.some(jsxUtil.isJSXAttributeKey);
}
/**
* @param {ASTNode} node
* @returns {boolean}
*/
function containsCallExpression(node) {
return node
&& node.type === 'JSXExpressionContainer'
&& astUtil.isCallExpression(node.expression);
}
const messages = {
NeedsMoreChildren: 'Fragments should contain more than one child - otherwise, theres no need for a Fragment at all.',
ChildOfHtmlElement: 'Passing a fragment to an HTML element is useless.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
fixable: 'code',
docs: {
description: 'Disallow unnecessary fragments',
category: 'Possible Errors',
recommended: false,
url: docsUrl('jsx-no-useless-fragment'),
},
messages,
schema: [{
type: 'object',
properties: {
allowExpressions: {
type: 'boolean',
},
},
}],
},
create(context) {
const config = context.options[0] || {};
const allowExpressions = config.allowExpressions || false;
const reactPragma = pragmaUtil.getFromContext(context);
const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
/**
* Test whether a node is an padding spaces trimmed by react runtime.
* @param {ASTNode} node
* @returns {boolean}
*/
function isPaddingSpaces(node) {
return isJSXText(node)
&& isOnlyWhitespace(node.raw)
&& arrayIncludes(node.raw, '\n');
}
function isFragmentWithSingleExpression(node) {
const children = node && node.children.filter((child) => !isPaddingSpaces(child));
return (
children
&& children.length === 1
&& children[0].type === 'JSXExpressionContainer'
);
}
/**
* Test whether a JSXElement has less than two children, excluding paddings spaces.
* @param {JSXElement|JSXFragment} node
* @returns {boolean}
*/
function hasLessThanTwoChildren(node) {
if (!node || !node.children) {
return true;
}
/** @type {ASTNode[]} */
const nonPaddingChildren = node.children.filter(
(child) => !isPaddingSpaces(child)
);
if (nonPaddingChildren.length < 2) {
return !containsCallExpression(nonPaddingChildren[0]);
}
}
/**
* @param {JSXElement|JSXFragment} node
* @returns {boolean}
*/
function isChildOfHtmlElement(node) {
return node.parent.type === 'JSXElement'
&& node.parent.openingElement.name.type === 'JSXIdentifier'
&& /^[a-z]+$/.test(node.parent.openingElement.name.name);
}
/**
* @param {JSXElement|JSXFragment} node
* @return {boolean}
*/
function isChildOfComponentElement(node) {
return node.parent.type === 'JSXElement'
&& !isChildOfHtmlElement(node)
&& !jsxUtil.isFragment(node.parent, reactPragma, fragmentPragma);
}
/**
* @param {ASTNode} node
* @returns {boolean}
*/
function canFix(node) {
// Not safe to fix fragments without a jsx parent.
if (!(node.parent.type === 'JSXElement' || node.parent.type === 'JSXFragment')) {
// const a = <></>
if (node.children.length === 0) {
return false;
}
// const a = <>cat {meow}</>
if (node.children.some(isNonspaceJSXTextOrJSXCurly)) {
return false;
}
}
// Not safe to fix `<Eeee><>foo</></Eeee>` because `Eeee` might require its children be a ReactElement.
if (isChildOfComponentElement(node)) {
return false;
}
// old TS parser can't handle this one
if (node.type === 'JSXFragment' && (!node.openingFragment || !node.closingFragment)) {
return false;
}
return true;
}
/**
* @param {ASTNode} node
* @returns {Function | undefined}
*/
function getFix(node) {
if (!canFix(node)) {
return undefined;
}
return function fix(fixer) {
const opener = node.type === 'JSXFragment' ? node.openingFragment : node.openingElement;
const closer = node.type === 'JSXFragment' ? node.closingFragment : node.closingElement;
const childrenText = opener.selfClosing ? '' : getText(context).slice(opener.range[1], closer.range[0]);
return fixer.replaceText(node, trimLikeReact(childrenText));
};
}
function checkNode(node) {
if (isKeyedElement(node)) {
return;
}
if (
hasLessThanTwoChildren(node)
&& !isFragmentWithOnlyTextAndIsNotChild(node)
&& !(allowExpressions && isFragmentWithSingleExpression(node))
) {
report(context, messages.NeedsMoreChildren, 'NeedsMoreChildren', {
node,
fix: getFix(node),
});
}
if (isChildOfHtmlElement(node)) {
report(context, messages.ChildOfHtmlElement, 'ChildOfHtmlElement', {
node,
fix: getFix(node),
});
}
}
return {
JSXElement(node) {
if (jsxUtil.isFragment(node, reactPragma, fragmentPragma)) {
checkNode(node);
}
},
JSXFragment: checkNode,
};
},
};

View File

@@ -0,0 +1,249 @@
/**
* @fileoverview Limit to one expression per line in JSX
* @author Mark Ivan Allen <Vydia.com>
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const eslintUtil = require('../util/eslint');
const jsxUtil = require('../util/jsx');
const report = require('../util/report');
const getSourceCode = eslintUtil.getSourceCode;
const getText = eslintUtil.getText;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const optionDefaults = {
allow: 'none',
};
const messages = {
moveToNewLine: '`{{descriptor}}` must be placed on a new line',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Require one JSX element per line',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-one-expression-per-line'),
},
fixable: 'whitespace',
messages,
schema: [
{
type: 'object',
properties: {
allow: {
enum: ['none', 'literal', 'single-child', 'non-jsx'],
},
},
default: optionDefaults,
additionalProperties: false,
},
],
},
create(context) {
const options = Object.assign({}, optionDefaults, context.options[0]);
function nodeKey(node) {
return `${node.loc.start.line},${node.loc.start.column}`;
}
/**
* @param {ASTNode} n
* @returns {string}
*/
function nodeDescriptor(n) {
return n.openingElement ? n.openingElement.name.name : getText(context, n).replace(/\n/g, '');
}
function handleJSX(node) {
const children = node.children;
if (!children || !children.length) {
return;
}
if (
options.allow === 'non-jsx'
&& !children.find((child) => (child.type === 'JSXFragment' || child.type === 'JSXElement'))
) {
return;
}
const openingElement = node.openingElement || node.openingFragment;
const closingElement = node.closingElement || node.closingFragment;
const openingElementStartLine = openingElement.loc.start.line;
const openingElementEndLine = openingElement.loc.end.line;
const closingElementStartLine = closingElement.loc.start.line;
const closingElementEndLine = closingElement.loc.end.line;
if (children.length === 1) {
const child = children[0];
if (
openingElementStartLine === openingElementEndLine
&& openingElementEndLine === closingElementStartLine
&& closingElementStartLine === closingElementEndLine
&& closingElementEndLine === child.loc.start.line
&& child.loc.start.line === child.loc.end.line
) {
if (
options.allow === 'single-child'
|| (options.allow === 'literal' && (child.type === 'Literal' || child.type === 'JSXText'))
) {
return;
}
}
}
const childrenGroupedByLine = {};
const fixDetailsByNode = {};
children.forEach((child) => {
let countNewLinesBeforeContent = 0;
let countNewLinesAfterContent = 0;
if (child.type === 'Literal' || child.type === 'JSXText') {
if (jsxUtil.isWhiteSpaces(child.raw)) {
return;
}
countNewLinesBeforeContent = (child.raw.match(/^\s*\n/g) || []).length;
countNewLinesAfterContent = (child.raw.match(/\n\s*$/g) || []).length;
}
const startLine = child.loc.start.line + countNewLinesBeforeContent;
const endLine = child.loc.end.line - countNewLinesAfterContent;
if (startLine === endLine) {
if (!childrenGroupedByLine[startLine]) {
childrenGroupedByLine[startLine] = [];
}
childrenGroupedByLine[startLine].push(child);
} else {
if (!childrenGroupedByLine[startLine]) {
childrenGroupedByLine[startLine] = [];
}
childrenGroupedByLine[startLine].push(child);
if (!childrenGroupedByLine[endLine]) {
childrenGroupedByLine[endLine] = [];
}
childrenGroupedByLine[endLine].push(child);
}
});
Object.keys(childrenGroupedByLine).forEach((_line) => {
const line = parseInt(_line, 10);
const firstIndex = 0;
const lastIndex = childrenGroupedByLine[line].length - 1;
childrenGroupedByLine[line].forEach((child, i) => {
let prevChild;
let nextChild;
if (i === firstIndex) {
if (line === openingElementEndLine) {
prevChild = openingElement;
}
} else {
prevChild = childrenGroupedByLine[line][i - 1];
}
if (i === lastIndex) {
if (line === closingElementStartLine) {
nextChild = closingElement;
}
} else {
// We don't need to append a trailing because the next child will prepend a leading.
// nextChild = childrenGroupedByLine[line][i + 1];
}
function spaceBetweenPrev() {
return ((prevChild.type === 'Literal' || prevChild.type === 'JSXText') && / $/.test(prevChild.raw))
|| ((child.type === 'Literal' || child.type === 'JSXText') && /^ /.test(child.raw))
|| getSourceCode(context).isSpaceBetweenTokens(prevChild, child);
}
function spaceBetweenNext() {
return ((nextChild.type === 'Literal' || nextChild.type === 'JSXText') && /^ /.test(nextChild.raw))
|| ((child.type === 'Literal' || child.type === 'JSXText') && / $/.test(child.raw))
|| getSourceCode(context).isSpaceBetweenTokens(child, nextChild);
}
if (!prevChild && !nextChild) {
return;
}
const source = getText(context, child);
const leadingSpace = !!(prevChild && spaceBetweenPrev());
const trailingSpace = !!(nextChild && spaceBetweenNext());
const leadingNewLine = !!prevChild;
const trailingNewLine = !!nextChild;
const key = nodeKey(child);
if (!fixDetailsByNode[key]) {
fixDetailsByNode[key] = {
node: child,
source,
descriptor: nodeDescriptor(child),
};
}
if (leadingSpace) {
fixDetailsByNode[key].leadingSpace = true;
}
if (leadingNewLine) {
fixDetailsByNode[key].leadingNewLine = true;
}
if (trailingNewLine) {
fixDetailsByNode[key].trailingNewLine = true;
}
if (trailingSpace) {
fixDetailsByNode[key].trailingSpace = true;
}
});
});
Object.keys(fixDetailsByNode).forEach((key) => {
const details = fixDetailsByNode[key];
const nodeToReport = details.node;
const descriptor = details.descriptor;
const source = details.source.replace(/(^ +| +(?=\n)*$)/g, '');
const leadingSpaceString = details.leadingSpace ? '\n{\' \'}' : '';
const trailingSpaceString = details.trailingSpace ? '{\' \'}\n' : '';
const leadingNewLineString = details.leadingNewLine ? '\n' : '';
const trailingNewLineString = details.trailingNewLine ? '\n' : '';
const replaceText = `${leadingSpaceString}${leadingNewLineString}${source}${trailingNewLineString}${trailingSpaceString}`;
report(context, messages.moveToNewLine, 'moveToNewLine', {
node: nodeToReport,
data: {
descriptor,
},
fix(fixer) {
return fixer.replaceText(nodeToReport, replaceText);
},
});
});
}
return {
JSXElement: handleJSX,
JSXFragment: handleJSX,
};
},
};

View File

@@ -0,0 +1,164 @@
/**
* @fileoverview Enforce PascalCase for user-defined JSX components
* @author Jake Marsh
*/
'use strict';
const elementType = require('jsx-ast-utils/elementType');
const minimatch = require('minimatch');
const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');
const report = require('../util/report');
function testDigit(char) {
const charCode = char.charCodeAt(0);
return charCode >= 48 && charCode <= 57;
}
function testUpperCase(char) {
const upperCase = char.toUpperCase();
return char === upperCase && upperCase !== char.toLowerCase();
}
function testLowerCase(char) {
const lowerCase = char.toLowerCase();
return char === lowerCase && lowerCase !== char.toUpperCase();
}
function testPascalCase(name) {
if (!testUpperCase(name.charAt(0))) {
return false;
}
const anyNonAlphaNumeric = Array.prototype.some.call(
name.slice(1),
(char) => char.toLowerCase() === char.toUpperCase() && !testDigit(char)
);
if (anyNonAlphaNumeric) {
return false;
}
return Array.prototype.some.call(
name.slice(1),
(char) => testLowerCase(char) || testDigit(char)
);
}
function testAllCaps(name) {
const firstChar = name.charAt(0);
if (!(testUpperCase(firstChar) || testDigit(firstChar))) {
return false;
}
for (let i = 1; i < name.length - 1; i += 1) {
const char = name.charAt(i);
if (!(testUpperCase(char) || testDigit(char) || char === '_')) {
return false;
}
}
const lastChar = name.charAt(name.length - 1);
if (!(testUpperCase(lastChar) || testDigit(lastChar))) {
return false;
}
return true;
}
function ignoreCheck(ignore, name) {
return ignore.some(
(entry) => name === entry || minimatch(name, entry, { noglobstar: true })
);
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
usePascalCase: 'Imported JSX component {{name}} must be in PascalCase',
usePascalOrSnakeCase: 'Imported JSX component {{name}} must be in PascalCase or SCREAMING_SNAKE_CASE',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce PascalCase for user-defined JSX components',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-pascal-case'),
},
messages,
schema: [{
type: 'object',
properties: {
allowAllCaps: {
type: 'boolean',
},
allowLeadingUnderscore: {
type: 'boolean',
},
allowNamespace: {
type: 'boolean',
},
ignore: {
items: [
{
type: 'string',
},
],
minItems: 0,
type: 'array',
uniqueItems: true,
},
},
additionalProperties: false,
}],
},
create(context) {
const configuration = context.options[0] || {};
const allowAllCaps = configuration.allowAllCaps || false;
const allowLeadingUnderscore = configuration.allowLeadingUnderscore || false;
const allowNamespace = configuration.allowNamespace || false;
const ignore = configuration.ignore || [];
return {
JSXOpeningElement(node) {
const isCompatTag = jsxUtil.isDOMComponent(node);
if (isCompatTag) return undefined;
const name = elementType(node);
let checkNames = [name];
let index = 0;
if (name.lastIndexOf(':') > -1) {
checkNames = name.split(':');
} else if (name.lastIndexOf('.') > -1) {
checkNames = name.split('.');
}
do {
const splitName = checkNames[index];
if (splitName.length === 1) return undefined;
const isIgnored = ignoreCheck(ignore, splitName);
const checkName = allowLeadingUnderscore && splitName.startsWith('_') ? splitName.slice(1) : splitName;
const isPascalCase = testPascalCase(checkName);
const isAllowedAllCaps = allowAllCaps && testAllCaps(checkName);
if (!isPascalCase && !isAllowedAllCaps && !isIgnored) {
const messageId = allowAllCaps ? 'usePascalOrSnakeCase' : 'usePascalCase';
report(context, messages[messageId], messageId, {
node,
data: {
name: splitName,
},
});
break;
}
index += 1;
} while (index < checkNames.length && !allowNamespace);
},
};
},
};

View File

@@ -0,0 +1,144 @@
/**
* @fileoverview Disallow multiple spaces between inline JSX props
* @author Adrian Moennich
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const eslintUtil = require('../util/eslint');
const report = require('../util/report');
const propsUtil = require('../util/props');
const getSourceCode = eslintUtil.getSourceCode;
const getText = eslintUtil.getText;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
noLineGap: 'Expected no line gap between “{{prop1}}” and “{{prop2}}”',
onlyOneSpace: 'Expected only one space between “{{prop1}}” and “{{prop2}}”',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow multiple spaces between inline JSX props',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-props-no-multi-spaces'),
},
fixable: 'code',
messages,
schema: [],
},
create(context) {
const sourceCode = getSourceCode(context);
function getPropName(propNode) {
switch (propNode.type) {
case 'JSXSpreadAttribute':
return getText(context, propNode.argument);
case 'JSXIdentifier':
return propNode.name;
case 'JSXMemberExpression':
return `${getPropName(propNode.object)}.${propNode.property.name}`;
default:
return propNode.name
? propNode.name.name
: `${getText(context, propNode.object)}.${propNode.property.name}`; // needed for typescript-eslint parser
}
}
// First and second must be adjacent nodes
function hasEmptyLines(first, second) {
const comments = sourceCode.getCommentsBefore ? sourceCode.getCommentsBefore(second) : [];
const nodes = [].concat(first, comments, second);
for (let i = 1; i < nodes.length; i += 1) {
const prev = nodes[i - 1];
const curr = nodes[i];
if (curr.loc.start.line - prev.loc.end.line >= 2) {
return true;
}
}
return false;
}
function checkSpacing(prev, node) {
if (hasEmptyLines(prev, node)) {
report(context, messages.noLineGap, 'noLineGap', {
node,
data: {
prop1: getPropName(prev),
prop2: getPropName(node),
},
});
}
if (prev.loc.end.line !== node.loc.end.line) {
return;
}
const between = getSourceCode(context).text.slice(prev.range[1], node.range[0]);
if (between !== ' ') {
report(context, messages.onlyOneSpace, 'onlyOneSpace', {
node,
data: {
prop1: getPropName(prev),
prop2: getPropName(node),
},
fix(fixer) {
return fixer.replaceTextRange([prev.range[1], node.range[0]], ' ');
},
});
}
}
function containsGenericType(node) {
const nodeTypeArguments = propsUtil.getTypeArguments(node);
if (typeof nodeTypeArguments === 'undefined') {
return false;
}
return nodeTypeArguments.type === 'TSTypeParameterInstantiation';
}
function getGenericNode(node) {
const name = node.name;
if (containsGenericType(node)) {
const nodeTypeArguments = propsUtil.getTypeArguments(node);
return Object.assign(
{},
node,
{
range: [
name.range[0],
nodeTypeArguments.range[1],
],
}
);
}
return name;
}
return {
JSXOpeningElement(node) {
node.attributes.reduce((prev, prop) => {
checkSpacing(prev, prop);
return prop;
}, getGenericNode(node));
},
};
},
};

View File

@@ -0,0 +1,53 @@
/**
* @fileoverview Prevent JSX prop spreading the same expression multiple times
* @author Simon Schick
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
noMultiSpreading: 'Spreading the same expression multiple times is forbidden',
};
module.exports = {
meta: {
docs: {
description: 'Disallow JSX prop spreading the same identifier multiple times',
category: 'Best Practices',
recommended: false,
url: docsUrl('jsx-props-no-spread-multi'),
},
messages,
},
create(context) {
return {
JSXOpeningElement(node) {
const spreads = node.attributes.filter(
(attr) => attr.type === 'JSXSpreadAttribute'
&& attr.argument.type === 'Identifier'
);
if (spreads.length < 2) {
return;
}
// We detect duplicate expressions by their identifier
const identifierNames = new Set();
spreads.forEach((spread) => {
if (identifierNames.has(spread.argument.name)) {
report(context, messages.noMultiSpreading, 'noMultiSpreading', {
node: spread,
});
}
identifierNames.add(spread.argument.name);
});
},
};
},
};

View File

@@ -0,0 +1,145 @@
/**
* @fileoverview Prevent JSX prop spreading
* @author Ashish Gambhir
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const OPTIONS = { ignore: 'ignore', enforce: 'enforce' };
const DEFAULTS = {
html: OPTIONS.enforce,
custom: OPTIONS.enforce,
explicitSpread: OPTIONS.enforce,
exceptions: [],
};
const isException = (tag, allExceptions) => allExceptions.indexOf(tag) !== -1;
const isProperty = (property) => property.type === 'Property';
const getTagNameFromMemberExpression = (node) => {
if (node.property.parent) {
return `${node.property.parent.object.name}.${node.property.name}`;
}
// for eslint 3
return `${node.object.name}.${node.property.name}`;
};
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
noSpreading: 'Prop spreading is forbidden',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow JSX prop spreading',
category: 'Best Practices',
recommended: false,
url: docsUrl('jsx-props-no-spreading'),
},
messages,
schema: [{
allOf: [{
type: 'object',
properties: {
html: {
enum: [OPTIONS.enforce, OPTIONS.ignore],
},
custom: {
enum: [OPTIONS.enforce, OPTIONS.ignore],
},
explicitSpread: {
enum: [OPTIONS.enforce, OPTIONS.ignore],
},
exceptions: {
type: 'array',
items: {
type: 'string',
uniqueItems: true,
},
},
},
}, {
not: {
type: 'object',
required: ['html', 'custom'],
properties: {
html: {
enum: [OPTIONS.ignore],
},
custom: {
enum: [OPTIONS.ignore],
},
exceptions: {
type: 'array',
minItems: 0,
maxItems: 0,
},
},
},
}],
}],
},
create(context) {
const configuration = context.options[0] || {};
const ignoreHtmlTags = (configuration.html || DEFAULTS.html) === OPTIONS.ignore;
const ignoreCustomTags = (configuration.custom || DEFAULTS.custom) === OPTIONS.ignore;
const ignoreExplicitSpread = (configuration.explicitSpread || DEFAULTS.explicitSpread) === OPTIONS.ignore;
const exceptions = configuration.exceptions || DEFAULTS.exceptions;
return {
JSXSpreadAttribute(node) {
const jsxOpeningElement = node.parent.name;
const type = jsxOpeningElement.type;
let tagName;
if (type === 'JSXIdentifier') {
tagName = jsxOpeningElement.name;
} else if (type === 'JSXMemberExpression') {
tagName = getTagNameFromMemberExpression(jsxOpeningElement);
} else {
tagName = undefined;
}
const isHTMLTag = tagName && tagName[0] !== tagName[0].toUpperCase();
const isCustomTag = tagName && (tagName[0] === tagName[0].toUpperCase() || tagName.includes('.'));
if (
isHTMLTag
&& ((ignoreHtmlTags && !isException(tagName, exceptions))
|| (!ignoreHtmlTags && isException(tagName, exceptions)))
) {
return;
}
if (
isCustomTag
&& ((ignoreCustomTags && !isException(tagName, exceptions))
|| (!ignoreCustomTags && isException(tagName, exceptions)))
) {
return;
}
if (
ignoreExplicitSpread
&& node.argument.type === 'ObjectExpression'
&& node.argument.properties.every(isProperty)
) {
return;
}
report(context, messages.noSpreading, 'noSpreading', {
node,
});
},
};
},
};

View File

@@ -0,0 +1,195 @@
/**
* @fileoverview Enforce default props alphabetical sorting
* @author Vladimir Kattsov
* @deprecated
*/
'use strict';
const variableUtil = require('../util/variable');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
const log = require('../util/log');
const eslintUtil = require('../util/eslint');
const getFirstTokens = eslintUtil.getFirstTokens;
const getText = eslintUtil.getText;
let isWarnedForDeprecation = false;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
propsNotSorted: 'Default prop types declarations should be sorted alphabetically',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: true,
replacedBy: ['sort-default-props'],
docs: {
description: 'Enforce defaultProps declarations alphabetical sorting',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-sort-default-props'),
},
// fixable: 'code',
messages,
schema: [{
type: 'object',
properties: {
ignoreCase: {
type: 'boolean',
},
},
additionalProperties: false,
}],
},
create(context) {
const configuration = context.options[0] || {};
const ignoreCase = configuration.ignoreCase || false;
/**
* Get properties name
* @param {Object} node - Property.
* @returns {string} Property name.
*/
function getPropertyName(node) {
if (node.key || ['MethodDefinition', 'Property'].indexOf(node.type) !== -1) {
return node.key.name;
}
if (node.type === 'MemberExpression') {
return node.property.name;
// Special case for class properties
// (babel-eslint@5 does not expose property name so we have to rely on tokens)
}
if (node.type === 'ClassProperty') {
const tokens = getFirstTokens(context, node, 2);
return tokens[1] && tokens[1].type === 'Identifier' ? tokens[1].value : tokens[0].value;
}
return '';
}
/**
* Checks if the Identifier node passed in looks like a defaultProps declaration.
* @param {ASTNode} node The node to check. Must be an Identifier node.
* @returns {boolean} `true` if the node is a defaultProps declaration, `false` if not
*/
function isDefaultPropsDeclaration(node) {
const propName = getPropertyName(node);
return (propName === 'defaultProps' || propName === 'getDefaultProps');
}
function getKey(node) {
return getText(context, node.key || node.argument);
}
/**
* Find a variable by name in the current scope.
* @param {ASTNode} node The node to look for.
* @param {string} name Name of the variable to look for.
* @returns {ASTNode|null} Return null if the variable could not be found, ASTNode otherwise.
*/
function findVariableByName(node, name) {
const variable = variableUtil
.getVariableFromContext(context, node, name);
if (!variable || !variable.defs[0] || !variable.defs[0].node) {
return null;
}
if (variable.defs[0].node.type === 'TypeAlias') {
return variable.defs[0].node.right;
}
return variable.defs[0].node.init;
}
/**
* Checks if defaultProps declarations are sorted
* @param {Array} declarations The array of AST nodes being checked.
* @returns {void}
*/
function checkSorted(declarations) {
// function fix(fixer) {
// return propTypesSortUtil.fixPropTypesSort(context, fixer, declarations, ignoreCase);
// }
declarations.reduce((prev, curr, idx, decls) => {
if (/Spread(?:Property|Element)$/.test(curr.type)) {
return decls[idx + 1];
}
let prevPropName = getKey(prev);
let currentPropName = getKey(curr);
if (ignoreCase) {
prevPropName = prevPropName.toLowerCase();
currentPropName = currentPropName.toLowerCase();
}
if (currentPropName < prevPropName) {
report(context, messages.propsNotSorted, 'propsNotSorted', {
node: curr,
// fix
});
return prev;
}
return curr;
}, declarations[0]);
}
function checkNode(node) {
if (!node) {
return;
}
if (node.type === 'ObjectExpression') {
checkSorted(node.properties);
} else if (node.type === 'Identifier') {
const propTypesObject = findVariableByName(node, node.name);
if (propTypesObject && propTypesObject.properties) {
checkSorted(propTypesObject.properties);
}
}
}
// --------------------------------------------------------------------------
// Public API
// --------------------------------------------------------------------------
return {
'ClassProperty, PropertyDefinition'(node) {
if (!isDefaultPropsDeclaration(node)) {
return;
}
checkNode(node.value);
},
MemberExpression(node) {
if (!isDefaultPropsDeclaration(node)) {
return;
}
checkNode('right' in node.parent && node.parent.right);
},
Program() {
if (isWarnedForDeprecation) {
return;
}
log('The react/jsx-sort-default-props rule is deprecated. It has been renamed to `react/sort-default-props`.');
isWarnedForDeprecation = true;
},
};
},
};

View File

@@ -0,0 +1,531 @@
/**
* @fileoverview Enforce props alphabetical sorting
* @author Ilya Volodin, Yannick Croissant
*/
'use strict';
const propName = require('jsx-ast-utils/propName');
const includes = require('array-includes');
const toSorted = require('array.prototype.tosorted');
const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');
const report = require('../util/report');
const propTypesSortUtil = require('../util/propTypesSort');
const eslintUtil = require('../util/eslint');
const getText = eslintUtil.getText;
const getSourceCode = eslintUtil.getSourceCode;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function isMultilineProp(node) {
return node.loc.start.line !== node.loc.end.line;
}
const messages = {
noUnreservedProps: 'A customized reserved first list must only contain a subset of React reserved props. Remove: {{unreservedWords}}',
listIsEmpty: 'A customized reserved first list must not be empty',
listReservedPropsFirst: 'Reserved props must be listed before all other props',
listCallbacksLast: 'Callbacks must be listed after all other props',
listShorthandFirst: 'Shorthand props must be listed before all other props',
listShorthandLast: 'Shorthand props must be listed after all other props',
listMultilineFirst: 'Multiline props must be listed before all other props',
listMultilineLast: 'Multiline props must be listed after all other props',
sortPropsByAlpha: 'Props should be sorted alphabetically',
};
const RESERVED_PROPS_LIST = [
'children',
'dangerouslySetInnerHTML',
'key',
'ref',
];
function isReservedPropName(name, list) {
return list.indexOf(name) >= 0;
}
let attributeMap;
// attributeMap = { end: endrange, hasComment: true||false if comment in between nodes exists, it needs to be sorted to end }
function shouldSortToEnd(node) {
const attr = attributeMap.get(node);
return !!attr && !!attr.hasComment;
}
function contextCompare(a, b, options) {
let aProp = propName(a);
let bProp = propName(b);
const aSortToEnd = shouldSortToEnd(a);
const bSortToEnd = shouldSortToEnd(b);
if (aSortToEnd && !bSortToEnd) {
return 1;
}
if (!aSortToEnd && bSortToEnd) {
return -1;
}
if (options.reservedFirst) {
const aIsReserved = isReservedPropName(aProp, options.reservedList);
const bIsReserved = isReservedPropName(bProp, options.reservedList);
if (aIsReserved && !bIsReserved) {
return -1;
}
if (!aIsReserved && bIsReserved) {
return 1;
}
}
if (options.callbacksLast) {
const aIsCallback = propTypesSortUtil.isCallbackPropName(aProp);
const bIsCallback = propTypesSortUtil.isCallbackPropName(bProp);
if (aIsCallback && !bIsCallback) {
return 1;
}
if (!aIsCallback && bIsCallback) {
return -1;
}
}
if (options.shorthandFirst || options.shorthandLast) {
const shorthandSign = options.shorthandFirst ? -1 : 1;
if (!a.value && b.value) {
return shorthandSign;
}
if (a.value && !b.value) {
return -shorthandSign;
}
}
if (options.multiline !== 'ignore') {
const multilineSign = options.multiline === 'first' ? -1 : 1;
const aIsMultiline = isMultilineProp(a);
const bIsMultiline = isMultilineProp(b);
if (aIsMultiline && !bIsMultiline) {
return multilineSign;
}
if (!aIsMultiline && bIsMultiline) {
return -multilineSign;
}
}
if (options.noSortAlphabetically) {
return 0;
}
const actualLocale = options.locale === 'auto' ? undefined : options.locale;
if (options.ignoreCase) {
aProp = aProp.toLowerCase();
bProp = bProp.toLowerCase();
return aProp.localeCompare(bProp, actualLocale);
}
if (aProp === bProp) {
return 0;
}
if (options.locale === 'auto') {
return aProp < bProp ? -1 : 1;
}
return aProp.localeCompare(bProp, actualLocale);
}
/**
* Create an array of arrays where each subarray is composed of attributes
* that are considered sortable.
* @param {Array<JSXSpreadAttribute|JSXAttribute>} attributes
* @param {Object} context The context of the rule
* @return {Array<Array<JSXAttribute>>}
*/
function getGroupsOfSortableAttributes(attributes, context) {
const sourceCode = getSourceCode(context);
const sortableAttributeGroups = [];
let groupCount = 0;
function addtoSortableAttributeGroups(attribute) {
sortableAttributeGroups[groupCount - 1].push(attribute);
}
for (let i = 0; i < attributes.length; i++) {
const attribute = attributes[i];
const nextAttribute = attributes[i + 1];
const attributeline = attribute.loc.start.line;
let comment = [];
try {
comment = sourceCode.getCommentsAfter(attribute);
} catch (e) { /**/ }
const lastAttr = attributes[i - 1];
const attrIsSpread = attribute.type === 'JSXSpreadAttribute';
// If we have no groups or if the last attribute was JSXSpreadAttribute
// then we start a new group. Append attributes to the group until we
// come across another JSXSpreadAttribute or exhaust the array.
if (
!lastAttr
|| (lastAttr.type === 'JSXSpreadAttribute' && !attrIsSpread)
) {
groupCount += 1;
sortableAttributeGroups[groupCount - 1] = [];
}
if (!attrIsSpread) {
if (comment.length === 0) {
attributeMap.set(attribute, { end: attribute.range[1], hasComment: false });
addtoSortableAttributeGroups(attribute);
} else {
const firstComment = comment[0];
const commentline = firstComment.loc.start.line;
if (comment.length === 1) {
if (attributeline + 1 === commentline && nextAttribute) {
attributeMap.set(attribute, { end: nextAttribute.range[1], hasComment: true });
addtoSortableAttributeGroups(attribute);
i += 1;
} else if (attributeline === commentline) {
if (firstComment.type === 'Block' && nextAttribute) {
attributeMap.set(attribute, { end: nextAttribute.range[1], hasComment: true });
i += 1;
} else if (firstComment.type === 'Block') {
attributeMap.set(attribute, { end: firstComment.range[1], hasComment: true });
} else {
attributeMap.set(attribute, { end: firstComment.range[1], hasComment: false });
}
addtoSortableAttributeGroups(attribute);
}
} else if (comment.length > 1 && attributeline + 1 === comment[1].loc.start.line && nextAttribute) {
const commentNextAttribute = sourceCode.getCommentsAfter(nextAttribute);
attributeMap.set(attribute, { end: nextAttribute.range[1], hasComment: true });
if (
commentNextAttribute.length === 1
&& nextAttribute.loc.start.line === commentNextAttribute[0].loc.start.line
) {
attributeMap.set(attribute, { end: commentNextAttribute[0].range[1], hasComment: true });
}
addtoSortableAttributeGroups(attribute);
i += 1;
}
}
}
}
return sortableAttributeGroups;
}
function generateFixerFunction(node, context, reservedList) {
const attributes = node.attributes.slice(0);
const configuration = context.options[0] || {};
const ignoreCase = configuration.ignoreCase || false;
const callbacksLast = configuration.callbacksLast || false;
const shorthandFirst = configuration.shorthandFirst || false;
const shorthandLast = configuration.shorthandLast || false;
const multiline = configuration.multiline || 'ignore';
const noSortAlphabetically = configuration.noSortAlphabetically || false;
const reservedFirst = configuration.reservedFirst || false;
const locale = configuration.locale || 'auto';
// Sort props according to the context. Only supports ignoreCase.
// Since we cannot safely move JSXSpreadAttribute (due to potential variable overrides),
// we only consider groups of sortable attributes.
const options = {
ignoreCase,
callbacksLast,
shorthandFirst,
shorthandLast,
multiline,
noSortAlphabetically,
reservedFirst,
reservedList,
locale,
};
const sortableAttributeGroups = getGroupsOfSortableAttributes(attributes, context);
const sortedAttributeGroups = sortableAttributeGroups
.slice(0)
.map((group) => toSorted(group, (a, b) => contextCompare(a, b, options)));
return function fixFunction(fixer) {
const fixers = [];
let source = getText(context);
sortableAttributeGroups.forEach((sortableGroup, ii) => {
sortableGroup.forEach((attr, jj) => {
const sortedAttr = sortedAttributeGroups[ii][jj];
const sortedAttrText = source.slice(sortedAttr.range[0], attributeMap.get(sortedAttr).end);
fixers.push({
range: [attr.range[0], attributeMap.get(attr).end],
text: sortedAttrText,
});
});
});
fixers.sort((a, b) => b.range[0] - a.range[0]);
const firstFixer = fixers[0];
const lastFixer = fixers[fixers.length - 1];
const rangeStart = lastFixer ? lastFixer.range[0] : 0;
const rangeEnd = firstFixer ? firstFixer.range[1] : -0;
fixers.forEach((fix) => {
source = `${source.slice(0, fix.range[0])}${fix.text}${source.slice(fix.range[1])}`;
});
return fixer.replaceTextRange([rangeStart, rangeEnd], source.slice(rangeStart, rangeEnd));
};
}
/**
* Checks if the `reservedFirst` option is valid
* @param {Object} context The context of the rule
* @param {boolean | string[]} reservedFirst The `reservedFirst` option
* @return {Function | undefined} If an error is detected, a function to generate the error message, otherwise, `undefined`
*/
// eslint-disable-next-line consistent-return
function validateReservedFirstConfig(context, reservedFirst) {
if (reservedFirst) {
if (Array.isArray(reservedFirst)) {
// Only allow a subset of reserved words in customized lists
const nonReservedWords = reservedFirst.filter((word) => !isReservedPropName(
word,
RESERVED_PROPS_LIST
));
if (reservedFirst.length === 0) {
return function Report(decl) {
report(context, messages.listIsEmpty, 'listIsEmpty', {
node: decl,
});
};
}
if (nonReservedWords.length > 0) {
return function Report(decl) {
report(context, messages.noUnreservedProps, 'noUnreservedProps', {
node: decl,
data: {
unreservedWords: nonReservedWords.toString(),
},
});
};
}
}
}
}
const reportedNodeAttributes = new WeakMap();
/**
* Check if the current node attribute has already been reported with the same error type
* if that's the case then we don't report a new error
* otherwise we report the error
* @param {Object} nodeAttribute The node attribute to be reported
* @param {string} errorType The error type to be reported
* @param {Object} node The parent node for the node attribute
* @param {Object} context The context of the rule
* @param {Array<String>} reservedList The list of reserved props
*/
function reportNodeAttribute(nodeAttribute, errorType, node, context, reservedList) {
const errors = reportedNodeAttributes.get(nodeAttribute) || [];
if (includes(errors, errorType)) {
return;
}
errors.push(errorType);
reportedNodeAttributes.set(nodeAttribute, errors);
report(context, messages[errorType], errorType, {
node: nodeAttribute.name,
fix: generateFixerFunction(node, context, reservedList),
});
}
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce props alphabetical sorting',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-sort-props'),
},
fixable: 'code',
messages,
schema: [{
type: 'object',
properties: {
// Whether callbacks (prefixed with "on") should be listed at the very end,
// after all other props. Supersedes shorthandLast.
callbacksLast: {
type: 'boolean',
},
// Whether shorthand properties (without a value) should be listed first
shorthandFirst: {
type: 'boolean',
},
// Whether shorthand properties (without a value) should be listed last
shorthandLast: {
type: 'boolean',
},
// Whether multiline properties should be listed first or last
multiline: {
enum: ['ignore', 'first', 'last'],
default: 'ignore',
},
ignoreCase: {
type: 'boolean',
},
// Whether alphabetical sorting should be enforced
noSortAlphabetically: {
type: 'boolean',
},
reservedFirst: {
type: ['array', 'boolean'],
},
locale: {
type: 'string',
default: 'auto',
},
},
additionalProperties: false,
}],
},
create(context) {
const configuration = context.options[0] || {};
const ignoreCase = configuration.ignoreCase || false;
const callbacksLast = configuration.callbacksLast || false;
const shorthandFirst = configuration.shorthandFirst || false;
const shorthandLast = configuration.shorthandLast || false;
const multiline = configuration.multiline || 'ignore';
const noSortAlphabetically = configuration.noSortAlphabetically || false;
const reservedFirst = configuration.reservedFirst || false;
const reservedFirstError = validateReservedFirstConfig(context, reservedFirst);
const reservedList = Array.isArray(reservedFirst) ? reservedFirst : RESERVED_PROPS_LIST;
const locale = configuration.locale || 'auto';
return {
Program() {
attributeMap = new WeakMap();
},
JSXOpeningElement(node) {
// `dangerouslySetInnerHTML` is only "reserved" on DOM components
const nodeReservedList = reservedFirst && !jsxUtil.isDOMComponent(node) ? reservedList.filter((prop) => prop !== 'dangerouslySetInnerHTML') : reservedList;
node.attributes.reduce((memo, decl, idx, attrs) => {
if (decl.type === 'JSXSpreadAttribute') {
return attrs[idx + 1];
}
let previousPropName = propName(memo);
let currentPropName = propName(decl);
const previousValue = memo.value;
const currentValue = decl.value;
const previousIsCallback = propTypesSortUtil.isCallbackPropName(previousPropName);
const currentIsCallback = propTypesSortUtil.isCallbackPropName(currentPropName);
if (ignoreCase) {
previousPropName = previousPropName.toLowerCase();
currentPropName = currentPropName.toLowerCase();
}
if (reservedFirst) {
if (reservedFirstError) {
reservedFirstError(decl);
return memo;
}
const previousIsReserved = isReservedPropName(previousPropName, nodeReservedList);
const currentIsReserved = isReservedPropName(currentPropName, nodeReservedList);
if (previousIsReserved && !currentIsReserved) {
return decl;
}
if (!previousIsReserved && currentIsReserved) {
reportNodeAttribute(decl, 'listReservedPropsFirst', node, context, nodeReservedList);
return memo;
}
}
if (callbacksLast) {
if (!previousIsCallback && currentIsCallback) {
// Entering the callback prop section
return decl;
}
if (previousIsCallback && !currentIsCallback) {
// Encountered a non-callback prop after a callback prop
reportNodeAttribute(memo, 'listCallbacksLast', node, context, nodeReservedList);
return memo;
}
}
if (shorthandFirst) {
if (currentValue && !previousValue) {
return decl;
}
if (!currentValue && previousValue) {
reportNodeAttribute(decl, 'listShorthandFirst', node, context, nodeReservedList);
return memo;
}
}
if (shorthandLast) {
if (!currentValue && previousValue) {
return decl;
}
if (currentValue && !previousValue) {
reportNodeAttribute(memo, 'listShorthandLast', node, context, nodeReservedList);
return memo;
}
}
const previousIsMultiline = isMultilineProp(memo);
const currentIsMultiline = isMultilineProp(decl);
if (multiline === 'first') {
if (previousIsMultiline && !currentIsMultiline) {
// Exiting the multiline prop section
return decl;
}
if (!previousIsMultiline && currentIsMultiline) {
// Encountered a non-multiline prop before a multiline prop
reportNodeAttribute(decl, 'listMultilineFirst', node, context, nodeReservedList);
return memo;
}
} else if (multiline === 'last') {
if (!previousIsMultiline && currentIsMultiline) {
// Entering the multiline prop section
return decl;
}
if (previousIsMultiline && !currentIsMultiline) {
// Encountered a non-multiline prop after a multiline prop
reportNodeAttribute(memo, 'listMultilineLast', node, context, nodeReservedList);
return memo;
}
}
if (
!noSortAlphabetically
&& (
(ignoreCase || locale !== 'auto')
? previousPropName.localeCompare(currentPropName, locale === 'auto' ? undefined : locale) > 0
: previousPropName > currentPropName
)
) {
reportNodeAttribute(decl, 'sortPropsByAlpha', node, context, nodeReservedList);
return memo;
}
return decl;
}, node.attributes[0]);
},
};
},
};

View File

@@ -0,0 +1,98 @@
/**
* @fileoverview Validate spacing before closing bracket in JSX.
* @author ryym
* @deprecated
*/
'use strict';
const getTokenBeforeClosingBracket = require('../util/getTokenBeforeClosingBracket');
const docsUrl = require('../util/docsUrl');
const log = require('../util/log');
const report = require('../util/report');
const getSourceCode = require('../util/eslint').getSourceCode;
let isWarnedForDeprecation = false;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
noSpaceBeforeClose: 'A space is forbidden before closing bracket',
needSpaceBeforeClose: 'A space is required before closing bracket',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
deprecated: true,
replacedBy: ['jsx-tag-spacing'],
docs: {
description: 'Enforce spacing before closing bracket in JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-space-before-closing'),
},
fixable: 'code',
messages,
schema: [{
enum: ['always', 'never'],
}],
},
create(context) {
const configuration = context.options[0] || 'always';
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
JSXOpeningElement(node) {
if (!node.selfClosing) {
return;
}
const sourceCode = getSourceCode(context);
const leftToken = getTokenBeforeClosingBracket(node);
const closingSlash = /** @type {import("eslint").AST.Token} */ (sourceCode.getTokenAfter(leftToken));
if (leftToken.loc.end.line !== closingSlash.loc.start.line) {
return;
}
if (configuration === 'always' && !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
report(context, messages.needSpaceBeforeClose, 'needSpaceBeforeClose', {
loc: closingSlash.loc.start,
fix(fixer) {
return fixer.insertTextBefore(closingSlash, ' ');
},
});
} else if (configuration === 'never' && sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
report(context, messages.noSpaceBeforeClose, 'noSpaceBeforeClose', {
loc: closingSlash.loc.start,
fix(fixer) {
const previousToken = sourceCode.getTokenBefore(closingSlash);
return fixer.removeRange([previousToken.range[1], closingSlash.range[0]]);
},
});
}
},
Program() {
if (isWarnedForDeprecation) {
return;
}
log('The react/jsx-space-before-closing rule is deprecated. '
+ 'Please use the react/jsx-tag-spacing rule with the '
+ '"beforeSelfClosing" option instead.');
isWarnedForDeprecation = true;
},
};
},
};

View File

@@ -0,0 +1,328 @@
/**
* @fileoverview Validates whitespace in and around the JSX opening and closing brackets
* @author Diogo Franco (Kovensky)
*/
'use strict';
const getTokenBeforeClosingBracket = require('../util/getTokenBeforeClosingBracket');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
const eslintUtil = require('../util/eslint');
const getFirstTokens = eslintUtil.getFirstTokens;
const getSourceCode = eslintUtil.getSourceCode;
const messages = {
selfCloseSlashNoSpace: 'Whitespace is forbidden between `/` and `>`; write `/>`',
selfCloseSlashNeedSpace: 'Whitespace is required between `/` and `>`; write `/ >`',
closeSlashNoSpace: 'Whitespace is forbidden between `<` and `/`; write `</`',
closeSlashNeedSpace: 'Whitespace is required between `<` and `/`; write `< /`',
beforeSelfCloseNoSpace: 'A space is forbidden before closing bracket',
beforeSelfCloseNeedSpace: 'A space is required before closing bracket',
beforeSelfCloseNeedNewline: 'A newline is required before closing bracket',
afterOpenNoSpace: 'A space is forbidden after opening bracket',
afterOpenNeedSpace: 'A space is required after opening bracket',
beforeCloseNoSpace: 'A space is forbidden before closing bracket',
beforeCloseNeedSpace: 'Whitespace is required before closing bracket',
beforeCloseNeedNewline: 'A newline is required before closing bracket',
};
// ------------------------------------------------------------------------------
// Validators
// ------------------------------------------------------------------------------
function validateClosingSlash(context, node, option) {
const sourceCode = getSourceCode(context);
let adjacent;
if (node.selfClosing) {
const lastTokens = sourceCode.getLastTokens(node, 2);
adjacent = !sourceCode.isSpaceBetweenTokens(lastTokens[0], lastTokens[1]);
if (option === 'never') {
if (!adjacent) {
report(context, messages.selfCloseSlashNoSpace, 'selfCloseSlashNoSpace', {
node,
loc: {
start: lastTokens[0].loc.start,
end: lastTokens[1].loc.end,
},
fix(fixer) {
return fixer.removeRange([lastTokens[0].range[1], lastTokens[1].range[0]]);
},
});
}
} else if (option === 'always' && adjacent) {
report(context, messages.selfCloseSlashNeedSpace, 'selfCloseSlashNeedSpace', {
node,
loc: {
start: lastTokens[0].loc.start,
end: lastTokens[1].loc.end,
},
fix(fixer) {
return fixer.insertTextBefore(lastTokens[1], ' ');
},
});
}
} else {
const firstTokens = getFirstTokens(context, node, 2);
adjacent = !sourceCode.isSpaceBetweenTokens(firstTokens[0], firstTokens[1]);
if (option === 'never') {
if (!adjacent) {
report(context, messages.closeSlashNoSpace, 'closeSlashNoSpace', {
node,
loc: {
start: firstTokens[0].loc.start,
end: firstTokens[1].loc.end,
},
fix(fixer) {
return fixer.removeRange([firstTokens[0].range[1], firstTokens[1].range[0]]);
},
});
}
} else if (option === 'always' && adjacent) {
report(context, messages.closeSlashNeedSpace, 'closeSlashNeedSpace', {
node,
loc: {
start: firstTokens[0].loc.start,
end: firstTokens[1].loc.end,
},
fix(fixer) {
return fixer.insertTextBefore(firstTokens[1], ' ');
},
});
}
}
}
function validateBeforeSelfClosing(context, node, option) {
const sourceCode = getSourceCode(context);
const leftToken = getTokenBeforeClosingBracket(node);
const closingSlash = sourceCode.getTokenAfter(leftToken);
if (node.loc.start.line !== node.loc.end.line && option === 'proportional-always') {
if (leftToken.loc.end.line === closingSlash.loc.start.line) {
report(context, messages.beforeSelfCloseNeedNewline, 'beforeSelfCloseNeedNewline', {
node,
loc: leftToken.loc.end,
fix(fixer) {
return fixer.insertTextBefore(closingSlash, '\n');
},
});
return;
}
}
if (leftToken.loc.end.line !== closingSlash.loc.start.line) {
return;
}
const adjacent = !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash);
if ((option === 'always' || option === 'proportional-always') && adjacent) {
report(context, messages.beforeSelfCloseNeedSpace, 'beforeSelfCloseNeedSpace', {
node,
loc: closingSlash.loc.start,
fix(fixer) {
return fixer.insertTextBefore(closingSlash, ' ');
},
});
} else if (option === 'never' && !adjacent) {
report(context, messages.beforeSelfCloseNoSpace, 'beforeSelfCloseNoSpace', {
node,
loc: closingSlash.loc.start,
fix(fixer) {
const previousToken = sourceCode.getTokenBefore(closingSlash);
return fixer.removeRange([previousToken.range[1], closingSlash.range[0]]);
},
});
}
}
function validateAfterOpening(context, node, option) {
const sourceCode = getSourceCode(context);
const openingToken = sourceCode.getTokenBefore(node.name);
if (option === 'allow-multiline') {
if (openingToken.loc.start.line !== node.name.loc.start.line) {
return;
}
}
const adjacent = !sourceCode.isSpaceBetweenTokens(openingToken, node.name);
if (option === 'never' || option === 'allow-multiline') {
if (!adjacent) {
report(context, messages.afterOpenNoSpace, 'afterOpenNoSpace', {
node,
loc: {
start: openingToken.loc.start,
end: node.name.loc.start,
},
fix(fixer) {
return fixer.removeRange([openingToken.range[1], node.name.range[0]]);
},
});
}
} else if (option === 'always' && adjacent) {
report(context, messages.afterOpenNeedSpace, 'afterOpenNeedSpace', {
node,
loc: {
start: openingToken.loc.start,
end: node.name.loc.start,
},
fix(fixer) {
return fixer.insertTextBefore(node.name, ' ');
},
});
}
}
function validateBeforeClosing(context, node, option) {
// Don't enforce this rule for self closing tags
if (!node.selfClosing) {
const sourceCode = getSourceCode(context);
const leftToken = option === 'proportional-always'
? getTokenBeforeClosingBracket(node)
: sourceCode.getLastTokens(node, 2)[0];
const closingToken = sourceCode.getTokenAfter(leftToken);
if (node.loc.start.line !== node.loc.end.line && option === 'proportional-always') {
if (leftToken.loc.end.line === closingToken.loc.start.line) {
report(context, messages.beforeCloseNeedNewline, 'beforeCloseNeedNewline', {
node,
loc: leftToken.loc.end,
fix(fixer) {
return fixer.insertTextBefore(closingToken, '\n');
},
});
return;
}
}
if (leftToken.loc.start.line !== closingToken.loc.start.line) {
return;
}
const adjacent = !sourceCode.isSpaceBetweenTokens(leftToken, closingToken);
if (option === 'never' && !adjacent) {
report(context, messages.beforeCloseNoSpace, 'beforeCloseNoSpace', {
node,
loc: {
start: leftToken.loc.end,
end: closingToken.loc.start,
},
fix(fixer) {
return fixer.removeRange([leftToken.range[1], closingToken.range[0]]);
},
});
} else if (option === 'always' && adjacent) {
report(context, messages.beforeCloseNeedSpace, 'beforeCloseNeedSpace', {
node,
loc: {
start: leftToken.loc.end,
end: closingToken.loc.start,
},
fix(fixer) {
return fixer.insertTextBefore(closingToken, ' ');
},
});
} else if (option === 'proportional-always' && node.type === 'JSXOpeningElement' && adjacent !== (node.loc.start.line === node.loc.end.line)) {
report(context, messages.beforeCloseNeedSpace, 'beforeCloseNeedSpace', {
node,
loc: {
start: leftToken.loc.end,
end: closingToken.loc.start,
},
fix(fixer) {
return fixer.insertTextBefore(closingToken, ' ');
},
});
}
}
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const optionDefaults = {
closingSlash: 'never',
beforeSelfClosing: 'always',
afterOpening: 'never',
beforeClosing: 'allow',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce whitespace in and around the JSX opening and closing brackets',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-tag-spacing'),
},
fixable: 'whitespace',
messages,
schema: [
{
type: 'object',
properties: {
closingSlash: {
enum: ['always', 'never', 'allow'],
},
beforeSelfClosing: {
enum: ['always', 'proportional-always', 'never', 'allow'],
},
afterOpening: {
enum: ['always', 'allow-multiline', 'never', 'allow'],
},
beforeClosing: {
enum: ['always', 'proportional-always', 'never', 'allow'],
},
},
default: optionDefaults,
additionalProperties: false,
},
],
},
create(context) {
const options = Object.assign({}, optionDefaults, context.options[0]);
return {
JSXOpeningElement(node) {
if (options.closingSlash !== 'allow' && node.selfClosing) {
validateClosingSlash(context, node, options.closingSlash);
}
if (options.afterOpening !== 'allow') {
validateAfterOpening(context, node, options.afterOpening);
}
if (options.beforeSelfClosing !== 'allow' && node.selfClosing) {
validateBeforeSelfClosing(context, node, options.beforeSelfClosing);
}
if (options.beforeClosing !== 'allow') {
validateBeforeClosing(context, node, options.beforeClosing);
}
},
JSXClosingElement(node) {
if (options.afterOpening !== 'allow') {
validateAfterOpening(context, node, options.afterOpening);
}
if (options.closingSlash !== 'allow') {
validateClosingSlash(context, node, options.closingSlash);
}
if (options.beforeClosing !== 'allow') {
validateBeforeClosing(context, node, options.beforeClosing);
}
},
};
},
};

View File

@@ -0,0 +1,52 @@
/**
* @fileoverview Prevent React to be marked as unused
* @author Glen Mailer
*/
'use strict';
const pragmaUtil = require('../util/pragma');
const docsUrl = require('../util/docsUrl');
const markVariableAsUsed = require('../util/eslint').markVariableAsUsed;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
// eslint-disable-next-line eslint-plugin/prefer-message-ids -- https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/292
meta: {
docs: {
description: 'Disallow React to be incorrectly marked as unused',
category: 'Best Practices',
recommended: true,
url: docsUrl('jsx-uses-react'),
},
schema: [],
},
create(context) {
const pragma = pragmaUtil.getFromContext(context);
const fragment = pragmaUtil.getFragmentFromContext(context);
/**
* @param {ASTNode} node
* @returns {void}
*/
function handleOpeningElement(node) {
markVariableAsUsed(pragma, node, context);
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
JSXOpeningElement: handleOpeningElement,
JSXOpeningFragment: handleOpeningElement,
JSXFragment(node) {
markVariableAsUsed(fragment, node, context);
},
};
},
};

View File

@@ -0,0 +1,62 @@
/**
* @fileoverview Prevent variables used in JSX to be marked as unused
* @author Yannick Croissant
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const markVariableAsUsed = require('../util/eslint').markVariableAsUsed;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const isTagNameRe = /^[a-z]/;
const isTagName = (name) => isTagNameRe.test(name);
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
// eslint-disable-next-line eslint-plugin/prefer-message-ids -- https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/292
meta: {
docs: {
description: 'Disallow variables used in JSX to be incorrectly marked as unused',
category: 'Best Practices',
recommended: true,
url: docsUrl('jsx-uses-vars'),
},
schema: [],
},
create(context) {
return {
JSXOpeningElement(node) {
let name;
if (node.name.namespace) {
// <Foo:Bar>
return;
}
if (node.name.name) {
// <Foo>
name = node.name.name;
// Exclude lowercase tag names like <div>
if (isTagName(name)) {
return;
}
} else if (node.name.object) {
// <Foo...Bar>
let parent = node.name.object;
while (parent.object) {
parent = parent.object;
}
name = parent.name;
} else {
return;
}
markVariableAsUsed(name, node, context);
},
};
},
};

View File

@@ -0,0 +1,275 @@
/**
* @fileoverview Prevent missing parentheses around multilines JSX
* @author Yannick Croissant
*/
'use strict';
const has = require('hasown');
const docsUrl = require('../util/docsUrl');
const eslintUtil = require('../util/eslint');
const jsxUtil = require('../util/jsx');
const reportC = require('../util/report');
const isParenthesized = require('../util/ast').isParenthesized;
const getSourceCode = eslintUtil.getSourceCode;
const getText = eslintUtil.getText;
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const DEFAULTS = {
declaration: 'parens',
assignment: 'parens',
return: 'parens',
arrow: 'parens',
condition: 'ignore',
logical: 'ignore',
prop: 'ignore',
};
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
missingParens: 'Missing parentheses around multilines JSX',
extraParens: 'Expected no parentheses around multilines JSX',
parensOnNewLines: 'Parentheses around JSX should be on separate lines',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow missing parentheses around multiline JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-wrap-multilines'),
},
fixable: 'code',
messages,
schema: [{
type: 'object',
// true/false are for backwards compatibility
properties: {
declaration: {
enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
},
assignment: {
enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
},
return: {
enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
},
arrow: {
enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
},
condition: {
enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
},
logical: {
enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
},
prop: {
enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
},
},
additionalProperties: false,
}],
},
create(context) {
function getOption(type) {
const userOptions = context.options[0] || {};
if (has(userOptions, type)) {
return userOptions[type];
}
return DEFAULTS[type];
}
function isEnabled(type) {
const option = getOption(type);
return option && option !== 'ignore';
}
function needsOpeningNewLine(node) {
const previousToken = getSourceCode(context).getTokenBefore(node);
if (!isParenthesized(context, node)) {
return false;
}
if (previousToken.loc.end.line === node.loc.start.line) {
return true;
}
return false;
}
function needsClosingNewLine(node) {
const nextToken = getSourceCode(context).getTokenAfter(node);
if (!isParenthesized(context, node)) {
return false;
}
if (node.loc.end.line === nextToken.loc.end.line) {
return true;
}
return false;
}
function isMultilines(node) {
return node.loc.start.line !== node.loc.end.line;
}
function report(node, messageId, fix) {
reportC(context, messages[messageId], messageId, {
node,
fix,
});
}
function trimTokenBeforeNewline(node, tokenBefore) {
// if the token before the jsx is a bracket or curly brace
// we don't want a space between the opening parentheses and the multiline jsx
const isBracket = tokenBefore.value === '{' || tokenBefore.value === '[';
return `${tokenBefore.value.trim()}${isBracket ? '' : ' '}`;
}
function check(node, type) {
if (!node || !jsxUtil.isJSX(node)) {
return;
}
const sourceCode = getSourceCode(context);
const option = getOption(type);
if ((option === true || option === 'parens') && !isParenthesized(context, node) && isMultilines(node)) {
report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(${getText(context, node)})`));
}
if (option === 'parens-new-line' && isMultilines(node)) {
if (!isParenthesized(context, node)) {
const tokenBefore = sourceCode.getTokenBefore(node, { includeComments: true });
const tokenAfter = sourceCode.getTokenAfter(node, { includeComments: true });
const start = node.loc.start;
if (tokenBefore.loc.end.line < start.line) {
// Strip newline after operator if parens newline is specified
report(
node,
'missingParens',
(fixer) => fixer.replaceTextRange(
[tokenBefore.range[0], tokenAfter && (tokenAfter.value === ';' || tokenAfter.value === '}') ? tokenAfter.range[0] : node.range[1]],
`${trimTokenBeforeNewline(node, tokenBefore)}(\n${start.column > 0 ? ' '.repeat(start.column) : ''}${getText(context, node)}\n${start.column > 0 ? ' '.repeat(start.column - 2) : ''})`
)
);
} else {
report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(\n${getText(context, node)}\n)`));
}
} else {
const needsOpening = needsOpeningNewLine(node);
const needsClosing = needsClosingNewLine(node);
if (needsOpening || needsClosing) {
report(node, 'parensOnNewLines', (fixer) => {
const text = getText(context, node);
let fixed = text;
if (needsOpening) {
fixed = `\n${fixed}`;
}
if (needsClosing) {
fixed = `${fixed}\n`;
}
return fixer.replaceText(node, fixed);
});
}
}
}
if (option === 'never' && isParenthesized(context, node)) {
const tokenBefore = sourceCode.getTokenBefore(node);
const tokenAfter = sourceCode.getTokenAfter(node);
report(node, 'extraParens', (fixer) => fixer.replaceTextRange(
[tokenBefore.range[0], tokenAfter.range[1]],
getText(context, node)
));
}
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
VariableDeclarator(node) {
const type = 'declaration';
if (!isEnabled(type)) {
return;
}
if (!isEnabled('condition') && node.init && node.init.type === 'ConditionalExpression') {
check(node.init.consequent, type);
check(node.init.alternate, type);
return;
}
check(node.init, type);
},
AssignmentExpression(node) {
const type = 'assignment';
if (!isEnabled(type)) {
return;
}
if (!isEnabled('condition') && node.right.type === 'ConditionalExpression') {
check(node.right.consequent, type);
check(node.right.alternate, type);
return;
}
check(node.right, type);
},
ReturnStatement(node) {
const type = 'return';
if (isEnabled(type)) {
check(node.argument, type);
}
},
'ArrowFunctionExpression:exit': (node) => {
const arrowBody = node.body;
const type = 'arrow';
if (isEnabled(type) && arrowBody.type !== 'BlockStatement') {
check(arrowBody, type);
}
},
ConditionalExpression(node) {
const type = 'condition';
if (isEnabled(type)) {
check(node.consequent, type);
check(node.alternate, type);
}
},
LogicalExpression(node) {
const type = 'logical';
if (isEnabled(type)) {
check(node.right, type);
}
},
JSXAttribute(node) {
const type = 'prop';
if (isEnabled(type) && node.value && node.value.type === 'JSXExpressionContainer') {
check(node.value.expression, type);
}
},
};
},
};

View File

@@ -0,0 +1,211 @@
/**
* @fileoverview Prevent usage of this.state within setState
* @author Rolf Erik Lekang, Jørgen Aaberg
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const astUtil = require('../util/ast');
const componentUtil = require('../util/componentUtil');
const report = require('../util/report');
const getScope = require('../util/eslint').getScope;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
useCallback: 'Use callback in setState when referencing the previous state.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow when this.state is accessed within setState',
category: 'Possible Errors',
recommended: false,
url: docsUrl('no-access-state-in-setstate'),
},
messages,
},
create(context) {
function isSetStateCall(node) {
return astUtil.isCallExpression(node)
&& node.callee.property
&& node.callee.property.name === 'setState'
&& node.callee.object.type === 'ThisExpression';
}
function isFirstArgumentInSetStateCall(current, node) {
if (!isSetStateCall(current)) {
return false;
}
while (node && node.parent !== current) {
node = node.parent;
}
return current.arguments[0] === node;
}
/**
* @param {ASTNode} node
* @returns {boolean}
*/
function isClassComponent(node) {
return !!(
componentUtil.getParentES6Component(context, node)
|| componentUtil.getParentES5Component(context, node)
);
}
// The methods array contains all methods or functions that are using this.state
// or that are calling another method or function using this.state
const methods = [];
// The vars array contains all variables that contains this.state
const vars = [];
return {
CallExpression(node) {
if (!isClassComponent(node)) {
return;
}
// Appends all the methods that are calling another
// method containing this.state to the methods array
methods.forEach((method) => {
if ('name' in node.callee && node.callee.name === method.methodName) {
let current = node.parent;
while (current.type !== 'Program') {
if (current.type === 'MethodDefinition') {
methods.push({
methodName: 'name' in current.key ? current.key.name : undefined,
node: method.node,
});
break;
}
current = current.parent;
}
}
});
// Finding all CallExpressions that is inside a setState
// to further check if they contains this.state
let current = node.parent;
while (current.type !== 'Program') {
if (isFirstArgumentInSetStateCall(current, node)) {
const methodName = 'name' in node.callee ? node.callee.name : undefined;
methods.forEach((method) => {
if (method.methodName === methodName) {
report(context, messages.useCallback, 'useCallback', {
node: method.node,
});
}
});
break;
}
current = current.parent;
}
},
MemberExpression(node) {
if (
'name' in node.property
&& node.property.name === 'state'
&& node.object.type === 'ThisExpression'
&& isClassComponent(node)
) {
/** @type {import("eslint").Rule.Node} */
let current = node;
while (current.type !== 'Program') {
// Reporting if this.state is directly within this.setState
if (isFirstArgumentInSetStateCall(current, node)) {
report(context, messages.useCallback, 'useCallback', {
node,
});
break;
}
// Storing all functions and methods that contains this.state
if (current.type === 'MethodDefinition') {
methods.push({
methodName: 'name' in current.key ? current.key.name : undefined,
node,
});
break;
} else if (
current.type === 'FunctionExpression'
&& 'key' in current.parent
&& current.parent.key
) {
methods.push({
methodName: 'name' in current.parent.key ? current.parent.key.name : undefined,
node,
});
break;
}
// Storing all variables containing this.state
if (current.type === 'VariableDeclarator') {
vars.push({
node,
scope: getScope(context, node),
variableName: 'name' in current.id ? current.id.name : undefined,
});
break;
}
current = current.parent;
}
}
},
Identifier(node) {
// Checks if the identifier is a variable within an object
/** @type {import("eslint").Rule.Node} */
let current = node;
while (current.parent.type === 'BinaryExpression') {
current = current.parent;
}
if (
('value' in current.parent && current.parent.value === current)
|| ('object' in current.parent && current.parent.object === current)
) {
while (current.type !== 'Program') {
if (isFirstArgumentInSetStateCall(current, node)) {
vars
.filter((v) => v.scope === getScope(context, node) && v.variableName === node.name)
.forEach((v) => {
report(context, messages.useCallback, 'useCallback', {
node: v.node,
});
});
}
current = current.parent;
}
}
},
ObjectPattern(node) {
const isDerivedFromThis = 'init' in node.parent && node.parent.init && node.parent.init.type === 'ThisExpression';
node.properties.forEach((property) => {
if (
property
&& 'key' in property
&& property.key
&& 'name' in property.key
&& property.key.name === 'state'
&& isDerivedFromThis
) {
vars.push({
node: property.key,
scope: getScope(context, node),
variableName: property.key.name,
});
}
});
},
};
},
};

View File

@@ -0,0 +1,127 @@
/**
* @fileoverview Prevent adjacent inline elements not separated by whitespace.
* @author Sean Hayes
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const isCreateElement = require('../util/isCreateElement');
const report = require('../util/report');
const astUtil = require('../util/ast');
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
// https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements
const inlineNames = [
'a',
'b',
'big',
'i',
'small',
'tt',
'abbr',
'acronym',
'cite',
'code',
'dfn',
'em',
'kbd',
'strong',
'samp',
'time',
'var',
'bdo',
'br',
'img',
'map',
'object',
'q',
'script',
'span',
'sub',
'sup',
'button',
'input',
'label',
'select',
'textarea',
];
// Note: raw &nbsp; will be transformed into \u00a0.
const whitespaceRegex = /(?:^\s|\s$)/;
function isInline(node) {
if (node.type === 'Literal') {
// Regular whitespace will be removed.
const value = node.value;
// To properly separate inline elements, each end of the literal will need
// whitespace.
return !whitespaceRegex.test(value);
}
if (node.type === 'JSXElement' && inlineNames.indexOf(node.openingElement.name.name) > -1) {
return true;
}
if (astUtil.isCallExpression(node) && inlineNames.indexOf(node.arguments[0].value) > -1) {
return true;
}
return false;
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
inlineElement: 'Child elements which render as inline HTML elements should be separated by a space or wrapped in block level elements.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow adjacent inline elements not separated by whitespace.',
category: 'Best Practices',
recommended: false,
url: docsUrl('no-adjacent-inline-elements'),
},
schema: [],
messages,
},
create(context) {
function validate(node, children) {
let currentIsInline = false;
let previousIsInline = false;
if (!children) {
return;
}
for (let i = 0; i < children.length; i++) {
currentIsInline = isInline(children[i]);
if (previousIsInline && currentIsInline) {
report(context, messages.inlineElement, 'inlineElement', {
node,
});
return;
}
previousIsInline = currentIsInline;
}
}
return {
JSXElement(node) {
validate(node, node.children);
},
CallExpression(node) {
if (!isCreateElement(context, node)) {
return;
}
if (node.arguments.length < 2 || !node.arguments[2]) {
return;
}
const children = 'elements' in node.arguments[2] ? node.arguments[2].elements : undefined;
validate(node, children);
},
};
},
};

View File

@@ -0,0 +1,293 @@
/**
* @fileoverview Prevent usage of Array index in keys
* @author Joe Lencioni
*/
'use strict';
const has = require('hasown');
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
const pragma = require('../util/pragma');
const report = require('../util/report');
const variableUtil = require('../util/variable');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function isCreateCloneElement(node, context) {
if (!node) {
return false;
}
if (node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression') {
return node.object
&& node.object.name === pragma.getFromContext(context)
&& ['createElement', 'cloneElement'].indexOf(node.property.name) !== -1;
}
if (node.type === 'Identifier') {
const variable = variableUtil.findVariableByName(context, node, node.name);
if (variable && variable.type === 'ImportSpecifier') {
return variable.parent.source.value === 'react';
}
}
return false;
}
const messages = {
noArrayIndex: 'Do not use Array index in keys',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow usage of Array index in keys',
category: 'Best Practices',
recommended: false,
url: docsUrl('no-array-index-key'),
},
messages,
schema: [],
},
create(context) {
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
const indexParamNames = [];
const iteratorFunctionsToIndexParamPosition = {
every: 1,
filter: 1,
find: 1,
findIndex: 1,
flatMap: 1,
forEach: 1,
map: 1,
reduce: 2,
reduceRight: 2,
some: 1,
};
function isArrayIndex(node) {
return node.type === 'Identifier'
&& indexParamNames.indexOf(node.name) !== -1;
}
function isUsingReactChildren(node) {
const callee = node.callee;
if (
!callee
|| !callee.property
|| !callee.object
) {
return null;
}
const isReactChildMethod = ['map', 'forEach'].indexOf(callee.property.name) > -1;
if (!isReactChildMethod) {
return null;
}
const obj = callee.object;
if (obj && obj.name === 'Children') {
return true;
}
if (obj && obj.object && obj.object.name === pragma.getFromContext(context)) {
return true;
}
return false;
}
function getMapIndexParamName(node) {
const callee = node.callee;
if (callee.type !== 'MemberExpression' && callee.type !== 'OptionalMemberExpression') {
return null;
}
if (callee.property.type !== 'Identifier') {
return null;
}
if (!has(iteratorFunctionsToIndexParamPosition, callee.property.name)) {
return null;
}
const name = /** @type {keyof iteratorFunctionsToIndexParamPosition} */ (callee.property.name);
const callbackArg = isUsingReactChildren(node)
? node.arguments[1]
: node.arguments[0];
if (!callbackArg) {
return null;
}
if (!astUtil.isFunctionLikeExpression(callbackArg)) {
return null;
}
const params = callbackArg.params;
const indexParamPosition = iteratorFunctionsToIndexParamPosition[name];
if (params.length < indexParamPosition + 1) {
return null;
}
return params[indexParamPosition].name;
}
function getIdentifiersFromBinaryExpression(side) {
if (side.type === 'Identifier') {
return side;
}
if (side.type === 'BinaryExpression') {
// recurse
const left = getIdentifiersFromBinaryExpression(side.left);
const right = getIdentifiersFromBinaryExpression(side.right);
return [].concat(left, right).filter(Boolean);
}
return null;
}
function checkPropValue(node) {
if (isArrayIndex(node)) {
// key={bar}
report(context, messages.noArrayIndex, 'noArrayIndex', {
node,
});
return;
}
if (node.type === 'TemplateLiteral') {
// key={`foo-${bar}`}
node.expressions.filter(isArrayIndex).forEach(() => {
report(context, messages.noArrayIndex, 'noArrayIndex', {
node,
});
});
return;
}
if (node.type === 'BinaryExpression') {
// key={'foo' + bar}
const identifiers = getIdentifiersFromBinaryExpression(node);
identifiers.filter(isArrayIndex).forEach(() => {
report(context, messages.noArrayIndex, 'noArrayIndex', {
node,
});
});
return;
}
if (
astUtil.isCallExpression(node)
&& node.callee
&& node.callee.type === 'MemberExpression'
&& node.callee.object
&& isArrayIndex(node.callee.object)
&& node.callee.property
&& node.callee.property.type === 'Identifier'
&& node.callee.property.name === 'toString'
) {
// key={bar.toString()}
report(context, messages.noArrayIndex, 'noArrayIndex', {
node,
});
return;
}
if (
astUtil.isCallExpression(node)
&& node.callee
&& node.callee.type === 'Identifier'
&& node.callee.name === 'String'
&& Array.isArray(node.arguments)
&& node.arguments.length > 0
&& isArrayIndex(node.arguments[0])
) {
// key={String(bar)}
report(context, messages.noArrayIndex, 'noArrayIndex', {
node: node.arguments[0],
});
}
}
function popIndex(node) {
const mapIndexParamName = getMapIndexParamName(node);
if (!mapIndexParamName) {
return;
}
indexParamNames.pop();
}
return {
'CallExpression, OptionalCallExpression'(node) {
if (isCreateCloneElement(node.callee, context) && node.arguments.length > 1) {
// React.createElement
if (!indexParamNames.length) {
return;
}
const props = node.arguments[1];
if (props.type !== 'ObjectExpression') {
return;
}
props.properties.forEach((prop) => {
if (!prop.key || prop.key.name !== 'key') {
// { ...foo }
// { foo: bar }
return;
}
checkPropValue(prop.value);
});
return;
}
const mapIndexParamName = getMapIndexParamName(node);
if (!mapIndexParamName) {
return;
}
indexParamNames.push(mapIndexParamName);
},
JSXAttribute(node) {
if (node.name.name !== 'key') {
// foo={bar}
return;
}
if (!indexParamNames.length) {
// Not inside a call expression that we think has an index param.
return;
}
const value = node.value;
if (!value || value.type !== 'JSXExpressionContainer') {
// key='foo' or just simply 'key'
return;
}
checkPropValue(value.expression);
},
'CallExpression:exit': popIndex,
'OptionalCallExpression:exit': popIndex,
};
},
};

View File

@@ -0,0 +1,149 @@
/**
* @fileoverview Lifecycle methods should be methods on the prototype, not class fields
* @author Tan Nguyen
*/
'use strict';
const values = require('object.values');
const Components = require('../util/Components');
const astUtil = require('../util/ast');
const componentUtil = require('../util/componentUtil');
const docsUrl = require('../util/docsUrl');
const lifecycleMethods = require('../util/lifecycleMethods');
const report = require('../util/report');
const eslintUtil = require('../util/eslint');
const getSourceCode = eslintUtil.getSourceCode;
const getText = eslintUtil.getText;
function getRuleText(node) {
const params = node.value.params.map((p) => p.name);
if (node.type === 'Property') {
return `: function(${params.join(', ')}) `;
}
if (node.type === 'ClassProperty' || node.type === 'PropertyDefinition') {
return `(${params.join(', ')}) `;
}
return null;
}
const messages = {
lifecycle: '{{propertyName}} is a React lifecycle method, and should not be an arrow function or in a class field. Use an instance method instead.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Lifecycle methods should be methods on the prototype, not class fields',
category: 'Best Practices',
recommended: false,
url: docsUrl('no-arrow-function-lifecycle'),
},
messages,
schema: [],
fixable: 'code',
},
create: Components.detect((context, components) => {
/**
* @param {Array} properties list of component properties
*/
function reportNoArrowFunctionLifecycle(properties) {
properties.forEach((node) => {
if (!node || !node.value) {
return;
}
const propertyName = astUtil.getPropertyName(node);
const nodeType = node.value.type;
const isLifecycleMethod = (
node.static && !componentUtil.isES5Component(node, context)
? lifecycleMethods.static
: lifecycleMethods.instance
).indexOf(propertyName) > -1;
if (nodeType === 'ArrowFunctionExpression' && isLifecycleMethod) {
const body = node.value.body;
const isBlockBody = body.type === 'BlockStatement';
const sourceCode = getSourceCode(context);
let nextComment = [];
let previousComment = [];
let bodyRange;
if (!isBlockBody) {
const previousToken = sourceCode.getTokenBefore(body);
if (sourceCode.getCommentsBefore) {
// eslint >=4.x
previousComment = sourceCode.getCommentsBefore(body);
} else {
// eslint 3.x
const potentialComment = sourceCode.getTokenBefore(body, { includeComments: true });
previousComment = previousToken === potentialComment ? [] : [potentialComment];
}
if (sourceCode.getCommentsAfter) {
// eslint >=4.x
nextComment = sourceCode.getCommentsAfter(body);
} else {
// eslint 3.x
const potentialComment = sourceCode.getTokenAfter(body, { includeComments: true });
const nextToken = sourceCode.getTokenAfter(body);
nextComment = nextToken === potentialComment ? [] : [potentialComment];
}
bodyRange = [
(previousComment.length > 0 ? previousComment[0] : body).range[0],
(nextComment.length > 0 ? nextComment[nextComment.length - 1] : body).range[1]
+ (node.value.body.type === 'ObjectExpression' ? 1 : 0), // to account for a wrapped end paren
];
}
const headRange = [
node.key.range[1],
(previousComment.length > 0 ? previousComment[0] : body).range[0],
];
const hasSemi = node.value.expression && getText(context, node).slice(node.value.range[1] - node.range[0]) === ';';
report(
context,
messages.lifecycle,
'lifecycle',
{
node,
data: {
propertyName,
},
fix(fixer) {
if (!sourceCode.getCommentsAfter) {
// eslint 3.x
return isBlockBody && fixer.replaceTextRange(headRange, getRuleText(node));
}
return [].concat(
fixer.replaceTextRange(headRange, getRuleText(node)),
isBlockBody ? [] : fixer.replaceTextRange(
[bodyRange[0], bodyRange[1] + (hasSemi ? 1 : 0)],
`{ return ${previousComment.map((x) => getText(context, x)).join('')}${getText(context, body)}${nextComment.map((x) => getText(context, x)).join('')}; }`
)
);
},
}
);
}
});
}
return {
'Program:exit'() {
values(components.list()).forEach((component) => {
const properties = astUtil.getComponentProperties(component.node);
reportNoArrowFunctionLifecycle(properties);
});
},
};
}),
};

View File

@@ -0,0 +1,125 @@
/**
* @fileoverview Prevent passing of children as props
* @author Benjamin Stepp
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const isCreateElement = require('../util/isCreateElement');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Checks if the node is a createElement call with a props literal.
* @param {ASTNode} node - The AST node being checked.
* @param {Context} context - The AST node being checked.
* @returns {boolean} - True if node is a createElement call with a props
* object literal, False if not.
*/
function isCreateElementWithProps(node, context) {
return isCreateElement(context, node)
&& node.arguments.length > 1
&& node.arguments[1].type === 'ObjectExpression';
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
nestChildren: 'Do not pass children as props. Instead, nest children between the opening and closing tags.',
passChildrenAsArgs: 'Do not pass children as props. Instead, pass them as additional arguments to React.createElement.',
nestFunction: 'Do not nest a function between the opening and closing tags. Instead, pass it as a prop.',
passFunctionAsArgs: 'Do not pass a function as an additional argument to React.createElement. Instead, pass it as a prop.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow passing of children as props',
category: 'Best Practices',
recommended: true,
url: docsUrl('no-children-prop'),
},
messages,
schema: [{
type: 'object',
properties: {
allowFunctions: {
type: 'boolean',
default: false,
},
},
additionalProperties: false,
}],
},
create(context) {
const configuration = context.options[0] || {};
function isFunction(node) {
return configuration.allowFunctions && (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression');
}
return {
JSXAttribute(node) {
if (node.name.name !== 'children') {
return;
}
const value = node.value;
if (value && value.type === 'JSXExpressionContainer' && isFunction(value.expression)) {
return;
}
report(context, messages.nestChildren, 'nestChildren', {
node,
});
},
CallExpression(node) {
if (!isCreateElementWithProps(node, context)) {
return;
}
const props = 'properties' in node.arguments[1] ? node.arguments[1].properties : undefined;
const childrenProp = props.find((prop) => (
'key' in prop
&& prop.key
&& 'name' in prop.key
&& prop.key.name === 'children'
));
if (childrenProp) {
if ('value' in childrenProp && childrenProp.value && !isFunction(childrenProp.value)) {
report(context, messages.passChildrenAsArgs, 'passChildrenAsArgs', {
node,
});
}
} else if (node.arguments.length === 3) {
const children = node.arguments[2];
if (isFunction(children)) {
report(context, messages.passFunctionAsArgs, 'passFunctionAsArgs', {
node,
});
}
}
},
JSXElement(node) {
const children = node.children;
if (children && children.length === 1 && children[0].type === 'JSXExpressionContainer') {
if (isFunction(children[0].expression)) {
report(context, messages.nestFunction, 'nestFunction', {
node,
});
}
}
},
};
},
};

View File

@@ -0,0 +1,157 @@
/**
* @fileoverview Report when a DOM element is using both children and dangerouslySetInnerHTML
* @author David Petersen
*/
'use strict';
const variableUtil = require('../util/variable');
const jsxUtil = require('../util/jsx');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
dangerWithChildren: 'Only set one of `children` or `props.dangerouslySetInnerHTML`',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow when a DOM element is using both children and dangerouslySetInnerHTML',
category: 'Possible Errors',
recommended: true,
url: docsUrl('no-danger-with-children'),
},
messages,
schema: [], // no options
},
create(context) {
function findSpreadVariable(node, name) {
return variableUtil.getVariableFromContext(context, node, name);
}
/**
* Takes a ObjectExpression and returns the value of the prop if it has it
* @param {object} node - ObjectExpression node
* @param {string} propName - name of the prop to look for
* @param {string[]} seenProps
* @returns {object | boolean}
*/
function findObjectProp(node, propName, seenProps) {
if (!node.properties) {
return false;
}
return node.properties.find((prop) => {
if (prop.type === 'Property') {
return prop.key.name === propName;
}
if (prop.type === 'ExperimentalSpreadProperty' || prop.type === 'SpreadElement') {
const variable = findSpreadVariable(node, prop.argument.name);
if (variable && variable.defs.length && variable.defs[0].node.init) {
if (seenProps.indexOf(prop.argument.name) > -1) {
return false;
}
const newSeenProps = seenProps.concat(prop.argument.name || []);
return findObjectProp(variable.defs[0].node.init, propName, newSeenProps);
}
}
return false;
});
}
/**
* Takes a JSXElement and returns the value of the prop if it has it
* @param {object} node - JSXElement node
* @param {string} propName - name of the prop to look for
* @returns {object | boolean}
*/
function findJsxProp(node, propName) {
const attributes = node.openingElement.attributes;
return attributes.find((attribute) => {
if (attribute.type === 'JSXSpreadAttribute') {
const variable = findSpreadVariable(node, attribute.argument.name);
if (variable && variable.defs.length && variable.defs[0].node.init) {
return findObjectProp(variable.defs[0].node.init, propName, []);
}
}
return attribute.name && attribute.name.name === propName;
});
}
/**
* Checks to see if a node is a line break
* @param {ASTNode} node The AST node being checked
* @returns {boolean} True if node is a line break, false if not
*/
function isLineBreak(node) {
const isLiteral = node.type === 'Literal' || node.type === 'JSXText';
const isMultiline = node.loc.start.line !== node.loc.end.line;
const isWhiteSpaces = jsxUtil.isWhiteSpaces(node.value);
return isLiteral && isMultiline && isWhiteSpaces;
}
return {
JSXElement(node) {
let hasChildren = false;
if (node.children.length && !isLineBreak(node.children[0])) {
hasChildren = true;
} else if (findJsxProp(node, 'children')) {
hasChildren = true;
}
if (
node.openingElement.attributes
&& hasChildren
&& findJsxProp(node, 'dangerouslySetInnerHTML')
) {
report(context, messages.dangerWithChildren, 'dangerWithChildren', {
node,
});
}
},
CallExpression(node) {
if (
node.callee
&& node.callee.type === 'MemberExpression'
&& 'name' in node.callee.property
&& node.callee.property.name === 'createElement'
&& node.arguments.length > 1
) {
let hasChildren = false;
let props = node.arguments[1];
if (props.type === 'Identifier') {
const variable = variableUtil.getVariableFromContext(context, node, props.name);
if (variable && variable.defs.length && variable.defs[0].node.init) {
props = variable.defs[0].node.init;
}
}
const dangerously = findObjectProp(props, 'dangerouslySetInnerHTML', []);
if (node.arguments.length === 2) {
if (findObjectProp(props, 'children', [])) {
hasChildren = true;
}
} else {
hasChildren = true;
}
if (dangerously && hasChildren) {
report(context, messages.dangerWithChildren, 'dangerWithChildren', {
node,
});
}
}
},
};
},
};

View File

@@ -0,0 +1,95 @@
/**
* @fileoverview Prevent usage of dangerous JSX props
* @author Scott Andrews
*/
'use strict';
const has = require('hasown');
const fromEntries = require('object.fromentries/polyfill')();
const minimatch = require('minimatch');
const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const DANGEROUS_PROPERTY_NAMES = [
'dangerouslySetInnerHTML',
];
const DANGEROUS_PROPERTIES = fromEntries(DANGEROUS_PROPERTY_NAMES.map((prop) => [prop, prop]));
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Checks if a JSX attribute is dangerous.
* @param {string} name - Name of the attribute to check.
* @returns {boolean} Whether or not the attribute is dangerous.
*/
function isDangerous(name) {
return has(DANGEROUS_PROPERTIES, name);
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
dangerousProp: 'Dangerous property \'{{name}}\' found',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow usage of dangerous JSX properties',
category: 'Best Practices',
recommended: false,
url: docsUrl('no-danger'),
},
messages,
schema: [{
type: 'object',
properties: {
customComponentNames: {
items: {
type: 'string',
},
minItems: 0,
type: 'array',
uniqueItems: true,
},
},
}],
},
create(context) {
const configuration = context.options[0] || {};
const customComponentNames = configuration.customComponentNames || [];
return {
JSXAttribute(node) {
const functionName = node.parent.name.name;
const enableCheckingCustomComponent = customComponentNames.some((name) => minimatch(functionName, name));
if ((enableCheckingCustomComponent || jsxUtil.isDOMComponent(node.parent)) && isDangerous(node.name.name)) {
report(context, messages.dangerousProp, 'dangerousProp', {
node,
data: {
name: node.name.name,
},
});
}
},
};
},
};

View File

@@ -0,0 +1,271 @@
/**
* @fileoverview Prevent usage of deprecated methods
* @author Yannick Croissant
* @author Scott Feeney
* @author Sergei Startsev
*/
'use strict';
const entries = require('object.entries');
const astUtil = require('../util/ast');
const componentUtil = require('../util/componentUtil');
const docsUrl = require('../util/docsUrl');
const pragmaUtil = require('../util/pragma');
const testReactVersion = require('../util/version').testReactVersion;
const report = require('../util/report');
const getText = require('../util/eslint').getText;
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const MODULES = {
react: ['React'],
'react-addons-perf': ['ReactPerf', 'Perf'],
'react-dom': ['ReactDOM'],
'react-dom/server': ['ReactDOMServer'],
};
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function getDeprecated(pragma) {
const deprecated = {};
// 0.12.0
deprecated[`${pragma}.renderComponent`] = ['0.12.0', `${pragma}.render`];
deprecated[`${pragma}.renderComponentToString`] = ['0.12.0', `${pragma}.renderToString`];
deprecated[`${pragma}.renderComponentToStaticMarkup`] = ['0.12.0', `${pragma}.renderToStaticMarkup`];
deprecated[`${pragma}.isValidComponent`] = ['0.12.0', `${pragma}.isValidElement`];
deprecated[`${pragma}.PropTypes.component`] = ['0.12.0', `${pragma}.PropTypes.element`];
deprecated[`${pragma}.PropTypes.renderable`] = ['0.12.0', `${pragma}.PropTypes.node`];
deprecated[`${pragma}.isValidClass`] = ['0.12.0'];
deprecated['this.transferPropsTo'] = ['0.12.0', 'spread operator ({...})'];
// 0.13.0
deprecated[`${pragma}.addons.classSet`] = ['0.13.0', 'the npm module classnames'];
deprecated[`${pragma}.addons.cloneWithProps`] = ['0.13.0', `${pragma}.cloneElement`];
// 0.14.0
deprecated[`${pragma}.render`] = ['0.14.0', 'ReactDOM.render'];
deprecated[`${pragma}.unmountComponentAtNode`] = ['0.14.0', 'ReactDOM.unmountComponentAtNode'];
deprecated[`${pragma}.findDOMNode`] = ['0.14.0', 'ReactDOM.findDOMNode'];
deprecated[`${pragma}.renderToString`] = ['0.14.0', 'ReactDOMServer.renderToString'];
deprecated[`${pragma}.renderToStaticMarkup`] = ['0.14.0', 'ReactDOMServer.renderToStaticMarkup'];
// 15.0.0
deprecated[`${pragma}.addons.LinkedStateMixin`] = ['15.0.0'];
deprecated['ReactPerf.printDOM'] = ['15.0.0', 'ReactPerf.printOperations'];
deprecated['Perf.printDOM'] = ['15.0.0', 'Perf.printOperations'];
deprecated['ReactPerf.getMeasurementsSummaryMap'] = ['15.0.0', 'ReactPerf.getWasted'];
deprecated['Perf.getMeasurementsSummaryMap'] = ['15.0.0', 'Perf.getWasted'];
// 15.5.0
deprecated[`${pragma}.createClass`] = ['15.5.0', 'the npm module create-react-class'];
deprecated[`${pragma}.addons.TestUtils`] = ['15.5.0', 'ReactDOM.TestUtils'];
deprecated[`${pragma}.PropTypes`] = ['15.5.0', 'the npm module prop-types'];
// 15.6.0
deprecated[`${pragma}.DOM`] = ['15.6.0', 'the npm module react-dom-factories'];
// 16.9.0
// For now the following life-cycle methods are just legacy, not deprecated:
// `componentWillMount`, `componentWillReceiveProps`, `componentWillUpdate`
// https://github.com/yannickcr/eslint-plugin-react/pull/1750#issuecomment-425975934
deprecated.componentWillMount = [
'16.9.0',
'UNSAFE_componentWillMount',
'https://reactjs.org/docs/react-component.html#unsafe_componentwillmount. '
+ 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.',
];
deprecated.componentWillReceiveProps = [
'16.9.0',
'UNSAFE_componentWillReceiveProps',
'https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops. '
+ 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.',
];
deprecated.componentWillUpdate = [
'16.9.0',
'UNSAFE_componentWillUpdate',
'https://reactjs.org/docs/react-component.html#unsafe_componentwillupdate. '
+ 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.',
];
// 18.0.0
// https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#deprecations
deprecated['ReactDOM.render'] = [
'18.0.0',
'createRoot',
'https://reactjs.org/link/switch-to-createroot',
];
deprecated['ReactDOM.hydrate'] = [
'18.0.0',
'hydrateRoot',
'https://reactjs.org/link/switch-to-createroot',
];
deprecated['ReactDOM.unmountComponentAtNode'] = [
'18.0.0',
'root.unmount',
'https://reactjs.org/link/switch-to-createroot',
];
deprecated['ReactDOMServer.renderToNodeStream'] = [
'18.0.0',
'renderToPipeableStream',
'https://reactjs.org/docs/react-dom-server.html#rendertonodestream',
];
return deprecated;
}
const messages = {
deprecated: '{{oldMethod}} is deprecated since React {{version}}{{newMethod}}{{refs}}',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow usage of deprecated methods',
category: 'Best Practices',
recommended: true,
url: docsUrl('no-deprecated'),
},
messages,
schema: [],
},
create(context) {
const pragma = pragmaUtil.getFromContext(context);
const deprecated = getDeprecated(pragma);
function isDeprecated(method) {
return (
deprecated
&& deprecated[method]
&& deprecated[method][0]
&& testReactVersion(context, `>= ${deprecated[method][0]}`)
);
}
function checkDeprecation(node, methodName, methodNode) {
if (!isDeprecated(methodName)) {
return;
}
const version = deprecated[methodName][0];
const newMethod = deprecated[methodName][1];
const refs = deprecated[methodName][2];
report(context, messages.deprecated, 'deprecated', {
node: methodNode || node,
data: {
oldMethod: methodName,
version,
newMethod: newMethod ? `, use ${newMethod} instead` : '',
refs: refs ? `, see ${refs}` : '',
},
});
}
function getReactModuleName(node) {
let moduleName = false;
if (!node.init) {
return false;
}
entries(MODULES).some((entry) => {
const key = entry[0];
const moduleNames = entry[1];
if (
node.init.arguments
&& node.init.arguments.length > 0
&& node.init.arguments[0]
&& key === node.init.arguments[0].value
) {
moduleName = MODULES[key][0];
} else {
moduleName = moduleNames.find((name) => name === node.init.name);
}
return moduleName;
});
return moduleName;
}
/**
* Returns life cycle methods if available
* @param {ASTNode} node The AST node being checked.
* @returns {Array} The array of methods.
*/
function getLifeCycleMethods(node) {
const properties = astUtil.getComponentProperties(node);
return properties.map((property) => ({
name: astUtil.getPropertyName(property),
node: astUtil.getPropertyNameNode(property),
}));
}
/**
* Checks life cycle methods
* @param {ASTNode} node The AST node being checked.
*/
function checkLifeCycleMethods(node) {
if (
componentUtil.isES5Component(node, context)
|| componentUtil.isES6Component(node, context)
) {
const methods = getLifeCycleMethods(node);
methods.forEach((method) => checkDeprecation(node, method.name, method.node));
}
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
MemberExpression(node) {
checkDeprecation(node, getText(context, node));
},
ImportDeclaration(node) {
const isReactImport = typeof MODULES[node.source.value] !== 'undefined';
if (!isReactImport) {
return;
}
node.specifiers.filter(((s) => 'imported' in s && s.imported)).forEach((specifier) => {
// TODO, semver-major: remove `in` check as part of jsdoc->tsdoc migration
checkDeprecation(node, 'imported' in specifier && `${MODULES[node.source.value][0]}.${specifier.imported.name}`, specifier);
});
},
VariableDeclarator(node) {
const reactModuleName = getReactModuleName(node);
const isRequire = node.init
&& 'callee' in node.init
&& node.init.callee
&& 'name' in node.init.callee
&& node.init.callee.name === 'require';
const isReactRequire = node.init
&& 'arguments' in node.init
&& node.init.arguments
&& node.init.arguments.length
&& typeof MODULES['value' in node.init.arguments[0] ? node.init.arguments[0].value : undefined] !== 'undefined';
const isDestructuring = node.id && node.id.type === 'ObjectPattern';
if (
!(isDestructuring && reactModuleName)
&& !(isDestructuring && isRequire && isReactRequire)
) {
return;
}
('properties' in node.id ? node.id.properties : undefined).filter((p) => p.type !== 'RestElement' && p.key).forEach((property) => {
checkDeprecation(
node,
'key' in property && 'name' in property.key && `${reactModuleName || pragma}.${property.key.name}`,
property
);
});
},
ClassDeclaration: checkLifeCycleMethods,
ClassExpression: checkLifeCycleMethods,
ObjectExpression: checkLifeCycleMethods,
};
},
};

View File

@@ -0,0 +1,11 @@
/**
* @fileoverview Prevent usage of setState in componentDidMount
* @author Yannick Croissant
*/
'use strict';
const makeNoMethodSetStateRule = require('../util/makeNoMethodSetStateRule');
/** @type {import('eslint').Rule.RuleModule} */
module.exports = makeNoMethodSetStateRule('componentDidMount');

View File

@@ -0,0 +1,11 @@
/**
* @fileoverview Prevent usage of setState in componentDidUpdate
* @author Yannick Croissant
*/
'use strict';
const makeNoMethodSetStateRule = require('../util/makeNoMethodSetStateRule');
/** @type {import('eslint').Rule.RuleModule} */
module.exports = makeNoMethodSetStateRule('componentDidUpdate');

View File

@@ -0,0 +1,155 @@
/**
* @fileoverview Prevent direct mutation of this.state
* @author David Petersen
* @author Nicolas Fernandez <@burabure>
*/
'use strict';
const values = require('object.values');
const Components = require('../util/Components');
const componentUtil = require('../util/componentUtil');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
noDirectMutation: 'Do not mutate state directly. Use setState().',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow direct mutation of this.state',
category: 'Possible Errors',
recommended: true,
url: docsUrl('no-direct-mutation-state'),
},
messages,
},
create: Components.detect((context, components, utils) => {
/**
* Checks if the component is valid
* @param {Object} component The component to process
* @returns {boolean} True if the component is valid, false if not.
*/
function isValid(component) {
return !!component && !component.mutateSetState;
}
/**
* Reports undeclared proptypes for a given component
* @param {Object} component The component to process
*/
function reportMutations(component) {
let mutation;
for (let i = 0, j = component.mutations.length; i < j; i++) {
mutation = component.mutations[i];
report(context, messages.noDirectMutation, 'noDirectMutation', {
node: mutation,
});
}
}
/**
* Walks through the MemberExpression to the top-most property.
* @param {Object} node The node to process
* @returns {Object} The outer-most MemberExpression
*/
function getOuterMemberExpression(node) {
while (node.object && node.object.property) {
node = node.object;
}
return node;
}
/**
* Determine if we should currently ignore assignments in this component.
* @param {?Object} component The component to process
* @returns {boolean} True if we should skip assignment checks.
*/
function shouldIgnoreComponent(component) {
return !component || (component.inConstructor && !component.inCallExpression);
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
MethodDefinition(node) {
if (node.kind === 'constructor') {
components.set(node, {
inConstructor: true,
});
}
},
CallExpression(node) {
components.set(node, {
inCallExpression: true,
});
},
AssignmentExpression(node) {
const component = components.get(utils.getParentComponent(node));
if (shouldIgnoreComponent(component) || !node.left || !node.left.object) {
return;
}
const item = getOuterMemberExpression(node.left);
if (componentUtil.isStateMemberExpression(item)) {
const mutations = (component && component.mutations) || [];
mutations.push(node.left.object);
components.set(node, {
mutateSetState: true,
mutations,
});
}
},
UpdateExpression(node) {
const component = components.get(utils.getParentComponent(node));
if (shouldIgnoreComponent(component) || node.argument.type !== 'MemberExpression') {
return;
}
const item = getOuterMemberExpression(node.argument);
if (componentUtil.isStateMemberExpression(item)) {
const mutations = (component && component.mutations) || [];
mutations.push(item);
components.set(node, {
mutateSetState: true,
mutations,
});
}
},
'CallExpression:exit'(node) {
components.set(node, {
inCallExpression: false,
});
},
'MethodDefinition:exit'(node) {
if (node.kind === 'constructor') {
components.set(node, {
inConstructor: false,
});
}
},
'Program:exit'() {
values(components.list())
.filter((component) => !isValid(component))
.forEach((component) => {
reportMutations(component);
});
},
};
}),
};

View File

@@ -0,0 +1,56 @@
/**
* @fileoverview Prevent usage of findDOMNode
* @author Yannick Croissant
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
noFindDOMNode: 'Do not use findDOMNode. It doesnt work with function components and is deprecated in StrictMode. See https://reactjs.org/docs/react-dom.html#finddomnode',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow usage of findDOMNode',
category: 'Best Practices',
recommended: true,
url: docsUrl('no-find-dom-node'),
},
messages,
schema: [],
},
create(context) {
return {
CallExpression(node) {
const callee = node.callee;
const isFindDOMNode = ('name' in callee && callee.name === 'findDOMNode') || (
'property' in callee
&& callee.property
&& 'name' in callee.property
&& callee.property.name === 'findDOMNode'
);
if (!isFindDOMNode) {
return;
}
report(context, messages.noFindDOMNode, 'noFindDOMNode', {
node: callee,
});
},
};
},
};

View File

@@ -0,0 +1,654 @@
/**
* @fileoverview Check if tag attributes to have non-valid value
* @author Sebastian Malton
*/
'use strict';
const matchAll = require('string.prototype.matchall');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const rel = new Map([
['alternate', new Set(['link', 'area', 'a'])],
['apple-touch-icon', new Set(['link'])],
['apple-touch-startup-image', new Set(['link'])],
['author', new Set(['link', 'area', 'a'])],
['bookmark', new Set(['area', 'a'])],
['canonical', new Set(['link'])],
['dns-prefetch', new Set(['link'])],
['external', new Set(['area', 'a', 'form'])],
['help', new Set(['link', 'area', 'a', 'form'])],
['icon', new Set(['link'])],
['license', new Set(['link', 'area', 'a', 'form'])],
['manifest', new Set(['link'])],
['mask-icon', new Set(['link'])],
['modulepreload', new Set(['link'])],
['next', new Set(['link', 'area', 'a', 'form'])],
['nofollow', new Set(['area', 'a', 'form'])],
['noopener', new Set(['area', 'a', 'form'])],
['noreferrer', new Set(['area', 'a', 'form'])],
['opener', new Set(['area', 'a', 'form'])],
['pingback', new Set(['link'])],
['preconnect', new Set(['link'])],
['prefetch', new Set(['link'])],
['preload', new Set(['link'])],
['prerender', new Set(['link'])],
['prev', new Set(['link', 'area', 'a', 'form'])],
['search', new Set(['link', 'area', 'a', 'form'])],
['shortcut', new Set(['link'])], // generally allowed but needs pair with "icon"
['shortcut\u0020icon', new Set(['link'])],
['stylesheet', new Set(['link'])],
['tag', new Set(['area', 'a'])],
]);
const pairs = new Map([
['shortcut', new Set(['icon'])],
]);
/**
* Map between attributes and a mapping between valid values and a set of tags they are valid on
* @type {Map<string, Map<string, Set<string>>>}
*/
const VALID_VALUES = new Map([
['rel', rel],
]);
/**
* Map between attributes and a mapping between pair-values and a set of values they are valid with
* @type {Map<string, Map<string, Set<string>>>}
*/
const VALID_PAIR_VALUES = new Map([
['rel', pairs],
]);
/**
* The set of all possible HTML elements. Used for skipping custom types
* @type {Set<string>}
*/
const HTML_ELEMENTS = new Set([
'a',
'abbr',
'acronym',
'address',
'applet',
'area',
'article',
'aside',
'audio',
'b',
'base',
'basefont',
'bdi',
'bdo',
'bgsound',
'big',
'blink',
'blockquote',
'body',
'br',
'button',
'canvas',
'caption',
'center',
'cite',
'code',
'col',
'colgroup',
'content',
'data',
'datalist',
'dd',
'del',
'details',
'dfn',
'dialog',
'dir',
'div',
'dl',
'dt',
'em',
'embed',
'fieldset',
'figcaption',
'figure',
'font',
'footer',
'form',
'frame',
'frameset',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'head',
'header',
'hgroup',
'hr',
'html',
'i',
'iframe',
'image',
'img',
'input',
'ins',
'kbd',
'keygen',
'label',
'legend',
'li',
'link',
'main',
'map',
'mark',
'marquee',
'math',
'menu',
'menuitem',
'meta',
'meter',
'nav',
'nobr',
'noembed',
'noframes',
'noscript',
'object',
'ol',
'optgroup',
'option',
'output',
'p',
'param',
'picture',
'plaintext',
'portal',
'pre',
'progress',
'q',
'rb',
'rp',
'rt',
'rtc',
'ruby',
's',
'samp',
'script',
'section',
'select',
'shadow',
'slot',
'small',
'source',
'spacer',
'span',
'strike',
'strong',
'style',
'sub',
'summary',
'sup',
'svg',
'table',
'tbody',
'td',
'template',
'textarea',
'tfoot',
'th',
'thead',
'time',
'title',
'tr',
'track',
'tt',
'u',
'ul',
'var',
'video',
'wbr',
'xmp',
]);
/**
* Map between attributes and set of tags that the attribute is valid on
* @type {Map<string, Set<string>>}
*/
const COMPONENT_ATTRIBUTE_MAP = new Map([
['rel', new Set(['link', 'a', 'area', 'form'])],
]);
/* eslint-disable eslint-plugin/no-unused-message-ids -- false positives, these messageIds are used */
const messages = {
emptyIsMeaningless: 'An empty “{{attributeName}}” attribute is meaningless.',
neverValid: '“{{reportingValue}}” is never a valid “{{attributeName}}” attribute value.',
noEmpty: 'An empty “{{attributeName}}” attribute is meaningless.',
noMethod: 'The ”{{attributeName}}“ attribute cannot be a method.',
notAlone: '“{{reportingValue}}” must be directly followed by “{{missingValue}}”.',
notPaired: '“{{reportingValue}}” can not be directly followed by “{{secondValue}}” without “{{missingValue}}”.',
notValidFor: '“{{reportingValue}}” is not a valid “{{attributeName}}” attribute value for <{{elementName}}>.',
onlyMeaningfulFor: 'The ”{{attributeName}}“ attribute only has meaning on the tags: {{tagNames}}',
onlyStrings: '“{{attributeName}}” attribute only supports strings.',
spaceDelimited: '”{{attributeName}}“ attribute values should be space delimited.',
suggestRemoveDefault: '"remove {{attributeName}}"',
suggestRemoveEmpty: '"remove empty attribute {{attributeName}}"',
suggestRemoveInvalid: '“remove invalid attribute {{reportingValue}}”',
suggestRemoveWhitespaces: 'remove whitespaces in “{{attributeName}}”',
suggestRemoveNonString: 'remove non-string value in “{{attributeName}}”',
};
function splitIntoRangedParts(node, regex) {
const valueRangeStart = node.range[0] + 1; // the plus one is for the initial quote
return Array.from(matchAll(node.value, regex), (match) => {
const start = match.index + valueRangeStart;
const end = start + match[0].length;
return {
reportingValue: `${match[1]}`,
value: match[1],
range: [start, end],
};
});
}
function checkLiteralValueNode(context, attributeName, node, parentNode, parentNodeName) {
if (typeof node.value !== 'string') {
const data = { attributeName, reportingValue: node.value };
report(context, messages.onlyStrings, 'onlyStrings', {
node,
data,
suggest: [{
messageId: 'suggestRemoveNonString',
data,
fix(fixer) { return fixer.remove(parentNode); },
}],
});
return;
}
if (!node.value.trim()) {
const data = { attributeName, reportingValue: node.value };
report(context, messages.noEmpty, 'noEmpty', {
node,
data,
suggest: [{
messageId: 'suggestRemoveEmpty',
data,
fix(fixer) { return fixer.remove(node.parent); },
}],
});
return;
}
const singleAttributeParts = splitIntoRangedParts(node, /(\S+)/g);
singleAttributeParts.forEach((singlePart) => {
const allowedTags = VALID_VALUES.get(attributeName).get(singlePart.value);
const reportingValue = singlePart.reportingValue;
if (!allowedTags) {
const data = {
attributeName,
reportingValue,
};
const suggest = [{
messageId: 'suggestRemoveInvalid',
data,
fix(fixer) { return fixer.removeRange(singlePart.range); },
}];
report(context, messages.neverValid, 'neverValid', {
node,
data,
suggest,
});
} else if (!allowedTags.has(parentNodeName)) {
const data = {
attributeName,
reportingValue,
elementName: parentNodeName,
};
const suggest = [{
messageId: 'suggestRemoveInvalid',
data,
fix(fixer) { return fixer.removeRange(singlePart.range); },
}];
report(context, messages.notValidFor, 'notValidFor', {
node,
data,
suggest,
});
}
});
const allowedPairsForAttribute = VALID_PAIR_VALUES.get(attributeName);
if (allowedPairsForAttribute) {
const pairAttributeParts = splitIntoRangedParts(node, /(?=(\b\S+\s*\S+))/g);
pairAttributeParts.forEach((pairPart) => {
allowedPairsForAttribute.forEach((siblings, pairing) => {
const attributes = pairPart.reportingValue.split('\u0020');
const firstValue = attributes[0];
const secondValue = attributes[1];
if (firstValue === pairing) {
const lastValue = attributes[attributes.length - 1]; // in case of multiple white spaces
if (!siblings.has(lastValue)) {
const message = secondValue ? messages.notPaired : messages.notAlone;
const messageId = secondValue ? 'notPaired' : 'notAlone';
report(context, message, messageId, {
node,
data: {
reportingValue: firstValue,
secondValue,
missingValue: Array.from(siblings).join(', '),
},
suggest: false,
});
}
}
});
});
}
const whitespaceParts = splitIntoRangedParts(node, /(\s+)/g);
whitespaceParts.forEach((whitespacePart) => {
const data = { attributeName };
if (whitespacePart.range[0] === (node.range[0] + 1) || whitespacePart.range[1] === (node.range[1] - 1)) {
report(context, messages.spaceDelimited, 'spaceDelimited', {
node,
data,
suggest: [{
messageId: 'suggestRemoveWhitespaces',
data,
fix(fixer) { return fixer.removeRange(whitespacePart.range); },
}],
});
} else if (whitespacePart.value !== '\u0020') {
report(context, messages.spaceDelimited, 'spaceDelimited', {
node,
data,
suggest: [{
messageId: 'suggestRemoveWhitespaces',
data,
fix(fixer) { return fixer.replaceTextRange(whitespacePart.range, '\u0020'); },
}],
});
}
});
}
const DEFAULT_ATTRIBUTES = ['rel'];
function checkAttribute(context, node) {
const attribute = node.name.name;
const parentNodeName = node.parent.name.name;
if (!COMPONENT_ATTRIBUTE_MAP.has(attribute) || !COMPONENT_ATTRIBUTE_MAP.get(attribute).has(parentNodeName)) {
const tagNames = Array.from(
COMPONENT_ATTRIBUTE_MAP.get(attribute).values(),
(tagName) => `"<${tagName}>"`
).join(', ');
const data = {
attributeName: attribute,
tagNames,
};
report(context, messages.onlyMeaningfulFor, 'onlyMeaningfulFor', {
node: node.name,
data,
suggest: [{
messageId: 'suggestRemoveDefault',
data,
fix(fixer) { return fixer.remove(node); },
}],
});
return;
}
function fix(fixer) { return fixer.remove(node); }
if (!node.value) {
const data = { attributeName: attribute };
report(context, messages.emptyIsMeaningless, 'emptyIsMeaningless', {
node: node.name,
data,
suggest: [{
messageId: 'suggestRemoveEmpty',
data,
fix,
}],
});
return;
}
if (node.value.type === 'Literal') {
return checkLiteralValueNode(context, attribute, node.value, node, parentNodeName);
}
if (node.value.expression.type === 'Literal') {
return checkLiteralValueNode(context, attribute, node.value.expression, node, parentNodeName);
}
if (node.value.type !== 'JSXExpressionContainer') {
return;
}
if (node.value.expression.type === 'ObjectExpression') {
const data = { attributeName: attribute };
report(context, messages.onlyStrings, 'onlyStrings', {
node: node.value,
data,
suggest: [{
messageId: 'suggestRemoveDefault',
data,
fix,
}],
});
} else if (node.value.expression.type === 'Identifier' && node.value.expression.name === 'undefined') {
const data = { attributeName: attribute };
report(context, messages.onlyStrings, 'onlyStrings', {
node: node.value,
data,
suggest: [{
messageId: 'suggestRemoveDefault',
data,
fix,
}],
});
}
}
function isValidCreateElement(node) {
return node.callee
&& node.callee.type === 'MemberExpression'
&& node.callee.object.name === 'React'
&& node.callee.property.name === 'createElement'
&& node.arguments.length > 0;
}
function checkPropValidValue(context, node, value, attribute) {
const validTags = VALID_VALUES.get(attribute);
if (value.type !== 'Literal') {
return; // cannot check non-literals
}
const validTagSet = validTags.get(value.value);
if (!validTagSet) {
const data = {
attributeName: attribute,
reportingValue: value.value,
};
report(context, messages.neverValid, 'neverValid', {
node: value,
data,
suggest: [{
messageId: 'suggestRemoveInvalid',
data,
fix(fixer) { return fixer.replaceText(value, value.raw.replace(value.value, '')); },
}],
});
} else if (!validTagSet.has(node.arguments[0].value)) {
report(context, messages.notValidFor, 'notValidFor', {
node: value,
data: {
attributeName: attribute,
reportingValue: value.raw,
elementName: node.arguments[0].value,
},
suggest: false,
});
}
}
/**
*
* @param {*} context
* @param {*} node
* @param {string} attribute
*/
function checkCreateProps(context, node, attribute) {
const propsArg = node.arguments[1];
if (!propsArg || propsArg.type !== 'ObjectExpression') {
return; // can't check variables, computed, or shorthands
}
for (const prop of propsArg.properties) {
if (!prop.key || prop.key.type !== 'Identifier') {
// eslint-disable-next-line no-continue
continue; // cannot check computed keys
}
if (prop.key.name !== attribute) {
// eslint-disable-next-line no-continue
continue; // ignore not this attribute
}
if (!COMPONENT_ATTRIBUTE_MAP.get(attribute).has(node.arguments[0].value)) {
const tagNames = Array.from(
COMPONENT_ATTRIBUTE_MAP.get(attribute).values(),
(tagName) => `"<${tagName}>"`
).join(', ');
report(context, messages.onlyMeaningfulFor, 'onlyMeaningfulFor', {
node: prop.key,
data: {
attributeName: attribute,
tagNames,
},
suggest: false,
});
// eslint-disable-next-line no-continue
continue;
}
if (prop.method) {
report(context, messages.noMethod, 'noMethod', {
node: prop,
data: {
attributeName: attribute,
},
suggest: false,
});
// eslint-disable-next-line no-continue
continue;
}
if (prop.shorthand || prop.computed) {
// eslint-disable-next-line no-continue
continue; // cannot check these
}
if (prop.value.type === 'ArrayExpression') {
prop.value.elements.forEach((value) => {
checkPropValidValue(context, node, value, attribute);
});
// eslint-disable-next-line no-continue
continue;
}
checkPropValidValue(context, node, prop.value, attribute);
}
}
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow usage of invalid attributes',
category: 'Possible Errors',
url: docsUrl('no-invalid-html-attribute'),
},
messages,
schema: [{
type: 'array',
uniqueItems: true,
items: {
enum: ['rel'],
},
}],
type: 'suggestion',
hasSuggestions: true, // eslint-disable-line eslint-plugin/require-meta-has-suggestions
},
create(context) {
return {
JSXAttribute(node) {
const attributes = new Set(context.options[0] || DEFAULT_ATTRIBUTES);
// ignore attributes that aren't configured to be checked
if (!attributes.has(node.name.name)) {
return;
}
// ignore non-HTML elements
if (!HTML_ELEMENTS.has(node.parent.name.name)) {
return;
}
checkAttribute(context, node);
},
CallExpression(node) {
if (!isValidCreateElement(node)) {
return;
}
const elemNameArg = node.arguments[0];
if (!elemNameArg || elemNameArg.type !== 'Literal') {
return; // can only check literals
}
// ignore non-HTML elements
if (typeof elemNameArg.value === 'string' && !HTML_ELEMENTS.has(elemNameArg.value)) {
return;
}
const attributes = new Set(context.options[0] || DEFAULT_ATTRIBUTES);
attributes.forEach((attribute) => {
checkCreateProps(context, node, attribute);
});
},
};
},
};

View File

@@ -0,0 +1,61 @@
/**
* @fileoverview Prevent usage of isMounted
* @author Joe Lencioni
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const getAncestors = require('../util/eslint').getAncestors;
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
noIsMounted: 'Do not use isMounted',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow usage of isMounted',
category: 'Best Practices',
recommended: true,
url: docsUrl('no-is-mounted'),
},
messages,
schema: [],
},
create(context) {
return {
CallExpression(node) {
const callee = node.callee;
if (callee.type !== 'MemberExpression') {
return;
}
if (
callee.object.type !== 'ThisExpression'
|| !('name' in callee.property)
|| callee.property.name !== 'isMounted'
) {
return;
}
const ancestors = getAncestors(context, node);
for (let i = 0, j = ancestors.length; i < j; i++) {
if (ancestors[i].type === 'Property' || ancestors[i].type === 'MethodDefinition') {
report(context, messages.noIsMounted, 'noIsMounted', {
node: callee,
});
break;
}
}
},
};
},
};

View File

@@ -0,0 +1,81 @@
/**
* @fileoverview Prevent multiple component definition per file
* @author Yannick Croissant
*/
'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 = {
onlyOneComponent: 'Declare only one React component per file',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow multiple component definition per file',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('no-multi-comp'),
},
messages,
schema: [{
type: 'object',
properties: {
ignoreStateless: {
default: false,
type: 'boolean',
},
},
additionalProperties: false,
}],
},
create: Components.detect((context, components, utils) => {
const configuration = context.options[0] || {};
const ignoreStateless = configuration.ignoreStateless || false;
/**
* Checks if the component is ignored
* @param {Object} component The component being checked.
* @returns {boolean} True if the component is ignored, false if not.
*/
function isIgnored(component) {
return (
ignoreStateless && (
/Function/.test(component.node.type)
|| utils.isPragmaComponentWrapper(component.node)
)
);
}
return {
'Program:exit'() {
if (components.length() <= 1) {
return;
}
values(components.list())
.filter((component) => !isIgnored(component))
.slice(1)
.forEach((component) => {
report(context, messages.onlyOneComponent, 'onlyOneComponent', {
node: component.node,
});
});
},
};
}),
};

View File

@@ -0,0 +1,62 @@
/**
* @fileoverview Enforce that namespaces are not used in React elements
* @author Yacine Hmito
*/
'use strict';
const elementType = require('jsx-ast-utils/elementType');
const docsUrl = require('../util/docsUrl');
const isCreateElement = require('../util/isCreateElement');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
noNamespace: 'React component {{name}} must not be in a namespace, as React does not support them',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce that namespaces are not used in React elements',
category: 'Possible Errors',
recommended: false,
url: docsUrl('no-namespace'),
},
messages,
schema: [],
},
create(context) {
return {
CallExpression(node) {
if (isCreateElement(context, node) && node.arguments.length > 0 && node.arguments[0].type === 'Literal') {
const name = node.arguments[0].value;
if (typeof name !== 'string' || name.indexOf(':') === -1) return undefined;
report(context, messages.noNamespace, 'noNamespace', {
node,
data: {
name,
},
});
}
},
JSXOpeningElement(node) {
const name = elementType(node);
if (typeof name !== 'string' || name.indexOf(':') === -1) return undefined;
report(context, messages.noNamespace, 'noNamespace', {
node,
data: {
name,
},
});
},
};
},
};

View File

@@ -0,0 +1,105 @@
/**
* @fileoverview Prevent usage of referential-type variables as default param in functional component
* @author Chang Yan
*/
'use strict';
const values = require('object.values');
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const astUtil = require('../util/ast');
const report = require('../util/report');
const FORBIDDEN_TYPES_MAP = {
ArrowFunctionExpression: 'arrow function',
FunctionExpression: 'function expression',
ObjectExpression: 'object literal',
ArrayExpression: 'array literal',
ClassExpression: 'class expression',
NewExpression: 'construction expression',
JSXElement: 'JSX element',
};
const FORBIDDEN_TYPES = new Set(Object.keys(FORBIDDEN_TYPES_MAP));
const MESSAGE_ID = 'forbiddenTypeDefaultParam';
const messages = {
[MESSAGE_ID]: '{{propName}} has a/an {{forbiddenType}} as default prop. This could lead to potential infinite render loop in React. Use a variable reference instead of {{forbiddenType}}.',
};
function hasUsedObjectDestructuringSyntax(params) {
return (
params != null
&& params.length >= 1
&& params[0].type === 'ObjectPattern'
);
}
function verifyDefaultPropsDestructuring(context, properties) {
// Loop through each of the default params
properties.filter((prop) => prop.type === 'Property' && prop.value.type === 'AssignmentPattern').forEach((prop) => {
const propName = prop.key.name;
const propDefaultValue = prop.value;
const propDefaultValueType = propDefaultValue.right.type;
if (
propDefaultValueType === 'Literal'
&& propDefaultValue.right.regex != null
) {
report(context, messages[MESSAGE_ID], MESSAGE_ID, {
node: propDefaultValue,
data: {
propName,
forbiddenType: 'regex literal',
},
});
} else if (
astUtil.isCallExpression(propDefaultValue.right)
&& propDefaultValue.right.callee.type === 'Identifier'
&& propDefaultValue.right.callee.name === 'Symbol'
) {
report(context, messages[MESSAGE_ID], MESSAGE_ID, {
node: propDefaultValue,
data: {
propName,
forbiddenType: 'Symbol literal',
},
});
} else if (FORBIDDEN_TYPES.has(propDefaultValueType)) {
report(context, messages[MESSAGE_ID], MESSAGE_ID, {
node: propDefaultValue,
data: {
propName,
forbiddenType: FORBIDDEN_TYPES_MAP[propDefaultValueType],
},
});
}
});
}
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow usage of referential-type variables as default param in functional component',
category: 'Best Practices',
recommended: false,
url: docsUrl('no-object-type-as-default-prop'),
},
messages,
},
create: Components.detect((context, components) => ({
'Program:exit'() {
const list = components.list();
values(list)
.filter((component) => hasUsedObjectDestructuringSyntax(component.node.params))
.forEach((component) => {
const node = component.node;
const properties = node.params[0].properties;
verifyDefaultPropsDestructuring(context, properties);
});
},
})),
};

View File

@@ -0,0 +1,88 @@
/**
* @fileoverview Flag shouldComponentUpdate when extending PureComponent
*/
'use strict';
const astUtil = require('../util/ast');
const componentUtil = require('../util/componentUtil');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
noShouldCompUpdate: '{{component}} does not need shouldComponentUpdate when extending React.PureComponent.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow usage of shouldComponentUpdate when extending React.PureComponent',
category: 'Possible Errors',
recommended: false,
url: docsUrl('no-redundant-should-component-update'),
},
messages,
schema: [],
},
create(context) {
/**
* Checks for shouldComponentUpdate property
* @param {ASTNode} node The AST node being checked.
* @returns {boolean} Whether or not the property exists.
*/
function hasShouldComponentUpdate(node) {
const properties = astUtil.getComponentProperties(node);
return properties.some((property) => {
const name = astUtil.getPropertyName(property);
return name === 'shouldComponentUpdate';
});
}
/**
* Get name of node if available
* @param {ASTNode} node The AST node being checked.
* @return {string} The name of the node
*/
function getNodeName(node) {
if (node.id) {
return node.id.name;
}
if (node.parent && node.parent.id) {
return node.parent.id.name;
}
return '';
}
/**
* Checks for violation of rule
* @param {ASTNode} node The AST node being checked.
*/
function checkForViolation(node) {
if (componentUtil.isPureComponent(node, context)) {
const hasScu = hasShouldComponentUpdate(node);
if (hasScu) {
const className = getNodeName(node);
report(context, messages.noShouldCompUpdate, 'noShouldCompUpdate', {
node,
data: {
component: className,
},
});
}
}
}
return {
ClassDeclaration: checkForViolation,
ClassExpression: checkForViolation,
};
},
};

View File

@@ -0,0 +1,82 @@
/**
* @fileoverview Prevent usage of the return value of React.render
* @author Dustan Kasten
*/
'use strict';
const testReactVersion = require('../util/version').testReactVersion;
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
noReturnValue: 'Do not depend on the return value from {{node}}.render',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow usage of the return value of ReactDOM.render',
category: 'Best Practices',
recommended: true,
url: docsUrl('no-render-return-value'),
},
messages,
schema: [],
},
create(context) {
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
let calleeObjectName = /^ReactDOM$/;
if (testReactVersion(context, '>= 15.0.0')) {
calleeObjectName = /^ReactDOM$/;
} else if (testReactVersion(context, '^0.14.0')) {
calleeObjectName = /^React(DOM)?$/;
} else if (testReactVersion(context, '^0.13.0')) {
calleeObjectName = /^React$/;
}
return {
CallExpression(node) {
const callee = node.callee;
const parent = node.parent;
if (callee.type !== 'MemberExpression') {
return;
}
if (
callee.object.type !== 'Identifier'
|| !calleeObjectName.test(callee.object.name)
|| (!('name' in callee.property) || callee.property.name !== 'render')
) {
return;
}
if (
parent.type === 'VariableDeclarator'
|| parent.type === 'Property'
|| parent.type === 'ReturnStatement'
|| parent.type === 'ArrowFunctionExpression'
|| parent.type === 'AssignmentExpression'
) {
report(context, messages.noReturnValue, 'noReturnValue', {
node: callee,
data: {
node: callee.object.name,
},
});
}
},
};
},
};

View File

@@ -0,0 +1,88 @@
/**
* @fileoverview Prevent usage of setState
* @author Mark Dalgleish
*/
'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 = {
noSetState: 'Do not use setState',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow usage of setState',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('no-set-state'),
},
messages,
schema: [],
},
create: Components.detect((context, components, utils) => {
/**
* Checks if the component is valid
* @param {Object} component The component to process
* @returns {boolean} True if the component is valid, false if not.
*/
function isValid(component) {
return !!component && !component.useSetState;
}
/**
* Reports usages of setState for a given component
* @param {Object} component The component to process
*/
function reportSetStateUsages(component) {
for (let i = 0, j = component.setStateUsages.length; i < j; i++) {
const setStateUsage = component.setStateUsages[i];
report(context, messages.noSetState, 'noSetState', {
node: setStateUsage,
});
}
}
return {
CallExpression(node) {
const callee = node.callee;
if (
callee.type !== 'MemberExpression'
|| callee.object.type !== 'ThisExpression'
|| callee.property.name !== 'setState'
) {
return;
}
const component = components.get(utils.getParentComponent(node));
const setStateUsages = (component && component.setStateUsages) || [];
setStateUsages.push(callee);
components.set(node, {
useSetState: true,
setStateUsages,
});
},
'Program:exit'() {
values(components.list())
.filter((component) => !isValid(component))
.forEach((component) => {
reportSetStateUsages(component);
});
},
};
}),
};

View File

@@ -0,0 +1,117 @@
/**
* @fileoverview Prevent string definitions for references and prevent referencing this.refs
* @author Tom Hastjarjanto
*/
'use strict';
const componentUtil = require('../util/componentUtil');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
const testReactVersion = require('../util/version').testReactVersion;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
thisRefsDeprecated: 'Using this.refs is deprecated.',
stringInRefDeprecated: 'Using string literals in ref attributes is deprecated.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow using string references',
category: 'Best Practices',
recommended: true,
url: docsUrl('no-string-refs'),
},
messages,
schema: [{
type: 'object',
properties: {
noTemplateLiterals: {
type: 'boolean',
},
},
additionalProperties: false,
}],
},
create(context) {
const checkRefsUsage = testReactVersion(context, '< 18.3.0'); // `this.refs` is writable in React 18.3.0 and later, see https://github.com/facebook/react/pull/28867
const detectTemplateLiterals = context.options[0] ? context.options[0].noTemplateLiterals : false;
/**
* Checks if we are using refs
* @param {ASTNode} node The AST node being checked.
* @returns {boolean} True if we are using refs, false if not.
*/
function isRefsUsage(node) {
return !!(
(componentUtil.getParentES6Component(context, node) || componentUtil.getParentES5Component(context, node))
&& node.object.type === 'ThisExpression'
&& node.property.name === 'refs'
);
}
/**
* Checks if we are using a ref attribute
* @param {ASTNode} node The AST node being checked.
* @returns {boolean} True if we are using a ref attribute, false if not.
*/
function isRefAttribute(node) {
return node.type === 'JSXAttribute'
&& !!node.name
&& node.name.name === 'ref';
}
/**
* Checks if a node contains a string value
* @param {ASTNode} node The AST node being checked.
* @returns {boolean} True if the node contains a string value, false if not.
*/
function containsStringLiteral(node) {
return !!node.value
&& node.value.type === 'Literal'
&& typeof node.value.value === 'string';
}
/**
* Checks if a node contains a string value within a jsx expression
* @param {ASTNode} node The AST node being checked.
* @returns {boolean} True if the node contains a string value within a jsx expression, false if not.
*/
function containsStringExpressionContainer(node) {
return !!node.value
&& node.value.type === 'JSXExpressionContainer'
&& node.value.expression
&& ((node.value.expression.type === 'Literal' && typeof node.value.expression.value === 'string')
|| (node.value.expression.type === 'TemplateLiteral' && detectTemplateLiterals));
}
return {
MemberExpression(node) {
if (checkRefsUsage && isRefsUsage(node)) {
report(context, messages.thisRefsDeprecated, 'thisRefsDeprecated', {
node,
});
}
},
JSXAttribute(node) {
if (
isRefAttribute(node)
&& (containsStringLiteral(node) || containsStringExpressionContainer(node))
) {
report(context, messages.stringInRefDeprecated, 'stringInRefDeprecated', {
node,
});
}
},
};
},
};

View File

@@ -0,0 +1,47 @@
/**
* @fileoverview Report "this" being used in stateless functional components.
*/
'use strict';
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
noThisInSFC: 'Stateless functional components should not use `this`',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow `this` from being used in stateless functional components',
category: 'Possible Errors',
recommended: false,
url: docsUrl('no-this-in-sfc'),
},
messages,
schema: [],
},
create: Components.detect((context, components, utils) => ({
MemberExpression(node) {
if (node.object.type === 'ThisExpression') {
const component = components.get(utils.getParentStatelessComponent(node));
if (!component || (component.node && component.node.parent && component.node.parent.type === 'Property')) {
return;
}
report(context, messages.noThisInSFC, 'noThisInSFC', {
node,
});
}
},
})),
};

260
node_modules/eslint-plugin-react/lib/rules/no-typos.js generated vendored Normal file
View File

@@ -0,0 +1,260 @@
/**
* @fileoverview Prevent common casing typos
*/
'use strict';
const PROP_TYPES = Object.keys(require('prop-types'));
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const astUtil = require('../util/ast');
const componentUtil = require('../util/componentUtil');
const report = require('../util/report');
const lifecycleMethods = require('../util/lifecycleMethods');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const STATIC_CLASS_PROPERTIES = ['propTypes', 'contextTypes', 'childContextTypes', 'defaultProps'];
const messages = {
typoPropTypeChain: 'Typo in prop type chain qualifier: {{name}}',
typoPropType: 'Typo in declared prop type: {{name}}',
typoStaticClassProp: 'Typo in static class property declaration',
typoPropDeclaration: 'Typo in property declaration',
typoLifecycleMethod: 'Typo in component lifecycle method declaration: {{actual}} should be {{expected}}',
staticLifecycleMethod: 'Lifecycle method should be static: {{method}}',
noPropTypesBinding: '`\'prop-types\'` imported without a local `PropTypes` binding.',
noReactBinding: '`\'react\'` imported without a local `React` binding.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow common typos',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('no-typos'),
},
messages,
schema: [],
},
create: Components.detect((context, components, utils) => {
let propTypesPackageName = null;
let reactPackageName = null;
function checkValidPropTypeQualifier(node) {
if (node.name !== 'isRequired') {
report(context, messages.typoPropTypeChain, 'typoPropTypeChain', {
node,
data: { name: node.name },
});
}
}
function checkValidPropType(node) {
if (node.name && !PROP_TYPES.some((propTypeName) => propTypeName === node.name)) {
report(context, messages.typoPropType, 'typoPropType', {
node,
data: { name: node.name },
});
}
}
function isPropTypesPackage(node) {
return (
node.type === 'Identifier'
&& node.name === propTypesPackageName
) || (
node.type === 'MemberExpression'
&& node.property.name === 'PropTypes'
&& node.object.name === reactPackageName
);
}
/* eslint-disable no-use-before-define */
function checkValidCallExpression(node) {
const callee = node.callee;
if (callee.type === 'MemberExpression' && callee.property.name === 'shape') {
checkValidPropObject(node.arguments[0]);
} else if (callee.type === 'MemberExpression' && callee.property.name === 'oneOfType') {
const args = node.arguments[0];
if (args && args.type === 'ArrayExpression') {
args.elements.forEach((el) => {
checkValidProp(el);
});
}
}
}
function checkValidProp(node) {
if ((!propTypesPackageName && !reactPackageName) || !node) {
return;
}
if (node.type === 'MemberExpression') {
if (
node.object.type === 'MemberExpression'
&& isPropTypesPackage(node.object.object)
) { // PropTypes.myProp.isRequired
checkValidPropType(node.object.property);
checkValidPropTypeQualifier(node.property);
} else if (
isPropTypesPackage(node.object)
&& node.property.name !== 'isRequired'
) { // PropTypes.myProp
checkValidPropType(node.property);
} else if (astUtil.isCallExpression(node.object)) {
checkValidPropTypeQualifier(node.property);
checkValidCallExpression(node.object);
}
} else if (astUtil.isCallExpression(node)) {
checkValidCallExpression(node);
}
}
/* eslint-enable no-use-before-define */
function checkValidPropObject(node) {
if (node && node.type === 'ObjectExpression') {
node.properties.forEach((prop) => checkValidProp(prop.value));
}
}
function reportErrorIfPropertyCasingTypo(propertyValue, propertyKey, isClassProperty) {
const propertyName = propertyKey.name;
if (propertyName === 'propTypes' || propertyName === 'contextTypes' || propertyName === 'childContextTypes') {
checkValidPropObject(propertyValue);
}
STATIC_CLASS_PROPERTIES.forEach((CLASS_PROP) => {
if (propertyName && CLASS_PROP.toLowerCase() === propertyName.toLowerCase() && CLASS_PROP !== propertyName) {
const messageId = isClassProperty
? 'typoStaticClassProp'
: 'typoPropDeclaration';
report(context, messages[messageId], messageId, {
node: propertyKey,
});
}
});
}
function reportErrorIfLifecycleMethodCasingTypo(node) {
const key = node.key;
let nodeKeyName = key.name;
if (key.type === 'Literal') {
nodeKeyName = key.value;
}
if (key.type === 'PrivateName' || (node.computed && typeof nodeKeyName !== 'string')) {
return;
}
lifecycleMethods.static.forEach((method) => {
if (!node.static && nodeKeyName && nodeKeyName.toLowerCase() === method.toLowerCase()) {
report(context, messages.staticLifecycleMethod, 'staticLifecycleMethod', {
node,
data: {
method: nodeKeyName,
},
});
}
});
lifecycleMethods.instance.concat(lifecycleMethods.static).forEach((method) => {
if (nodeKeyName && method.toLowerCase() === nodeKeyName.toLowerCase() && method !== nodeKeyName) {
report(context, messages.typoLifecycleMethod, 'typoLifecycleMethod', {
node,
data: { actual: nodeKeyName, expected: method },
});
}
});
}
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 {
report(context, messages.noPropTypesBinding, 'noPropTypesBinding', {
node,
});
}
} 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"`
} else {
report(context, messages.noReactBinding, 'noReactBinding', {
node,
});
}
if (node.specifiers.length >= 1) {
const propTypesSpecifier = node.specifiers.find((specifier) => (
specifier.imported
&& specifier.imported.name === 'PropTypes'
));
if (propTypesSpecifier) {
propTypesPackageName = propTypesSpecifier.local.name;
}
}
}
},
'ClassProperty, PropertyDefinition'(node) {
if (!node.static || !componentUtil.isES6Component(node.parent.parent, context)) {
return;
}
reportErrorIfPropertyCasingTypo(node.value, node.key, true);
},
MemberExpression(node) {
const propertyName = node.property.name;
if (
!propertyName
|| STATIC_CLASS_PROPERTIES.map((prop) => prop.toLocaleLowerCase()).indexOf(propertyName.toLowerCase()) === -1
) {
return;
}
const relatedComponent = utils.getRelatedComponent(node);
if (
relatedComponent
&& (componentUtil.isES6Component(relatedComponent.node, context) || (
relatedComponent.node.type !== 'ClassDeclaration' && utils.isReturningJSX(relatedComponent.node)))
&& (node.parent && node.parent.type === 'AssignmentExpression' && node.parent.right)
) {
reportErrorIfPropertyCasingTypo(node.parent.right, node.property, true);
}
},
MethodDefinition(node) {
if (!componentUtil.isES6Component(node.parent.parent, context)) {
return;
}
reportErrorIfLifecycleMethodCasingTypo(node);
},
ObjectExpression(node) {
const component = componentUtil.isES5Component(node, context) && components.get(node);
if (!component) {
return;
}
node.properties.filter((property) => property.type !== 'SpreadElement').forEach((property) => {
reportErrorIfPropertyCasingTypo(property.value, property.key, false);
reportErrorIfLifecycleMethodCasingTypo(property);
});
},
};
}),
};

View File

@@ -0,0 +1,135 @@
/**
* @fileoverview HTML special characters should be escaped.
* @author Patrick Hayes
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const getSourceCode = require('../util/eslint').getSourceCode;
const jsxUtil = require('../util/jsx');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
// NOTE: '<' and '{' are also problematic characters, but they do not need
// to be included here because it is a syntax error when these characters are
// included accidentally.
const DEFAULTS = [{
char: '>',
alternatives: ['&gt;'],
}, {
char: '"',
alternatives: ['&quot;', '&ldquo;', '&#34;', '&rdquo;'],
}, {
char: '\'',
alternatives: ['&apos;', '&lsquo;', '&#39;', '&rsquo;'],
}, {
char: '}',
alternatives: ['&#125;'],
}];
const messages = {
unescapedEntity: 'HTML entity, `{{entity}}` , must be escaped.',
unescapedEntityAlts: '`{{entity}}` can be escaped with {{alts}}.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow unescaped HTML entities from appearing in markup',
category: 'Possible Errors',
recommended: true,
url: docsUrl('no-unescaped-entities'),
},
messages,
schema: [{
type: 'object',
properties: {
forbid: {
type: 'array',
items: {
anyOf: [{
type: 'string',
}, {
type: 'object',
properties: {
char: {
type: 'string',
},
alternatives: {
type: 'array',
uniqueItems: true,
items: {
type: 'string',
},
},
},
}],
},
},
},
additionalProperties: false,
}],
},
create(context) {
function reportInvalidEntity(node) {
const configuration = context.options[0] || {};
const entities = configuration.forbid || DEFAULTS;
// HTML entities are already escaped in node.value (as well as node.raw),
// so pull the raw text from getSourceCode(context)
for (let i = node.loc.start.line; i <= node.loc.end.line; i++) {
let rawLine = getSourceCode(context).lines[i - 1];
let start = 0;
let end = rawLine.length;
if (i === node.loc.start.line) {
start = node.loc.start.column;
}
if (i === node.loc.end.line) {
end = node.loc.end.column;
}
rawLine = rawLine.slice(start, end);
for (let j = 0; j < entities.length; j++) {
for (let index = 0; index < rawLine.length; index++) {
const c = rawLine[index];
if (typeof entities[j] === 'string') {
if (c === entities[j]) {
report(context, messages.unescapedEntity, 'unescapedEntity', {
node,
loc: { line: i, column: start + index },
data: {
entity: entities[j],
},
});
}
} else if (c === entities[j].char) {
report(context, messages.unescapedEntityAlts, 'unescapedEntityAlts', {
node,
loc: { line: i, column: start + index },
data: {
entity: entities[j].char,
alts: entities[j].alternatives.map((alt) => `\`${alt}\``).join(', '),
},
});
}
}
}
}
}
return {
'Literal, JSXText'(node) {
if (jsxUtil.isJSX(node.parent)) {
reportInvalidEntity(node);
}
},
};
},
};

View File

@@ -0,0 +1,655 @@
/**
* @fileoverview Prevent usage of unknown DOM property
* @author Yannick Croissant
*/
'use strict';
const has = require('hasown');
const docsUrl = require('../util/docsUrl');
const getText = require('../util/eslint').getText;
const testReactVersion = require('../util/version').testReactVersion;
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const DEFAULTS = {
ignore: [],
requireDataLowercase: false,
};
const DOM_ATTRIBUTE_NAMES = {
'accept-charset': 'acceptCharset',
class: 'className',
'http-equiv': 'httpEquiv',
crossorigin: 'crossOrigin',
for: 'htmlFor',
nomodule: 'noModule',
};
const ATTRIBUTE_TAGS_MAP = {
abbr: ['th', 'td'],
charset: ['meta'],
checked: ['input'],
// image is required for SVG support, all other tags are HTML.
crossOrigin: ['script', 'img', 'video', 'audio', 'link', 'image'],
displaystyle: ['math'],
// https://html.spec.whatwg.org/multipage/links.html#downloading-resources
download: ['a', 'area'],
fill: [ // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill
// Fill color
'altGlyph',
'circle',
'ellipse',
'g',
'line',
'marker',
'mask',
'path',
'polygon',
'polyline',
'rect',
'svg',
'symbol',
'text',
'textPath',
'tref',
'tspan',
'use',
// Animation final state
'animate',
'animateColor',
'animateMotion',
'animateTransform',
'set',
],
focusable: ['svg'],
imageSizes: ['link'],
imageSrcSet: ['link'],
property: ['meta'],
viewBox: ['marker', 'pattern', 'svg', 'symbol', 'view'],
as: ['link'],
align: ['applet', 'caption', 'col', 'colgroup', 'hr', 'iframe', 'img', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr'], // deprecated, but known
valign: ['tr', 'td', 'th', 'thead', 'tbody', 'tfoot', 'colgroup', 'col'], // deprecated, but known
noModule: ['script'],
// Media events allowed only on audio and video tags, see https://github.com/facebook/react/blob/256aefbea1449869620fb26f6ec695536ab453f5/CHANGELOG.md#notable-enhancements
onAbort: ['audio', 'video'],
onCancel: ['dialog'],
onCanPlay: ['audio', 'video'],
onCanPlayThrough: ['audio', 'video'],
onClose: ['dialog'],
onDurationChange: ['audio', 'video'],
onEmptied: ['audio', 'video'],
onEncrypted: ['audio', 'video'],
onEnded: ['audio', 'video'],
onError: ['audio', 'video', 'img', 'link', 'source', 'script', 'picture', 'iframe'],
onLoad: ['script', 'img', 'link', 'picture', 'iframe', 'object', 'source'],
onLoadedData: ['audio', 'video'],
onLoadedMetadata: ['audio', 'video'],
onLoadStart: ['audio', 'video'],
onPause: ['audio', 'video'],
onPlay: ['audio', 'video'],
onPlaying: ['audio', 'video'],
onProgress: ['audio', 'video'],
onRateChange: ['audio', 'video'],
onResize: ['audio', 'video'],
onSeeked: ['audio', 'video'],
onSeeking: ['audio', 'video'],
onStalled: ['audio', 'video'],
onSuspend: ['audio', 'video'],
onTimeUpdate: ['audio', 'video'],
onVolumeChange: ['audio', 'video'],
onWaiting: ['audio', 'video'],
autoPictureInPicture: ['video'],
controls: ['audio', 'video'],
controlsList: ['audio', 'video'],
disablePictureInPicture: ['video'],
disableRemotePlayback: ['audio', 'video'],
loop: ['audio', 'video'],
muted: ['audio', 'video'],
playsInline: ['video'],
allowFullScreen: ['iframe', 'video'],
webkitAllowFullScreen: ['iframe', 'video'],
mozAllowFullScreen: ['iframe', 'video'],
poster: ['video'],
preload: ['audio', 'video'],
scrolling: ['iframe'],
returnValue: ['dialog'],
webkitDirectory: ['input'],
};
const SVGDOM_ATTRIBUTE_NAMES = {
'accent-height': 'accentHeight',
'alignment-baseline': 'alignmentBaseline',
'arabic-form': 'arabicForm',
'baseline-shift': 'baselineShift',
'cap-height': 'capHeight',
'clip-path': 'clipPath',
'clip-rule': 'clipRule',
'color-interpolation': 'colorInterpolation',
'color-interpolation-filters': 'colorInterpolationFilters',
'color-profile': 'colorProfile',
'color-rendering': 'colorRendering',
'dominant-baseline': 'dominantBaseline',
'enable-background': 'enableBackground',
'fill-opacity': 'fillOpacity',
'fill-rule': 'fillRule',
'flood-color': 'floodColor',
'flood-opacity': 'floodOpacity',
'font-family': 'fontFamily',
'font-size': 'fontSize',
'font-size-adjust': 'fontSizeAdjust',
'font-stretch': 'fontStretch',
'font-style': 'fontStyle',
'font-variant': 'fontVariant',
'font-weight': 'fontWeight',
'glyph-name': 'glyphName',
'glyph-orientation-horizontal': 'glyphOrientationHorizontal',
'glyph-orientation-vertical': 'glyphOrientationVertical',
'horiz-adv-x': 'horizAdvX',
'horiz-origin-x': 'horizOriginX',
'image-rendering': 'imageRendering',
'letter-spacing': 'letterSpacing',
'lighting-color': 'lightingColor',
'marker-end': 'markerEnd',
'marker-mid': 'markerMid',
'marker-start': 'markerStart',
'overline-position': 'overlinePosition',
'overline-thickness': 'overlineThickness',
'paint-order': 'paintOrder',
'panose-1': 'panose1',
'pointer-events': 'pointerEvents',
'rendering-intent': 'renderingIntent',
'shape-rendering': 'shapeRendering',
'stop-color': 'stopColor',
'stop-opacity': 'stopOpacity',
'strikethrough-position': 'strikethroughPosition',
'strikethrough-thickness': 'strikethroughThickness',
'stroke-dasharray': 'strokeDasharray',
'stroke-dashoffset': 'strokeDashoffset',
'stroke-linecap': 'strokeLinecap',
'stroke-linejoin': 'strokeLinejoin',
'stroke-miterlimit': 'strokeMiterlimit',
'stroke-opacity': 'strokeOpacity',
'stroke-width': 'strokeWidth',
'text-anchor': 'textAnchor',
'text-decoration': 'textDecoration',
'text-rendering': 'textRendering',
'underline-position': 'underlinePosition',
'underline-thickness': 'underlineThickness',
'unicode-bidi': 'unicodeBidi',
'unicode-range': 'unicodeRange',
'units-per-em': 'unitsPerEm',
'v-alphabetic': 'vAlphabetic',
'v-hanging': 'vHanging',
'v-ideographic': 'vIdeographic',
'v-mathematical': 'vMathematical',
'vector-effect': 'vectorEffect',
'vert-adv-y': 'vertAdvY',
'vert-origin-x': 'vertOriginX',
'vert-origin-y': 'vertOriginY',
'word-spacing': 'wordSpacing',
'writing-mode': 'writingMode',
'x-height': 'xHeight',
'xlink:actuate': 'xlinkActuate',
'xlink:arcrole': 'xlinkArcrole',
'xlink:href': 'xlinkHref',
'xlink:role': 'xlinkRole',
'xlink:show': 'xlinkShow',
'xlink:title': 'xlinkTitle',
'xlink:type': 'xlinkType',
'xml:base': 'xmlBase',
'xml:lang': 'xmlLang',
'xml:space': 'xmlSpace',
};
const DOM_PROPERTY_NAMES_ONE_WORD = [
// Global attributes - can be used on any HTML/DOM element
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes
'dir', 'draggable', 'hidden', 'id', 'lang', 'nonce', 'part', 'slot', 'style', 'title', 'translate', 'inert',
// Element specific attributes
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes (includes global attributes too)
// To be considered if these should be added also to ATTRIBUTE_TAGS_MAP
'accept', 'action', 'allow', 'alt', 'as', 'async', 'buffered', 'capture', 'challenge', 'cite', 'code', 'cols',
'content', 'coords', 'csp', 'data', 'decoding', 'default', 'defer', 'disabled', 'form',
'headers', 'height', 'high', 'href', 'icon', 'importance', 'integrity', 'kind', 'label',
'language', 'loading', 'list', 'loop', 'low', 'manifest', 'max', 'media', 'method', 'min', 'multiple', 'muted',
'name', 'open', 'optimum', 'pattern', 'ping', 'placeholder', 'poster', 'preload', 'profile',
'rel', 'required', 'reversed', 'role', 'rows', 'sandbox', 'scope', 'seamless', 'selected', 'shape', 'size', 'sizes',
'span', 'src', 'start', 'step', 'summary', 'target', 'type', 'value', 'width', 'wmode', 'wrap',
// SVG attributes
// See https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
'accumulate', 'additive', 'alphabetic', 'amplitude', 'ascent', 'azimuth', 'bbox', 'begin',
'bias', 'by', 'clip', 'color', 'cursor', 'cx', 'cy', 'd', 'decelerate', 'descent', 'direction',
'display', 'divisor', 'dur', 'dx', 'dy', 'elevation', 'end', 'exponent', 'fill', 'filter',
'format', 'from', 'fr', 'fx', 'fy', 'g1', 'g2', 'hanging', 'height', 'hreflang', 'ideographic',
'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'local', 'mask', 'mode',
'offset', 'opacity', 'operator', 'order', 'orient', 'orientation', 'origin', 'overflow', 'path',
'ping', 'points', 'r', 'radius', 'rel', 'restart', 'result', 'rotate', 'rx', 'ry', 'scale',
'seed', 'slope', 'spacing', 'speed', 'stemh', 'stemv', 'string', 'stroke', 'to', 'transform',
'u1', 'u2', 'unicode', 'values', 'version', 'visibility', 'widths', 'x', 'x1', 'x2', 'xmlns',
'y', 'y1', 'y2', 'z',
// OpenGraph meta tag attributes
'property',
// React specific attributes
'ref', 'key', 'children',
// Non-standard
'results', 'security',
// Video specific
'controls',
// popovers
'popover', 'popovertarget', 'popovertargetaction',
];
const DOM_PROPERTY_NAMES_TWO_WORDS = [
// Global attributes - can be used on any HTML/DOM element
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes
'accessKey', 'autoCapitalize', 'autoFocus', 'contentEditable', 'enterKeyHint', 'exportParts',
'inputMode', 'itemID', 'itemRef', 'itemProp', 'itemScope', 'itemType', 'spellCheck', 'tabIndex',
// Element specific attributes
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes (includes global attributes too)
// To be considered if these should be added also to ATTRIBUTE_TAGS_MAP
'acceptCharset', 'autoComplete', 'autoPlay', 'border', 'cellPadding', 'cellSpacing', 'classID', 'codeBase',
'colSpan', 'contextMenu', 'dateTime', 'encType', 'formAction', 'formEncType', 'formMethod', 'formNoValidate', 'formTarget',
'frameBorder', 'hrefLang', 'httpEquiv', 'imageSizes', 'imageSrcSet', 'isMap', 'keyParams', 'keyType', 'marginHeight', 'marginWidth',
'maxLength', 'mediaGroup', 'minLength', 'noValidate', 'onAnimationEnd', 'onAnimationIteration', 'onAnimationStart',
'onBlur', 'onChange', 'onClick', 'onContextMenu', 'onCopy', 'onCompositionEnd', 'onCompositionStart',
'onCompositionUpdate', 'onCut', 'onDoubleClick', 'onDrag', 'onDragEnd', 'onDragEnter', 'onDragExit', 'onDragLeave',
'onError', 'onFocus', 'onInput', 'onKeyDown', 'onKeyPress', 'onKeyUp', 'onLoad', 'onWheel', 'onDragOver',
'onDragStart', 'onDrop', 'onMouseDown', 'onMouseEnter', 'onMouseLeave', 'onMouseMove', 'onMouseOut', 'onMouseOver',
'onMouseUp', 'onPaste', 'onScroll', 'onSelect', 'onSubmit', 'onToggle', 'onTransitionEnd', 'radioGroup', 'readOnly', 'referrerPolicy',
'rowSpan', 'srcDoc', 'srcLang', 'srcSet', 'useMap', 'fetchPriority',
// SVG attributes
// See https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
'crossOrigin', 'accentHeight', 'alignmentBaseline', 'arabicForm', 'attributeName',
'attributeType', 'baseFrequency', 'baselineShift', 'baseProfile', 'calcMode', 'capHeight',
'clipPathUnits', 'clipPath', 'clipRule', 'colorInterpolation', 'colorInterpolationFilters',
'colorProfile', 'colorRendering', 'contentScriptType', 'contentStyleType', 'diffuseConstant',
'dominantBaseline', 'edgeMode', 'enableBackground', 'fillOpacity', 'fillRule', 'filterRes',
'filterUnits', 'floodColor', 'floodOpacity', 'fontFamily', 'fontSize', 'fontSizeAdjust',
'fontStretch', 'fontStyle', 'fontVariant', 'fontWeight', 'glyphName',
'glyphOrientationHorizontal', 'glyphOrientationVertical', 'glyphRef', 'gradientTransform',
'gradientUnits', 'horizAdvX', 'horizOriginX', 'imageRendering', 'kernelMatrix',
'kernelUnitLength', 'keyPoints', 'keySplines', 'keyTimes', 'lengthAdjust', 'letterSpacing',
'lightingColor', 'limitingConeAngle', 'markerEnd', 'markerMid', 'markerStart', 'markerHeight',
'markerUnits', 'markerWidth', 'maskContentUnits', 'maskUnits', 'mathematical', 'numOctaves',
'overlinePosition', 'overlineThickness', 'panose1', 'paintOrder', 'pathLength',
'patternContentUnits', 'patternTransform', 'patternUnits', 'pointerEvents', 'pointsAtX',
'pointsAtY', 'pointsAtZ', 'preserveAlpha', 'preserveAspectRatio', 'primitiveUnits',
'referrerPolicy', 'refX', 'refY', 'rendering-intent', 'repeatCount', 'repeatDur',
'requiredExtensions', 'requiredFeatures', 'shapeRendering', 'specularConstant',
'specularExponent', 'spreadMethod', 'startOffset', 'stdDeviation', 'stitchTiles', 'stopColor',
'stopOpacity', 'strikethroughPosition', 'strikethroughThickness', 'strokeDasharray',
'strokeDashoffset', 'strokeLinecap', 'strokeLinejoin', 'strokeMiterlimit', 'strokeOpacity',
'strokeWidth', 'surfaceScale', 'systemLanguage', 'tableValues', 'targetX', 'targetY',
'textAnchor', 'textDecoration', 'textRendering', 'textLength', 'transformOrigin',
'underlinePosition', 'underlineThickness', 'unicodeBidi', 'unicodeRange', 'unitsPerEm',
'vAlphabetic', 'vHanging', 'vIdeographic', 'vMathematical', 'vectorEffect', 'vertAdvY',
'vertOriginX', 'vertOriginY', 'viewBox', 'viewTarget', 'wordSpacing', 'writingMode', 'xHeight',
'xChannelSelector', 'xlinkActuate', 'xlinkArcrole', 'xlinkHref', 'xlinkRole', 'xlinkShow',
'xlinkTitle', 'xlinkType', 'xmlBase', 'xmlLang', 'xmlnsXlink', 'xmlSpace', 'yChannelSelector',
'zoomAndPan',
// Safari/Apple specific, no listing available
'autoCorrect', // https://stackoverflow.com/questions/47985384/html-autocorrect-for-text-input-is-not-working
'autoSave', // https://stackoverflow.com/questions/25456396/what-is-autosave-attribute-supposed-to-do-how-do-i-use-it
// React specific attributes https://reactjs.org/docs/dom-elements.html#differences-in-attributes
'className', 'dangerouslySetInnerHTML', 'defaultValue', 'defaultChecked', 'htmlFor',
// Events' capture events
'onBeforeInput', 'onChange',
'onInvalid', 'onReset', 'onTouchCancel', 'onTouchEnd', 'onTouchMove', 'onTouchStart', 'suppressContentEditableWarning', 'suppressHydrationWarning',
'onAbort', 'onCanPlay', 'onCanPlayThrough', 'onDurationChange', 'onEmptied', 'onEncrypted', 'onEnded',
'onLoadedData', 'onLoadedMetadata', 'onLoadStart', 'onPause', 'onPlay', 'onPlaying', 'onProgress', 'onRateChange', 'onResize',
'onSeeked', 'onSeeking', 'onStalled', 'onSuspend', 'onTimeUpdate', 'onVolumeChange', 'onWaiting',
'onCopyCapture', 'onCutCapture', 'onPasteCapture', 'onCompositionEndCapture', 'onCompositionStartCapture', 'onCompositionUpdateCapture',
'onFocusCapture', 'onBlurCapture', 'onChangeCapture', 'onBeforeInputCapture', 'onInputCapture', 'onResetCapture', 'onSubmitCapture',
'onInvalidCapture', 'onLoadCapture', 'onErrorCapture', 'onKeyDownCapture', 'onKeyPressCapture', 'onKeyUpCapture',
'onAbortCapture', 'onCanPlayCapture', 'onCanPlayThroughCapture', 'onDurationChangeCapture', 'onEmptiedCapture', 'onEncryptedCapture',
'onEndedCapture', 'onLoadedDataCapture', 'onLoadedMetadataCapture', 'onLoadStartCapture', 'onPauseCapture', 'onPlayCapture',
'onPlayingCapture', 'onProgressCapture', 'onRateChangeCapture', 'onSeekedCapture', 'onSeekingCapture', 'onStalledCapture', 'onSuspendCapture',
'onTimeUpdateCapture', 'onVolumeChangeCapture', 'onWaitingCapture', 'onSelectCapture', 'onTouchCancelCapture', 'onTouchEndCapture',
'onTouchMoveCapture', 'onTouchStartCapture', 'onScrollCapture', 'onWheelCapture', 'onAnimationEndCapture', 'onAnimationIteration',
'onAnimationStartCapture', 'onTransitionEndCapture',
'onAuxClick', 'onAuxClickCapture', 'onClickCapture', 'onContextMenuCapture', 'onDoubleClickCapture',
'onDragCapture', 'onDragEndCapture', 'onDragEnterCapture', 'onDragExitCapture', 'onDragLeaveCapture',
'onDragOverCapture', 'onDragStartCapture', 'onDropCapture', 'onMouseDown', 'onMouseDownCapture',
'onMouseMoveCapture', 'onMouseOutCapture', 'onMouseOverCapture', 'onMouseUpCapture',
// Video specific
'autoPictureInPicture', 'controlsList', 'disablePictureInPicture', 'disableRemotePlayback',
];
const DOM_PROPERTIES_IGNORE_CASE = ['charset', 'allowFullScreen', 'webkitAllowFullScreen', 'mozAllowFullScreen', 'webkitDirectory'];
const ARIA_PROPERTIES = [
// See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes
// Global attributes
'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current',
'aria-describedby', 'aria-description', 'aria-details',
'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-flowto', 'aria-grabbed', 'aria-haspopup',
'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live',
'aria-owns', 'aria-relevant', 'aria-roledescription',
// Widget attributes
'aria-autocomplete', 'aria-checked', 'aria-expanded', 'aria-level', 'aria-modal', 'aria-multiline', 'aria-multiselectable',
'aria-orientation', 'aria-placeholder', 'aria-pressed', 'aria-readonly', 'aria-required', 'aria-selected',
'aria-sort', 'aria-valuemax', 'aria-valuemin', 'aria-valuenow', 'aria-valuetext',
// Relationship attributes
'aria-activedescendant', 'aria-colcount', 'aria-colindex', 'aria-colindextext', 'aria-colspan',
'aria-posinset', 'aria-rowcount', 'aria-rowindex', 'aria-rowindextext', 'aria-rowspan', 'aria-setsize',
];
const REACT_ON_PROPS = [
'onGotPointerCapture',
'onGotPointerCaptureCapture',
'onLostPointerCapture',
'onLostPointerCapture',
'onLostPointerCaptureCapture',
'onPointerCancel',
'onPointerCancelCapture',
'onPointerDown',
'onPointerDownCapture',
'onPointerEnter',
'onPointerEnterCapture',
'onPointerLeave',
'onPointerLeaveCapture',
'onPointerMove',
'onPointerMoveCapture',
'onPointerOut',
'onPointerOutCapture',
'onPointerOver',
'onPointerOverCapture',
'onPointerUp',
'onPointerUpCapture',
];
function getDOMPropertyNames(context) {
const ALL_DOM_PROPERTY_NAMES = DOM_PROPERTY_NAMES_TWO_WORDS.concat(DOM_PROPERTY_NAMES_ONE_WORD);
// this was removed in React v16.1+, see https://github.com/facebook/react/pull/10823
if (!testReactVersion(context, '>= 16.1.0')) {
return ALL_DOM_PROPERTY_NAMES.concat('allowTransparency');
}
// these were added in React v16.4.0, see https://reactjs.org/blog/2018/05/23/react-v-16-4.html and https://github.com/facebook/react/pull/12507
if (testReactVersion(context, '>= 16.4.0')) {
return ALL_DOM_PROPERTY_NAMES.concat(REACT_ON_PROPS);
}
return ALL_DOM_PROPERTY_NAMES;
}
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Checks if a node's parent is a JSX tag that is written with lowercase letters,
* and is not a custom web component. Custom web components have a hyphen in tag name,
* or have an `is="some-elem"` attribute.
*
* Note: does not check if a tag's parent against a list of standard HTML/DOM tags. For example,
* a `<fake>`'s child would return `true` because "fake" is written only with lowercase letters
* without a hyphen and does not have a `is="some-elem"` attribute.
*
* @param {Object} childNode - JSX element being tested.
* @returns {boolean} Whether or not the node name match the JSX tag convention.
*/
function isValidHTMLTagInJSX(childNode) {
const tagConvention = /^[a-z][^-]*$/;
if (tagConvention.test(childNode.parent.name.name)) {
return !childNode.parent.attributes.some((attrNode) => (
attrNode.type === 'JSXAttribute'
&& attrNode.name.type === 'JSXIdentifier'
&& attrNode.name.name === 'is'
// To learn more about custom web components and `is` attribute,
// see https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-customized-builtin-example
));
}
return false;
}
/**
* Checks if the attribute name is included in the attributes that are excluded
* from the camel casing.
*
* // returns 'charSet'
* @example normalizeAttributeCase('charset')
*
* Note - these exclusions are not made by React core team, but `eslint-plugin-react` community.
*
* @param {string} name - Attribute name to be normalized
* @returns {string} Result
*/
function normalizeAttributeCase(name) {
return DOM_PROPERTIES_IGNORE_CASE.find((element) => element.toLowerCase() === name.toLowerCase()) || name;
}
/**
* Checks if an attribute name is a valid `data-*` attribute:
* if the name starts with "data-" and has alphanumeric words (browsers require lowercase, but React and TS lowercase them),
* not start with any casing of "xml", and separated by hyphens (-) (which is also called "kebab case" or "dash case"),
* then the attribute is a valid data attribute.
*
* @param {string} name - Attribute name to be tested
* @returns {boolean} Result
*/
function isValidDataAttribute(name) {
return !/^data-xml/i.test(name) && /^data-[^:]*$/.test(name);
}
/**
* Checks if an attribute name has at least one uppercase characters
*
* @param {string} name
* @returns {boolean} Result
*/
function hasUpperCaseCharacter(name) {
return name.toLowerCase() !== name;
}
/**
* Checks if an attribute name is a standard aria attribute by compering it to a list
* of standard aria property names
*
* @param {string} name - Attribute name to be tested
* @returns {boolean} Result
*/
function isValidAriaAttribute(name) {
return ARIA_PROPERTIES.some((element) => element === name);
}
/**
* Extracts the tag name for the JSXAttribute
* @param {JSXAttribute} node - JSXAttribute being tested.
* @returns {string | null} tag name
*/
function getTagName(node) {
if (
node
&& node.parent
&& node.parent.name
) {
return node.parent.name.name;
}
return null;
}
/**
* Test wether the tag name for the JSXAttribute is
* something like <Foo.bar />
* @param {JSXAttribute} node - JSXAttribute being tested.
* @returns {boolean} result
*/
function tagNameHasDot(node) {
return !!(
node.parent
&& node.parent.name
&& node.parent.name.type === 'JSXMemberExpression'
);
}
/**
* Get the standard name of the attribute.
* @param {string} name - Name of the attribute.
* @param {object} context - eslint context
* @returns {string | undefined} The standard name of the attribute, or undefined if no standard name was found.
*/
function getStandardName(name, context) {
if (has(DOM_ATTRIBUTE_NAMES, name)) {
return DOM_ATTRIBUTE_NAMES[/** @type {keyof DOM_ATTRIBUTE_NAMES} */ (name)];
}
if (has(SVGDOM_ATTRIBUTE_NAMES, name)) {
return SVGDOM_ATTRIBUTE_NAMES[/** @type {keyof SVGDOM_ATTRIBUTE_NAMES} */ (name)];
}
const names = getDOMPropertyNames(context);
// Let's find a possible attribute match with a case-insensitive search.
return names.find((element) => element.toLowerCase() === name.toLowerCase());
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
invalidPropOnTag: 'Invalid property \'{{name}}\' found on tag \'{{tagName}}\', but it is only allowed on: {{allowedTags}}',
unknownPropWithStandardName: 'Unknown property \'{{name}}\' found, use \'{{standardName}}\' instead',
unknownProp: 'Unknown property \'{{name}}\' found',
dataLowercaseRequired: 'React does not recognize data-* props with uppercase characters on a DOM element. Found \'{{name}}\', use \'{{lowerCaseName}}\' instead',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow usage of unknown DOM property',
category: 'Possible Errors',
recommended: true,
url: docsUrl('no-unknown-property'),
},
fixable: 'code',
messages,
schema: [{
type: 'object',
properties: {
ignore: {
type: 'array',
items: {
type: 'string',
},
},
requireDataLowercase: {
type: 'boolean',
default: false,
},
},
additionalProperties: false,
}],
},
create(context) {
function getIgnoreConfig() {
return (context.options[0] && context.options[0].ignore) || DEFAULTS.ignore;
}
function getRequireDataLowercase() {
return (context.options[0] && typeof context.options[0].requireDataLowercase !== 'undefined')
? !!context.options[0].requireDataLowercase
: DEFAULTS.requireDataLowercase;
}
return {
JSXAttribute(node) {
const ignoreNames = getIgnoreConfig();
const actualName = getText(context, node.name);
if (ignoreNames.indexOf(actualName) >= 0) {
return;
}
const name = normalizeAttributeCase(actualName);
// Ignore tags like <Foo.bar />
if (tagNameHasDot(node)) {
return;
}
if (isValidDataAttribute(name)) {
if (getRequireDataLowercase() && hasUpperCaseCharacter(name)) {
report(context, messages.dataLowercaseRequired, 'dataLowercaseRequired', {
node,
data: {
name: actualName,
lowerCaseName: actualName.toLowerCase(),
},
});
}
return;
}
if (isValidAriaAttribute(name)) { return; }
const tagName = getTagName(node);
if (tagName === 'fbt' || tagName === 'fbs') { return; } // fbt/fbs nodes are bonkers, let's not go there
if (!isValidHTMLTagInJSX(node)) { return; }
// Let's dive deeper into tags that are HTML/DOM elements (`<button>`), and not React components (`<Button />`)
// Some attributes are allowed on some tags only
const allowedTags = has(ATTRIBUTE_TAGS_MAP, name)
? ATTRIBUTE_TAGS_MAP[/** @type {keyof ATTRIBUTE_TAGS_MAP} */ (name)]
: null;
if (tagName && allowedTags) {
// Scenario 1A: Allowed attribute found where not supposed to, report it
if (allowedTags.indexOf(tagName) === -1) {
report(context, messages.invalidPropOnTag, 'invalidPropOnTag', {
node,
data: {
name: actualName,
tagName,
allowedTags: allowedTags.join(', '),
},
});
}
// Scenario 1B: There are allowed attributes on allowed tags, no need to report it
return;
}
// Let's see if the attribute is a close version to some standard property name
const standardName = getStandardName(name, context);
const hasStandardNameButIsNotUsed = standardName && standardName !== name;
const usesStandardName = standardName && standardName === name;
if (usesStandardName) {
// Scenario 2A: The attribute name is the standard name, no need to report it
return;
}
if (hasStandardNameButIsNotUsed) {
// Scenario 2B: The name of the attribute is close to a standard one, report it with the standard name
report(context, messages.unknownPropWithStandardName, 'unknownPropWithStandardName', {
node,
data: {
name: actualName,
standardName,
},
fix(fixer) {
return fixer.replaceText(node.name, standardName);
},
});
return;
}
// Scenario 3: We have an attribute that is unknown, report it
report(context, messages.unknownProp, 'unknownProp', {
node,
data: {
name: actualName,
},
});
},
};
},
};

151
node_modules/eslint-plugin-react/lib/rules/no-unsafe.js generated vendored Normal file
View File

@@ -0,0 +1,151 @@
/**
* @fileoverview Prevent usage of unsafe lifecycle methods
* @author Sergei Startsev
*/
'use strict';
const astUtil = require('../util/ast');
const componentUtil = require('../util/componentUtil');
const docsUrl = require('../util/docsUrl');
const testReactVersion = require('../util/version').testReactVersion;
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
unsafeMethod: '{{method}} is unsafe for use in async rendering. Update the component to use {{newMethod}} instead. {{details}}',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow usage of unsafe lifecycle methods',
category: 'Best Practices',
recommended: false,
url: docsUrl('no-unsafe'),
},
messages,
schema: [
{
type: 'object',
properties: {
checkAliases: {
default: false,
type: 'boolean',
},
},
additionalProperties: false,
},
],
},
create(context) {
const config = context.options[0] || {};
const checkAliases = config.checkAliases || false;
const isApplicable = testReactVersion(context, '>= 16.3.0');
if (!isApplicable) {
return {};
}
const unsafe = {
UNSAFE_componentWillMount: {
newMethod: 'componentDidMount',
details: 'See https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html.',
},
UNSAFE_componentWillReceiveProps: {
newMethod: 'getDerivedStateFromProps',
details: 'See https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html.',
},
UNSAFE_componentWillUpdate: {
newMethod: 'componentDidUpdate',
details: 'See https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html.',
},
};
if (checkAliases) {
unsafe.componentWillMount = unsafe.UNSAFE_componentWillMount;
unsafe.componentWillReceiveProps = unsafe.UNSAFE_componentWillReceiveProps;
unsafe.componentWillUpdate = unsafe.UNSAFE_componentWillUpdate;
}
/**
* Returns a list of unsafe methods
* @returns {Array} A list of unsafe methods
*/
function getUnsafeMethods() {
return Object.keys(unsafe);
}
/**
* Checks if a passed method is unsafe
* @param {string} method Life cycle method
* @returns {boolean} Returns true for unsafe methods, otherwise returns false
*/
function isUnsafe(method) {
const unsafeMethods = getUnsafeMethods();
return unsafeMethods.indexOf(method) !== -1;
}
/**
* Reports the error for an unsafe method
* @param {ASTNode} node The AST node being checked
* @param {string} method Life cycle method
*/
function checkUnsafe(node, method) {
if (!isUnsafe(method)) {
return;
}
const meta = unsafe[method];
const newMethod = meta.newMethod;
const details = meta.details;
const propertyNode = astUtil.getComponentProperties(node)
.find((property) => astUtil.getPropertyName(property) === method);
report(context, messages.unsafeMethod, 'unsafeMethod', {
node: propertyNode,
data: {
method,
newMethod,
details,
},
});
}
/**
* Returns life cycle methods if available
* @param {ASTNode} node The AST node being checked.
* @returns {Array} The array of methods.
*/
function getLifeCycleMethods(node) {
const properties = astUtil.getComponentProperties(node);
return properties.map((property) => astUtil.getPropertyName(property));
}
/**
* Checks life cycle methods
* @param {ASTNode} node The AST node being checked.
*/
function checkLifeCycleMethods(node) {
if (componentUtil.isES5Component(node, context) || componentUtil.isES6Component(node, context)) {
const methods = getLifeCycleMethods(node);
methods
.sort((a, b) => a.localeCompare(b))
.forEach((method) => checkUnsafe(node, method));
}
}
return {
ClassDeclaration: checkLifeCycleMethods,
ClassExpression: checkLifeCycleMethods,
ObjectExpression: checkLifeCycleMethods,
};
},
};

View File

@@ -0,0 +1,483 @@
/**
* @fileoverview Prevent creating unstable components inside components
* @author Ari Perkkiö
*/
'use strict';
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const astUtil = require('../util/ast');
const isCreateElement = require('../util/isCreateElement');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const COMPONENT_AS_PROPS_INFO = ' If you want to allow component creation in props, set allowAsProps option to true.';
const HOOK_REGEXP = /^use[A-Z0-9].*$/;
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Generate error message with given parent component name
* @param {string} parentName Name of the parent component, if known
* @returns {string} Error message with parent component name
*/
function generateErrorMessageWithParentName(parentName) {
return `Do not define components during render. React will see a new component type on every render and destroy the entire subtrees DOM nodes and state (https://reactjs.org/docs/reconciliation.html#elements-of-different-types). Instead, move this component definition out of the parent component${parentName ? `${parentName}` : ' '}and pass data as props.`;
}
/**
* Check whether given text starts with `render`. Comparison is case-sensitive.
* @param {string} text Text to validate
* @returns {boolean}
*/
function startsWithRender(text) {
return typeof text === 'string' && text.startsWith('render');
}
/**
* Get closest parent matching given matcher
* @param {ASTNode} node The AST node
* @param {Context} context eslint context
* @param {Function} matcher Method used to match the parent
* @returns {ASTNode} The matching parent node, if any
*/
function getClosestMatchingParent(node, context, matcher) {
if (!node || !node.parent || node.parent.type === 'Program') {
return;
}
if (matcher(node.parent, context)) {
return node.parent;
}
return getClosestMatchingParent(node.parent, context, matcher);
}
/**
* Matcher used to check whether given node is a `createElement` call
* @param {ASTNode} node The AST node
* @param {Context} context eslint context
* @returns {boolean} True if node is a `createElement` call, false if not
*/
function isCreateElementMatcher(node, context) {
return (
astUtil.isCallExpression(node)
&& isCreateElement(context, node)
);
}
/**
* Matcher used to check whether given node is a `ObjectExpression`
* @param {ASTNode} node The AST node
* @returns {boolean} True if node is a `ObjectExpression`, false if not
*/
function isObjectExpressionMatcher(node) {
return node && node.type === 'ObjectExpression';
}
/**
* Matcher used to check whether given node is a `JSXExpressionContainer`
* @param {ASTNode} node The AST node
* @returns {boolean} True if node is a `JSXExpressionContainer`, false if not
*/
function isJSXExpressionContainerMatcher(node) {
return node && node.type === 'JSXExpressionContainer';
}
/**
* Matcher used to check whether given node is a `JSXAttribute` of `JSXExpressionContainer`
* @param {ASTNode} node The AST node
* @returns {boolean} True if node is a `JSXAttribute` of `JSXExpressionContainer`, false if not
*/
function isJSXAttributeOfExpressionContainerMatcher(node) {
return (
node
&& node.type === 'JSXAttribute'
&& node.value
&& node.value.type === 'JSXExpressionContainer'
);
}
/**
* Matcher used to check whether given node is an object `Property`
* @param {ASTNode} node The AST node
* @returns {boolean} True if node is a `Property`, false if not
*/
function isPropertyOfObjectExpressionMatcher(node) {
return (
node
&& node.parent
&& node.parent.type === 'Property'
);
}
/**
* Check whether given node or its parent is directly inside `map` call
* ```jsx
* {items.map(item => <li />)}
* ```
* @param {ASTNode} node The AST node
* @returns {boolean} True if node is directly inside `map` call, false if not
*/
function isMapCall(node) {
return (
node
&& node.callee
&& node.callee.property
&& node.callee.property.name === 'map'
);
}
/**
* Check whether given node is `ReturnStatement` of a React hook
* @param {ASTNode} node The AST node
* @param {Context} context eslint context
* @returns {boolean} True if node is a `ReturnStatement` of a React hook, false if not
*/
function isReturnStatementOfHook(node, context) {
if (
!node
|| !node.parent
|| node.parent.type !== 'ReturnStatement'
) {
return false;
}
const callExpression = getClosestMatchingParent(node, context, astUtil.isCallExpression);
return (
callExpression
&& callExpression.callee
&& HOOK_REGEXP.test(callExpression.callee.name)
);
}
/**
* Check whether given node is declared inside a render prop
* ```jsx
* <Component renderFooter={() => <div />} />
* <Component>{() => <div />}</Component>
* ```
* @param {ASTNode} node The AST node
* @param {Context} context eslint context
* @returns {boolean} True if component is declared inside a render prop, false if not
*/
function isComponentInRenderProp(node, context) {
if (
node
&& node.parent
&& node.parent.type === 'Property'
&& node.parent.key
&& startsWithRender(node.parent.key.name)
) {
return true;
}
// Check whether component is a render prop used as direct children, e.g. <Component>{() => <div />}</Component>
if (
node
&& node.parent
&& node.parent.type === 'JSXExpressionContainer'
&& node.parent.parent
&& node.parent.parent.type === 'JSXElement'
) {
return true;
}
const jsxExpressionContainer = getClosestMatchingParent(node, context, isJSXExpressionContainerMatcher);
// Check whether prop name indicates accepted patterns
if (
jsxExpressionContainer
&& jsxExpressionContainer.parent
&& jsxExpressionContainer.parent.type === 'JSXAttribute'
&& jsxExpressionContainer.parent.name
&& jsxExpressionContainer.parent.name.type === 'JSXIdentifier'
) {
const propName = jsxExpressionContainer.parent.name.name;
// Starts with render, e.g. <Component renderFooter={() => <div />} />
if (startsWithRender(propName)) {
return true;
}
// Uses children prop explicitly, e.g. <Component children={() => <div />} />
if (propName === 'children') {
return true;
}
}
return false;
}
/**
* Check whether given node is declared directly inside a render property
* ```jsx
* const rows = { render: () => <div /> }
* <Component rows={ [{ render: () => <div /> }] } />
* ```
* @param {ASTNode} node The AST node
* @returns {boolean} True if component is declared inside a render property, false if not
*/
function isDirectValueOfRenderProperty(node) {
return (
node
&& node.parent
&& node.parent.type === 'Property'
&& node.parent.key
&& node.parent.key.type === 'Identifier'
&& startsWithRender(node.parent.key.name)
);
}
/**
* Resolve the component name of given node
* @param {ASTNode} node The AST node of the component
* @returns {string} Name of the component, if any
*/
function resolveComponentName(node) {
const parentName = node.id && node.id.name;
if (parentName) return parentName;
return (
node.type === 'ArrowFunctionExpression'
&& node.parent
&& node.parent.id
&& node.parent.id.name
);
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow creating unstable components inside components',
category: 'Possible Errors',
recommended: false,
url: docsUrl('no-unstable-nested-components'),
},
schema: [{
type: 'object',
properties: {
customValidators: {
type: 'array',
items: {
type: 'string',
},
},
allowAsProps: {
type: 'boolean',
},
},
additionalProperties: false,
}],
},
create: Components.detect((context, components, utils) => {
const allowAsProps = context.options.some((option) => option && option.allowAsProps);
/**
* Check whether given node is declared inside class component's render block
* ```jsx
* class Component extends React.Component {
* render() {
* class NestedClassComponent extends React.Component {
* ...
* ```
* @param {ASTNode} node The AST node being checked
* @returns {boolean} True if node is inside class component's render block, false if not
*/
function isInsideRenderMethod(node) {
const parentComponent = utils.getParentComponent(node);
if (!parentComponent || parentComponent.type !== 'ClassDeclaration') {
return false;
}
return (
node
&& node.parent
&& node.parent.type === 'MethodDefinition'
&& node.parent.key
&& node.parent.key.name === 'render'
);
}
/**
* Check whether given node is a function component declared inside class component.
* Util's component detection fails to detect function components inside class components.
* ```jsx
* class Component extends React.Component {
* render() {
* const NestedComponent = () => <div />;
* ...
* ```
* @param {ASTNode} node The AST node being checked
* @returns {boolean} True if given node a function component declared inside class component, false if not
*/
function isFunctionComponentInsideClassComponent(node) {
const parentComponent = utils.getParentComponent(node);
const parentStatelessComponent = utils.getParentStatelessComponent(node);
return (
parentComponent
&& parentStatelessComponent
&& parentComponent.type === 'ClassDeclaration'
&& utils.getStatelessComponent(parentStatelessComponent)
&& utils.isReturningJSX(node)
);
}
/**
* Check whether given node is declared inside `createElement` call's props
* ```js
* React.createElement(Component, {
* footer: () => React.createElement("div", null)
* })
* ```
* @param {ASTNode} node The AST node
* @returns {boolean} True if node is declare inside `createElement` call's props, false if not
*/
function isComponentInsideCreateElementsProp(node) {
if (!components.get(node)) {
return false;
}
const createElementParent = getClosestMatchingParent(node, context, isCreateElementMatcher);
return (
createElementParent
&& createElementParent.arguments
&& createElementParent.arguments[1] === getClosestMatchingParent(node, context, isObjectExpressionMatcher)
);
}
/**
* Check whether given node is declared inside a component/object prop.
* ```jsx
* <Component footer={() => <div />} />
* { footer: () => <div /> }
* ```
* @param {ASTNode} node The AST node being checked
* @returns {boolean} True if node is a component declared inside prop, false if not
*/
function isComponentInProp(node) {
if (isPropertyOfObjectExpressionMatcher(node)) {
return utils.isReturningJSX(node);
}
const jsxAttribute = getClosestMatchingParent(node, context, isJSXAttributeOfExpressionContainerMatcher);
if (!jsxAttribute) {
return isComponentInsideCreateElementsProp(node);
}
return utils.isReturningJSX(node);
}
/**
* Check whether given node is a stateless component returning non-JSX
* ```jsx
* {{ a: () => null }}
* ```
* @param {ASTNode} node The AST node being checked
* @returns {boolean} True if node is a stateless component returning non-JSX, false if not
*/
function isStatelessComponentReturningNull(node) {
const component = utils.getStatelessComponent(node);
return component && !utils.isReturningJSX(component);
}
/**
* Check whether given node is a unstable nested component
* @param {ASTNode} node The AST node being checked
*/
function validate(node) {
if (!node || !node.parent) {
return;
}
const isDeclaredInsideProps = isComponentInProp(node);
if (
!components.get(node)
&& !isFunctionComponentInsideClassComponent(node)
&& !isDeclaredInsideProps) {
return;
}
if (
// Support allowAsProps option
(isDeclaredInsideProps && (allowAsProps || isComponentInRenderProp(node, context)))
// Prevent reporting components created inside Array.map calls
|| isMapCall(node)
|| isMapCall(node.parent)
// Do not mark components declared inside hooks (or falsy '() => null' clean-up methods)
|| isReturnStatementOfHook(node, context)
// Do not mark objects containing render methods
|| isDirectValueOfRenderProperty(node)
// Prevent reporting nested class components twice
|| isInsideRenderMethod(node)
// Prevent falsely reporting detected "components" which do not return JSX
|| isStatelessComponentReturningNull(node)
) {
return;
}
// Get the closest parent component
const parentComponent = getClosestMatchingParent(
node,
context,
(nodeToMatch) => components.get(nodeToMatch)
);
if (parentComponent) {
const parentName = resolveComponentName(parentComponent);
// Exclude lowercase parents, e.g. function createTestComponent()
// React-dom prevents creating lowercase components
if (parentName && parentName[0] === parentName[0].toLowerCase()) {
return;
}
let message = generateErrorMessageWithParentName(parentName);
// Add information about allowAsProps option when component is declared inside prop
if (isDeclaredInsideProps && !allowAsProps) {
message += COMPONENT_AS_PROPS_INFO;
}
report(context, message, null, {
node,
});
}
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
FunctionDeclaration(node) { validate(node); },
ArrowFunctionExpression(node) { validate(node); },
FunctionExpression(node) { validate(node); },
ClassDeclaration(node) { validate(node); },
CallExpression(node) { validate(node); },
};
}),
};

View File

@@ -0,0 +1,258 @@
/**
* @fileoverview Prevent declaring unused methods and properties of component class
* @author Paweł Nowak, Berton Zhu
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const componentUtil = require('../util/componentUtil');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const LIFECYCLE_METHODS = new Set([
'constructor',
'componentDidCatch',
'componentDidMount',
'componentDidUpdate',
'componentWillMount',
'componentWillReceiveProps',
'componentWillUnmount',
'componentWillUpdate',
'getChildContext',
'getSnapshotBeforeUpdate',
'render',
'shouldComponentUpdate',
'UNSAFE_componentWillMount',
'UNSAFE_componentWillReceiveProps',
'UNSAFE_componentWillUpdate',
]);
const ES6_LIFECYCLE = new Set([
'state',
]);
const ES5_LIFECYCLE = new Set([
'getInitialState',
'getDefaultProps',
'mixins',
]);
function isKeyLiteralLike(node, property) {
return property.type === 'Literal'
|| (property.type === 'TemplateLiteral' && property.expressions.length === 0)
|| (node.computed === false && property.type === 'Identifier');
}
// Descend through all wrapping TypeCastExpressions and return the expression
// that was cast.
function uncast(node) {
while (node.type === 'TypeCastExpression') {
node = node.expression;
}
return node;
}
// Return the name of an identifier or the string value of a literal. Useful
// anywhere that a literal may be used as a key (e.g., member expressions,
// method definitions, ObjectExpression property keys).
function getName(node) {
node = uncast(node);
const type = node.type;
if (type === 'Identifier') {
return node.name;
}
if (type === 'Literal') {
return String(node.value);
}
if (type === 'TemplateLiteral' && node.expressions.length === 0) {
return node.quasis[0].value.raw;
}
return null;
}
function isThisExpression(node) {
return uncast(node).type === 'ThisExpression';
}
function getInitialClassInfo(node, isClass) {
return {
classNode: node,
isClass,
// Set of nodes where properties were defined.
properties: new Set(),
// Set of names of properties that we've seen used.
usedProperties: new Set(),
inStatic: false,
};
}
const messages = {
unused: 'Unused method or property "{{name}}"',
unusedWithClass: 'Unused method or property "{{name}}" of class "{{className}}"',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow declaring unused methods of component class',
category: 'Best Practices',
recommended: false,
url: docsUrl('no-unused-class-component-methods'),
},
messages,
schema: [],
},
create: ((context) => {
let classInfo = null;
// Takes an ObjectExpression node and adds all named Property nodes to the
// current set of properties.
function addProperty(node) {
classInfo.properties.add(node);
}
// Adds the name of the given node as a used property if the node is an
// Identifier or a Literal. Other node types are ignored.
function addUsedProperty(node) {
const name = getName(node);
if (name) {
classInfo.usedProperties.add(name);
}
}
function reportUnusedProperties() {
// Report all unused properties.
for (const node of classInfo.properties) { // eslint-disable-line no-restricted-syntax
const name = getName(node);
if (
!classInfo.usedProperties.has(name)
&& !LIFECYCLE_METHODS.has(name)
&& (classInfo.isClass ? !ES6_LIFECYCLE.has(name) : !ES5_LIFECYCLE.has(name))
) {
const className = (classInfo.classNode.id && classInfo.classNode.id.name) || '';
const messageID = className ? 'unusedWithClass' : 'unused';
report(
context,
messages[messageID],
messageID,
{
node,
data: {
name,
className,
},
}
);
}
}
}
function exitMethod() {
if (!classInfo || !classInfo.inStatic) {
return;
}
classInfo.inStatic = false;
}
return {
ClassDeclaration(node) {
if (componentUtil.isES6Component(node, context)) {
classInfo = getInitialClassInfo(node, true);
}
},
ObjectExpression(node) {
if (componentUtil.isES5Component(node, context)) {
classInfo = getInitialClassInfo(node, false);
}
},
'ClassDeclaration:exit'() {
if (!classInfo) {
return;
}
reportUnusedProperties();
classInfo = null;
},
'ObjectExpression:exit'(node) {
if (!classInfo || classInfo.classNode !== node) {
return;
}
reportUnusedProperties();
classInfo = null;
},
Property(node) {
if (!classInfo || classInfo.classNode !== node.parent) {
return;
}
if (isKeyLiteralLike(node, node.key)) {
addProperty(node.key);
}
},
'ClassProperty, MethodDefinition, PropertyDefinition'(node) {
if (!classInfo) {
return;
}
if (node.static) {
classInfo.inStatic = true;
return;
}
if (isKeyLiteralLike(node, node.key)) {
addProperty(node.key);
}
},
'ClassProperty:exit': exitMethod,
'MethodDefinition:exit': exitMethod,
'PropertyDefinition:exit': exitMethod,
MemberExpression(node) {
if (!classInfo || classInfo.inStatic) {
return;
}
if (isThisExpression(node.object) && isKeyLiteralLike(node, node.property)) {
if (node.parent.type === 'AssignmentExpression' && node.parent.left === node) {
// detect `this.property = xxx`
addProperty(node.property);
} else {
// detect `this.property()`, `x = this.property`, etc.
addUsedProperty(node.property);
}
}
},
VariableDeclarator(node) {
if (!classInfo || classInfo.inStatic) {
return;
}
// detect `{ foo, bar: baz } = this`
if (node.init && isThisExpression(node.init) && node.id.type === 'ObjectPattern') {
node.id.properties
.filter((prop) => prop.type === 'Property' && isKeyLiteralLike(prop, prop.key))
.forEach((prop) => {
addUsedProperty('key' in prop ? prop.key : undefined);
});
}
},
};
}),
};

View File

@@ -0,0 +1,171 @@
/**
* @fileoverview Prevent definitions of unused prop types
* @author Evgueni Naverniouk
*/
'use strict';
const values = require('object.values');
// As for exceptions for props.children or props.className (and alike) look at
// https://github.com/jsx-eslint/eslint-plugin-react/issues/7
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
/**
* Checks if the component must be validated
* @param {Object} component The component to process
* @returns {boolean} True if the component must be validated, false if not.
*/
function mustBeValidated(component) {
return !!component && !component.ignoreUnusedPropTypesValidation;
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
unusedPropType: '\'{{name}}\' PropType is defined but prop is never used',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow definitions of unused propTypes',
category: 'Best Practices',
recommended: false,
url: docsUrl('no-unused-prop-types'),
},
messages,
schema: [{
type: 'object',
properties: {
ignore: {
type: 'array',
items: {
type: 'string',
},
uniqueItems: true,
},
customValidators: {
type: 'array',
items: {
type: 'string',
},
},
skipShapeProps: {
type: 'boolean',
},
},
additionalProperties: false,
}],
},
create: Components.detect((context, components) => {
const defaults = { skipShapeProps: true, customValidators: [], ignore: [] };
const configuration = Object.assign({}, defaults, context.options[0] || {});
/**
* Checks if the prop is ignored
* @param {string} name Name of the prop to check.
* @returns {boolean} True if the prop is ignored, false if not.
*/
function isIgnored(name) {
return configuration.ignore.indexOf(name) !== -1;
}
/**
* Checks if a prop is used
* @param {ASTNode} node The AST node being checked.
* @param {Object} prop Declared prop object
* @returns {boolean} True if the prop is used, false if not.
*/
function isPropUsed(node, prop) {
const usedPropTypes = node.usedPropTypes || [];
for (let i = 0, l = usedPropTypes.length; i < l; i++) {
const usedProp = usedPropTypes[i];
if (
prop.type === 'shape'
|| prop.type === 'exact'
|| prop.name === '__ANY_KEY__'
|| usedProp.name === prop.name
) {
return true;
}
}
return false;
}
/**
* Used to recursively loop through each declared prop type
* @param {Object} component The component to process
* @param {ASTNode[]|true} props List of props to validate
*/
function reportUnusedPropType(component, props) {
// Skip props that check instances
if (props === true) {
return;
}
Object.keys(props || {}).forEach((key) => {
const prop = props[key];
// Skip props that check instances
if (prop === true) {
return;
}
if ((prop.type === 'shape' || prop.type === 'exact') && configuration.skipShapeProps) {
return;
}
if (prop.node && prop.node.typeAnnotation && prop.node.typeAnnotation.typeAnnotation
&& prop.node.typeAnnotation.typeAnnotation.type === 'TSNeverKeyword') {
return;
}
if (prop.node && !isIgnored(prop.fullName) && !isPropUsed(component, prop)) {
report(context, messages.unusedPropType, 'unusedPropType', {
node: prop.node.key || prop.node,
data: {
name: prop.fullName,
},
});
}
if (prop.children) {
reportUnusedPropType(component, prop.children);
}
});
}
/**
* Reports unused proptypes for a given component
* @param {Object} component The component to process
*/
function reportUnusedPropTypes(component) {
reportUnusedPropType(component, component.declaredPropTypes);
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
'Program:exit'() {
// Report undeclared proptypes for all classes
values(components.list())
.filter((component) => mustBeValidated(component))
.forEach((component) => {
reportUnusedPropTypes(component);
});
},
};
}),
};

View File

@@ -0,0 +1,529 @@
/**
* @fileoverview Attempts to discover all state fields in a React component and
* warn if any of them are never read.
*
* State field definitions are collected from `this.state = {}` assignments in
* the constructor, objects passed to `this.setState()`, and `state = {}` class
* property assignments.
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const astUtil = require('../util/ast');
const componentUtil = require('../util/componentUtil');
const report = require('../util/report');
const getScope = require('../util/eslint').getScope;
// Descend through all wrapping TypeCastExpressions and return the expression
// that was cast.
function uncast(node) {
while (node.type === 'TypeCastExpression') {
node = node.expression;
}
return node;
}
// Return the name of an identifier or the string value of a literal. Useful
// anywhere that a literal may be used as a key (e.g., member expressions,
// method definitions, ObjectExpression property keys).
function getName(node) {
node = uncast(node);
const type = node.type;
if (type === 'Identifier') {
return node.name;
}
if (type === 'Literal') {
return String(node.value);
}
if (type === 'TemplateLiteral' && node.expressions.length === 0) {
return node.quasis[0].value.raw;
}
return null;
}
function isThisExpression(node) {
return astUtil.unwrapTSAsExpression(uncast(node)).type === 'ThisExpression';
}
function getInitialClassInfo() {
return {
// Set of nodes where state fields were defined.
stateFields: new Set(),
// Set of names of state fields that we've seen used.
usedStateFields: new Set(),
// Names of local variables that may be pointing to this.state. To
// track this properly, we would need to keep track of all locals,
// shadowing, assignments, etc. To keep things simple, we only
// maintain one set of aliases per method and accept that it will
// produce some false negatives.
aliases: null,
};
}
function isSetStateCall(node) {
const unwrappedCalleeNode = astUtil.unwrapTSAsExpression(node.callee);
return (
unwrappedCalleeNode.type === 'MemberExpression'
&& isThisExpression(unwrappedCalleeNode.object)
&& getName(unwrappedCalleeNode.property) === 'setState'
);
}
const messages = {
unusedStateField: 'Unused state field: \'{{name}}\'',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow definitions of unused state',
category: 'Best Practices',
recommended: false,
url: docsUrl('no-unused-state'),
},
messages,
schema: [],
},
create(context) {
// Non-null when we are inside a React component ClassDeclaration and we have
// not yet encountered any use of this.state which we have chosen not to
// analyze. If we encounter any such usage (like this.state being spread as
// JSX attributes), then this is again set to null.
let classInfo = null;
function isStateParameterReference(node) {
const classMethods = [
'shouldComponentUpdate',
'componentWillUpdate',
'UNSAFE_componentWillUpdate',
'getSnapshotBeforeUpdate',
'componentDidUpdate',
];
let scope = getScope(context, node);
while (scope) {
const parent = scope.block && scope.block.parent;
if (
parent
&& parent.type === 'MethodDefinition' && (
(parent.static && parent.key.name === 'getDerivedStateFromProps')
|| classMethods.indexOf(parent.key.name) !== -1
)
&& parent.value.type === 'FunctionExpression'
&& parent.value.params[1]
&& parent.value.params[1].name === node.name
) {
return true;
}
scope = scope.upper;
}
return false;
}
// Returns true if the given node is possibly a reference to `this.state` or the state parameter of
// a lifecycle method.
function isStateReference(node) {
node = uncast(node);
const isDirectStateReference = node.type === 'MemberExpression'
&& isThisExpression(node.object)
&& node.property.name === 'state';
const isAliasedStateReference = node.type === 'Identifier'
&& classInfo.aliases
&& classInfo.aliases.has(node.name);
return isDirectStateReference || isAliasedStateReference || isStateParameterReference(node);
}
// Takes an ObjectExpression node and adds all named Property nodes to the
// current set of state fields.
function addStateFields(node) {
node.properties.filter((prop) => (
prop.type === 'Property'
&& (prop.key.type === 'Literal'
|| (prop.key.type === 'TemplateLiteral' && prop.key.expressions.length === 0)
|| (prop.computed === false && prop.key.type === 'Identifier'))
&& getName(prop.key) !== null
)).forEach((prop) => {
classInfo.stateFields.add(prop);
});
}
// Adds the name of the given node as a used state field if the node is an
// Identifier or a Literal. Other node types are ignored.
function addUsedStateField(node) {
if (!classInfo) {
return;
}
const name = getName(node);
if (name) {
classInfo.usedStateFields.add(name);
}
}
// Records used state fields and new aliases for an ObjectPattern which
// destructures `this.state`.
function handleStateDestructuring(node) {
node.properties.forEach((prop) => {
if (prop.type === 'Property') {
addUsedStateField(prop.key);
} else if (
(prop.type === 'ExperimentalRestProperty' || prop.type === 'RestElement')
&& classInfo.aliases
) {
classInfo.aliases.add(getName(prop.argument));
}
});
}
// Used to record used state fields and new aliases for both
// AssignmentExpressions and VariableDeclarators.
function handleAssignment(left, right) {
const unwrappedRight = astUtil.unwrapTSAsExpression(right);
switch (left.type) {
case 'Identifier':
if (isStateReference(unwrappedRight) && classInfo.aliases) {
classInfo.aliases.add(left.name);
}
break;
case 'ObjectPattern':
if (isStateReference(unwrappedRight)) {
handleStateDestructuring(left);
} else if (isThisExpression(unwrappedRight) && classInfo.aliases) {
left.properties.forEach((prop) => {
if (prop.type === 'Property' && getName(prop.key) === 'state') {
const name = getName(prop.value);
if (name) {
classInfo.aliases.add(name);
} else if (prop.value.type === 'ObjectPattern') {
handleStateDestructuring(prop.value);
}
}
});
}
break;
default:
// pass
}
}
function reportUnusedFields() {
// Report all unused state fields.
classInfo.stateFields.forEach((node) => {
const name = getName(node.key);
if (!classInfo.usedStateFields.has(name)) {
report(context, messages.unusedStateField, 'unusedStateField', {
node,
data: {
name,
},
});
}
});
}
function handleES6ComponentEnter(node) {
if (componentUtil.isES6Component(node, context)) {
classInfo = getInitialClassInfo();
}
}
function handleES6ComponentExit() {
if (!classInfo) {
return;
}
reportUnusedFields();
classInfo = null;
}
function isGDSFP(node) {
const name = getName(node.key);
if (
!node.static
|| name !== 'getDerivedStateFromProps'
|| !node.value
|| !node.value.params
|| node.value.params.length < 2 // no `state` argument
) {
return false;
}
return true;
}
return {
ClassDeclaration: handleES6ComponentEnter,
'ClassDeclaration:exit': handleES6ComponentExit,
ClassExpression: handleES6ComponentEnter,
'ClassExpression:exit': handleES6ComponentExit,
ObjectExpression(node) {
if (componentUtil.isES5Component(node, context)) {
classInfo = getInitialClassInfo();
}
},
'ObjectExpression:exit'(node) {
if (!classInfo) {
return;
}
if (componentUtil.isES5Component(node, context)) {
reportUnusedFields();
classInfo = null;
}
},
CallExpression(node) {
if (!classInfo) {
return;
}
const unwrappedNode = astUtil.unwrapTSAsExpression(node);
const unwrappedArgumentNode = astUtil.unwrapTSAsExpression(unwrappedNode.arguments[0]);
// If we're looking at a `this.setState({})` invocation, record all the
// properties as state fields.
if (
isSetStateCall(unwrappedNode)
&& unwrappedNode.arguments.length > 0
&& unwrappedArgumentNode.type === 'ObjectExpression'
) {
addStateFields(unwrappedArgumentNode);
} else if (
isSetStateCall(unwrappedNode)
&& unwrappedNode.arguments.length > 0
&& unwrappedArgumentNode.type === 'ArrowFunctionExpression'
) {
const unwrappedBodyNode = astUtil.unwrapTSAsExpression(unwrappedArgumentNode.body);
if (unwrappedBodyNode.type === 'ObjectExpression') {
addStateFields(unwrappedBodyNode);
}
if (unwrappedArgumentNode.params.length > 0 && classInfo.aliases) {
const firstParam = unwrappedArgumentNode.params[0];
if (firstParam.type === 'ObjectPattern') {
handleStateDestructuring(firstParam);
} else {
classInfo.aliases.add(getName(firstParam));
}
}
}
},
'ClassProperty, PropertyDefinition'(node) {
if (!classInfo) {
return;
}
// If we see state being assigned as a class property using an object
// expression, record all the fields of that object as state fields.
const unwrappedValueNode = astUtil.unwrapTSAsExpression(node.value);
const name = getName(node.key);
if (
name === 'state'
&& !node.static
&& unwrappedValueNode
&& unwrappedValueNode.type === 'ObjectExpression'
) {
addStateFields(unwrappedValueNode);
}
if (
!node.static
&& unwrappedValueNode
&& unwrappedValueNode.type === 'ArrowFunctionExpression'
) {
// Create a new set for this.state aliases local to this method.
classInfo.aliases = new Set();
}
},
'ClassProperty:exit'(node) {
if (
classInfo
&& !node.static
&& node.value
&& node.value.type === 'ArrowFunctionExpression'
) {
// Forget our set of local aliases.
classInfo.aliases = null;
}
},
'PropertyDefinition, ClassProperty'(node) {
if (!isGDSFP(node)) {
return;
}
const childScope = getScope(context, node).childScopes.find((x) => x.block === node.value);
if (!childScope) {
return;
}
const scope = childScope.variableScope.childScopes.find((x) => x.block === node.value);
const stateArg = node.value.params[1]; // probably "state"
if (!scope || !scope.variables) {
return;
}
const argVar = scope.variables.find((x) => x.name === stateArg.name);
if (argVar) {
const stateRefs = argVar.references;
stateRefs.forEach((ref) => {
const identifier = ref.identifier;
if (identifier && identifier.parent && identifier.parent.type === 'MemberExpression') {
addUsedStateField(identifier.parent.property);
}
});
}
},
'PropertyDefinition:exit'(node) {
if (
classInfo
&& !node.static
&& node.value
&& node.value.type === 'ArrowFunctionExpression'
&& !isGDSFP(node)
) {
// Forget our set of local aliases.
classInfo.aliases = null;
}
},
MethodDefinition() {
if (!classInfo) {
return;
}
// Create a new set for this.state aliases local to this method.
classInfo.aliases = new Set();
},
'MethodDefinition:exit'() {
if (!classInfo) {
return;
}
// Forget our set of local aliases.
classInfo.aliases = null;
},
FunctionExpression(node) {
if (!classInfo) {
return;
}
const parent = node.parent;
if (!componentUtil.isES5Component(parent.parent, context)) {
return;
}
if (
'key' in parent
&& 'name' in parent.key
&& parent.key.name === 'getInitialState'
) {
const body = node.body.body;
const lastBodyNode = body[body.length - 1];
if (
lastBodyNode.type === 'ReturnStatement'
&& lastBodyNode.argument.type === 'ObjectExpression'
) {
addStateFields(lastBodyNode.argument);
}
} else {
// Create a new set for this.state aliases local to this method.
classInfo.aliases = new Set();
}
},
AssignmentExpression(node) {
if (!classInfo) {
return;
}
const unwrappedLeft = astUtil.unwrapTSAsExpression(node.left);
const unwrappedRight = astUtil.unwrapTSAsExpression(node.right);
// Check for assignments like `this.state = {}`
if (
unwrappedLeft.type === 'MemberExpression'
&& isThisExpression(unwrappedLeft.object)
&& getName(unwrappedLeft.property) === 'state'
&& unwrappedRight.type === 'ObjectExpression'
) {
// Find the nearest function expression containing this assignment.
/** @type {import("eslint").Rule.Node} */
let fn = node;
while (fn.type !== 'FunctionExpression' && fn.parent) {
fn = fn.parent;
}
// If the nearest containing function is the constructor, then we want
// to record all the assigned properties as state fields.
if (
fn.parent
&& fn.parent.type === 'MethodDefinition'
&& fn.parent.kind === 'constructor'
) {
addStateFields(unwrappedRight);
}
} else {
// Check for assignments like `alias = this.state` and record the alias.
handleAssignment(unwrappedLeft, unwrappedRight);
}
},
VariableDeclarator(node) {
if (!classInfo || !node.init) {
return;
}
handleAssignment(node.id, node.init);
},
'MemberExpression, OptionalMemberExpression'(node) {
if (!classInfo) {
return;
}
if (isStateReference(astUtil.unwrapTSAsExpression(node.object))) {
// If we see this.state[foo] access, give up.
if (node.computed && node.property.type !== 'Literal') {
classInfo = null;
return;
}
// Otherwise, record that we saw this property being accessed.
addUsedStateField(node.property);
// If we see a `this.state` access in a CallExpression, give up.
} else if (isStateReference(node) && astUtil.isCallExpression(node.parent)) {
classInfo = null;
}
},
JSXSpreadAttribute(node) {
if (classInfo && isStateReference(node.argument)) {
classInfo = null;
}
},
'ExperimentalSpreadProperty, SpreadElement'(node) {
if (classInfo && isStateReference(node.argument)) {
classInfo = null;
}
},
};
},
};

View File

@@ -0,0 +1,15 @@
/**
* @fileoverview Prevent usage of setState in componentWillUpdate
* @author Yannick Croissant
*/
'use strict';
const makeNoMethodSetStateRule = require('../util/makeNoMethodSetStateRule');
const testReactVersion = require('../util/version').testReactVersion;
/** @type {import('eslint').Rule.RuleModule} */
module.exports = makeNoMethodSetStateRule(
'componentWillUpdate',
(context) => testReactVersion(context, '>= 16.3.0')
);

View File

@@ -0,0 +1,58 @@
/**
* @fileoverview Enforce ES5 or ES6 class for React Components
* @author Dan Hamilton
*/
'use strict';
const componentUtil = require('../util/componentUtil');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
shouldUseES6Class: 'Component should use es6 class instead of createClass',
shouldUseCreateClass: 'Component should use createClass instead of es6 class',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce ES5 or ES6 class for React Components',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('prefer-es6-class'),
},
messages,
schema: [{
enum: ['always', 'never'],
}],
},
create(context) {
const configuration = context.options[0] || 'always';
return {
ObjectExpression(node) {
if (componentUtil.isES5Component(node, context) && configuration === 'always') {
report(context, messages.shouldUseES6Class, 'shouldUseES6Class', {
node,
});
}
},
ClassDeclaration(node) {
if (componentUtil.isES6Component(node, context) && configuration === 'never') {
report(context, messages.shouldUseCreateClass, 'shouldUseCreateClass', {
node,
});
}
},
};
},
};

View File

@@ -0,0 +1,162 @@
/**
* @fileoverview Prefer exact proptype definitions
*/
'use strict';
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const astUtil = require('../util/ast');
const propsUtil = require('../util/props');
const propWrapperUtil = require('../util/propWrapper');
const variableUtil = require('../util/variable');
const report = require('../util/report');
const getText = require('../util/eslint').getText;
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
const messages = {
propTypes: 'Component propTypes should be exact by using {{exactPropWrappers}}.',
flow: 'Component flow props should be set with exact objects.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Prefer exact proptype definitions',
category: 'Possible Errors',
recommended: false,
url: docsUrl('prefer-exact-props'),
},
messages,
schema: [],
},
create: Components.detect((context, components, utils) => {
const typeAliases = {};
const exactWrappers = propWrapperUtil.getExactPropWrapperFunctions(context);
function getPropTypesErrorMessage() {
const formattedWrappers = propWrapperUtil.formatPropWrapperFunctions(exactWrappers);
const message = exactWrappers.size > 1 ? `one of ${formattedWrappers}` : formattedWrappers;
return { exactPropWrappers: message };
}
function isNonExactObjectTypeAnnotation(node) {
return (
node
&& node.type === 'ObjectTypeAnnotation'
&& node.properties.length > 0
&& !node.exact
);
}
function hasNonExactObjectTypeAnnotation(node) {
const typeAnnotation = node.typeAnnotation;
return (
typeAnnotation
&& typeAnnotation.typeAnnotation
&& isNonExactObjectTypeAnnotation(typeAnnotation.typeAnnotation)
);
}
function hasGenericTypeAnnotation(node) {
const typeAnnotation = node.typeAnnotation;
return (
typeAnnotation
&& typeAnnotation.typeAnnotation
&& typeAnnotation.typeAnnotation.type === 'GenericTypeAnnotation'
);
}
function isNonEmptyObjectExpression(node) {
return (
node
&& node.type === 'ObjectExpression'
&& node.properties.length > 0
);
}
function isNonExactPropWrapperFunction(node) {
return (
astUtil.isCallExpression(node)
&& !propWrapperUtil.isExactPropWrapperFunction(context, getText(context, node.callee))
);
}
function reportPropTypesError(node) {
report(context, messages.propTypes, 'propTypes', {
node,
data: getPropTypesErrorMessage(),
});
}
function reportFlowError(node) {
report(context, messages.flow, 'flow', {
node,
});
}
return {
TypeAlias(node) {
// working around an issue with eslint@3 and babel-eslint not finding the TypeAlias in scope
typeAliases[node.id.name] = node;
},
'ClassProperty, PropertyDefinition'(node) {
if (!propsUtil.isPropTypesDeclaration(node)) {
return;
}
if (hasNonExactObjectTypeAnnotation(node)) {
reportFlowError(node);
} else if (exactWrappers.size > 0 && isNonEmptyObjectExpression(node.value)) {
reportPropTypesError(node);
} else if (exactWrappers.size > 0 && isNonExactPropWrapperFunction(node.value)) {
reportPropTypesError(node);
}
},
Identifier(node) {
if (!utils.getStatelessComponent(node.parent)) {
return;
}
if (hasNonExactObjectTypeAnnotation(node)) {
reportFlowError(node);
} else if (hasGenericTypeAnnotation(node)) {
const identifier = node.typeAnnotation.typeAnnotation.id.name;
const typeAlias = typeAliases[identifier];
const propsDefinition = typeAlias ? typeAlias.right : null;
if (isNonExactObjectTypeAnnotation(propsDefinition)) {
reportFlowError(node);
}
}
},
MemberExpression(node) {
if (!propsUtil.isPropTypesDeclaration(node) || exactWrappers.size === 0) {
return;
}
const right = node.parent.right;
if (isNonEmptyObjectExpression(right)) {
reportPropTypesError(node);
} else if (isNonExactPropWrapperFunction(right)) {
reportPropTypesError(node);
} else if (right.type === 'Identifier') {
const identifier = right.name;
const propsDefinition = variableUtil.findVariableByName(context, node, identifier);
if (isNonEmptyObjectExpression(propsDefinition)) {
reportPropTypesError(node);
} else if (isNonExactPropWrapperFunction(propsDefinition)) {
reportPropTypesError(node);
}
}
},
};
}),
};

View File

@@ -0,0 +1,117 @@
/**
* @fileoverview Require component props to be typed as read-only.
* @author Luke Zapart
*/
'use strict';
const flatMap = require('array.prototype.flatmap');
const values = require('object.values');
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
function isFlowPropertyType(node) {
return node.type === 'ObjectTypeProperty';
}
function isTypescriptPropertyType(node) {
return node.type === 'TSPropertySignature';
}
function isCovariant(node) {
return (node.variance && node.variance.kind === 'plus')
|| (
node.parent
&& node.parent.parent
&& node.parent.parent.parent
&& node.parent.parent.parent.id
&& node.parent.parent.parent.id.name === '$ReadOnly'
);
}
function isReadonly(node) {
return (
node.typeAnnotation
&& node.typeAnnotation.parent
&& node.typeAnnotation.parent.readonly
);
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
readOnlyProp: 'Prop \'{{name}}\' should be read-only.',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce that props are read-only',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('prefer-read-only-props'),
},
fixable: 'code',
messages,
schema: [],
},
create: Components.detect((context, components) => {
function reportReadOnlyProp(prop, propName, fixer) {
report(context, messages.readOnlyProp, 'readOnlyProp', {
node: prop.node,
data: {
name: propName,
},
fix: fixer,
});
}
return {
'Program:exit'() {
flatMap(
values(components.list()),
(component) => component.declaredPropTypes || []
).forEach((declaredPropTypes) => {
Object.keys(declaredPropTypes).forEach((propName) => {
const prop = declaredPropTypes[propName];
if (!prop.node) {
return;
}
if (isFlowPropertyType(prop.node)) {
if (!isCovariant(prop.node)) {
reportReadOnlyProp(prop, propName, (fixer) => {
if (!prop.node.variance) {
// Insert covariance
return fixer.insertTextBefore(prop.node, '+');
}
// Replace contravariance with covariance
return fixer.replaceText(prop.node.variance, '+');
});
}
return;
}
if (isTypescriptPropertyType(prop.node)) {
if (!isReadonly(prop.node)) {
reportReadOnlyProp(prop, propName, (fixer) => (
fixer.insertTextBefore(prop.node, 'readonly ')
));
}
}
});
});
},
};
}),
};

View File

@@ -0,0 +1,397 @@
/**
* @fileoverview Enforce stateless components to be written as a pure function
* @author Yannick Croissant
* @author Alberto Rodríguez
* @copyright 2015 Alberto Rodríguez. All rights reserved.
*/
'use strict';
const values = require('object.values');
const Components = require('../util/Components');
const testReactVersion = require('../util/version').testReactVersion;
const astUtil = require('../util/ast');
const componentUtil = require('../util/componentUtil');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
const eslintUtil = require('../util/eslint');
const getScope = eslintUtil.getScope;
const getText = eslintUtil.getText;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
componentShouldBePure: 'Component should be written as a pure function',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce stateless components to be written as a pure function',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('prefer-stateless-function'),
},
messages,
schema: [{
type: 'object',
properties: {
ignorePureComponents: {
default: false,
type: 'boolean',
},
},
additionalProperties: false,
}],
},
create: Components.detect((context, components, utils) => {
const configuration = context.options[0] || {};
const ignorePureComponents = configuration.ignorePureComponents || false;
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
/**
* Checks whether a given array of statements is a single call of `super`.
* @see eslint no-useless-constructor rule
* @param {ASTNode[]} body - An array of statements to check.
* @returns {boolean} `true` if the body is a single call of `super`.
*/
function isSingleSuperCall(body) {
return (
body.length === 1
&& body[0].type === 'ExpressionStatement'
&& astUtil.isCallExpression(body[0].expression)
&& body[0].expression.callee.type === 'Super'
);
}
/**
* Checks whether a given node is a pattern which doesn't have any side effects.
* Default parameters and Destructuring parameters can have side effects.
* @see eslint no-useless-constructor rule
* @param {ASTNode} node - A pattern node.
* @returns {boolean} `true` if the node doesn't have any side effects.
*/
function isSimple(node) {
return node.type === 'Identifier' || node.type === 'RestElement';
}
/**
* Checks whether a given array of expressions is `...arguments` or not.
* `super(...arguments)` passes all arguments through.
* @see eslint no-useless-constructor rule
* @param {ASTNode[]} superArgs - An array of expressions to check.
* @returns {boolean} `true` if the superArgs is `...arguments`.
*/
function isSpreadArguments(superArgs) {
return (
superArgs.length === 1
&& superArgs[0].type === 'SpreadElement'
&& superArgs[0].argument.type === 'Identifier'
&& superArgs[0].argument.name === 'arguments'
);
}
/**
* Checks whether given 2 nodes are identifiers which have the same name or not.
* @see eslint no-useless-constructor rule
* @param {ASTNode} ctorParam - A node to check.
* @param {ASTNode} superArg - A node to check.
* @returns {boolean} `true` if the nodes are identifiers which have the same
* name.
*/
function isValidIdentifierPair(ctorParam, superArg) {
return (
ctorParam.type === 'Identifier'
&& superArg.type === 'Identifier'
&& ctorParam.name === superArg.name
);
}
/**
* Checks whether given 2 nodes are a rest/spread pair which has the same values.
* @see eslint no-useless-constructor rule
* @param {ASTNode} ctorParam - A node to check.
* @param {ASTNode} superArg - A node to check.
* @returns {boolean} `true` if the nodes are a rest/spread pair which has the
* same values.
*/
function isValidRestSpreadPair(ctorParam, superArg) {
return (
ctorParam.type === 'RestElement'
&& superArg.type === 'SpreadElement'
&& isValidIdentifierPair(ctorParam.argument, superArg.argument)
);
}
/**
* Checks whether given 2 nodes have the same value or not.
* @see eslint no-useless-constructor rule
* @param {ASTNode} ctorParam - A node to check.
* @param {ASTNode} superArg - A node to check.
* @returns {boolean} `true` if the nodes have the same value or not.
*/
function isValidPair(ctorParam, superArg) {
return (
isValidIdentifierPair(ctorParam, superArg)
|| isValidRestSpreadPair(ctorParam, superArg)
);
}
/**
* Checks whether the parameters of a constructor and the arguments of `super()`
* have the same values or not.
* @see eslint no-useless-constructor rule
* @param {ASTNode[]} ctorParams - The parameters of a constructor to check.
* @param {ASTNode} superArgs - The arguments of `super()` to check.
* @returns {boolean} `true` if those have the same values.
*/
function isPassingThrough(ctorParams, superArgs) {
if (ctorParams.length !== superArgs.length) {
return false;
}
for (let i = 0; i < ctorParams.length; ++i) {
if (!isValidPair(ctorParams[i], superArgs[i])) {
return false;
}
}
return true;
}
/**
* Checks whether the constructor body is a redundant super call.
* @see eslint no-useless-constructor rule
* @param {Array} body - constructor body content.
* @param {Array} ctorParams - The params to check against super call.
* @returns {boolean} true if the constructor body is redundant
*/
function isRedundantSuperCall(body, ctorParams) {
return (
isSingleSuperCall(body)
&& ctorParams.every(isSimple)
&& (
isSpreadArguments(body[0].expression.arguments)
|| isPassingThrough(ctorParams, body[0].expression.arguments)
)
);
}
/**
* Check if a given AST node have any other properties the ones available in stateless components
* @param {ASTNode} node The AST node being checked.
* @returns {boolean} True if the node has at least one other property, false if not.
*/
function hasOtherProperties(node) {
const properties = astUtil.getComponentProperties(node);
return properties.some((property) => {
const name = astUtil.getPropertyName(property);
const isDisplayName = name === 'displayName';
const isPropTypes = name === 'propTypes' || ((name === 'props') && property.typeAnnotation);
const contextTypes = name === 'contextTypes';
const defaultProps = name === 'defaultProps';
const isUselessConstructor = property.kind === 'constructor'
&& !!property.value.body
&& isRedundantSuperCall(property.value.body.body, property.value.params);
const isRender = name === 'render';
return !isDisplayName && !isPropTypes && !contextTypes && !defaultProps && !isUselessConstructor && !isRender;
});
}
/**
* Mark component as pure as declared
* @param {ASTNode} node The AST node being checked.
*/
function markSCUAsDeclared(node) {
components.set(node, {
hasSCU: true,
});
}
/**
* Mark childContextTypes as declared
* @param {ASTNode} node The AST node being checked.
*/
function markChildContextTypesAsDeclared(node) {
components.set(node, {
hasChildContextTypes: true,
});
}
/**
* Mark a setState as used
* @param {ASTNode} node The AST node being checked.
*/
function markThisAsUsed(node) {
components.set(node, {
useThis: true,
});
}
/**
* Mark a props or context as used
* @param {ASTNode} node The AST node being checked.
*/
function markPropsOrContextAsUsed(node) {
components.set(node, {
usePropsOrContext: true,
});
}
/**
* Mark a ref as used
* @param {ASTNode} node The AST node being checked.
*/
function markRefAsUsed(node) {
components.set(node, {
useRef: true,
});
}
/**
* Mark return as invalid
* @param {ASTNode} node The AST node being checked.
*/
function markReturnAsInvalid(node) {
components.set(node, {
invalidReturn: true,
});
}
/**
* Mark a ClassDeclaration as having used decorators
* @param {ASTNode} node The AST node being checked.
*/
function markDecoratorsAsUsed(node) {
components.set(node, {
useDecorators: true,
});
}
function visitClass(node) {
if (ignorePureComponents && componentUtil.isPureComponent(node, context)) {
markSCUAsDeclared(node);
}
if (node.decorators && node.decorators.length) {
markDecoratorsAsUsed(node);
}
}
return {
ClassDeclaration: visitClass,
ClassExpression: visitClass,
// Mark `this` destructuring as a usage of `this`
VariableDeclarator(node) {
// Ignore destructuring on other than `this`
if (!node.id || node.id.type !== 'ObjectPattern' || !node.init || node.init.type !== 'ThisExpression') {
return;
}
// Ignore `props` and `context`
const useThis = node.id.properties.some((property) => {
const name = astUtil.getPropertyName(property);
return name !== 'props' && name !== 'context';
});
if (!useThis) {
markPropsOrContextAsUsed(node);
return;
}
markThisAsUsed(node);
},
// Mark `this` usage
MemberExpression(node) {
if (node.object.type !== 'ThisExpression') {
if (node.property && node.property.name === 'childContextTypes') {
const component = utils.getRelatedComponent(node);
if (!component) {
return;
}
markChildContextTypesAsDeclared(component.node);
}
return;
// Ignore calls to `this.props` and `this.context`
}
if (
(node.property.name || node.property.value) === 'props'
|| (node.property.name || node.property.value) === 'context'
) {
markPropsOrContextAsUsed(node);
return;
}
markThisAsUsed(node);
},
// Mark `ref` usage
JSXAttribute(node) {
const name = getText(context, node.name);
if (name !== 'ref') {
return;
}
markRefAsUsed(node);
},
// Mark `render` that do not return some JSX
ReturnStatement(node) {
let blockNode;
let scope = getScope(context, node);
while (scope) {
blockNode = scope.block && scope.block.parent;
if (blockNode && (blockNode.type === 'MethodDefinition' || blockNode.type === 'Property')) {
break;
}
scope = scope.upper;
}
const isRender = blockNode
&& blockNode.key
&& blockNode.key.name === 'render';
const allowNull = testReactVersion(context, '>= 15.0.0'); // Stateless components can return null since React 15
const isReturningJSX = utils.isReturningJSX(node, !allowNull);
const isReturningNull = node.argument && (node.argument.value === null || node.argument.value === false);
if (
!isRender
|| (allowNull && (isReturningJSX || isReturningNull))
|| (!allowNull && isReturningJSX)
) {
return;
}
markReturnAsInvalid(node);
},
'Program:exit'() {
const list = components.list();
values(list)
.filter((component) => (
!hasOtherProperties(component.node)
&& !component.useThis
&& !component.useRef
&& !component.invalidReturn
&& !component.hasChildContextTypes
&& !component.useDecorators
&& !component.hasSCU
&& (
componentUtil.isES5Component(component.node, context)
|| componentUtil.isES6Component(component.node, context)
)
))
.forEach((component) => {
report(context, messages.componentShouldBePure, 'componentShouldBePure', {
node: component.node,
});
});
},
};
}),
};

View File

@@ -0,0 +1,225 @@
/**
* @fileoverview Prevent missing props validation in a React component definition
* @author Yannick Croissant
*/
'use strict';
// As for exceptions for props.children or props.className (and alike) look at
// https://github.com/jsx-eslint/eslint-plugin-react/issues/7
const values = require('object.values');
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
missingPropType: '\'{{name}}\' is missing in props validation',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow missing props validation in a React component definition',
category: 'Best Practices',
recommended: true,
url: docsUrl('prop-types'),
},
messages,
schema: [{
type: 'object',
properties: {
ignore: {
type: 'array',
items: {
type: 'string',
},
},
customValidators: {
type: 'array',
items: {
type: 'string',
},
},
skipUndeclared: {
type: 'boolean',
},
},
additionalProperties: false,
}],
},
create: Components.detect((context, components) => {
const configuration = context.options[0] || {};
const ignored = configuration.ignore || [];
const skipUndeclared = configuration.skipUndeclared || false;
/**
* Checks if the prop is ignored
* @param {string} name Name of the prop to check.
* @returns {boolean} True if the prop is ignored, false if not.
*/
function isIgnored(name) {
return ignored.indexOf(name) !== -1;
}
/**
* Checks if the component must be validated
* @param {Object} component The component to process
* @returns {boolean} True if the component must be validated, false if not.
*/
function mustBeValidated(component) {
const isSkippedByConfig = skipUndeclared && typeof component.declaredPropTypes === 'undefined';
return !!(
component
&& component.usedPropTypes
&& !component.ignorePropsValidation
&& !isSkippedByConfig
);
}
/**
* Internal: Checks if the prop is declared
* @param {Object} declaredPropTypes Description of propTypes declared in the current component
* @param {string[]} keyList Dot separated name of the prop to check.
* @returns {boolean} True if the prop is declared, false if not.
*/
function internalIsDeclaredInComponent(declaredPropTypes, keyList) {
for (let i = 0, j = keyList.length; i < j; i++) {
const key = keyList[i];
const propType = (
declaredPropTypes && (
// Check if this key is declared
(declaredPropTypes[key] // If not, check if this type accepts any key
|| declaredPropTypes.__ANY_KEY__) // eslint-disable-line no-underscore-dangle
)
);
if (!propType) {
// If it's a computed property, we can't make any further analysis, but is valid
return key === '__COMPUTED_PROP__';
}
if (typeof propType === 'object' && !propType.type) {
return true;
}
// Consider every children as declared
if (propType.children === true || propType.containsUnresolvedSpread || propType.containsIndexers) {
return true;
}
if (propType.acceptedProperties) {
return key in propType.acceptedProperties;
}
if (propType.type === 'union') {
// If we fall in this case, we know there is at least one complex type in the union
if (i + 1 >= j) {
// this is the last key, accept everything
return true;
}
// non trivial, check all of them
const unionTypes = propType.children;
const unionPropType = {};
for (let k = 0, z = unionTypes.length; k < z; k++) {
unionPropType[key] = unionTypes[k];
const isValid = internalIsDeclaredInComponent(
unionPropType,
keyList.slice(i)
);
if (isValid) {
return true;
}
}
// every possible union were invalid
return false;
}
declaredPropTypes = propType.children;
}
return true;
}
/**
* Checks if the prop is declared
* @param {ASTNode} node The AST node being checked.
* @param {string[]} names List of names of the prop to check.
* @returns {boolean} True if the prop is declared, false if not.
*/
function isDeclaredInComponent(node, names) {
while (node) {
const component = components.get(node);
const isDeclared = component && component.confidence >= 2
&& internalIsDeclaredInComponent(component.declaredPropTypes || {}, names);
if (isDeclared) {
return true;
}
node = node.parent;
}
return false;
}
/**
* Reports undeclared proptypes for a given component
* @param {Object} component The component to process
*/
function reportUndeclaredPropTypes(component) {
const undeclareds = component.usedPropTypes.filter((propType) => (
propType.node
&& !isIgnored(propType.allNames[0])
&& !isDeclaredInComponent(component.node, propType.allNames)
));
undeclareds.forEach((propType) => {
report(context, messages.missingPropType, 'missingPropType', {
node: propType.node,
data: {
name: propType.allNames.join('.').replace(/\.__COMPUTED_PROP__/g, '[]'),
},
});
});
}
/**
* @param {Object} component The current component to process
* @param {Array} list The all components to process
* @returns {boolean} True if the component is nested False if not.
*/
function checkNestedComponent(component, list) {
const componentIsMemo = component.node.callee && component.node.callee.name === 'memo';
const argumentIsForwardRef = component.node.arguments && component.node.arguments[0].callee && component.node.arguments[0].callee.name === 'forwardRef';
if (componentIsMemo && argumentIsForwardRef) {
const forwardComponent = list.find(
(innerComponent) => (
innerComponent.node.range[0] === component.node.arguments[0].range[0]
&& innerComponent.node.range[0] === component.node.arguments[0].range[0]
));
const isValidated = mustBeValidated(forwardComponent);
const isIgnorePropsValidation = forwardComponent.ignorePropsValidation;
return isIgnorePropsValidation || isValidated;
}
}
return {
'Program:exit'() {
const list = components.list();
// Report undeclared proptypes for all classes
values(list)
.filter((component) => mustBeValidated(component))
.forEach((component) => {
if (checkNestedComponent(component, values(list))) return;
reportUndeclaredPropTypes(component);
});
},
};
}),
};

View File

@@ -0,0 +1,56 @@
/**
* @fileoverview Prevent missing React when using JSX
* @author Glen Mailer
*/
'use strict';
const variableUtil = require('../util/variable');
const pragmaUtil = require('../util/pragma');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
const messages = {
notInScope: '\'{{name}}\' must be in scope when using JSX',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow missing React when using JSX',
category: 'Possible Errors',
recommended: true,
url: docsUrl('react-in-jsx-scope'),
},
messages,
schema: [],
},
create(context) {
const pragma = pragmaUtil.getFromContext(context);
function checkIfReactIsInScope(node) {
if (variableUtil.getVariableFromContext(context, node, pragma)) {
return;
}
report(context, messages.notInScope, 'notInScope', {
node,
data: {
name: pragma,
},
});
}
return {
JSXOpeningElement: checkIfReactIsInScope,
JSXOpeningFragment: checkIfReactIsInScope,
};
},
};

View File

@@ -0,0 +1,209 @@
/**
* @fileOverview Enforce a defaultProps definition for every prop that is not a required prop.
* @author Vitor Balocco
*/
'use strict';
const entries = require('object.entries');
const values = require('object.values');
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const astUtil = require('../util/ast');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
noDefaultWithRequired: 'propType "{{name}}" is required and should not have a defaultProps declaration.',
shouldHaveDefault: 'propType "{{name}}" is not required, but has no corresponding defaultProps declaration.',
noDefaultPropsWithFunction: 'Dont use defaultProps with function components.',
shouldAssignObjectDefault: 'propType "{{name}}" is not required, but has no corresponding default argument value.',
destructureInSignature: 'Must destructure props in the function signature to initialize an optional prop.',
};
function isPropWithNoDefaulVal(prop) {
if (prop.type === 'RestElement' || prop.type === 'ExperimentalRestProperty') {
return false;
}
return prop.value.type !== 'AssignmentPattern';
}
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Enforce a defaultProps definition for every prop that is not a required prop',
category: 'Best Practices',
url: docsUrl('require-default-props'),
},
messages,
schema: [{
type: 'object',
properties: {
forbidDefaultForRequired: {
type: 'boolean',
},
classes: {
enum: ['defaultProps', 'ignore'],
},
functions: {
enum: ['defaultArguments', 'defaultProps', 'ignore'],
},
/**
* @deprecated
*/
ignoreFunctionalComponents: {
type: 'boolean',
},
},
additionalProperties: false,
}],
},
create: Components.detect((context, components) => {
const configuration = context.options[0] || {};
const forbidDefaultForRequired = configuration.forbidDefaultForRequired || false;
const classes = configuration.classes || 'defaultProps';
/**
* @todo
* - Remove ignoreFunctionalComponents
* - Change default to 'defaultArguments'
*/
const functions = configuration.ignoreFunctionalComponents
? 'ignore'
: configuration.functions || 'defaultProps';
/**
* Reports all propTypes passed in that don't have a defaultProps counterpart.
* @param {Object[]} propTypes List of propTypes to check.
* @param {Object} defaultProps Object of defaultProps to check. Keys are the props names.
* @return {void}
*/
function reportPropTypesWithoutDefault(propTypes, defaultProps) {
entries(propTypes).forEach((propType) => {
const propName = propType[0];
const prop = propType[1];
if (!prop.node) {
return;
}
if (prop.isRequired) {
if (forbidDefaultForRequired && defaultProps[propName]) {
report(context, messages.noDefaultWithRequired, 'noDefaultWithRequired', {
node: prop.node,
data: { name: propName },
});
}
return;
}
if (defaultProps[propName]) {
return;
}
report(context, messages.shouldHaveDefault, 'shouldHaveDefault', {
node: prop.node,
data: { name: propName },
});
});
}
/**
* If functions option is 'defaultArguments', reports defaultProps is used and all params that doesn't initialized.
* @param {Object} componentNode Node of component.
* @param {Object[]} declaredPropTypes List of propTypes to check `isRequired`.
* @param {Object} defaultProps Object of defaultProps to check used.
*/
function reportFunctionComponent(componentNode, declaredPropTypes, defaultProps) {
if (defaultProps) {
report(context, messages.noDefaultPropsWithFunction, 'noDefaultPropsWithFunction', {
node: componentNode,
});
}
const props = componentNode.params[0];
const propTypes = declaredPropTypes;
if (!props) {
return;
}
if (props.type === 'Identifier') {
const hasOptionalProp = values(propTypes).some((propType) => !propType.isRequired);
if (hasOptionalProp) {
report(context, messages.destructureInSignature, 'destructureInSignature', {
node: props,
});
}
} else if (props.type === 'ObjectPattern') {
// Filter required props with default value and report error
props.properties.filter((prop) => {
const propName = prop && prop.key && prop.key.name;
const isPropRequired = propTypes[propName] && propTypes[propName].isRequired;
return propTypes[propName] && isPropRequired && !isPropWithNoDefaulVal(prop);
}).forEach((prop) => {
report(context, messages.noDefaultWithRequired, 'noDefaultWithRequired', {
node: prop,
data: { name: prop.key.name },
});
});
// Filter non required props with no default value and report error
props.properties.filter((prop) => {
const propName = prop && prop.key && prop.key.name;
const isPropRequired = propTypes[propName] && propTypes[propName].isRequired;
return propTypes[propName] && !isPropRequired && isPropWithNoDefaulVal(prop);
}).forEach((prop) => {
report(context, messages.shouldAssignObjectDefault, 'shouldAssignObjectDefault', {
node: prop,
data: { name: prop.key.name },
});
});
}
}
// --------------------------------------------------------------------------
// Public API
// --------------------------------------------------------------------------
return {
'Program:exit'() {
const list = components.list();
values(list).filter((component) => {
if (functions === 'ignore' && astUtil.isFunctionLike(component.node)) {
return false;
}
if (classes === 'ignore' && astUtil.isClass(component.node)) {
return false;
}
// If this defaultProps is "unresolved", then we should ignore this component and not report
// any errors for it, to avoid false-positives with e.g. external defaultProps declarations or spread operators.
if (component.defaultProps === 'unresolved') {
return false;
}
return component.declaredPropTypes !== undefined;
}).forEach((component) => {
if (functions === 'defaultArguments' && astUtil.isFunctionLike(component.node)) {
reportFunctionComponent(
component.node,
component.declaredPropTypes,
component.defaultProps
);
} else {
reportPropTypesWithoutDefault(
component.declaredPropTypes,
component.defaultProps || {}
);
}
});
},
};
}),
};

Some files were not shown because too many files have changed in this diff Show More