0.2.0 - Mid migration

This commit is contained in:
Daniel Mason 2022-04-25 14:47:15 +12:00
parent 139e6a915e
commit 7e38fdbd7d
42393 changed files with 5358157 additions and 62 deletions

3374
web/node_modules/eslint-plugin-react/CHANGELOG.md generated vendored Normal file

File diff suppressed because it is too large Load diff

22
web/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.

272
web/node_modules/eslint-plugin-react/README.md generated vendored Normal file
View file

@ -0,0 +1,272 @@
ESLint-plugin-React
===================
[![Maintenance Status][status-image]][status-url] [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][deps-image]][deps-url] [![Code Climate][climate-image]][climate-url] [![Tidelift][tidelift-image]][tidelift-url]
React specific linting rules for ESLint
# Installation
Install [ESLint](https://www.github.com/eslint/eslint) either locally or globally. (Note that locally, per project, is strongly preferred)
```sh
$ npm install eslint --save-dev
```
If you installed `ESLint` globally, you have to install React plugin globally too. Otherwise, install it locally.
```sh
$ npm install eslint-plugin-react --save-dev
```
# Configuration
Use [our preset](#recommended) to get reasonable defaults:
```json
"extends": [
"eslint:recommended",
"plugin:react/recommended"
]
```
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.
// default to latest and warns if missing
// It will default to "detect" in the future
"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"}
],
"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
],
"linkComponents": [
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
"Hyperlink",
{"name": "Link", "linkAttribute": "to"}
]
}
}
```
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",
}
```
# List of supported rules
✔: Enabled in the [`recommended`](#recommended) configuration.\
🔧: Fixable with [`eslint --fix`](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems).
<!-- AUTO-GENERATED-CONTENT:START (BASIC_RULES) -->
| ✔ | 🔧 | Rule | Description |
| :---: | :---: | :--- | :--- |
| | | [react/boolean-prop-naming](docs/rules/boolean-prop-naming.md) | Enforces consistent naming for boolean props |
| | | [react/button-has-type](docs/rules/button-has-type.md) | Forbid "button" element without an explicit "type" attribute |
| | | [react/default-props-match-prop-types](docs/rules/default-props-match-prop-types.md) | Enforce all defaultProps are defined and not "required" in propTypes. |
| | | [react/destructuring-assignment](docs/rules/destructuring-assignment.md) | Enforce consistent usage of destructuring assignment of props, state, and context |
| ✔ | | [react/display-name](docs/rules/display-name.md) | Prevent missing displayName in a React component definition |
| | | [react/forbid-component-props](docs/rules/forbid-component-props.md) | Forbid certain props on components |
| | | [react/forbid-dom-props](docs/rules/forbid-dom-props.md) | Forbid certain props on DOM Nodes |
| | | [react/forbid-elements](docs/rules/forbid-elements.md) | Forbid certain elements |
| | | [react/forbid-foreign-prop-types](docs/rules/forbid-foreign-prop-types.md) | Forbid using another component's propTypes |
| | | [react/forbid-prop-types](docs/rules/forbid-prop-types.md) | Forbid certain propTypes |
| | 🔧 | [react/function-component-definition](docs/rules/function-component-definition.md) | Standardize the way function component get defined |
| | | [react/no-access-state-in-setstate](docs/rules/no-access-state-in-setstate.md) | Reports when this.state is accessed within setState |
| | | [react/no-adjacent-inline-elements](docs/rules/no-adjacent-inline-elements.md) | Prevent adjacent inline elements not separated by whitespace. |
| | | [react/no-array-index-key](docs/rules/no-array-index-key.md) | Prevent usage of Array index in keys |
| ✔ | | [react/no-children-prop](docs/rules/no-children-prop.md) | Prevent passing of children as props. |
| | | [react/no-danger](docs/rules/no-danger.md) | Prevent usage of dangerous JSX props |
| ✔ | | [react/no-danger-with-children](docs/rules/no-danger-with-children.md) | Report when a DOM element is using both children and dangerouslySetInnerHTML |
| ✔ | | [react/no-deprecated](docs/rules/no-deprecated.md) | Prevent usage of deprecated methods |
| | | [react/no-did-mount-set-state](docs/rules/no-did-mount-set-state.md) | Prevent usage of setState in componentDidMount |
| | | [react/no-did-update-set-state](docs/rules/no-did-update-set-state.md) | Prevent usage of setState in componentDidUpdate |
| ✔ | | [react/no-direct-mutation-state](docs/rules/no-direct-mutation-state.md) | Prevent direct mutation of this.state |
| ✔ | | [react/no-find-dom-node](docs/rules/no-find-dom-node.md) | Prevent usage of findDOMNode |
| ✔ | | [react/no-is-mounted](docs/rules/no-is-mounted.md) | Prevent usage of isMounted |
| | | [react/no-multi-comp](docs/rules/no-multi-comp.md) | Prevent multiple component definition per file |
| | | [react/no-redundant-should-component-update](docs/rules/no-redundant-should-component-update.md) | Flag shouldComponentUpdate when extending PureComponent |
| ✔ | | [react/no-render-return-value](docs/rules/no-render-return-value.md) | Prevent usage of the return value of React.render |
| | | [react/no-set-state](docs/rules/no-set-state.md) | Prevent usage of setState |
| ✔ | | [react/no-string-refs](docs/rules/no-string-refs.md) | Prevent string definitions for references and prevent referencing this.refs |
| | | [react/no-this-in-sfc](docs/rules/no-this-in-sfc.md) | Report "this" being used in stateless components |
| | | [react/no-typos](docs/rules/no-typos.md) | Prevent common typos |
| ✔ | | [react/no-unescaped-entities](docs/rules/no-unescaped-entities.md) | Detect unescaped HTML entities, which might represent malformed tags |
| ✔ | 🔧 | [react/no-unknown-property](docs/rules/no-unknown-property.md) | Prevent usage of unknown DOM property |
| | | [react/no-unsafe](docs/rules/no-unsafe.md) | Prevent usage of unsafe lifecycle methods |
| | | [react/no-unstable-nested-components](docs/rules/no-unstable-nested-components.md) | Prevent creating unstable components inside components |
| | | [react/no-unused-prop-types](docs/rules/no-unused-prop-types.md) | Prevent definitions of unused prop types |
| | | [react/no-unused-state](docs/rules/no-unused-state.md) | Prevent definition of unused state fields |
| | | [react/no-will-update-set-state](docs/rules/no-will-update-set-state.md) | Prevent usage of setState in componentWillUpdate |
| | | [react/prefer-es6-class](docs/rules/prefer-es6-class.md) | Enforce ES5 or ES6 class for React Components |
| | 🔧 | [react/prefer-read-only-props](docs/rules/prefer-read-only-props.md) | Require read-only props. |
| | | [react/prefer-stateless-function](docs/rules/prefer-stateless-function.md) | Enforce stateless components to be written as a pure function |
| ✔ | | [react/prop-types](docs/rules/prop-types.md) | Prevent missing props validation in a React component definition |
| ✔ | | [react/react-in-jsx-scope](docs/rules/react-in-jsx-scope.md) | Prevent missing React when using JSX |
| | | [react/require-default-props](docs/rules/require-default-props.md) | Enforce a defaultProps definition for every prop that is not a required prop. |
| | | [react/require-optimization](docs/rules/require-optimization.md) | Enforce React components to have a shouldComponentUpdate method |
| ✔ | | [react/require-render-return](docs/rules/require-render-return.md) | Enforce ES5 or ES6 class for returning value in render function |
| | 🔧 | [react/self-closing-comp](docs/rules/self-closing-comp.md) | Prevent extra closing tags for components without children |
| | | [react/sort-comp](docs/rules/sort-comp.md) | Enforce component methods order |
| | | [react/sort-prop-types](docs/rules/sort-prop-types.md) | Enforce propTypes declarations alphabetical sorting |
| | | [react/state-in-constructor](docs/rules/state-in-constructor.md) | State initialization in an ES6 class component should be in a constructor |
| | | [react/static-property-placement](docs/rules/static-property-placement.md) | Defines where React component static properties should be positioned. |
| | | [react/style-prop-object](docs/rules/style-prop-object.md) | Enforce style prop value is an object |
| | | [react/void-dom-elements-no-children](docs/rules/void-dom-elements-no-children.md) | Prevent passing of children to void DOM elements (e.g. `<br />`). |
<!-- AUTO-GENERATED-CONTENT:END -->
## JSX-specific rules
<!-- AUTO-GENERATED-CONTENT:START (JSX_RULES) -->
| ✔ | 🔧 | Rule | Description |
| :---: | :---: | :--- | :--- |
| | 🔧 | [react/jsx-boolean-value](docs/rules/jsx-boolean-value.md) | Enforce boolean attributes notation in JSX |
| | | [react/jsx-child-element-spacing](docs/rules/jsx-child-element-spacing.md) | Ensures inline tags are not rendered without spaces between them |
| | 🔧 | [react/jsx-closing-bracket-location](docs/rules/jsx-closing-bracket-location.md) | Validate closing bracket location in JSX |
| | 🔧 | [react/jsx-closing-tag-location](docs/rules/jsx-closing-tag-location.md) | Validate closing tag location for multiline JSX |
| | 🔧 | [react/jsx-curly-brace-presence](docs/rules/jsx-curly-brace-presence.md) | Disallow unnecessary JSX expressions when literals alone are sufficient or enfore JSX expressions on literals in JSX children or attributes |
| | 🔧 | [react/jsx-curly-newline](docs/rules/jsx-curly-newline.md) | Enforce consistent line breaks inside jsx curly |
| | 🔧 | [react/jsx-curly-spacing](docs/rules/jsx-curly-spacing.md) | Enforce or disallow spaces inside of curly braces in JSX attributes |
| | 🔧 | [react/jsx-equals-spacing](docs/rules/jsx-equals-spacing.md) | Disallow or enforce spaces around equal signs in JSX attributes |
| | | [react/jsx-filename-extension](docs/rules/jsx-filename-extension.md) | Restrict file extensions that may contain JSX |
| | 🔧 | [react/jsx-first-prop-new-line](docs/rules/jsx-first-prop-new-line.md) | Ensure proper position of the first property in JSX |
| | 🔧 | [react/jsx-fragments](docs/rules/jsx-fragments.md) | Enforce shorthand or standard form for React fragments |
| | | [react/jsx-handler-names](docs/rules/jsx-handler-names.md) | Enforce event handler naming conventions in JSX |
| | 🔧 | [react/jsx-indent](docs/rules/jsx-indent.md) | Validate JSX indentation |
| | 🔧 | [react/jsx-indent-props](docs/rules/jsx-indent-props.md) | Validate props indentation in JSX |
| ✔ | | [react/jsx-key](docs/rules/jsx-key.md) | Report missing `key` props in iterators/collection literals |
| | | [react/jsx-max-depth](docs/rules/jsx-max-depth.md) | Validate JSX maximum depth |
| | 🔧 | [react/jsx-max-props-per-line](docs/rules/jsx-max-props-per-line.md) | Limit maximum of props on a single line in JSX |
| | 🔧 | [react/jsx-newline](docs/rules/jsx-newline.md) | Require or prevent a new line after jsx elements and expressions. |
| | | [react/jsx-no-bind](docs/rules/jsx-no-bind.md) | Prevents usage of Function.prototype.bind and arrow functions in React component props |
| ✔ | | [react/jsx-no-comment-textnodes](docs/rules/jsx-no-comment-textnodes.md) | Comments inside children section of tag should be placed inside braces |
| | | [react/jsx-no-constructed-context-values](docs/rules/jsx-no-constructed-context-values.md) | Prevents JSX context provider values from taking values that will cause needless rerenders. |
| ✔ | | [react/jsx-no-duplicate-props](docs/rules/jsx-no-duplicate-props.md) | Enforce no duplicate props |
| | | [react/jsx-no-literals](docs/rules/jsx-no-literals.md) | Prevent using string literals in React component definition |
| | | [react/jsx-no-script-url](docs/rules/jsx-no-script-url.md) | Forbid `javascript:` URLs |
| ✔ | 🔧 | [react/jsx-no-target-blank](docs/rules/jsx-no-target-blank.md) | Forbid `target="_blank"` attribute without `rel="noreferrer"` |
| ✔ | | [react/jsx-no-undef](docs/rules/jsx-no-undef.md) | Disallow undeclared variables in JSX |
| | 🔧 | [react/jsx-no-useless-fragment](docs/rules/jsx-no-useless-fragment.md) | Disallow unnecessary fragments |
| | 🔧 | [react/jsx-one-expression-per-line](docs/rules/jsx-one-expression-per-line.md) | Limit to one expression per line in JSX |
| | | [react/jsx-pascal-case](docs/rules/jsx-pascal-case.md) | Enforce PascalCase for user-defined JSX components |
| | 🔧 | [react/jsx-props-no-multi-spaces](docs/rules/jsx-props-no-multi-spaces.md) | Disallow multiple spaces between inline JSX props |
| | | [react/jsx-props-no-spreading](docs/rules/jsx-props-no-spreading.md) | Prevent JSX prop spreading |
| | | [react/jsx-sort-default-props](docs/rules/jsx-sort-default-props.md) | Enforce default props alphabetical sorting |
| | 🔧 | [react/jsx-sort-props](docs/rules/jsx-sort-props.md) | Enforce props alphabetical sorting |
| | 🔧 | [react/jsx-space-before-closing](docs/rules/jsx-space-before-closing.md) | Validate spacing before closing bracket in JSX |
| | 🔧 | [react/jsx-tag-spacing](docs/rules/jsx-tag-spacing.md) | Validate whitespace in and around the JSX opening and closing brackets |
| ✔ | | [react/jsx-uses-react](docs/rules/jsx-uses-react.md) | Prevent React to be marked as unused |
| ✔ | | [react/jsx-uses-vars](docs/rules/jsx-uses-vars.md) | Prevent variables used in JSX to be marked as unused |
| | 🔧 | [react/jsx-wrap-multilines](docs/rules/jsx-wrap-multilines.md) | Prevent missing parentheses around multilines JSX |
<!-- AUTO-GENERATED-CONTENT:END -->
## 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/evcohen/eslint-plugin-jsx-a11y)
- React Native: [eslint-plugin-react-native](https://github.com/Intellicode/eslint-plugin-react-native)
# Shareable configurations
## 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](http://eslint.org/docs/user-guide/configuring#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](http://eslint.org/docs/user-guide/configuring#specifying-parser-options).
# License
ESLint-plugin-React is licensed under the [MIT License](http://www.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
[travis-url]: https://travis-ci.org/yannickcr/eslint-plugin-react
[travis-image]: https://img.shields.io/travis/yannickcr/eslint-plugin-react/master.svg
[deps-url]: https://david-dm.org/yannickcr/eslint-plugin-react
[deps-image]: https://img.shields.io/david/dev/yannickcr/eslint-plugin-react.svg
[climate-url]: https://codeclimate.com/github/yannickcr/eslint-plugin-react
[climate-image]: https://img.shields.io/codeclimate/maintainability/yannickcr/eslint-plugin-react.svg
[status-url]: https://github.com/yannickcr/eslint-plugin-react/pulse
[status-image]: https://img.shields.io/github/last-commit/yannickcr/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/github/yannickcr/eslint-plugin-react?style=flat

178
web/node_modules/eslint-plugin-react/index.js generated vendored Normal file
View file

@ -0,0 +1,178 @@
'use strict';
const fromEntries = require('object.fromentries');
const entries = require('object.entries');
/* eslint-disable global-require */
const allRules = {
'boolean-prop-naming': require('./lib/rules/boolean-prop-naming'),
'button-has-type': require('./lib/rules/button-has-type'),
'default-props-match-prop-types': require('./lib/rules/default-props-match-prop-types'),
'destructuring-assignment': require('./lib/rules/destructuring-assignment'),
'display-name': require('./lib/rules/display-name'),
'forbid-component-props': require('./lib/rules/forbid-component-props'),
'forbid-dom-props': require('./lib/rules/forbid-dom-props'),
'forbid-elements': require('./lib/rules/forbid-elements'),
'forbid-foreign-prop-types': require('./lib/rules/forbid-foreign-prop-types'),
'forbid-prop-types': require('./lib/rules/forbid-prop-types'),
'function-component-definition': require('./lib/rules/function-component-definition'),
'jsx-boolean-value': require('./lib/rules/jsx-boolean-value'),
'jsx-child-element-spacing': require('./lib/rules/jsx-child-element-spacing'),
'jsx-closing-bracket-location': require('./lib/rules/jsx-closing-bracket-location'),
'jsx-closing-tag-location': require('./lib/rules/jsx-closing-tag-location'),
'jsx-curly-spacing': require('./lib/rules/jsx-curly-spacing'),
'jsx-curly-newline': require('./lib/rules/jsx-curly-newline'),
'jsx-equals-spacing': require('./lib/rules/jsx-equals-spacing'),
'jsx-filename-extension': require('./lib/rules/jsx-filename-extension'),
'jsx-first-prop-new-line': require('./lib/rules/jsx-first-prop-new-line'),
'jsx-handler-names': require('./lib/rules/jsx-handler-names'),
'jsx-indent': require('./lib/rules/jsx-indent'),
'jsx-indent-props': require('./lib/rules/jsx-indent-props'),
'jsx-key': require('./lib/rules/jsx-key'),
'jsx-max-depth': require('./lib/rules/jsx-max-depth'),
'jsx-max-props-per-line': require('./lib/rules/jsx-max-props-per-line'),
'jsx-newline': require('./lib/rules/jsx-newline'),
'jsx-no-bind': require('./lib/rules/jsx-no-bind'),
'jsx-no-comment-textnodes': require('./lib/rules/jsx-no-comment-textnodes'),
'jsx-no-constructed-context-values': require('./lib/rules/jsx-no-constructed-context-values'),
'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'),
'jsx-no-literals': require('./lib/rules/jsx-no-literals'),
'jsx-no-script-url': require('./lib/rules/jsx-no-script-url'),
'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank'),
'jsx-no-useless-fragment': require('./lib/rules/jsx-no-useless-fragment'),
'jsx-one-expression-per-line': require('./lib/rules/jsx-one-expression-per-line'),
'jsx-no-undef': require('./lib/rules/jsx-no-undef'),
'jsx-curly-brace-presence': require('./lib/rules/jsx-curly-brace-presence'),
'jsx-pascal-case': require('./lib/rules/jsx-pascal-case'),
'jsx-fragments': require('./lib/rules/jsx-fragments'),
'jsx-props-no-multi-spaces': require('./lib/rules/jsx-props-no-multi-spaces'),
'jsx-props-no-spreading': require('./lib/rules/jsx-props-no-spreading'),
'jsx-sort-default-props': require('./lib/rules/jsx-sort-default-props'),
'jsx-sort-props': require('./lib/rules/jsx-sort-props'),
'jsx-space-before-closing': require('./lib/rules/jsx-space-before-closing'),
'jsx-tag-spacing': require('./lib/rules/jsx-tag-spacing'),
'jsx-uses-react': require('./lib/rules/jsx-uses-react'),
'jsx-uses-vars': require('./lib/rules/jsx-uses-vars'),
'jsx-wrap-multilines': require('./lib/rules/jsx-wrap-multilines'),
'no-access-state-in-setstate': require('./lib/rules/no-access-state-in-setstate'),
'no-adjacent-inline-elements': require('./lib/rules/no-adjacent-inline-elements'),
'no-array-index-key': require('./lib/rules/no-array-index-key'),
'no-children-prop': require('./lib/rules/no-children-prop'),
'no-danger': require('./lib/rules/no-danger'),
'no-danger-with-children': require('./lib/rules/no-danger-with-children'),
'no-deprecated': require('./lib/rules/no-deprecated'),
'no-did-mount-set-state': require('./lib/rules/no-did-mount-set-state'),
'no-did-update-set-state': require('./lib/rules/no-did-update-set-state'),
'no-direct-mutation-state': require('./lib/rules/no-direct-mutation-state'),
'no-find-dom-node': require('./lib/rules/no-find-dom-node'),
'no-is-mounted': require('./lib/rules/no-is-mounted'),
'no-multi-comp': require('./lib/rules/no-multi-comp'),
'no-set-state': require('./lib/rules/no-set-state'),
'no-string-refs': require('./lib/rules/no-string-refs'),
'no-redundant-should-component-update': require('./lib/rules/no-redundant-should-component-update'),
'no-render-return-value': require('./lib/rules/no-render-return-value'),
'no-this-in-sfc': require('./lib/rules/no-this-in-sfc'),
'no-typos': require('./lib/rules/no-typos'),
'no-unescaped-entities': require('./lib/rules/no-unescaped-entities'),
'no-unknown-property': require('./lib/rules/no-unknown-property'),
'no-unsafe': require('./lib/rules/no-unsafe'),
'no-unstable-nested-components': require('./lib/rules/no-unstable-nested-components'),
'no-unused-prop-types': require('./lib/rules/no-unused-prop-types'),
'no-unused-state': require('./lib/rules/no-unused-state'),
'no-will-update-set-state': require('./lib/rules/no-will-update-set-state'),
'prefer-es6-class': require('./lib/rules/prefer-es6-class'),
'prefer-read-only-props': require('./lib/rules/prefer-read-only-props'),
'prefer-stateless-function': require('./lib/rules/prefer-stateless-function'),
'prop-types': require('./lib/rules/prop-types'),
'react-in-jsx-scope': require('./lib/rules/react-in-jsx-scope'),
'require-default-props': require('./lib/rules/require-default-props'),
'require-optimization': require('./lib/rules/require-optimization'),
'require-render-return': require('./lib/rules/require-render-return'),
'self-closing-comp': require('./lib/rules/self-closing-comp'),
'sort-comp': require('./lib/rules/sort-comp'),
'sort-prop-types': require('./lib/rules/sort-prop-types'),
'state-in-constructor': require('./lib/rules/state-in-constructor'),
'static-property-placement': require('./lib/rules/static-property-placement'),
'style-prop-object': require('./lib/rules/style-prop-object'),
'void-dom-elements-no-children': require('./lib/rules/void-dom-elements-no-children')
};
/* eslint-enable */
function filterRules(rules, predicate) {
return fromEntries(entries(rules).filter((entry) => predicate(entry[1])));
}
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);
module.exports = {
deprecatedRules,
rules: allRules,
configs: {
recommended: {
plugins: [
'react'
],
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: [
'react'
],
parserOptions: {
ecmaFeatures: {
jsx: true
}
},
rules: activeRulesConfig
},
'jsx-runtime': {
plugins: [
'react'
],
parserOptions: {
ecmaFeatures: {
jsx: true
}
},
rules: {
'react/react-in-jsx-scope': 0,
'react/jsx-uses-react': 0
}
}
}
};

View file

