init source

This commit is contained in:
Le Viet
2022-03-07 22:07:57 +07:00
parent e4376f3777
commit 8aba590a8d
11240 changed files with 1012977 additions and 0 deletions
+2756
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2014 Yannick Croissant
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+273
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
};
}
};
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,114 @@
/**
* @fileoverview Forbid certain elements
* @author Kenneth Chung
*/
'use strict';
const has = require('has');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Forbid certain elements',
category: 'Best Practices',
recommended: false,
url: docsUrl('forbid-elements')
},
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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
};
}
};
@@ -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
View File
@@ -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
};
}
};
+313
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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, theres no need for a Fragment at all.',
ChildOfHtmlElement: 'Passing a fragment to an HTML element is useless.'
}
},
create(context) {
const reactPragma = pragmaUtil.getFromContext(context);
const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
/**
* Test whether a node is an padding spaces trimmed by react runtime.
* @param {ASTNode} node
* @returns {boolean}
*/
function isPaddingSpaces(node) {
return isJSXText(node) &&
isOnlyWhitespace(node.raw) &&
arrayIncludes(node.raw, '\n');
}
/**
* Test whether a JSXElement has less than two children, excluding paddings spaces.
* @param {JSXElement|JSXFragment} node
* @returns {boolean}
*/
function hasLessThanTwoChildren(node) {
if (!node || !node.children || 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
};
}
};
@@ -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
View File
@@ -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});
}
}
};
}
};
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
};
}
};
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,10 @@
/**
* @fileoverview Prevent usage of setState in componentDidMount
* @author Yannick Croissant
*/
'use strict';
const makeNoMethodSetStateRule = require('../util/makeNoMethodSetStateRule');
module.exports = makeNoMethodSetStateRule('componentDidMount');
+10
View File
@@ -0,0 +1,10 @@
/**
* @fileoverview Prevent usage of setState in componentDidUpdate
* @author Yannick Croissant
*/
'use strict';
const makeNoMethodSetStateRule = require('../util/makeNoMethodSetStateRule');
module.exports = makeNoMethodSetStateRule('componentDidUpdate');
+147
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
});
}
});
}
};
})
};
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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: ['&gt;']
}, {
char: '"',
alternatives: ['&quot;', '&ldquo;', '&#34;', '&rdquo;']
}, {
char: '\'',
alternatives: ['&apos;', '&lsquo;', '&#39;', '&rsquo;']
}, {
char: '}',
alternatives: ['&#125;']
}];
module.exports = {
meta: {
docs: {
description: 'Detect unescaped HTML entities, which might represent malformed tags',
category: 'Possible Errors',
recommended: true,
url: docsUrl('no-unescaped-entities')
},
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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,14 @@
/**
* @fileoverview Prevent usage of setState in componentWillUpdate
* @author Yannick Croissant
*/
'use strict';
const makeNoMethodSetStateRule = require('../util/makeNoMethodSetStateRule');
const versionUtil = require('../util/version');
module.exports = makeNoMethodSetStateRule(
'componentWillUpdate',
context => versionUtil.testReactVersion(context, '16.3.0')
);
+51
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,230 @@
/**
* @fileoverview Enforce React components to have a shouldComponentUpdate method
* @author Evgueni Naverniouk
*/
'use strict';
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
module.exports = {
meta: {
docs: {
description: 'Enforce React components to have a shouldComponentUpdate method',
category: 'Best Practices',
recommended: false,
url: docsUrl('require-optimization')
},
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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
};
}
};
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,32 @@
/**
* @fileoverview Utility functions for type annotation detection.
* @author Yannick Croissant
* @author Vitor Balocco
*/
'use strict';
/**
* Checks if we are declaring a `props` argument with a flow type annotation.
* @param {ASTNode} node The AST node being checked.
* @param {Object} context
* @returns {Boolean} True if the node is a type annotated props declaration, false if not.
*/
function isAnnotatedFunctionPropsDeclaration(node, context) {
if (!node || !node.params || !node.params.length) {
return false;
}
const typeNode = node.params[0].type === 'AssignmentPattern' ? node.params[0].left : node.params[0];
const tokens = context.getFirstTokens(typeNode, 2);
const isAnnotated = typeNode.typeAnnotation;
const isDestructuredProps = typeNode.type === 'ObjectPattern';
const isProps = tokens[0].value === 'props' || (tokens[1] && tokens[1].value === 'props');
return (isAnnotated && (isDestructuredProps || isProps));
}
module.exports = {
isAnnotatedFunctionPropsDeclaration
};
+197
View File
@@ -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
View File
@@ -0,0 +1,267 @@
/**
* @fileoverview Common defaultProps detection functionality.
*/
'use strict';
const fromEntries = require('object.fromentries');
const astUtil = require('./ast');
const propsUtil = require('./props');
const variableUtil = require('./variable');
const propWrapperUtil = require('../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
View File
@@ -0,0 +1,7 @@
'use strict';
function docsUrl(ruleName) {
return `https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules/${ruleName}.md`;
}
module.exports = docsUrl;
+14
View File
@@ -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;
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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