init source
This commit is contained in:
+2756
File diff suppressed because it is too large
Load Diff
+22
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Yannick Croissant
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
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] [![Coverage Status][coverage-image]][coverage-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#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"
|
||||
"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"}
|
||||
],
|
||||
"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
|
||||
|
||||
* [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): Prevent extraneous defaultProps on components
|
||||
* [react/destructuring-assignment](docs/rules/destructuring-assignment.md): Rule enforces consistent usage of destructuring assignment in component
|
||||
* [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-prop-types](docs/rules/forbid-prop-types.md): Forbid certain propTypes
|
||||
* [react/forbid-foreign-prop-types](docs/rules/forbid-foreign-prop-types.md): Forbid foreign propTypes
|
||||
* [react/no-access-state-in-setstate](docs/rules/no-access-state-in-setstate.md): Prevent using this.state inside this.setState
|
||||
* [react/no-array-index-key](docs/rules/no-array-index-key.md): Prevent using Array index in `key` props
|
||||
* [react/no-children-prop](docs/rules/no-children-prop.md): Prevent passing children as props
|
||||
* [react/no-danger](docs/rules/no-danger.md): Prevent usage of dangerous JSX properties
|
||||
* [react/no-danger-with-children](docs/rules/no-danger-with-children.md): Prevent problem with children and props.dangerouslySetInnerHTML
|
||||
* [react/no-deprecated](docs/rules/no-deprecated.md): Prevent usage of deprecated methods, including component lifecycle 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): Prevent usage of `shouldComponentUpdate` when extending React.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-typos](docs/rules/no-typos.md): Prevent common casing typos
|
||||
* [react/no-string-refs](docs/rules/no-string-refs.md): Prevent using string references in `ref` attribute.
|
||||
* [react/no-this-in-sfc](docs/rules/no-this-in-sfc.md): Prevent using `this` in stateless functional components
|
||||
* [react/no-unescaped-entities](docs/rules/no-unescaped-entities.md): Prevent invalid characters from appearing in markup
|
||||
* [react/no-unknown-property](docs/rules/no-unknown-property.md): Prevent usage of unknown DOM property (fixable)
|
||||
* [react/no-unsafe](docs/rules/no-unsafe.md): Prevent usage of unsafe lifecycle methods
|
||||
* [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 definitions of unused state properties
|
||||
* [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): Enforce that props are read-only
|
||||
* [react/prefer-stateless-function](docs/rules/prefer-stateless-function.md): Enforce stateless React 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 (fixable)
|
||||
* [react/sort-comp](docs/rules/sort-comp.md): Enforce component methods order (fixable)
|
||||
* [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): Enforce the state initialization style to be either in a constructor or with a class property
|
||||
* [react/static-property-placement](docs/rules/static-property-placement.md): Enforces where React component static properties should be positioned.
|
||||
* [react/style-prop-object](docs/rules/style-prop-object.md): Enforce style prop value being an object
|
||||
* [react/void-dom-elements-no-children](docs/rules/void-dom-elements-no-children.md): Prevent void DOM elements (e.g. `<img />`, `<br />`) from receiving children
|
||||
|
||||
## JSX-specific rules
|
||||
|
||||
* [react/jsx-boolean-value](docs/rules/jsx-boolean-value.md): Enforce boolean attributes notation in JSX (fixable)
|
||||
* [react/jsx-child-element-spacing](docs/rules/jsx-child-element-spacing.md): Enforce or disallow spaces inside of curly braces in JSX attributes and expressions.
|
||||
* [react/jsx-closing-bracket-location](docs/rules/jsx-closing-bracket-location.md): Validate closing bracket location in JSX (fixable)
|
||||
* [react/jsx-closing-tag-location](docs/rules/jsx-closing-tag-location.md): Validate closing tag location in JSX (fixable)
|
||||
* [react/jsx-curly-newline](docs/rules/jsx-curly-newline.md): Enforce or disallow newlines inside of curly braces in JSX attributes and expressions (fixable)
|
||||
* [react/jsx-curly-spacing](docs/rules/jsx-curly-spacing.md): Enforce or disallow spaces inside of curly braces in JSX attributes and expressions (fixable)
|
||||
* [react/jsx-equals-spacing](docs/rules/jsx-equals-spacing.md): Enforce or disallow spaces around equal signs in JSX attributes (fixable)
|
||||
* [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): Enforce position of the first prop in JSX (fixable)
|
||||
* [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 (fixable)
|
||||
* [react/jsx-indent-props](docs/rules/jsx-indent-props.md): Validate props indentation in JSX (fixable)
|
||||
* [react/jsx-key](docs/rules/jsx-key.md): Validate JSX has key prop when in array or iterator
|
||||
* [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 (fixable)
|
||||
* [react/jsx-no-bind](docs/rules/jsx-no-bind.md): Prevent usage of `.bind()` and arrow functions in JSX props
|
||||
* [react/jsx-no-comment-textnodes](docs/rules/jsx-no-comment-textnodes.md): Prevent comments from being inserted as text nodes
|
||||
* [react/jsx-no-duplicate-props](docs/rules/jsx-no-duplicate-props.md): Prevent duplicate props in JSX
|
||||
* [react/jsx-no-literals](docs/rules/jsx-no-literals.md): Prevent usage of unwrapped JSX strings
|
||||
* [react/jsx-no-target-blank](docs/rules/jsx-no-target-blank.md): Prevent usage of unsafe `target='_blank'`
|
||||
* [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 unnescessary fragments (fixable)
|
||||
* [react/jsx-one-expression-per-line](docs/rules/jsx-one-expression-per-line.md): Limit to one expression per line in JSX
|
||||
* [react/jsx-curly-brace-presence](docs/rules/jsx-curly-brace-presence.md): Enforce curly braces or disallow unnecessary curly braces in JSX
|
||||
* [react/jsx-fragments](docs/rules/jsx-fragments.md): Enforce shorthand or standard form for React fragments
|
||||
* [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 (fixable)
|
||||
* [react/jsx-props-no-spreading](docs/rules/jsx-props-no-spreading.md): Disallow JSX props 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 (fixable)
|
||||
* [react/jsx-space-before-closing](docs/rules/jsx-space-before-closing.md): Validate spacing before closing bracket in JSX (fixable)
|
||||
* [react/jsx-tag-spacing](docs/rules/jsx-tag-spacing.md): Validate whitespace in and around the JSX opening and closing brackets (fixable)
|
||||
* [react/jsx-uses-react](docs/rules/jsx-uses-react.md): Prevent React to be incorrectly marked as unused
|
||||
* [react/jsx-uses-vars](docs/rules/jsx-uses-vars.md): Prevent variables used in JSX to be incorrectly marked as unused
|
||||
* [react/jsx-wrap-multilines](docs/rules/jsx-wrap-multilines.md): Prevent missing parentheses around multilines JSX (fixable)
|
||||
|
||||
## Other useful plugins
|
||||
|
||||
- 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.
|
||||
|
||||
The rules enabled in this configuration are:
|
||||
|
||||
* [react/display-name](docs/rules/display-name.md)
|
||||
* [react/jsx-key](docs/rules/jsx-key.md)
|
||||
* [react/jsx-no-comment-textnodes](docs/rules/jsx-no-comment-textnodes.md)
|
||||
* [react/jsx-no-duplicate-props](docs/rules/jsx-no-duplicate-props.md)
|
||||
* [react/jsx-no-target-blank](docs/rules/jsx-no-target-blank.md)
|
||||
* [react/jsx-no-undef](docs/rules/jsx-no-undef.md)
|
||||
* [react/jsx-uses-react](docs/rules/jsx-uses-react.md)
|
||||
* [react/jsx-uses-vars](docs/rules/jsx-uses-vars.md)
|
||||
* [react/no-children-prop](docs/rules/no-children-prop.md)
|
||||
* [react/no-danger-with-children](docs/rules/no-danger-with-children.md)
|
||||
* [react/no-deprecated](docs/rules/no-deprecated.md)
|
||||
* [react/no-direct-mutation-state](docs/rules/no-direct-mutation-state.md)
|
||||
* [react/no-find-dom-node](docs/rules/no-find-dom-node.md)
|
||||
* [react/no-is-mounted](docs/rules/no-is-mounted.md)
|
||||
* [react/no-render-return-value](docs/rules/no-render-return-value.md)
|
||||
* [react/no-string-refs](docs/rules/no-string-refs.md)
|
||||
* [react/no-unescaped-entities](docs/rules/no-unescaped-entities.md)
|
||||
* [react/no-unknown-property](docs/rules/no-unknown-property.md)
|
||||
* [react/prop-types](docs/rules/prop-types.md)
|
||||
* [react/react-in-jsx-scope](docs/rules/react-in-jsx-scope.md)
|
||||
* [react/require-render-return](docs/rules/require-render-return.md)
|
||||
|
||||
## 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
|
||||
|
||||
[coverage-url]: https://coveralls.io/r/yannickcr/eslint-plugin-react?branch=master
|
||||
[coverage-image]: https://img.shields.io/coveralls/yannickcr/eslint-plugin-react/master.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
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
'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'),
|
||||
'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-no-bind': require('./lib/rules/jsx-no-bind'),
|
||||
'jsx-no-comment-textnodes': require('./lib/rules/jsx-no-comment-textnodes'),
|
||||
'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'),
|
||||
'jsx-no-literals': require('./lib/rules/jsx-no-literals'),
|
||||
'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-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-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
|
||||
}
|
||||
}
|
||||
};
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
/**
|
||||
* @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
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
category: 'Stylistic Issues',
|
||||
description: 'Enforces consistent naming for boolean props',
|
||||
recommended: false,
|
||||
url: docsUrl('boolean-prop-naming')
|
||||
},
|
||||
|
||||
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.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.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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)) {
|
||||
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({
|
||||
node: propNode,
|
||||
message: config.message || 'Prop name ({{ propName }}) doesn\'t match rule ({{ pattern }})',
|
||||
data: {
|
||||
component: propName,
|
||||
propName,
|
||||
pattern: config.rule
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkPropWrapperArguments(node, args) {
|
||||
if (!node || !Array.isArray(args)) {
|
||||
return;
|
||||
}
|
||||
args.filter(arg => arg.type === 'ObjectExpression').forEach(object => validatePropNaming(node, object.properties));
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 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);
|
||||
}
|
||||
},
|
||||
|
||||
'Program:exit': function () {
|
||||
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.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;
|
||||
}
|
||||
if (propType) {
|
||||
validatePropNaming(list[component].node, propType.properties);
|
||||
}
|
||||
}
|
||||
|
||||
if (list[component].invalidProps && list[component].invalidProps.length > 0) {
|
||||
reportInvalidNaming(list[component]);
|
||||
}
|
||||
});
|
||||
|
||||
// Reset cache
|
||||
objectTypeAnnotations.clear();
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
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,
|
||||
message: 'Missing an explicit type attribute for button'
|
||||
});
|
||||
}
|
||||
|
||||
function checkValue(node, value, quoteFn) {
|
||||
const q = quoteFn || (x => `"${x}"`);
|
||||
if (!(value in configuration)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `${q(value)} is an invalid value for button type attribute`
|
||||
});
|
||||
} else if (!configuration[value]) {
|
||||
context.report({
|
||||
node,
|
||||
message: `${q(value)} is a forbidden value for button type attribute`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
JSXElement(node) {
|
||||
if (node.openingElement.name.name !== 'button') {
|
||||
return;
|
||||
}
|
||||
|
||||
const typeProp = getProp(node.openingElement.attributes, 'type');
|
||||
|
||||
if (!typeProp) {
|
||||
reportMissing(node);
|
||||
return;
|
||||
}
|
||||
|
||||
const propValue = getLiteralPropValue(typeProp);
|
||||
if (!propValue && typeProp.value && typeProp.value.expression) {
|
||||
checkValue(node, typeProp.value.expression.name, x => `\`${x}\``);
|
||||
} else {
|
||||
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 || typeProp.value.type !== 'Literal') {
|
||||
reportMissing(node);
|
||||
return;
|
||||
}
|
||||
|
||||
checkValue(node, typeProp.value.value);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
|
||||
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,
|
||||
message: 'defaultProp "{{name}}" defined for isRequired propType.',
|
||||
data: {name: defaultPropName}
|
||||
});
|
||||
} else {
|
||||
context.report({
|
||||
node: defaultProp.node,
|
||||
message: 'defaultProp "{{name}}" has no corresponding propTypes declaration.',
|
||||
data: {name: defaultPropName}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public API
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
'Program:exit': function () {
|
||||
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 || {}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* @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';
|
||||
|
||||
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')
|
||||
},
|
||||
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;
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node We expect either an ArrowFunctionExpression,
|
||||
* FunctionDeclaration, or FunctionExpression
|
||||
*/
|
||||
function handleStatelessComponent(node) {
|
||||
const destructuringProps = node.params && node.params[0] && node.params[0].type === 'ObjectPattern';
|
||||
const destructuringContext = node.params && node.params[1] && node.params[1].type === 'ObjectPattern';
|
||||
|
||||
if (destructuringProps && components.get(node) && configuration === 'never') {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Must never use destructuring props assignment in SFC argument'
|
||||
});
|
||||
} else if (destructuringContext && components.get(node) && configuration === 'never') {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Must never use destructuring context assignment in SFC argument'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleSFCUsage(node) {
|
||||
// props.aProp || context.aProp
|
||||
const isPropUsed = (node.object.name === 'props' || node.object.name === 'context') && !isAssignmentLHS(node);
|
||||
if (isPropUsed && configuration === 'always') {
|
||||
context.report({
|
||||
node,
|
||||
message: `Must use destructuring ${node.object.name} assignment`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
message: `Must use destructuring ${node.object.property.name} assignment`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
FunctionDeclaration: handleStatelessComponent,
|
||||
|
||||
ArrowFunctionExpression: handleStatelessComponent,
|
||||
|
||||
FunctionExpression: handleStatelessComponent,
|
||||
|
||||
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,
|
||||
message: `Must never use destructuring ${node.init.name} assignment`
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
classComponent && destructuringClass && configuration === 'never' &&
|
||||
!(ignoreClassFields && node.parent.type === 'ClassProperty')
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Must never use destructuring ${node.init.property.name} assignment`
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
|
||||
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;
|
||||
|
||||
const MISSING_MESSAGE = 'Component definition is missing display name';
|
||||
|
||||
/**
|
||||
* 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,
|
||||
message: MISSING_MESSAGE,
|
||||
data: {
|
||||
component: component.name
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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': function () {
|
||||
const list = components.list();
|
||||
// Report missing display name for all components
|
||||
Object.keys(list).filter(component => !list[component].hasDisplayName).forEach((component) => {
|
||||
reportMissingDisplayName(list[component]);
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
},
|
||||
|
||||
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 whitelist = typeof value === 'string' ? [] : (value.allowedFor || []);
|
||||
return [propName, whitelist];
|
||||
}));
|
||||
|
||||
function isForbidden(prop, tagName) {
|
||||
const whitelist = forbid.get(prop);
|
||||
// if the tagName is undefined (`<this.something>`), we assume it's a forbidden element
|
||||
return typeof whitelist !== 'undefined' && (typeof tagName === 'undefined' || whitelist.indexOf(tagName) === -1);
|
||||
}
|
||||
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
const tag = node.parent.name.name;
|
||||
if (tag && tag[0] !== tag[0].toUpperCase()) {
|
||||
// This is a DOM node, not a Component, so exit.
|
||||
return;
|
||||
}
|
||||
|
||||
const prop = node.name.name;
|
||||
|
||||
if (!isForbidden(prop, tag)) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
message: `Prop \`${prop}\` is forbidden on Components`
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
forbid: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
minLength: 1
|
||||
},
|
||||
uniqueItems: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
function isForbidden(prop) {
|
||||
const configuration = context.options[0] || {};
|
||||
|
||||
const forbid = configuration.forbid || DEFAULTS;
|
||||
return forbid.indexOf(prop) >= 0;
|
||||
}
|
||||
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
const tag = node.parent.name.name;
|
||||
if (!(tag && 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;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
message: `Prop \`${prop}\` is forbidden on DOM Nodes`
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+114
@@ -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')
|
||||
},
|
||||
|
||||
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 errorMessageForElement(name) {
|
||||
const message = `<${name}> is forbidden`;
|
||||
const additionalMessage = indexedForbidConfigs[name].message;
|
||||
|
||||
if (additionalMessage) {
|
||||
return `${message}, ${additionalMessage}`;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
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)) {
|
||||
context.report({
|
||||
node,
|
||||
message: errorMessageForElement(element)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
|
||||
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,
|
||||
message: 'Using propTypes from another component is not safe because they may be removed in production builds'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
ObjectPattern(node) {
|
||||
const propTypesNode = node.properties.find(property => property.type === 'Property' && property.key.name === 'propTypes');
|
||||
|
||||
if (propTypesNode) {
|
||||
context.report({
|
||||
node: propTypesNode,
|
||||
message: 'Using propTypes from another component is not safe because they may be removed in production builds'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
|
||||
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 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) {
|
||||
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.callee.type === 'MemberExpression'
|
||||
) {
|
||||
value = value.callee;
|
||||
}
|
||||
if (value.property) {
|
||||
target = value.property.name;
|
||||
} else if (value.type === 'Identifier') {
|
||||
target = value.name;
|
||||
}
|
||||
if (isForbidden(target)) {
|
||||
context.report({
|
||||
node: declaration,
|
||||
message: `Prop type \`${target}\` is forbidden`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
};
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* @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',
|
||||
|
||||
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]) || []);
|
||||
|
||||
const NEVER_MESSAGE = 'Value must be omitted for boolean attributes{{exceptionsMessage}}';
|
||||
const ALWAYS_MESSAGE = 'Value must be set for boolean attributes{{exceptionsMessage}}';
|
||||
|
||||
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,
|
||||
message: ALWAYS_MESSAGE,
|
||||
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,
|
||||
message: NEVER_MESSAGE,
|
||||
data,
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([node.name.range[1], value.range[1]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
|
||||
// This list is taken from https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements
|
||||
const INLINE_ELEMENTS = new Set([
|
||||
'a',
|
||||
'abbr',
|
||||
'acronym',
|
||||
'b',
|
||||
'bdo',
|
||||
'big',
|
||||
'br',
|
||||
'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: false,
|
||||
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,
|
||||
message: `Ambiguous spacing after previous element ${elementName(lastChild)}`
|
||||
});
|
||||
} else if (nextChild && child.value.match(TEXT_PRECEDING_ELEMENT_PATTERN)) {
|
||||
context.report({
|
||||
node: nextChild,
|
||||
loc: nextChild.loc.start,
|
||||
message: `Ambiguous spacing before next element ${elementName(nextChild)}`
|
||||
});
|
||||
}
|
||||
}
|
||||
lastChild = child;
|
||||
child = nextChild;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
JSXElement: handleJSX,
|
||||
JSXFragment: handleJSX
|
||||
};
|
||||
}
|
||||
};
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* @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',
|
||||
|
||||
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 = 'The closing bracket must be {{location}}{{details}}';
|
||||
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 openingStartOfLine = {
|
||||
column: /^\s*/.exec(openingLine)[0].length,
|
||||
line: opening.line
|
||||
};
|
||||
return {
|
||||
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': function (node) {
|
||||
const attributeNode = lastAttributeNode[getOpeningElementId(node)];
|
||||
const cachedLastAttributeEndPos = attributeNode ? attributeNode.range[1] : null;
|
||||
let expectedNextLine;
|
||||
const tokens = getTokensLocations(node);
|
||||
const expectedLocation = getExpectedLocation(tokens);
|
||||
|
||||
if (hasCorrectLocation(tokens, expectedLocation)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {location: MESSAGE_LOCATION[expectedLocation], details: ''};
|
||||
const correctColumn = getCorrectColumn(tokens, expectedLocation);
|
||||
|
||||
if (correctColumn !== null) {
|
||||
expectedNextLine = tokens.lastProp &&
|
||||
(tokens.lastProp.lastLine === tokens.closing.line);
|
||||
data.details = ` (expected column ${correctColumn + 1}${expectedNextLine ? ' on the next line)' : ')'}`;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
loc: tokens.closing,
|
||||
message: MESSAGE,
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @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'
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
let message;
|
||||
if (!astUtil.isNodeFirstInLine(context, node)) {
|
||||
message = 'Closing tag of a multiline JSX expression must be on its own line.';
|
||||
} else {
|
||||
message = 'Expected closing tag to match indentation of opening.';
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
loc: node.loc,
|
||||
message,
|
||||
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
|
||||
};
|
||||
}
|
||||
};
|
||||
Generated
Vendored
Executable
+313
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* @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',
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
props: {enum: OPTION_VALUES},
|
||||
children: {enum: OPTION_VALUES}
|
||||
},
|
||||
additionalProperties: false
|
||||
},
|
||||
{
|
||||
enum: OPTION_VALUES
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
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 /&[A-Za-z\d#]+;/.test(rawStringValue);
|
||||
}
|
||||
|
||||
function containsDisallowedJSXTextChars(rawStringValue) {
|
||||
return /[{<>}]/.test(rawStringValue);
|
||||
}
|
||||
|
||||
function containsQuoteCharacters(value) {
|
||||
return /['"]/.test(value);
|
||||
}
|
||||
|
||||
function escapeDoubleQuotes(rawStringValue) {
|
||||
return rawStringValue.replace(/\\"/g, '"').replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
function escapeBackslashes(rawStringValue) {
|
||||
return rawStringValue.replace(/\\/g, '\\\\');
|
||||
}
|
||||
|
||||
function needToEscapeCharacterForJSX(raw) {
|
||||
return (
|
||||
containsBackslash(raw) ||
|
||||
containsHTMLEntity(raw) ||
|
||||
containsDisallowedJSXTextChars(raw)
|
||||
);
|
||||
}
|
||||
|
||||
function containsWhitespaceExpression(child) {
|
||||
if (child.type === 'JSXExpressionContainer') {
|
||||
const value = child.expression.value;
|
||||
return value ? jsxUtil.isWhiteSpaces(value) : false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
message: 'Curly braces are unnecessary here.',
|
||||
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,
|
||||
message: 'Need to wrap this literal in a JSX expression.',
|
||||
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 (
|
||||
containsHTMLEntity(literalNode.raw) ||
|
||||
containsLineTerminators(literalNode.raw)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const expression = literalNode.parent.type === 'JSXAttribute' ?
|
||||
`{"${escapeDoubleQuotes(escapeBackslashes(
|
||||
literalNode.raw.substring(1, literalNode.raw.length - 1)
|
||||
))}"}` :
|
||||
`{${JSON.stringify(literalNode.value)}}`;
|
||||
|
||||
return fixer.replaceText(literalNode, expression);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isWhiteSpaceLiteral(node) {
|
||||
return node.type && node.type === 'Literal' && node.value && jsxUtil.isWhiteSpaces(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;
|
||||
|
||||
if (
|
||||
(expressionType === 'Literal' || expressionType === 'JSXText') &&
|
||||
typeof expression.value === 'string' &&
|
||||
!isWhiteSpaceLiteral(expression) &&
|
||||
!needToEscapeCharacterForJSX(expression.raw) && (
|
||||
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 &&
|
||||
!needToEscapeCharacterForJSX(expression.quasis[0].value.raw) && (
|
||||
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) {
|
||||
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) {
|
||||
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(parent, config) {
|
||||
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.parent, userConfig)) {
|
||||
reportMissingCurly(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+406
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* @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',
|
||||
|
||||
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,
|
||||
message: `There should be no newline after '${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,
|
||||
message: `There should be no newline before '${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,
|
||||
message: `There should be no space after '${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].start), '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,
|
||||
message: `There should be no space before '${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].end), 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,
|
||||
message: `A space is required after '${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,
|
||||
message: `A space is required before '${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
|
||||
};
|
||||
}
|
||||
};
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @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',
|
||||
|
||||
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,
|
||||
message: 'There should be no space before \'=\'',
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([attrNode.name.range[1], equalToken.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (spacedAfter) {
|
||||
context.report({
|
||||
node: attrNode,
|
||||
loc: equalToken.loc.start,
|
||||
message: 'There should be no space after \'=\'',
|
||||
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,
|
||||
message: 'A space is required before \'=\'',
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(equalToken, ' ');
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!spacedAfter) {
|
||||
context.report({
|
||||
node: attrNode,
|
||||
loc: equalToken.loc.start,
|
||||
message: 'A space is required after \'=\'',
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(equalToken, ' ');
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @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 = {
|
||||
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')
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
extensions: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
let invalidExtension;
|
||||
let invalidNode;
|
||||
|
||||
function getExtensionsConfig() {
|
||||
return context.options[0] && context.options[0].extensions || DEFAULTS.extensions;
|
||||
}
|
||||
|
||||
function handleJSX(node) {
|
||||
const filename = context.getFilename();
|
||||
if (filename === '<text>') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (invalidNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allowedExtensions = getExtensionsConfig();
|
||||
const isAllowedExtension = allowedExtensions.some(extension => filename.slice(-extension.length) === extension);
|
||||
|
||||
if (isAllowedExtension) {
|
||||
return;
|
||||
}
|
||||
|
||||
invalidNode = node;
|
||||
invalidExtension = path.extname(filename);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
JSXElement: handleJSX,
|
||||
JSXFragment: handleJSX,
|
||||
|
||||
'Program:exit': function () {
|
||||
if (!invalidNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node: invalidNode,
|
||||
message: `JSX not allowed in files with extension '${invalidExtension}'`
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @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',
|
||||
|
||||
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,
|
||||
message: 'Property should be placed on a new line',
|
||||
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,
|
||||
message: 'Property should be placed on the same line as the component declaration',
|
||||
fix(fixer) {
|
||||
return fixer.replaceTextRange([node.name.range[1], firstNode.range[0]], ' ');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* @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',
|
||||
|
||||
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,
|
||||
message: '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.'
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getFixerToLong(jsxFragment) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
return function (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 (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,
|
||||
message: `Prefer ${reactPragma}.${fragmentPragma} over fragment shorthand`,
|
||||
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': function () {
|
||||
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,
|
||||
message: `Prefer fragment shorthand over ${reactPragma}.${fragmentPragma}`,
|
||||
fix: getFixerToShort(node)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
|
||||
schema: [{
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
eventHandlerPrefix: {type: 'string'},
|
||||
eventHandlerPropPrefix: {type: 'string'}
|
||||
},
|
||||
additionalProperties: false
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
eventHandlerPrefix: {type: 'string'},
|
||||
eventHandlerPropPrefix: {
|
||||
type: 'boolean',
|
||||
enum: [false]
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
eventHandlerPrefix: {
|
||||
type: 'boolean',
|
||||
enum: [false]
|
||||
},
|
||||
eventHandlerPropPrefix: {type: 'string'}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
function isPrefixDisabled(prefix) {
|
||||
return prefix === false;
|
||||
}
|
||||
|
||||
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}))[A-Z].*$`);
|
||||
const PROP_EVENT_HANDLER_REGEX = !eventHandlerPropPrefix ?
|
||||
null :
|
||||
new RegExp(`^(${eventHandlerPropPrefix}[A-Z].*|ref)$`);
|
||||
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
if (!node.value || !node.value.expression || !node.value.expression.object) {
|
||||
return;
|
||||
}
|
||||
|
||||
const propKey = typeof node.name === 'object' ? node.name.name : node.name;
|
||||
const propValue = context.getSourceCode().getText(node.value.expression).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,
|
||||
message: `Handler function for ${propKey} prop key must begin with '${eventHandlerPrefix}'`
|
||||
});
|
||||
} else if (
|
||||
propFnIsNamedCorrectly &&
|
||||
propIsEventHandler !== null &&
|
||||
!propIsEventHandler
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Prop key for ${propValue} must begin with '${eventHandlerPropPrefix}'`
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* @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',
|
||||
|
||||
schema: [{
|
||||
oneOf: [{
|
||||
enum: ['tab', 'first']
|
||||
}, {
|
||||
type: 'integer'
|
||||
}]
|
||||
}]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const MESSAGE = 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.';
|
||||
|
||||
const extraColumnStart = 0;
|
||||
let indentType = 'space';
|
||||
/** @type {number|'first'} */
|
||||
let indentSize = 4;
|
||||
|
||||
if (context.options.length) {
|
||||
if (context.options[0] === 'first') {
|
||||
indentSize = 'first';
|
||||
indentType = 'space';
|
||||
} else if (context.options[0] === 'tab') {
|
||||
indentSize = 1;
|
||||
indentType = 'tab';
|
||||
} else if (typeof context.options[0] === 'number') {
|
||||
indentSize = context.options[0];
|
||||
indentType = 'space';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
message: MESSAGE,
|
||||
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);
|
||||
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 (
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* @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 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',
|
||||
schema: [{
|
||||
oneOf: [{
|
||||
enum: ['tab']
|
||||
}, {
|
||||
type: 'integer'
|
||||
}]
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
checkAttributes: {
|
||||
type: 'boolean'
|
||||
},
|
||||
indentLogicalExpressions: {
|
||||
type: 'boolean'
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const MESSAGE = 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.';
|
||||
|
||||
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 (fixer) {
|
||||
const indent = Array(needed + 1).join(indentChar);
|
||||
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
|
||||
};
|
||||
|
||||
if (loc) {
|
||||
context.report({
|
||||
node,
|
||||
loc,
|
||||
message: MESSAGE,
|
||||
data: msgContext,
|
||||
fix: getFixerFunction(node, needed)
|
||||
});
|
||||
} else {
|
||||
context.report({
|
||||
node,
|
||||
message: MESSAGE,
|
||||
data: msgContext,
|
||||
fix: getFixerFunction(node, needed)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* @fileoverview Report missing `key` props in iterators/collection literals.
|
||||
* @author Ben Mosher
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const hasProp = require('jsx-ast-utils/hasProp');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const pragmaUtil = require('../util/pragma');
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const defaultOptions = {
|
||||
checkFragmentShorthand: false
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Report missing `key` props in iterators/collection literals',
|
||||
category: 'Possible Errors',
|
||||
recommended: true,
|
||||
url: docsUrl('jsx-key')
|
||||
},
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
checkFragmentShorthand: {
|
||||
type: 'boolean',
|
||||
default: defaultOptions.checkFragmentShorthand
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = Object.assign({}, defaultOptions, context.options[0]);
|
||||
const checkFragmentShorthand = options.checkFragmentShorthand;
|
||||
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,
|
||||
message: 'Missing "key" prop for element in iterator'
|
||||
});
|
||||
} else if (checkFragmentShorthand && node.type === 'JSXFragment') {
|
||||
context.report({
|
||||
node,
|
||||
message: `Missing "key" prop for element in iterator. Shorthand fragment syntax does not support providing keys. Use ${reactPragma}.${fragmentPragma} instead`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getReturnStatement(body) {
|
||||
return body.filter(item => item.type === 'ReturnStatement')[0];
|
||||
}
|
||||
|
||||
return {
|
||||
JSXElement(node) {
|
||||
if (hasProp(node.openingElement.attributes, 'key')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.parent.type === 'ArrayExpression') {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Missing "key" prop for element in array'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
JSXFragment(node) {
|
||||
if (!checkFragmentShorthand) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.parent.type === 'ArrayExpression') {
|
||||
context.report({
|
||||
node,
|
||||
message: `Missing "key" prop for element in array. Shorthand fragment syntax does not support providing keys. Use ${reactPragma}.${fragmentPragma} instead`
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Array.prototype.map
|
||||
CallExpression(node) {
|
||||
if (node.callee && node.callee.type !== 'MemberExpression') {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @fileoverview Validate JSX maximum depth
|
||||
* @author Chris<wfsr@foxmail.com>
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const has = require('has');
|
||||
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')
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
max: {
|
||||
type: 'integer',
|
||||
minimum: 0
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
},
|
||||
create(context) {
|
||||
const MESSAGE = 'Expected the depth of nested jsx elements to be <= {{needed}}, but found {{found}}.';
|
||||
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.length || !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,
|
||||
message: MESSAGE,
|
||||
data: {
|
||||
found: depth,
|
||||
needed: maxDepth
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function findJSXElementOrFragment(variables, name) {
|
||||
function find(refs) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const variable = variableUtil.getVariable(variables, name);
|
||||
return variable && variable.references && find(variable.references);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @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',
|
||||
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 (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],
|
||||
message: `Prop \`${name}\` must be placed on a new line`,
|
||||
fix: generateFixFunction(propsInLine, maximum)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* @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
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const violationMessageStore = {
|
||||
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'
|
||||
};
|
||||
|
||||
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')
|
||||
},
|
||||
|
||||
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, message: violationMessageStore[type]});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function findVariableViolation(node, name) {
|
||||
getBlockStatementAncestors(node).find(
|
||||
block => reportVariableViolation(node, name, block.start)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
BlockStatement(node) {
|
||||
setBlockVariableNameSet(node.start);
|
||||
},
|
||||
|
||||
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].start
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
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, message: violationMessageStore[nodeViolationType]
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @fileoverview Comments inside children section of tag should be placed inside braces.
|
||||
* @author Ben Vinegar
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
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')
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {},
|
||||
additionalProperties: false
|
||||
}]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
function reportLiteralNode(node) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Comments inside children section of tag should be placed inside braces'
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Literal(node) {
|
||||
// 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) {
|
||||
reportLiteralNode(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
|
||||
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,
|
||||
message: 'No duplicate props allowed'
|
||||
});
|
||||
} else {
|
||||
props[name] = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* @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
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Prevent using string literals in React component definition',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-no-literals')
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
noStrings: {
|
||||
type: 'boolean'
|
||||
},
|
||||
allowedStrings: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const isNoStrings = context.options[0] ? context.options[0].noStrings : false;
|
||||
const allowedStrings = context.options[0] ? new Set(context.options[0].allowedStrings) : false;
|
||||
|
||||
const message = isNoStrings ?
|
||||
'Strings not allowed in JSX files' :
|
||||
'Missing JSX expression container around literal string';
|
||||
|
||||
function reportLiteralNode(node) {
|
||||
context.report({
|
||||
node,
|
||||
message: `${message}: “${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 (allowedStrings && allowedStrings.has(node.value)) {
|
||||
return false;
|
||||
}
|
||||
const parent = getParentIgnoringBinaryExpressions(node);
|
||||
const standard = !/^[\s]+$/.test(node.value) &&
|
||||
typeof node.value === 'string' &&
|
||||
parent.type.indexOf('JSX') !== -1 &&
|
||||
parent.type !== 'JSXAttribute';
|
||||
if (isNoStrings) {
|
||||
return standard;
|
||||
}
|
||||
return standard && parent.type !== 'JSXExpressionContainer';
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
|
||||
Literal(node) {
|
||||
if (getValidation(node)) {
|
||||
reportLiteralNode(node);
|
||||
}
|
||||
},
|
||||
|
||||
JSXText(node) {
|
||||
if (getValidation(node)) {
|
||||
reportLiteralNode(node);
|
||||
}
|
||||
},
|
||||
|
||||
TemplateLiteral(node) {
|
||||
const parent = getParentIgnoringBinaryExpressions(node);
|
||||
if (isNoStrings && parent.type === 'JSXExpressionContainer') {
|
||||
reportLiteralNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
};
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @fileoverview Forbid target='_blank' attribute
|
||||
* @author Kevin Miller
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const linkComponentsUtil = require('../util/linkComponents');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
function isTargetBlank(attr) {
|
||||
return attr.name &&
|
||||
attr.name.name === 'target' &&
|
||||
attr.value &&
|
||||
attr.value.type === 'Literal' &&
|
||||
attr.value.value.toLowerCase() === '_blank';
|
||||
}
|
||||
|
||||
function hasExternalLink(element, linkAttribute) {
|
||||
return element.attributes.some(attr => attr.name &&
|
||||
attr.name.name === linkAttribute &&
|
||||
attr.value.type === 'Literal' &&
|
||||
/^(?:\w+:|\/\/)/.test(attr.value.value));
|
||||
}
|
||||
|
||||
function hasDynamicLink(element, linkAttribute) {
|
||||
return element.attributes.some(attr => attr.name &&
|
||||
attr.name.name === linkAttribute &&
|
||||
attr.value.type === 'JSXExpressionContainer');
|
||||
}
|
||||
|
||||
function hasSecureRel(element) {
|
||||
return element.attributes.find((attr) => {
|
||||
if (attr.type === 'JSXAttribute' && attr.name.name === 'rel') {
|
||||
const tags = attr.value && attr.value.type === 'Literal' && attr.value.value.toLowerCase().split(' ');
|
||||
return tags && (tags.indexOf('noopener') >= 0 && tags.indexOf('noreferrer') >= 0);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Forbid target="_blank" attribute without rel="noopener noreferrer"',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
url: docsUrl('jsx-no-target-blank')
|
||||
},
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
enforceDynamicLinks: {
|
||||
enum: ['always', 'never']
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const enforceDynamicLinks = configuration.enforceDynamicLinks || 'always';
|
||||
const components = linkComponentsUtil.getLinkComponents(context);
|
||||
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
if (!components.has(node.parent.name.name) || !isTargetBlank(node) || hasSecureRel(node.parent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const linkAttribute = components.get(node.parent.name.name);
|
||||
|
||||
if (hasExternalLink(node.parent, linkAttribute) || (enforceDynamicLinks === 'always' && hasDynamicLink(node.parent, linkAttribute))) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Using target="_blank" without rel="noopener noreferrer" ' +
|
||||
'is a security risk: see https://mathiasbynens.github.io/rel-noopener'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
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;
|
||||
let variables = scope.variables;
|
||||
let scopeType = 'global';
|
||||
let i;
|
||||
let len;
|
||||
|
||||
// Ignore 'this' keyword (also maked as JSXIdentifier when used in JSX)
|
||||
if (node.name === 'this') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!allowGlobals && sourceType === 'module') {
|
||||
scopeType = 'module';
|
||||
}
|
||||
|
||||
while (scope.type !== scopeType) {
|
||||
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,
|
||||
message: `'${node.name}' is not defined.`
|
||||
});
|
||||
}
|
||||
|
||||
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':
|
||||
node = node.name.namespace;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
checkIdentifierInJSX(node);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
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, there‘s 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 || node.children.length < 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
node.children.length -
|
||||
(+isPaddingSpaces(node.children[0])) -
|
||||
(+isPaddingSpaces(node.children[node.children.length - 1]))
|
||||
) < 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 = 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
|
||||
};
|
||||
}
|
||||
};
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* @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',
|
||||
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,
|
||||
message: `\`${descriptor}\` must be placed on a new line`,
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(nodeToReport, replaceText);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
JSXElement: handleJSX,
|
||||
JSXFragment: handleJSX
|
||||
};
|
||||
}
|
||||
};
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @fileoverview Enforce PascalCase for user-defined JSX components
|
||||
* @author Jake Marsh
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const elementType = require('jsx-ast-utils/elementType');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const jsxUtil = require('../util/jsx');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const PASCAL_CASE_REGEX = /^([A-Z0-9]|[A-Z0-9]+[a-z0-9]+(?:[A-Z0-9]+[a-z0-9]*)*)$/;
|
||||
const ALL_CAPS_TAG_REGEX = /^[A-Z0-9]+([A-Z0-9_]*[A-Z0-9]+)?$/;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce PascalCase for user-defined JSX components',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-pascal-case')
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowAllCaps: {
|
||||
type: 'boolean'
|
||||
},
|
||||
ignore: {
|
||||
type: 'array'
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const allowAllCaps = configuration.allowAllCaps || false;
|
||||
const ignore = configuration.ignore || [];
|
||||
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
let name = elementType(node);
|
||||
if (name.length === 1) return undefined;
|
||||
|
||||
// Get namespace if the type is JSXNamespacedName or JSXMemberExpression
|
||||
if (name.indexOf(':') > -1) {
|
||||
name = name.substring(0, name.indexOf(':'));
|
||||
} else if (name.indexOf('.') > -1) {
|
||||
name = name.substring(0, name.indexOf('.'));
|
||||
}
|
||||
|
||||
const isPascalCase = PASCAL_CASE_REGEX.test(name);
|
||||
const isCompatTag = jsxUtil.isDOMComponent(node);
|
||||
const isAllowedAllCaps = allowAllCaps && ALL_CAPS_TAG_REGEX.test(name);
|
||||
const isIgnored = ignore.indexOf(name) !== -1;
|
||||
|
||||
if (!isPascalCase && !isCompatTag && !isAllowedAllCaps && !isIgnored) {
|
||||
let message = `Imported JSX component ${name} must be in PascalCase`;
|
||||
|
||||
if (allowAllCaps) {
|
||||
message += ' or SCREAMING_SNAKE_CASE';
|
||||
}
|
||||
|
||||
context.report({node, message});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* @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',
|
||||
schema: []
|
||||
},
|
||||
|
||||
create(context) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function checkSpacing(prev, 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,
|
||||
message: `Expected only one space between "${getPropName(prev)}" and "${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));
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @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, exceptions: []};
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Prevent JSX prop spreading',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-props-no-spreading')
|
||||
},
|
||||
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 exceptions = configuration.exceptions || DEFAULTS.exceptions;
|
||||
const isException = (tag, allExceptions) => allExceptions.indexOf(tag) !== -1;
|
||||
return {
|
||||
JSXSpreadAttribute(node) {
|
||||
const tagName = node.parent.name.name;
|
||||
const isHTMLTag = tagName && tagName[0] !== tagName[0].toUpperCase();
|
||||
const isCustomTag = tagName && tagName[0] === tagName[0].toUpperCase();
|
||||
if (isHTMLTag &&
|
||||
((ignoreHtmlTags && !isException(tagName, exceptions)) ||
|
||||
(!ignoreHtmlTags && isException(tagName, exceptions)))) {
|
||||
return;
|
||||
}
|
||||
if (isCustomTag &&
|
||||
((ignoreCustomTags && !isException(tagName, exceptions)) ||
|
||||
(!ignoreCustomTags && isException(tagName, exceptions)))) {
|
||||
return;
|
||||
}
|
||||
context.report({
|
||||
node,
|
||||
message: 'Prop spreading is forbidden'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* @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',
|
||||
|
||||
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,
|
||||
message: 'Default prop types declarations should be sorted alphabetically',
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (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 (decl) {
|
||||
context.report({
|
||||
node: decl,
|
||||
message: 'A customized reserved first list must not be empty'
|
||||
});
|
||||
};
|
||||
}
|
||||
if (nonReservedWords.length > 0) {
|
||||
return function (decl) {
|
||||
context.report({
|
||||
node: decl,
|
||||
message: 'A customized reserved first list must only contain a subset of React reserved props.' +
|
||||
' Remove: {{ nonReservedWords }}',
|
||||
data: {
|
||||
nonReservedWords: nonReservedWords.toString()
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce props alphabetical sorting',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-sort-props')
|
||||
},
|
||||
fixable: 'code',
|
||||
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,
|
||||
message: 'Reserved props must be listed before all other props',
|
||||
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,
|
||||
message: 'Callbacks must be listed after all other props',
|
||||
fix: generateFixerFunction(node, context, reservedList)
|
||||
});
|
||||
return memo;
|
||||
}
|
||||
}
|
||||
|
||||
if (shorthandFirst) {
|
||||
if (currentValue && !previousValue) {
|
||||
return decl;
|
||||
}
|
||||
if (!currentValue && previousValue) {
|
||||
context.report({
|
||||
node: memo.name,
|
||||
message: 'Shorthand props must be listed before all other props',
|
||||
fix: generateFixerFunction(node, context, reservedList)
|
||||
});
|
||||
return memo;
|
||||
}
|
||||
}
|
||||
|
||||
if (shorthandLast) {
|
||||
if (!currentValue && previousValue) {
|
||||
return decl;
|
||||
}
|
||||
if (currentValue && !previousValue) {
|
||||
context.report({
|
||||
node: memo.name,
|
||||
message: 'Shorthand props must be listed after all other props',
|
||||
fix: generateFixerFunction(node, context, reservedList)
|
||||
});
|
||||
return memo;
|
||||
}
|
||||
}
|
||||
|
||||
if (!noSortAlphabetically && previousPropName.localeCompare(currentPropName) > 0) {
|
||||
context.report({
|
||||
node: decl.name,
|
||||
message: 'Props should be sorted alphabetically',
|
||||
fix: generateFixerFunction(node, context, reservedList)
|
||||
});
|
||||
return memo;
|
||||
}
|
||||
|
||||
return decl;
|
||||
}, node.attributes[0]);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @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',
|
||||
|
||||
schema: [{
|
||||
enum: ['always', 'never']
|
||||
}]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || 'always';
|
||||
|
||||
const NEVER_MESSAGE = 'A space is forbidden before closing bracket';
|
||||
const ALWAYS_MESSAGE = 'A space is required before closing bracket';
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 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,
|
||||
message: ALWAYS_MESSAGE,
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(closingSlash, ' ');
|
||||
}
|
||||
});
|
||||
} else if (configuration === 'never' && sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
|
||||
context.report({
|
||||
loc: closingSlash.loc.start,
|
||||
message: NEVER_MESSAGE,
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* @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();
|
||||
|
||||
const SELF_CLOSING_NEVER_MESSAGE = 'Whitespace is forbidden between `/` and `>`; write `/>`';
|
||||
const SELF_CLOSING_ALWAYS_MESSAGE = 'Whitespace is required between `/` and `>`; write `/ >`';
|
||||
const NEVER_MESSAGE = 'Whitespace is forbidden between `<` and `/`; write `</`';
|
||||
const ALWAYS_MESSAGE = 'Whitespace is required between `<` and `/`; write `< /`';
|
||||
|
||||
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
|
||||
},
|
||||
message: SELF_CLOSING_NEVER_MESSAGE,
|
||||
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
|
||||
},
|
||||
message: SELF_CLOSING_ALWAYS_MESSAGE,
|
||||
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
|
||||
},
|
||||
message: NEVER_MESSAGE,
|
||||
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
|
||||
},
|
||||
message: ALWAYS_MESSAGE,
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(firstTokens[1], ' ');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateBeforeSelfClosing(context, node, option) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
const NEVER_MESSAGE = 'A space is forbidden before closing bracket';
|
||||
const ALWAYS_MESSAGE = 'A space is required before closing bracket';
|
||||
|
||||
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,
|
||||
message: ALWAYS_MESSAGE,
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(closingSlash, ' ');
|
||||
}
|
||||
});
|
||||
} else if (option === 'never' && sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
|
||||
context.report({
|
||||
node,
|
||||
loc: closingSlash.loc.start,
|
||||
message: NEVER_MESSAGE,
|
||||
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 NEVER_MESSAGE = 'A space is forbidden after opening bracket';
|
||||
const ALWAYS_MESSAGE = 'A space is required after opening bracket';
|
||||
|
||||
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
|
||||
},
|
||||
message: NEVER_MESSAGE,
|
||||
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
|
||||
},
|
||||
message: ALWAYS_MESSAGE,
|
||||
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 NEVER_MESSAGE = 'A space is forbidden before closing bracket';
|
||||
const ALWAYS_MESSAGE = 'Whitespace is required before closing bracket';
|
||||
|
||||
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
|
||||
},
|
||||
message: NEVER_MESSAGE,
|
||||
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
|
||||
},
|
||||
message: ALWAYS_MESSAGE,
|
||||
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',
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @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);
|
||||
|
||||
function handleOpeningElement() {
|
||||
context.markVariableAsUsed(pragma);
|
||||
}
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
JSXOpeningElement: handleOpeningElement,
|
||||
JSXOpeningFragment: handleOpeningElement
|
||||
};
|
||||
}
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @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 && node.name.namespace.name) {
|
||||
// <Foo:Bar>
|
||||
name = node.name.namespace.name;
|
||||
} else 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);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
};
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* @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'
|
||||
};
|
||||
|
||||
const MISSING_PARENS = 'Missing parentheses around multilines JSX';
|
||||
const PARENS_NEW_LINES = 'Parentheses around JSX should be on separate lines';
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// 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',
|
||||
|
||||
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, message, fix) {
|
||||
context.report({
|
||||
node,
|
||||
message,
|
||||
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, MISSING_PARENS, 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});
|
||||
if (tokenBefore.loc.end.line < node.loc.start.line) {
|
||||
// Strip newline after operator if parens newline is specified
|
||||
report(
|
||||
node,
|
||||
MISSING_PARENS,
|
||||
fixer => fixer.replaceTextRange(
|
||||
[tokenBefore.range[0], tokenAfter && (tokenAfter.value === ';' || tokenAfter.value === '}') ? tokenAfter.range[0] : node.range[1]],
|
||||
`${trimTokenBeforeNewline(node, tokenBefore)}(\n${' '.repeat(node.loc.start.column)}${sourceCode.getText(node)}\n${' '.repeat(node.loc.start.column - 2)})`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
report(node, MISSING_PARENS, fixer => fixer.replaceText(node, `(\n${sourceCode.getText(node)}\n)`));
|
||||
}
|
||||
} else {
|
||||
const needsOpening = needsOpeningNewLine(node);
|
||||
const needsClosing = needsClosingNewLine(node);
|
||||
if (needsOpening || needsClosing) {
|
||||
report(node, PARENS_NEW_LINES, (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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of this.state within setState
|
||||
* @author Rolf Erik Lekang, Jørgen Aaberg
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// 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')
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 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,
|
||||
message: 'Use callback in setState when referencing the previous state.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
},
|
||||
|
||||
MemberExpression(node) {
|
||||
if (
|
||||
node.property.name === 'state' &&
|
||||
node.object.type === 'ThisExpression'
|
||||
) {
|
||||
let current = node;
|
||||
while (current.type !== 'Program') {
|
||||
// Reporting if this.state is directly within this.setState
|
||||
if (isFirstArgumentInSetStateCall(current, node)) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Use callback in setState when referencing the previous state.'
|
||||
});
|
||||
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,
|
||||
message: 'Use callback in setState when referencing the previous state.'
|
||||
});
|
||||
});
|
||||
}
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
|
||||
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
|
||||
};
|
||||
const ERROR_MESSAGE = 'Do not use Array index in keys';
|
||||
|
||||
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') {
|
||||
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,
|
||||
message: ERROR_MESSAGE
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'TemplateLiteral') {
|
||||
// key={`foo-${bar}`}
|
||||
node.expressions.filter(isArrayIndex).forEach(() => {
|
||||
context.report({node, message: ERROR_MESSAGE});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'BinaryExpression') {
|
||||
// key={'foo' + bar}
|
||||
const identifiers = getIdentifiersFromBinaryExpression(node);
|
||||
|
||||
identifiers.filter(isArrayIndex).forEach(() => {
|
||||
context.report({node, message: ERROR_MESSAGE});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.callee &&
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
['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': function (node) {
|
||||
const mapIndexParamName = getMapIndexParamName(node);
|
||||
if (!mapIndexParamName) {
|
||||
return;
|
||||
}
|
||||
|
||||
indexParamNames.pop();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
schema: []
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
if (node.name.name !== 'children') {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
message: 'Do not pass children as props. Instead, nest children between the opening and closing tags.'
|
||||
});
|
||||
},
|
||||
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,
|
||||
message: 'Do not pass children as props. Instead, pass them as additional arguments to React.createElement.'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* @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: '',
|
||||
recommended: true,
|
||||
url: docsUrl('no-danger-with-children')
|
||||
},
|
||||
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,
|
||||
message: 'Only set one of `children` or `props.dangerouslySetInnerHTML`'
|
||||
});
|
||||
}
|
||||
},
|
||||
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,
|
||||
message: 'Only set one of `children` or `props.dangerouslySetInnerHTML`'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @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_MESSAGE = 'Dangerous property \'{{name}}\' found';
|
||||
|
||||
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')
|
||||
},
|
||||
schema: []
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
|
||||
JSXAttribute(node) {
|
||||
if (jsxUtil.isDOMComponent(node.parent) && isDangerous(node.name.name)) {
|
||||
context.report({
|
||||
node,
|
||||
message: DANGEROUS_MESSAGE,
|
||||
data: {
|
||||
name: node.name.name
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
};
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* @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']
|
||||
};
|
||||
|
||||
const DEPRECATED_MESSAGE = '{{oldMethod}} is deprecated since React {{version}}{{newMethod}}{{refs}}';
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Prevent usage of deprecated methods',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
url: docsUrl('no-deprecated')
|
||||
},
|
||||
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,
|
||||
message: DEPRECATED_MESSAGE,
|
||||
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
|
||||
};
|
||||
})
|
||||
};
|
||||
+10
@@ -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');
|
||||
+10
@@ -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');
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @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')
|
||||
}
|
||||
},
|
||||
|
||||
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,
|
||||
message: 'Do not mutate state directly. Use setState().'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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': function (node) {
|
||||
components.set(node, {
|
||||
inCallExpression: false
|
||||
});
|
||||
},
|
||||
|
||||
'MethodDefinition:exit': function (node) {
|
||||
if (node.kind === 'constructor') {
|
||||
components.set(node, {
|
||||
inConstructor: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'Program:exit': function () {
|
||||
const list = components.list();
|
||||
|
||||
Object.keys(list).forEach((key) => {
|
||||
if (!isValid(list[key])) {
|
||||
reportMutations(list[key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
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,
|
||||
message: 'Do not use findDOMNode'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
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,
|
||||
message: 'Do not use isMounted'
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
|
||||
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;
|
||||
|
||||
const MULTI_COMP_MESSAGE = 'Declare only one React component per file';
|
||||
|
||||
/**
|
||||
* 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': function () {
|
||||
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,
|
||||
message: MULTI_COMP_MESSAGE
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
Generated
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @fileoverview Flag shouldComponentUpdate when extending PureComponent
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const astUtil = require('../util/ast');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
|
||||
function errorMessage(node) {
|
||||
return `${node} does not need shouldComponentUpdate when extending React.PureComponent.`;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Flag shouldComponentUpdate when extending PureComponent',
|
||||
category: 'Possible Errors',
|
||||
recommended: false,
|
||||
url: docsUrl('no-redundant-should-component-update')
|
||||
},
|
||||
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,
|
||||
message: errorMessage(className)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ClassDeclaration: checkForViolation,
|
||||
ClassExpression: checkForViolation
|
||||
};
|
||||
})
|
||||
};
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
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'
|
||||
) {
|
||||
context.report({
|
||||
node: callee,
|
||||
message: `Do not depend on the return value from ${callee.object.name}.render`
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
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,
|
||||
message: 'Do not use setState'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 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': function () {
|
||||
const list = components.list();
|
||||
Object.keys(list).filter(component => !isValid(list[component])).forEach((component) => {
|
||||
reportSetStateUsages(list[component]);
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
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,
|
||||
message: 'Using this.refs is deprecated.'
|
||||
});
|
||||
}
|
||||
},
|
||||
JSXAttribute(node) {
|
||||
if (
|
||||
isRefAttribute(node) &&
|
||||
(containsStringLiteral(node) || containsStringExpressionContainer(node))
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Using string literals in ref attributes is deprecated.'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @fileoverview Report "this" being used in stateless functional components.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const ERROR_MESSAGE = 'Stateless functional components should not use this';
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// 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')
|
||||
},
|
||||
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,
|
||||
message: ERROR_MESSAGE
|
||||
});
|
||||
}
|
||||
}
|
||||
}))
|
||||
};
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* @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 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')
|
||||
},
|
||||
schema: []
|
||||
},
|
||||
|
||||
create: Components.detect((context, components, utils) => {
|
||||
let propTypesPackageName = null;
|
||||
let reactPackageName = null;
|
||||
|
||||
function checkValidPropTypeQualifier(node) {
|
||||
if (node.name !== 'isRequired') {
|
||||
context.report({
|
||||
node,
|
||||
message: `Typo in prop type chain qualifier: ${node.name}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function checkValidPropType(node) {
|
||||
if (node.name && !PROP_TYPES.some(propTypeName => propTypeName === node.name)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Typo in declared prop type: ${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(node, propertyName, isClassProperty) {
|
||||
if (propertyName === 'propTypes' || propertyName === 'contextTypes' || propertyName === 'childContextTypes') {
|
||||
checkValidPropObject(node);
|
||||
}
|
||||
STATIC_CLASS_PROPERTIES.forEach((CLASS_PROP) => {
|
||||
if (propertyName && CLASS_PROP.toLowerCase() === propertyName.toLowerCase() && CLASS_PROP !== propertyName) {
|
||||
const message = isClassProperty ?
|
||||
'Typo in static class property declaration' :
|
||||
'Typo in property declaration';
|
||||
context.report({
|
||||
node,
|
||||
message
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reportErrorIfLifecycleMethodCasingTypo(node) {
|
||||
LIFECYCLE_METHODS.forEach((method) => {
|
||||
if (method.toLowerCase() === node.key.name.toLowerCase() && method !== node.key.name) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Typo in component lifecycle method declaration'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
if (node.source && node.source.value === 'prop-types') { // import PropType from "prop-types"
|
||||
propTypesPackageName = node.specifiers[0].local.name;
|
||||
} else if (node.source && node.source.value === 'react') { // import { PropTypes } from "react"
|
||||
if (node.specifiers.length > 0) {
|
||||
reactPackageName = node.specifiers[0].local.name; // guard against accidental anonymous `import "react"`
|
||||
}
|
||||
if (node.specifiers.length >= 1) {
|
||||
const propTypesSpecifier = node.specifiers.find(specifier => (
|
||||
specifier.imported && specifier.imported.name === 'PropTypes'
|
||||
));
|
||||
if (propTypesSpecifier) {
|
||||
propTypesPackageName = propTypesSpecifier.local.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ClassProperty(node) {
|
||||
if (!node.static || !utils.isES6Component(node.parent.parent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tokens = context.getFirstTokens(node, 2);
|
||||
const propertyName = tokens[1].value;
|
||||
reportErrorIfPropertyCasingTypo(node.value, propertyName, 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, propertyName, 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) => {
|
||||
reportErrorIfPropertyCasingTypo(property.value, property.key.name, false);
|
||||
reportErrorIfLifecycleMethodCasingTypo(property);
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* @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: ['>']
|
||||
}, {
|
||||
char: '"',
|
||||
alternatives: ['"', '“', '"', '”']
|
||||
}, {
|
||||
char: '\'',
|
||||
alternatives: [''', '‘', ''', '’']
|
||||
}, {
|
||||
char: '}',
|
||||
alternatives: ['}']
|
||||
}];
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Detect unescaped HTML entities, which might represent malformed tags',
|
||||
category: 'Possible Errors',
|
||||
recommended: true,
|
||||
url: docsUrl('no-unescaped-entities')
|
||||
},
|
||||
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({
|
||||
loc: {line: i, column: start + index},
|
||||
message: `HTML entity, \`${entities[j]}\` , must be escaped.`,
|
||||
node
|
||||
});
|
||||
}
|
||||
} else if (c === entities[j].char) {
|
||||
context.report({
|
||||
loc: {line: i, column: start + index},
|
||||
message: `\`${entities[j].char}\` can be escaped with ${entities[j].alternatives.map(alt => `\`${alt}\``).join(', ')}.`,
|
||||
node
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'Literal, JSXText': function (node) {
|
||||
if (jsxUtil.isJSX(node.parent)) {
|
||||
reportInvalidEntity(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of unknown DOM property
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const DEFAULTS = {
|
||||
ignore: []
|
||||
};
|
||||
|
||||
const UNKNOWN_MESSAGE = 'Unknown property \'{{name}}\' found, use \'{{standardName}}\' instead';
|
||||
const WRONG_TAG_MESSAGE = 'Invalid property \'{{name}}\' found on tag \'{{tagName}}\', but it is only allowed on: {{allowedTags}}';
|
||||
|
||||
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', 'allowTransparency', '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'
|
||||
];
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks if a node matches the JSX tag convention.
|
||||
* @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.
|
||||
* @returns {String} The standard name of the attribute.
|
||||
*/
|
||||
function getStandardName(name) {
|
||||
if (DOM_ATTRIBUTE_NAMES[name]) {
|
||||
return DOM_ATTRIBUTE_NAMES[name];
|
||||
}
|
||||
if (SVGDOM_ATTRIBUTE_NAMES[name]) {
|
||||
return SVGDOM_ATTRIBUTE_NAMES[name];
|
||||
}
|
||||
let i = -1;
|
||||
const found = DOM_PROPERTY_NAMES.some((element, index) => {
|
||||
i = index;
|
||||
return element.toLowerCase() === name;
|
||||
});
|
||||
return found ? DOM_PROPERTY_NAMES[i] : null;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// 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',
|
||||
|
||||
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);
|
||||
const allowedTags = ATTRIBUTE_TAGS_MAP[name];
|
||||
if (tagName && allowedTags && /[^A-Z]/.test(tagName.charAt(0)) && allowedTags.indexOf(tagName) === -1) {
|
||||
context.report({
|
||||
node,
|
||||
message: WRONG_TAG_MESSAGE,
|
||||
data: {
|
||||
name,
|
||||
tagName,
|
||||
allowedTags: allowedTags.join(', ')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const standardName = getStandardName(name);
|
||||
if (!isTagName(node) || !standardName) {
|
||||
return;
|
||||
}
|
||||
context.report({
|
||||
node,
|
||||
message: UNKNOWN_MESSAGE,
|
||||
data: {
|
||||
name,
|
||||
standardName
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(node.name, standardName);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
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,
|
||||
message: `${method} is unsafe for use in async rendering. Update the component to use ${newMethod} instead. ${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
|
||||
};
|
||||
})
|
||||
};
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
customValidators: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
skipShapeProps: {
|
||||
type: 'boolean'
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}]
|
||||
},
|
||||
|
||||
create: Components.detect((context, components) => {
|
||||
const defaults = {skipShapeProps: true, customValidators: []};
|
||||
const configuration = Object.assign({}, defaults, context.options[0] || {});
|
||||
const UNUSED_MESSAGE = '\'{{name}}\' PropType is defined but prop is never used';
|
||||
|
||||
/**
|
||||
* 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.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' && configuration.skipShapeProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prop.node && !isPropUsed(component, prop)) {
|
||||
context.report({
|
||||
node: prop.node.value || prop.node,
|
||||
message: UNUSED_MESSAGE,
|
||||
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': function () {
|
||||
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]);
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+432
@@ -0,0 +1,432 @@
|
||||
/**
|
||||
* @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');
|
||||
|
||||
// Descend through all wrapping TypeCastExpressions and return the expression
|
||||
// that was cast.
|
||||
function uncast(node) {
|
||||
while (node.type === 'TypeCastExpression') {
|
||||
node = node.expression;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
// Return the name of an identifier or the string value of a literal. Useful
|
||||
// anywhere that a literal may be used as a key (e.g., member expressions,
|
||||
// method definitions, ObjectExpression property keys).
|
||||
function getName(node) {
|
||||
node = uncast(node);
|
||||
const type = node.type;
|
||||
|
||||
if (type === 'Identifier') {
|
||||
return node.name;
|
||||
}
|
||||
if (type === 'Literal') {
|
||||
return String(node.value);
|
||||
}
|
||||
if (type === 'TemplateLiteral' && node.expressions.length === 0) {
|
||||
return node.quasis[0].value.raw;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isThisExpression(node) {
|
||||
return uncast(node).type === 'ThisExpression';
|
||||
}
|
||||
|
||||
function getInitialClassInfo() {
|
||||
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) {
|
||||
return (
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
isThisExpression(node.callee.object) &&
|
||||
getName(node.callee.property) === 'setState'
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Prevent definition of unused state fields',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('no-unused-state')
|
||||
},
|
||||
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) {
|
||||
for (const prop of node.properties) {
|
||||
const key = prop.key;
|
||||
|
||||
if (
|
||||
prop.type === 'Property' &&
|
||||
(key.type === 'Literal' ||
|
||||
(key.type === 'TemplateLiteral' && key.expressions.length === 0) ||
|
||||
(prop.computed === false && key.type === 'Identifier')) &&
|
||||
getName(prop.key) !== null
|
||||
) {
|
||||
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) {
|
||||
switch (left.type) {
|
||||
case 'Identifier':
|
||||
if (isStateReference(right) && classInfo.aliases) {
|
||||
classInfo.aliases.add(left.name);
|
||||
}
|
||||
break;
|
||||
case 'ObjectPattern':
|
||||
if (isStateReference(right)) {
|
||||
handleStateDestructuring(left);
|
||||
} else if (isThisExpression(right) && 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,
|
||||
message: `Unused state field: '${name}'`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ClassDeclaration(node) {
|
||||
if (utils.isES6Component(node)) {
|
||||
classInfo = getInitialClassInfo();
|
||||
}
|
||||
},
|
||||
|
||||
ObjectExpression(node) {
|
||||
if (utils.isES5Component(node)) {
|
||||
classInfo = getInitialClassInfo();
|
||||
}
|
||||
},
|
||||
|
||||
'ObjectExpression:exit': function (node) {
|
||||
if (!classInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (utils.isES5Component(node)) {
|
||||
reportUnusedFields();
|
||||
classInfo = null;
|
||||
}
|
||||
},
|
||||
|
||||
'ClassDeclaration:exit': function () {
|
||||
if (!classInfo) {
|
||||
return;
|
||||
}
|
||||
reportUnusedFields();
|
||||
classInfo = null;
|
||||
},
|
||||
|
||||
CallExpression(node) {
|
||||
if (!classInfo) {
|
||||
return;
|
||||
}
|
||||
// If we're looking at a `this.setState({})` invocation, record all the
|
||||
// properties as state fields.
|
||||
if (
|
||||
isSetStateCall(node) &&
|
||||
node.arguments.length > 0 &&
|
||||
node.arguments[0].type === 'ObjectExpression'
|
||||
) {
|
||||
addStateFields(node.arguments[0]);
|
||||
} else if (
|
||||
isSetStateCall(node) &&
|
||||
node.arguments.length > 0 &&
|
||||
node.arguments[0].type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
if (node.arguments[0].body.type === 'ObjectExpression') {
|
||||
addStateFields(node.arguments[0].body);
|
||||
}
|
||||
if (node.arguments[0].params.length > 0 && classInfo.aliases) {
|
||||
const firstParam = node.arguments[0].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.
|
||||
if (
|
||||
getName(node.key) === 'state' &&
|
||||
!node.static &&
|
||||
node.value &&
|
||||
node.value.type === 'ObjectExpression'
|
||||
) {
|
||||
addStateFields(node.value);
|
||||
}
|
||||
|
||||
if (
|
||||
!node.static &&
|
||||
node.value &&
|
||||
node.value.type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
// Create a new set for this.state aliases local to this method.
|
||||
classInfo.aliases = new Set();
|
||||
}
|
||||
},
|
||||
|
||||
'ClassProperty:exit': function (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': function () {
|
||||
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;
|
||||
}
|
||||
// Check for assignments like `this.state = {}`
|
||||
if (
|
||||
node.left.type === 'MemberExpression' &&
|
||||
isThisExpression(node.left.object) &&
|
||||
getName(node.left.property) === 'state' &&
|
||||
node.right.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(node.right);
|
||||
}
|
||||
} else {
|
||||
// Check for assignments like `alias = this.state` and record the alias.
|
||||
handleAssignment(node.left, node.right);
|
||||
}
|
||||
},
|
||||
|
||||
VariableDeclarator(node) {
|
||||
if (!classInfo || !node.init) {
|
||||
return;
|
||||
}
|
||||
handleAssignment(node.id, node.init);
|
||||
},
|
||||
|
||||
MemberExpression(node) {
|
||||
if (!classInfo) {
|
||||
return;
|
||||
}
|
||||
if (isStateReference(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': function (node) {
|
||||
if (classInfo && isStateReference(node.argument)) {
|
||||
classInfo = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+14
@@ -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')
|
||||
);
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
|
||||
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,
|
||||
message: 'Component should use es6 class instead of createClass'
|
||||
});
|
||||
}
|
||||
},
|
||||
ClassDeclaration(node) {
|
||||
if (utils.isES6Component(node) && configuration === 'never') {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Component should use createClass instead of es6 class'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @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';
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Require read-only props.',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('prefer-read-only-props')
|
||||
},
|
||||
fixable: 'code',
|
||||
schema: []
|
||||
},
|
||||
|
||||
create: Components.detect((context, components) => ({
|
||||
'Program:exit': function () {
|
||||
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,
|
||||
message: 'Prop \'{{propName}}\' should be read-only.',
|
||||
data: {
|
||||
propName
|
||||
},
|
||||
fix: (fixer) => {
|
||||
if (!prop.node.variance) {
|
||||
// Insert covariance
|
||||
return fixer.insertTextBefore(prop.node, '+');
|
||||
}
|
||||
|
||||
// Replace contravariance with covariance
|
||||
return fixer.replaceText(prop.node.variance, '+');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}))
|
||||
};
|
||||
+380
@@ -0,0 +1,380 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
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' &&
|
||||
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.
|
||||
*/
|
||||
const markSCUAsDeclared = function (node) {
|
||||
components.set(node, {
|
||||
hasSCU: true
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark childContextTypes as declared
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
*/
|
||||
const markChildContextTypesAsDeclared = function (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': function () {
|
||||
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,
|
||||
message: 'Component should be written as a pure function'
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
|
||||
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;
|
||||
|
||||
const MISSING_MESSAGE = '\'{{name}}\' is missing in props validation';
|
||||
|
||||
/**
|
||||
* 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.containsSpread || 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,
|
||||
message: MISSING_MESSAGE,
|
||||
data: {
|
||||
name: propType.allNames.join('.').replace(/\.__COMPUTED_PROP__/g, '[]')
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
'Program:exit': function () {
|
||||
const list = components.list();
|
||||
// Report undeclared proptypes for all classes
|
||||
Object.keys(list).filter(component => mustBeValidated(list[component])).forEach((component) => {
|
||||
reportUndeclaredPropTypes(list[component]);
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
schema: []
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const pragma = pragmaUtil.getFromContext(context);
|
||||
const NOT_DEFINED_MESSAGE = '\'{{name}}\' must be in scope when using JSX';
|
||||
|
||||
function checkIfReactIsInScope(node) {
|
||||
const variables = variableUtil.variablesInScope(context);
|
||||
if (variableUtil.findVariable(variables, pragma)) {
|
||||
return;
|
||||
}
|
||||
context.report({
|
||||
node,
|
||||
message: NOT_DEFINED_MESSAGE,
|
||||
data: {
|
||||
name: pragma
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
JSXOpeningElement: checkIfReactIsInScope,
|
||||
JSXOpeningFragment: checkIfReactIsInScope
|
||||
};
|
||||
}
|
||||
};
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @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');
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// 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')
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
forbidDefaultForRequired: {
|
||||
type: 'boolean'
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}]
|
||||
},
|
||||
|
||||
create: Components.detect((context, components) => {
|
||||
const configuration = context.options[0] || {};
|
||||
const forbidDefaultForRequired = configuration.forbidDefaultForRequired || 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,
|
||||
message: 'propType "{{name}}" is required and should not have a defaultProps declaration.',
|
||||
data: {name: propName}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (defaultProps[propName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node: prop.node,
|
||||
message: 'propType "{{name}}" is not required, but has no corresponding defaultProps declaration.',
|
||||
data: {name: propName}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public API
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
'Program:exit': function () {
|
||||
const list = components.list();
|
||||
|
||||
Object.keys(list).filter(component => list[component].declaredPropTypes).forEach((component) => {
|
||||
reportPropTypesWithoutDefault(
|
||||
list[component].declaredPropTypes,
|
||||
list[component].defaultProps || {}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+230
@@ -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')
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowDecorators: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}]
|
||||
},
|
||||
|
||||
create: Components.detect((context, components, utils) => {
|
||||
const MISSING_MESSAGE = 'Component is not optimized. Please add a shouldComponentUpdate method.';
|
||||
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.
|
||||
*/
|
||||
const hasPureRenderDecorator = function (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.
|
||||
*/
|
||||
const hasCustomDecorator = function (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.
|
||||
*/
|
||||
const isSCUDeclared = function (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.
|
||||
*/
|
||||
const isPureRenderDeclared = function (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.
|
||||
*/
|
||||
const markSCUAsDeclared = function (node) {
|
||||
components.set(node, {
|
||||
hasSCU: true
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Reports missing optimization for a given component
|
||||
* @param {Object} component The component to process
|
||||
*/
|
||||
const reportMissingOptimization = function (component) {
|
||||
context.report({
|
||||
node: component.node,
|
||||
message: MISSING_MESSAGE,
|
||||
data: {
|
||||
component: component.name
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if we are declaring function in class
|
||||
* @returns {Boolean} True if we are declaring function in class, false if not.
|
||||
*/
|
||||
const isFunctionInClass = function () {
|
||||
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': function () {
|
||||
const list = components.list();
|
||||
|
||||
// Report missing shouldComponentUpdate for all components
|
||||
Object.keys(list).filter(component => !list[component].hasSCU).forEach((component) => {
|
||||
reportMissingOptimization(list[component]);
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
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': function () {
|
||||
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),
|
||||
message: 'Your render method should have return statement'
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @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',
|
||||
|
||||
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' && !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,
|
||||
message: 'Empty components are self-closing',
|
||||
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, ' />');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+443
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
|
||||
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 MISPOSITION_MESSAGE = '{{propA}} should be placed {{position}} {{propB}}';
|
||||
|
||||
const methodsOrder = getMethodsOrder(context.options[0]);
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
const regExpRegExp = /\/(.*)\/([g|y|i|m]*)/;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
message: MISPOSITION_MESSAGE,
|
||||
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': function () {
|
||||
const list = components.list();
|
||||
Object.keys(list).forEach((component) => {
|
||||
const properties = astUtil.getComponentProperties(list[component].node);
|
||||
checkPropsOrder(properties);
|
||||
});
|
||||
|
||||
reportErrors();
|
||||
}
|
||||
};
|
||||
}),
|
||||
|
||||
defaultConfig
|
||||
};
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* @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',
|
||||
|
||||
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,
|
||||
message: 'Required prop types must be listed before all other prop types',
|
||||
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,
|
||||
message: 'Callback prop types must be listed after all other prop types',
|
||||
fix
|
||||
});
|
||||
return prev;
|
||||
}
|
||||
}
|
||||
|
||||
if (!noSortAlphabetically && currentPropName < prevPropName) {
|
||||
context.report({
|
||||
node: curr,
|
||||
message: 'Prop types declarations should be sorted alphabetically',
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
};
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @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')
|
||||
},
|
||||
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,
|
||||
message: 'State initialization should be in a constructor'
|
||||
});
|
||||
}
|
||||
},
|
||||
AssignmentExpression(node) {
|
||||
if (
|
||||
option === 'never' &&
|
||||
utils.isStateMemberExpression(node.left) &&
|
||||
utils.inConstructor() &&
|
||||
utils.getParentES6Component()
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'State initialization should be in a class property'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* @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]: '\'{{name}}\' should be declared as a static class property.',
|
||||
[STATIC_GETTER]: '\'{{name}}\' should be declared as a static getter class function.',
|
||||
[PROPERTY_ASSIGNMENT]: '\'{{name}}\' should be declared outside the class body.'
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// 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'
|
||||
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,
|
||||
message: ERROR_MESSAGES[config[name]],
|
||||
data: {name}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Public
|
||||
// ----------------------------------------------------------------------
|
||||
return {
|
||||
ClassProperty: node => 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 (isContextInClass() && node.static && node.kind === 'get') {
|
||||
// Report error if needed
|
||||
reportNodeIncorrectlyPositioned(node, STATIC_GETTER);
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @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: '',
|
||||
recommended: false,
|
||||
url: docsUrl('style-prop-object')
|
||||
},
|
||||
schema: []
|
||||
},
|
||||
|
||||
create(context) {
|
||||
/**
|
||||
* @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,
|
||||
message: 'Style prop value must be an object'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.callee &&
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
node.callee.property.name === 'createElement' &&
|
||||
node.arguments.length > 1
|
||||
) {
|
||||
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,
|
||||
message: 'Style prop value must be an object'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
JSXAttribute(node) {
|
||||
if (!node.value || node.name.name !== 'style') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.value.type !== 'JSXExpressionContainer' || isNonNullaryLiteral(node.value.expression)) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Style prop value must be an object'
|
||||
});
|
||||
} else if (node.value.expression.type === 'Identifier') {
|
||||
checkIdentifiers(node.value.expression);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
function errorMessage(elementName) {
|
||||
return `Void DOM element <${elementName} /> cannot receive children.`;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// 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')
|
||||
},
|
||||
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,
|
||||
message: errorMessage(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,
|
||||
message: errorMessage(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,
|
||||
message: errorMessage(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,
|
||||
message: errorMessage(elementName)
|
||||
});
|
||||
}
|
||||
}
|
||||
}))
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
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 UnionTypeDefinitionChildren = unknown[];
|
||||
type UnionTypeDefinition = {
|
||||
type: 'union' | 'shape';
|
||||
children: UnionTypeDefinitionChildren | true;
|
||||
};
|
||||
}
|
||||
+905
@@ -0,0 +1,905 @@
|
||||
/**
|
||||
* @fileoverview Utility class and functions for React components detection
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const doctrine = require('doctrine');
|
||||
const arrayIncludes = require('array-includes');
|
||||
const values = require('object.values');
|
||||
|
||||
const variableUtil = require('./variable');
|
||||
const pragmaUtil = require('./pragma');
|
||||
const astUtil = require('./ast');
|
||||
const propTypesUtil = require('./propTypes');
|
||||
const jsxUtil = require('./jsx');
|
||||
const usedPropTypesUtil = require('./usedPropTypes');
|
||||
const defaultPropsUtil = require('./defaultProps');
|
||||
|
||||
function getId(node) {
|
||||
return node && node.range.join(':');
|
||||
}
|
||||
|
||||
function usedPropTypesAreEquivalent(propA, propB) {
|
||||
if (propA.name === propB.name) {
|
||||
if (!propA.allNames && !propB.allNames) {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(propA.allNames) && Array.isArray(propB.allNames) && propA.allNames.join('') === propB.allNames.join('')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function mergeUsedPropTypes(propsList, newPropsList) {
|
||||
const propsToAdd = [];
|
||||
newPropsList.forEach((newProp) => {
|
||||
const newPropisAlreadyInTheList = propsList.some(prop => usedPropTypesAreEquivalent(prop, newProp));
|
||||
if (!newPropisAlreadyInTheList) {
|
||||
propsToAdd.push(newProp);
|
||||
}
|
||||
});
|
||||
|
||||
return propsList.concat(propsToAdd);
|
||||
}
|
||||
|
||||
const Lists = new WeakMap();
|
||||
|
||||
/**
|
||||
* Components
|
||||
*/
|
||||
class Components {
|
||||
constructor() {
|
||||
Lists.set(this, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a node to the components list, or update it if it's already in the list
|
||||
*
|
||||
* @param {ASTNode} node The AST node being added.
|
||||
* @param {Number} confidence Confidence in the component detection (0=banned, 1=maybe, 2=yes)
|
||||
* @returns {Object} Added component object
|
||||
*/
|
||||
add(node, confidence) {
|
||||
const id = getId(node);
|
||||
const list = Lists.get(this);
|
||||
if (list[id]) {
|
||||
if (confidence === 0 || list[id].confidence === 0) {
|
||||
list[id].confidence = 0;
|
||||
} else {
|
||||
list[id].confidence = Math.max(list[id].confidence, confidence);
|
||||
}
|
||||
return list[id];
|
||||
}
|
||||
list[id] = {
|
||||
node,
|
||||
confidence
|
||||
};
|
||||
return list[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a component in the list using its node
|
||||
*
|
||||
* @param {ASTNode} node The AST node being searched.
|
||||
* @returns {Object} Component object, undefined if the component is not found or has confidence value of 0.
|
||||
*/
|
||||
get(node) {
|
||||
const id = getId(node);
|
||||
const item = Lists.get(this)[id];
|
||||
if (item && item.confidence >= 1) {
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a component in the list
|
||||
*
|
||||
* @param {ASTNode} node The AST node being updated.
|
||||
* @param {Object} props Additional properties to add to the component.
|
||||
*/
|
||||
set(node, props) {
|
||||
const list = Lists.get(this);
|
||||
let component = list[getId(node)];
|
||||
while (!component) {
|
||||
node = node.parent;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
component = list[getId(node)];
|
||||
}
|
||||
|
||||
Object.assign(
|
||||
component,
|
||||
props,
|
||||
{
|
||||
usedPropTypes: mergeUsedPropTypes(
|
||||
component.usedPropTypes || [],
|
||||
props.usedPropTypes || []
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the components list
|
||||
* Components for which we are not confident are not returned
|
||||
*
|
||||
* @returns {Object} Components list
|
||||
*/
|
||||
list() {
|
||||
const thisList = Lists.get(this);
|
||||
const list = {};
|
||||
const usedPropTypes = {};
|
||||
|
||||
// Find props used in components for which we are not confident
|
||||
Object.keys(thisList).filter(i => thisList[i].confidence < 2).forEach((i) => {
|
||||
let component = null;
|
||||
let node = null;
|
||||
node = thisList[i].node;
|
||||
while (!component && node.parent) {
|
||||
node = node.parent;
|
||||
// Stop moving up if we reach a decorator
|
||||
if (node.type === 'Decorator') {
|
||||
break;
|
||||
}
|
||||
component = this.get(node);
|
||||
}
|
||||
if (component) {
|
||||
const newUsedProps = (thisList[i].usedPropTypes || []).filter(propType => !propType.node || propType.node.kind !== 'init');
|
||||
|
||||
const componentId = getId(component.node);
|
||||
|
||||
usedPropTypes[componentId] = mergeUsedPropTypes(usedPropTypes[componentId] || [], newUsedProps);
|
||||
}
|
||||
});
|
||||
|
||||
// Assign used props in not confident components to the parent component
|
||||
Object.keys(thisList).filter(j => thisList[j].confidence >= 2).forEach((j) => {
|
||||
const id = getId(thisList[j].node);
|
||||
list[j] = thisList[j];
|
||||
if (usedPropTypes[id]) {
|
||||
list[j].usedPropTypes = mergeUsedPropTypes(list[j].usedPropTypes || [], usedPropTypes[id]);
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the length of the components list
|
||||
* Components for which we are not confident are not counted
|
||||
*
|
||||
* @returns {Number} Components list length
|
||||
*/
|
||||
length() {
|
||||
const list = Lists.get(this);
|
||||
return Object.keys(list).filter(i => list[i].confidence >= 2).length;
|
||||
}
|
||||
}
|
||||
|
||||
function componentRule(rule, context) {
|
||||
const createClass = pragmaUtil.getCreateClassFromContext(context);
|
||||
const pragma = pragmaUtil.getFromContext(context);
|
||||
const sourceCode = context.getSourceCode();
|
||||
const components = new Components();
|
||||
|
||||
// Utilities for component detection
|
||||
const utils = {
|
||||
|
||||
/**
|
||||
* Check if the node is a React ES5 component
|
||||
*
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {Boolean} True if the node is a React ES5 component, false if not
|
||||
*/
|
||||
isES5Component(node) {
|
||||
if (!node.parent) {
|
||||
return false;
|
||||
}
|
||||
return new RegExp(`^(${pragma}\\.)?${createClass}$`).test(sourceCode.getText(node.parent.callee));
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the node is a React ES6 component
|
||||
*
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {Boolean} True if the node is a React ES6 component, false if not
|
||||
*/
|
||||
isES6Component(node) {
|
||||
if (utils.isExplicitComponent(node)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!node.superClass) {
|
||||
return false;
|
||||
}
|
||||
return new RegExp(`^(${pragma}\\.)?(Pure)?Component$`).test(sourceCode.getText(node.superClass));
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the node is explicitly declared as a descendant of a React Component
|
||||
*
|
||||
* @param {ASTNode} node The AST node being checked (can be a ReturnStatement or an ArrowFunctionExpression).
|
||||
* @returns {Boolean} True if the node is explicitly declared as a descendant of a React Component, false if not
|
||||
*/
|
||||
isExplicitComponent(node) {
|
||||
let comment;
|
||||
// Sometimes the passed node may not have been parsed yet by eslint, and this function call crashes.
|
||||
// Can be removed when eslint sets "parent" property for all nodes on initial AST traversal: https://github.com/eslint/eslint-scope/issues/27
|
||||
// eslint-disable-next-line no-warning-comments
|
||||
// FIXME: Remove try/catch when https://github.com/eslint/eslint-scope/issues/27 is implemented.
|
||||
try {
|
||||
comment = sourceCode.getJSDocComment(node);
|
||||
} catch (e) {
|
||||
comment = null;
|
||||
}
|
||||
|
||||
if (comment === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const commentAst = doctrine.parse(comment.value, {
|
||||
unwrap: true,
|
||||
tags: ['extends', 'augments']
|
||||
});
|
||||
|
||||
const relevantTags = commentAst.tags.filter(tag => tag.name === 'React.Component' || tag.name === 'React.PureComponent');
|
||||
|
||||
return relevantTags.length > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks to see if our component extends React.PureComponent
|
||||
*
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {Boolean} True if node extends React.PureComponent, false if not
|
||||
*/
|
||||
isPureComponent(node) {
|
||||
if (node.superClass) {
|
||||
return new RegExp(`^(${pragma}\\.)?PureComponent$`).test(sourceCode.getText(node.superClass));
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if variable is destructured from pragma import
|
||||
*
|
||||
* @param {string} variable The variable name to check
|
||||
* @returns {Boolean} True if createElement is destructured from the pragma
|
||||
*/
|
||||
isDestructuredFromPragmaImport(variable) {
|
||||
const variables = variableUtil.variablesInScope(context);
|
||||
const variableInScope = variableUtil.getVariable(variables, variable);
|
||||
if (variableInScope) {
|
||||
const map = variableInScope.scope.set;
|
||||
return map.has(pragma);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks to see if node is called within createElement from pragma
|
||||
*
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {Boolean} True if createElement called from pragma
|
||||
*/
|
||||
isCreateElement(node) {
|
||||
const calledOnPragma = (
|
||||
node &&
|
||||
node.callee &&
|
||||
node.callee.object &&
|
||||
node.callee.object.name === pragma &&
|
||||
node.callee.property &&
|
||||
node.callee.property.name === 'createElement'
|
||||
);
|
||||
|
||||
const calledDirectly = (
|
||||
node &&
|
||||
node.callee &&
|
||||
node.callee.name === 'createElement'
|
||||
);
|
||||
|
||||
if (this.isDestructuredFromPragmaImport('createElement')) {
|
||||
return calledDirectly || calledOnPragma;
|
||||
}
|
||||
return calledOnPragma;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if we are in a class constructor
|
||||
* @return {boolean} true if we are in a class constructor, false if not
|
||||
*/
|
||||
inConstructor() {
|
||||
let scope = context.getScope();
|
||||
while (scope) {
|
||||
if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') {
|
||||
return true;
|
||||
}
|
||||
scope = scope.upper;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the node is MemberExpression of `this.state`
|
||||
* @param {Object} node The node to process
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isStateMemberExpression(node) {
|
||||
return node.type === 'MemberExpression' && node.object.type === 'ThisExpression' && node.property.name === 'state';
|
||||
},
|
||||
|
||||
getReturnPropertyAndNode(ASTnode) {
|
||||
let property;
|
||||
let node = ASTnode;
|
||||
switch (node.type) {
|
||||
case 'ReturnStatement':
|
||||
property = 'argument';
|
||||
break;
|
||||
case 'ArrowFunctionExpression':
|
||||
property = 'body';
|
||||
if (node[property] && node[property].type === 'BlockStatement') {
|
||||
node = utils.findReturnStatement(node);
|
||||
property = 'argument';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
node = utils.findReturnStatement(node);
|
||||
property = 'argument';
|
||||
}
|
||||
return {
|
||||
node,
|
||||
property
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the node is returning JSX
|
||||
*
|
||||
* @param {ASTNode} ASTnode The AST node being checked
|
||||
* @param {Boolean} [strict] If true, in a ternary condition the node must return JSX in both cases
|
||||
* @returns {Boolean} True if the node is returning JSX, false if not
|
||||
*/
|
||||
isReturningJSX(ASTnode, strict) {
|
||||
const nodeAndProperty = utils.getReturnPropertyAndNode(ASTnode);
|
||||
const node = nodeAndProperty.node;
|
||||
const property = nodeAndProperty.property;
|
||||
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const returnsConditionalJSXConsequent = node[property] &&
|
||||
node[property].type === 'ConditionalExpression' &&
|
||||
jsxUtil.isJSX(node[property].consequent);
|
||||
const returnsConditionalJSXAlternate = node[property] &&
|
||||
node[property].type === 'ConditionalExpression' &&
|
||||
jsxUtil.isJSX(node[property].alternate);
|
||||
const returnsConditionalJSX = strict ?
|
||||
(returnsConditionalJSXConsequent && returnsConditionalJSXAlternate) :
|
||||
(returnsConditionalJSXConsequent || returnsConditionalJSXAlternate);
|
||||
|
||||
const returnsJSX = node[property] &&
|
||||
jsxUtil.isJSX(node[property]);
|
||||
const returnsPragmaCreateElement = this.isCreateElement(node[property]);
|
||||
|
||||
return Boolean(
|
||||
returnsConditionalJSX ||
|
||||
returnsJSX ||
|
||||
returnsPragmaCreateElement
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the node is returning null
|
||||
*
|
||||
* @param {ASTNode} ASTnode The AST node being checked
|
||||
* @returns {Boolean} True if the node is returning null, false if not
|
||||
*/
|
||||
isReturningNull(ASTnode) {
|
||||
const nodeAndProperty = utils.getReturnPropertyAndNode(ASTnode);
|
||||
const property = nodeAndProperty.property;
|
||||
const node = nodeAndProperty.node;
|
||||
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return node[property] && node[property].value === null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the node is returning JSX or null
|
||||
*
|
||||
* @param {ASTNode} ASTNode The AST node being checked
|
||||
* @param {Boolean} [strict] If true, in a ternary condition the node must return JSX in both cases
|
||||
* @returns {Boolean} True if the node is returning JSX or null, false if not
|
||||
*/
|
||||
isReturningJSXOrNull(ASTNode, strict) {
|
||||
return utils.isReturningJSX(ASTNode, strict) || utils.isReturningNull(ASTNode);
|
||||
},
|
||||
|
||||
getPragmaComponentWrapper(node) {
|
||||
let isPragmaComponentWrapper;
|
||||
let currentNode = node;
|
||||
let prevNode;
|
||||
do {
|
||||
currentNode = currentNode.parent;
|
||||
isPragmaComponentWrapper = this.isPragmaComponentWrapper(currentNode);
|
||||
if (isPragmaComponentWrapper) {
|
||||
prevNode = currentNode;
|
||||
}
|
||||
} while (isPragmaComponentWrapper);
|
||||
|
||||
return prevNode;
|
||||
},
|
||||
|
||||
getComponentNameFromJSXElement(node) {
|
||||
if (node.type !== 'JSXElement') {
|
||||
return null;
|
||||
}
|
||||
if (node.openingElement && node.openingElement.name && node.openingElement.name.name) {
|
||||
return node.openingElement.name.name;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Getting the first JSX element's name.
|
||||
* @param {object} node
|
||||
* @returns {string | null}
|
||||
*/
|
||||
getNameOfWrappedComponent(node) {
|
||||
if (node.length < 1) {
|
||||
return null;
|
||||
}
|
||||
const body = node[0].body;
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
if (body.type === 'JSXElement') {
|
||||
return this.getComponentNameFromJSXElement(body);
|
||||
}
|
||||
if (body.type === 'BlockStatement') {
|
||||
const jsxElement = body.body.find(item => item.type === 'ReturnStatement');
|
||||
return jsxElement && this.getComponentNameFromJSXElement(jsxElement.argument);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the list of names of components created till now
|
||||
* @returns {string | boolean}
|
||||
*/
|
||||
getDetectedComponents() {
|
||||
const list = components.list();
|
||||
return values(list).filter((val) => {
|
||||
if (val.node.type === 'ClassDeclaration') {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
val.node.type === 'ArrowFunctionExpression' &&
|
||||
val.node.parent &&
|
||||
val.node.parent.type === 'VariableDeclarator' &&
|
||||
val.node.parent.id
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}).map((val) => {
|
||||
if (val.node.type === 'ArrowFunctionExpression') return val.node.parent.id.name;
|
||||
return val.node.id.name;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* It will check wheater memo/forwardRef is wrapping existing component or
|
||||
* creating a new one.
|
||||
* @param {object} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
nodeWrapsComponent(node) {
|
||||
const childComponent = this.getNameOfWrappedComponent(node.arguments);
|
||||
const componentList = this.getDetectedComponents();
|
||||
return !!childComponent && arrayIncludes(componentList, childComponent);
|
||||
},
|
||||
|
||||
isPragmaComponentWrapper(node) {
|
||||
if (!node || node.type !== 'CallExpression') {
|
||||
return false;
|
||||
}
|
||||
const propertyNames = ['forwardRef', 'memo'];
|
||||
const calleeObject = node.callee.object;
|
||||
if (calleeObject && node.callee.property) {
|
||||
return arrayIncludes(propertyNames, node.callee.property.name) &&
|
||||
calleeObject.name === pragma &&
|
||||
!this.nodeWrapsComponent(node);
|
||||
}
|
||||
return arrayIncludes(propertyNames, node.callee.name) && this.isDestructuredFromPragmaImport(node.callee.name);
|
||||
},
|
||||
|
||||
/**
|
||||
* Find a return statment in the current node
|
||||
*
|
||||
* @param {ASTNode} ASTnode The AST node being checked
|
||||
*/
|
||||
findReturnStatement: astUtil.findReturnStatement,
|
||||
|
||||
/**
|
||||
* Get the parent component node from the current scope
|
||||
*
|
||||
* @returns {ASTNode} component node, null if we are not in a component
|
||||
*/
|
||||
getParentComponent() {
|
||||
return (
|
||||
utils.getParentES6Component() ||
|
||||
utils.getParentES5Component() ||
|
||||
utils.getParentStatelessComponent()
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the parent ES5 component node from the current scope
|
||||
*
|
||||
* @returns {ASTNode} component node, null if we are not in a component
|
||||
*/
|
||||
getParentES5Component() {
|
||||
let scope = context.getScope();
|
||||
while (scope) {
|
||||
const node = scope.block && scope.block.parent && scope.block.parent.parent;
|
||||
if (node && utils.isES5Component(node)) {
|
||||
return node;
|
||||
}
|
||||
scope = scope.upper;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the parent ES6 component node from the current scope
|
||||
*
|
||||
* @returns {ASTNode} component node, null if we are not in a component
|
||||
*/
|
||||
getParentES6Component() {
|
||||
let scope = context.getScope();
|
||||
while (scope && scope.type !== 'class') {
|
||||
scope = scope.upper;
|
||||
}
|
||||
const node = scope && scope.block;
|
||||
if (!node || !utils.isES6Component(node)) {
|
||||
return null;
|
||||
}
|
||||
return node;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isInAllowedPositionForComponent(node) {
|
||||
switch (node.parent.type) {
|
||||
case 'VariableDeclarator':
|
||||
case 'AssignmentExpression':
|
||||
case 'Property':
|
||||
case 'ReturnStatement':
|
||||
case 'ExportDefaultDeclaration': {
|
||||
return true;
|
||||
}
|
||||
case 'SequenceExpression': {
|
||||
return utils.isInAllowedPositionForComponent(node.parent) &&
|
||||
node === node.parent.expressions[node.parent.expressions.length - 1];
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get node if node is a stateless component, or node.parent in cases like
|
||||
* `React.memo` or `React.forwardRef`. Otherwise returns `undefined`.
|
||||
* @param {ASTNode} node
|
||||
* @returns {ASTNode | undefined}
|
||||
*/
|
||||
getStatelessComponent(node) {
|
||||
if (node.type === 'FunctionDeclaration') {
|
||||
if (utils.isReturningJSXOrNull(node)) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
|
||||
if (utils.isInAllowedPositionForComponent(node) && utils.isReturningJSXOrNull(node)) {
|
||||
return node;
|
||||
}
|
||||
|
||||
// Case like `React.memo(() => <></>)` or `React.forwardRef(...)`
|
||||
const pragmaComponentWrapper = utils.getPragmaComponentWrapper(node);
|
||||
if (pragmaComponentWrapper) {
|
||||
return pragmaComponentWrapper;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the parent stateless component node from the current scope
|
||||
*
|
||||
* @returns {ASTNode} component node, null if we are not in a component
|
||||
*/
|
||||
getParentStatelessComponent() {
|
||||
let scope = context.getScope();
|
||||
while (scope) {
|
||||
const node = scope.block;
|
||||
const statelessComponent = utils.getStatelessComponent(node);
|
||||
if (statelessComponent) {
|
||||
return statelessComponent;
|
||||
}
|
||||
scope = scope.upper;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the related component from a node
|
||||
*
|
||||
* @param {ASTNode} node The AST node being checked (must be a MemberExpression).
|
||||
* @returns {ASTNode} component node, null if we cannot find the component
|
||||
*/
|
||||
getRelatedComponent(node) {
|
||||
let i;
|
||||
let j;
|
||||
let k;
|
||||
let l;
|
||||
let componentNode;
|
||||
// Get the component path
|
||||
const componentPath = [];
|
||||
while (node) {
|
||||
if (node.property && node.property.type === 'Identifier') {
|
||||
componentPath.push(node.property.name);
|
||||
}
|
||||
if (node.object && node.object.type === 'Identifier') {
|
||||
componentPath.push(node.object.name);
|
||||
}
|
||||
node = node.object;
|
||||
}
|
||||
componentPath.reverse();
|
||||
const componentName = componentPath.slice(0, componentPath.length - 1).join('.');
|
||||
|
||||
// Find the variable in the current scope
|
||||
const variableName = componentPath.shift();
|
||||
if (!variableName) {
|
||||
return null;
|
||||
}
|
||||
let variableInScope;
|
||||
const variables = variableUtil.variablesInScope(context);
|
||||
for (i = 0, j = variables.length; i < j; i++) {
|
||||
if (variables[i].name === variableName) {
|
||||
variableInScope = variables[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!variableInScope) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to find the component using variable references
|
||||
const refs = variableInScope.references;
|
||||
refs.some((ref) => {
|
||||
let refId = ref.identifier;
|
||||
if (refId.parent && refId.parent.type === 'MemberExpression') {
|
||||
refId = refId.parent;
|
||||
}
|
||||
if (sourceCode.getText(refId) !== componentName) {
|
||||
return false;
|
||||
}
|
||||
if (refId.type === 'MemberExpression') {
|
||||
componentNode = refId.parent.right;
|
||||
} else if (
|
||||
refId.parent &&
|
||||
refId.parent.type === 'VariableDeclarator' &&
|
||||
refId.parent.init &&
|
||||
refId.parent.init.type !== 'Identifier'
|
||||
) {
|
||||
componentNode = refId.parent.init;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (componentNode) {
|
||||
// Return the component
|
||||
return components.add(componentNode, 1);
|
||||
}
|
||||
|
||||
// Try to find the component using variable declarations
|
||||
const defs = variableInScope.defs;
|
||||
const defInScope = defs.find(def => (
|
||||
def.type === 'ClassName' ||
|
||||
def.type === 'FunctionName' ||
|
||||
def.type === 'Variable'
|
||||
));
|
||||
if (!defInScope || !defInScope.node) {
|
||||
return null;
|
||||
}
|
||||
componentNode = defInScope.node.init || defInScope.node;
|
||||
|
||||
// Traverse the node properties to the component declaration
|
||||
for (i = 0, j = componentPath.length; i < j; i++) {
|
||||
if (!componentNode.properties) {
|
||||
continue; // eslint-disable-line no-continue
|
||||
}
|
||||
for (k = 0, l = componentNode.properties.length; k < l; k++) {
|
||||
if (componentNode.properties[k].key && componentNode.properties[k].key.name === componentPath[i]) {
|
||||
componentNode = componentNode.properties[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!componentNode || !componentNode.value) {
|
||||
return null;
|
||||
}
|
||||
componentNode = componentNode.value;
|
||||
}
|
||||
|
||||
// Return the component
|
||||
return components.add(componentNode, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Component detection instructions
|
||||
const detectionInstructions = {
|
||||
CallExpression(node) {
|
||||
if (!utils.isPragmaComponentWrapper(node)) {
|
||||
return;
|
||||
}
|
||||
if (node.arguments.length > 0 && astUtil.isFunctionLikeExpression(node.arguments[0])) {
|
||||
components.add(node, 2);
|
||||
}
|
||||
},
|
||||
|
||||
ClassExpression(node) {
|
||||
if (!utils.isES6Component(node)) {
|
||||
return;
|
||||
}
|
||||
components.add(node, 2);
|
||||
},
|
||||
|
||||
ClassDeclaration(node) {
|
||||
if (!utils.isES6Component(node)) {
|
||||
return;
|
||||
}
|
||||
components.add(node, 2);
|
||||
},
|
||||
|
||||
ClassProperty(node) {
|
||||
node = utils.getParentComponent();
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
components.add(node, 2);
|
||||
},
|
||||
|
||||
ObjectExpression(node) {
|
||||
if (!utils.isES5Component(node)) {
|
||||
return;
|
||||
}
|
||||
components.add(node, 2);
|
||||
},
|
||||
|
||||
FunctionExpression(node) {
|
||||
if (node.async) {
|
||||
components.add(node, 0);
|
||||
return;
|
||||
}
|
||||
const component = utils.getParentComponent();
|
||||
if (
|
||||
!component ||
|
||||
(component.parent && component.parent.type === 'JSXExpressionContainer')
|
||||
) {
|
||||
// Ban the node if we cannot find a parent component
|
||||
components.add(node, 0);
|
||||
return;
|
||||
}
|
||||
components.add(component, 1);
|
||||
},
|
||||
|
||||
FunctionDeclaration(node) {
|
||||
if (node.async) {
|
||||
components.add(node, 0);
|
||||
return;
|
||||
}
|
||||
node = utils.getParentComponent();
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
components.add(node, 1);
|
||||
},
|
||||
|
||||
ArrowFunctionExpression(node) {
|
||||
if (node.async) {
|
||||
components.add(node, 0);
|
||||
return;
|
||||
}
|
||||
const component = utils.getParentComponent();
|
||||
if (
|
||||
!component ||
|
||||
(component.parent && component.parent.type === 'JSXExpressionContainer')
|
||||
) {
|
||||
// Ban the node if we cannot find a parent component
|
||||
components.add(node, 0);
|
||||
return;
|
||||
}
|
||||
if (component.expression && utils.isReturningJSX(component)) {
|
||||
components.add(component, 2);
|
||||
} else {
|
||||
components.add(component, 1);
|
||||
}
|
||||
},
|
||||
|
||||
ThisExpression(node) {
|
||||
const component = utils.getParentComponent();
|
||||
if (!component || !/Function/.test(component.type) || !node.parent.property) {
|
||||
return;
|
||||
}
|
||||
// Ban functions accessing a property on a ThisExpression
|
||||
components.add(node, 0);
|
||||
},
|
||||
|
||||
ReturnStatement(node) {
|
||||
if (!utils.isReturningJSX(node)) {
|
||||
return;
|
||||
}
|
||||
node = utils.getParentComponent();
|
||||
if (!node) {
|
||||
const scope = context.getScope();
|
||||
components.add(scope.block, 1);
|
||||
return;
|
||||
}
|
||||
components.add(node, 2);
|
||||
}
|
||||
};
|
||||
|
||||
// Update the provided rule instructions to add the component detection
|
||||
const ruleInstructions = rule(context, components, utils);
|
||||
const updatedRuleInstructions = Object.assign({}, ruleInstructions);
|
||||
const propTypesInstructions = propTypesUtil(context, components, utils);
|
||||
const usedPropTypesInstructions = usedPropTypesUtil(context, components, utils);
|
||||
const defaultPropsInstructions = defaultPropsUtil(context, components, utils);
|
||||
const allKeys = new Set(Object.keys(detectionInstructions).concat(
|
||||
Object.keys(propTypesInstructions),
|
||||
Object.keys(usedPropTypesInstructions),
|
||||
Object.keys(defaultPropsInstructions)
|
||||
));
|
||||
|
||||
allKeys.forEach((instruction) => {
|
||||
updatedRuleInstructions[instruction] = function (node) {
|
||||
if (instruction in detectionInstructions) {
|
||||
detectionInstructions[instruction](node);
|
||||
}
|
||||
if (instruction in propTypesInstructions) {
|
||||
propTypesInstructions[instruction](node);
|
||||
}
|
||||
if (instruction in usedPropTypesInstructions) {
|
||||
usedPropTypesInstructions[instruction](node);
|
||||
}
|
||||
if (instruction in defaultPropsInstructions) {
|
||||
defaultPropsInstructions[instruction](node);
|
||||
}
|
||||
if (ruleInstructions[instruction]) {
|
||||
return ruleInstructions[instruction](node);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Return the updated rule instructions
|
||||
return updatedRuleInstructions;
|
||||
}
|
||||
|
||||
module.exports = Object.assign(Components, {
|
||||
detect(rule) {
|
||||
return componentRule.bind(this, rule);
|
||||
}
|
||||
});
|
||||
+32
@@ -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
|
||||
};
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* @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} 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)
|
||||
);
|
||||
}
|
||||
const key = node.key || node.argument;
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
findReturnStatement,
|
||||
getFirstNodeInLine,
|
||||
getPropertyName,
|
||||
getPropertyNameNode,
|
||||
getComponentProperties,
|
||||
getKeyValue,
|
||||
isAssignmentLHS,
|
||||
isClass,
|
||||
isFunction,
|
||||
isFunctionLikeExpression,
|
||||
isNodeFirstInLine
|
||||
};
|
||||
+267
@@ -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('../util/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));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
+7
@@ -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;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Logs out a message if there is no format option set.
|
||||
* @param {String} message - Message to log.
|
||||
*/
|
||||
function error(message) {
|
||||
if (!/=-(f|-format)=/.test(process.argv.join('='))) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = error;
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Find the token before the closing bracket.
|
||||
* @param {ASTNode} node - The JSX element node.
|
||||
* @returns {Token} The token before the closing bracket.
|
||||
*/
|
||||
function getTokenBeforeClosingBracket(node) {
|
||||
const attributes = node.attributes;
|
||||
if (attributes.length === 0) {
|
||||
return node.name;
|
||||
}
|
||||
return attributes[attributes.length - 1];
|
||||
}
|
||||
|
||||
module.exports = getTokenBeforeClosingBracket;
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @fileoverview Utility functions for JSX
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const elementType = require('jsx-ast-utils/elementType');
|
||||
|
||||
const COMPAT_TAG_REGEX = /^[a-z]|-/;
|
||||
|
||||
/**
|
||||
* Checks if a node represents a DOM element.
|
||||
* @param {object} node - JSXOpeningElement to check.
|
||||
* @returns {boolean} Whether or not the node corresponds to a DOM element.
|
||||
*/
|
||||
function isDOMComponent(node) {
|
||||
let name = elementType(node);
|
||||
|
||||
// Get namespace if the type is JSXNamespacedName or JSXMemberExpression
|
||||
if (name.indexOf(':') > -1) {
|
||||
name = name.slice(0, name.indexOf(':'));
|
||||
} else if (name.indexOf('.') > -1) {
|
||||
name = name.slice(0, name.indexOf('.'));
|
||||
}
|
||||
|
||||
return COMPAT_TAG_REGEX.test(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a JSXElement is a fragment
|
||||
* @param {JSXElement} node
|
||||
* @param {string} reactPragma
|
||||
* @param {string} fragmentPragma
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isFragment(node, reactPragma, fragmentPragma) {
|
||||
const name = node.openingElement.name;
|
||||
|
||||
// <Fragment>
|
||||
if (name.type === 'JSXIdentifier' && name.name === fragmentPragma) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// <React.Fragment>
|
||||
if (
|
||||
name.type === 'JSXMemberExpression' &&
|
||||
name.object.type === 'JSXIdentifier' &&
|
||||
name.object.name === reactPragma &&
|
||||
name.property.type === 'JSXIdentifier' &&
|
||||
name.property.name === fragmentPragma
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a node represents a JSX element or fragment.
|
||||
* @param {object} node - node to check.
|
||||
* @returns {boolean} Whether or not the node if a JSX element or fragment.
|
||||
*/
|
||||
function isJSX(node) {
|
||||
return node && ['JSXElement', 'JSXFragment'].indexOf(node.type) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if node is like `key={...}` as in `<Foo key={...} />`
|
||||
* @param {ASTNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isJSXAttributeKey(node) {
|
||||
return node.type === 'JSXAttribute' &&
|
||||
node.name &&
|
||||
node.name.type === 'JSXIdentifier' &&
|
||||
node.name.name === 'key';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if value has only whitespaces
|
||||
* @param {string} value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isWhiteSpaces(value) {
|
||||
return typeof value === 'string' ? /^\s*$/.test(value) : false;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isDOMComponent,
|
||||
isFragment,
|
||||
isJSX,
|
||||
isJSXAttributeKey,
|
||||
isWhiteSpaces
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @fileoverview Utility functions for propWrapperFunctions setting
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/** TODO: type {(string | { name: string, linkAttribute: string })[]} */
|
||||
/** @type {any} */
|
||||
const DEFAULT_LINK_COMPONENTS = ['a'];
|
||||
const DEFAULT_LINK_ATTRIBUTE = 'href';
|
||||
|
||||
function getLinkComponents(context) {
|
||||
const settings = context.settings || {};
|
||||
const linkComponents = /** @type {typeof DEFAULT_LINK_COMPONENTS} */ (
|
||||
DEFAULT_LINK_COMPONENTS.concat(settings.linkComponents || [])
|
||||
);
|
||||
return new Map(linkComponents.map((value) => {
|
||||
if (typeof value === 'string') {
|
||||
return [value, DEFAULT_LINK_ATTRIBUTE];
|
||||
}
|
||||
return [value.name, value.linkAttribute];
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getLinkComponents
|
||||
};
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Logs out a message if there is no format option set.
|
||||
* @param {String} message - Message to log.
|
||||
*/
|
||||
function log(message) {
|
||||
if (!/=-(f|-format)=/.test(process.argv.join('='))) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = log;
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of setState in lifecycle methods
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('./docsUrl');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
function mapTitle(methodName) {
|
||||
const map = {
|
||||
componentDidMount: 'did-mount',
|
||||
componentDidUpdate: 'did-update',
|
||||
componentWillUpdate: 'will-update'
|
||||
};
|
||||
const title = map[methodName];
|
||||
if (!title) {
|
||||
throw Error(`No docsUrl for '${methodName}'`);
|
||||
}
|
||||
return `no-${title}-set-state`;
|
||||
}
|
||||
|
||||
function makeNoMethodSetStateRule(methodName, shouldCheckUnsafeCb) {
|
||||
return {
|
||||
meta: {
|
||||
docs: {
|
||||
description: `Prevent usage of setState in ${methodName}`,
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl(mapTitle(methodName))
|
||||
},
|
||||
|
||||
schema: [{
|
||||
enum: ['disallow-in-func']
|
||||
}]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const mode = context.options[0] || 'allow-in-func';
|
||||
|
||||
function nameMatches(name) {
|
||||
if (name === methodName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof shouldCheckUnsafeCb === 'function' && shouldCheckUnsafeCb(context)) {
|
||||
return name === `UNSAFE_${methodName}`;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
|
||||
CallExpression(node) {
|
||||
const callee = node.callee;
|
||||
if (
|
||||
callee.type !== 'MemberExpression' ||
|
||||
callee.object.type !== 'ThisExpression' ||
|
||||
callee.property.name !== 'setState'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const ancestors = context.getAncestors(callee).reverse();
|
||||
let depth = 0;
|
||||
ancestors.some((ancestor) => {
|
||||
if (/Function(Expression|Declaration)$/.test(ancestor.type)) {
|
||||
depth++;
|
||||
}
|
||||
if (
|
||||
(ancestor.type !== 'Property' && ancestor.type !== 'MethodDefinition' && ancestor.type !== 'ClassProperty') ||
|
||||
!nameMatches(ancestor.key.name) ||
|
||||
(mode !== 'disallow-in-func' && depth > 1)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
context.report({
|
||||
node: callee,
|
||||
message: `Do not use setState in ${ancestor.key.name}`
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = makeNoMethodSetStateRule;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user