@ -0,0 +1,355 @@
/**
* @fileoverview Enforces consistent naming for boolean props
* @author Ev Haus
*/
'use strict';
const Components = require('../util/Components');
const propsUtil = require('../util/props');
const docsUrl = require('../util/docsUrl');
const propWrapperUtil = require('../util/propWrapper');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
// Predefine message for use in context.report conditional.
// messageId will still be usable in tests.
const PATTERN_MISMATCH_MSG = 'Prop name ({{propName}}) doesn\'t match rule ({{pattern}})';
module.exports = {
meta: {
docs: {
category: 'Stylistic Issues',
description: 'Enforces consistent naming for boolean props',
recommended: false,
url: docsUrl('boolean-prop-naming')
},
messages: {
patternMismatch: PATTERN_MISMATCH_MSG
},
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 context.getSourceCode().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
);
}
/**
* Checks if prop is nested
* @param {Object} prop Property object, single prop type declaration
* @returns {Boolean}
*/
function nestedPropTypes(prop) {
return (
prop.type === 'Property'
&& prop.value.type === 'CallExpression'
);
}
/**
* 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) {
proptypes = 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);
context.report(Object.assign({
node: propNode,
data: {
component: propName,
propName,
pattern: config.rule
}
}, config.message ? {message: config.message} : {messageId: 'patternMismatch'}));
});
}
function checkPropWrapperArguments(node, args) {
if (!node || !Array.isArray(args)) {
return;
}
args.filter((arg) => arg.type === 'ObjectExpression').forEach((object) => validatePropNaming(node, object.properties));
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
ClassProperty(node) {
if (!rule || !propsUtil.isPropTypesDeclaration(node)) {
return;
}
if (
node.value
&& node.value.type === 'CallExpression'
&& propWrapperUtil.isPropWrapperFunction(
context,
context.getSourceCode().getText(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 (
right.type === 'CallExpression'
&& propWrapperUtil.isPropWrapperFunction(
context,
context.getSourceCode().getText(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) {
// Cache all ObjectType annotations, we will check them at the end
if (node.right.type === 'ObjectTypeAnnotation') {
objectTypeAnnotations.set(node.id.name, node.right);
}
},
TSTypeAliasDeclaration(node) {
if (node.typeAnnotation.type === 'TSTypeLiteral') {
objectTypeAnnotations.set(node.id.name, node.typeAnnotation);
}
},
// eslint-disable-next-line object-shorthand
'Program:exit'() {
if (!rule) {
return;
}
const list = components.list();
Object.keys(list).forEach((component) => {
// If this is a functional component that uses a global type, check it
if (
(
list[component].node.type === 'FunctionDeclaration'
|| list[component].node.type === 'ArrowFunctionExpression'
)
&& list[component].node.params
&& list[component].node.params.length
&& list[component].node.params[0].typeAnnotation
) {
const typeNode = list[component].node.params[0].typeAnnotation;
const annotation = typeNode.typeAnnotation;
let propType;
if (annotation.type === 'GenericTypeAnnotation') {
propType = objectTypeAnnotations.get(annotation.id.name);
} else if (annotation.type === 'ObjectTypeAnnotation') {
propType = annotation;
} else if (annotation.type === 'TSTypeReference') {
propType = objectTypeAnnotations.get(annotation.typeName.name);
}
if (propType) {
validatePropNaming(
list[component].node,
propType.properties || propType.members
);
}
}
if (list[component].invalidProps && list[component].invalidProps.length > 0) {
reportInvalidNaming(list[component]);
}
});
// Reset cache
objectTypeAnnotations.clear();
}
};
})
};

View file

@ -0,0 +1,178 @@
/**
* @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 pragmaUtil = require('../util/pragma');
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
function isCreateElement(node, context) {
const pragma = pragmaUtil.getFromContext(context);
return node.callee
&& node.callee.type === 'MemberExpression'
&& node.callee.property.name === 'createElement'
&& node.callee.object
&& node.callee.object.name === pragma
&& node.arguments.length > 0;
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const optionDefaults = {
button: true,
submit: true,
reset: true
};
module.exports = {
meta: {
docs: {
description: 'Forbid "button" element without an explicit "type" attribute',
category: 'Possible Errors',
recommended: false,
url: docsUrl('button-has-type')
},
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'
},
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) {
context.report({
node,
messageId: 'missingType'
});
}
function reportComplex(node) {
context.report({
node,
messageId: 'complexType'
});
}
function checkValue(node, value) {
if (!(value in configuration)) {
context.report({
node,
messageId: 'invalidValue',
data: {
value
}
});
} else if (!configuration[value]) {
context.report({
node,
messageId: 'forbiddenValue',
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.type === 'JSXExpressionContainer') {
checkExpression(node, typeProp.value.expression);
return;
}
const propValue = getLiteralPropValue(typeProp);
checkValue(node, propValue);
},
CallExpression(node) {
if (!isCreateElement(node, context)) {
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) => prop.key && prop.key.name === 'type');
if (!typeProp) {
reportMissing(node);
return;
}
checkExpression(node, typeProp.value);
}
};
}
};

View file

@ -0,0 +1,105 @@
/**
* @fileOverview Enforce all defaultProps are defined in propTypes
* @author Vitor Balocco
* @author Roy Sutton
*/
'use strict';
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce all defaultProps are defined and not "required" in propTypes.',
category: 'Best Practices',
url: docsUrl('default-props-match-prop-types')
},
messages: {
requiredHasDefault: 'defaultProp "{{name}}" defined for isRequired propType.',
defaultHasNoType: 'defaultProp "{{name}}" has no corresponding propTypes declaration.'
},
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) {
context.report({
node: defaultProp.node,
messageId: 'requiredHasDefault',
data: {
name: defaultPropName
}
});
} else {
context.report({
node: defaultProp.node,
messageId: 'defaultHasNoType',
data: {
name: defaultPropName
}
});
}
});
}
// --------------------------------------------------------------------------
// Public API
// --------------------------------------------------------------------------
return {
'Program:exit'() {
const list = components.list();
// If no defaultProps could be found, we don't report anything.
Object.keys(list).filter((component) => list[component].defaultProps).forEach((component) => {
reportInvalidDefaultProps(
list[component].declaredPropTypes,
list[component].defaultProps || {}
);
});
}
};
})
};

View file

@ -0,0 +1,233 @@
/**
* @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 isAssignmentLHS = require('../util/ast').isAssignmentLHS;
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.name;
}
};
}
function evalParams(params) {
return params.map((param) => ({
destructuring: param.type === 'ObjectPattern',
name: param.type === 'Identifier' && param.name
}));
}
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')
},
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'
},
schema: [{
type: 'string',
enum: [
'always',
'never'
]
}, {
type: 'object',
properties: {
ignoreClassFields: {
type: 'boolean'
}
},
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 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(context.getScope(node).block);
if (!SFCComponent) {
return;
}
sfcParams.push(params);
if (params[0] && params[0].destructuring && components.get(node) && configuration === 'never') {
context.report({
node,
messageId: 'noDestructPropsInSFCArg'
});
} else if (params[1] && params[1].destructuring && components.get(node) && configuration === 'never') {
context.report({
node,
messageId: 'noDestructContextInSFCArg'
});
}
}
function handleStatelessComponentExit(node) {
const SFCComponent = components.get(context.getScope(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') {
context.report({
node,
messageId: 'useDestructAssignment',
data: {
type: node.object.name
}
});
}
}
function isInClassProperty(node) {
let curNode = node.parent;
while (curNode) {
if (curNode.type === 'ClassProperty') {
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))
) {
context.report({
node,
messageId: 'useDestructAssignment',
data: {
type: node.object.property.name
}
});
}
}
return {
FunctionDeclaration: handleStatelessComponent,
ArrowFunctionExpression: handleStatelessComponent,
FunctionExpression: handleStatelessComponent,
'FunctionDeclaration:exit': handleStatelessComponentExit,
'ArrowFunctionExpression:exit': handleStatelessComponentExit,
'FunctionExpression:exit': handleStatelessComponentExit,
MemberExpression(node) {
const SFCComponent = components.get(context.getScope(node).block);
const classComponent = utils.getParentComponent(node);
if (SFCComponent) {
handleSFCUsage(node);
}
if (classComponent) {
handleClassUsage(node);
}
},
VariableDeclarator(node) {
const classComponent = utils.getParentComponent(node);
const SFCComponent = components.get(context.getScope(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') {
context.report({
node,
messageId: 'noDestructAssignment',
data: {
type: node.init.name
}
});
}
if (
classComponent && destructuringClass && configuration === 'never'
&& !(ignoreClassFields && node.parent.type === 'ClassProperty')
) {
context.report({
node,
messageId: 'noDestructAssignment',
data: {
type: node.init.property.name
}
});
}
}
};
})
};

View file

@ -0,0 +1,238 @@
/**
* @fileoverview Prevent missing displayName in a React component definition
* @author Yannick Croissant
*/
'use strict';
const Components = require('../util/Components');
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
const propsUtil = require('../util/props');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent missing displayName in a React component definition',
category: 'Best Practices',
recommended: true,
url: docsUrl('display-name')
},
messages: {
noDisplayName: 'Component definition is missing display name'
},
schema: [{
type: 'object',
properties: {
ignoreTranspilerName: {
type: 'boolean'
}
},
additionalProperties: false
}]
},
create: Components.detect((context, components, utils) => {
const config = context.options[0] || {};
const ignoreTranspilerName = config.ignoreTranspilerName || false;
/**
* Mark a prop type as declared
* @param {ASTNode} node The AST node being checked.
*/
function markDisplayNameAsDeclared(node) {
components.set(node, {
hasDisplayName: true
});
}
/**
* Reports missing display name for a given component
* @param {Object} component The component to process
*/
function reportMissingDisplayName(component) {
context.report({
node: component.node,
messageId: 'noDisplayName'
});
}
/**
* 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.method === true)
&& (!node.parent.parent || !utils.isES5Component(node.parent.parent))
);
if (
namedObjectAssignment || namedObjectDeclaration
|| namedClass
|| namedFunctionDeclaration || namedFunctionExpression
) {
return true;
}
return false;
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
ClassProperty(node) {
if (!propsUtil.isDisplayNameDeclaration(node)) {
return;
}
markDisplayNameAsDeclared(node);
},
MemberExpression(node) {
if (!propsUtil.isDisplayNameDeclaration(node.property)) {
return;
}
const component = utils.getRelatedComponent(node);
if (!component) {
return;
}
markDisplayNameAsDeclared(component.node);
},
FunctionExpression(node) {
if (ignoreTranspilerName || !hasTranspilerName(node)) {
return;
}
if (components.get(node)) {
markDisplayNameAsDeclared(node);
}
},
FunctionDeclaration(node) {
if (ignoreTranspilerName || !hasTranspilerName(node)) {
return;
}
if (components.get(node)) {
markDisplayNameAsDeclared(node);
}
},
ArrowFunctionExpression(node) {
if (ignoreTranspilerName || !hasTranspilerName(node)) {
return;
}
if (components.get(node)) {
markDisplayNameAsDeclared(node);
}
},
MethodDefinition(node) {
if (!propsUtil.isDisplayNameDeclaration(node.key)) {
return;
}
markDisplayNameAsDeclared(node);
},
ClassExpression(node) {
if (ignoreTranspilerName || !hasTranspilerName(node)) {
return;
}
markDisplayNameAsDeclared(node);
},
ClassDeclaration(node) {
if (ignoreTranspilerName || !hasTranspilerName(node)) {
return;
}
markDisplayNameAsDeclared(node);
},
ObjectExpression(node) {
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 embeded 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
Object.keys(list).filter((component) => !list[component].hasDisplayName).forEach((component) => {
reportMissingDisplayName(list[component]);
});
}
};
})
};

View file

@ -0,0 +1,111 @@
/**
* @fileoverview Forbid certain props on components
* @author Joe Lencioni
*/
'use strict';
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const DEFAULTS = ['className', 'style'];
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Forbid certain props on components',
category: 'Best Practices',
recommended: false,
url: docsUrl('forbid-component-props')
},
messages: {
propIsForbidden: 'Prop "{{prop}}" is forbidden on Components'
},
schema: [{
type: 'object',
properties: {
forbid: {
type: 'array',
items: {
oneOf: [{
type: 'string'
}, {
type: 'object',
properties: {
propName: {
type: 'string'
},
allowedFor: {
type: 'array',
uniqueItems: true,
items: {
type: 'string'
}
},
message: {
type: 'string'
}
}
}]
}
}
}
}]
},
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 options = {
allowList: typeof value === 'string' ? [] : (value.allowedFor || []),
message: typeof value === 'string' ? null : value.message
};
return [propName, options];
}));
function isForbidden(prop, tagName) {
const options = forbid.get(prop);
const allowList = options ? options.allowList : undefined;
// if the tagName is undefined (`<this.something>`), we assume it's a forbidden element
return typeof allowList !== 'undefined' && (typeof tagName === 'undefined' || allowList.indexOf(tagName) === -1);
}
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 = forbid.get(prop).message;
context.report(Object.assign({
node,
data: {
prop
}
}, customMessage ? {message: customMessage} : {messageId: 'propIsForbidden'}));
}
};
}
};

View file

@ -0,0 +1,100 @@
/**
* @fileoverview Forbid certain props on DOM Nodes
* @author David Vázquez
*/
'use strict';
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const DEFAULTS = [];
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Forbid certain props on DOM Nodes',
category: 'Best Practices',
recommended: false,
url: docsUrl('forbid-dom-props')
},
messages: {
propIsForbidden: 'Prop "{{prop}}" is forbidden on DOM Nodes'
},
schema: [{
type: 'object',
properties: {
forbid: {
type: 'array',
items: {
oneOf: [{
type: 'string'
}, {
type: 'object',
properties: {
propName: {
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;
const options = {
message: typeof value === 'string' ? null : value.message
};
return [propName, options];
}));
function isForbidden(prop) {
return forbid.has(prop);
}
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(prop)) {
return;
}
const customMessage = forbid.get(prop).message;
context.report(Object.assign({
node,
data: {
prop
}
}, customMessage ? {message: customMessage} : {messageId: 'propIsForbidden'}));
}
};
}
};

View file

@ -0,0 +1,114 @@
/**
* @fileoverview Forbid certain elements
* @author Kenneth Chung
*/
'use strict';
const has = require('has');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Forbid certain elements',
category: 'Best Practices',
recommended: false,
url: docsUrl('forbid-elements')
},
messages: {
forbiddenElement: '<{{element}}> is forbidden',
forbiddenElement_message: '<{{element}}> is forbidden, {{message}}'
},
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 || [];
const indexedForbidConfigs = {};
forbidConfiguration.forEach((item) => {
if (typeof item === 'string') {
indexedForbidConfigs[item] = {element: item};
} else {
indexedForbidConfigs[item.element] = item;
}
});
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 reportIfForbidden(element, node) {
if (has(indexedForbidConfigs, element)) {
const message = indexedForbidConfigs[element].message;
context.report({
node,
messageId: message ? 'forbiddenElement_message' : 'forbiddenElement',
data: {
element,
message
}
});
}
}
return {
JSXOpeningElement(node) {
reportIfForbidden(context.getSourceCode().getText(node.name), node.name);
},
CallExpression(node) {
if (!isValidCreateElement(node)) {
return;
}
const argument = node.arguments[0];
const argType = argument.type;
if (argType === 'Identifier' && /^[A-Z_]/.test(argument.name)) {
reportIfForbidden(argument.name, argument);
} else if (argType === 'Literal' && /^[a-z][^.]*$/.test(argument.value)) {
reportIfForbidden(argument.value, argument);
} else if (argType === 'MemberExpression') {
reportIfForbidden(context.getSourceCode().getText(argument), argument);
}
}
};
}
};

View file

@ -0,0 +1,133 @@
/**
* @fileoverview Forbid using another component's propTypes
* @author Ian Christian Myers
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const ast = require('../util/ast');
module.exports = {
meta: {
docs: {
description: 'Forbid using another component\'s propTypes',
category: 'Best Practices',
recommended: false,
url: docsUrl('forbid-foreign-prop-types')
},
messages: {
forbiddenPropType: 'Using propTypes from another component is not safe because they may be removed in production builds'
},
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') {
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)
)) || (
(node.property.type === 'Literal' || node.property.type === 'JSXText')
&& node.property.value === 'propTypes'
&& !ast.isAssignmentLHS(node)
&& !isAllowedAssignment(node)
)
) {
context.report({
node: node.property,
messageId: 'forbiddenPropType'
});
}
},
ObjectPattern(node) {
const propTypesNode = node.properties.find((property) => property.type === 'Property' && property.key.name === 'propTypes');
if (propTypesNode) {
context.report({
node: propTypesNode,
messageId: 'forbiddenPropType'
});
}
}
};
}
};

View file

@ -0,0 +1,223 @@
/**
* @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');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const DEFAULTS = ['any', 'array', 'object'];
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Forbid certain propTypes',
category: 'Best Practices',
recommended: false,
url: docsUrl('forbid-prop-types')
},
messages: {
forbiddenPropType: 'Prop type "{{target}}" is forbidden'
},
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;
function isForbidden(type) {
const forbid = configuration.forbid || DEFAULTS;
return forbid.indexOf(type) >= 0;
}
function reportIfForbidden(type, declaration, target) {
if (isForbidden(type)) {
context.report({
node: declaration,
messageId: 'forbiddenPropType',
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 (value.type === 'CallExpression') {
value.arguments.forEach((arg) => {
reportIfForbidden(arg.name, declaration, target);
});
value = value.callee;
}
if (value.property) {
target = value.property.name;
} else if (value.type === 'Identifier') {
target = value.name;
}
reportIfForbidden(target, declaration, target);
});
}
}
function checkNode(node) {
switch (node && node.type) {
case 'ObjectExpression':
checkProperties(node.properties);
break;
case 'Identifier': {
const propTypesObject = variableUtil.findVariableByName(context, node.name);
if (propTypesObject && propTypesObject.properties) {
checkProperties(propTypesObject.properties);
}
break;
}
case 'CallExpression': {
const innerNode = node.arguments && node.arguments[0];
if (propWrapperUtil.isPropWrapperFunction(context, context.getSource(node.callee)) && innerNode) {
checkNode(innerNode);
}
break;
}
default:
break;
}
}
return {
ClassProperty(node) {
if (
!propsUtil.isPropTypesDeclaration(node)
&& !shouldCheckContextTypes(node)
&& !shouldCheckChildContextTypes(node)
) {
return;
}
checkNode(node.value);
},
MemberExpression(node) {
if (
!propsUtil.isPropTypesDeclaration(node)
&& !shouldCheckContextTypes(node)
&& !shouldCheckChildContextTypes(node)
) {
return;
}
checkNode(node.parent.right);
},
CallExpression(node) {
if (
node.arguments.length > 0
&& (node.callee.name === 'shape' || astUtil.getPropertyName(node.callee) === 'shape')
) {
checkProperties(node.arguments[0].properties);
}
},
MethodDefinition(node) {
if (
!propsUtil.isPropTypesDeclaration(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 (!property.key) {
return;
}
if (
!propsUtil.isPropTypesDeclaration(property)
&& !shouldCheckContextTypes(property)
&& !shouldCheckChildContextTypes(property)
) {
return;
}
if (property.value.type === 'ObjectExpression') {
checkProperties(property.value.properties);
}
});
}
};
}
};

View file

@ -0,0 +1,196 @@
/**
* @fileoverview Standardize the way function component get defined
* @author Stefan Wullems
*/
'use strict';
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// 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': 'var {name}{typeAnnotation} = {typeParams}({params}){returnType} => {body}',
'function-expression': 'var {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) {
if (node.typeParameters) {
return node.typeParameters.params.length === 1 && !node.typeParameters.params[0].constraint;
}
return false;
}
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;
}
module.exports = {
meta: {
docs: {
description: 'Standardize the way function component get defined',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('function-component-definition')
},
fixable: 'code',
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'
},
schema: [{
type: 'object',
properties: {
namedComponents: {
enum: ['function-declaration', 'arrow-function', 'function-expression']
},
unnamedComponents: {
enum: ['arrow-function', 'function-expression']
}
}
}]
},
create: Components.detect((context, components) => {
const configuration = context.options[0] || {};
const namedConfig = configuration.namedComponents || 'function-declaration';
const unnamedConfig = configuration.unnamedComponents || 'function-expression';
function getFixer(node, options) {
const sourceCode = context.getSourceCode();
const source = sourceCode.getText();
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;
return (fixer) => fixer.replaceTextRange(options.range, buildFunction(options.template, {
typeAnnotation,
typeParams: getNodeText(node.typeParameters, source),
params: getParams(node, source),
returnType: getNodeText(node.returnType, source),
body: getBody(node, source),
name: getName(node)
}));
}
function report(node, options) {
context.report({
node,
messageId: options.messageId,
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) && namedConfig !== functionType) {
report(node, {
messageId: namedConfig,
fixerOptions: {
type: namedConfig,
template: NAMED_FUNCTION_TEMPLATES[namedConfig],
range: node.type === 'FunctionDeclaration'
? node.range
: node.parent.parent.range
}
});
}
if (!hasName(node) && unnamedConfig !== functionType) {
report(node, {
messageId: unnamedConfig,
fixerOptions: {
type: unnamedConfig,
template: UNNAMED_FUNCTION_TEMPLATES[unnamedConfig],
range: node.range
}
});
}
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
FunctionDeclaration(node) { validate(node, 'function-declaration'); },
ArrowFunctionExpression(node) { validate(node, 'arrow-function'); },
FunctionExpression(node) { validate(node, 'function-expression'); }
};
})
};

View file

@ -0,0 +1,134 @@
/**
* @fileoverview Enforce boolean attributes notation in JSX
* @author Yannick Croissant
*/
'use strict';
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const exceptionsSchema = {
type: 'array',
items: {type: 'string', minLength: 1},
uniqueItems: true
};
const ALWAYS = 'always';
const NEVER = 'never';
const errorData = new WeakMap();
function getErrorData(exceptions) {
if (!errorData.has(exceptions)) {
const exceptionProps = Array.from(exceptions, (name) => `\`${name}\``).join(', ');
const exceptionsMessage = exceptions.size > 0 ? ` for the following props: ${exceptionProps}` : '';
errorData.set(exceptions, {exceptionsMessage});
}
return errorData.get(exceptions);
}
function isAlways(configuration, exceptions, propName) {
const isException = exceptions.has(propName);
if (configuration === ALWAYS) {
return !isException;
}
return isException;
}
function isNever(configuration, exceptions, propName) {
const isException = exceptions.has(propName);
if (configuration === NEVER) {
return !isException;
}
return isException;
}
module.exports = {
meta: {
docs: {
description: 'Enforce boolean attributes notation in JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-boolean-value')
},
fixable: 'code',
messages: {
omitBoolean: 'Value must be omitted for boolean attributes{{exceptionsMessage}}',
omitBoolean_noMessage: 'Value must be omitted for boolean attributes',
setBoolean: 'Value must be set for boolean attributes{{exceptionsMessage}}',
setBoolean_noMessage: 'Value must be set for boolean attributes'
},
schema: {
anyOf: [{
type: 'array',
items: [{enum: [ALWAYS, NEVER]}],
additionalItems: false
}, {
type: 'array',
items: [{
enum: [ALWAYS]
}, {
type: 'object',
additionalProperties: false,
properties: {
[NEVER]: exceptionsSchema
}
}],
additionalItems: false
}, {
type: 'array',
items: [{
enum: [NEVER]
}, {
type: 'object',
additionalProperties: false,
properties: {
[ALWAYS]: exceptionsSchema
}
}],
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 data = getErrorData(exceptions);
context.report({
node,
messageId: data.exceptionsMessage ? 'setBoolean' : 'setBoolean_noMessage',
data,
fix(fixer) {
return fixer.insertTextAfter(node, '={true}');
}
});
}
if (isNever(configuration, exceptions, propName) && value && value.type === 'JSXExpressionContainer' && value.expression.value === true) {
const data = getErrorData(exceptions);
context.report({
node,
messageId: data.exceptionsMessage ? 'omitBoolean' : 'omitBoolean_noMessage',
data,
fix(fixer) {
return fixer.removeRange([node.name.range[1], value.range[1]]);
}
});
}
}
};
}
};

View file

@ -0,0 +1,122 @@
'use strict';
const docsUrl = require('../util/docsUrl');
// 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'
]);
module.exports = {
meta: {
docs: {
description: 'Ensures inline tags are not rendered without spaces between them',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-child-element-spacing')
},
fixable: null,
messages: {
spacingAfterPrev: 'Ambiguous spacing after previous element {{element}}',
spacingBeforeNext: 'Ambiguous spacing before next element {{element}}'
},
schema: [
{
type: 'object',
properties: {},
default: {},
additionalProperties: false
}
]
},
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)) {
context.report({
node: lastChild,
loc: lastChild.loc.end,
messageId: 'spacingAfterPrev',
data: {
element: elementName(lastChild)
}
});
} else if (nextChild && child.value.match(TEXT_PRECEDING_ELEMENT_PATTERN)) {
context.report({
node: nextChild,
loc: nextChild.loc.start,
messageId: 'spacingBeforeNext',
data: {
element: elementName(nextChild)
}
});
}
}
lastChild = child;
child = nextChild;
});
};
return {
JSXElement: handleJSX,
JSXFragment: handleJSX
};
}
};

View file

@ -0,0 +1,305 @@
/**
* @fileoverview Validate closing bracket location in JSX
* @author Yannick Croissant
*/
'use strict';
const has = require('has');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Validate closing bracket location in JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-closing-bracket-location')
},
fixable: 'code',
messages: {
bracketLocation: 'The closing bracket must be {{location}}{{details}}'
},
schema: [{
oneOf: [
{
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) {
correctColumn = correctColumn || 0;
let indentation;
let spaces = [];
switch (expectedLocation) {
case 'props-aligned':
indentation = /^\s*/.exec(context.getSourceCode().lines[tokens.lastProp.firstLine - 1])[0];
break;
case 'tag-aligned':
case 'line-aligned':
indentation = /^\s*/.exec(context.getSourceCode().lines[tokens.opening.line - 1])[0];
break;
default:
indentation = '';
}
if (indentation.length + 1 < correctColumn) {
// Non-whitespace characters were included in the column offset
spaces = new Array(+correctColumn + 1 - indentation.length);
}
return indentation + spaces.join(' ');
}
/**
* 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 = context.getSourceCode();
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]};
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)' : ')'}`;
}
context.report({
node,
loc: tokens.closing,
messageId: 'bracketLocation',
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,71 @@
/**
* @fileoverview Validate closing tag location in JSX
* @author Ross Solomon
*/
'use strict';
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Validate closing tag location for multiline JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-closing-tag-location')
},
fixable: 'whitespace',
messages: {
onOwnLine: 'Closing tag of a multiline JSX expression must be on its own line.',
matchIndent: 'Expected closing tag to match indentation of opening.'
}
},
create(context) {
function handleClosingElement(node) {
if (!node.parent) {
return;
}
const opening = node.parent.openingElement || node.parent.openingFragment;
if (opening.loc.start.line === node.loc.start.line) {
return;
}
if (opening.loc.start.column === node.loc.start.column) {
return;
}
context.report({
node,
loc: node.loc,
messageId: astUtil.isNodeFirstInLine(context, node)
? 'matchIndent'
: 'onOwnLine',
fix(fixer) {
const indent = Array(opening.loc.start.column + 1).join(' ');
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,395 @@
/**
* @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');
// ------------------------------------------------------------------------------
// 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};
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description:
'Disallow unnecessary JSX expressions when literals alone are sufficient '
+ 'or enfore JSX expressions on literals in JSX children or attributes',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-curly-brace-presence')
},
fixable: 'code',
messages: {
unnecessaryCurly: 'Curly braces are unnecessary here.',
missingCurly: 'Need to wrap this literal in a JSX expression.'
},
schema: [
{
oneOf: [
{
type: 'object',
properties: {
props: {enum: OPTION_VALUES},
children: {enum: OPTION_VALUES}
},
additionalProperties: false
},
{
enum: OPTION_VALUES
}
]
}
]
},
create(context) {
const HTML_ENTITY_REGEX = () => /&[A-Za-z\d#]+;/g;
const ruleOptions = context.options[0];
const userConfig = typeof ruleOptions === 'string'
? {props: ruleOptions, children: ruleOptions}
: Object.assign({}, DEFAULT_CONFIG, ruleOptions);
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, htmlEntitiy) => (
acc.replace(HTML_ENTITY, htmlEntitiy)
), 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');
}
/**
* 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) {
context.report({
node: JSXExpressionNode,
messageId: 'unnecessaryCurly',
fix(fixer) {
const expression = JSXExpressionNode.expression;
const expressionType = expression.type;
const parentType = JSXExpressionNode.parent.type;
let textToReplace;
if (parentType === 'JSXAttribute') {
textToReplace = `"${expressionType === 'TemplateLiteral'
? expression.quasis[0].value.raw
: expression.raw.substring(1, expression.raw.length - 1)
}"`;
} else if (jsxUtil.isJSX(expression)) {
const sourceCode = context.getSourceCode();
textToReplace = sourceCode.getText(expression);
} else {
textToReplace = expressionType === 'TemplateLiteral'
? expression.quasis[0].value.cooked : expression.value;
}
return fixer.replaceText(JSXExpressionNode, textToReplace);
}
});
}
function reportMissingCurly(literalNode) {
context.report({
node: literalNode,
messageId: 'missingCurly',
fix(fixer) {
// 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.substring(1, literalNode.raw.length - 1)
))}"}`
: wrapWithCurlyBraces(literalNode.raw);
return fixer.replaceText(literalNode, expression);
}
});
}
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);
}
// Bail out if there is any character that needs to be escaped in JSX
// because escaping decreases readiblity and the original code may be more
// readible anyway or intentional for other specific reasons
function lintUnnecessaryCurly(JSXExpressionNode) {
const expression = JSXExpressionNode.expression;
const expressionType = expression.type;
// Curly braces containing comments are necessary
if (context.getSourceCode().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)
)
) {
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) && (
jsxUtil.isJSX(JSXExpressionNode.parent)
|| !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(parent, node, config) {
// 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 (
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 {
JSXExpressionContainer: (node) => {
if (shouldCheckForUnnecessaryCurly(node.parent, node, userConfig)) {
lintUnnecessaryCurly(node);
}
},
'Literal, JSXText': (node) => {
if (shouldCheckForMissingCurly(node, userConfig)) {
reportMissingCurly(node);
}
}
};
}
};

View file

@ -0,0 +1,185 @@
/**
* @fileoverview enforce consistent line breaks inside jsx curly
*/
'use strict';
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// 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'
};
}
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'Enforce consistent line breaks inside jsx curly',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-curly-newline')
},
fixable: 'whitespace',
schema: [
{
oneOf: [
{
enum: ['consistent', 'never']
},
{
type: 'object',
properties: {
singleline: {enum: ['consistent', 'require', 'forbid']},
multiline: {enum: ['consistent', 'require', 'forbid']}
},
additionalProperties: false
}
]
}
],
messages: {
expectedBefore: 'Expected newline before \'}\'.',
expectedAfter: 'Expected newline after \'{\'.',
unexpectedBefore: 'Unexpected newline before \'}\'.',
unexpectedAfter: 'Unexpected newline after \'{\'.'
}
},
create(context) {
const sourceCode = context.getSourceCode();
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) {
context.report({
node: leftCurly,
messageId: 'unexpectedAfter',
fix(fixer) {
return sourceCode
.getText()
.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) {
context.report({
node: leftCurly,
messageId: 'expectedAfter',
fix: (fixer) => fixer.insertTextAfter(leftCurly, '\n')
});
}
if (hasRightNewline && !needsNewlines) {
context.report({
node: rightCurly,
messageId: 'unexpectedBefore',
fix(fixer) {
return sourceCode
.getText()
.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) {
context.report({
node: rightCurly,
messageId: 'expectedBefore',
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,433 @@
/**
* @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('has');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const SPACING = {
always: 'always',
never: 'never'
};
const SPACING_VALUES = [SPACING.always, SPACING.never];
module.exports = {
meta: {
docs: {
description: 'Enforce or disallow spaces inside of curly braces in JSX attributes',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-curly-spacing')
},
fixable: 'code',
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}}\''
},
schema: {
definitions: {
basicConfig: {
type: 'object',
properties: {
when: {
enum: SPACING_VALUES
},
allowMultiline: {
type: 'boolean'
},
spacing: {
type: 'object',
properties: {
objectLiterals: {
enum: SPACING_VALUES
}
}
}
}
},
basicConfigOrBoolean: {
oneOf: [{
$ref: '#/definitions/basicConfig'
}, {
type: 'boolean'
}]
}
},
type: 'array',
items: [{
oneOf: [{
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 = context.getSourceCode().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) {
context.report({
node,
loc: token.loc.start,
messageId: 'noNewlineAfter',
data: {
token: token.value
},
fix(fixer) {
const nextToken = context.getSourceCode().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) {
context.report({
node,
loc: token.loc.start,
messageId: 'noNewlineBefore',
data: {
token: token.value
},
fix(fixer) {
const previousToken = context.getSourceCode().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) {
context.report({
node,
loc: token.loc.start,
messageId: 'noSpaceAfter',
data: {
token: token.value
},
fix(fixer) {
const sourceCode = context.getSourceCode();
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) {
context.report({
node,
loc: token.loc.start,
messageId: 'noSpaceBefore',
data: {
token: token.value
},
fix(fixer) {
const sourceCode = context.getSourceCode();
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) {
context.report({
node,
loc: token.loc.start,
messageId: 'spaceNeededAfter',
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) {
context.report({
node,
loc: token.loc.start,
messageId: 'spaceNeededBefore',
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 = context.getSourceCode();
const first = context.getFirstToken(node);
const last = sourceCode.getLastToken(node);
let second = context.getTokenAfter(first, {includeComments: true});
let penultimate = sourceCode.getTokenBefore(last, {includeComments: true});
if (!second) {
second = context.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,115 @@
/**
* @fileoverview Disallow or enforce spaces around equal signs in JSX attributes.
* @author ryym
*/
'use strict';
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Disallow or enforce spaces around equal signs in JSX attributes',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-equals-spacing')
},
fixable: 'code',
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 \'=\''
},
schema: [{
enum: ['always', 'never']
}]
},
create(context) {
const config = context.options[0];
/**
* 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 = context.getSourceCode();
const equalToken = sourceCode.getTokenAfter(attrNode.name);
const spacedBefore = sourceCode.isSpaceBetweenTokens(attrNode.name, equalToken);
const spacedAfter = sourceCode.isSpaceBetweenTokens(equalToken, attrNode.value);
switch (config) {
default:
case 'never':
if (spacedBefore) {
context.report({
node: attrNode,
loc: equalToken.loc.start,
messageId: 'noSpaceBefore',
fix(fixer) {
return fixer.removeRange([attrNode.name.range[1], equalToken.range[0]]);
}
});
}
if (spacedAfter) {
context.report({
node: attrNode,
loc: equalToken.loc.start,
messageId: 'noSpaceAfter',
fix(fixer) {
return fixer.removeRange([equalToken.range[1], attrNode.value.range[0]]);
}
});
}
break;
case 'always':
if (!spacedBefore) {
context.report({
node: attrNode,
loc: equalToken.loc.start,
messageId: 'needSpaceBefore',
fix(fixer) {
return fixer.insertTextBefore(equalToken, ' ');
}
});
}
if (!spacedAfter) {
context.report({
node: attrNode,
loc: equalToken.loc.start,
messageId: 'needSpaceAfter',
fix(fixer) {
return fixer.insertTextAfter(equalToken, ' ');
}
});
}
break;
}
});
}
};
}
};

View file

@ -0,0 +1,109 @@
/**
* @fileoverview Restrict file extensions that may contain JSX
* @author Joe Lencioni
*/
'use strict';
const path = require('path');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const DEFAULTS = {
allow: 'always',
extensions: ['.jsx']
};
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Restrict file extensions that may contain JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-filename-extension')
},
messages: {
noJSXWithExtension: 'JSX not allowed in files with extension \'{{ext}}\'',
extensionOnlyForJSX: 'Only files containing JSX may use the extension \'{{ext}}\''
},
schema: [{
type: 'object',
properties: {
allow: {
enum: ['always', 'as-needed']
},
extensions: {
type: 'array',
items: {
type: 'string'
}
}
},
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 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) {
context.report({
node: jsxNode,
messageId: 'noJSXWithExtension',
data: {
ext: path.extname(filename)
}
});
}
return;
}
if (isAllowedExtension && allow === 'as-needed') {
context.report({
node,
messageId: 'extensionOnlyForJSX',
data: {
ext: path.extname(filename)
}
});
}
}
};
}
};

View file

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

View file

@ -0,0 +1,204 @@
/**
* @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 versionUtil = require('../util/version');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function replaceNode(source, node, text) {
return `${source.slice(0, node.range[0])}${text}${source.slice(node.range[1])}`;
}
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: {
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}}'
},
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 (!versionUtil.testReactVersion(context, '16.2.0')) {
context.report({
node,
messageId: 'fragmentsNotSupported'
});
return true;
}
return false;
}
function getFixerToLong(jsxFragment) {
const sourceCode = context.getSourceCode();
return function fix(fixer) {
let source = sourceCode.getText();
source = replaceNode(source, jsxFragment.closingFragment, closeFragLong);
source = replaceNode(source, jsxFragment.openingFragment, openFragLong);
const lengthDiff = openFragLong.length - sourceCode.getText(jsxFragment.openingFragment).length
+ closeFragLong.length - sourceCode.getText(jsxFragment.closingFragment).length;
const range = jsxFragment.range;
return fixer.replaceTextRange(range, source.slice(range[0], range[1] + lengthDiff));
};
}
function getFixerToShort(jsxElement) {
const sourceCode = context.getSourceCode();
return function fix(fixer) {
let source = sourceCode.getText();
let lengthDiff;
if (jsxElement.closingElement) {
source = replaceNode(source, jsxElement.closingElement, closeFragShort);
source = replaceNode(source, jsxElement.openingElement, openFragShort);
lengthDiff = sourceCode.getText(jsxElement.openingElement).length - openFragShort.length
+ sourceCode.getText(jsxElement.closingElement).length - closeFragShort.length;
} else {
source = replaceNode(source, jsxElement.openingElement, `${openFragShort}${closeFragShort}`);
lengthDiff = sourceCode.getText(jsxElement.openingElement).length - openFragShort.length
- closeFragShort.length;
}
const range = jsxElement.range;
return fixer.replaceTextRange(range, source.slice(range[0], range[1] - lengthDiff));
};
}
function refersToReactFragment(name) {
const variableInit = variableUtil.findVariableByName(context, 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') {
context.report({
node,
messageId: 'preferPragma',
data: {
react: reactPragma,
fragment: fragmentPragma
},
fix: getFixerToLong(node)
});
}
},
ImportDeclaration(node) {
if (node.source && node.source.value === 'react') {
node.specifiers.forEach((spec) => {
if (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(elName)) {
if (reportOnReactVersion(node)) {
return;
}
const attrs = openingEl.attributes;
if (configuration === 'syntax' && !(attrs && attrs.length > 0)) {
context.report({
node,
messageId: 'preferFragment',
data: {
react: reactPragma,
fragment: fragmentPragma
},
fix: getFixerToShort(node)
});
}
}
});
}
};
}
};

View file

@ -0,0 +1,170 @@
/**
* @fileoverview Enforce event handler naming conventions in JSX
* @author Jake Marsh
*/
'use strict';
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce event handler naming conventions in JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-handler-names')
},
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}}\''
},
schema: [{
anyOf: [
{
type: 'object',
properties: {
eventHandlerPrefix: {type: 'string'},
eventHandlerPropPrefix: {type: 'string'},
checkLocalVariables: {type: 'boolean'},
checkInlineFunction: {type: 'boolean'}
},
additionalProperties: false
}, {
type: 'object',
properties: {
eventHandlerPrefix: {type: 'string'},
eventHandlerPropPrefix: {
type: 'boolean',
enum: [false]
},
checkLocalVariables: {type: 'boolean'},
checkInlineFunction: {type: 'boolean'}
},
additionalProperties: false
}, {
type: 'object',
properties: {
eventHandlerPrefix: {
type: 'boolean',
enum: [false]
},
eventHandlerPropPrefix: {type: 'string'},
checkLocalVariables: {type: 'boolean'},
checkInlineFunction: {type: 'boolean'}
},
additionalProperties: false
}, {
type: 'object',
properties: {
checkLocalVariables: {type: 'boolean'}
},
additionalProperties: false
}, {
type: 'object',
properties: {
checkInlineFunction: {type: 'boolean'}
},
additionalProperties: false
}
]
}]
},
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;
return {
JSXAttribute(node) {
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
)
)
) {
return;
}
const propKey = typeof node.name === 'object' ? node.name.name : node.name;
const expression = node.value.expression;
const propValue = context.getSourceCode()
.getText(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
) {
context.report({
node,
messageId: 'badHandlerName',
data: {
propKey,
handlerPrefix: eventHandlerPrefix
}
});
} else if (
propFnIsNamedCorrectly
&& propIsEventHandler !== null
&& !propIsEventHandler
) {
context.report({
node,
messageId: 'badPropKey',
data: {
propValue,
handlerPropPrefix: eventHandlerPropPrefix
}
});
}
}
};
}
};

View file

@ -0,0 +1,204 @@
/**
* @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 astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Validate props indentation in JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-indent-props')
},
fixable: 'code',
messages: {
wrongIndent: 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.'
},
schema: [{
oneOf: [{
enum: ['tab', 'first']
}, {
type: 'integer'
}, {
type: 'object',
properties: {
indentMode: {
oneOf: [{
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
};
context.report({
node,
messageId: 'wrongIndent',
data: msgContext,
fix(fixer) {
return fixer.replaceTextRange([node.range[0] - node.loc.start.column, node.range[0]],
Array(needed + 1).join(indentType === 'space' ? ' ' : '\t'));
}
});
}
/**
* Get node indent
* @param {ASTNode} node Node to examine
* @return {Number} Indent
*/
function getNodeIndent(node) {
let src = context.getSourceCode().getText(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 = /^([ ]|[\t])*[<]/.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) {
nodes.forEach((node) => {
const nodeIndent = getNodeIndent(node);
if (line.isUsingOperator && !line.currentOperator && indentSize !== 'first' && !ignoreTernaryOperator) {
indent += indentSize;
line.isUsingOperator = false;
}
if (
node.type !== 'ArrayExpression' && node.type !== 'ObjectExpression'
&& nodeIndent !== indent && astUtil.isNodeFirstInLine(context, node)
) {
report(node, indent, 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,394 @@
/**
* @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 astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Validate JSX indentation',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-indent')
},
fixable: 'whitespace',
messages: {
wrongIndent: 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.'
},
schema: [{
oneOf: [{
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) {
return function fix(fixer) {
const indent = Array(needed + 1).join(indentChar);
if (node.type === 'JSXText' || node.type === 'Literal') {
const regExp = /\n[\t ]*(\S)/g;
const fixedText = node.raw.replace(regExp, (match, p1) => `\n${indent}${p1}`);
return fixer.replaceText(node, fixedText);
}
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
};
context.report(Object.assign({
node,
messageId: 'wrongIndent',
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) {
byLastLine = byLastLine || false;
excludeCommas = excludeCommas || false;
let src = context.getSourceCode().getText(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
&& context.getSourceCode().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 = context.getSourceCode();
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 = context.getSourceCode().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
};
}
};

View file

@ -0,0 +1,164 @@
/**
* @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 docsUrl = require('../util/docsUrl');
const pragmaUtil = require('../util/pragma');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const defaultOptions = {
checkFragmentShorthand: false,
checkKeyMustBeforeSpread: false
};
module.exports = {
meta: {
docs: {
description: 'Report missing `key` props in iterators/collection literals',
category: 'Possible Errors',
recommended: true,
url: docsUrl('jsx-key')
},
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`'
},
schema: [{
type: 'object',
properties: {
checkFragmentShorthand: {
type: 'boolean',
default: defaultOptions.checkFragmentShorthand
},
checkKeyMustBeforeSpread: {
type: 'boolean',
default: defaultOptions.checkKeyMustBeforeSpread
}
},
additionalProperties: false
}]
},
create(context) {
const options = Object.assign({}, defaultOptions, context.options[0]);
const checkFragmentShorthand = options.checkFragmentShorthand;
const checkKeyMustBeforeSpread = options.checkKeyMustBeforeSpread;
const reactPragma = pragmaUtil.getFromContext(context);
const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
function checkIteratorElement(node) {
if (node.type === 'JSXElement' && !hasProp(node.openingElement.attributes, 'key')) {
context.report({
node,
messageId: 'missingIterKey'
});
} else if (checkFragmentShorthand && node.type === 'JSXFragment') {
context.report({
node,
messageId: 'missingIterKeyUsePrag',
data: {
reactPrag: reactPragma,
fragPrag: fragmentPragma
}
});
}
}
function getReturnStatement(body) {
return body.filter((item) => item.type === 'ReturnStatement')[0];
}
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';
});
}
return {
JSXElement(node) {
if (hasProp(node.openingElement.attributes, 'key')) {
if (checkKeyMustBeforeSpread && isKeyAfterSpread(node.openingElement.attributes)) {
context.report({
node,
messageId: 'keyBeforeSpread'
});
}
return;
}
if (node.parent.type === 'ArrayExpression') {
context.report({
node,
messageId: 'missingArrayKey'
});
}
},
JSXFragment(node) {
if (!checkFragmentShorthand) {
return;
}
if (node.parent.type === 'ArrayExpression') {
context.report({
node,
messageId: 'missingArrayKeyUsePrag',
data: {
reactPrag: reactPragma,
fragPrag: fragmentPragma
}
});
}
},
// Array.prototype.map
'CallExpression, OptionalCallExpression'(node) {
if (node.callee && node.callee.type !== 'MemberExpression' && node.callee.type !== 'OptionalMemberExpression') {
return;
}
if (node.callee && node.callee.property && node.callee.property.name !== 'map') {
return;
}
const fn = node.arguments[0];
const isFn = fn && fn.type === 'FunctionExpression';
const isArrFn = fn && fn.type === 'ArrowFunctionExpression';
if (isArrFn && (fn.body.type === 'JSXElement' || fn.body.type === 'JSXFragment')) {
checkIteratorElement(fn.body);
}
if (isFn || isArrFn) {
if (fn.body.type === 'BlockStatement') {
const returnStatement = getReturnStatement(fn.body.body);
if (returnStatement && returnStatement.argument) {
checkIteratorElement(returnStatement.argument);
}
}
}
}
};
}
};

View file

@ -0,0 +1,165 @@
/**
* @fileoverview Validate JSX maximum depth
* @author Chris<wfsr@foxmail.com>
*/
'use strict';
const has = require('has');
const includes = require('array-includes');
const variableUtil = require('../util/variable');
const jsxUtil = require('../util/jsx');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Validate JSX maximum depth',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-max-depth')
},
messages: {
wrongDepth: 'Expected the depth of nested jsx elements to be <= {{needed}}, but found {{found}}.'
},
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++;
}
}
return count;
}
function report(node, depth) {
context.report({
node,
messageId: 'wrongDepth',
data: {
found: depth,
needed: maxDepth
}
});
}
function findJSXElementOrFragment(variables, name, previousReferences) {
function find(refs, prevRefs) {
let i = refs.length;
while (--i >= 0) {
if (has(refs[i], 'writeExpr')) {
const writeExpr = refs[i].writeExpr;
return (jsxUtil.isJSX(writeExpr)
&& writeExpr)
|| ((writeExpr && writeExpr.type === 'Identifier')
&& findJSXElementOrFragment(variables, writeExpr.name, prevRefs));
}
}
return null;
}
const variable = variableUtil.getVariable(variables, 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++;
(children || []).forEach((node) => {
if (!hasJSX(node)) {
return;
}
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 variables = variableUtil.variablesInScope(context);
const element = findJSXElementOrFragment(variables, node.expression.name, []);
if (element) {
const baseDepth = getDepth(node);
checkDescendant(baseDepth, element.children);
}
}
};
}
};

View file

@ -0,0 +1,113 @@
/**
* @fileoverview Limit maximum of props on a single line in JSX
* @author Yannick Croissant
*/
'use strict';
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Limit maximum of props on a single line in JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-max-props-per-line')
},
fixable: 'code',
messages: {
newLine: 'Prop `{{prop}}` must be placed on a new line'
},
schema: [{
type: 'object',
properties: {
maximum: {
type: 'integer',
minimum: 1
},
when: {
type: 'string',
enum: ['always', 'multiline']
}
}
}]
},
create(context) {
const configuration = context.options[0] || {};
const maximum = configuration.maximum || 1;
const when = configuration.when || 'always';
function getPropName(propNode) {
if (propNode.type === 'JSXSpreadAttribute') {
return context.getSourceCode().getText(propNode.argument);
}
return propNode.name.name;
}
function generateFixFunction(line, max) {
const sourceCode = context.getSourceCode();
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 sourceCode.getText(curr);
}
return `${prev} ${sourceCode.getText(curr)}`;
}, ''));
}
const code = output.join('\n');
return function fix(fixer) {
return fixer.replaceTextRange([front, back], code);
};
}
return {
JSXOpeningElement(node) {
if (!node.attributes.length) {
return;
}
if (when === 'multiline' && node.loc.start.line === node.loc.end.line) {
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) => {
if (propsInLine.length > maximum) {
const name = getPropName(propsInLine[maximum]);
context.report({
node: propsInLine[maximum],
messageId: 'newLine',
data: {
prop: name
},
fix: generateFixFunction(propsInLine, maximum)
});
}
});
}
};
}
};

View file

@ -0,0 +1,99 @@
/**
* @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');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
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: {
require: 'JSX element should start in a new line',
prevent: 'JSX element should not start in a new line'
},
schema: [
{
type: 'object',
properties: {
prevent: {
default: false,
type: 'boolean'
}
},
additionalProperties: false
}
]
},
create(context) {
const jsxElementParents = new Set();
const sourceCode = context.getSourceCode();
return {
'Program:exit'() {
jsxElementParents.forEach((parent) => {
parent.children.forEach((element, index, elements) => {
if (element.type === 'JSXElement' || element.type === 'JSXExpressionContainer') {
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);
const prevent = !!(context.options[0] || {}).prevent;
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';
context.report({
node: secondAdjacentSibling,
messageId,
fix(fixer) {
return fixer.replaceText(
firstAdjacentSibling,
// double or remove the last newline
sourceCode.getText(firstAdjacentSibling)
.replace(regex, replacement)
);
}
});
}
});
});
},
':matches(JSXElement, JSXFragment) > :matches(JSXElement, JSXExpressionContainer)': (node) => {
jsxElementParents.add(node.parent);
}
};
}
};

View file

@ -0,0 +1,189 @@
/**
* @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 Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevents usage of Function.prototype.bind and arrow functions in React component props',
category: 'Best Practices',
recommended: false,
url: docsUrl('jsx-no-bind')
},
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'
},
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: Components.detect((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 = {};
function setBlockVariableNameSet(blockStart) {
blockVariableNameSets[blockStart] = {
arrowFunc: new Set(),
bindCall: new Set(),
bindExpression: new Set(),
func: new Set()
};
}
function getNodeViolationType(node) {
const nodeType = node.type;
if (
!configuration.allowBind
&& nodeType === 'CallExpression'
&& node.callee.type === 'MemberExpression'
&& node.callee.property.type === 'Identifier'
&& node.callee.property.name === 'bind'
) {
return 'bindCall';
}
if (nodeType === 'ConditionalExpression') {
return getNodeViolationType(node.test)
|| getNodeViolationType(node.consequent)
|| getNodeViolationType(node.alternate);
}
if (!configuration.allowArrowFunctions && nodeType === 'ArrowFunctionExpression') {
return 'arrowFunc';
}
if (!configuration.allowFunctions && nodeType === 'FunctionExpression') {
return 'func';
}
if (!configuration.allowBind && nodeType === 'BindExpression') {
return 'bindExpression';
}
return null;
}
function addVariableNameToSet(violationType, variableName, blockStart) {
blockVariableNameSets[blockStart][violationType].add(variableName);
}
function getBlockStatementAncestors(node) {
return context.getAncestors(node).reverse().filter(
(ancestor) => ancestor.type === 'BlockStatement'
);
}
function reportVariableViolation(node, name, blockStart) {
const blockSets = blockVariableNameSets[blockStart];
const violationTypes = Object.keys(blockSets);
return violationTypes.find((type) => {
if (blockSets[type].has(name)) {
context.report({
node,
messageId: type
});
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]);
},
VariableDeclarator(node) {
if (!node.init) {
return;
}
const blockAncestors = getBlockStatementAncestors(node);
const variableViolationType = getNodeViolationType(node.init);
if (
blockAncestors.length > 0
&& variableViolationType
&& node.parent.kind === 'const' // only support const right now
) {
addVariableNameToSet(
variableViolationType, node.id.name, 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) {
context.report({
node,
messageId: nodeViolationType
});
}
}
};
})
};

View file

@ -0,0 +1,66 @@
/**
* @fileoverview Comments inside children section of tag should be placed inside braces.
* @author Ben Vinegar
*/
'use strict';
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function checkText(node, context) {
// since babel-eslint has the wrong node.raw, we'll get the source text
const rawValue = context.getSourceCode().getText(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
) {
context.report({
node,
messageId: 'putCommentInBraces'
});
}
}
}
module.exports = {
meta: {
docs: {
description: 'Comments inside children section of tag should be placed inside braces',
category: 'Possible Errors',
recommended: true,
url: docsUrl('jsx-no-comment-textnodes')
},
messages: {
putCommentInBraces: 'Comments inside children section of tag should be placed inside braces'
},
schema: [{
type: 'object',
properties: {},
additionalProperties: false
}]
},
create(context) {
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
Literal(node) {
checkText(node, context);
},
JSXText(node) {
checkText(node, context);
}
};
}
};

View file

@ -0,0 +1,219 @@
/**
* @fileoverview Prevents jsx context provider values from taking values that
* will cause needless rerenders.
* @author Dylan Oshima
*/
'use strict';
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// 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
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevents 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: {
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.'
}
},
create(context) {
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 = context.getScope();
// Check if the value prop is a construction
const constructInfo = isConstruction(valueExpression, invocationScope);
if (constructInfo == null) {
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';
}
context.report({
node: constructNode,
messageId,
data
});
}
};
}
};

View file

@ -0,0 +1,74 @@
/**
* @fileoverview Enforce no duplicate props
* @author Markus Ånöstam
*/
'use strict';
const has = require('has');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce no duplicate props',
category: 'Possible Errors',
recommended: true,
url: docsUrl('jsx-no-duplicate-props')
},
messages: {
noDuplicateProps: 'No duplicate props allowed'
},
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)) {
context.report({
node: decl,
messageId: 'noDuplicateProps'
});
} else {
props[name] = 1;
}
});
}
};
}
};

View file

@ -0,0 +1,186 @@
/**
* @fileoverview Prevent using string literals in React component definition
* @author Caleb Morris
* @author David Buchan-Swanson
*/
'use strict';
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function trimIfString(val) {
return typeof val === 'string' ? val.trim() : val;
}
module.exports = {
meta: {
docs: {
description: 'Prevent using string literals in React component definition',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-no-literals')
},
messages: {
invalidPropValue: 'Invalid prop value: "{{text}}"',
noStringsInAttributes: 'Strings not allowed in attributes: "{{text}}"',
noStringsInJSX: 'Strings not allowed in JSX files: "{{text}}"',
literalNotInJSXExpression: 'Missing JSX expression container around literal string: "{{text}}"'
},
schema: [{
type: 'object',
properties: {
noStrings: {
type: 'boolean'
},
allowedStrings: {
type: 'array',
uniqueItems: true,
items: {
type: 'string'
}
},
ignoreProps: {
type: 'boolean'
},
noAttributeStrings: {
type: 'boolean'
}
},
additionalProperties: false
}]
},
create(context) {
const defaults = {
noStrings: false,
allowedStrings: [],
ignoreProps: false,
noAttributeStrings: false
};
const config = Object.assign({}, defaults, context.options[0] || {});
config.allowedStrings = new Set(config.allowedStrings.map(trimIfString));
function defaultMessageId() {
if (config.noAttributeStrings) {
return 'noStringsInAttributes';
}
if (config.noStrings) {
return 'noStringsInJSX';
}
return 'literalNotInJSXExpression';
}
function reportLiteralNode(node, messageId) {
messageId = messageId || defaultMessageId();
context.report({
node,
messageId,
data: {
text: context.getSourceCode().getText(node).trim()
}
});
}
function getParentIgnoringBinaryExpressions(node) {
let current = node;
while (current.parent.type === 'BinaryExpression') {
current = current.parent;
}
return current.parent;
}
function getValidation(node) {
if (config.allowedStrings.has(trimIfString(node.value))) {
return false;
}
const parent = getParentIgnoringBinaryExpressions(node);
function isParentNodeStandard() {
if (!/^[\s]+$/.test(node.value) && typeof node.value === 'string' && parent.type.includes('JSX')) {
if (config.noAttributeStrings) {
return parent.type === 'JSXAttribute';
}
if (!config.noAttributeStrings) {
return parent.type !== 'JSXAttribute';
}
}
return false;
}
const standard = isParentNodeStandard();
if (config.noStrings) {
return standard;
}
return standard && parent.type !== 'JSXExpressionContainer';
}
function getParentAndGrandParentType(node) {
const parent = getParentIgnoringBinaryExpressions(node);
const parentType = parent.type;
const grandParentType = parent.parent.type;
return {
parent,
parentType,
grandParentType,
grandParent: parent.parent
};
}
function hasJSXElementParentOrGrandParent(node) {
const parents = getParentAndGrandParentType(node);
const parentType = parents.parentType;
const grandParentType = parents.grandParentType;
return parentType === 'JSXFragment' || parentType === 'JSXElement' || grandParentType === 'JSXElement';
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
Literal(node) {
if (getValidation(node) && (hasJSXElementParentOrGrandParent(node) || !config.ignoreProps)) {
reportLiteralNode(node);
}
},
JSXAttribute(node) {
const isNodeValueString = node && node.value && node.value.type === 'Literal' && typeof node.value.value === 'string' && !config.allowedStrings.has(node.value.value);
if (config.noStrings && !config.ignoreProps && isNodeValueString) {
const messageId = 'invalidPropValue';
reportLiteralNode(node, messageId);
}
},
JSXText(node) {
if (getValidation(node)) {
reportLiteralNode(node);
}
},
TemplateLiteral(node) {
const parents = getParentAndGrandParentType(node);
const parentType = parents.parentType;
const grandParentType = parents.grandParentType;
const isParentJSXExpressionCont = parentType === 'JSXExpressionContainer';
const isParentJSXElement = parentType === 'JSXElement' || grandParentType === 'JSXElement';
if (isParentJSXExpressionCont && config.noStrings && (isParentJSXElement || !config.ignoreProps)) {
reportLiteralNode(node);
}
}
};
}
};

View file

@ -0,0 +1,96 @@
/**
* @fileoverview Prevent usage of `javascript:` URLs
* @author Sergei Startsev
*/
'use strict';
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// 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 shouldVerifyElement(node, config) {
const name = node.name && node.name.name;
return name === 'a' || config.find((i) => i.name === name);
}
function shouldVerifyProp(node, config) {
const name = node.name && node.name.name;
const parentName = node.parent.name && node.parent.name.name;
if (parentName === 'a' && name === 'href') {
return true;
}
const el = config.find((i) => i.name === parentName);
if (!el) {
return false;
}
const props = el.props || [];
return node.name && props.indexOf(name) !== -1;
}
module.exports = {
meta: {
docs: {
description: 'Forbid `javascript:` URLs',
category: 'Best Practices',
recommended: false,
url: docsUrl('jsx-no-script-url')
},
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.'
},
schema: [{
type: 'array',
uniqueItems: true,
items: {
type: 'object',
properties: {
name: {
type: 'string'
},
props: {
type: 'array',
items: {
type: 'string',
uniqueItems: true
}
}
},
required: ['name', 'props'],
additionalProperties: false
}
}]
},
create(context) {
const config = context.options[0] || [];
return {
JSXAttribute(node) {
const parent = node.parent;
if (shouldVerifyElement(parent, config) && shouldVerifyProp(node, config) && hasJavaScriptProtocol(node)) {
context.report({
node,
messageId: 'noScriptURL'
});
}
}
};
}
};

View file

@ -0,0 +1,209 @@
/**
* @fileoverview Forbid target='_blank' attribute
* @author Kevin Miller
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const linkComponentsUtil = require('../util/linkComponents');
// ------------------------------------------------------------------------------
// 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, linkAttribute, warnOnSpreadAttributes, spreadAttributeIndex) {
const linkIndex = findLastIndex(node.attributes, (attr) => attr.name && attr.name.name === linkAttribute);
const foundExternalLink = linkIndex !== -1 && ((attr) => attr.value.type === 'Literal' && /^(?:\w+:|\/\/)/.test(attr.value.value))(
node.attributes[linkIndex]);
return foundExternalLink || (warnOnSpreadAttributes && linkIndex < spreadAttributeIndex);
}
function hasDynamicLink(node, linkAttribute) {
const dynamicLinkIndex = findLastIndex(node.attributes, (attr) => attr.name
&& attr.name.name === linkAttribute
&& attr.value
&& attr.value.type === 'JSXExpressionContainer');
if (dynamicLinkIndex !== -1) {
return true;
}
}
function getStringFromValue(value) {
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;
}
return value.expression && value.expression.value;
}
}
return null;
}
function hasSecureRel(node, allowReferrer, warnOnSpreadAttributes, spreadAttributeIndex) {
const relIndex = findLastIndex(node.attributes, (attr) => (attr.type === 'JSXAttribute' && attr.name.name === 'rel'));
if (relIndex === -1 || (warnOnSpreadAttributes && relIndex < spreadAttributeIndex)) {
return false;
}
const relAttribute = node.attributes[relIndex];
const value = getStringFromValue(relAttribute.value);
const tags = value && typeof value === 'string' && value.toLowerCase().split(' ');
const noreferrer = tags && tags.indexOf('noreferrer') >= 0;
if (noreferrer) {
return true;
}
return allowReferrer && tags && tags.indexOf('noopener') >= 0;
}
module.exports = {
meta: {
fixable: 'code',
docs: {
description: 'Forbid `target="_blank"` attribute without `rel="noreferrer"`',
category: 'Best Practices',
recommended: true,
url: docsUrl('jsx-no-target-blank')
},
messages: {
noTargetBlank: 'Using target="_blank" without rel="noreferrer" '
+ 'is a security risk: see https://html.spec.whatwg.org/multipage/links.html#link-type-noopener'
},
schema: [{
type: 'object',
properties: {
allowReferrer: {
type: 'boolean'
},
enforceDynamicLinks: {
enum: ['always', 'never']
},
warnOnSpreadAttributes: {
type: 'boolean'
}
},
additionalProperties: false
}]
},
create(context) {
const configuration = context.options[0] || {};
const allowReferrer = configuration.allowReferrer || false;
const warnOnSpreadAttributes = configuration.warnOnSpreadAttributes || false;
const enforceDynamicLinks = configuration.enforceDynamicLinks || 'always';
const components = linkComponentsUtil.getLinkComponents(context);
return {
JSXOpeningElement(node) {
if (!components.has(node.name.name)) {
return;
}
const targetIndex = findLastIndex(node.attributes, (attr) => attr.name && attr.name.name === 'target');
const spreadAttributeIndex = findLastIndex(node.attributes, (attr) => (attr.type === 'JSXSpreadAttribute'));
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 linkAttribute = components.get(node.name.name);
const hasDangerousLink = hasExternalLink(node, linkAttribute, warnOnSpreadAttributes, spreadAttributeIndex)
|| (enforceDynamicLinks === 'always' && hasDynamicLink(node, linkAttribute));
if (hasDangerousLink && !hasSecureRel(node, allowReferrer, warnOnSpreadAttributes, spreadAttributeIndex)) {
context.report({
node,
messageId: 'noTargetBlank',
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="noreferrer"');
}
if (!relAttribute.value) {
return fixer.insertTextAfter(relAttribute, '="noreferrer"');
}
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;
}
});
}
}
};
}
};

View file

@ -0,0 +1,113 @@
/**
* @fileoverview Disallow undeclared variables in JSX
* @author Yannick Croissant
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Disallow undeclared variables in JSX',
category: 'Possible Errors',
recommended: true,
url: docsUrl('jsx-no-undef')
},
messages: {
undefined: '\'{{identifier}}\' is not defined.'
},
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 = context.getScope();
const sourceCode = context.getSourceCode();
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;
}
}
context.report({
node,
messageId: 'undefined',
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,227 @@
/**
* @fileoverview Disallow useless fragments
*/
'use strict';
const arrayIncludes = require('array-includes');
const pragmaUtil = require('../util/pragma');
const jsxUtil = require('../util/jsx');
const docsUrl = require('../util/docsUrl');
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'
&& node.expression
&& node.expression.type === 'CallExpression';
}
module.exports = {
meta: {
type: 'suggestion',
fixable: 'code',
docs: {
description: 'Disallow unnecessary fragments',
category: 'Possible Errors',
recommended: false,
url: docsUrl('jsx-no-useless-fragment')
},
messages: {
NeedsMoreChidren: '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.'
}
},
create(context) {
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');
}
/**
* 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;
}
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 ? '' : context.getSourceCode().getText().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)) {
context.report({
node,
messageId: 'NeedsMoreChidren',
fix: getFix(node)
});
}
if (isChildOfHtmlElement(node)) {
context.report({
node,
messageId: 'ChildOfHtmlElement',
fix: getFix(node)
});
}
}
return {
JSXElement(node) {
if (jsxUtil.isFragment(node, reactPragma, fragmentPragma)) {
checkNode(node);
}
},
JSXFragment: checkNode
};
}
};

View file

@ -0,0 +1,231 @@
/**
* @fileoverview Limit to one expression per line in JSX
* @author Mark Ivan Allen <Vydia.com>
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const optionDefaults = {
allow: 'none'
};
module.exports = {
meta: {
docs: {
description: 'Limit to one expression per line in JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-one-expression-per-line')
},
fixable: 'whitespace',
messages: {
moveToNewLine: '`{{descriptor}}` must be placed on a new line'
},
schema: [
{
type: 'object',
properties: {
allow: {
enum: ['none', 'literal', 'single-child']
}
},
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}`;
}
function nodeDescriptor(n) {
return n.openingElement ? n.openingElement.name.name : context.getSourceCode().getText(n).replace(/\n/g, '');
}
function handleJSX(node) {
const children = node.children;
if (!children || !children.length) {
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))
|| context.getSourceCode().isSpaceBetweenTokens(prevChild, child);
}
function spaceBetweenNext() {
return ((nextChild.type === 'Literal' || nextChild.type === 'JSXText') && /^ /.test(nextChild.raw))
|| ((child.type === 'Literal' || child.type === 'JSXText') && / $/.test(child.raw))
|| context.getSourceCode().isSpaceBetweenTokens(child, nextChild);
}
if (!prevChild && !nextChild) {
return;
}
const source = context.getSourceCode().getText(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}`;
context.report({
node: nodeToReport,
messageId: 'moveToNewLine',
data: {
descriptor
},
fix(fixer) {
return fixer.replaceText(nodeToReport, replaceText);
}
});
});
}
return {
JSXElement: handleJSX,
JSXFragment: handleJSX
};
}
};

View file

@ -0,0 +1,154 @@
/**
* @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');
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
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce PascalCase for user-defined JSX components',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-pascal-case')
},
messages: {
usePascalCase: 'Imported JSX component {{name}} must be in PascalCase',
usePascalOrSnakeCase: 'Imported JSX component {{name}} must be in PascalCase or SCREAMING_SNAKE_CASE'
},
schema: [{
type: 'object',
properties: {
allowAllCaps: {
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 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 isPascalCase = testPascalCase(splitName);
const isAllowedAllCaps = allowAllCaps && testAllCaps(splitName);
const isIgnored = ignoreCheck(ignore, splitName);
if (!isPascalCase && !isAllowedAllCaps && !isIgnored) {
context.report({
node,
messageId: allowAllCaps ? 'usePascalOrSnakeCase' : 'usePascalCase',
data: {
name: splitName
}
});
break;
}
index++;
} while (index < checkNames.length && !allowNamespace);
}
};
}
};

View file

@ -0,0 +1,131 @@
/**
* @fileoverview Disallow multiple spaces between inline JSX props
* @author Adrian Moennich
*/
'use strict';
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
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: {
noLineGap: 'Expected no line gap between “{{prop1}}” and “{{prop2}}”',
onlyOneSpace: 'Expected only one space between “{{prop1}}” and “{{prop2}}”'
},
schema: []
},
create(context) {
const sourceCode = context.getSourceCode();
function getPropName(propNode) {
switch (propNode.type) {
case 'JSXSpreadAttribute':
return context.getSourceCode().getText(propNode.argument);
case 'JSXIdentifier':
return propNode.name;
case 'JSXMemberExpression':
return `${getPropName(propNode.object)}.${propNode.property.name}`;
default:
return propNode.name.name;
}
}
// First and second must be adjacent nodes
function hasEmptyLines(first, second) {
const comments = 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)) {
context.report({
node,
messageId: 'noLineGap',
data: {
prop1: getPropName(prev),
prop2: getPropName(node)
}
});
}
if (prev.loc.end.line !== node.loc.end.line) {
return;
}
const between = context.getSourceCode().text.slice(prev.range[1], node.range[0]);
if (between !== ' ') {
context.report({
node,
messageId: 'onlyOneSpace',
data: {
prop1: getPropName(prev),
prop2: getPropName(node)
},
fix(fixer) {
return fixer.replaceTextRange([prev.range[1], node.range[0]], ' ');
}
});
}
}
function containsGenericType(node) {
const containsTypeParams = typeof node.typeParameters !== 'undefined';
return containsTypeParams && node.typeParameters.type === 'TSTypeParameterInstantiation';
}
function getGenericNode(node) {
const name = node.name;
if (containsGenericType(node)) {
const type = node.typeParameters;
return Object.assign(
{},
node,
{
range: [
name.range[0],
type.range[1]
]
}
);
}
return name;
}
return {
JSXOpeningElement(node) {
node.attributes.reduce((prev, prop) => {
checkSpacing(prev, prop);
return prop;
}, getGenericNode(node));
}
};
}
};

View file

@ -0,0 +1,132 @@
/**
* @fileoverview Prevent JSX prop spreading
* @author Ashish Gambhir
*/
'use strict';
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const OPTIONS = {ignore: 'ignore', enforce: 'enforce'};
const DEFAULTS = {
html: OPTIONS.enforce,
custom: OPTIONS.enforce,
explicitSpread: OPTIONS.enforce,
exceptions: []
};
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent JSX prop spreading',
category: 'Best Practices',
recommended: false,
url: docsUrl('jsx-props-no-spreading')
},
messages: {
noSpreading: 'Prop spreading is forbidden'
},
schema: [{
allOf: [{
type: 'object',
properties: {
html: {
enum: [OPTIONS.enforce, OPTIONS.ignore]
},
custom: {
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;
const isException = (tag, allExceptions) => allExceptions.indexOf(tag) !== -1;
const isProperty = (property) => property.type === 'Property';
const getTagNameFromMemberExpression = (node) => `${node.property.parent.object.name}.${node.property.name}`;
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;
}
context.report({
node,
messageId: 'noSpreading'
});
}
};
}
};

View file

@ -0,0 +1,183 @@
/**
* @fileoverview Enforce default props alphabetical sorting
* @author Vladimir Kattsov
*/
'use strict';
const variableUtil = require('../util/variable');
const docsUrl = require('../util/docsUrl');
const propWrapperUtil = require('../util/propWrapper');
// const propTypesSortUtil = require('../util/propTypesSort');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce default props alphabetical sorting',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-sort-default-props')
},
// fixable: 'code',
messages: {
propsNotSorted: 'Default prop types declarations should be sorted alphabetically'
},
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 = context.getFirstTokens(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 context.getSourceCode().getText(node.key || node.argument);
}
/**
* Find a variable by name in the current scope.
* @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(name) {
const variable = variableUtil.variablesInScope(context).find((item) => item.name === 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(fixer, context, 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) {
context.report({
node: curr,
messageId: 'propsNotSorted'
// fix
});
return prev;
}
return curr;
}, declarations[0]);
}
function checkNode(node) {
switch (node && node.type) {
case 'ObjectExpression':
checkSorted(node.properties);
break;
case 'Identifier': {
const propTypesObject = findVariableByName(node.name);
if (propTypesObject && propTypesObject.properties) {
checkSorted(propTypesObject.properties);
}
break;
}
case 'CallExpression': {
const innerNode = node.arguments && node.arguments[0];
if (propWrapperUtil.isPropWrapperFunction(context, node.callee.name) && innerNode) {
checkNode(innerNode);
}
break;
}
default:
break;
}
}
// --------------------------------------------------------------------------
// Public API
// --------------------------------------------------------------------------
return {
ClassProperty(node) {
if (!isDefaultPropsDeclaration(node)) {
return;
}
checkNode(node.value);
},
MemberExpression(node) {
if (!isDefaultPropsDeclaration(node)) {
return;
}
checkNode(node.parent.right);
}
};
}
};

View file

@ -0,0 +1,380 @@
/**
* @fileoverview Enforce props alphabetical sorting
* @author Ilya Volodin, Yannick Croissant
*/
'use strict';
const propName = require('jsx-ast-utils/propName');
const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function isCallbackPropName(name) {
return /^on[A-Z]/.test(name);
}
const RESERVED_PROPS_LIST = [
'children',
'dangerouslySetInnerHTML',
'key',
'ref'
];
function isReservedPropName(name, list) {
return list.indexOf(name) >= 0;
}
function contextCompare(a, b, options) {
let aProp = propName(a);
let bProp = propName(b);
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 = isCallbackPropName(aProp);
const bIsCallback = 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.noSortAlphabetically) {
return 0;
}
if (options.ignoreCase) {
aProp = aProp.toLowerCase();
bProp = bProp.toLowerCase();
return aProp.localeCompare(bProp);
}
if (aProp === bProp) {
return 0;
}
return aProp < bProp ? -1 : 1;
}
/**
* Create an array of arrays where each subarray is composed of attributes
* that are considered sortable.
* @param {Array<JSXSpreadAttribute|JSXAttribute>} attributes
* @return {Array<Array<JSXAttribute>>}
*/
function getGroupsOfSortableAttributes(attributes) {
const sortableAttributeGroups = [];
let groupCount = 0;
for (let i = 0; i < attributes.length; i++) {
const lastAttr = attributes[i - 1];
// 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'
&& attributes[i].type !== 'JSXSpreadAttribute')
) {
groupCount++;
sortableAttributeGroups[groupCount - 1] = [];
}
if (attributes[i].type !== 'JSXSpreadAttribute') {
sortableAttributeGroups[groupCount - 1].push(attributes[i]);
}
}
return sortableAttributeGroups;
}
const generateFixerFunction = (node, context, reservedList) => {
const sourceCode = context.getSourceCode();
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 noSortAlphabetically = configuration.noSortAlphabetically || false;
const reservedFirst = configuration.reservedFirst || false;
// 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,
noSortAlphabetically,
reservedFirst,
reservedList
};
const sortableAttributeGroups = getGroupsOfSortableAttributes(attributes);
const sortedAttributeGroups = sortableAttributeGroups
.slice(0)
.map((group) => group.slice(0).sort((a, b) => contextCompare(a, b, options)));
return function fixFunction(fixer) {
const fixers = [];
let source = sourceCode.getText();
// Replace each unsorted attribute with the sorted one.
sortableAttributeGroups.forEach((sortableGroup, ii) => {
sortableGroup.forEach((attr, jj) => {
const sortedAttr = sortedAttributeGroups[ii][jj];
const sortedAttrText = sourceCode.getText(sortedAttr);
fixers.push({
range: [attr.range[0], attr.range[1]],
text: sortedAttrText
});
});
});
fixers.sort((a, b) => b.range[0] - a.range[0]);
const rangeStart = fixers[fixers.length - 1].range[0];
const rangeEnd = fixers[0].range[1];
fixers.forEach((fix) => {
source = `${source.substr(0, fix.range[0])}${fix.text}${source.substr(fix.range[1])}`;
});
return fixer.replaceTextRange([rangeStart, rangeEnd], source.substr(rangeStart, rangeEnd - rangeStart));
};
};
/**
* Checks if the `reservedFirst` option is valid
* @param {Object} context The context of the rule
* @param {Boolean|Array<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) {
context.report({
node: decl,
messageId: 'listIsEmpty'
});
};
}
if (nonReservedWords.length > 0) {
return function report(decl) {
context.report({
node: decl,
messageId: 'noUnreservedProps',
data: {
unreservedWords: nonReservedWords.toString()
}
});
};
}
}
}
}
module.exports = {
meta: {
docs: {
description: 'Enforce props alphabetical sorting',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-sort-props')
},
fixable: 'code',
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',
sortPropsByAlpha: 'Props should be sorted alphabetically'
},
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'
},
ignoreCase: {
type: 'boolean'
},
// Whether alphabetical sorting should be enforced
noSortAlphabetically: {
type: 'boolean'
},
reservedFirst: {
type: ['array', 'boolean']
}
},
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 noSortAlphabetically = configuration.noSortAlphabetically || false;
const reservedFirst = configuration.reservedFirst || false;
const reservedFirstError = validateReservedFirstConfig(context, reservedFirst);
let reservedList = Array.isArray(reservedFirst) ? reservedFirst : RESERVED_PROPS_LIST;
return {
JSXOpeningElement(node) {
// `dangerouslySetInnerHTML` is only "reserved" on DOM components
if (reservedFirst && !jsxUtil.isDOMComponent(node)) {
reservedList = reservedList.filter((prop) => prop !== 'dangerouslySetInnerHTML');
}
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 = isCallbackPropName(previousPropName);
const currentIsCallback = isCallbackPropName(currentPropName);
if (ignoreCase) {
previousPropName = previousPropName.toLowerCase();
currentPropName = currentPropName.toLowerCase();
}
if (reservedFirst) {
if (reservedFirstError) {
reservedFirstError(decl);
return memo;
}
const previousIsReserved = isReservedPropName(previousPropName, reservedList);
const currentIsReserved = isReservedPropName(currentPropName, reservedList);
if (previousIsReserved && !currentIsReserved) {
return decl;
}
if (!previousIsReserved && currentIsReserved) {
context.report({
node: decl.name,
messageId: 'listReservedPropsFirst',
fix: generateFixerFunction(node, context, reservedList)
});
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
context.report({
node: memo.name,
messageId: 'listCallbacksLast',
fix: generateFixerFunction(node, context, reservedList)
});
return memo;
}
}
if (shorthandFirst) {
if (currentValue && !previousValue) {
return decl;
}
if (!currentValue && previousValue) {
context.report({
node: memo.name,
messageId: 'listShorthandFirst',
fix: generateFixerFunction(node, context, reservedList)
});
return memo;
}
}
if (shorthandLast) {
if (!currentValue && previousValue) {
return decl;
}
if (currentValue && !previousValue) {
context.report({
node: memo.name,
messageId: 'listShorthandLast',
fix: generateFixerFunction(node, context, reservedList)
});
return memo;
}
}
if (
!noSortAlphabetically
&& (
ignoreCase
? previousPropName.localeCompare(currentPropName) > 0
: previousPropName > currentPropName
)
) {
context.report({
node: decl.name,
messageId: 'sortPropsByAlpha',
fix: generateFixerFunction(node, context, reservedList)
});
return memo;
}
return decl;
}, node.attributes[0]);
}
};
}
};

View file

@ -0,0 +1,94 @@
/**
* @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');
let isWarnedForDeprecation = false;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
deprecated: true,
docs: {
description: 'Validate spacing before closing bracket in JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-space-before-closing')
},
fixable: 'code',
messages: {
noSpaceBeforeClose: 'A space is forbidden before closing bracket',
needSpaceBeforeClose: 'A space is required before closing bracket'
},
schema: [{
enum: ['always', 'never']
}]
},
create(context) {
const configuration = context.options[0] || 'always';
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
JSXOpeningElement(node) {
if (!node.selfClosing) {
return;
}
const sourceCode = context.getSourceCode();
const leftToken = getTokenBeforeClosingBracket(node);
const closingSlash = sourceCode.getTokenAfter(leftToken);
if (leftToken.loc.end.line !== closingSlash.loc.start.line) {
return;
}
if (configuration === 'always' && !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
context.report({
loc: closingSlash.loc.start,
messageId: 'needSpaceBeforeClose',
fix(fixer) {
return fixer.insertTextBefore(closingSlash, ' ');
}
});
} else if (configuration === 'never' && sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
context.report({
loc: closingSlash.loc.start,
messageId: 'noSpaceBeforeClose',
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,288 @@
/**
* @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');
// ------------------------------------------------------------------------------
// Validators
// ------------------------------------------------------------------------------
function validateClosingSlash(context, node, option) {
const sourceCode = context.getSourceCode();
let adjacent;
if (node.selfClosing) {
const lastTokens = sourceCode.getLastTokens(node, 2);
adjacent = !sourceCode.isSpaceBetweenTokens(lastTokens[0], lastTokens[1]);
if (option === 'never') {
if (!adjacent) {
context.report({
node,
loc: {
start: lastTokens[0].loc.start,
end: lastTokens[1].loc.end
},
messageId: 'selfCloseSlashNoSpace',
fix(fixer) {
return fixer.removeRange([lastTokens[0].range[1], lastTokens[1].range[0]]);
}
});
}
} else if (option === 'always' && adjacent) {
context.report({
node,
loc: {
start: lastTokens[0].loc.start,
end: lastTokens[1].loc.end
},
messageId: 'selfCloseSlashNeedSpace',
fix(fixer) {
return fixer.insertTextBefore(lastTokens[1], ' ');
}
});
}
} else {
const firstTokens = sourceCode.getFirstTokens(node, 2);
adjacent = !sourceCode.isSpaceBetweenTokens(firstTokens[0], firstTokens[1]);
if (option === 'never') {
if (!adjacent) {
context.report({
node,
loc: {
start: firstTokens[0].loc.start,
end: firstTokens[1].loc.end
},
messageId: 'closeSlashNoSpace',
fix(fixer) {
return fixer.removeRange([firstTokens[0].range[1], firstTokens[1].range[0]]);
}
});
}
} else if (option === 'always' && adjacent) {
context.report({
node,
loc: {
start: firstTokens[0].loc.start,
end: firstTokens[1].loc.end
},
messageId: 'closeSlashNeedSpace',
fix(fixer) {
return fixer.insertTextBefore(firstTokens[1], ' ');
}
});
}
}
}
function validateBeforeSelfClosing(context, node, option) {
const sourceCode = context.getSourceCode();
const leftToken = getTokenBeforeClosingBracket(node);
const closingSlash = sourceCode.getTokenAfter(leftToken);
if (leftToken.loc.end.line !== closingSlash.loc.start.line) {
return;
}
if (option === 'always' && !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
context.report({
node,
loc: closingSlash.loc.start,
messageId: 'beforeSelfCloseNeedSpace',
fix(fixer) {
return fixer.insertTextBefore(closingSlash, ' ');
}
});
} else if (option === 'never' && sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
context.report({
node,
loc: closingSlash.loc.start,
messageId: 'beforeSelfCloseNoSpace',
fix(fixer) {
const previousToken = sourceCode.getTokenBefore(closingSlash);
return fixer.removeRange([previousToken.range[1], closingSlash.range[0]]);
}
});
}
}
function validateAfterOpening(context, node, option) {
const sourceCode = context.getSourceCode();
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) {
context.report({
node,
loc: {
start: openingToken.loc.start,
end: node.name.loc.start
},
messageId: 'afterOpenNoSpace',
fix(fixer) {
return fixer.removeRange([openingToken.range[1], node.name.range[0]]);
}
});
}
} else if (option === 'always' && adjacent) {
context.report({
node,
loc: {
start: openingToken.loc.start,
end: node.name.loc.start
},
messageId: 'afterOpenNeedSpace',
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 = context.getSourceCode();
const lastTokens = sourceCode.getLastTokens(node, 2);
const closingToken = lastTokens[1];
const leftToken = lastTokens[0];
if (leftToken.loc.start.line !== closingToken.loc.start.line) {
return;
}
const adjacent = !sourceCode.isSpaceBetweenTokens(leftToken, closingToken);
if (option === 'never' && !adjacent) {
context.report({
node,
loc: {
start: leftToken.loc.end,
end: closingToken.loc.start
},
messageId: 'beforeCloseNoSpace',
fix(fixer) {
return fixer.removeRange([leftToken.range[1], closingToken.range[0]]);
}
});
} else if (option === 'always' && adjacent) {
context.report({
node,
loc: {
start: leftToken.loc.end,
end: closingToken.loc.start
},
messageId: 'beforeCloseNeedSpace',
fix(fixer) {
return fixer.insertTextBefore(closingToken, ' ');
}
});
}
}
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const optionDefaults = {
closingSlash: 'never',
beforeSelfClosing: 'always',
afterOpening: 'never',
beforeClosing: 'allow'
};
module.exports = {
meta: {
docs: {
description: 'Validate whitespace in and around the JSX opening and closing brackets',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-tag-spacing')
},
fixable: 'whitespace',
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',
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'
},
schema: [
{
type: 'object',
properties: {
closingSlash: {
enum: ['always', 'never', 'allow']
},
beforeSelfClosing: {
enum: ['always', 'never', 'allow']
},
afterOpening: {
enum: ['always', 'allow-multiline', 'never', 'allow']
},
beforeClosing: {
enum: ['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,45 @@
/**
* @fileoverview Prevent React to be marked as unused
* @author Glen Mailer
*/
'use strict';
const pragmaUtil = require('../util/pragma');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent React to be 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);
function handleOpeningElement() {
context.markVariableAsUsed(pragma);
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
JSXOpeningElement: handleOpeningElement,
JSXOpeningFragment: handleOpeningElement,
JSXFragment() {
context.markVariableAsUsed(fragment);
}
};
}
};

View file

@ -0,0 +1,52 @@
/**
* @fileoverview Prevent variables used in JSX to be marked as unused
* @author Yannick Croissant
*/
'use strict';
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent variables used in JSX to be 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;
} else if (node.name.object) {
// <Foo...Bar>
let parent = node.name.object;
while (parent.object) {
parent = parent.object;
}
name = parent.name;
} else {
return;
}
context.markVariableAsUsed(name);
}
};
}
};

View file

@ -0,0 +1,267 @@
/**
* @fileoverview Prevent missing parentheses around multilines JSX
* @author Yannick Croissant
*/
'use strict';
const has = require('has');
const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const DEFAULTS = {
declaration: 'parens',
assignment: 'parens',
return: 'parens',
arrow: 'parens',
condition: 'ignore',
logical: 'ignore',
prop: 'ignore'
};
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent missing parentheses around multilines JSX',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-wrap-multilines')
},
fixable: 'code',
messages: {
missingParens: 'Missing parentheses around multilines JSX',
parensOnNewLines: 'Parentheses around JSX should be on separate lines'
},
schema: [{
type: 'object',
// true/false are for backwards compatibility
properties: {
declaration: {
enum: [true, false, 'ignore', 'parens', 'parens-new-line']
},
assignment: {
enum: [true, false, 'ignore', 'parens', 'parens-new-line']
},
return: {
enum: [true, false, 'ignore', 'parens', 'parens-new-line']
},
arrow: {
enum: [true, false, 'ignore', 'parens', 'parens-new-line']
},
condition: {
enum: [true, false, 'ignore', 'parens', 'parens-new-line']
},
logical: {
enum: [true, false, 'ignore', 'parens', 'parens-new-line']
},
prop: {
enum: [true, false, 'ignore', 'parens', 'parens-new-line']
}
},
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 isParenthesised(node) {
const sourceCode = context.getSourceCode();
const previousToken = sourceCode.getTokenBefore(node);
const nextToken = sourceCode.getTokenAfter(node);
return previousToken && nextToken
&& previousToken.value === '(' && previousToken.range[1] <= node.range[0]
&& nextToken.value === ')' && nextToken.range[0] >= node.range[1];
}
function needsOpeningNewLine(node) {
const previousToken = context.getSourceCode().getTokenBefore(node);
if (!isParenthesised(node)) {
return false;
}
if (previousToken.loc.end.line === node.loc.start.line) {
return true;
}
return false;
}
function needsClosingNewLine(node) {
const nextToken = context.getSourceCode().getTokenAfter(node);
if (!isParenthesised(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) {
context.report({
node,
messageId,
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 = context.getSourceCode();
const option = getOption(type);
if ((option === true || option === 'parens') && !isParenthesised(node) && isMultilines(node)) {
report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(${sourceCode.getText(node)})`));
}
if (option === 'parens-new-line' && isMultilines(node)) {
if (!isParenthesised(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) : ''}${sourceCode.getText(node)}\n${start.column > 0 ? ' '.repeat(start.column - 2) : ''})`
)
);
} else {
report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(\n${sourceCode.getText(node)}\n)`));
}
} else {
const needsOpening = needsOpeningNewLine(node);
const needsClosing = needsClosingNewLine(node);
if (needsOpening || needsClosing) {
report(node, 'parensOnNewLines', (fixer) => {
const text = sourceCode.getText(node);
let fixed = text;
if (needsOpening) {
fixed = `\n${fixed}`;
}
if (needsClosing) {
fixed = `${fixed}\n`;
}
return fixer.replaceText(node, fixed);
});
}
}
}
}
// --------------------------------------------------------------------------
// 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,187 @@
/**
* @fileoverview Prevent usage of this.state within setState
* @author Rolf Erik Lekang, Jørgen Aaberg
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const Components = require('../util/Components');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Reports when this.state is accessed within setState',
category: 'Possible Errors',
recommended: false,
url: docsUrl('no-access-state-in-setstate')
},
messages: {
useCallback: 'Use callback in setState when referencing the previous state.'
}
},
create: Components.detect((context, components, utils) => {
function isSetStateCall(node) {
return node.type === 'CallExpression'
&& 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;
}
function isClassComponent() {
return !!(utils.getParentES6Component() || utils.getParentES5Component());
}
// 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()) {
return;
}
// Appends all the methods that are calling another
// method containing this.state to the methods array
methods.forEach((method) => {
if (node.callee.name === method.methodName) {
let current = node.parent;
while (current.type !== 'Program') {
if (current.type === 'MethodDefinition') {
methods.push({
methodName: current.key.name,
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 = node.callee.name;
methods.forEach((method) => {
if (method.methodName === methodName) {
context.report({
node: method.node,
messageId: 'useCallback'
});
}
});
break;
}
current = current.parent;
}
},
MemberExpression(node) {
if (
node.property.name === 'state'
&& node.object.type === 'ThisExpression'
&& isClassComponent()
) {
let current = node;
while (current.type !== 'Program') {
// Reporting if this.state is directly within this.setState
if (isFirstArgumentInSetStateCall(current, node)) {
context.report({
node,
messageId: 'useCallback'
});
break;
}
// Storing all functions and methods that contains this.state
if (current.type === 'MethodDefinition') {
methods.push({
methodName: current.key.name,
node
});
break;
} else if (current.type === 'FunctionExpression' && current.parent.key) {
methods.push({
methodName: current.parent.key.name,
node
});
break;
}
// Storing all variables containg this.state
if (current.type === 'VariableDeclarator') {
vars.push({
node,
scope: context.getScope(),
variableName: current.id.name
});
break;
}
current = current.parent;
}
}
},
Identifier(node) {
// Checks if the identifier is a variable within an object
let current = node;
while (current.parent.type === 'BinaryExpression') {
current = current.parent;
}
if (
current.parent.value === current
|| current.parent.object === current
) {
while (current.type !== 'Program') {
if (isFirstArgumentInSetStateCall(current, node)) {
vars
.filter((v) => v.scope === context.getScope() && v.variableName === node.name)
.forEach((v) => {
context.report({
node: v.node,
messageId: 'useCallback'
});
});
}
current = current.parent;
}
}
},
ObjectPattern(node) {
const isDerivedFromThis = node.parent.init && node.parent.init.type === 'ThisExpression';
node.properties.forEach((property) => {
if (property && property.key && property.key.name === 'state' && isDerivedFromThis) {
vars.push({
node: property.key,
scope: context.getScope(),
variableName: property.key.name
});
}
});
}
};
})
};

View file

@ -0,0 +1,122 @@
/**
* @fileoverview Prevent adjacent inline elements not separated by whitespace.
* @author Sean Hayes
*/
'use strict';
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// 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 (node.type === 'CallExpression' && inlineNames.indexOf(node.arguments[0].value) > -1) {
return true;
}
return false;
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent adjacent inline elements not separated by whitespace.',
category: 'Best Practices',
recommended: false,
url: docsUrl('no-adjacent-inline-elements')
},
schema: [],
messages: {
inlineElement: 'Child elements which render as inline HTML elements should be separated by a space or wrapped in block level elements.'
}
},
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) {
context.report({
node,
messageId: 'inlineElement'
});
return;
}
previousIsInline = currentIsInline;
}
}
return {
JSXElement(node) {
validate(node, node.children);
},
CallExpression(node) {
if (!node.callee || node.callee.type !== 'MemberExpression' || node.callee.property.name !== 'createElement') {
return;
}
if (node.arguments.length < 2 || !node.arguments[2]) {
return;
}
const children = node.arguments[2].elements;
validate(node, children);
}
};
}
};

View file

@ -0,0 +1,232 @@
/**
* @fileoverview Prevent usage of Array index in keys
* @author Joe Lencioni
*/
'use strict';
const has = require('has');
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
const pragma = require('../util/pragma');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent usage of Array index in keys',
category: 'Best Practices',
recommended: false,
url: docsUrl('no-array-index-key')
},
messages: {
noArrayIndex: 'Do not use Array index in keys'
},
schema: []
},
create(context) {
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
const indexParamNames = [];
const iteratorFunctionsToIndexParamPosition = {
every: 1,
filter: 1,
find: 1,
findIndex: 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 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[callee.property.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}
context.report({
node,
messageId: 'noArrayIndex'
});
return;
}
if (node.type === 'TemplateLiteral') {
// key={`foo-${bar}`}
node.expressions.filter(isArrayIndex).forEach(() => {
context.report({node, messageId: 'noArrayIndex'});
});
return;
}
if (node.type === 'BinaryExpression') {
// key={'foo' + bar}
const identifiers = getIdentifiersFromBinaryExpression(node);
identifiers.filter(isArrayIndex).forEach(() => {
context.report({node, messageId: 'noArrayIndex'});
});
}
}
function popIndex(node) {
const mapIndexParamName = getMapIndexParamName(node);
if (!mapIndexParamName) {
return;
}
indexParamNames.pop();
}
return {
'CallExpression, OptionalCallExpression'(node) {
if (
node.callee
&& (node.callee.type === 'MemberExpression' || node.callee.type === 'OptionalMemberExpression')
&& ['createElement', 'cloneElement'].indexOf(node.callee.property.name) !== -1
&& 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,77 @@
/**
* @fileoverview Prevent passing of children as props
* @author Benjamin Stepp
*/
'use strict';
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Checks if the node is a createElement call with a props literal.
* @param {ASTNode} node - 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) {
return node.callee
&& node.callee.type === 'MemberExpression'
&& node.callee.property.name === 'createElement'
&& node.arguments.length > 1
&& node.arguments[1].type === 'ObjectExpression';
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent passing of children as props.',
category: 'Best Practices',
recommended: true,
url: docsUrl('no-children-prop')
},
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.'
},
schema: []
},
create(context) {
return {
JSXAttribute(node) {
if (node.name.name !== 'children') {
return;
}
context.report({
node,
messageId: 'nestChildren'
});
},
CallExpression(node) {
if (!isCreateElementWithProps(node)) {
return;
}
const props = node.arguments[1].properties;
const childrenProp = props.find((prop) => prop.key && prop.key.name === 'children');
if (childrenProp) {
context.report({
node,
messageId: 'passChildrenAsArgs'
});
}
}
};
}
};

View file

@ -0,0 +1,154 @@
/**
* @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');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Report when a DOM element is using both children and dangerouslySetInnerHTML',
category: 'Possible Errors',
recommended: true,
url: docsUrl('no-danger-with-children')
},
messages: {
dangerWithChildren: 'Only set one of `children` or `props.dangerouslySetInnerHTML`'
},
schema: [] // no options
},
create(context) {
function findSpreadVariable(name) {
return variableUtil.variablesInScope(context).find((item) => item.name === 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(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(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')
) {
context.report({
node,
messageId: 'dangerWithChildren'
});
}
},
CallExpression(node) {
if (
node.callee
&& node.callee.type === 'MemberExpression'
&& node.callee.property.name === 'createElement'
&& node.arguments.length > 1
) {
let hasChildren = false;
let props = node.arguments[1];
if (props.type === 'Identifier') {
const variable = variableUtil.variablesInScope(context).find((item) => item.name === 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) {
context.report({
node,
messageId: 'dangerWithChildren'
});
}
}
}
};
}
};

View file

@ -0,0 +1,74 @@
/**
* @fileoverview Prevent usage of dangerous JSX props
* @author Scott Andrews
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const DANGEROUS_PROPERTY_NAMES = [
'dangerouslySetInnerHTML'
];
const DANGEROUS_PROPERTIES = DANGEROUS_PROPERTY_NAMES.reduce((props, prop) => {
props[prop] = prop;
return props;
}, Object.create(null));
// ------------------------------------------------------------------------------
// 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 dnagerous.
*/
function isDangerous(name) {
return name in DANGEROUS_PROPERTIES;
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent usage of dangerous JSX props',
category: 'Best Practices',
recommended: false,
url: docsUrl('no-danger')
},
messages: {
dangerousProp: 'Dangerous property \'{{name}}\' found'
},
schema: []
},
create(context) {
return {
JSXAttribute(node) {
if (jsxUtil.isDOMComponent(node.parent) && isDangerous(node.name.name)) {
context.report({
node,
messageId: 'dangerousProp',
data: {
name: node.name.name
}
});
}
}
};
}
};

View file

@ -0,0 +1,223 @@
/**
* @fileoverview Prevent usage of deprecated methods
* @author Yannick Croissant
* @author Scott Feeney
* @author Sergei Startsev
*/
'use strict';
const values = require('object.values');
const Components = require('../util/Components');
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
const pragmaUtil = require('../util/pragma');
const versionUtil = require('../util/version');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const MODULES = {
react: ['React'],
'react-addons-perf': ['ReactPerf', 'Perf']
};
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent usage of deprecated methods',
category: 'Best Practices',
recommended: true,
url: docsUrl('no-deprecated')
},
messages: {
deprecated: '{{oldMethod}} is deprecated since React {{version}}{{newMethod}}{{refs}}'
},
schema: []
},
create: Components.detect((context, components, utils) => {
const pragma = pragmaUtil.getFromContext(context);
function getDeprecated() {
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.'
];
return deprecated;
}
function isDeprecated(method) {
const deprecated = getDeprecated();
return (
deprecated
&& deprecated[method]
&& deprecated[method][0]
&& versionUtil.testReactVersion(context, deprecated[method][0])
);
}
function checkDeprecation(node, methodName, methodNode) {
if (!isDeprecated(methodName)) {
return;
}
const deprecated = getDeprecated();
const version = deprecated[methodName][0];
const newMethod = deprecated[methodName][1];
const refs = deprecated[methodName][2];
context.report({
node: methodNode || node,
messageId: 'deprecated',
data: {
oldMethod: methodName,
version,
newMethod: newMethod ? `, use ${newMethod} instead` : '',
refs: refs ? `, see ${refs}` : ''
}
});
}
function getReactModuleName(node) {
let moduleName = false;
if (!node.init) {
return moduleName;
}
values(MODULES).some((moduleNames) => {
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 (utils.isES5Component(node) || utils.isES6Component(node)) {
const methods = getLifeCycleMethods(node);
methods.forEach((method) => checkDeprecation(node, method.name, method.node));
}
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
MemberExpression(node) {
checkDeprecation(node, context.getSourceCode().getText(node));
},
ImportDeclaration(node) {
const isReactImport = typeof MODULES[node.source.value] !== 'undefined';
if (!isReactImport) {
return;
}
node.specifiers.forEach((specifier) => {
if (!specifier.imported) {
return;
}
checkDeprecation(node, `${MODULES[node.source.value][0]}.${specifier.imported.name}`);
});
},
VariableDeclarator(node) {
const reactModuleName = getReactModuleName(node);
const isRequire = node.init && node.init.callee && node.init.callee.name === 'require';
const isReactRequire = node.init
&& node.init.arguments
&& node.init.arguments.length
&& typeof MODULES[node.init.arguments[0].value] !== 'undefined';
const isDestructuring = node.id && node.id.type === 'ObjectPattern';
if (
!(isDestructuring && reactModuleName)
&& !(isDestructuring && isRequire && isReactRequire)
) {
return;
}
node.id.properties.forEach((property) => {
checkDeprecation(node, `${reactModuleName || pragma}.${property.key.name}`);
});
},
ClassDeclaration: checkLifeCycleMethods,
ClassExpression: checkLifeCycleMethods,
ObjectExpression: checkLifeCycleMethods
};
})
};

View file

@ -0,0 +1,10 @@
/**
* @fileoverview Prevent usage of setState in componentDidMount
* @author Yannick Croissant
*/
'use strict';
const makeNoMethodSetStateRule = require('../util/makeNoMethodSetStateRule');
module.exports = makeNoMethodSetStateRule('componentDidMount');

View file

@ -0,0 +1,10 @@
/**
* @fileoverview Prevent usage of setState in componentDidUpdate
* @author Yannick Croissant
*/
'use strict';
const makeNoMethodSetStateRule = require('../util/makeNoMethodSetStateRule');
module.exports = makeNoMethodSetStateRule('componentDidUpdate');

View file

@ -0,0 +1,151 @@
/**
* @fileoverview Prevent direct mutation of this.state
* @author David Petersen
* @author Nicolas Fernandez <@burabure>
*/
'use strict';
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent direct mutation of this.state',
category: 'Possible Errors',
recommended: true,
url: docsUrl('no-direct-mutation-state')
},
messages: {
noDirectMutation: 'Do not mutate state directly. Use setState().'
}
},
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 Boolean(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];
context.report({
node: mutation,
messageId: 'noDirectMutation'
});
}
}
/**
* Walks throughs 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());
if (shouldIgnoreComponent(component) || !node.left || !node.left.object) {
return;
}
const item = getOuterMemberExpression(node.left);
if (utils.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());
if (shouldIgnoreComponent(component) || node.argument.type !== 'MemberExpression') {
return;
}
const item = getOuterMemberExpression(node.argument);
if (utils.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'() {
const list = components.list();
Object.keys(list).forEach((key) => {
if (!isValid(list[key])) {
reportMutations(list[key]);
}
});
}
};
})
};

View file

@ -0,0 +1,53 @@
/**
* @fileoverview Prevent usage of findDOMNode
* @author Yannick Croissant
*/
'use strict';
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent usage of findDOMNode',
category: 'Best Practices',
recommended: true,
url: docsUrl('no-find-dom-node')
},
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'
},
schema: []
},
create(context) {
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
CallExpression(node) {
const callee = node.callee;
const isfindDOMNode = (callee.name === 'findDOMNode')
|| (callee.property && callee.property.name === 'findDOMNode');
if (!isfindDOMNode) {
return;
}
context.report({
node: callee,
messageId: 'noFindDOMNode'
});
}
};
}
};

View file

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

View file

@ -0,0 +1,81 @@
/**
* @fileoverview Prevent multiple component definition per file
* @author Yannick Croissant
*/
'use strict';
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent multiple component definition per file',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('no-multi-comp')
},
messages: {
onlyOneComponent: 'Declare only one React component per file'
},
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)
)
);
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
'Program:exit'() {
if (components.length() <= 1) {
return;
}
const list = components.list();
Object.keys(list).filter((component) => !isIgnored(list[component])).forEach((component, i) => {
if (i >= 1) {
context.report({
node: list[component].node,
messageId: 'onlyOneComponent'
});
}
});
}
};
})
};

View file

@ -0,0 +1,85 @@
/**
* @fileoverview Flag shouldComponentUpdate when extending PureComponent
*/
'use strict';
const Components = require('../util/Components');
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Flag shouldComponentUpdate when extending PureComponent',
category: 'Possible Errors',
recommended: false,
url: docsUrl('no-redundant-should-component-update')
},
messages: {
noShouldCompUpdate: '{{component}} does not need shouldComponentUpdate when extending React.PureComponent.'
},
schema: []
},
create: Components.detect((context, components, utils) => {
/**
* 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 (utils.isPureComponent(node)) {
const hasScu = hasShouldComponentUpdate(node);
if (hasScu) {
const className = getNodeName(node);
context.report({
node,
messageId: 'noShouldCompUpdate',
data: {
component: className
}
});
}
}
}
return {
ClassDeclaration: checkForViolation,
ClassExpression: checkForViolation
};
})
};

View file

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

View file

@ -0,0 +1,88 @@
/**
* @fileoverview Prevent usage of setState
* @author Mark Dalgleish
*/
'use strict';
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent usage of setState',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('no-set-state')
},
messages: {
noSetState: 'Do not use setState'
},
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 Boolean(component && !component.useSetState);
}
/**
* Reports usages of setState for a given component
* @param {Object} component The component to process
*/
function reportSetStateUsages(component) {
let setStateUsage;
for (let i = 0, j = component.setStateUsages.length; i < j; i++) {
setStateUsage = component.setStateUsages[i];
context.report({
node: setStateUsage,
messageId: 'noSetState'
});
}
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
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());
const setStateUsages = (component && component.setStateUsages) || [];
setStateUsages.push(callee);
components.set(node, {
useSetState: true,
setStateUsages
});
},
'Program:exit'() {
const list = components.list();
Object.keys(list).filter((component) => !isValid(list[component])).forEach((component) => {
reportSetStateUsages(list[component]);
});
}
};
})
};

View file

@ -0,0 +1,121 @@
/**
* @fileoverview Prevent string definitions for references and prevent referencing this.refs
* @author Tom Hastjarjanto
*/
'use strict';
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent string definitions for references and prevent referencing this.refs',
category: 'Best Practices',
recommended: true,
url: docsUrl('no-string-refs')
},
messages: {
thisRefsDeprecated: 'Using this.refs is deprecated.',
stringInRefDeprecated: 'Using string literals in ref attributes is deprecated.'
},
schema: [{
type: 'object',
properties: {
noTemplateLiterals: {
type: 'boolean'
}
},
additionalProperties: false
}]
},
create: Components.detect((context, components, utils) => {
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 Boolean(
(
utils.getParentES6Component()
|| utils.getParentES5Component()
)
&& 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 Boolean(
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 Boolean(
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 Boolean(
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 (isRefsUsage(node)) {
context.report({
node,
messageId: 'thisRefsDeprecated'
});
}
},
JSXAttribute(node) {
if (
isRefAttribute(node)
&& (containsStringLiteral(node) || containsStringExpressionContainer(node))
) {
context.report({
node,
messageId: 'stringInRefDeprecated'
});
}
}
};
})
};

View file

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

View file

@ -0,0 +1,275 @@
/**
* @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');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const STATIC_CLASS_PROPERTIES = ['propTypes', 'contextTypes', 'childContextTypes', 'defaultProps'];
const STATIC_LIFECYCLE_METHODS = ['getDerivedStateFromProps'];
const LIFECYCLE_METHODS = [
'getDerivedStateFromProps',
'componentWillMount',
'UNSAFE_componentWillMount',
'componentDidMount',
'componentWillReceiveProps',
'UNSAFE_componentWillReceiveProps',
'shouldComponentUpdate',
'componentWillUpdate',
'UNSAFE_componentWillUpdate',
'getSnapshotBeforeUpdate',
'componentDidUpdate',
'componentDidCatch',
'componentWillUnmount',
'render'
];
module.exports = {
meta: {
docs: {
description: 'Prevent common typos',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('no-typos')
},
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.'
},
schema: []
},
create: Components.detect((context, components, utils) => {
let propTypesPackageName = null;
let reactPackageName = null;
function checkValidPropTypeQualifier(node) {
if (node.name !== 'isRequired') {
context.report({
node,
messageId: 'typoPropTypeChain',
data: {name: node.name}
});
}
}
function checkValidPropType(node) {
if (node.name && !PROP_TYPES.some((propTypeName) => propTypeName === node.name)) {
context.report({
node,
messageId: 'typoPropType',
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 (node.object.type === 'CallExpression') {
checkValidPropTypeQualifier(node.property);
checkValidCallExpression(node.object);
}
} else if (node.type === 'CallExpression') {
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) {
context.report({
node: propertyKey,
messageId: isClassProperty
? 'typoStaticClassProp'
: 'typoPropDeclaration'
});
}
});
}
function reportErrorIfLifecycleMethodCasingTypo(node) {
let nodeKeyName = node.key.name;
if (node.key.type === 'Literal') {
nodeKeyName = node.key.value;
}
if (node.computed && typeof nodeKeyName !== 'string') {
return;
}
STATIC_LIFECYCLE_METHODS.forEach((method) => {
if (!node.static && nodeKeyName.toLowerCase() === method.toLowerCase()) {
context.report({
node,
messageId: 'staticLifecycleMethod',
data: {
method: nodeKeyName
}
});
}
});
LIFECYCLE_METHODS.forEach((method) => {
if (method.toLowerCase() === nodeKeyName.toLowerCase() && method !== nodeKeyName) {
context.report({
node,
messageId: 'typoLifecycleMethod',
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 {
context.report({
node,
messageId: 'noPropTypesBinding'
});
}
} 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 {
context.report({
node,
messageId: 'noReactBinding'
});
}
if (node.specifiers.length >= 1) {
const propTypesSpecifier = node.specifiers.find((specifier) => (
specifier.imported && specifier.imported.name === 'PropTypes'
));
if (propTypesSpecifier) {
propTypesPackageName = propTypesSpecifier.local.name;
}
}
}
},
ClassProperty(node) {
if (!node.static || !utils.isES6Component(node.parent.parent)) {
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
&& (utils.isES6Component(relatedComponent.node) || utils.isReturningJSX(relatedComponent.node))
&& (node.parent && node.parent.type === 'AssignmentExpression' && node.parent.right)
) {
reportErrorIfPropertyCasingTypo(node.parent.right, node.property, true);
}
},
MethodDefinition(node) {
if (!utils.isES6Component(node.parent.parent)) {
return;
}
reportErrorIfLifecycleMethodCasingTypo(node);
},
ObjectExpression(node) {
const component = utils.isES5Component(node) && components.get(node);
if (!component) {
return;
}
node.properties.forEach((property) => {
if (property.type !== 'SpreadElement') {
reportErrorIfPropertyCasingTypo(property.value, property.key, false);
reportErrorIfLifecycleMethodCasingTypo(property);
}
});
}
};
})
};

View file

@ -0,0 +1,132 @@
/**
* @fileoverview HTML special characters should be escaped.
* @author Patrick Hayes
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');
// ------------------------------------------------------------------------------
// 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;']
}];
module.exports = {
meta: {
docs: {
description: 'Detect unescaped HTML entities, which might represent malformed tags',
category: 'Possible Errors',
recommended: true,
url: docsUrl('no-unescaped-entities')
},
messages: {
unescapedEntity: 'HTML entity, `{{entity}}` , must be escaped.',
unescapedEntityAlts: '`{{entity}}` can be escaped with {{alts}}.'
},
schema: [{
type: 'object',
properties: {
forbid: {
type: 'array',
items: {
oneOf: [{
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 entites are already escaped in node.value (as well as node.raw),
// so pull the raw text from context.getSourceCode()
for (let i = node.loc.start.line; i <= node.loc.end.line; i++) {
let rawLine = context.getSourceCode().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.substring(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]) {
context.report({
node,
loc: {line: i, column: start + index},
messageId: 'unescapedEntity',
data: {
entity: entities[j]
}
});
}
} else if (c === entities[j].char) {
context.report({
node,
loc: {line: i, column: start + index},
messageId: 'unescapedEntityAlts',
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,299 @@
/**
* @fileoverview Prevent usage of unknown DOM property
* @author Yannick Croissant
*/
'use strict';
const has = require('has');
const docsUrl = require('../util/docsUrl');
const versionUtil = require('../util/version');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const DEFAULTS = {
ignore: []
};
const DOM_ATTRIBUTE_NAMES = {
'accept-charset': 'acceptCharset',
class: 'className',
for: 'htmlFor',
'http-equiv': 'httpEquiv',
crossorigin: 'crossOrigin'
};
const ATTRIBUTE_TAGS_MAP = {
crossOrigin: ['script', 'img', 'video', 'audio', 'link']
};
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 = [
// Standard
'acceptCharset', 'accessKey', 'allowFullScreen', 'autoComplete', 'autoFocus', 'autoPlay',
'cellPadding', 'cellSpacing', 'classID', 'className', 'colSpan', 'contentEditable', 'contextMenu',
'dateTime', 'encType', 'formAction', 'formEncType', 'formMethod', 'formNoValidate', 'formTarget',
'frameBorder', 'hrefLang', 'htmlFor', 'httpEquiv', 'inputMode', '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', 'onTransitionEnd', 'radioGroup', 'readOnly', 'rowSpan',
'spellCheck', 'srcDoc', 'srcLang', 'srcSet', 'tabIndex', 'useMap',
// Non standard
'autoCapitalize', 'autoCorrect',
'autoSave',
'itemProp', 'itemScope', 'itemType', 'itemRef', 'itemID'
];
function getDOMPropertyNames(context) {
// this was removed in React v16.1+, see https://github.com/facebook/react/pull/10823
if (!versionUtil.testReactVersion(context, '16.1.0')) {
return ['allowTransparency'].concat(DOM_PROPERTY_NAMES);
}
return DOM_PROPERTY_NAMES;
}
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Checks if a node matches the JSX tag convention. This also checks if a node
* is extended as a webcomponent using the attribute "is".
* @param {Object} node - JSX element being tested.
* @returns {boolean} Whether or not the node name match the JSX tag convention.
*/
const tagConvention = /^[a-z][^-]*$/;
function isTagName(node) {
if (tagConvention.test(node.parent.name.name)) {
// http://www.w3.org/TR/custom-elements/#type-extension-semantics
return !node.parent.attributes.some((attrNode) => (
attrNode.type === 'JSXAttribute'
&& attrNode.name.type === 'JSXIdentifier'
&& attrNode.name.name === 'is'
));
}
return false;
}
/**
* 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 && 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 {String} 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[name];
}
if (has(SVGDOM_ATTRIBUTE_NAMES, name)) {
return 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
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent usage of unknown DOM property',
category: 'Possible Errors',
recommended: true,
url: docsUrl('no-unknown-property')
},
fixable: 'code',
messages: {
invalidPropOnTag: 'Invalid property \'{{name}}\' found on tag \'{{tagName}}\', but it is only allowed on: {{allowedTags}}',
unknownProp: 'Unknown property \'{{name}}\' found, use \'{{standardName}}\' instead'
},
schema: [{
type: 'object',
properties: {
ignore: {
type: 'array',
items: {
type: 'string'
}
}
},
additionalProperties: false
}]
},
create(context) {
function getIgnoreConfig() {
return (context.options[0] && context.options[0].ignore) || DEFAULTS.ignore;
}
return {
JSXAttribute(node) {
const ignoreNames = getIgnoreConfig();
const name = context.getSourceCode().getText(node.name);
if (ignoreNames.indexOf(name) >= 0) {
return;
}
// Ignore tags like <Foo.bar />
if (tagNameHasDot(node)) {
return;
}
const tagName = getTagName(node);
// 1. Some attributes are allowed on some tags only.
const allowedTags = has(ATTRIBUTE_TAGS_MAP, name) ? ATTRIBUTE_TAGS_MAP[name] : null;
if (tagName && allowedTags && /[^A-Z]/.test(tagName.charAt(0)) && allowedTags.indexOf(tagName) === -1) {
context.report({
node,
messageId: 'invalidPropOnTag',
data: {
name,
tagName,
allowedTags: allowedTags.join(', ')
}
});
}
// 2. Otherwise, we'll try to find if the attribute is a close version
// of what we should normally have with React. If yes, we'll report an
// error. We don't want to report if the input attribute name is the
// standard name though!
const standardName = getStandardName(name, context);
if (!isTagName(node) || !standardName || standardName === name) {
return;
}
context.report({
node,
messageId: 'unknownProp',
data: {
name,
standardName
},
fix(fixer) {
return fixer.replaceText(node.name, standardName);
}
});
}
};
}
};

View file

@ -0,0 +1,146 @@
/**
* @fileoverview Prevent usage of unsafe lifecycle methods
* @author Sergei Startsev
*/
'use strict';
const Components = require('../util/Components');
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
const versionUtil = require('../util/version');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent usage of unsafe lifecycle methods',
category: 'Best Practices',
recommended: false,
url: docsUrl('no-unsafe')
},
messages: {
unsafeMethod: '{{method}} is unsafe for use in async rendering. Update the component to use {{newMethod}} instead. {{details}}'
},
schema: [
{
type: 'object',
properties: {
checkAliases: {
default: false,
type: 'boolean'
}
},
additionalProperties: false
}
]
},
create: Components.detect((context, components, utils) => {
const config = context.options[0] || {};
const checkAliases = config.checkAliases || false;
const isApplicable = versionUtil.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;
context.report({
node,
messageId: 'unsafeMethod',
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 (utils.isES5Component(node) || utils.isES6Component(node)) {
const methods = getLifeCycleMethods(node);
methods.forEach((method) => checkUnsafe(node, method));
}
}
return {
ClassDeclaration: checkLifeCycleMethods,
ClassExpression: checkLifeCycleMethods,
ObjectExpression: checkLifeCycleMethods
};
})
};

View file

@ -0,0 +1,468 @@
/**
* @fileoverview Prevent creating unstable components inside components
* @author Ari Perkkiö
*/
'use strict';
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const ERROR_MESSAGE_WITHOUT_NAME = 'Declare this component outside parent component or memoize it.';
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
* @returns {String} Error message with parent component name
*/
function generateErrorMessageWithParentName(parentName) {
return `Declare this component outside parent component "${parentName}" or memoize it.`;
}
/**
* Check whether given text starts with `render`. Comparison is case-sensitive.
* @param {String} text Text to validate
* @returns {Boolean}
*/
function startsWithRender(text) {
return (text || '').startsWith('render');
}
/**
* Get closest parent matching given matcher
* @param {ASTNode} node The AST node
* @param {Function} matcher Method used to match the parent
* @returns {ASTNode} The matching parent node, if any
*/
function getClosestMatchingParent(node, matcher) {
if (!node || !node.parent || node.parent.type === 'Program') {
return;
}
if (matcher(node.parent)) {
return node.parent;
}
return getClosestMatchingParent(node.parent, matcher);
}
/**
* Matcher used to check whether given node is a `createElement` call
* @param {ASTNode} node The AST node
* @returns {Boolean} True if node is a `createElement` call, false if not
*/
function isCreateElementMatcher(node) {
return (
node
&& node.type === 'CallExpression'
&& node.callee
&& node.callee.property
&& node.callee.property.name === 'createElement'
);
}
/**
* 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 a `CallExpression`
* @param {ASTNode} node The AST node
* @returns {Boolean} True if node is a `CallExpression`, false if not
*/
function isCallExpressionMatcher(node) {
return node && node.type === 'CallExpression';
}
/**
* 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
* @returns {Boolean} True if node is a `ReturnStatement` of a React hook, false if not
*/
function isReturnStatementOfHook(node) {
if (
!node
|| !node.parent
|| node.parent.type !== 'ReturnStatement'
) {
return false;
}
const callExpression = getClosestMatchingParent(node, isCallExpressionMatcher);
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
* @returns {Boolean} True if component is declared inside a render prop, false if not
*/
function isComponentInRenderProp(node) {
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, 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
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent 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();
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();
const parentStatelessComponent = utils.getParentStatelessComponent();
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, isCreateElementMatcher);
return (
createElementParent
&& createElementParent.arguments
&& createElementParent.arguments[1] === getClosestMatchingParent(node, isObjectExpressionMatcher)
);
}
/**
* Check whether given node is declared inside a component prop.
* ```jsx
* <Component 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) {
const jsxAttribute = getClosestMatchingParent(node, 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)))
// Prevent reporting components created inside Array.map calls
|| isMapCall(node)
|| isMapCall(node.parent)
// Do not mark components declared inside hooks (or falsly '() => null' clean-up methods)
|| isReturnStatementOfHook(node)
// Do not mark objects containing render methods
|| isDirectValueOfRenderProperty(node)
// Prevent reporting nested class components twice
|| isInsideRenderMethod(node)
// Prevent falsely reporting deteceted "components" which do not return JSX
|| isStatelessComponentReturningNull(node)
) {
return;
}
// Get the closest parent component
const parentComponent = getClosestMatchingParent(
node,
(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 = parentName
? generateErrorMessageWithParentName(parentName)
: ERROR_MESSAGE_WITHOUT_NAME;
// Add information about allowAsProps option when component is declared inside prop
if (isDeclaredInsideProps && !allowAsProps) {
message += COMPONENT_AS_PROPS_INFO;
}
context.report({node, message});
}
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
FunctionDeclaration(node) { validate(node); },
ArrowFunctionExpression(node) { validate(node); },
FunctionExpression(node) { validate(node); },
ClassDeclaration(node) { validate(node); }
};
})
};

View file

@ -0,0 +1,171 @@
/**
* @fileoverview Prevent definitions of unused prop types
* @author Evgueni Naverniouk
*/
'use strict';
// As for exceptions for props.children or props.className (and alike) look at
// https://github.com/yannickcr/eslint-plugin-react/issues/7
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent definitions of unused prop types',
category: 'Best Practices',
recommended: false,
url: docsUrl('no-unused-prop-types')
},
messages: {
unusedPropType: '\'{{name}}\' PropType is defined but prop is never used'
},
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 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 Boolean(
component
&& !component.ignoreUnusedPropTypesValidation
);
}
/**
* 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)) {
context.report({
node: prop.node.key || prop.node,
messageId: 'unusedPropType',
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'() {
const list = components.list();
// Report undeclared proptypes for all classes
Object.keys(list).filter((component) => mustBeValidated(list[component])).forEach((component) => {
if (!mustBeValidated(list[component])) {
return;
}
reportUnusedPropTypes(list[component]);
});
}
};
})
};

View file

@ -0,0 +1,461 @@
/**
* @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 Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const ast = require('../util/ast');
// 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 ast.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 = ast.unwrapTSAsExpression(node.callee);
return (
unwrappedCalleeNode.type === 'MemberExpression'
&& isThisExpression(unwrappedCalleeNode.object)
&& getName(unwrappedCalleeNode.property) === 'setState'
);
}
module.exports = {
meta: {
docs: {
description: 'Prevent definition of unused state fields',
category: 'Best Practices',
recommended: false,
url: docsUrl('no-unused-state')
},
messages: {
unusedStateField: 'Unused state field: \'{{name}}\''
},
schema: []
},
create: Components.detect((context, components, utils) => {
// 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 = context.getScope();
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) {
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) {
for (const prop of node.properties) {
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 = ast.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) {
for (const prop of left.properties) {
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.
for (const node of classInfo.stateFields) {
const name = getName(node.key);
if (!classInfo.usedStateFields.has(name)) {
context.report({
node,
messageId: 'unusedStateField',
data: {
name
}
});
}
}
}
function handleES6ComponentEnter(node) {
if (utils.isES6Component(node)) {
classInfo = getInitialClassInfo();
}
}
function handleES6ComponentExit() {
if (!classInfo) {
return;
}
reportUnusedFields();
classInfo = null;
}
return {
ClassDeclaration: handleES6ComponentEnter,
'ClassDeclaration:exit': handleES6ComponentExit,
ClassExpression: handleES6ComponentEnter,
'ClassExpression:exit': handleES6ComponentExit,
ObjectExpression(node) {
if (utils.isES5Component(node)) {
classInfo = getInitialClassInfo();
}
},
'ObjectExpression:exit'(node) {
if (!classInfo) {
return;
}
if (utils.isES5Component(node)) {
reportUnusedFields();
classInfo = null;
}
},
CallExpression(node) {
if (!classInfo) {
return;
}
const unwrappedNode = ast.unwrapTSAsExpression(node);
const unwrappedArgumentNode = ast.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 = ast.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(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 = ast.unwrapTSAsExpression(node.value);
if (
getName(node.key) === '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;
}
},
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 (!utils.isES5Component(parent.parent)) {
return;
}
if (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 = ast.unwrapTSAsExpression(node.left);
const unwrappedRight = ast.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.
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(ast.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) && node.parent.type === 'CallExpression') {
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,14 @@
/**
* @fileoverview Prevent usage of setState in componentWillUpdate
* @author Yannick Croissant
*/
'use strict';
const makeNoMethodSetStateRule = require('../util/makeNoMethodSetStateRule');
const versionUtil = require('../util/version');
module.exports = makeNoMethodSetStateRule(
'componentWillUpdate',
(context) => versionUtil.testReactVersion(context, '16.3.0')
);

View file

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

View file

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

View file

@ -0,0 +1,386 @@
/**
* @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 Components = require('../util/Components');
const versionUtil = require('../util/version');
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
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: {
componentShouldBePure: 'Component should be written as a pure function'
},
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'
&& body[0].expression.type === 'CallExpression'
&& 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 construtor 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 && utils.isPureComponent(node)) {
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 = context.getSourceCode().getText(node.name);
if (name !== 'ref') {
return;
}
markRefAsUsed(node);
},
// Mark `render` that do not return some JSX
ReturnStatement(node) {
let blockNode;
let scope = context.getScope();
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 = versionUtil.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();
Object.keys(list).forEach((component) => {
if (
hasOtherProperties(list[component].node)
|| list[component].useThis
|| list[component].useRef
|| list[component].invalidReturn
|| list[component].hasChildContextTypes
|| list[component].useDecorators
|| (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node))
) {
return;
}
if (list[component].hasSCU) {
return;
}
context.report({
node: list[component].node,
messageId: 'componentShouldBePure'
});
});
}
};
})
};

View file

@ -0,0 +1,199 @@
/**
* @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/yannickcr/eslint-plugin-react/issues/7
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent missing props validation in a React component definition',
category: 'Best Practices',
recommended: true,
url: docsUrl('prop-types')
},
messages: {
missingPropType: '\'{{name}}\' is missing in props validation'
},
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 Boolean(
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) => {
context.report({
node: propType.node,
messageId: 'missingPropType',
data: {
name: propType.allNames.join('.').replace(/\.__COMPUTED_PROP__/g, '[]')
}
});
});
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
'Program:exit'() {
const list = components.list();
// Report undeclared proptypes for all classes
Object.keys(list).filter((component) => mustBeValidated(list[component])).forEach((component) => {
reportUndeclaredPropTypes(list[component]);
});
}
};
})
};

View file

@ -0,0 +1,54 @@
/**
* @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');
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent missing React when using JSX',
category: 'Possible Errors',
recommended: true,
url: docsUrl('react-in-jsx-scope')
},
messages: {
notInScope: '\'{{name}}\' must be in scope when using JSX'
},
schema: []
},
create(context) {
const pragma = pragmaUtil.getFromContext(context);
function checkIfReactIsInScope(node) {
const variables = variableUtil.variablesInScope(context);
if (variableUtil.findVariable(variables, pragma)) {
return;
}
context.report({
node,
messageId: 'notInScope',
data: {
name: pragma
}
});
}
return {
JSXOpeningElement: checkIfReactIsInScope,
JSXOpeningFragment: checkIfReactIsInScope
};
}
};

View file

@ -0,0 +1,109 @@
/**
* @fileOverview Enforce a defaultProps definition for every prop that is not a required prop.
* @author Vitor Balocco
*/
'use strict';
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const astUtil = require('../util/ast');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
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: {
noDefaultWithRequired: 'propType "{{name}}" is required and should not have a defaultProps declaration.',
shouldHaveDefault: 'propType "{{name}}" is not required, but has no corresponding defaultProps declaration.'
},
schema: [{
type: 'object',
properties: {
forbidDefaultForRequired: {
type: 'boolean'
},
ignoreFunctionalComponents: {
type: 'boolean'
}
},
additionalProperties: false
}]
},
create: Components.detect((context, components) => {
const configuration = context.options[0] || {};
const forbidDefaultForRequired = configuration.forbidDefaultForRequired || false;
const ignoreFunctionalComponents = configuration.ignoreFunctionalComponents || false;
/**
* 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) {
// 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 (defaultProps === 'unresolved') {
return;
}
Object.keys(propTypes).forEach((propName) => {
const prop = propTypes[propName];
if (prop.isRequired) {
if (forbidDefaultForRequired && defaultProps[propName]) {
context.report({
node: prop.node,
messageId: 'noDefaultWithRequired',
data: {name: propName}
});
}
return;
}
if (defaultProps[propName]) {
return;
}
context.report({
node: prop.node,
messageId: 'shouldHaveDefault',
data: {name: propName}
});
});
}
// --------------------------------------------------------------------------
// Public API
// --------------------------------------------------------------------------
return {
'Program:exit'() {
const list = components.list();
Object.keys(list).filter((component) => {
if (ignoreFunctionalComponents
&& (astUtil.isFunction(list[component].node) || astUtil.isFunctionLikeExpression(list[component].node))) {
return false;
}
return list[component].declaredPropTypes;
}).forEach((component) => {
reportPropTypesWithoutDefault(
list[component].declaredPropTypes,
list[component].defaultProps || {}
);
});
}
};
})
};

View file

@ -0,0 +1,230 @@
/**
* @fileoverview Enforce React components to have a shouldComponentUpdate method
* @author Evgueni Naverniouk
*/
'use strict';
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
module.exports = {
meta: {
docs: {
description: 'Enforce React components to have a shouldComponentUpdate method',
category: 'Best Practices',
recommended: false,
url: docsUrl('require-optimization')
},
messages: {
noShouldComponentUpdate: 'Component is not optimized. Please add a shouldComponentUpdate method.'
},
schema: [{
type: 'object',
properties: {
allowDecorators: {
type: 'array',
items: {
type: 'string'
}
}
},
additionalProperties: false
}]
},
create: Components.detect((context, components, utils) => {
const configuration = context.options[0] || {};
const allowDecorators = configuration.allowDecorators || [];
/**
* Checks to see if our component is decorated by PureRenderMixin via reactMixin
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean} True if node is decorated with a PureRenderMixin, false if not.
*/
function hasPureRenderDecorator(node) {
if (node.decorators && node.decorators.length) {
for (let i = 0, l = node.decorators.length; i < l; i++) {
if (
node.decorators[i].expression
&& node.decorators[i].expression.callee
&& node.decorators[i].expression.callee.object
&& node.decorators[i].expression.callee.object.name === 'reactMixin'
&& node.decorators[i].expression.callee.property
&& node.decorators[i].expression.callee.property.name === 'decorate'
&& node.decorators[i].expression.arguments
&& node.decorators[i].expression.arguments.length
&& node.decorators[i].expression.arguments[0].name === 'PureRenderMixin'
) {
return true;
}
}
}
return false;
}
/**
* Checks to see if our component is custom decorated
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean} True if node is decorated name with a custom decorated, false if not.
*/
function hasCustomDecorator(node) {
const allowLength = allowDecorators.length;
if (allowLength && node.decorators && node.decorators.length) {
for (let i = 0; i < allowLength; i++) {
for (let j = 0, l = node.decorators.length; j < l; j++) {
if (
node.decorators[j].expression
&& node.decorators[j].expression.name === allowDecorators[i]
) {
return true;
}
}
}
}
return false;
}
/**
* Checks if we are declaring a shouldComponentUpdate method
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean} True if we are declaring a shouldComponentUpdate method, false if not.
*/
function isSCUDeclared(node) {
return Boolean(
node
&& node.name === 'shouldComponentUpdate'
);
}
/**
* Checks if we are declaring a PureRenderMixin mixin
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean} True if we are declaring a PureRenderMixin method, false if not.
*/
function isPureRenderDeclared(node) {
let hasPR = false;
if (node.value && node.value.elements) {
for (let i = 0, l = node.value.elements.length; i < l; i++) {
if (node.value.elements[i] && node.value.elements[i].name === 'PureRenderMixin') {
hasPR = true;
break;
}
}
}
return Boolean(
node
&& node.key.name === 'mixins'
&& hasPR
);
}
/**
* Mark shouldComponentUpdate as declared
* @param {ASTNode} node The AST node being checked.
*/
function markSCUAsDeclared(node) {
components.set(node, {
hasSCU: true
});
}
/**
* Reports missing optimization for a given component
* @param {Object} component The component to process
*/
function reportMissingOptimization(component) {
context.report({
node: component.node,
messageId: 'noShouldComponentUpdate'
});
}
/**
* Checks if we are declaring function in class
* @returns {Boolean} True if we are declaring function in class, false if not.
*/
function isFunctionInClass() {
let blockNode;
let scope = context.getScope();
while (scope) {
blockNode = scope.block;
if (blockNode && blockNode.type === 'ClassDeclaration') {
return true;
}
scope = scope.upper;
}
return false;
}
return {
ArrowFunctionExpression(node) {
// Skip if the function is declared in the class
if (isFunctionInClass()) {
return;
}
// Stateless Functional Components cannot be optimized (yet)
markSCUAsDeclared(node);
},
ClassDeclaration(node) {
if (!(hasPureRenderDecorator(node) || hasCustomDecorator(node) || utils.isPureComponent(node))) {
return;
}
markSCUAsDeclared(node);
},
FunctionDeclaration(node) {
// Skip if the function is declared in the class
if (isFunctionInClass()) {
return;
}
// Stateless Functional Components cannot be optimized (yet)
markSCUAsDeclared(node);
},
FunctionExpression(node) {
// Skip if the function is declared in the class
if (isFunctionInClass()) {
return;
}
// Stateless Functional Components cannot be optimized (yet)
markSCUAsDeclared(node);
},
MethodDefinition(node) {
if (!isSCUDeclared(node.key)) {
return;
}
markSCUAsDeclared(node);
},
ObjectExpression(node) {
// Search for the shouldComponentUpdate declaration
const found = node.properties.some((property) => (
property.key
&& (isSCUDeclared(property.key) || isPureRenderDeclared(property))
));
if (found) {
markSCUAsDeclared(node);
}
},
'Program:exit'() {
const list = components.list();
// Report missing shouldComponentUpdate for all components
Object.keys(list).filter((component) => !list[component].hasSCU).forEach((component) => {
reportMissingOptimization(list[component]);
});
}
};
})
};

View file

@ -0,0 +1,98 @@
/**
* @fileoverview Enforce ES5 or ES6 class for returning value in render function.
* @author Mark Orel
*/
'use strict';
const Components = require('../util/Components');
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce ES5 or ES6 class for returning value in render function',
category: 'Possible Errors',
recommended: true,
url: docsUrl('require-render-return')
},
messages: {
noRenderReturn: 'Your render method should have a return statement'
},
schema: []
},
create: Components.detect((context, components, utils) => {
/**
* Mark a return statement as present
* @param {ASTNode} node The AST node being checked.
*/
function markReturnStatementPresent(node) {
components.set(node, {
hasReturnStatement: true
});
}
/**
* Find render method in a given AST node
* @param {ASTNode} node The component to find render method.
* @returns {ASTNode} Method node if found, undefined if not.
*/
function findRenderMethod(node) {
const properties = astUtil.getComponentProperties(node);
return properties
.filter((property) => astUtil.getPropertyName(property) === 'render' && property.value)
.find((property) => astUtil.isFunctionLikeExpression(property.value));
}
return {
ReturnStatement(node) {
const ancestors = context.getAncestors(node).reverse();
let depth = 0;
ancestors.forEach((ancestor) => {
if (/Function(Expression|Declaration)$/.test(ancestor.type)) {
depth++;
}
if (
/(MethodDefinition|(Class)?Property)$/.test(ancestor.type)
&& astUtil.getPropertyName(ancestor) === 'render'
&& depth <= 1
) {
markReturnStatementPresent(node);
}
});
},
ArrowFunctionExpression(node) {
if (node.expression === false || astUtil.getPropertyName(node.parent) !== 'render') {
return;
}
markReturnStatementPresent(node);
},
'Program:exit'() {
const list = components.list();
Object.keys(list).forEach((component) => {
if (
!findRenderMethod(list[component].node)
|| list[component].hasReturnStatement
|| (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node))
) {
return;
}
context.report({
node: findRenderMethod(list[component].node),
messageId: 'noRenderReturn'
});
});
}
};
})
};

View file

@ -0,0 +1,106 @@
/**
* @fileoverview Prevent extra closing tags for components without children
* @author Yannick Croissant
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const optionDefaults = {component: true, html: true};
module.exports = {
meta: {
docs: {
description: 'Prevent extra closing tags for components without children',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('self-closing-comp')
},
fixable: 'code',
messages: {
notSelfClosing: 'Empty components are self-closing'
},
schema: [{
type: 'object',
properties: {
component: {
default: optionDefaults.component,
type: 'boolean'
},
html: {
default: optionDefaults.html,
type: 'boolean'
}
},
additionalProperties: false
}]
},
create(context) {
function isComponent(node) {
return (
node.name
&& (node.name.type === 'JSXIdentifier' || node.name.type === 'JSXMemberExpression')
&& !jsxUtil.isDOMComponent(node)
);
}
function childrenIsEmpty(node) {
return node.parent.children.length === 0;
}
function childrenIsMultilineSpaces(node) {
const childrens = node.parent.children;
return (
childrens.length === 1
&& (childrens[0].type === 'Literal' || childrens[0].type === 'JSXText')
&& childrens[0].value.indexOf('\n') !== -1
&& childrens[0].value.replace(/(?!\xA0)\s/g, '') === ''
);
}
function isShouldBeSelfClosed(node) {
const configuration = Object.assign({}, optionDefaults, context.options[0]);
return (
(configuration.component && isComponent(node))
|| (configuration.html && jsxUtil.isDOMComponent(node))
) && !node.selfClosing && (childrenIsEmpty(node) || childrenIsMultilineSpaces(node));
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
JSXOpeningElement(node) {
if (!isShouldBeSelfClosed(node)) {
return;
}
context.report({
node,
messageId: 'notSelfClosing',
fix(fixer) {
// Represents the last character of the JSXOpeningElement, the '>' character
const openingElementEnding = node.range[1] - 1;
// Represents the last character of the JSXClosingElement, the '>' character
const closingElementEnding = node.parent.closingElement.range[1];
// Replace />.*<\/.*>/ with '/>'
const range = [openingElementEnding, closingElementEnding];
return fixer.replaceTextRange(range, ' />');
}
});
}
};
}
};

View file

@ -0,0 +1,444 @@
/**
* @fileoverview Enforce component methods order
* @author Yannick Croissant
*/
'use strict';
const has = require('has');
const entries = require('object.entries');
const arrayIncludes = require('array-includes');
const Components = require('../util/Components');
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
const defaultConfig = {
order: [
'static-methods',
'lifecycle',
'everything-else',
'render'
],
groups: {
lifecycle: [
'displayName',
'propTypes',
'contextTypes',
'childContextTypes',
'mixins',
'statics',
'defaultProps',
'constructor',
'getDefaultProps',
'state',
'getInitialState',
'getChildContext',
'getDerivedStateFromProps',
'componentWillMount',
'UNSAFE_componentWillMount',
'componentDidMount',
'componentWillReceiveProps',
'UNSAFE_componentWillReceiveProps',
'shouldComponentUpdate',
'componentWillUpdate',
'UNSAFE_componentWillUpdate',
'getSnapshotBeforeUpdate',
'componentDidUpdate',
'componentDidCatch',
'componentWillUnmount'
]
}
};
/**
* Get the methods order from the default config and the user config
* @param {Object} userConfig The user configuration.
* @returns {Array} Methods order
*/
function getMethodsOrder(userConfig) {
userConfig = userConfig || {};
const groups = Object.assign({}, defaultConfig.groups, userConfig.groups);
const order = userConfig.order || defaultConfig.order;
let config = [];
let entry;
for (let i = 0, j = order.length; i < j; i++) {
entry = order[i];
if (has(groups, entry)) {
config = config.concat(groups[entry]);
} else {
config.push(entry);
}
}
return config;
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce component methods order',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('sort-comp')
},
messages: {
unsortedProps: '{{propA}} should be placed {{position}} {{propB}}'
},
schema: [{
type: 'object',
properties: {
order: {
type: 'array',
items: {
type: 'string'
}
},
groups: {
type: 'object',
patternProperties: {
'^.*$': {
type: 'array',
items: {
type: 'string'
}
}
}
}
},
additionalProperties: false
}]
},
create: Components.detect((context, components) => {
const errors = {};
const methodsOrder = getMethodsOrder(context.options[0]);
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
const regExpRegExp = /\/(.*)\/([gimsuy]*)/;
/**
* Get indexes of the matching patterns in methods order configuration
* @param {Object} method - Method metadata.
* @returns {Array} The matching patterns indexes. Return [Infinity] if there is no match.
*/
function getRefPropIndexes(method) {
const methodGroupIndexes = [];
methodsOrder.forEach((currentGroup, groupIndex) => {
if (currentGroup === 'getters') {
if (method.getter) {
methodGroupIndexes.push(groupIndex);
}
} else if (currentGroup === 'setters') {
if (method.setter) {
methodGroupIndexes.push(groupIndex);
}
} else if (currentGroup === 'type-annotations') {
if (method.typeAnnotation) {
methodGroupIndexes.push(groupIndex);
}
} else if (currentGroup === 'static-variables') {
if (method.staticVariable) {
methodGroupIndexes.push(groupIndex);
}
} else if (currentGroup === 'static-methods') {
if (method.staticMethod) {
methodGroupIndexes.push(groupIndex);
}
} else if (currentGroup === 'instance-variables') {
if (method.instanceVariable) {
methodGroupIndexes.push(groupIndex);
}
} else if (currentGroup === 'instance-methods') {
if (method.instanceMethod) {
methodGroupIndexes.push(groupIndex);
}
} else if (arrayIncludes([
'displayName',
'propTypes',
'contextTypes',
'childContextTypes',
'mixins',
'statics',
'defaultProps',
'constructor',
'getDefaultProps',
'state',
'getInitialState',
'getChildContext',
'getDerivedStateFromProps',
'componentWillMount',
'UNSAFE_componentWillMount',
'componentDidMount',
'componentWillReceiveProps',
'UNSAFE_componentWillReceiveProps',
'shouldComponentUpdate',
'componentWillUpdate',
'UNSAFE_componentWillUpdate',
'getSnapshotBeforeUpdate',
'componentDidUpdate',
'componentDidCatch',
'componentWillUnmount',
'render'
], currentGroup)) {
if (currentGroup === method.name) {
methodGroupIndexes.push(groupIndex);
}
} else {
// Is the group a regex?
const isRegExp = currentGroup.match(regExpRegExp);
if (isRegExp) {
const isMatching = new RegExp(isRegExp[1], isRegExp[2]).test(method.name);
if (isMatching) {
methodGroupIndexes.push(groupIndex);
}
} else if (currentGroup === method.name) {
methodGroupIndexes.push(groupIndex);
}
}
});
// No matching pattern, return 'everything-else' index
if (methodGroupIndexes.length === 0) {
const everythingElseIndex = methodsOrder.indexOf('everything-else');
if (everythingElseIndex !== -1) {
methodGroupIndexes.push(everythingElseIndex);
} else {
// No matching pattern and no 'everything-else' group
methodGroupIndexes.push(Infinity);
}
}
return methodGroupIndexes;
}
/**
* Get properties name
* @param {Object} node - Property.
* @returns {String} Property name.
*/
function getPropertyName(node) {
if (node.kind === 'get') {
return 'getter functions';
}
if (node.kind === 'set') {
return 'setter functions';
}
return astUtil.getPropertyName(node);
}
/**
* Store a new error in the error list
* @param {Object} propA - Mispositioned property.
* @param {Object} propB - Reference property.
*/
function storeError(propA, propB) {
// Initialize the error object if needed
if (!errors[propA.index]) {
errors[propA.index] = {
node: propA.node,
score: 0,
closest: {
distance: Infinity,
ref: {
node: null,
index: 0
}
}
};
}
// Increment the prop score
errors[propA.index].score++;
// Stop here if we already have pushed another node at this position
if (getPropertyName(errors[propA.index].node) !== getPropertyName(propA.node)) {
return;
}
// Stop here if we already have a closer reference
if (Math.abs(propA.index - propB.index) > errors[propA.index].closest.distance) {
return;
}
// Update the closest reference
errors[propA.index].closest.distance = Math.abs(propA.index - propB.index);
errors[propA.index].closest.ref.node = propB.node;
errors[propA.index].closest.ref.index = propB.index;
}
/**
* Dedupe errors, only keep the ones with the highest score and delete the others
*/
function dedupeErrors() {
for (const i in errors) {
if (has(errors, i)) {
const index = errors[i].closest.ref.index;
if (errors[index]) {
if (errors[i].score > errors[index].score) {
delete errors[index];
} else {
delete errors[i];
}
}
}
}
}
/**
* Report errors
*/
function reportErrors() {
dedupeErrors();
entries(errors).forEach((entry) => {
const nodeA = entry[1].node;
const nodeB = entry[1].closest.ref.node;
const indexA = entry[0];
const indexB = entry[1].closest.ref.index;
context.report({
node: nodeA,
messageId: 'unsortedProps',
data: {
propA: getPropertyName(nodeA),
propB: getPropertyName(nodeB),
position: indexA < indexB ? 'before' : 'after'
}
});
});
}
/**
* Compare two properties and find out if they are in the right order
* @param {Array} propertiesInfos Array containing all the properties metadata.
* @param {Object} propA First property name and metadata
* @param {Object} propB Second property name.
* @returns {Object} Object containing a correct true/false flag and the correct indexes for the two properties.
*/
function comparePropsOrder(propertiesInfos, propA, propB) {
let i;
let j;
let k;
let l;
let refIndexA;
let refIndexB;
// Get references indexes (the correct position) for given properties
const refIndexesA = getRefPropIndexes(propA);
const refIndexesB = getRefPropIndexes(propB);
// Get current indexes for given properties
const classIndexA = propertiesInfos.indexOf(propA);
const classIndexB = propertiesInfos.indexOf(propB);
// Loop around the references indexes for the 1st property
for (i = 0, j = refIndexesA.length; i < j; i++) {
refIndexA = refIndexesA[i];
// Loop around the properties for the 2nd property (for comparison)
for (k = 0, l = refIndexesB.length; k < l; k++) {
refIndexB = refIndexesB[k];
if (
// Comparing the same properties
refIndexA === refIndexB
// 1st property is placed before the 2nd one in reference and in current component
|| ((refIndexA < refIndexB) && (classIndexA < classIndexB))
// 1st property is placed after the 2nd one in reference and in current component
|| ((refIndexA > refIndexB) && (classIndexA > classIndexB))
) {
return {
correct: true,
indexA: classIndexA,
indexB: classIndexB
};
}
}
}
// We did not find any correct match between reference and current component
return {
correct: false,
indexA: refIndexA,
indexB: refIndexB
};
}
/**
* Check properties order from a properties list and store the eventual errors
* @param {Array} properties Array containing all the properties.
*/
function checkPropsOrder(properties) {
const propertiesInfos = properties.map((node) => ({
name: getPropertyName(node),
getter: node.kind === 'get',
setter: node.kind === 'set',
staticVariable: node.static
&& node.type === 'ClassProperty'
&& (!node.value || !astUtil.isFunctionLikeExpression(node.value)),
staticMethod: node.static
&& (node.type === 'ClassProperty' || node.type === 'MethodDefinition')
&& node.value
&& (astUtil.isFunctionLikeExpression(node.value)),
instanceVariable: !node.static
&& node.type === 'ClassProperty'
&& (!node.value || !astUtil.isFunctionLikeExpression(node.value)),
instanceMethod: !node.static
&& node.type === 'ClassProperty'
&& node.value
&& (astUtil.isFunctionLikeExpression(node.value)),
typeAnnotation: !!node.typeAnnotation && node.value === null
}));
// Loop around the properties
propertiesInfos.forEach((propA, i) => {
// Loop around the properties a second time (for comparison)
propertiesInfos.forEach((propB, k) => {
if (i === k) {
return;
}
// Compare the properties order
const order = comparePropsOrder(propertiesInfos, propA, propB);
if (!order.correct) {
// Store an error if the order is incorrect
storeError({
node: properties[i],
index: order.indexA
}, {
node: properties[k],
index: order.indexB
});
}
});
});
}
return {
'Program:exit'() {
const list = components.list();
Object.keys(list).forEach((component) => {
const properties = astUtil.getComponentProperties(list[component].node);
checkPropsOrder(properties);
});
reportErrors();
}
};
}),
defaultConfig
};

View file

@ -0,0 +1,253 @@
/**
* @fileoverview Enforce propTypes declarations alphabetical sorting
*/
'use strict';
const variableUtil = require('../util/variable');
const propsUtil = require('../util/props');
const docsUrl = require('../util/docsUrl');
const propWrapperUtil = require('../util/propWrapper');
// const propTypesSortUtil = require('../util/propTypesSort');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce propTypes declarations alphabetical sorting',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('sort-prop-types')
},
// fixable: 'code',
messages: {
requiredPropsFirst: 'Required prop types must be listed before all other prop types',
callbackPropsLast: 'Callback prop types must be listed after all other prop types',
propsNotSorted: 'Prop types declarations should be sorted alphabetically'
},
schema: [{
type: 'object',
properties: {
requiredFirst: {
type: 'boolean'
},
callbacksLast: {
type: 'boolean'
},
ignoreCase: {
type: 'boolean'
},
// Whether alphabetical sorting should be enforced
noSortAlphabetically: {
type: 'boolean'
},
sortShapeProp: {
type: 'boolean'
}
},
additionalProperties: false
}]
},
create(context) {
const configuration = context.options[0] || {};
const requiredFirst = configuration.requiredFirst || false;
const callbacksLast = configuration.callbacksLast || false;
const ignoreCase = configuration.ignoreCase || false;
const noSortAlphabetically = configuration.noSortAlphabetically || false;
const sortShapeProp = configuration.sortShapeProp || false;
function getKey(node) {
if (node.key && node.key.value) {
return node.key.value;
}
return context.getSourceCode().getText(node.key || node.argument);
}
function getValueName(node) {
return node.type === 'Property' && node.value.property && node.value.property.name;
}
function isCallbackPropName(propName) {
return /^on[A-Z]/.test(propName);
}
function isRequiredProp(node) {
return getValueName(node) === 'isRequired';
}
function isShapeProp(node) {
return Boolean(
node && node.callee && node.callee.property && node.callee.property.name === 'shape'
);
}
function toLowerCase(item) {
return String(item).toLowerCase();
}
/**
* Checks if propTypes declarations are sorted
* @param {Array} declarations The array of AST nodes being checked.
* @returns {void}
*/
function checkSorted(declarations) {
// Declarations will be `undefined` if the `shape` is not a literal. For
// example, if it is a propType imported from another file.
if (!declarations) {
return;
}
// function fix(fixer) {
// return propTypesSortUtil.fixPropTypesSort(
// fixer,
// context,
// declarations,
// ignoreCase,
// requiredFirst,
// callbacksLast,
// sortShapeProp
// );
// }
declarations.reduce((prev, curr, idx, decls) => {
if (curr.type === 'ExperimentalSpreadProperty' || curr.type === 'SpreadElement') {
return decls[idx + 1];
}
let prevPropName = getKey(prev);
let currentPropName = getKey(curr);
const previousIsRequired = isRequiredProp(prev);
const currentIsRequired = isRequiredProp(curr);
const previousIsCallback = isCallbackPropName(prevPropName);
const currentIsCallback = isCallbackPropName(currentPropName);
if (ignoreCase) {
prevPropName = toLowerCase(prevPropName);
currentPropName = toLowerCase(currentPropName);
}
if (requiredFirst) {
if (previousIsRequired && !currentIsRequired) {
// Transition between required and non-required. Don't compare for alphabetical.
return curr;
}
if (!previousIsRequired && currentIsRequired) {
// Encountered a non-required prop after a required prop
context.report({
node: curr,
messageId: 'requiredPropsFirst'
// fix
});
return curr;
}
}
if (callbacksLast) {
if (!previousIsCallback && currentIsCallback) {
// Entering the callback prop section
return curr;
}
if (previousIsCallback && !currentIsCallback) {
// Encountered a non-callback prop after a callback prop
context.report({
node: prev,
messageId: 'callbackPropsLast'
// fix
});
return prev;
}
}
if (!noSortAlphabetically && currentPropName < prevPropName) {
context.report({
node: curr,
messageId: 'propsNotSorted'
// fix
});
return prev;
}
return curr;
}, declarations[0]);
}
function checkNode(node) {
switch (node && node.type) {
case 'ObjectExpression':
checkSorted(node.properties);
break;
case 'Identifier': {
const propTypesObject = variableUtil.findVariableByName(context, node.name);
if (propTypesObject && propTypesObject.properties) {
checkSorted(propTypesObject.properties);
}
break;
}
case 'CallExpression': {
const innerNode = node.arguments && node.arguments[0];
if (propWrapperUtil.isPropWrapperFunction(context, node.callee.name) && innerNode) {
checkNode(innerNode);
}
break;
}
default:
break;
}
}
return {
CallExpression(node) {
if (!sortShapeProp || !isShapeProp(node) || !(node.arguments && node.arguments[0])) {
return;
}
const firstArg = node.arguments[0];
if (firstArg.properties) {
checkSorted(firstArg.properties);
} else if (firstArg.type === 'Identifier') {
const variable = variableUtil.findVariableByName(context, firstArg.name);
if (variable && variable.properties) {
checkSorted(variable.properties);
}
}
},
ClassProperty(node) {
if (!propsUtil.isPropTypesDeclaration(node)) {
return;
}
checkNode(node.value);
},
MemberExpression(node) {
if (!propsUtil.isPropTypesDeclaration(node)) {
return;
}
checkNode(node.parent.right);
},
ObjectExpression(node) {
node.properties.forEach((property) => {
if (!property.key) {
return;
}
if (!propsUtil.isPropTypesDeclaration(property)) {
return;
}
if (property.value.type === 'ObjectExpression') {
checkSorted(property.value.properties);
}
});
}
};
}
};

View file

@ -0,0 +1,65 @@
/**
* @fileoverview Enforce the state initialization style to be either in a constructor or with a class property
* @author Kanitkorn Sujautra
*/
'use strict';
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'State initialization in an ES6 class component should be in a constructor',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('state-in-constructor')
},
messages: {
stateInitConstructor: 'State initialization should be in a constructor',
stateInitClassProp: 'State initialization should be in a class property'
},
schema: [{
enum: ['always', 'never']
}]
},
create: Components.detect((context, components, utils) => {
const option = context.options[0] || 'always';
return {
ClassProperty(node) {
if (
option === 'always'
&& !node.static
&& node.key.name === 'state'
&& utils.getParentES6Component()
) {
context.report({
node,
messageId: 'stateInitConstructor'
});
}
},
AssignmentExpression(node) {
if (
option === 'never'
&& utils.isStateMemberExpression(node.left)
&& utils.inConstructor()
&& utils.getParentES6Component()
) {
context.report({
node,
messageId: 'stateInitClassProp'
});
}
}
};
})
};

View file

@ -0,0 +1,178 @@
/**
* @fileoverview Defines where React component static properties should be positioned.
* @author Daniel Mason
*/
'use strict';
const fromEntries = require('object.fromentries');
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const astUtil = require('../util/ast');
const propsUtil = require('../util/props');
// ------------------------------------------------------------------------------
// Positioning Options
// ------------------------------------------------------------------------------
const STATIC_PUBLIC_FIELD = 'static public field';
const STATIC_GETTER = 'static getter';
const PROPERTY_ASSIGNMENT = 'property assignment';
const POSITION_SETTINGS = [STATIC_PUBLIC_FIELD, STATIC_GETTER, PROPERTY_ASSIGNMENT];
// ------------------------------------------------------------------------------
// Rule messages
// ------------------------------------------------------------------------------
const ERROR_MESSAGES = {
[STATIC_PUBLIC_FIELD]: 'notStaticClassProp',
[STATIC_GETTER]: 'notGetterClassFunc',
[PROPERTY_ASSIGNMENT]: 'declareOutsideClass'
};
// ------------------------------------------------------------------------------
// Properties to check
// ------------------------------------------------------------------------------
const propertiesToCheck = {
propTypes: propsUtil.isPropTypesDeclaration,
defaultProps: propsUtil.isDefaultPropsDeclaration,
childContextTypes: propsUtil.isChildContextTypesDeclaration,
contextTypes: propsUtil.isContextTypesDeclaration,
contextType: propsUtil.isContextTypeDeclaration,
displayName: (node) => propsUtil.isDisplayNameDeclaration(astUtil.getPropertyNameNode(node))
};
const classProperties = Object.keys(propertiesToCheck);
const schemaProperties = fromEntries(classProperties.map((property) => [property, {enum: POSITION_SETTINGS}]));
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Defines where React component static properties should be positioned.',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('static-property-placement')
},
fixable: null, // or 'code' or 'whitespace'
messages: {
notStaticClassProp: '\'{{name}}\' should be declared as a static class property.',
notGetterClassFunc: '\'{{name}}\' should be declared as a static getter class function.',
declareOutsideClass: '\'{{name}}\' should be declared outside the class body.'
},
schema: [
{enum: POSITION_SETTINGS},
{
type: 'object',
properties: schemaProperties,
additionalProperties: false
}
]
},
create: Components.detect((context, components, utils) => {
// variables should be defined here
const options = context.options;
const defaultCheckType = options[0] || STATIC_PUBLIC_FIELD;
const hasAdditionalConfig = options.length > 1;
const additionalConfig = hasAdditionalConfig ? options[1] : {};
// Set config
const config = fromEntries(classProperties.map((property) => [
property,
additionalConfig[property] || defaultCheckType
]));
// ----------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------
/**
* Checks if we are declaring context in class
* @returns {Boolean} True if we are declaring context in class, false if not.
*/
function isContextInClass() {
let blockNode;
let scope = context.getScope();
while (scope) {
blockNode = scope.block;
if (blockNode && blockNode.type === 'ClassDeclaration') {
return true;
}
scope = scope.upper;
}
return false;
}
/**
* Check if we should report this property node
* @param {ASTNode} node
* @param {string} expectedRule
*/
function reportNodeIncorrectlyPositioned(node, expectedRule) {
// Detect if this node is an expected property declaration adn return the property name
const name = classProperties.find((propertyName) => {
if (propertiesToCheck[propertyName](node)) {
return !!propertyName;
}
return false;
});
// If name is set but the configured rule does not match expected then report error
if (name && config[name] !== expectedRule) {
// Report the error
context.report({
node,
messageId: ERROR_MESSAGES[config[name]],
data: {name}
});
}
}
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return {
ClassProperty: (node) => {
if (!utils.getParentES6Component()) {
return;
}
reportNodeIncorrectlyPositioned(node, STATIC_PUBLIC_FIELD);
},
MemberExpression: (node) => {
// If definition type is undefined then it must not be a defining expression or if the definition is inside a
// class body then skip this node.
const right = node.parent.right;
if (!right || right.type === 'undefined' || isContextInClass()) {
return;
}
// Get the related component
const relatedComponent = utils.getRelatedComponent(node);
// If the related component is not an ES6 component then skip this node
if (!relatedComponent || !utils.isES6Component(relatedComponent.node)) {
return;
}
// Report if needed
reportNodeIncorrectlyPositioned(node, PROPERTY_ASSIGNMENT);
},
MethodDefinition: (node) => {
// If the function is inside a class and is static getter then check if correctly positioned
if (utils.getParentES6Component() && node.static && node.kind === 'get') {
// Report error if needed
reportNodeIncorrectlyPositioned(node, STATIC_GETTER);
}
}
};
})
};

View file

@ -0,0 +1,138 @@
/**
* @fileoverview Enforce style prop value is an object
* @author David Petersen
*/
'use strict';
const variableUtil = require('../util/variable');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce style prop value is an object',
category: 'Possible Errors',
recommended: false,
url: docsUrl('style-prop-object')
},
messages: {
stylePropNotObject: 'Style prop value must be an object'
},
schema: [
{
type: 'object',
properties: {
allow: {
type: 'array',
items: {
type: 'string'
},
additionalItems: false,
uniqueItems: true
}
}
}
]
},
create(context) {
const allowed = new Set(((context.options.length > 0) && context.options[0].allow) || []);
/**
* @param {ASTNode} expression An Identifier node
* @returns {boolean}
*/
function isNonNullaryLiteral(expression) {
return expression.type === 'Literal' && expression.value !== null;
}
/**
* @param {object} node A Identifier node
*/
function checkIdentifiers(node) {
const variable = variableUtil.variablesInScope(context).find((item) => item.name === node.name);
if (!variable || !variable.defs[0] || !variable.defs[0].node.init) {
return;
}
if (isNonNullaryLiteral(variable.defs[0].node.init)) {
context.report({
node,
messageId: 'stylePropNotObject'
});
}
}
return {
CallExpression(node) {
if (
node.callee
&& node.callee.type === 'MemberExpression'
&& node.callee.property.name === 'createElement'
&& node.arguments.length > 1
) {
if (node.arguments[0].name) {
// store name of component
const componentName = node.arguments[0].name;
// allowed list contains the name
if (allowed.has(componentName)) {
// abort operation
return;
}
}
if (node.arguments[1].type === 'ObjectExpression') {
const style = node.arguments[1].properties.find((property) => property.key && property.key.name === 'style' && !property.computed);
if (style) {
if (style.value.type === 'Identifier') {
checkIdentifiers(style.value);
} else if (isNonNullaryLiteral(style.value)) {
context.report({
node: style.value,
messageId: 'stylePropNotObject'
});
}
}
}
}
},
JSXAttribute(node) {
if (!node.value || node.name.name !== 'style') {
return;
}
// store parent element
const parentElement = node.parent;
// parent element is a JSXOpeningElement
if (parentElement && parentElement.type === 'JSXOpeningElement') {
// get the name of the JSX element
const name = parentElement.name && parentElement.name.name;
// allowed list contains the name
if (allowed.has(name)) {
// abort operation
return;
}
}
if (node.value.type !== 'JSXExpressionContainer' || isNonNullaryLiteral(node.value.expression)) {
context.report({
node,
messageId: 'stylePropNotObject'
});
} else if (node.value.expression.type === 'Identifier') {
checkIdentifiers(node.value.expression);
}
}
};
}
};

View file

@ -0,0 +1,166 @@
/**
* @fileoverview Prevent void elements (e.g. <img />, <br />) from receiving
* children
* @author Joe Lencioni
*/
'use strict';
const has = require('has');
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
// Using an object here to avoid array scan. We should switch to Set once
// support is good enough.
const VOID_DOM_ELEMENTS = {
area: true,
base: true,
br: true,
col: true,
embed: true,
hr: true,
img: true,
input: true,
keygen: true,
link: true,
menuitem: true,
meta: true,
param: true,
source: true,
track: true,
wbr: true
};
function isVoidDOMElement(elementName) {
return has(VOID_DOM_ELEMENTS, elementName);
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent passing of children to void DOM elements (e.g. `<br />`).',
category: 'Best Practices',
recommended: false,
url: docsUrl('void-dom-elements-no-children')
},
messages: {
noChildrenInVoidEl: 'Void DOM element <{{element}} /> cannot receive children.'
},
schema: []
},
create: Components.detect((context, components, utils) => ({
JSXElement(node) {
const elementName = node.openingElement.name.name;
if (!isVoidDOMElement(elementName)) {
// e.g. <div />
return;
}
if (node.children.length > 0) {
// e.g. <br>Foo</br>
context.report({
node,
messageId: 'noChildrenInVoidEl',
data: {
element: elementName
}
});
}
const attributes = node.openingElement.attributes;
const hasChildrenAttributeOrDanger = attributes.some((attribute) => {
if (!attribute.name) {
return false;
}
return attribute.name.name === 'children' || attribute.name.name === 'dangerouslySetInnerHTML';
});
if (hasChildrenAttributeOrDanger) {
// e.g. <br children="Foo" />
context.report({
node,
messageId: 'noChildrenInVoidEl',
data: {
element: elementName
}
});
}
},
CallExpression(node) {
if (node.callee.type !== 'MemberExpression' && node.callee.type !== 'Identifier') {
return;
}
if (!utils.isCreateElement(node)) {
return;
}
const args = node.arguments;
if (args.length < 1) {
// React.createElement() should not crash linter
return;
}
const elementName = args[0].value;
if (!isVoidDOMElement(elementName)) {
// e.g. React.createElement('div');
return;
}
if (args.length < 2 || args[1].type !== 'ObjectExpression') {
return;
}
const firstChild = args[2];
if (firstChild) {
// e.g. React.createElement('br', undefined, 'Foo')
context.report({
node,
messageId: 'noChildrenInVoidEl',
data: {
element: elementName
}
});
}
const props = args[1].properties;
const hasChildrenPropOrDanger = props.some((prop) => {
if (!prop.key) {
return false;
}
return prop.key.name === 'children' || prop.key.name === 'dangerouslySetInnerHTML';
});
if (hasChildrenPropOrDanger) {
// e.g. React.createElement('br', { children: 'Foo' })
context.report({
node,
messageId: 'noChildrenInVoidEl',
data: {
element: elementName
}
});
}
}
}))
};

30
web/node_modules/eslint-plugin-react/lib/types.d.ts generated vendored Normal file
View file

@ -0,0 +1,30 @@
import eslint from 'eslint';
import estree from 'estree';
declare global {
interface ASTNode extends estree.BaseNode {
[_: string]: any; // TODO: fixme
}
type Scope = eslint.Scope.Scope;
type Token = eslint.AST.Token;
type Fixer = eslint.Rule.RuleFixer;
type JSXAttribute = ASTNode;
type JSXElement = ASTNode;
type JSXFragment = ASTNode;
type JSXSpreadAttribute = ASTNode;
interface Context extends eslint.SourceCode {
getFirstTokens(node: estree.Node | ASTNode, options?: eslint.SourceCode.CursorWithCountOptions): eslint.AST.Token[];
}
type TypeDeclarationBuilder = (annotation: ASTNode, parentName: string, seen: Set<typeof annotation>) => object;
type TypeDeclarationBuilders = {
[k in string]: TypeDeclarationBuilder;
};
type UnionTypeDefinition = {
type: 'union' | 'shape';
children: unknown[];
};
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,32 @@
/**
* @fileoverview Utility functions for type annotation detection.
* @author Yannick Croissant
* @author Vitor Balocco
*/
'use strict';
/**
* Checks if we are declaring a `props` argument with a flow type annotation.
* @param {ASTNode} node The AST node being checked.
* @param {Object} context
* @returns {Boolean} True if the node is a type annotated props declaration, false if not.
*/
function isAnnotatedFunctionPropsDeclaration(node, context) {
if (!node || !node.params || !node.params.length) {
return false;
}
const typeNode = node.params[0].type === 'AssignmentPattern' ? node.params[0].left : node.params[0];
const tokens = context.getFirstTokens(typeNode, 2);
const isAnnotated = typeNode.typeAnnotation;
const isDestructuredProps = typeNode.type === 'ObjectPattern';
const isProps = tokens[0].value === 'props' || (tokens[1] && tokens[1].value === 'props');
return (isAnnotated && (isDestructuredProps || isProps));
}
module.exports = {
isAnnotatedFunctionPropsDeclaration
};

294
web/node_modules/eslint-plugin-react/lib/util/ast.js generated vendored Normal file
View file

@ -0,0 +1,294 @@
/**
* @fileoverview Utility functions for AST
*/
'use strict';
/**
* Find a return statment in the current node
*
* @param {ASTNode} node The AST node being checked
* @returns {ASTNode | false}
*/
function findReturnStatement(node) {
if (
(!node.value || !node.value.body || !node.value.body.body)
&& (!node.body || !node.body.body)
) {
return false;
}
const bodyNodes = (node.value ? node.value.body.body : node.body.body);
return (function loopNodes(nodes) {
let i = nodes.length - 1;
for (; i >= 0; i--) {
if (nodes[i].type === 'ReturnStatement') {
return nodes[i];
}
if (nodes[i].type === 'SwitchStatement') {
let j = nodes[i].cases.length - 1;
for (; j >= 0; j--) {
return loopNodes(nodes[i].cases[j].consequent);
}
}
}
return false;
}(bodyNodes));
}
/**
* Get node with property's name
* @param {Object} node - Property.
* @returns {Object} Property name node.
*/
function getPropertyNameNode(node) {
if (node.key || ['MethodDefinition', 'Property'].indexOf(node.type) !== -1) {
return node.key;
}
if (node.type === 'MemberExpression') {
return node.property;
}
return null;
}
/**
* Get properties name
* @param {Object} node - Property.
* @returns {String} Property name.
*/
function getPropertyName(node) {
const nameNode = getPropertyNameNode(node);
return nameNode ? nameNode.name : '';
}
/**
* Get properties for a given AST node
* @param {ASTNode} node The AST node being checked.
* @returns {Array} Properties array.
*/
function getComponentProperties(node) {
switch (node.type) {
case 'ClassDeclaration':
case 'ClassExpression':
return node.body.body;
case 'ObjectExpression':
return node.properties;
default:
return [];
}
}
/**
* Gets the first node in a line from the initial node, excluding whitespace.
* @param {Object} context The node to check
* @param {ASTNode} node The node to check
* @return {ASTNode} the first node in the line
*/
function getFirstNodeInLine(context, node) {
const sourceCode = context.getSourceCode();
let token = node;
let lines;
do {
token = sourceCode.getTokenBefore(token);
lines = token.type === 'JSXText'
? token.value.split('\n')
: null;
} while (
token.type === 'JSXText'
&& /^\s*$/.test(lines[lines.length - 1])
);
return token;
}
/**
* Checks if the node is the first in its line, excluding whitespace.
* @param {Object} context The node to check
* @param {ASTNode} node The node to check
* @return {Boolean} true if it's the first node in its line
*/
function isNodeFirstInLine(context, node) {
const token = getFirstNodeInLine(context, node);
const startLine = node.loc.start.line;
const endLine = token ? token.loc.end.line : -1;
return startLine !== endLine;
}
/**
* Checks if the node is a function or arrow function expression.
* @param {ASTNode} node The node to check
* @return {Boolean} true if it's a function-like expression
*/
function isFunctionLikeExpression(node) {
return node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression';
}
/**
* Checks if the node is a function.
* @param {ASTNode} node The node to check
* @return {Boolean} true if it's a function
*/
function isFunction(node) {
return node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration';
}
/**
* Checks if the node is a class.
* @param {ASTNode} node The node to check
* @return {Boolean} true if it's a class
*/
function isClass(node) {
return node.type === 'ClassDeclaration' || node.type === 'ClassExpression';
}
/**
* Removes quotes from around an identifier.
* @param {string} string the identifier to strip
* @returns {string}
*/
function stripQuotes(string) {
return string.replace(/^'|'$/g, '');
}
/**
* Retrieve the name of a key node
* @param {Context} context The AST node with the key.
* @param {ASTNode} node The AST node with the key.
* @return {string | undefined} the name of the key
*/
function getKeyValue(context, node) {
if (node.type === 'ObjectTypeProperty') {
const tokens = context.getFirstTokens(node, 2);
return (tokens[0].value === '+' || tokens[0].value === '-'
? tokens[1].value
: stripQuotes(tokens[0].value)
);
}
if (node.type === 'GenericTypeAnnotation') {
return node.id.name;
}
if (node.type === 'ObjectTypeAnnotation') {
return;
}
const key = node.key || node.argument;
if (!key) {
return;
}
return key.type === 'Identifier' ? key.name : key.value;
}
/**
* Checks if a node is being assigned a value: props.bar = 'bar'
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean}
*/
function isAssignmentLHS(node) {
return (
node.parent
&& node.parent.type === 'AssignmentExpression'
&& node.parent.left === node
);
}
/**
* Extracts the expression node that is wrapped inside a TS type assertion
*
* @param {ASTNode} node - potential TS node
* @returns {ASTNode} - unwrapped expression node
*/
function unwrapTSAsExpression(node) {
if (node && node.type === 'TSAsExpression') return node.expression;
return node;
}
function isTSTypeReference(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSTypeReference';
}
function isTSTypeAnnotation(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSTypeAnnotation';
}
function isTSTypeLiteral(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSTypeLiteral';
}
function isTSIntersectionType(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSIntersectionType';
}
function isTSInterfaceHeritage(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSInterfaceHeritage';
}
function isTSInterfaceDeclaration(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSInterfaceDeclaration';
}
function isTSTypeAliasDeclaration(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSTypeAliasDeclaration';
}
function isTSParenthesizedType(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSTypeAliasDeclaration';
}
function isTSFunctionType(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSFunctionType';
}
function isTSTypeQuery(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSTypeQuery';
}
function isTSTypeParameterInstantiation(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSTypeParameterInstantiation';
}
module.exports = {
findReturnStatement,
getFirstNodeInLine,
getPropertyName,
getPropertyNameNode,
getComponentProperties,
getKeyValue,
isAssignmentLHS,
isClass,
isFunction,
isFunctionLikeExpression,
isNodeFirstInLine,
unwrapTSAsExpression,
isTSTypeReference,
isTSTypeAnnotation,
isTSTypeLiteral,
isTSIntersectionType,
isTSInterfaceHeritage,
isTSInterfaceDeclaration,
isTSTypeAliasDeclaration,
isTSParenthesizedType,
isTSFunctionType,
isTSTypeQuery,
isTSTypeParameterInstantiation
};

View file

@ -0,0 +1,267 @@
/**
* @fileoverview Common defaultProps detection functionality.
*/
'use strict';
const fromEntries = require('object.fromentries');
const astUtil = require('./ast');
const propsUtil = require('./props');
const variableUtil = require('./variable');
const propWrapperUtil = require('./propWrapper');
const QUOTES_REGEX = /^["']|["']$/g;
module.exports = function defaultPropsInstructions(context, components, utils) {
const sourceCode = context.getSourceCode();
/**
* Try to resolve the node passed in to a variable in the current scope. If the node passed in is not
* an Identifier, then the node is simply returned.
* @param {ASTNode} node The node to resolve.
* @returns {ASTNode|null} Return null if the value could not be resolved, ASTNode otherwise.
*/
function resolveNodeValue(node) {
if (node.type === 'Identifier') {
return variableUtil.findVariableByName(context, node.name);
}
if (
node.type === 'CallExpression'
&& propWrapperUtil.isPropWrapperFunction(context, node.callee.name)
&& node.arguments && node.arguments[0]
) {
return resolveNodeValue(node.arguments[0]);
}
return node;
}
/**
* Extracts a DefaultProp from an ObjectExpression node.
* @param {ASTNode} objectExpression ObjectExpression node.
* @returns {Object|string} Object representation of a defaultProp, to be consumed by
* `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps
* from this ObjectExpression can't be resolved.
*/
function getDefaultPropsFromObjectExpression(objectExpression) {
const hasSpread = objectExpression.properties.find((property) => property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement');
if (hasSpread) {
return 'unresolved';
}
return objectExpression.properties.map((defaultProp) => ({
name: sourceCode.getText(defaultProp.key).replace(QUOTES_REGEX, ''),
node: defaultProp
}));
}
/**
* Marks a component's DefaultProps declaration as "unresolved". A component's DefaultProps is
* marked as "unresolved" if we cannot safely infer the values of its defaultProps declarations
* without risking false negatives.
* @param {Object} component The component to mark.
* @returns {void}
*/
function markDefaultPropsAsUnresolved(component) {
components.set(component.node, {
defaultProps: 'unresolved'
});
}
/**
* Adds defaultProps to the component passed in.
* @param {ASTNode} component The component to add the defaultProps to.
* @param {Object[]|'unresolved'} defaultProps defaultProps to add to the component or the string "unresolved"
* if this component has defaultProps that can't be resolved.
* @returns {void}
*/
function addDefaultPropsToComponent(component, defaultProps) {
// Early return if this component's defaultProps is already marked as "unresolved".
if (component.defaultProps === 'unresolved') {
return;
}
if (defaultProps === 'unresolved') {
markDefaultPropsAsUnresolved(component);
return;
}
const defaults = component.defaultProps || {};
const newDefaultProps = Object.assign(
{},
defaults,
fromEntries(defaultProps.map((prop) => [prop.name, prop]))
);
components.set(component.node, {
defaultProps: newDefaultProps
});
}
return {
MemberExpression(node) {
const isDefaultProp = propsUtil.isDefaultPropsDeclaration(node);
if (!isDefaultProp) {
return;
}
// find component this defaultProps belongs to
const component = utils.getRelatedComponent(node);
if (!component) {
return;
}
// e.g.:
// MyComponent.propTypes = {
// foo: React.PropTypes.string.isRequired,
// bar: React.PropTypes.string
// };
//
// or:
//
// MyComponent.propTypes = myPropTypes;
if (node.parent.type === 'AssignmentExpression') {
const expression = resolveNodeValue(node.parent.right);
if (!expression || expression.type !== 'ObjectExpression') {
// If a value can't be found, we mark the defaultProps declaration as "unresolved", because
// we should ignore this component and not report any errors for it, to avoid false-positives
// with e.g. external defaultProps declarations.
if (isDefaultProp) {
markDefaultPropsAsUnresolved(component);
}
return;
}
addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
return;
}
// e.g.:
// MyComponent.propTypes.baz = React.PropTypes.string;
if (node.parent.type === 'MemberExpression' && node.parent.parent
&& node.parent.parent.type === 'AssignmentExpression') {
addDefaultPropsToComponent(component, [{
name: node.parent.property.name,
node: node.parent.parent
}]);
}
},
// e.g.:
// class Hello extends React.Component {
// static get defaultProps() {
// return {
// name: 'Dean'
// };
// }
// render() {
// return <div>Hello {this.props.name}</div>;
// }
// }
MethodDefinition(node) {
if (!node.static || node.kind !== 'get') {
return;
}
if (!propsUtil.isDefaultPropsDeclaration(node)) {
return;
}
// find component this propTypes/defaultProps belongs to
const component = components.get(utils.getParentES6Component());
if (!component) {
return;
}
const returnStatement = utils.findReturnStatement(node);
if (!returnStatement) {
return;
}
const expression = resolveNodeValue(returnStatement.argument);
if (!expression || expression.type !== 'ObjectExpression') {
return;
}
addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
},
// e.g.:
// class Greeting extends React.Component {
// render() {
// return (
// <h1>Hello, {this.props.foo} {this.props.bar}</h1>
// );
// }
// static defaultProps = {
// foo: 'bar',
// bar: 'baz'
// };
// }
ClassProperty(node) {
if (!(node.static && node.value)) {
return;
}
const propName = astUtil.getPropertyName(node);
const isDefaultProp = propName === 'defaultProps' || propName === 'getDefaultProps';
if (!isDefaultProp) {
return;
}
// find component this propTypes/defaultProps belongs to
const component = components.get(utils.getParentES6Component());
if (!component) {
return;
}
const expression = resolveNodeValue(node.value);
if (!expression || expression.type !== 'ObjectExpression') {
return;
}
addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
},
// e.g.:
// React.createClass({
// render: function() {
// return <div>{this.props.foo}</div>;
// },
// getDefaultProps: function() {
// return {
// foo: 'default'
// };
// }
// });
ObjectExpression(node) {
// find component this propTypes/defaultProps belongs to
const component = utils.isES5Component(node) && components.get(node);
if (!component) {
return;
}
// Search for the proptypes declaration
node.properties.forEach((property) => {
if (property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement') {
return;
}
const isDefaultProp = propsUtil.isDefaultPropsDeclaration(property);
if (isDefaultProp && property.value.type === 'FunctionExpression') {
const returnStatement = utils.findReturnStatement(property);
if (!returnStatement || returnStatement.argument.type !== 'ObjectExpression') {
return;
}
addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(returnStatement.argument));
}
});
}
};
};

View file

@ -0,0 +1,7 @@
'use strict';
function docsUrl(ruleName) {
return `https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules/${ruleName}.md`;
}
module.exports = docsUrl;

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