Added logging, changed some directory structure

This commit is contained in:
2018-01-13 21:33:40 -05:00
parent f079a5f067
commit 8e72ffb917
73656 changed files with 35284 additions and 53718 deletions

File diff suppressed because it is too large Load Diff

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.

View File

@@ -0,0 +1,239 @@
ESLint-plugin-React
===================
[![Maintenance Status][status-image]][status-url] [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Build Status][appveyor-image]][appveyor-url] [![Dependency Status][deps-image]][deps-url] [![Coverage Status][coverage-image]][coverage-url] [![Code Climate][climate-image]][climate-url]
React specific linting rules for ESLint
# Installation
Install [ESLint](https://www.github.com/eslint/eslint) either locally or globally.
```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
Add `plugins` section and specify ESLint-plugin-React as a plugin.
```json
{
"plugins": [
"react"
]
}
```
You can also specify some settings that will be shared across all the plugin rules.
```json5
{
"settings": {
"react": {
"createClass": "createReactClass", // Regex for Component Factory to use, default to "createReactClass"
"pragma": "React", // Pragma to use, default to "React"
"version": "15.0" // React version, default to the latest React stable release
"flowVersion": "0.53" // Flow version
},
"propWrapperFunctions": [ "forbidExtraProps" ] // The names of any functions used to wrap the propTypes object, such as `forbidExtraProps`. If this isn't set, any propTypes wrapped in a function will be skipped.
}
}
```
If it is not already the case you must also configure `ESLint` to support JSX.
With ESLint 1.x.x:
```json
{
"ecmaFeatures": {
"jsx": true
}
}
```
With ESLint 2.x.x or 3.x.x:
```json
{
"parserOptions": {
"ecmaFeatures": {
"jsx": true
}
}
}
```
Finally, enable all of the rules that you would like to use. Use [our preset](#recommended) to get reasonable defaults quickly, and/or choose your own:
```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/default-props-match-prop-types](docs/rules/default-props-match-prop-types.md): Prevent extraneous defaultProps on components
* [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-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-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
* [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-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-unused-prop-types](docs/rules/no-unused-prop-types.md): Prevent definitions of unused prop types
* [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-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/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-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-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-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-curly-brace-presence](docs/rules/jsx-curly-brace-presence.md): Enforce curly braces or disallow unnecessary curly braces in JSX
* [react/jsx-pascal-case](docs/rules/jsx-pascal-case.md): Enforce PascalCase for user-defined JSX components
* [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?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSItMTQyLjUgLTE0Mi41IDI4NSAyODUiPjxjaXJjbGUgcj0iMTQxLjciIGZpbGw9IiNERDQ4MTQiLz48ZyBpZD0iYSIgZmlsbD0iI0ZGRiI%2BPGNpcmNsZSBjeD0iLTk2LjQiIHI9IjE4LjkiLz48cGF0aCBkPSJNLTQ1LjYgNjguNGMtMTYuNi0xMS0yOS0yOC0zNC00Ny44IDYtNSA5LjgtMTIuMyA5LjgtMjAuNnMtMy44LTE1LjctOS44LTIwLjZjNS0xOS44IDE3LjQtMzYuNyAzNC00Ny44bDEzLjggMjMuMkMtNDYtMzUuMi01NS4zLTE4LjctNTUuMyAwYzAgMTguNyA5LjMgMzUuMiAyMy41IDQ1LjJ6Ii8%2BPC9nPjx1c2UgeGxpbms6aHJlZj0iI2EiIHRyYW5zZm9ybT0icm90YXRlKDEyMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNhIiB0cmFuc2Zvcm09InJvdGF0ZSgyNDApIi8%2BPC9zdmc%2B
[appveyor-url]: https://ci.appveyor.com/project/yannickcr/eslint-plugin-react
[appveyor-image]: https://img.shields.io/appveyor/ci/yannickcr/eslint-plugin-react/master.svg?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjEyOCIgaGVpZ2h0PSIxMjgiIHZpZXdCb3g9IjAgMCAxMjggMTI4Ij48ZyBmaWxsPSIjMUJBMUUyIiB0cmFuc2Zvcm09InNjYWxlKDgpIj48cGF0aCBkPSJNMCAyLjI2NWw2LjUzOS0uODg4LjAwMyA2LjI4OC02LjUzNi4wMzd6Ii8%2BPHBhdGggZD0iTTYuNTM2IDguMzlsLjAwNSA2LjI5My02LjUzNi0uODk2di01LjQ0eiIvPjxwYXRoIGQ9Ik03LjMyOCAxLjI2MWw4LjY3LTEuMjYxdjcuNTg1bC04LjY3LjA2OXoiLz48cGF0aCBkPSJNMTYgOC40NDlsLS4wMDIgNy41NTEtOC42Ny0xLjIyLS4wMTItNi4zNDV6Ii8%2BPC9nPjwvc3ZnPg==
[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/github/yannickcr/eslint-plugin-react.svg
[status-url]: https://github.com/yannickcr/eslint-plugin-react/pulse
[status-image]: https://img.shields.io/badge/status-maintained-brightgreen.svg

View File

@@ -0,0 +1,149 @@
'use strict';
const has = require('has');
const allRules = {
'boolean-prop-naming': require('./lib/rules/boolean-prop-naming'),
'default-props-match-prop-types': require('./lib/rules/default-props-match-prop-types'),
'display-name': require('./lib/rules/display-name'),
'forbid-component-props': require('./lib/rules/forbid-component-props'),
'forbid-elements': require('./lib/rules/forbid-elements'),
'forbid-prop-types': require('./lib/rules/forbid-prop-types'),
'forbid-foreign-prop-types': require('./lib/rules/forbid-foreign-prop-types'),
'jsx-boolean-value': require('./lib/rules/jsx-boolean-value'),
'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-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-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-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-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-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-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-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-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'),
'style-prop-object': require('./lib/rules/style-prop-object'),
'void-dom-elements-no-children': require('./lib/rules/void-dom-elements-no-children')
};
function filterRules(rules, predicate) {
const result = {};
for (const key in rules) {
if (has(rules, key) && predicate(rules[key])) {
result[key] = rules[key];
}
}
return result;
}
function configureAsError(rules) {
const result = {};
for (const key in rules) {
if (!has(rules, key)) {
continue;
}
result[`react/${key}`] = 2;
}
return result;
}
const activeRules = filterRules(allRules, rule => !rule.meta.deprecated);
const activeRulesConfig = configureAsError(activeRules);
const deprecatedRules = filterRules(allRules, rule => rule.meta.deprecated);
module.exports = {
deprecatedRules: 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/prop-types': 2,
'react/react-in-jsx-scope': 2,
'react/require-render-return': 2
}
},
all: {
plugins: [
'react'
],
parserOptions: {
ecmaFeatures: {
jsx: true
}
},
rules: activeRulesConfig
}
}
};

View File

@@ -0,0 +1,241 @@
/**
* @fileoverview Enforces consistent naming for boolean props
* @author Ev Haus
*/
'use strict';
const has = require('has');
const Components = require('../util/Components');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
category: 'Stylistic Issues',
description: 'Enforces consistent naming for boolean props',
recommended: false
},
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'
}
},
type: 'object'
}]
},
create: Components.detect((context, components, utils) => {
const sourceCode = context.getSourceCode();
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();
/**
* Checks if node is `propTypes` declaration
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean} True if node is `propTypes` declaration, false if not.
*/
function isPropTypesDeclaration(node) {
// Special case for class properties
// (babel-eslint does not expose property name so we have to rely on tokens)
if (node && node.type === 'ClassProperty') {
const tokens = context.getFirstTokens(node, 2);
if (tokens[0].value === 'propTypes' || (tokens[1] && tokens[1].value === 'propTypes')) {
return true;
}
// Flow support
if (node.typeAnnotation && node.key.name === 'props') {
return true;
}
return false;
}
return Boolean(
node &&
node.name === 'propTypes'
);
}
/**
* Returns the prop key to ensure we handle the following cases:
* propTypes: {
* full: React.PropTypes.bool,
* short: PropTypes.bool,
* direct: bool
* }
* @param {Object} node The node we're getting the name of
*/
function getPropKey(node) {
if (node.value.property) {
return node.value.property.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
*/
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 sourceCode.getFirstToken(node).value;
}
return node.key.name;
}
/**
* 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 || [];
(proptypes || []).forEach(prop => {
const propKey = getPropKey(prop);
const flowCheck = (
prop.type === 'ObjectTypeProperty' &&
prop.value.type === 'BooleanTypeAnnotation' &&
rule.test(getPropName(prop)) === false
);
const regularCheck = (
propKey &&
propTypeNames.indexOf(propKey) >= 0 &&
rule.test(getPropName(prop)) === false
);
if (flowCheck || regularCheck) {
invalidProps.push(prop);
}
});
components.set(node, {
invalidProps: 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: `Prop name (${propName}) doesn't match rule (${config.rule})`,
data: {
component: propName
}
});
});
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
ClassProperty: function(node) {
if (!rule || !isPropTypesDeclaration(node)) {
return;
}
if (node.value && node.value.properties) {
validatePropNaming(node, node.value.properties);
}
if (node.typeAnnotation && node.typeAnnotation.typeAnnotation) {
validatePropNaming(node, node.typeAnnotation.typeAnnotation.properties);
}
},
MemberExpression: function(node) {
if (!rule || !isPropTypesDeclaration(node.property)) {
return;
}
const component = utils.getRelatedComponent(node);
if (!component || !node.parent.right.properties) {
return;
}
validatePropNaming(component.node, node.parent.right.properties);
},
ObjectExpression: function(node) {
if (!rule) {
return;
}
// Search for the proptypes declaration
node.properties.forEach(property => {
if (!isPropTypesDeclaration(property.key)) {
return;
}
validatePropNaming(node, property.value.properties);
});
},
TypeAlias: function(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 propType = objectTypeAnnotations.get(typeNode.typeAnnotation.id.name);
if (propType) {
validatePropNaming(list[component].node, propType.properties);
}
}
if (!has(list, component) || (list[component].invalidProps || []).length) {
reportInvalidNaming(list[component]);
}
});
// Reset cache
objectTypeAnnotations.clear();
}
};
})
};

View File

@@ -0,0 +1,638 @@
/**
* @fileOverview Enforce all defaultProps are defined in propTypes
* @author Vitor Balocco
* @author Roy Sutton
*/
'use strict';
const has = require('has');
const Components = require('../util/Components');
const variableUtil = require('../util/variable');
const annotations = require('../util/annotations');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce all defaultProps are defined and not "required" in propTypes.',
category: 'Best Practices'
},
schema: [{
type: 'object',
properties: {
allowRequiredDefaults: {
default: false,
type: 'boolean'
}
},
additionalProperties: false
}]
},
create: Components.detect((context, components, utils) => {
const configuration = context.options[0] || {};
const allowRequiredDefaults = configuration.allowRequiredDefaults || false;
const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
/**
* 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;
} else 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)
} else 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 propTypes declaration.
* @param {ASTNode} node The node to check. Must be an Identifier node.
* @returns {Boolean} `true` if the node is a propTypes declaration, `false` if not
*/
function isPropTypesDeclaration(node) {
return getPropertyName(node) === 'propTypes';
}
/**
* 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');
}
/**
* Checks if the PropTypes MemberExpression node passed in declares a required propType.
* @param {ASTNode} propTypeExpression node to check. Must be a `PropTypes` MemberExpression.
* @returns {Boolean} `true` if this PropType is required, `false` if not.
*/
function isRequiredPropType(propTypeExpression) {
return propTypeExpression.type === 'MemberExpression' && propTypeExpression.property.name === 'isRequired';
}
/**
* 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;
}
/**
* 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 findVariableByName(node.name);
}
if (
node.type === 'CallExpression' &&
propWrapperFunctions.has(node.callee.name) &&
node.arguments && node.arguments[0]
) {
return resolveNodeValue(node.arguments[0]);
}
return node;
}
/**
* Tries to find the definition of a GenericTypeAnnotation in the current scope.
* @param {ASTNode} node The node GenericTypeAnnotation node to resolve.
* @return {ASTNode|null} Return null if definition cannot be found, ASTNode otherwise.
*/
function resolveGenericTypeAnnotation(node) {
if (node.type !== 'GenericTypeAnnotation' || node.id.type !== 'Identifier') {
return null;
}
return findVariableByName(node.id.name);
}
function resolveUnionTypeAnnotation(node) {
// Go through all the union and resolve any generic types.
return node.types.map(annotation => {
if (annotation.type === 'GenericTypeAnnotation') {
return resolveGenericTypeAnnotation(annotation);
}
return annotation;
});
}
/**
* Extracts a PropType from an ObjectExpression node.
* @param {ASTNode} objectExpression ObjectExpression node.
* @returns {Object[]} Array of PropType object representations, to be consumed by `addPropTypesToComponent`.
*/
function getPropTypesFromObjectExpression(objectExpression) {
const props = objectExpression.properties.filter(property => property.type !== 'ExperimentalSpreadProperty');
return props.map(property => ({
name: property.key.name,
isRequired: isRequiredPropType(property.value),
node: property
}));
}
/**
* Handles Props defined in IntersectionTypeAnnotation nodes
* e.g. type Props = PropsA & PropsB
* @param {ASTNode} intersectionTypeAnnotation ObjectExpression node.
* @returns {Object[]}
*/
function getPropertiesFromIntersectionTypeAnnotationNode(annotation) {
return annotation.types.reduce((properties, type) => {
annotation = resolveGenericTypeAnnotation(type);
if (annotation && annotation.id) {
annotation = findVariableByName(annotation.id.name);
}
return properties.concat(annotation.properties);
}, []);
}
/**
* Extracts a PropType from a TypeAnnotation node.
* @param {ASTNode} node TypeAnnotation node.
* @returns {Object[]} Array of PropType object representations, to be consumed by `addPropTypesToComponent`.
*/
function getPropTypesFromTypeAnnotation(node) {
let properties = [];
switch (node.typeAnnotation.type) {
case 'GenericTypeAnnotation':
let annotation = resolveGenericTypeAnnotation(node.typeAnnotation);
if (annotation && annotation.type === 'IntersectionTypeAnnotation') {
properties = getPropertiesFromIntersectionTypeAnnotationNode(annotation);
} else {
if (annotation && annotation.id) {
annotation = findVariableByName(annotation.id.name);
}
properties = annotation ? (annotation.properties || []) : [];
}
break;
case 'UnionTypeAnnotation':
const union = resolveUnionTypeAnnotation(node.typeAnnotation);
properties = union.reduce((acc, curr) => {
if (!curr) {
return acc;
}
return acc.concat(curr.properties);
}, []);
break;
case 'ObjectTypeAnnotation':
properties = node.typeAnnotation.properties;
break;
default:
properties = [];
break;
}
const props = properties.filter(property => property.type === 'ObjectTypeProperty');
return props.map(property => {
// the `key` property is not present in ObjectTypeProperty nodes, so we need to get the key name manually.
const tokens = context.getFirstTokens(property, 1);
const name = tokens[0].value;
return {
name: name,
isRequired: !property.optional,
node: property
};
});
}
/**
* 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');
if (hasSpread) {
return 'unresolved';
}
return objectExpression.properties.map(defaultProp => ({
name: defaultProp.key.name,
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 propTypes to the component passed in.
* @param {ASTNode} component The component to add the propTypes to.
* @param {Object[]} propTypes propTypes to add to the component.
* @returns {void}
*/
function addPropTypesToComponent(component, propTypes) {
const props = component.propTypes || [];
components.set(component.node, {
propTypes: props.concat(propTypes)
});
}
/**
* Adds defaultProps to the component passed in.
* @param {ASTNode} component The component to add the defaultProps to.
* @param {String[]|String} 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 || [];
components.set(component.node, {
defaultProps: defaults.concat(defaultProps)
});
}
/**
* Tries to find a props type annotation in a stateless component.
* @param {ASTNode} node The AST node to look for a props type annotation.
* @return {void}
*/
function handleStatelessComponent(node) {
if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) {
return;
}
// find component this props annotation belongs to
const component = components.get(utils.getParentStatelessComponent());
if (!component) {
return;
}
addPropTypesToComponent(component, getPropTypesFromTypeAnnotation(node.params[0].typeAnnotation, context));
}
function handlePropTypeAnnotationClassProperty(node) {
// find component this props annotation belongs to
const component = components.get(utils.getParentES6Component());
if (!component) {
return;
}
addPropTypesToComponent(component, getPropTypesFromTypeAnnotation(node.typeAnnotation, context));
}
function isPropTypeAnnotation(node) {
return (getPropertyName(node) === 'props' && !!node.typeAnnotation);
}
function propFromName(propTypes, name) {
return propTypes.find(prop => prop.name === name);
}
/**
* 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) {
return;
}
defaultProps.forEach(defaultProp => {
const prop = propFromName(propTypes, defaultProp.name);
if (prop && (allowRequiredDefaults || !prop.isRequired)) {
return;
}
if (prop) {
context.report(
defaultProp.node,
'defaultProp "{{name}}" defined for isRequired propType.',
{name: defaultProp.name}
);
} else {
context.report(
defaultProp.node,
'defaultProp "{{name}}" has no corresponding propTypes declaration.',
{name: defaultProp.name}
);
}
});
}
// --------------------------------------------------------------------------
// Public API
// --------------------------------------------------------------------------
return {
MemberExpression: function(node) {
const isPropType = isPropTypesDeclaration(node);
const isDefaultProp = isDefaultPropsDeclaration(node);
if (!isPropType && !isDefaultProp) {
return;
}
// find component this propTypes/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;
}
if (isPropType) {
addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression));
} else {
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') {
if (isPropType) {
addPropTypesToComponent(component, [{
name: node.parent.property.name,
isRequired: isRequiredPropType(node.parent.parent.right),
node: node.parent.parent
}]);
} else {
addDefaultPropsToComponent(component, [{
name: node.parent.property.name,
node: node.parent.parent
}]);
}
return;
}
},
// e.g.:
// class Hello extends React.Component {
// static get propTypes() {
// return {
// name: React.PropTypes.string
// };
// }
// static get defaultProps() {
// return {
// name: 'Dean'
// };
// }
// render() {
// return <div>Hello {this.props.name}</div>;
// }
// }
MethodDefinition: function(node) {
if (!node.static || node.kind !== 'get') {
return;
}
const isPropType = isPropTypesDeclaration(node);
const isDefaultProp = isDefaultPropsDeclaration(node);
if (!isPropType && !isDefaultProp) {
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;
}
if (isPropType) {
addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression));
} else {
addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
}
},
// e.g.:
// class Greeting extends React.Component {
// render() {
// return (
// <h1>Hello, {this.props.foo} {this.props.bar}</h1>
// );
// }
// static propTypes = {
// foo: React.PropTypes.string,
// bar: React.PropTypes.string.isRequired
// };
// }
ClassProperty: function(node) {
if (isPropTypeAnnotation(node)) {
handlePropTypeAnnotationClassProperty(node);
return;
}
if (!node.static) {
return;
}
if (!node.value) {
return;
}
const propName = getPropertyName(node);
const isPropType = propName === 'propTypes';
const isDefaultProp = propName === 'defaultProps' || propName === 'getDefaultProps';
if (!isPropType && !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;
}
if (isPropType) {
addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression));
} else {
addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
}
},
// e.g.:
// React.createClass({
// render: function() {
// return <div>{this.props.foo}</div>;
// },
// propTypes: {
// foo: React.PropTypes.string.isRequired,
// },
// getDefaultProps: function() {
// return {
// foo: 'default'
// };
// }
// });
ObjectExpression: function(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') {
return;
}
const isPropType = isPropTypesDeclaration(property);
const isDefaultProp = isDefaultPropsDeclaration(property);
if (!isPropType && !isDefaultProp) {
return;
}
if (isPropType && property.value.type === 'ObjectExpression') {
addPropTypesToComponent(component, getPropTypesFromObjectExpression(property.value));
return;
}
if (isDefaultProp && property.value.type === 'FunctionExpression') {
const returnStatement = utils.findReturnStatement(property);
if (!returnStatement || returnStatement.argument.type !== 'ObjectExpression') {
return;
}
addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(returnStatement.argument));
}
});
},
// Check for type annotations in stateless components
FunctionDeclaration: handleStatelessComponent,
ArrowFunctionExpression: handleStatelessComponent,
FunctionExpression: handleStatelessComponent,
'Program:exit': function() {
const list = components.list();
for (const component in list) {
if (!has(list, component)) {
continue;
}
// If no defaultProps could be found, we don't report anything.
if (!list[component].defaultProps) {
return;
}
reportInvalidDefaultProps(
list[component].propTypes,
list[component].defaultProps || {}
);
}
}
};
})
};

View File

@@ -0,0 +1,235 @@
/**
* @fileoverview Prevent missing displayName in a React component definition
* @author Yannick Croissant
*/
'use strict';
const has = require('has');
const Components = require('../util/Components');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent missing displayName in a React component definition',
category: 'Best Practices',
recommended: true
},
schema: [{
type: 'object',
properties: {
ignoreTranspilerName: {
type: 'boolean'
}
},
additionalProperties: false
}]
},
create: Components.detect((context, components, utils) => {
const sourceCode = context.getSourceCode();
const config = context.options[0] || {};
const ignoreTranspilerName = config.ignoreTranspilerName || false;
const MISSING_MESSAGE = 'Component definition is missing display name';
/**
* Checks if we are declaring a display name
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean} True if we are declaring a display name, false if not.
*/
function isDisplayNameDeclaration(node) {
switch (node.type) {
// Special case for class properties
// (babel-eslint does not expose property name so we have to rely on tokens)
case 'ClassProperty':
const tokens = sourceCode.getFirstTokens(node, 2);
if (
tokens[0].value === 'displayName' ||
(tokens[1] && tokens[1].value === 'displayName')
) {
return true;
}
return false;
case 'Identifier':
return node.name === 'displayName';
case 'Literal':
return node.value === 'displayName';
default:
return false;
}
}
/**
* Mark a prop type as declared
* @param {ASTNode} node The AST node being checked.
*/
function markDisplayNameAsDeclared(node) {
components.set(node, {
hasDisplayName: true
});
}
/**
* Reports missing display name for a given component
* @param {Object} component The component to process
*/
function reportMissingDisplayName(component) {
context.report({
node: component.node,
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 = (
(node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') &&
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: function(node) {
if (!isDisplayNameDeclaration(node)) {
return;
}
markDisplayNameAsDeclared(node);
},
MemberExpression: function(node) {
if (!isDisplayNameDeclaration(node.property)) {
return;
}
const component = utils.getRelatedComponent(node);
if (!component) {
return;
}
markDisplayNameAsDeclared(component.node);
},
FunctionExpression: function(node) {
if (ignoreTranspilerName || !hasTranspilerName(node)) {
return;
}
markDisplayNameAsDeclared(node);
},
FunctionDeclaration: function(node) {
if (ignoreTranspilerName || !hasTranspilerName(node)) {
return;
}
markDisplayNameAsDeclared(node);
},
ArrowFunctionExpression: function(node) {
if (ignoreTranspilerName || !hasTranspilerName(node)) {
return;
}
markDisplayNameAsDeclared(node);
},
MethodDefinition: function(node) {
if (!isDisplayNameDeclaration(node.key)) {
return;
}
markDisplayNameAsDeclared(node);
},
ClassExpression: function(node) {
if (ignoreTranspilerName || !hasTranspilerName(node)) {
return;
}
markDisplayNameAsDeclared(node);
},
ClassDeclaration: function(node) {
if (ignoreTranspilerName || !hasTranspilerName(node)) {
return;
}
markDisplayNameAsDeclared(node);
},
ObjectExpression: function(node) {
if (ignoreTranspilerName || !hasTranspilerName(node)) {
// Search for the displayName declaration
node.properties.forEach(property => {
if (!property.key || !isDisplayNameDeclaration(property.key)) {
return;
}
markDisplayNameAsDeclared(node);
});
return;
}
markDisplayNameAsDeclared(node);
},
'Program:exit': function() {
const list = components.list();
// Report missing display name for all components
for (const component in list) {
if (!has(list, component) || list[component].hasDisplayName) {
continue;
}
reportMissingDisplayName(list[component]);
}
}
};
})
};

View File

@@ -0,0 +1,68 @@
/**
* @fileoverview Forbid certain props on components
* @author Joe Lencioni
*/
'use strict';
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const DEFAULTS = ['className', 'style'];
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Forbid certain props on components',
category: 'Best Practices',
recommended: false
},
schema: [{
type: 'object',
properties: {
forbid: {
type: 'array',
items: {
type: 'string'
}
}
},
additionalProperties: true
}]
},
create: function(context) {
function isForbidden(prop) {
const configuration = context.options[0] || {};
const forbid = configuration.forbid || DEFAULTS;
return forbid.indexOf(prop) >= 0;
}
return {
JSXAttribute: function(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)) {
return;
}
context.report({
node: node,
message: `Prop \`${prop}\` is forbidden on Components`
});
}
};
}
};

View File

@@ -0,0 +1,112 @@
/**
* @fileoverview Forbid certain elements
* @author Kenneth Chung
*/
'use strict';
const has = require('has');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Forbid certain elements',
category: 'Best Practices',
recommended: false
},
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: function(context) {
const sourceCode = context.getSourceCode();
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: node,
message: errorMessageForElement(element)
});
}
}
return {
JSXOpeningElement: function(node) {
reportIfForbidden(sourceCode.getText(node.name), node.name);
},
CallExpression: function(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(sourceCode.getText(argument), argument);
}
}
};
}
};

View File

@@ -0,0 +1,59 @@
/**
* @fileoverview Forbid using another component's propTypes
* @author Ian Christian Myers
*/
'use strict';
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Forbid using another component\'s propTypes',
category: 'Best Practices',
recommended: false
}
},
create: function(context) {
// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------
function isLeftSideOfAssignment(node) {
return node.parent.type === 'AssignmentExpression' && node.parent.left === node;
}
return {
MemberExpression: function(node) {
if (!node.computed && node.property && node.property.type === 'Identifier' &&
node.property.name === 'propTypes' && !isLeftSideOfAssignment(node) ||
node.property && node.property.type === 'Literal' &&
node.property.value === 'propTypes' && !isLeftSideOfAssignment(node)) {
context.report({
node: node.property,
message: 'Using another component\'s propTypes is forbidden'
});
}
},
ObjectPattern: function(node) {
const propTypesNode = node.properties.find(property => property.type === 'Property' && property.key.name === 'propTypes');
if (propTypesNode) {
context.report({
node: propTypesNode,
message: 'Using another component\'s propTypes is forbidden'
});
}
}
};
}
};

View File

@@ -0,0 +1,187 @@
/**
* @fileoverview Forbid certain propTypes
*/
'use strict';
const variableUtil = require('../util/variable');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const DEFAULTS = ['any', 'array', 'object'];
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Forbid certain propTypes',
category: 'Best Practices',
recommended: false
},
schema: [{
type: 'object',
properties: {
forbid: {
type: 'array',
items: {
type: 'string'
}
}
},
additionalProperties: true
}]
},
create: function(context) {
const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
function isForbidden(type) {
const configuration = context.options[0] || {};
const forbid = configuration.forbid || DEFAULTS;
return forbid.indexOf(type) >= 0;
}
/**
* Checks if node is `propTypes` declaration
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean} True if node is `propTypes` declaration, false if not.
*/
function isPropTypesDeclaration(node) {
// Special case for class properties
// (babel-eslint does not expose property name so we have to rely on tokens)
if (node.type === 'ClassProperty') {
const tokens = context.getFirstTokens(node, 2);
if (tokens[0].value === 'propTypes' || (tokens[1] && tokens[1].value === 'propTypes')) {
return true;
}
return false;
}
return Boolean(
node &&
node.name === 'propTypes'
);
}
/**
* 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 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 = findVariableByName(node.name);
if (propTypesObject && propTypesObject.properties) {
checkProperties(propTypesObject.properties);
}
break;
case 'CallExpression':
const innerNode = node.arguments && node.arguments[0];
if (propWrapperFunctions.has(node.callee.name) && innerNode) {
checkNode(innerNode);
}
break;
default:
break;
}
}
return {
ClassProperty: function(node) {
if (!isPropTypesDeclaration(node)) {
return;
}
checkNode(node.value);
},
MemberExpression: function(node) {
if (!isPropTypesDeclaration(node.property)) {
return;
}
checkNode(node.parent.right);
},
ObjectExpression: function(node) {
node.properties.forEach(property => {
if (!property.key) {
return;
}
if (!isPropTypesDeclaration(property.key)) {
return;
}
if (property.value.type === 'ObjectExpression') {
checkProperties(property.value.properties);
}
});
}
};
}
};

View File

@@ -0,0 +1,126 @@
/**
* @fileoverview Enforce boolean attributes notation in JSX
* @author Yannick Croissant
*/
'use strict';
// ------------------------------------------------------------------------------
// 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: 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
},
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: node,
message: ALWAYS_MESSAGE,
data: 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: node,
message: NEVER_MESSAGE,
data: data,
fix(fixer) {
return fixer.removeRange([node.name.range[1], value.range[1]]);
}
});
}
}
};
}
};

View File

@@ -0,0 +1,284 @@
/**
* @fileoverview Validate closing bracket location in JSX
* @author Yannick Croissant
*/
'use strict';
const has = require('has');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Validate closing bracket location in JSX',
category: 'Stylistic Issues',
recommended: false
},
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: function(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 sourceCode = context.getSourceCode();
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
* @return {String} The characters used for indentation
*/
function getIndentation(tokens, expectedLocation, correctColumn) {
let indentation, spaces = [];
switch (expectedLocation) {
case 'props-aligned':
indentation = /^\s*/.exec(sourceCode.lines[tokens.lastProp.firstLine - 1])[0];
break;
case 'tag-aligned':
case 'line-aligned':
indentation = /^\s*/.exec(sourceCode.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 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: tag,
opening: opening,
closing: closing,
lastProp: lastProp,
selfClosing: node.selfClosing,
openingStartOfLine: 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: function(node) {
lastAttributeNode[getOpeningElementId(node.parent)] = node;
},
JSXSpreadAttribute: function(node) {
lastAttributeNode[getOpeningElementId(node.parent)] = node;
},
'JSXOpeningElement:exit': function(node) {
const attributeNode = lastAttributeNode[getOpeningElementId(node)];
const cachedLastAttributeEndPos = attributeNode ? attributeNode.end : 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: node,
loc: tokens.closing,
message: MESSAGE,
data: data,
fix: function(fixer) {
const closingTag = tokens.selfClosing ? '/>' : '>';
switch (expectedLocation) {
case 'after-tag':
if (cachedLastAttributeEndPos) {
return fixer.replaceTextRange([cachedLastAttributeEndPos, node.end],
(expectedNextLine ? '\n' : '') + closingTag);
}
return fixer.replaceTextRange([node.name.range[1], node.end],
(expectedNextLine ? '\n' : ' ') + closingTag);
case 'after-props':
return fixer.replaceTextRange([cachedLastAttributeEndPos, node.end],
(expectedNextLine ? '\n' : '') + closingTag);
case 'props-aligned':
case 'tag-aligned':
case 'line-aligned':
return fixer.replaceTextRange([cachedLastAttributeEndPos, node.end],
`\n${getIndentation(tokens, expectedLocation, correctColumn)}${closingTag}`);
default:
return true;
}
}
});
}
};
}
};

View File

@@ -0,0 +1,87 @@
/**
* @fileoverview Validate closing tag location in JSX
* @author Ross Solomon
*/
'use strict';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Validate closing tag location for multiline JSX',
category: 'Stylistic Issues',
recommended: false
},
fixable: 'whitespace'
},
create: function(context) {
const sourceCode = context.getSourceCode();
/**
* Checks if the node is the first in its line, excluding whitespace.
* @param {ASTNode} node The node to check
* @return {Boolean} true if its the first node in its line
*/
function isNodeFirstInLine(node) {
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])
);
const startLine = node.loc.start.line;
const endLine = token ? token.loc.end.line : -1;
return startLine !== endLine;
}
return {
JSXClosingElement: function(node) {
if (!node.parent) {
return;
}
const opening = node.parent.openingElement;
if (opening.loc.start.line === node.loc.start.line) {
return;
}
if (opening.loc.start.column === node.loc.start.column) {
return;
}
let message;
if (!isNodeFirstInLine(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: node,
loc: node.loc,
message,
fix: function(fixer) {
const indent = Array(opening.loc.start.column + 1).join(' ');
if (isNodeFirstInLine(node)) {
return fixer.replaceTextRange(
[node.start - node.loc.start.column, node.start],
indent
);
}
return fixer.insertTextBefore(node, `\n${indent}`);
}
});
}
};
}
};

View File

@@ -0,0 +1,189 @@
/**
* @fileoverview Enforce curly braces or disallow unnecessary curly brace in JSX
* @author Jacky Ho
*/
'use strict';
// ------------------------------------------------------------------------------
// 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
},
fixable: 'code',
schema: [
{
oneOf: [
{
type: 'object',
properties: {
props: {enum: OPTION_VALUES, default: OPTION_NEVER},
children: {enum: OPTION_VALUES, default: OPTION_NEVER}
},
additionalProperties: false
},
{
enum: OPTION_VALUES
}
]
}
]
},
create: function(context) {
const ruleOptions = context.options[0];
const userConfig = typeof ruleOptions === 'string' ?
{props: ruleOptions, children: ruleOptions} :
Object.assign({}, DEFAULT_CONFIG, ruleOptions);
function containsBackslashForEscaping(rawStringValue) {
return rawStringValue.includes('\\');
}
function escapeDoubleQuotes(rawStringValue) {
return rawStringValue.replace(/\\"/g, '"').replace(/"/g, '\\"');
}
/**
* Report and fix an unnecessary curly brace violation on a node
* @param {ASTNode} node - The AST node with an unnecessary JSX expression
* @param {String} text - The text to replace the unnecessary JSX expression
*/
function reportUnnecessaryCurly(JSXExpressionNode) {
context.report({
node: JSXExpressionNode,
message: 'Curly braces are unnecessary here.',
fix: function(fixer) {
const expression = JSXExpressionNode.expression;
const expressionType = expression.type;
const parentType = JSXExpressionNode.parent.type;
let textToReplace;
if (parentType === 'JSXAttribute') {
textToReplace = `"${escapeDoubleQuotes(
expressionType === 'TemplateLiteral' ?
expression.quasis[0].value.raw :
expression.raw.substring(1, expression.raw.length - 1)
)}"`;
} 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: function(fixer) {
const expression = literalNode.parent.type === 'JSXAttribute' ?
`{"${escapeDoubleQuotes(
literalNode.raw.substring(1, literalNode.raw.length - 1)
)}"}` :
`{${JSON.stringify(literalNode.value)}}`;
return fixer.replaceText(literalNode, expression);
}
});
}
function lintUnnecessaryCurly(JSXExpressionNode) {
const expression = JSXExpressionNode.expression;
const expressionType = expression.type;
const parentType = JSXExpressionNode.parent.type;
if (
expressionType === 'Literal' &&
typeof expression.value === 'string' && (
parentType === 'JSXAttribute' ||
!containsBackslashForEscaping(expression.raw))
) {
reportUnnecessaryCurly(JSXExpressionNode);
} else if (
expressionType === 'TemplateLiteral' &&
expression.expressions.length === 0 && (
parentType === 'JSXAttribute' ||
!containsBackslashForEscaping(expression.quasis[0].value.raw))
) {
reportUnnecessaryCurly(JSXExpressionNode);
}
}
function areRuleConditionsSatisfied(parentType, config, ruleCondition) {
return (
parentType === 'JSXAttribute' &&
typeof config.props === 'string' &&
config.props === ruleCondition
) || (
parentType === 'JSXElement' &&
typeof config.children === 'string' &&
config.children === ruleCondition
);
}
function shouldCheckForUnnecessaryCurly(parent, config) {
const parentType = parent.type;
// If there are more than one JSX child, there is no need to check for
// unnecessary curly braces.
if (parentType === 'JSXElement' && parent.children.length !== 1) {
return false;
}
return areRuleConditionsSatisfied(parentType, config, OPTION_NEVER);
}
function shouldCheckForMissingCurly(parentType, config) {
return areRuleConditionsSatisfied(parentType, config, OPTION_ALWAYS);
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
JSXExpressionContainer: node => {
const parent = node.parent;
if (shouldCheckForUnnecessaryCurly(parent, userConfig)) {
lintUnnecessaryCurly(node);
}
},
Literal: node => {
const parentType = node.parent.type;
if (shouldCheckForMissingCurly(parentType, userConfig)) {
reportMissingCurly(node);
}
}
};
}
};

View File

@@ -0,0 +1,346 @@
/**
* @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');
// ------------------------------------------------------------------------------
// 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
},
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: function(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;
const sourceCode = context.getSourceCode();
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.start.line !== right.loc.start.line;
}
/**
* 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.
* @returns {void}
*/
function reportNoBeginningNewline(node, token, spacing) {
context.report({
node: node,
loc: token.loc.start,
message: `There should be no newline after '${token.value}'`,
fix: function(fixer) {
const nextToken = sourceCode.getTokenAfter(token);
return fixer.replaceTextRange([token.range[1], nextToken.range[0]], spacing === SPACING.always ? ' ' : '');
}
});
}
/**
* 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.
* @returns {void}
*/
function reportNoEndingNewline(node, token, spacing) {
context.report({
node: node,
loc: token.loc.start,
message: `There should be no newline before '${token.value}'`,
fix: function(fixer) {
const previousToken = sourceCode.getTokenBefore(token);
return fixer.replaceTextRange([previousToken.range[1], token.range[0]], spacing === SPACING.always ? ' ' : '');
}
});
}
/**
* 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: node,
loc: token.loc.start,
message: `There should be no space after '${token.value}'`,
fix: function(fixer) {
const nextToken = sourceCode.getTokenAfter(token);
const nextNode = sourceCode.getNodeByRangeIndex(nextToken.range[0]);
const leadingComments = sourceCode.getComments(nextNode).leading;
const rangeEndRef = leadingComments.length ? leadingComments[0] : nextToken;
return fixer.removeRange([token.range[1], rangeEndRef.range[0]]);
}
});
}
/**
* 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: node,
loc: token.loc.start,
message: `There should be no space before '${token.value}'`,
fix: function(fixer) {
const previousToken = sourceCode.getTokenBefore(token);
const previousNode = sourceCode.getNodeByRangeIndex(previousToken.range[0]);
const trailingComments = sourceCode.getComments(previousNode).trailing;
const rangeStartRef = trailingComments.length ? trailingComments[trailingComments.length - 1] : previousToken;
return fixer.removeRange([rangeStartRef.range[1], token.range[0]]);
}
});
}
/**
* 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: node,
loc: token.loc.start,
message: `A space is required after '${token.value}'`,
fix: function(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: node,
loc: token.loc.start,
message: `A space is required before '${token.value}'`,
fix: function(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':
config = childrenConfig;
break;
default:
return;
}
if (config === null) {
return;
}
const first = context.getFirstToken(node);
const last = sourceCode.getLastToken(node);
let second = context.getTokenAfter(first, {includeComments: true});
let penultimate = sourceCode.getTokenBefore(last, {includeComments: true});
if (!second) {
second = context.getTokenAfter(first);
const leadingComments = sourceCode.getNodeByRangeIndex(second.range[0]).leadingComments;
second = leadingComments ? leadingComments[0] : second;
}
if (!penultimate) {
penultimate = sourceCode.getTokenBefore(last);
const trailingComments = sourceCode.getNodeByRangeIndex(penultimate.range[0]).trailingComments;
penultimate = trailingComments ? trailingComments[trailingComments.length - 1] : penultimate;
}
const isObjectLiteral = first.value === second.value;
const spacing = isObjectLiteral ? config.objectLiteralSpaces : config.when;
if (spacing === SPACING.always) {
if (!sourceCode.isSpaceBetweenTokens(first, second)) {
reportRequiredBeginningSpace(node, first);
} else if (!config.allowMultiline && isMultiline(first, second)) {
reportNoBeginningNewline(node, first, spacing);
}
if (!sourceCode.isSpaceBetweenTokens(penultimate, last)) {
reportRequiredEndingSpace(node, last);
} else if (!config.allowMultiline && isMultiline(penultimate, last)) {
reportNoEndingNewline(node, last, spacing);
}
} else if (spacing === SPACING.never) {
if (isMultiline(first, second)) {
if (!config.allowMultiline) {
reportNoBeginningNewline(node, first, spacing);
}
} else if (sourceCode.isSpaceBetweenTokens(first, second)) {
reportNoBeginningSpace(node, first);
}
if (isMultiline(penultimate, last)) {
if (!config.allowMultiline) {
reportNoEndingNewline(node, last, spacing);
}
} else if (sourceCode.isSpaceBetweenTokens(penultimate, last)) {
reportNoEndingSpace(node, last);
}
}
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
JSXExpressionContainer: validateBraceSpacing,
JSXSpreadAttribute: validateBraceSpacing
};
}
};

View File

@@ -0,0 +1,104 @@
/**
* @fileoverview Disallow or enforce spaces around equal signs in JSX attributes.
* @author ryym
*/
'use strict';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Disallow or enforce spaces around equal signs in JSX attributes',
category: 'Stylistic Issues',
recommended: false
},
fixable: 'code',
schema: [{
enum: ['always', 'never']
}]
},
create: function(context) {
const config = context.options[0];
const sourceCode = context.getSourceCode();
/**
* 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: function(node) {
node.attributes.forEach(attrNode => {
if (!hasEqual(attrNode)) {
return;
}
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: function(fixer) {
return fixer.removeRange([attrNode.name.range[1], equalToken.start]);
}
});
}
if (spacedAfter) {
context.report({
node: attrNode,
loc: equalToken.loc.start,
message: 'There should be no space after \'=\'',
fix: function(fixer) {
return fixer.removeRange([equalToken.end, attrNode.value.range[0]]);
}
});
}
break;
case 'always':
if (!spacedBefore) {
context.report({
node: attrNode,
loc: equalToken.loc.start,
message: 'A space is required before \'=\'',
fix: function(fixer) {
return fixer.insertTextBefore(equalToken, ' ');
}
});
}
if (!spacedAfter) {
context.report({
node: attrNode,
loc: equalToken.loc.start,
message: 'A space is required after \'=\'',
fix: function(fixer) {
return fixer.insertTextAfter(equalToken, ' ');
}
});
}
break;
}
});
}
};
}
};

View File

@@ -0,0 +1,89 @@
/**
* @fileoverview Restrict file extensions that may contain JSX
* @author Joe Lencioni
*/
'use strict';
const path = require('path');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const DEFAULTS = {
extensions: ['.jsx']
};
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Restrict file extensions that may contain JSX',
category: 'Stylistic Issues',
recommended: false
},
schema: [{
type: 'object',
properties: {
extensions: {
type: 'array',
items: {
type: 'string'
}
}
},
additionalProperties: false
}]
},
create: function(context) {
function getExtensionsConfig() {
return context.options[0] && context.options[0].extensions || DEFAULTS.extensions;
}
let invalidExtension;
let invalidNode;
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
JSXElement: function(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);
},
'Program:exit': function() {
if (!invalidNode) {
return;
}
context.report({
node: invalidNode,
message: `JSX not allowed in files with extension '${invalidExtension}'`
});
}
};
}
};

View File

@@ -0,0 +1,68 @@
/**
* @fileoverview Ensure proper position of the first property in JSX
* @author Joachim Seminck
*/
'use strict';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Ensure proper position of the first property in JSX',
category: 'Stylistic Issues',
recommended: false
},
fixable: 'code',
schema: [{
enum: ['always', 'never', 'multiline', 'multiline-multiprop']
}]
},
create: function (context) {
const configuration = context.options[0] || 'multiline-multiprop';
function isMultilineJSX(jsxNode) {
return jsxNode.loc.start.line < jsxNode.loc.end.line;
}
return {
JSXOpeningElement: function (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: function(fixer) {
return fixer.replaceTextRange([node.name.end, decl.start], '\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: function(fixer) {
return fixer.replaceTextRange([node.name.end, firstNode.start], ' ');
}
});
return;
}
}
return;
}
};
}
};

View File

@@ -0,0 +1,72 @@
/**
* @fileoverview Enforce event handler naming conventions in JSX
* @author Jake Marsh
*/
'use strict';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce event handler naming conventions in JSX',
category: 'Stylistic Issues',
recommended: false
},
schema: [{
type: 'object',
properties: {
eventHandlerPrefix: {
type: 'string'
},
eventHandlerPropPrefix: {
type: 'string'
}
},
additionalProperties: false
}]
},
create: function(context) {
const sourceCode = context.getSourceCode();
const configuration = context.options[0] || {};
const eventHandlerPrefix = configuration.eventHandlerPrefix || 'handle';
const eventHandlerPropPrefix = configuration.eventHandlerPropPrefix || 'on';
const EVENT_HANDLER_REGEX = new RegExp(`^((props\\.${eventHandlerPropPrefix})|((.*\\.)?${eventHandlerPrefix}))[A-Z].*$`);
const PROP_EVENT_HANDLER_REGEX = new RegExp(`^(${eventHandlerPropPrefix}[A-Z].*|ref)$`);
return {
JSXAttribute: function(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 = sourceCode.getText(node.value.expression).replace(/^this\.|.*::/, '');
if (propKey === 'ref') {
return;
}
const propIsEventHandler = PROP_EVENT_HANDLER_REGEX.test(propKey);
const propFnIsNamedCorrectly = EVENT_HANDLER_REGEX.test(propValue);
if (propIsEventHandler && !propFnIsNamedCorrectly) {
context.report({
node: node,
message: `Handler function for ${propKey} prop key must begin with '${eventHandlerPrefix}'`
});
} else if (propFnIsNamedCorrectly && !propIsEventHandler) {
context.report({
node: node,
message: `Prop key for ${propValue} must begin with '${eventHandlerPropPrefix}'`
});
}
}
};
}
};

View File

@@ -0,0 +1,178 @@
/**
* @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';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Validate props indentation in JSX',
category: 'Stylistic Issues',
recommended: false
},
fixable: 'code',
schema: [{
oneOf: [{
enum: ['tab']
}, {
type: 'integer'
}]
}]
},
create: function(context) {
const MESSAGE = 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.';
const extraColumnStart = 0;
let indentType = 'space';
let indentSize = 4;
const sourceCode = context.getSourceCode();
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';
}
}
/**
* 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: needed,
type: indentType,
characters: needed === 1 ? 'character' : 'characters',
gotten: gotten
};
if (loc) {
context.report({
node: node,
loc: loc,
message: MESSAGE,
data: msgContext
});
} else {
context.report({
node: node,
message: MESSAGE,
data: msgContext,
fix: function(fixer) {
return fixer.replaceTextRange([node.start - node.loc.start.column, node.start],
Array(needed + 1).join(indentType === 'space' ? ' ' : '\t'));
}
});
}
}
/**
* 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 = sourceCode.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;
}
/**
* Checks node is the first in its own start line. By default it looks by start line.
* @param {ASTNode} node The node to check
* @param {Boolean} [byEndLocation] Lookup based on start position or end
* @return {Boolean} true if its the first in the its start line
*/
function isNodeFirstInLine(node, byEndLocation) {
const firstToken = byEndLocation === true ? sourceCode.getLastToken(node, 1) : sourceCode.getTokenBefore(node);
const startLine = byEndLocation === true ? node.loc.end.line : node.loc.start.line;
const endLine = firstToken ? firstToken.loc.end.line : -1;
return startLine !== endLine;
}
/**
* Check indent for nodes list
* @param {ASTNode[]} nodes list of node objects
* @param {Number} indent needed indent
* @param {Boolean} excludeCommas skip comma on start of line
*/
function checkNodesIndent(nodes, indent, excludeCommas) {
nodes.forEach(node => {
const nodeIndent = getNodeIndent(node, false, excludeCommas);
if (
node.type !== 'ArrayExpression' && node.type !== 'ObjectExpression' &&
nodeIndent !== indent && isNodeFirstInLine(node)
) {
report(node, indent, nodeIndent);
}
});
}
return {
JSXOpeningElement: function(node) {
const elementIndent = getNodeIndent(node);
checkNodesIndent(node.attributes, elementIndent + indentSize);
}
};
}
};

View File

@@ -0,0 +1,266 @@
/**
* @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';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Validate JSX indentation',
category: 'Stylistic Issues',
recommended: false
},
fixable: 'whitespace',
schema: [{
oneOf: [{
enum: ['tab']
}, {
type: 'integer'
}]
}]
},
create: function(context) {
const MESSAGE = 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.';
const extraColumnStart = 0;
let indentType = 'space';
let indentSize = 4;
const sourceCode = context.getSourceCode();
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';
/**
* 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.start - node.loc.start.column, node.start],
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: needed,
type: indentType,
characters: needed === 1 ? 'character' : 'characters',
gotten: gotten
};
if (loc) {
context.report({
node: node,
loc: loc,
message: MESSAGE,
data: msgContext,
fix: getFixerFunction(node, needed)
});
} else {
context.report({
node: 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 = sourceCode.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;
}
/**
* Checks node is the first in its own start line. By default it looks by start line.
* @param {ASTNode} node The node to check
* @return {Boolean} true if its the first in the its start line
*/
function isNodeFirstInLine(node) {
let token = node;
do {
token = sourceCode.getTokenBefore(token);
} while (token.type === 'JSXText' && /^\s*$/.test(token.value));
const startLine = node.loc.start.line;
const endLine = token ? token.loc.end.line : -1;
return startLine !== endLine;
}
/**
* 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
);
}
/**
* 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 &&
sourceCode.getTokenBefore(node).value !== '('
);
}
/**
* 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 &&
isNodeFirstInLine(node) &&
!isCorrectRightInLogicalExp &&
!isCorrectAlternateInCondExp
) {
report(node, indent, nodeIndent);
}
}
return {
JSXOpeningElement: function(node) {
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.start);
prevToken = prevToken.type === 'Literal' ? 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 = 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)
) ? 0 : indentSize;
checkNodesIndent(node, parentElementIndent + indent);
},
JSXClosingElement: function(node) {
if (!node.parent) {
return;
}
const peerElementIndent = getNodeIndent(node.parent.openingElement);
checkNodesIndent(node, peerElementIndent);
},
JSXExpressionContainer: function(node) {
if (!node.parent) {
return;
}
const parentNodeIndent = getNodeIndent(node.parent);
checkNodesIndent(node, parentNodeIndent + indentSize);
}
};
}
};

View File

@@ -0,0 +1,82 @@
/**
* @fileoverview Report missing `key` props in iterators/collection literals.
* @author Ben Mosher
*/
'use strict';
// var Components = require('../util/Components');
const hasProp = require('jsx-ast-utils/hasProp');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Report missing `key` props in iterators/collection literals',
category: 'Possible Errors',
recommended: true
},
schema: []
},
create: function(context) {
function checkIteratorElement(node) {
if (node.type === 'JSXElement' && !hasProp(node.openingElement.attributes, 'key')) {
context.report({
node: node,
message: 'Missing "key" prop for element in iterator'
});
}
}
function getReturnStatement(body) {
return body.filter(item => item.type === 'ReturnStatement')[0];
}
return {
JSXElement: function(node) {
if (hasProp(node.openingElement.attributes, 'key')) {
return;
}
if (node.parent.type === 'ArrayExpression') {
context.report({
node: node,
message: 'Missing "key" prop for element in array'
});
}
},
// Array.prototype.map
CallExpression: function (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') {
checkIteratorElement(fn.body);
}
if (isFn || isArrFn) {
if (fn.body.type === 'BlockStatement') {
const returnStatement = getReturnStatement(fn.body.body);
if (returnStatement && returnStatement.argument) {
checkIteratorElement(returnStatement.argument);
}
}
}
}
};
}
};

View File

@@ -0,0 +1,102 @@
/**
* @fileoverview Limit maximum of props on a single line in JSX
* @author Yannick Croissant
*/
'use strict';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Limit maximum of props on a single line in JSX',
category: 'Stylistic Issues',
recommended: false
},
fixable: 'code',
schema: [{
type: 'object',
properties: {
maximum: {
type: 'integer',
minimum: 1
},
when: {
type: 'string',
enum: ['always', 'multiline']
}
}
}]
},
create: function (context) {
const sourceCode = context.getSourceCode();
const configuration = context.options[0] || {};
const maximum = configuration.maximum || 1;
const when = configuration.when || 'always';
function getPropName(propNode) {
if (propNode.type === 'JSXSpreadAttribute') {
return sourceCode.getText(propNode.argument);
}
return propNode.name.name;
}
function generateFixFunction(line, max) {
const output = [];
const front = line[0].start;
const back = line[line.length - 1].end;
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: function (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)
});
}
});
}
};
}
};

View File

@@ -0,0 +1,109 @@
/**
* @fileoverview Prevents usage of Function.prototype.bind and arrow functions
* in React component definition.
* @author Daniel Lo Nigro <dan.cx>
*/
'use strict';
const Components = require('../util/Components');
const propName = require('jsx-ast-utils/propName');
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevents usage of Function.prototype.bind and arrow functions in React component definition',
category: 'Best Practices',
recommended: false
},
schema: [{
type: 'object',
properties: {
allowArrowFunctions: {
default: false,
type: 'boolean'
},
allowBind: {
default: false,
type: 'boolean'
},
ignoreRefs: {
default: false,
type: 'boolean'
}
},
additionalProperties: false
}]
},
create: Components.detect((context, components, utils) => {
const configuration = context.options[0] || {};
return {
CallExpression: function(node) {
const callee = node.callee;
if (
!configuration.allowBind &&
(callee.type !== 'MemberExpression' || callee.property.name !== 'bind')
) {
return;
}
const ancestors = context.getAncestors(callee).reverse();
for (let i = 0, j = ancestors.length; i < j; i++) {
if (
!configuration.allowBind &&
(ancestors[i].type === 'MethodDefinition' && ancestors[i].key.name === 'render') ||
(ancestors[i].type === 'Property' && ancestors[i].key.name === 'render')
) {
if (utils.isReturningJSX(ancestors[i])) {
context.report({
node: callee,
message: 'JSX props should not use .bind()'
});
}
break;
}
}
},
JSXAttribute: function(node) {
const isRef = configuration.ignoreRefs && propName(node) === 'ref';
if (isRef || !node.value || !node.value.expression) {
return;
}
const valueNode = node.value.expression;
if (
!configuration.allowBind &&
valueNode.type === 'CallExpression' &&
valueNode.callee.type === 'MemberExpression' &&
valueNode.callee.property.name === 'bind'
) {
context.report({
node: node,
message: 'JSX props should not use .bind()'
});
} else if (
!configuration.allowArrowFunctions &&
valueNode.type === 'ArrowFunctionExpression'
) {
context.report({
node: node,
message: 'JSX props should not use arrow functions'
});
} else if (
!configuration.allowBind &&
valueNode.type === 'BindExpression'
) {
context.report({
node: node,
message: 'JSX props should not use ::'
});
}
}
};
})
};

View File

@@ -0,0 +1,48 @@
/**
* @fileoverview Comments inside children section of tag should be placed inside braces.
* @author Ben Vinegar
*/
'use strict';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Comments inside children section of tag should be placed inside braces',
category: 'Possible Errors',
recommended: true
},
schema: [{
type: 'object',
properties: {},
additionalProperties: false
}]
},
create: function(context) {
function reportLiteralNode(node) {
context.report(node, 'Comments inside children section of tag should be placed inside braces');
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
Literal: function(node) {
if (/^\s*\/(\/|\*)/m.test(node.value)) {
// inside component, e.g. <div>literal</div>
if (node.parent.type !== 'JSXAttribute' &&
node.parent.type !== 'JSXExpressionContainer' &&
node.parent.type.indexOf('JSX') !== -1) {
reportLiteralNode(node);
}
}
}
};
}
};

View File

@@ -0,0 +1,68 @@
/**
* @fileoverview Enforce no duplicate props
* @author Markus Ånöstam
*/
'use strict';
const has = require('has');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce no duplicate props',
category: 'Possible Errors',
recommended: true
},
schema: [{
type: 'object',
properties: {
ignoreCase: {
type: 'boolean'
}
},
additionalProperties: false
}]
},
create: function (context) {
const configuration = context.options[0] || {};
const ignoreCase = configuration.ignoreCase || false;
return {
JSXOpeningElement: function (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;
}
});
}
};
}
};

View File

@@ -0,0 +1,77 @@
/**
* @fileoverview Prevent using string literals in React component definition
* @author Caleb Morris
* @author David Buchan-Swanson
*/
'use strict';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent using string literals in React component definition',
category: 'Stylistic Issues',
recommended: false
},
schema: [{
type: 'object',
properties: {
noStrings: {
type: 'boolean'
}
},
additionalProperties: false
}]
},
create: function(context) {
const isNoStrings = context.options[0] ? context.options[0].noStrings : false;
const message = isNoStrings ?
'Strings not allowed in JSX files' :
'Missing JSX expression container around literal string';
function reportLiteralNode(node) {
context.report({
node: node,
message: message
});
}
function getValidation(node) {
const standard = !/^[\s]+$/.test(node.value) &&
typeof node.value === 'string' &&
node.parent &&
node.parent.type.indexOf('JSX') !== -1 &&
node.parent.type !== 'JSXAttribute';
if (isNoStrings) {
return standard;
}
return standard && node.parent.type !== 'JSXExpressionContainer';
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
Literal: function(node) {
if (getValidation(node)) {
reportLiteralNode(node);
}
},
TemplateLiteral: function(node) {
if (isNoStrings && node.parent.type === 'JSXExpressionContainer') {
reportLiteralNode(node);
}
}
};
}
};

View File

@@ -0,0 +1,62 @@
/**
* @fileoverview Forbid target='_blank' attribute
* @author Kevin Miller
*/
'use strict';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function isTargetBlank(attr) {
return attr.name.name === 'target' &&
attr.value.type === 'Literal' &&
attr.value.value.toLowerCase() === '_blank';
}
function hasExternalLink(element) {
return element.attributes.some(attr => attr.name &&
attr.name.name === 'href' &&
attr.value.type === 'Literal' &&
/^(?:\w+:|\/\/)/.test(attr.value.value));
}
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
},
schema: []
},
create: function(context) {
return {
JSXAttribute: function(node) {
if (node.parent.name.name !== 'a') {
return;
}
if (
isTargetBlank(node) &&
hasExternalLink(node.parent) &&
!hasSecureRel(node.parent)
) {
context.report(node, 'Using target="_blank" without rel="noopener noreferrer" ' +
'is a security risk: see https://mathiasbynens.github.io/rel-noopener');
}
}
};
}
};

View File

@@ -0,0 +1,116 @@
/**
* @fileoverview Disallow undeclared variables in JSX
* @author Yannick Croissant
*/
'use strict';
/**
* Checks if a node name match the JSX tag convention.
* @param {String} name - Name of the node to check.
* @returns {boolean} Whether or not the node name match the JSX tag convention.
*/
const tagConvention = /^[a-z]|\-/;
function isTagName(name) {
return tagConvention.test(name);
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Disallow undeclared variables in JSX',
category: 'Possible Errors',
recommended: true
},
schema: [{
type: 'object',
properties: {
allowGlobals: {
type: 'boolean'
}
},
additionalProperties: false
}]
},
create: function(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: node,
message: `'${node.name}' is not defined.`
});
}
return {
JSXOpeningElement: function(node) {
switch (node.name.type) {
case 'JSXIdentifier':
node = node.name;
if (isTagName(node.name)) {
return;
}
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);
}
};
}
};

View File

@@ -0,0 +1,74 @@
/**
* @fileoverview Enforce PascalCase for user-defined JSX components
* @author Jake Marsh
*/
'use strict';
const elementType = require('jsx-ast-utils/elementType');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const PASCAL_CASE_REGEX = /^([A-Z0-9]|[A-Z0-9]+[a-z0-9]+(?:[A-Z0-9]+[a-z0-9]*)*)$/;
const COMPAT_TAG_REGEX = /^[a-z]|\-/;
const ALL_CAPS_TAG_REGEX = /^[A-Z0-9]+$/;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce PascalCase for user-defined JSX components',
category: 'Stylistic Issues',
recommended: false
},
schema: [{
type: 'object',
properties: {
allowAllCaps: {
type: 'boolean'
},
ignore: {
type: 'array'
}
},
additionalProperties: false
}]
},
create: function(context) {
const configuration = context.options[0] || {};
const allowAllCaps = configuration.allowAllCaps || false;
const ignore = configuration.ignore || [];
return {
JSXOpeningElement: function(node) {
let name = elementType(node);
// 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 = COMPAT_TAG_REGEX.test(name);
const isAllowedAllCaps = allowAllCaps && ALL_CAPS_TAG_REGEX.test(name);
const isIgnored = ignore.indexOf(name) !== -1;
if (!isPascalCase && !isCompatTag && !isAllowedAllCaps && !isIgnored) {
context.report({
node: node,
message: `Imported JSX component ${name} must be in PascalCase`
});
}
}
};
}
};

View File

@@ -0,0 +1,314 @@
/**
* @fileoverview Enforce props alphabetical sorting
* @author Ilya Volodin, Yannick Croissant
*/
'use strict';
const elementType = require('jsx-ast-utils/elementType');
const propName = require('jsx-ast-utils/propName');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function isCallbackPropName(name) {
return /^on[A-Z]/.test(name);
}
const COMPAT_TAG_REGEX = /^[a-z]|\-/;
function isDOMComponent(node) {
let name = elementType(node);
// 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('.'));
}
return COMPAT_TAG_REGEX.test(name);
}
const RESERVED_PROPS_LIST = [
'children',
'dangerouslySetInnerHTML',
'key',
'ref'
];
function isReservedPropName(name, list) {
return list.indexOf(name) >= 0;
}
function alphabeticalCompare(a, b, ignoreCase) {
if (ignoreCase) {
a = a.toLowerCase();
b = b.toLowerCase();
}
return a.localeCompare(b);
}
/**
* 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) => {
const sourceCode = context.getSourceCode();
const attributes = node.attributes.slice(0);
const configuration = context.options[0] || {};
const ignoreCase = configuration.ignoreCase || 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 sortableAttributeGroups = getGroupsOfSortableAttributes(attributes);
const sortedAttributeGroups = sortableAttributeGroups.slice(0).map(group =>
group.slice(0).sort((a, b) =>
alphabeticalCompare(propName(a), propName(b), ignoreCase)
)
);
return function(fixer) {
const fixers = [];
// 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(
fixer.replaceTextRange([attr.start, attr.end], sortedAttrText)
);
});
});
return fixers;
};
};
/**
* 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} 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
// eslint-disable-next-line consistent-return
const nonReservedWords = reservedFirst.filter(word => {
if (!isReservedPropName(word, RESERVED_PROPS_LIST)) {
return true;
}
});
if (reservedFirst.length === 0) {
return function(decl) {
context.report({
node: decl,
message: 'A customized reserved first list must not be empty'
});
};
} else 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
},
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: function(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: function(node) {
// `dangerouslySetInnerHTML` is only "reserved" on DOM components
if (reservedFirst && !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) {
if (!noSortAlphabetically && currentPropName < previousPropName) {
context.report({
node: decl,
message: 'Props should be sorted alphabetically',
fix: generateFixerFunction(node, context)
});
return memo;
}
return decl;
}
if (!previousIsReserved && currentIsReserved) {
context.report({
node: decl,
message: 'Reserved props must be listed before all other props'
});
return memo;
}
return decl;
}
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,
message: 'Callbacks must be listed after all other props'
});
return memo;
}
}
if (shorthandFirst) {
if (currentValue && !previousValue) {
return decl;
}
if (!currentValue && previousValue) {
context.report({
node: memo,
message: 'Shorthand props must be listed before all other props'
});
return memo;
}
}
if (shorthandLast) {
if (!currentValue && previousValue) {
return decl;
}
if (currentValue && !previousValue) {
context.report({
node: memo,
message: 'Shorthand props must be listed after all other props'
});
return memo;
}
}
if (!noSortAlphabetically && currentPropName < previousPropName) {
context.report({
node: decl,
message: 'Props should be sorted alphabetically',
fix: generateFixerFunction(node, context)
});
return memo;
}
return decl;
}, node.attributes[0]);
}
};
}
};

View File

@@ -0,0 +1,88 @@
/**
* @fileoverview Validate spacing before closing bracket in JSX.
* @author ryym
* @deprecated
*/
'use strict';
const getTokenBeforeClosingBracket = require('../util/getTokenBeforeClosingBracket');
let isWarnedForDeprecation = false;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
deprecated: true,
docs: {
description: 'Validate spacing before closing bracket in JSX',
category: 'Stylistic Issues',
recommended: false
},
fixable: 'code',
schema: [{
enum: ['always', 'never']
}]
},
create: function(context) {
const configuration = context.options[0] || 'always';
const sourceCode = context.getSourceCode();
const NEVER_MESSAGE = 'A space is forbidden before closing bracket';
const ALWAYS_MESSAGE = 'A space is required before closing bracket';
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
JSXOpeningElement: function(node) {
if (!node.selfClosing) {
return;
}
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: function(fixer) {
return fixer.insertTextBefore(closingSlash, ' ');
}
});
} else if (configuration === 'never' && sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
context.report({
loc: closingSlash.loc.start,
message: NEVER_MESSAGE,
fix: function(fixer) {
const previousToken = sourceCode.getTokenBefore(closingSlash);
return fixer.removeRange([previousToken.range[1], closingSlash.range[0]]);
}
});
}
},
Program: function() {
if (isWarnedForDeprecation || /\=-(f|-format)=/.test(process.argv.join('='))) {
return;
}
/* eslint-disable no-console */
console.log('The react/jsx-space-before-closing rule is deprecated. ' +
'Please use the react/jsx-tag-spacing rule with the ' +
'"beforeSelfClosing" option instead.');
/* eslint-enable no-console */
isWarnedForDeprecation = true;
}
};
}
};

View File

@@ -0,0 +1,236 @@
/**
* @fileoverview Validates whitespace in and around the JSX opening and closing brackets
* @author Diogo Franco (Kovensky)
*/
'use strict';
const has = require('has');
const getTokenBeforeClosingBracket = require('../util/getTokenBeforeClosingBracket');
// ------------------------------------------------------------------------------
// 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: node,
loc: {
start: lastTokens[0].loc.start,
end: lastTokens[1].loc.end
},
message: SELF_CLOSING_NEVER_MESSAGE,
fix: function(fixer) {
return fixer.removeRange([lastTokens[0].range[1], lastTokens[1].range[0]]);
}
});
}
} else if (option === 'always' && adjacent) {
context.report({
node: node,
loc: {
start: lastTokens[0].loc.start,
end: lastTokens[1].loc.end
},
message: SELF_CLOSING_ALWAYS_MESSAGE,
fix: function(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: node,
loc: {
start: firstTokens[0].loc.start,
end: firstTokens[1].loc.end
},
message: NEVER_MESSAGE,
fix: function(fixer) {
return fixer.removeRange([firstTokens[0].range[1], firstTokens[1].range[0]]);
}
});
}
} else if (option === 'always' && adjacent) {
context.report({
node: node,
loc: {
start: firstTokens[0].loc.start,
end: firstTokens[1].loc.end
},
message: ALWAYS_MESSAGE,
fix: function(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: node,
loc: closingSlash.loc.start,
message: ALWAYS_MESSAGE,
fix: function(fixer) {
return fixer.insertTextBefore(closingSlash, ' ');
}
});
} else if (option === 'never' && sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
context.report({
node: node,
loc: closingSlash.loc.start,
message: NEVER_MESSAGE,
fix: function(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: node,
loc: {
start: openingToken.loc.start,
end: node.name.loc.start
},
message: NEVER_MESSAGE,
fix: function(fixer) {
return fixer.removeRange([openingToken.range[1], node.name.range[0]]);
}
});
}
} else if (option === 'always' && adjacent) {
context.report({
node: node,
loc: {
start: openingToken.loc.start,
end: node.name.loc.start
},
message: ALWAYS_MESSAGE,
fix: function(fixer) {
return fixer.insertTextBefore(node.name, ' ');
}
});
}
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {},
fixable: 'whitespace',
schema: [
{
type: 'object',
properties: {
closingSlash: {
enum: ['always', 'never', 'allow']
},
beforeSelfClosing: {
enum: ['always', 'never', 'allow']
},
afterOpening: {
enum: ['always', 'allow-multiline', 'never', 'allow']
}
},
default: {
closingSlash: 'never',
beforeSelfClosing: 'always',
afterOpening: 'never'
},
additionalProperties: false
}
]
},
create: function (context) {
const options = {
closingSlash: 'never',
beforeSelfClosing: 'always',
afterOpening: 'never'
};
for (const key in options) {
if (has(options, key) && has(context.options[0] || {}, key)) {
options[key] = context.options[0][key];
}
}
return {
JSXOpeningElement: function (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);
}
},
JSXClosingElement: function (node) {
if (options.afterOpening !== 'allow') {
validateAfterOpening(context, node, options.afterOpening);
}
if (options.closingSlash !== 'allow') {
validateClosingSlash(context, node, options.closingSlash);
}
}
};
}
};

View File

@@ -0,0 +1,38 @@
/**
* @fileoverview Prevent React to be marked as unused
* @author Glen Mailer
*/
'use strict';
const pragmaUtil = require('../util/pragma');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent React to be marked as unused',
category: 'Best Practices',
recommended: true
},
schema: []
},
create: function(context) {
const pragma = pragmaUtil.getFromContext(context);
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
JSXOpeningElement: function() {
context.markVariableAsUsed(pragma);
}
};
}
};

View File

@@ -0,0 +1,47 @@
/**
* @fileoverview Prevent variables used in JSX to be marked as unused
* @author Yannick Croissant
*/
'use strict';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent variables used in JSX to be marked as unused',
category: 'Best Practices',
recommended: true
},
schema: []
},
create: function(context) {
return {
JSXOpeningElement: function(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);
}
};
}
};

View File

@@ -0,0 +1,138 @@
/**
* @fileoverview Prevent missing parentheses around multilines JSX
* @author Yannick Croissant
*/
'use strict';
const has = require('has');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const DEFAULTS = {
declaration: true,
assignment: true,
return: true,
arrow: true
};
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent missing parentheses around multilines JSX',
category: 'Stylistic Issues',
recommended: false
},
fixable: 'code',
schema: [{
type: 'object',
properties: {
declaration: {
type: 'boolean'
},
assignment: {
type: 'boolean'
},
return: {
type: 'boolean'
},
arrow: {
type: 'boolean'
}
},
additionalProperties: false
}]
},
create: function(context) {
const sourceCode = context.getSourceCode();
function isParenthesised(node) {
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 isMultilines(node) {
return node.loc.start.line !== node.loc.end.line;
}
function check(node) {
if (!node || node.type !== 'JSXElement') {
return;
}
if (!isParenthesised(node) && isMultilines(node)) {
context.report({
node: node,
message: 'Missing parentheses around multilines JSX',
fix: function(fixer) {
return fixer.replaceText(node, `(${sourceCode.getText(node)})`);
}
});
}
}
function isEnabled(type) {
const userOptions = context.options[0] || {};
if (has(userOptions, type)) {
return userOptions[type];
}
return DEFAULTS[type];
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
VariableDeclarator: function(node) {
if (!isEnabled('declaration')) {
return;
}
if (node.init && node.init.type === 'ConditionalExpression') {
check(node.init.consequent);
check(node.init.alternate);
return;
}
check(node.init);
},
AssignmentExpression: function(node) {
if (!isEnabled('assignment')) {
return;
}
if (node.right.type === 'ConditionalExpression') {
check(node.right.consequent);
check(node.right.alternate);
return;
}
check(node.right);
},
ReturnStatement: function(node) {
if (isEnabled('return')) {
check(node.argument);
}
},
'ArrowFunctionExpression:exit': function (node) {
const arrowBody = node.body;
if (isEnabled('arrow') && arrowBody.type !== 'BlockStatement') {
check(arrowBody);
}
}
};
}
};

View File

@@ -0,0 +1,198 @@
/**
* @fileoverview Prevent usage of Array index in keys
* @author Joe Lencioni
*/
'use strict';
const has = require('has');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent usage of Array index in keys',
category: 'Best Practices',
recommended: false
},
schema: []
},
create: function(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 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 firstArg = node.arguments[0];
if (!firstArg) {
return null;
}
const isFunction = [
'ArrowFunctionExpression',
'FunctionExpression'
].indexOf(firstArg.type) !== -1;
if (!isFunction) {
return null;
}
const params = firstArg.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: node,
message: ERROR_MESSAGE
});
return;
}
if (node.type === 'TemplateLiteral') {
// key={`foo-${bar}`}
node.expressions.filter(isArrayIndex).forEach(() => {
context.report({node: node, message: ERROR_MESSAGE});
});
return;
}
if (node.type === 'BinaryExpression') {
// key={'foo' + bar}
const identifiers = getIdentifiersFromBinaryExpression(node);
identifiers.filter(isArrayIndex).forEach(() => {
context.report({node: node, message: ERROR_MESSAGE});
});
return;
}
}
return {
CallExpression: function(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: function(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();
}
};
}
};

View File

@@ -0,0 +1,67 @@
/**
* @fileoverview Prevent passing of children as props
* @author Benjamin Stepp
*/
'use strict';
// ------------------------------------------------------------------------------
// 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
},
schema: []
},
create: function(context) {
return {
JSXAttribute: function(node) {
if (node.name.name !== 'children') {
return;
}
context.report({
node: node,
message: 'Do not pass children as props. Instead, nest children between the opening and closing tags.'
});
},
CallExpression: function(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: node,
message: 'Do not pass children as props. Instead, pass them as additional arguments to React.createElement.'
});
}
}
};
}
};

View File

@@ -0,0 +1,131 @@
/**
* @fileoverview Report when a DOM element is using both children and dangerouslySetInnerHTML
* @author David Petersen
*/
'use strict';
const variableUtil = require('../util/variable');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Report when a DOM element is using both children and dangerouslySetInnerHTML',
category: '',
recommended: true
},
schema: [] // no options
},
create: function(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
*/
function findObjectProp(node, propName) {
if (!node.properties) {
return false;
}
return node.properties.find(prop => {
if (prop.type === 'Property') {
return prop.key.name === propName;
} else if (prop.type === 'ExperimentalSpreadProperty') {
const variable = findSpreadVariable(prop.argument.name);
if (variable && variable.defs.length && variable.defs[0].node.init) {
return findObjectProp(variable.defs[0].node.init, propName);
}
}
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
*/
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';
const isMultiline = node.loc.start.line !== node.loc.end.line;
const isWhiteSpaces = /^\s*$/.test(node.value);
return isLiteral && isMultiline && isWhiteSpaces;
}
return {
JSXElement: function (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, 'Only set one of `children` or `props.dangerouslySetInnerHTML`');
}
},
CallExpression: function (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, 'Only set one of `children` or `props.dangerouslySetInnerHTML`');
}
}
}
};
}
};

View File

@@ -0,0 +1,76 @@
/**
* @fileoverview Prevent usage of dangerous JSX props
* @author Scott Andrews
*/
'use strict';
// ------------------------------------------------------------------------------
// 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 node name match the JSX tag convention.
* @param {String} name - Name of the node to check.
* @returns {boolean} Whether or not the node name match the JSX tag convention.
*/
const tagConvention = /^[a-z]|\-/;
function isTagName(name) {
return tagConvention.test(name);
}
/**
* 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
},
schema: []
},
create: function(context) {
return {
JSXAttribute: function(node) {
if (isTagName(node.parent.name.name) && isDangerous(node.name.name)) {
context.report({
node: node,
message: DANGEROUS_MESSAGE,
data: {
name: node.name.name
}
});
}
}
};
}
};

View File

@@ -0,0 +1,163 @@
/**
* @fileoverview Prevent usage of deprecated methods
* @author Yannick Croissant
* @author Scott Feeney
*/
'use strict';
const has = require('has');
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}}';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent usage of deprecated methods',
category: 'Best Practices',
recommended: true
},
schema: []
},
create: function(context) {
const sourceCode = context.getSourceCode();
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}.PropTypes`] = ['15.5.0', 'the npm module prop-types'];
return deprecated;
}
function isDeprecated(method) {
const deprecated = getDeprecated();
return (
deprecated &&
deprecated[method] &&
versionUtil.testReactVersion(context, deprecated[method][0])
);
}
function checkDeprecation(node, method) {
if (!isDeprecated(method)) {
return;
}
const deprecated = getDeprecated();
context.report({
node: node,
message: DEPRECATED_MESSAGE,
data: {
oldMethod: method,
version: deprecated[method][0],
newMethod: deprecated[method][1] ? `, use ${deprecated[method][1]} instead` : ''
}
});
}
function getReactModuleName(node) {
let moduleName = false;
if (!node.init) {
return moduleName;
}
for (const module in MODULES) {
if (!has(MODULES, module)) {
continue;
}
moduleName = MODULES[module].find(name => name === node.init.name);
if (moduleName) {
break;
}
}
return moduleName;
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
MemberExpression: function(node) {
checkDeprecation(node, sourceCode.getText(node));
},
ImportDeclaration: function(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: function(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}`);
});
}
};
}
};

View File

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

View File

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

View File

@@ -0,0 +1,153 @@
/**
* @fileoverview Prevent direct mutation of this.state
* @author David Petersen
* @author Nicolas Fernandez <@burabure>
*/
'use strict';
const Components = require('../util/Components');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent direct mutation of this.state',
category: 'Possible Errors',
recommended: true
}
},
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 this MemberExpression is for `this.state`
* @param {Object} node The node to process
* @returns {Boolean}
*/
function isStateMemberExpression(node) {
return node.object.type === 'ThisExpression' && node.property.name === 'state';
}
/**
* 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: function(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 (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 (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]);
}
});
}
};
})
};

View File

@@ -0,0 +1,47 @@
/**
* @fileoverview Prevent usage of findDOMNode
* @author Yannick Croissant
*/
'use strict';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent usage of findDOMNode',
category: 'Best Practices',
recommended: true
},
schema: []
},
create: function(context) {
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
CallExpression: function(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'
});
}
};
}
};

View File

@@ -0,0 +1,49 @@
/**
* @fileoverview Prevent usage of isMounted
* @author Joe Lencioni
*/
'use strict';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent usage of isMounted',
category: 'Best Practices',
recommended: true
},
schema: []
},
create: function(context) {
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
CallExpression: function(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;
}
}
}
};
}
};

View File

@@ -0,0 +1,74 @@
/**
* @fileoverview Prevent multiple component definition per file
* @author Yannick Croissant
*/
'use strict';
const has = require('has');
const Components = require('../util/Components');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent multiple component definition per file',
category: 'Stylistic Issues',
recommended: false
},
schema: [{
type: 'object',
properties: {
ignoreStateless: {
default: false,
type: 'boolean'
}
},
additionalProperties: false
}]
},
create: Components.detect((context, components) => {
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);
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
'Program:exit': function() {
if (components.length() <= 1) {
return;
}
const list = components.list();
let i = 0;
for (const component in list) {
if (!has(list, component) || isIgnored(list[component]) || ++i === 1) {
continue;
}
context.report({
node: list[component].node,
message: MULTI_COMP_MESSAGE
});
}
}
};
})
};

View File

@@ -0,0 +1,108 @@
/**
* @fileoverview Flag shouldComponentUpdate when extending PureComponent
*/
'use strict';
const Components = require('../util/Components');
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
},
schema: []
},
create: Components.detect((context, components, utils) => {
/**
* Get properties name
* @param {Object} node - Property.
* @returns {String} Property name.
*/
function getPropertyName(node) {
if (node.key) {
return node.key.name;
} else if (node.type === 'ClassProperty') {
// Special case for class properties
// (babel-eslint does not expose property name so we have to rely on tokens)
const tokens = context.getFirstTokens(node, 2);
return tokens[1] && tokens[1].type === 'Identifier' ? tokens[1].value : tokens[0].value;
}
return '';
}
/**
* 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 'ClassExpression':
case 'ClassDeclaration':
return node.body.body;
default:
return [];
}
}
/**
* 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 = getComponentProperties(node);
return properties.some(property => {
const name = 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;
} else 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: node,
message: errorMessage(className)
});
}
}
}
return {
ClassDeclaration: checkForViolation,
ClassExpression: checkForViolation
};
})
};

View File

@@ -0,0 +1,68 @@
/**
* @fileoverview Prevent usage of the return value of React.render
* @author Dustan Kasten
*/
'use strict';
const versionUtil = require('../util/version');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent usage of the return value of React.render',
category: 'Best Practices',
recommended: true
},
schema: []
},
create: function(context) {
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
CallExpression: function(node) {
const callee = node.callee;
const parent = node.parent;
if (callee.type !== 'MemberExpression') {
return;
}
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$/;
}
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`
});
}
}
};
}
};

View File

@@ -0,0 +1,84 @@
/**
* @fileoverview Prevent usage of setState
* @author Mark Dalgleish
*/
'use strict';
const has = require('has');
const Components = require('../util/Components');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent usage of setState',
category: 'Stylistic Issues',
recommended: false
},
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: function(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: setStateUsages
});
},
'Program:exit': function() {
const list = components.list();
for (const component in list) {
if (!has(list, component) || isValid(list[component])) {
continue;
}
reportSetStateUsages(list[component]);
}
}
};
})
};

View File

@@ -0,0 +1,103 @@
/**
* @fileoverview Prevent string definitions for references and prevent referencing this.refs
* @author Tom Hastjarjanto
*/
'use strict';
const Components = require('../util/Components');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent string definitions for references and prevent referencing this.refs',
category: 'Best Practices',
recommended: true
},
schema: []
},
create: Components.detect((context, components, utils) => {
/**
* 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'
);
}
return {
MemberExpression: function(node) {
if (isRefsUsage(node)) {
context.report({
node: node,
message: 'Using this.refs is deprecated.'
});
}
},
JSXAttribute: function(node) {
if (
isRefAttribute(node) &&
(containsStringLiteral(node) || containsStringExpressionContainer(node))
) {
context.report({
node: node,
message: 'Using string literals in ref attributes is deprecated.'
});
}
}
};
})
};

View File

@@ -0,0 +1,148 @@
/**
* @fileoverview Prevent common casing typos
*/
'use strict';
const Components = require('../util/Components');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const STATIC_CLASS_PROPERTIES = ['propTypes', 'contextTypes', 'childContextTypes', 'defaultProps'];
const LIFECYCLE_METHODS = [
'componentWillMount',
'componentDidMount',
'componentWillReceiveProps',
'shouldComponentUpdate',
'componentWillUpdate',
'componentDidUpdate',
'componentWillUnmount',
'render'
];
const PROP_TYPES = Object.keys(require('prop-types'));
module.exports = {
meta: {
docs: {
description: 'Prevent common typos',
category: 'Stylistic Issues',
recommended: false
},
schema: []
},
create: Components.detect((context, components, utils) => {
function checkValidPropTypeQualfier(node) {
if (node.name !== 'isRequired') {
context.report({
node: 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: node,
message: `Typo in declared prop type: ${node.name}`
});
}
}
/* eslint-disable no-use-before-define */
function checkValidProp(node) {
if (node && node.type === 'MemberExpression' && node.object.type === 'MemberExpression') {
checkValidPropType(node.object.property);
checkValidPropTypeQualfier(node.property);
} else if (node && node.type === 'MemberExpression' && node.object.type === 'Identifier') {
checkValidPropType(node.property);
} else if (node && node.type === 'CallExpression') {
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 checkValidPropObject (node) {
if (node.type === 'ObjectExpression') {
node.properties.forEach(prop => checkValidProp(prop.value));
}
}
/* eslint-enable no-use-before-define */
function reportErrorIfClassPropertyCasingTypo(node, propertyName) {
if (propertyName === 'propTypes' || propertyName === 'contextTypes' || propertyName === 'childContextTypes') {
const propsNode = node && node.parent && node.parent.type === 'AssignmentExpression' && node.parent.right;
checkValidPropObject(propsNode);
}
STATIC_CLASS_PROPERTIES.forEach(CLASS_PROP => {
if (propertyName && CLASS_PROP.toLowerCase() === propertyName.toLowerCase() && CLASS_PROP !== propertyName) {
context.report({
node: node,
message: 'Typo in static class property declaration'
});
}
});
}
function reportErrorIfLifecycleMethodCasingTypo(node) {
LIFECYCLE_METHODS.forEach(method => {
if (method.toLowerCase() === node.key.name.toLowerCase() && method !== node.key.name) {
context.report({
node: node,
message: 'Typo in component lifecycle method declaration'
});
}
});
}
return {
ClassProperty: function(node) {
if (!node.static || !utils.isES6Component(node.parent.parent)) {
return;
}
const tokens = context.getFirstTokens(node, 2);
const propertyName = tokens[1].value;
reportErrorIfClassPropertyCasingTypo(node, propertyName);
},
MemberExpression: function(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))
) {
reportErrorIfClassPropertyCasingTypo(node, propertyName);
}
},
MethodDefinition: function (node) {
if (!utils.isES6Component(node.parent.parent)) {
return;
}
reportErrorIfLifecycleMethodCasingTypo(node);
}
};
})
};

View File

@@ -0,0 +1,78 @@
/**
* @fileoverview HTML special characters should be escaped.
* @author Patrick Hayes
*/
'use strict';
// ------------------------------------------------------------------------------
// 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 = ['>', '"', '\'', '}'];
module.exports = {
meta: {
docs: {
description: 'Detect unescaped HTML entities, which might represent malformed tags',
category: 'Possible Errors',
recommended: true
},
schema: [{
type: 'object',
properties: {
forbid: {
type: 'array',
items: {
type: 'string'
}
}
},
additionalProperties: false
}]
},
create: function(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 (c === entities[j]) {
context.report({
loc: {line: i, column: start + index},
message: 'HTML entities must be escaped.',
node: node
});
}
}
}
}
}
return {
Literal: function(node) {
if (node.type === 'Literal' && node.parent.type === 'JSXElement') {
reportInvalidEntity(node);
}
}
};
}
};

View File

@@ -0,0 +1,227 @@
/**
* @fileoverview Prevent usage of unknown DOM property
* @author Yannick Croissant
*/
'use strict';
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const DEFAULTS = {
ignore: []
};
const UNKNOWN_MESSAGE = 'Unknown property \'{{name}}\' found, use \'{{standardName}}\' instead';
const DOM_ATTRIBUTE_NAMES = {
'accept-charset': 'acceptCharset',
class: 'className',
for: 'htmlFor',
'http-equiv': 'httpEquiv'
};
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', 'charSet', 'classID', 'className', 'colSpan', 'contentEditable', 'contextMenu',
'crossOrigin', '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;
}
/**
* 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;
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
},
fixable: 'code',
schema: [{
type: 'object',
properties: {
ignore: {
type: 'array',
items: {
type: 'string'
}
}
},
additionalProperties: false
}]
},
create: function(context) {
function getIgnoreConfig() {
return context.options[0] && context.options[0].ignore || DEFAULTS.ignore;
}
const sourceCode = context.getSourceCode();
return {
JSXAttribute: function(node) {
const ignoreNames = getIgnoreConfig();
const name = sourceCode.getText(node.name);
const standardName = getStandardName(name);
if (!isTagName(node) || !standardName || ignoreNames.indexOf(name) >= 0) {
return;
}
context.report({
node: node,
message: UNKNOWN_MESSAGE,
data: {
name: name,
standardName: standardName
},
fix: function(fixer) {
return fixer.replaceText(node.name, standardName);
}
});
}
};
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,350 @@
/**
* @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');
// 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;
} else if (type === 'Literal') {
return String(node.value);
} else 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
};
}
module.exports = {
meta: {
docs: {
description: 'Prevent definition of unused state fields',
category: 'Best Practices',
recommended: false
},
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;
// Returns true if the given node is possibly a reference to `this.state`.
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;
}
// 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' &&
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, `Unused state field: '${name}'`);
}
}
}
return {
ClassDeclaration(node) {
if (utils.isES6Component(node)) {
classInfo = getInitialClassInfo();
}
},
ObjectExpression(node) {
if (utils.isES5Component(node)) {
classInfo = getInitialClassInfo();
}
},
'ObjectExpression:exit'(node) {
if (!classInfo) {
return;
}
if (utils.isES5Component(node)) {
reportUnusedFields();
classInfo = null;
}
},
'ClassDeclaration:exit'() {
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 (
node.callee.type === 'MemberExpression' &&
isThisExpression(node.callee.object) &&
getName(node.callee.property) === 'setState' &&
node.arguments.length > 0 &&
node.arguments[0].type === 'ObjectExpression'
) {
addStateFields(node.arguments[0]);
}
},
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);
}
},
MethodDefinition() {
if (!classInfo) {
return;
}
// Create a new set for this.state aliases local to this method.
classInfo.aliases = new Set();
},
'MethodDefinition:exit'() {
if (!classInfo) {
return;
}
// Forget our set of local aliases.
classInfo.aliases = null;
},
FunctionExpression(node) {
if (!classInfo) {
return;
}
const parent = node.parent;
if (!utils.isES5Component(parent.parent)) {
return;
}
if (parent.key.name === 'getInitialState') {
const body = node.body.body;
const lastBodyNode = body[body.length - 1];
if (
lastBodyNode.type === 'ReturnStatement' &&
lastBodyNode.argument.type === 'ObjectExpression'
) {
addStateFields(lastBodyNode.argument);
}
} else {
// Create a new set for this.state aliases local to this method.
classInfo.aliases = new Set();
}
},
AssignmentExpression(node) {
if (!classInfo) {
return;
}
// 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);
}
},
JSXSpreadAttribute(node) {
if (classInfo && isStateReference(node.argument)) {
classInfo = null;
}
},
ExperimentalSpreadProperty(node) {
if (classInfo && isStateReference(node.argument)) {
classInfo = null;
}
}
};
})
};

View File

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

View File

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

View File

@@ -0,0 +1,414 @@
/**
* @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 has = require('has');
const Components = require('../util/Components');
const versionUtil = require('../util/version');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce stateless components to be written as a pure function',
category: 'Stylistic Issues',
recommended: false
},
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;
const sourceCode = context.getSourceCode();
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
/**
* Get properties name
* @param {Object} node - Property.
* @returns {String} Property name.
*/
function getPropertyName(node) {
// Special case for class properties
// (babel-eslint 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 node.key.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 'ClassExpression':
case 'ClassDeclaration':
return node.body.body;
case 'ObjectExpression':
return node.properties;
default:
return [];
}
}
/**
* 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 = getComponentProperties(node);
return properties.some(property => {
const name = getPropertyName(property);
const isDisplayName = name === 'displayName';
const isPropTypes = name === 'propTypes' || name === 'props' && property.typeAnnotation;
const contextTypes = name === 'contextTypes';
const isUselessConstructor =
property.kind === 'constructor' &&
isRedundantSuperCall(property.value.body.body, property.value.params)
;
const isRender = name === 'render';
return !isDisplayName && !isPropTypes && !contextTypes && !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: function(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 = getPropertyName(property);
return name !== 'props' && name !== 'context';
});
if (!useThis) {
markPropsOrContextAsUsed(node);
return;
}
markThisAsUsed(node);
},
// Mark `this` usage
MemberExpression: function(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;
}
return;
// Ignore calls to `this.props` and `this.context`
} else if (
(node.property.name || node.property.value) === 'props' ||
(node.property.name || node.property.value) === 'context'
) {
markPropsOrContextAsUsed(node);
return;
}
markThisAsUsed(node);
},
// Mark `ref` usage
JSXAttribute: function(node) {
const name = sourceCode.getText(node.name);
if (name !== 'ref') {
return;
}
markRefAsUsed(node);
},
// Mark `render` that do not return some JSX
ReturnStatement: function(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();
for (const component in list) {
if (
!has(list, component) ||
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))
) {
continue;
}
if (list[component].hasSCU && list[component].usePropsOrContext) {
continue;
}
context.report({
node: list[component].node,
message: 'Component should be written as a pure function'
});
}
}
};
})
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
/**
* @fileoverview Prevent missing React when using JSX
* @author Glen Mailer
*/
'use strict';
const variableUtil = require('../util/variable');
const pragmaUtil = require('../util/pragma');
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent missing React when using JSX',
category: 'Possible Errors',
recommended: true
},
schema: []
},
create: function(context) {
const pragma = pragmaUtil.getFromContext(context);
const NOT_DEFINED_MESSAGE = '\'{{name}}\' must be in scope when using JSX';
return {
JSXOpeningElement: function(node) {
const variables = variableUtil.variablesInScope(context);
if (variableUtil.findVariable(variables, pragma)) {
return;
}
context.report({
node: node,
message: NOT_DEFINED_MESSAGE,
data: {
name: pragma
}
});
}
};
}
};

View File

@@ -0,0 +1,592 @@
/**
* @fileOverview Enforce a defaultProps definition for every prop that is not a required prop.
* @author Vitor Balocco
*/
'use strict';
const has = require('has');
const Components = require('../util/Components');
const variableUtil = require('../util/variable');
const annotations = require('../util/annotations');
const QUOTES_REGEX = /^["']|["']$/g;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce a defaultProps definition for every prop that is not a required prop.',
category: 'Best Practices'
},
schema: []
},
create: Components.detect((context, components, utils) => {
const sourceCode = context.getSourceCode();
const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
/**
* 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;
} else 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)
} else 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 propTypes declaration.
* @param {ASTNode} node The node to check. Must be an Identifier node.
* @returns {Boolean} `true` if the node is a propTypes declaration, `false` if not
*/
function isPropTypesDeclaration(node) {
return getPropertyName(node) === 'propTypes';
}
/**
* 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) {
return (getPropertyName(node) === 'defaultProps' || getPropertyName(node) === 'getDefaultProps');
}
/**
* Checks if the PropTypes MemberExpression node passed in declares a required propType.
* @param {ASTNode} propTypeExpression node to check. Must be a `PropTypes` MemberExpression.
* @returns {Boolean} `true` if this PropType is required, `false` if not.
*/
function isRequiredPropType(propTypeExpression) {
return propTypeExpression.type === 'MemberExpression' && propTypeExpression.property.name === 'isRequired';
}
/**
* 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;
}
/**
* 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 findVariableByName(node.name);
}
if (
node.type === 'CallExpression' &&
propWrapperFunctions.has(node.callee.name) &&
node.arguments && node.arguments[0]
) {
return resolveNodeValue(node.arguments[0]);
}
return node;
}
/**
* Tries to find the definition of a GenericTypeAnnotation in the current scope.
* @param {ASTNode} node The node GenericTypeAnnotation node to resolve.
* @return {ASTNode|null} Return null if definition cannot be found, ASTNode otherwise.
*/
function resolveGenericTypeAnnotation(node) {
if (node.type !== 'GenericTypeAnnotation' || node.id.type !== 'Identifier') {
return null;
}
return findVariableByName(node.id.name);
}
function resolveUnionTypeAnnotation(node) {
// Go through all the union and resolve any generic types.
return node.types.map(annotation => {
if (annotation.type === 'GenericTypeAnnotation') {
return resolveGenericTypeAnnotation(annotation);
}
return annotation;
});
}
/**
* Extracts a PropType from an ObjectExpression node.
* @param {ASTNode} objectExpression ObjectExpression node.
* @returns {Object[]} Array of PropType object representations, to be consumed by `addPropTypesToComponent`.
*/
function getPropTypesFromObjectExpression(objectExpression) {
const props = objectExpression.properties.filter(property => property.type !== 'ExperimentalSpreadProperty');
return props.map(property => ({
name: sourceCode.getText(property.key).replace(QUOTES_REGEX, ''),
isRequired: isRequiredPropType(property.value),
node: property
}));
}
/**
* Extracts a PropType from a TypeAnnotation node.
* @param {ASTNode} node TypeAnnotation node.
* @returns {Object[]} Array of PropType object representations, to be consumed by `addPropTypesToComponent`.
*/
function getPropTypesFromTypeAnnotation(node) {
let properties;
switch (node.typeAnnotation.type) {
case 'GenericTypeAnnotation':
let annotation = resolveGenericTypeAnnotation(node.typeAnnotation);
if (annotation && annotation.id) {
annotation = findVariableByName(annotation.id.name);
}
properties = annotation ? (annotation.properties || []) : [];
break;
case 'UnionTypeAnnotation':
const union = resolveUnionTypeAnnotation(node.typeAnnotation);
properties = union.reduce((acc, curr) => {
if (!curr) {
return acc;
}
return acc.concat(curr.properties);
}, []);
break;
case 'ObjectTypeAnnotation':
properties = node.typeAnnotation.properties;
break;
default:
properties = [];
break;
}
const props = properties.filter(property => property.type === 'ObjectTypeProperty');
return props.map(property => {
// the `key` property is not present in ObjectTypeProperty nodes, so we need to get the key name manually.
const tokens = context.getFirstTokens(property, 1);
const name = tokens[0].value;
return {
name: name,
isRequired: !property.optional,
node: property
};
});
}
/**
* 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');
if (hasSpread) {
return 'unresolved';
}
return objectExpression.properties.map(property => sourceCode.getText(property.key).replace(QUOTES_REGEX, ''));
}
/**
* 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 propTypes to the component passed in.
* @param {ASTNode} component The component to add the propTypes to.
* @param {Object[]} propTypes propTypes to add to the component.
* @returns {void}
*/
function addPropTypesToComponent(component, propTypes) {
const props = component.propTypes || [];
components.set(component.node, {
propTypes: props.concat(propTypes)
});
}
/**
* Adds defaultProps to the component passed in.
* @param {ASTNode} component The component to add the defaultProps to.
* @param {String[]|String} 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 || {};
defaultProps.forEach(defaultProp => {
defaults[defaultProp] = true;
});
components.set(component.node, {
defaultProps: defaults
});
}
/**
* Tries to find a props type annotation in a stateless component.
* @param {ASTNode} node The AST node to look for a props type annotation.
* @return {void}
*/
function handleStatelessComponent(node) {
if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) {
return;
}
// find component this props annotation belongs to
const component = components.get(utils.getParentStatelessComponent());
if (!component) {
return;
}
addPropTypesToComponent(component, getPropTypesFromTypeAnnotation(node.params[0].typeAnnotation, context));
}
function handlePropTypeAnnotationClassProperty(node) {
// find component this props annotation belongs to
const component = components.get(utils.getParentES6Component());
if (!component) {
return;
}
addPropTypesToComponent(component, getPropTypesFromTypeAnnotation(node.typeAnnotation, context));
}
function isPropTypeAnnotation(node) {
return (getPropertyName(node) === 'props' && !!node.typeAnnotation);
}
/**
* Reports all propTypes passed in that don't have a defaultProp 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;
}
propTypes.forEach(prop => {
if (prop.isRequired) {
return;
}
if (defaultProps[prop.name]) {
return;
}
context.report(
prop.node,
'propType "{{name}}" is not required, but has no corresponding defaultProp declaration.',
{name: prop.name}
);
});
}
// --------------------------------------------------------------------------
// Public API
// --------------------------------------------------------------------------
return {
MemberExpression: function(node) {
const isPropType = isPropTypesDeclaration(node);
const isDefaultProp = isDefaultPropsDeclaration(node);
if (!isPropType && !isDefaultProp) {
return;
}
// find component this propTypes/defaultProps belongs to
const component = utils.getRelatedComponent(node);
if (!component) {
return;
}
// e.g.:
// MyComponent.propTypes = {
// foo: PropTypes.string.isRequired,
// bar: 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;
}
if (isPropType) {
addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression));
} else {
addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
}
return;
}
// e.g.:
// MyComponent.propTypes.baz = PropTypes.string;
if (node.parent.type === 'MemberExpression' && node.parent.parent.type === 'AssignmentExpression') {
if (isPropType) {
addPropTypesToComponent(component, [{
name: node.parent.property.name,
isRequired: isRequiredPropType(node.parent.parent.right),
node: node.parent.parent
}]);
} else {
addDefaultPropsToComponent(component, [node.parent.property.name]);
}
return;
}
},
// e.g.:
// class Hello extends React.Component {
// static get propTypes() {
// return {
// name: PropTypes.string
// };
// }
// static get defaultProps() {
// return {
// name: 'Dean'
// };
// }
// render() {
// return <div>Hello {this.props.name}</div>;
// }
// }
MethodDefinition: function(node) {
if (!node.static || node.kind !== 'get') {
return;
}
const isPropType = isPropTypesDeclaration(node);
const isDefaultProp = isDefaultPropsDeclaration(node);
if (!isPropType && !isDefaultProp) {
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;
}
if (isPropType) {
addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression));
} else {
addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
}
},
// e.g.:
// class Greeting extends React.Component {
// render() {
// return (
// <h1>Hello, {this.props.foo} {this.props.bar}</h1>
// );
// }
// static propTypes = {
// foo: PropTypes.string,
// bar: PropTypes.string.isRequired
// };
// }
ClassProperty: function(node) {
if (isPropTypeAnnotation(node)) {
handlePropTypeAnnotationClassProperty(node);
return;
}
if (!node.static) {
return;
}
if (!node.value) {
return;
}
const isPropType = getPropertyName(node) === 'propTypes';
const isDefaultProp = getPropertyName(node) === 'defaultProps' || getPropertyName(node) === 'getDefaultProps';
if (!isPropType && !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;
}
if (isPropType) {
addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression));
} else {
addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
}
},
// e.g.:
// createReactClass({
// render: function() {
// return <div>{this.props.foo}</div>;
// },
// propTypes: {
// foo: PropTypes.string.isRequired,
// },
// getDefaultProps: function() {
// return {
// foo: 'default'
// };
// }
// });
ObjectExpression: function(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') {
return;
}
const isPropType = isPropTypesDeclaration(property);
const isDefaultProp = isDefaultPropsDeclaration(property);
if (!isPropType && !isDefaultProp) {
return;
}
if (isPropType && property.value.type === 'ObjectExpression') {
addPropTypesToComponent(component, getPropTypesFromObjectExpression(property.value));
return;
}
if (isDefaultProp && property.value.type === 'FunctionExpression') {
const returnStatement = utils.findReturnStatement(property);
if (!returnStatement || returnStatement.argument.type !== 'ObjectExpression') {
return;
}
addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(returnStatement.argument));
}
});
},
// Check for type annotations in stateless components
FunctionDeclaration: handleStatelessComponent,
ArrowFunctionExpression: handleStatelessComponent,
FunctionExpression: handleStatelessComponent,
'Program:exit': function() {
const list = components.list();
for (const component in list) {
if (!has(list, component)) {
continue;
}
// If no propTypes could be found, we don't report anything.
if (!list[component].propTypes) {
continue;
}
reportPropTypesWithoutDefault(
list[component].propTypes,
list[component].defaultProps || {}
);
}
}
};
})
};

View File

@@ -0,0 +1,231 @@
/**
* @fileoverview Enforce React components to have a shouldComponentUpdate method
* @author Evgueni Naverniouk
*/
'use strict';
const has = require('has');
const Components = require('../util/Components');
module.exports = {
meta: {
docs: {
description: 'Enforce React components to have a shouldComponentUpdate method',
category: 'Best Practices',
recommended: false
},
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 isSCUDeclarеd = 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].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: function (node) {
// Stateless Functional Components cannot be optimized (yet)
markSCUAsDeclared(node);
},
ClassDeclaration: function (node) {
if (!(hasPureRenderDecorator(node) || hasCustomDecorator(node) || utils.isPureComponent(node))) {
return;
}
markSCUAsDeclared(node);
},
FunctionDeclaration: function (node) {
// Skip if the function is declared in the class
if (isFunctionInClass()) {
return;
}
// Stateless Functional Components cannot be optimized (yet)
markSCUAsDeclared(node);
},
FunctionExpression: function (node) {
// Skip if the function is declared in the class
if (isFunctionInClass()) {
return;
}
// Stateless Functional Components cannot be optimized (yet)
markSCUAsDeclared(node);
},
MethodDefinition: function (node) {
if (!isSCUDeclarеd(node.key)) {
return;
}
markSCUAsDeclared(node);
},
ObjectExpression: function (node) {
// Search for the shouldComponentUpdate declaration
for (let i = 0, l = node.properties.length; i < l; i++) {
if (
!node.properties[i].key || (
!isSCUDeclarеd(node.properties[i].key) &&
!isPureRenderDeclared(node.properties[i])
)
) {
continue;
}
markSCUAsDeclared(node);
}
},
'Program:exit': function () {
const list = components.list();
// Report missing shouldComponentUpdate for all components
for (const component in list) {
if (!has(list, component) || list[component].hasSCU) {
continue;
}
reportMissingOptimization(list[component]);
}
}
};
})
};

View File

@@ -0,0 +1,129 @@
/**
* @fileoverview Enforce ES5 or ES6 class for returning value in render function.
* @author Mark Orel
*/
'use strict';
const has = require('has');
const Components = require('../util/Components');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce ES5 or ES6 class for returning value in render function',
category: 'Possible Errors',
recommended: true
},
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
});
}
/**
* 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':
return node.body.body;
case 'ObjectExpression':
return node.properties;
default:
return [];
}
}
/**
* Get properties name
* @param {Object} node - Property.
* @returns {String} Property name.
*/
function getPropertyName(node) {
// Special case for class properties
// (babel-eslint 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;
} else if (['MethodDefinition', 'Property'].indexOf(node.type) !== -1) {
return node.key.name;
}
return '';
}
/**
* Check if a given AST node has a render method
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean} True if there is a render method, false if not
*/
function hasRenderMethod(node) {
const properties = getComponentProperties(node);
for (let i = 0, j = properties.length; i < j; i++) {
if (getPropertyName(properties[i]) !== 'render' || !properties[i].value) {
continue;
}
return /FunctionExpression$/.test(properties[i].value.type);
}
return false;
}
return {
ReturnStatement: function(node) {
const ancestors = context.getAncestors(node).reverse();
let depth = 0;
for (let i = 0, j = ancestors.length; i < j; i++) {
if (/Function(Expression|Declaration)$/.test(ancestors[i].type)) {
depth++;
}
if (
!/(MethodDefinition|(Class)?Property)$/.test(ancestors[i].type) ||
getPropertyName(ancestors[i]) !== 'render' ||
depth > 1
) {
continue;
}
markReturnStatementPresent(node);
}
},
ArrowFunctionExpression: function(node) {
if (node.expression === false || getPropertyName(node.parent) !== 'render') {
return;
}
markReturnStatementPresent(node);
},
'Program:exit': function() {
const list = components.list();
for (const component in list) {
if (
!has(list, component) ||
!hasRenderMethod(list[component].node) ||
list[component].hasReturnStatement ||
(!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node))
) {
continue;
}
context.report({
node: list[component].node,
message: 'Your render method should have return statement'
});
}
}
};
})
};

View File

@@ -0,0 +1,92 @@
/**
* @fileoverview Prevent extra closing tags for components without children
* @author Yannick Croissant
*/
'use strict';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent extra closing tags for components without children',
category: 'Stylistic Issues',
recommended: false
},
fixable: 'code',
schema: [{
type: 'object',
properties: {
component: {
default: true,
type: 'boolean'
},
html: {
default: true,
type: 'boolean'
}
},
additionalProperties: false
}]
},
create: function(context) {
const tagConvention = /^[a-z]|\-/;
function isTagName(name) {
return tagConvention.test(name);
}
function isComponent(node) {
return node.name && node.name.type === 'JSXIdentifier' && !isTagName(node.name.name);
}
function hasChildren(node) {
const childrens = node.parent.children;
if (
!childrens.length ||
(childrens.length === 1 && childrens[0].type === 'Literal' && !childrens[0].value.replace(/(?!\xA0)\s/g, ''))
) {
return false;
}
return true;
}
function isShouldBeSelfClosed(node) {
const configuration = context.options[0] || {component: true, html: true};
return (
configuration.component && isComponent(node) ||
configuration.html && isTagName(node.name.name)
) && !node.selfClosing && !hasChildren(node);
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
JSXOpeningElement: function(node) {
if (!isShouldBeSelfClosed(node)) {
return;
}
context.report({
node: node,
message: 'Empty components are self-closing',
fix: function(fixer) {
// Represents the last character of the JSXOpeningElement, the '>' character
const openingElementEnding = node.end - 1;
// Represents the last character of the JSXClosingElement, the '>' character
const closingElementEnding = node.parent.closingElement.end;
// Replace />.*<\/.*>/ with '/>'
const range = [openingElementEnding, closingElementEnding];
return fixer.replaceTextRange(range, ' />');
}
});
}
};
}
};

View File

@@ -0,0 +1,441 @@
/**
* @fileoverview Enforce component methods order
* @author Yannick Croissant
*/
'use strict';
const has = require('has');
const util = require('util');
const Components = require('../util/Components');
/**
* Get the methods order from the default config and the user config
* @param {Object} defaultConfig The default configuration.
* @param {Object} userConfig The user configuration.
* @returns {Array} Methods order
*/
function getMethodsOrder(defaultConfig, userConfig) {
userConfig = userConfig || {};
const groups = util._extend(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
},
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({
order: [
'static-methods',
'lifecycle',
'everything-else',
'render'
],
groups: {
lifecycle: [
'displayName',
'propTypes',
'contextTypes',
'childContextTypes',
'mixins',
'statics',
'defaultProps',
'constructor',
'getDefaultProps',
'state',
'getInitialState',
'getChildContext',
'componentWillMount',
'componentDidMount',
'componentWillReceiveProps',
'shouldComponentUpdate',
'componentWillUpdate',
'componentDidUpdate',
'componentWillUnmount'
]
}
}, 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) {
let isRegExp;
let matching;
let i;
let j;
const indexes = [];
if (method.static) {
const staticIndex = methodsOrder.indexOf('static-methods');
if (staticIndex >= 0) {
indexes.push(staticIndex);
}
}
if (method.getter) {
const getterIndex = methodsOrder.indexOf('getters');
if (getterIndex >= 0) {
indexes.push(getterIndex);
}
}
if (method.setter) {
const setterIndex = methodsOrder.indexOf('setters');
if (setterIndex >= 0) {
indexes.push(setterIndex);
}
}
if (method.typeAnnotation) {
const annotationIndex = methodsOrder.indexOf('type-annotations');
if (annotationIndex >= 0) {
indexes.push(annotationIndex);
}
}
// Either this is not a static method or static methods are not specified
// in the methodsOrder.
if (indexes.length === 0) {
for (i = 0, j = methodsOrder.length; i < j; i++) {
isRegExp = methodsOrder[i].match(regExpRegExp);
if (isRegExp) {
matching = new RegExp(isRegExp[1], isRegExp[2]).test(method.name);
} else {
matching = methodsOrder[i] === method.name;
}
if (matching) {
indexes.push(i);
}
}
}
// No matching pattern, return 'everything-else' index
if (indexes.length === 0) {
for (i = 0, j = methodsOrder.length; i < j; i++) {
if (methodsOrder[i] === 'everything-else') {
indexes.push(i);
break;
}
}
}
// No matching pattern and no 'everything-else' group
if (indexes.length === 0) {
indexes.push(Infinity);
}
return indexes;
}
/**
* Get properties name
* @param {Object} node - Property.
* @returns {String} Property name.
*/
function getPropertyName(node) {
// Special case for class properties
// (babel-eslint 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;
}
if (node.kind === 'get') {
return 'getter functions';
}
if (node.kind === 'set') {
return 'setter functions';
}
return node.key.name;
}
/**
* 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)) {
continue;
}
const index = errors[i].closest.ref.index;
if (!errors[index]) {
continue;
}
if (errors[i].score > errors[index].score) {
delete errors[index];
} else {
delete errors[i];
}
}
}
/**
* Report errors
*/
function reportErrors() {
dedupeErrors();
let nodeA;
let nodeB;
let indexA;
let indexB;
for (const i in errors) {
if (!has(errors, i)) {
continue;
}
nodeA = errors[i].node;
nodeB = errors[i].closest.ref.node;
indexA = i;
indexB = errors[i].closest.ref.index;
context.report({
node: nodeA,
message: MISPOSITION_MESSAGE,
data: {
propA: getPropertyName(nodeA),
propB: getPropertyName(nodeB),
position: indexA < indexB ? 'before' : 'after'
}
});
}
}
/**
* 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 'ClassExpression':
case 'ClassDeclaration':
return node.body.body;
case 'ObjectExpression':
return node.properties.filter(property => property.type === 'Property');
default:
return [];
}
}
/**
* 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',
static: node.static,
typeAnnotation: !!node.typeAnnotation && node.value === null
}));
let i;
let j;
let k;
let l;
let propA;
let propB;
let order;
// Loop around the properties
for (i = 0, j = propertiesInfos.length; i < j; i++) {
propA = propertiesInfos[i];
// Loop around the properties a second time (for comparison)
for (k = 0, l = propertiesInfos.length; k < l; k++) {
propB = propertiesInfos[k];
// Compare the properties order
order = comparePropsOrder(propertiesInfos, propA, propB);
// Continue to next comparison is order is correct
if (order.correct === true) {
continue;
}
// 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();
for (const component in list) {
if (!has(list, component)) {
continue;
}
const properties = getComponentProperties(list[component].node);
checkPropsOrder(properties);
}
reportErrors();
}
};
})
};

View File

@@ -0,0 +1,220 @@
/**
* @fileoverview Enforce propTypes declarations alphabetical sorting
*/
'use strict';
const variableUtil = require('../util/variable');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce propTypes declarations alphabetical sorting',
category: 'Stylistic Issues',
recommended: false
},
schema: [{
type: 'object',
properties: {
requiredFirst: {
type: 'boolean'
},
callbacksLast: {
type: 'boolean'
},
ignoreCase: {
type: 'boolean'
}
},
additionalProperties: false
}]
},
create: function(context) {
const sourceCode = context.getSourceCode();
const configuration = context.options[0] || {};
const requiredFirst = configuration.requiredFirst || false;
const callbacksLast = configuration.callbacksLast || false;
const ignoreCase = configuration.ignoreCase || false;
const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
/**
* Checks if node is `propTypes` declaration
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean} True if node is `propTypes` declaration, false if not.
*/
function isPropTypesDeclaration(node) {
// Special case for class properties
// (babel-eslint does not expose property name so we have to rely on tokens)
if (node.type === 'ClassProperty') {
const tokens = context.getFirstTokens(node, 2);
return (tokens[0] && tokens[0].value === 'propTypes') ||
(tokens[1] && tokens[1].value === 'propTypes');
}
return Boolean(
node &&
node.name === 'propTypes'
);
}
function getKey(node) {
return sourceCode.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';
}
/**
* 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 propTypes declarations are sorted
* @param {Array} declarations The array of AST nodes being checked.
* @returns {void}
*/
function checkSorted(declarations) {
declarations.reduce((prev, curr, idx, decls) => {
if (/SpreadProperty$/.test(curr.type)) {
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 = prevPropName.toLowerCase();
currentPropName = currentPropName.toLowerCase();
}
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'
});
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'
});
return prev;
}
}
if (currentPropName < prevPropName) {
context.report({
node: curr,
message: 'Prop types declarations should be sorted alphabetically'
});
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 (propWrapperFunctions.has(node.callee.name) && innerNode) {
checkNode(innerNode);
}
break;
default:
break;
}
}
return {
ClassProperty: function(node) {
if (!isPropTypesDeclaration(node)) {
return;
}
checkNode(node.value);
},
MemberExpression: function(node) {
if (!isPropTypesDeclaration(node.property)) {
return;
}
checkNode(node.parent.right);
},
ObjectExpression: function(node) {
node.properties.forEach(property => {
if (!property.key) {
return;
}
if (!isPropTypesDeclaration(property.key)) {
return;
}
if (property.value.type === 'ObjectExpression') {
checkSorted(property.value.properties);
}
});
}
};
}
};

View File

@@ -0,0 +1,80 @@
/**
* @fileoverview Enforce style prop value is an object
* @author David Petersen
*/
'use strict';
const variableUtil = require('../util/variable');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce style prop value is an object',
category: '',
recommended: false
},
schema: []
},
create: function(context) {
/**
* @param {object} node An Identifier node
*/
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, 'Style prop value must be an object');
}
}
return {
CallExpression: function(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(style.value, 'Style prop value must be an object');
}
}
}
}
},
JSXAttribute: function(node) {
if (!node.value || node.name.name !== 'style') {
return;
}
if (node.value.type !== 'JSXExpressionContainer' || isNonNullaryLiteral(node.value.expression)) {
context.report(node, 'Style prop value must be an object');
} else if (node.value.expression.type === 'Identifier') {
checkIdentifiers(node.value.expression);
}
}
};
}
};

View File

@@ -0,0 +1,150 @@
/**
* @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');
// ------------------------------------------------------------------------------
// 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
},
schema: []
},
create: Components.detect((context, components, utils) => ({
JSXElement: function(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: 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: node,
message: errorMessage(elementName)
});
}
},
CallExpression: function(node) {
if (node.callee.type !== 'MemberExpression' && node.callee.type !== 'Identifier') {
return;
}
if (!utils.isReactCreateElement(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: 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: node,
message: errorMessage(elementName)
});
}
}
}))
};

View File

@@ -0,0 +1,677 @@
/**
* @fileoverview Utility class and functions for React components detection
* @author Yannick Croissant
*/
'use strict';
const has = require('has');
const util = require('util');
const doctrine = require('doctrine');
const variableUtil = require('./variable');
const pragmaUtil = require('./pragma');
const usedPropTypesAreEquivalent = (propA, propB) => {
if (propA.name === propB.name) {
if (!propA.allNames && !propB.allNames) {
return true;
} else if (Array.isArray(propA.allNames) && Array.isArray(propB.allNames) && propA.allNames.join('') === propB.allNames.join('')) {
return true;
}
return false;
}
return false;
};
const 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);
};
/**
* Components
* @class
*/
function Components() {
this._list = {};
this._getId = function(node) {
return node && node.range.join(':');
};
}
/**
* 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
*/
Components.prototype.add = function(node, confidence) {
const id = this._getId(node);
if (this._list[id]) {
if (confidence === 0 || this._list[id].confidence === 0) {
this._list[id].confidence = 0;
} else {
this._list[id].confidence = Math.max(this._list[id].confidence, confidence);
}
return this._list[id];
}
this._list[id] = {
node: node,
confidence: confidence
};
return this._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
*/
Components.prototype.get = function(node) {
const id = this._getId(node);
return this._list[id];
};
/**
* Update a component in the list
*
* @param {ASTNode} node The AST node being updated.
* @param {Object} props Additional properties to add to the component.
*/
Components.prototype.set = function(node, props) {
while (node && !this._list[this._getId(node)]) {
node = node.parent;
}
if (!node) {
return;
}
const id = this._getId(node);
let copyUsedPropTypes;
if (this._list[id]) {
// usedPropTypes is an array. _extend replaces existing array with a new one which caused issue #1309.
// preserving original array so it can be merged later on.
copyUsedPropTypes = this._list[id].usedPropTypes && this._list[id].usedPropTypes.slice();
}
this._list[id] = util._extend(this._list[id], props);
if (this._list[id] && props.usedPropTypes) {
this._list[id].usedPropTypes = mergeUsedPropTypes(copyUsedPropTypes || [], props.usedPropTypes);
}
};
/**
* Return the components list
* Components for which we are not confident are not returned
*
* @returns {Object} Components list
*/
Components.prototype.list = function() {
const list = {};
const usedPropTypes = {};
// Find props used in components for which we are not confident
for (const i in this._list) {
if (!has(this._list, i) || this._list[i].confidence >= 2) {
continue;
}
let component = null;
let node = null;
node = this._list[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 = (this._list[i].usedPropTypes || []).filter(propType => !propType.node || propType.node.kind !== 'init');
const componentId = this._getId(component.node);
usedPropTypes[componentId] = (usedPropTypes[componentId] || []).concat(newUsedProps);
}
}
// Assign used props in not confident components to the parent component
for (const j in this._list) {
if (!has(this._list, j) || this._list[j].confidence < 2) {
continue;
}
const id = this._getId(this._list[j].node);
list[j] = this._list[j];
if (usedPropTypes[id]) {
list[j].usedPropTypes = (list[j].usedPropTypes || []).concat(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
*/
Components.prototype.length = function() {
let length = 0;
for (const i in this._list) {
if (!has(this._list, i) || this._list[i].confidence < 2) {
continue;
}
length++;
}
return 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: function(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: function(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: function(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: function (node) {
if (node.superClass) {
return new RegExp(`^(${pragma}\\.)?PureComponent$`).test(sourceCode.getText(node.superClass));
}
return false;
},
/**
* Check if createElement is destructured from React import
*
* @returns {Boolean} True if createElement is destructured from React
*/
hasDestructuredReactCreateElement: function() {
const variables = variableUtil.variablesInScope(context);
const variable = variableUtil.getVariable(variables, 'createElement');
if (variable) {
const map = variable.scope.set;
if (map.has('React')) {
return true;
}
}
return false;
},
/**
* Checks to see if node is called within React.createElement
*
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean} True if React.createElement called
*/
isReactCreateElement: function(node) {
const calledOnReact = (
node &&
node.callee &&
node.callee.object &&
node.callee.object.name === 'React' &&
node.callee.property &&
node.callee.property.name === 'createElement'
);
const calledDirectly = (
node &&
node.callee &&
node.callee.name === 'createElement'
);
if (this.hasDestructuredReactCreateElement()) {
return calledDirectly || calledOnReact;
}
return calledOnReact;
},
/**
* 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: function(ASTnode, strict) {
let property;
let node = ASTnode;
switch (node.type) {
case 'ReturnStatement':
property = 'argument';
break;
case 'ArrowFunctionExpression':
property = 'body';
break;
default:
node = utils.findReturnStatement(node);
if (!node) {
return false;
}
property = 'argument';
}
const returnsConditionalJSXConsequent =
node[property] &&
node[property].type === 'ConditionalExpression' &&
node[property].consequent.type === 'JSXElement'
;
const returnsConditionalJSXAlternate =
node[property] &&
node[property].type === 'ConditionalExpression' &&
node[property].alternate.type === 'JSXElement'
;
const returnsConditionalJSX =
strict ?
(returnsConditionalJSXConsequent && returnsConditionalJSXAlternate) :
(returnsConditionalJSXConsequent || returnsConditionalJSXAlternate);
const returnsJSX =
node[property] &&
node[property].type === 'JSXElement'
;
const returnsReactCreateElement = this.isReactCreateElement(node[property]);
return Boolean(
returnsConditionalJSX ||
returnsJSX ||
returnsReactCreateElement
);
},
/**
* Find a return statment in the current node
*
* @param {ASTNode} ASTnode The AST node being checked
*/
findReturnStatement: function(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);
let i = bodyNodes.length - 1;
for (; i >= 0; i--) {
if (bodyNodes[i].type === 'ReturnStatement') {
return bodyNodes[i];
}
}
return false;
},
/**
* Get the parent component node from the current scope
*
* @returns {ASTNode} component node, null if we are not in a component
*/
getParentComponent: function() {
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: function() {
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: function() {
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;
},
/**
* Get the parent stateless component node from the current scope
*
* @returns {ASTNode} component node, null if we are not in a component
*/
getParentStatelessComponent: function() {
let scope = context.getScope();
while (scope) {
const node = scope.block;
const isClass = node.type === 'ClassExpression';
const isFunction = /Function/.test(node.type); // Functions
const isMethod = node.parent && node.parent.type === 'MethodDefinition'; // Classes methods
const isArgument = node.parent && node.parent.type === 'CallExpression'; // Arguments (callback, etc.)
// Attribute Expressions inside JSX Elements (<button onClick={() => props.handleClick()}></button>)
const isJSXExpressionContainer = node.parent && node.parent.type === 'JSXExpressionContainer';
// Stop moving up if we reach a class or an argument (like a callback)
if (isClass || isArgument) {
return null;
}
// Return the node if it is a function that is not a class method and is not inside a JSX Element
if (isFunction && !isMethod && !isJSXExpressionContainer) {
return node;
}
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: function(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;
let refId;
for (i = 0, j = refs.length; i < j; i++) {
refId = refs[i].identifier;
if (refId.parent && refId.parent.type === 'MemberExpression') {
refId = refId.parent;
}
if (sourceCode.getText(refId) !== componentName) {
continue;
}
if (refId.type === 'MemberExpression') {
componentNode = refId.parent.right;
} else if (refId.parent && refId.parent.type === 'VariableDeclarator') {
componentNode = refId.parent.init;
}
break;
}
if (componentNode) {
// Return the component
return components.add(componentNode, 1);
}
// Try to find the component using variable declarations
let defInScope;
const defs = variableInScope.defs;
for (i = 0, j = defs.length; i < j; i++) {
if (defs[i].type === 'ClassName' || defs[i].type === 'FunctionName' || defs[i].type === 'Variable') {
defInScope = defs[i];
break;
}
}
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;
}
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 = {
ClassExpression: function(node) {
if (!utils.isES6Component(node)) {
return;
}
components.add(node, 2);
},
ClassDeclaration: function(node) {
if (!utils.isES6Component(node)) {
return;
}
components.add(node, 2);
},
ClassProperty: function(node) {
node = utils.getParentComponent();
if (!node) {
return;
}
components.add(node, 2);
},
ObjectExpression: function(node) {
if (!utils.isES5Component(node)) {
return;
}
components.add(node, 2);
},
FunctionExpression: function(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: function(node) {
if (node.async) {
components.add(node, 0);
return;
}
node = utils.getParentComponent();
if (!node) {
return;
}
components.add(node, 1);
},
ArrowFunctionExpression: function(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: function(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: function(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 = util._extend({}, ruleInstructions);
Object.keys(detectionInstructions).forEach(instruction => {
updatedRuleInstructions[instruction] = function(node) {
detectionInstructions[instruction](node);
return ruleInstructions[instruction] ? ruleInstructions[instruction](node) : void 0;
};
});
// Return the updated rule instructions
return updatedRuleInstructions;
}
Components.detect = function(rule) {
return componentRule.bind(this, rule);
};
module.exports = Components;

View File

@@ -0,0 +1,28 @@
/**
* @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.
* @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 tokens = context.getFirstTokens(node.params[0], 2);
const isAnnotated = node.params[0].typeAnnotation;
const isDestructuredProps = node.params[0].type === 'ObjectPattern';
const isProps = tokens[0].value === 'props' || (tokens[1] && tokens[1].value === 'props');
return (isAnnotated && (isDestructuredProps || isProps));
}
module.exports = {
isAnnotatedFunctionPropsDeclaration: isAnnotatedFunctionPropsDeclaration
};

View File

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

View File

@@ -0,0 +1,68 @@
/**
* @fileoverview Prevent usage of setState in lifecycle methods
* @author Yannick Croissant
*/
'use strict';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function makeNoMethodSetStateRule(methodName) {
return {
meta: {
docs: {
description: `Prevent usage of setState in ${methodName}`,
category: 'Best Practices',
recommended: false
},
schema: [{
enum: ['disallow-in-func']
}]
},
create: function(context) {
const mode = context.options[0] || 'allow-in-func';
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
CallExpression: function(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;
for (let i = 0, j = ancestors.length; i < j; i++) {
if (/Function(Expression|Declaration)$/.test(ancestors[i].type)) {
depth++;
}
if (
(ancestors[i].type !== 'Property' && ancestors[i].type !== 'MethodDefinition') ||
ancestors[i].key.name !== methodName ||
(mode !== 'disallow-in-func' && depth > 1)
) {
continue;
}
context.report({
node: callee,
message: `Do not use setState in ${methodName}`
});
break;
}
}
};
}
};
}
module.exports = makeNoMethodSetStateRule;

View File

@@ -0,0 +1,47 @@
/**
* @fileoverview Utility functions for React pragma configuration
* @author Yannick Croissant
*/
'use strict';
const JSX_ANNOTATION_REGEX = /^\*\s*@jsx\s+([^\s]+)/;
// Does not check for reserved keywords or unicode characters
const JS_IDENTIFIER_REGEX = /^[_$a-zA-Z][_$a-zA-Z0-9]*$/;
function getCreateClassFromContext(context) {
let pragma = 'createReactClass';
// .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings)
if (context.settings.react && context.settings.react.createClass) {
pragma = context.settings.react.createClass;
}
if (!JS_IDENTIFIER_REGEX.test(pragma)) {
throw new Error(`createClass pragma ${pragma} is not a valid function name`);
}
return pragma;
}
function getFromContext(context) {
let pragma = 'React';
const sourceCode = context.getSourceCode();
const pragmaNode = sourceCode.getAllComments().find(node => JSX_ANNOTATION_REGEX.test(node.value));
if (pragmaNode) {
const matches = JSX_ANNOTATION_REGEX.exec(pragmaNode.value);
pragma = matches[1].split('.')[0];
// .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings)
} else if (context.settings.react && context.settings.react.pragma) {
pragma = context.settings.react.pragma;
}
if (!JS_IDENTIFIER_REGEX.test(pragma)) {
throw new Error(`React pragma ${pragma} is not a valid identifier`);
}
return pragma;
}
module.exports = {
getCreateClassFromContext: getCreateClassFromContext,
getFromContext: getFromContext
};

View File

@@ -0,0 +1,58 @@
/**
* @fileoverview Utility functions for React components detection
* @author Yannick Croissant
*/
'use strict';
/**
* Search a particular variable in a list
* @param {Array} variables The variables list.
* @param {Array} name The name of the variable to search.
* @returns {Boolean} True if the variable was found, false if not.
*/
function findVariable(variables, name) {
return variables.some(variable => variable.name === name);
}
/**
* Find and return a particular variable in a list
* @param {Array} variables The variables list.
* @param {Array} name The name of the variable to search.
* @returns {Object} Variable if the variable was found, null if not.
*/
function getVariable(variables, name) {
return variables.find(variable => variable.name === name);
}
/**
* List all variable in a given scope
*
* Contain a patch for babel-eslint to avoid https://github.com/babel/babel-eslint/issues/21
*
* @param {Object} context The current rule context.
* @returns {Array} The variables list
*/
function variablesInScope(context) {
let scope = context.getScope();
let variables = scope.variables;
while (scope.type !== 'global') {
scope = scope.upper;
variables = scope.variables.concat(variables);
}
if (scope.childScopes.length) {
variables = scope.childScopes[0].variables.concat(variables);
if (scope.childScopes[0].childScopes.length) {
variables = scope.childScopes[0].childScopes[0].variables.concat(variables);
}
}
variables.reverse();
return variables;
}
module.exports = {
findVariable: findVariable,
getVariable: getVariable,
variablesInScope: variablesInScope
};

View File

@@ -0,0 +1,49 @@
/**
* @fileoverview Utility functions for React and Flow version configuration
* @author Yannick Croissant
*/
'use strict';
function getReactVersionFromContext(context) {
let confVer = '999.999.999';
// .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings)
if (context.settings.react && context.settings.react.version) {
confVer = context.settings.react.version;
}
confVer = /^[0-9]+\.[0-9]+$/.test(confVer) ? `${confVer}.0` : confVer;
return confVer.split('.').map(part => Number(part));
}
function getFlowVersionFromContext(context) {
let confVer = '999.999.999';
// .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings)
if (context.settings.react && context.settings.react.flowVersion) {
confVer = context.settings.react.flowVersion;
} else {
throw 'Could not retrieve flowVersion from settings';
}
confVer = /^[0-9]+\.[0-9]+$/.test(confVer) ? `${confVer}.0` : confVer;
return confVer.split('.').map(part => Number(part));
}
function test(context, methodVer, confVer) {
methodVer = methodVer.split('.').map(part => Number(part));
const higherMajor = methodVer[0] < confVer[0];
const higherMinor = methodVer[0] === confVer[0] && methodVer[1] < confVer[1];
const higherOrEqualPatch = methodVer[0] === confVer[0] && methodVer[1] === confVer[1] && methodVer[2] <= confVer[2];
return higherMajor || higherMinor || higherOrEqualPatch;
}
function testReactVersion(context, methodVer) {
return test(context, methodVer, getReactVersionFromContext(context));
}
function testFlowVersion(context, methodVer) {
return test(context, methodVer, getFlowVersionFromContext(context));
}
module.exports = {
testReactVersion,
testFlowVersion
};

View File

@@ -0,0 +1,3 @@
{
"presets": ["es2015"]
}

View File

@@ -0,0 +1,3 @@
node_modules/
reports/
lib/

View File

@@ -0,0 +1,3 @@
{
extends: "airbnb-base"
}

View File

@@ -0,0 +1,6 @@
node_modules
reports
npm-debug.log
coverage
.gitignore
.DS_Store

View File

@@ -0,0 +1,11 @@
language: node_js
node_js:
- 4
- 5
- 6
cache:
yarn: true
directories:
- node_modules
after_success:
- npm run coveralls

View File

@@ -0,0 +1,79 @@
2.0.1 / 2017-08-31
==================
- [fix] Add support for BindExpression
2.0.0 / 2017-07-07
==================
- [breaking] Remove undefined return from `propName` so it always returns a value.
1.4.1 / 2017-04-19
==================
- [fix] - Fixing fatal throw in `getPropValue` for `ArrowFunctionExpression`
1.4.0 / 2017-02-02
==================
- [new] Add eventHandlers and eventHandlersByType to API. These are the event names for DOM elements on JSX-using libraries such as React, inferno, and preact.
1.3.5 / 2016-12-14
==================
- [fix] Normalize literals "true" and "false" before converting to boolean in Literal prop value extractor.
1.3.4 / 2016-11-15
==================
- [fix] Recursively resolve JSXMemberExpression names for elementType. (i.e. `<Component.Render.Me />`). Fixes [#9](https://github.com/evcohen/jsx-ast-utils/issues/9)
1.3.3 / 2016-10-28
==================
- [fix] Add support for `ArrayExpression`.
1.3.2 / 2016-10-11
==================
- [fix] Add support for `UpdateExpression`.
1.3.1 / 2016-07-13
==================
- [fix] Add `JSXElement` to expression types to handle recursively extracting prop value.
1.3.0 / 2016-07-12
==================
- [new] Add support for `TaggedTemplateExpression`.
1.2.1 / 2016-06-15
==================
- [fix] Point to `lib` instead of `src` for root exports.
1.2.0 / 2016-06-15
==================
- [new] Export functions from root so they can be imported like the following: `require('jsx-ast-utils/{function}')`.
1.1.1 / 2016-06-12
==================
- [fix] Better support for expressions in `TemplateLiteral` extraction.
1.1.0 / 2016-06-10
==================
- [new] Support for namespaced element names.
- [new] Add `propName` to API to get correct name for prop.
1.0.1 / 2016-06-10
==================
- [fix] Return actual reserved words instead of string representations of them.
1.0.0 / 2016-06-09
==================
- Initial stable release

View File

@@ -0,0 +1,8 @@
The MIT License (MIT)
Copyright (c) 2016 Ethan Cohen
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.

View File

@@ -0,0 +1,294 @@
<p align="center">
<a href="https://travis-ci.org/evcohen/jsx-ast-utils">
<img src="https://api.travis-ci.org/evcohen/jsx-ast-utils.svg?branch=master"
alt="build status">
</a>
<a href="https://npmjs.org/package/jsx-ast-utils">
<img src="https://img.shields.io/npm/v/jsx-ast-utils.svg"
alt="npm version">
</a>
<a href="https://github.com/evcohen/jsx-ast-utils/blob/master/LICENSE.md">
<img src="https://img.shields.io/npm/l/jsx-ast-utils.svg"
alt="license">
</a>
<a href='https://coveralls.io/github/evcohen/jsx-ast-utils?branch=master'>
<img src='https://coveralls.io/repos/github/evcohen/jsx-ast-utils/badge.svg?branch=master' alt='Coverage Status' />
</a>
<a href='https://npmjs.org/package/jsx-ast-utils'>
<img src='https://img.shields.io/npm/dt/jsx-ast-utils.svg'
alt='Total npm downloads' />
</a>
</p>
# jsx-ast-utils
AST utility module for statically analyzing JSX.
## Installation
```sh
$ npm i jsx-ast-utils --save
```
## Usage
This is a utility module to evaluate AST objects for JSX syntax. This can be super useful when writing linting rules for JSX code. It was originally in the code for [eslint-plugin-jsx-a11y](https://github.com/evcohen/eslint-plugin-jsx-a11y), however I thought it could be useful to be extracted and maintained separately so **you** could write new interesting rules to statically analyze JSX.
### ESLint example
```js
import { hasProp } from 'jsx-ast-utils';
// OR: var hasProp = require('jsx-ast-utils').hasProp;
// OR: const hasProp = require('jsx-ast-utils/hasProp');
// OR: import hasProp from 'jsx-ast-utils/hasProp';
module.exports = context => ({
JSXOpeningElement: node => {
const onChange = hasProp(node.attributes, 'onChange');
if (onChange) {
context.report({
node,
message: `No onChange!`
});
}
}
});
```
## API
### AST Resources
1. [JSX spec](https://github.com/facebook/jsx/blob/master/AST.md)
2. [JS spec](https://github.com/estree/estree/blob/master/spec.md)
### hasProp
```js
hasProp(props, prop, options);
```
Returns boolean indicating whether an prop exists as an attribute on a JSX element node.
#### Props
Object - The attributes on the visited node. (Usually `node.attributes`).
#### Prop
String - A string representation of the prop you want to check for existence.
#### Options
Object - An object representing options for existence checking
1. `ignoreCase` - automatically set to `true`.
2. `spreadStrict` - automatically set to `true`. This means if spread operator exists in
props, it will assume the prop you are looking for is not in the spread.
Example: `<div {...props} />` looking for specific prop here will return false if `spreadStrict` is `true`.
<hr />
### hasAnyProp
```js
hasAnyProp(props, prop, options);
```
Returns a boolean indicating if **any** of props in `prop` argument exist on the node.
#### Props
Object - The attributes on the visited node. (Usually `node.attributes`).
#### Prop
Array<String> - An array of strings representing the props you want to check for existence.
#### Options
Object - An object representing options for existence checking
1. `ignoreCase` - automatically set to `true`.
2. `spreadStrict` - automatically set to `true`. This means if spread operator exists in
props, it will assume the prop you are looking for is not in the spread.
Example: `<div {...props} />` looking for specific prop here will return false if `spreadStrict` is `true`.
<hr />
### hasEveryProp
```js
hasEveryProp(props, prop, options);
```
Returns a boolean indicating if **all** of props in `prop` argument exist on the node.
#### Props
Object - The attributes on the visited node. (Usually `node.attributes`).
#### Prop
Array<String> - An array of strings representing the props you want to check for existence.
#### Options
Object - An object representing options for existence checking
1. `ignoreCase` - automatically set to `true`.
2. `spreadStrict` - automatically set to `true`. This means if spread operator exists in
props, it will assume the prop you are looking for is not in the spread.
Example: `<div {...props} />` looking for specific prop here will return false if `spreadStrict` is `true`.
<hr />
### getProp
```js
getProp(props, prop, options);
```
Returns the JSXAttribute itself or undefined, indicating the prop is not present on the JSXOpeningElement.
#### Props
Object - The attributes on the visited node. (Usually `node.attributes`).
#### Prop
String - A string representation of the prop you want to check for existence.
#### Options
Object - An object representing options for existence checking
1. `ignoreCase` - automatically set to `true`.
<hr />
### elementType
```js
elementType(node)
```
Returns the tagName associated with a JSXElement.
#### Node
Object - The visited JSXElement node object.
<hr />
### getPropValue
```js
getPropValue(prop);
```
Returns the value of a given attribute. Different types of attributes have their associated values in different properties on the object.
This function should return the most *closely* associated value with the intention of the JSX.
#### Prop
Object - The JSXAttribute collected by AST parser.
<hr />
### getLiteralPropValue
```js
getLiteralPropValue(prop);
```
Returns the value of a given attribute. Different types of attributes have their associated values in different properties on the object.
This function should return a value only if we can extract a literal value from its attribute (i.e. values that have generic types in JavaScript - strings, numbers, booleans, etc.)
#### Prop
Object - The JSXAttribute collected by AST parser.
<hr />
### propName
```js
propName(prop);
```
Returns the name associated with a JSXAttribute. For example, given `<div foo="bar" />` and the JSXAttribute for `foo`, this will return the string `"foo"`.
#### Prop
Object - The JSXAttribute collected by AST parser.
<hr />
### eventHandlers
```js
console.log(eventHandlers);
/*
[
'onCopy',
'onCut',
'onPaste',
'onCompositionEnd',
'onCompositionStart',
'onCompositionUpdate',
'onKeyDown',
'onKeyPress',
'onKeyUp',
'onFocus',
'onBlur',
'onChange',
'onInput',
'onSubmit',
'onClick',
'onContextMenu',
'onDblClick',
'onDoubleClick',
'onDrag',
'onDragEnd',
'onDragEnter',
'onDragExit',
'onDragLeave',
'onDragOver',
'onDragStart',
'onDrop',
'onMouseDown',
'onMouseEnter',
'onMouseLeave',
'onMouseMove',
'onMouseOut',
'onMouseOver',
'onMouseUp',
'onSelect',
'onTouchCancel',
'onTouchEnd',
'onTouchMove',
'onTouchStart',
'onScroll',
'onWheel',
'onAbort',
'onCanPlay',
'onCanPlayThrough',
'onDurationChange',
'onEmptied',
'onEncrypted',
'onEnded',
'onError',
'onLoadedData',
'onLoadedMetadata',
'onLoadStart',
'onPause',
'onPlay',
'onPlaying',
'onProgress',
'onRateChange',
'onSeeked',
'onSeeking',
'onStalled',
'onSuspend',
'onTimeUpdate',
'onVolumeChange',
'onWaiting',
'onLoad',
'onError',
'onAnimationStart',
'onAnimationEnd',
'onAnimationIteration',
'onTransitionEnd',
]
*/
```
Contains a flat list of common event handler props used in JSX to attach behaviors
to DOM events.
#### eventHandlersByType
The same list as `eventHandlers`, grouped into types.
```js
console.log(eventHandlersByType);
/*
{
clipboard: [ 'onCopy', 'onCut', 'onPaste' ],
composition: [ 'onCompositionEnd', 'onCompositionStart', 'onCompositionUpdate' ],
keyboard: [ 'onKeyDown', 'onKeyPress', 'onKeyUp' ],
focus: [ 'onFocus', 'onBlur' ],
form: [ 'onChange', 'onInput', 'onSubmit' ],
mouse: [ 'onClick', 'onContextMenu', 'onDblClick', 'onDoubleClick', 'onDrag', 'onDragEnd', 'onDragEnter', 'onDragExit', 'onDragLeave', 'onDragOver', 'onDragStart', 'onDrop', 'onMouseDown', 'onMouseEnter', 'onMouseLeave', 'onMouseMove', 'onMouseOut', 'onMouseOver', 'onMouseUp' ],
selection: [ 'onSelect' ],
touch: [ 'onTouchCancel', 'onTouchEnd', 'onTouchMove', 'onTouchStart' ],
ui: [ 'onScroll' ],
wheel: [ 'onWheel' ],
media: [ 'onAbort', 'onCanPlay', 'onCanPlayThrough', 'onDurationChange', 'onEmptied', 'onEncrypted', 'onEnded', 'onError', 'onLoadedData', 'onLoadedMetadata', 'onLoadStart', 'onPause', 'onPlay', 'onPlaying', 'onProgress', 'onRateChange', 'onSeeked', 'onSeeking', 'onStalled', 'onSuspend', 'onTimeUpdate', 'onVolumeChange', 'onWaiting' ],
image: [ 'onLoad', 'onError' ],
animation: [ 'onAnimationStart', 'onAnimationEnd', 'onAnimationIteration' ],
transition: [ 'onTransitionEnd' ],
}
*/
```

View File

@@ -0,0 +1,19 @@
import getProp from '../src/getProp';
const parser = require('babylon');
function parse(code) {
return parser.parse(code, {
plugins: ['jsx', 'functionBind', 'estree'],
});
}
export function getOpeningElement(code) {
return parse(code).program.body[0].expression.openingElement;
}
export function extractProp(code, prop = 'foo') {
const node = getOpeningElement(code);
const { attributes: props } = node;
return getProp(props, prop);
}

View File

@@ -0,0 +1,82 @@
/* eslint-env mocha */
import assert from 'assert';
import { getOpeningElement } from '../helper';
import elementType from '../../src/elementType';
describe('elementType tests', () => {
it('should export a function', () => {
const expected = 'function';
const actual = typeof elementType;
assert.equal(expected, actual);
});
it('should throw an error if the argument is missing', () => {
assert.throws(() => { elementType(); }, Error);
});
it('should throw an error if the argument not a JSX node', () => {
assert.throws(() => { elementType({ a: 'foo' }); }, Error);
});
it('should return the correct type of the DOM element given its node object', () => {
const code = '<div />';
const node = getOpeningElement(code);
const expected = 'div';
const actual = elementType(node);
assert.equal(expected, actual);
});
it('should return the correct type of the custom element given its node object', () => {
const code = '<Slider />';
const node = getOpeningElement(code);
const expected = 'Slider';
const actual = elementType(node);
assert.equal(expected, actual);
});
it('should return the correct type of the custom object element given its node object', () => {
const code = '<UX.Slider />';
const node = getOpeningElement(code);
const expected = 'UX.Slider';
const actual = elementType(node);
assert.equal(expected, actual);
});
it('should return the correct type of the namespaced element given its node object', () => {
const code = '<UX:Slider />';
const node = getOpeningElement(code);
const expected = 'UX:Slider';
const actual = elementType(node);
assert.equal(expected, actual);
});
it('should return the correct type of the multiple custom object element given its node object',
() => {
const code = '<UX.Slider.Blue.Light />';
const node = getOpeningElement(code);
const expected = 'UX.Slider.Blue.Light';
const actual = elementType(node);
assert.equal(expected, actual);
});
it('should return this.Component when given its node object', () => {
const code = '<this.Component />';
const node = getOpeningElement(code);
const expected = 'this.Component';
const actual = elementType(node);
assert.equal(expected, actual);
});
});

View File

@@ -0,0 +1,101 @@
/* eslint-env mocha */
import assert from 'assert';
import includes from 'array-includes';
import eventHandlers, { eventHandlersByType } from '../../src/eventHandlers';
describe('eventHandlers', () => {
it('should contain a list of common JSX event handlers', () => {
assert([
'onCopy',
'onCut',
'onPaste',
'onCompositionEnd',
'onCompositionStart',
'onCompositionUpdate',
'onKeyDown',
'onKeyPress',
'onKeyUp',
'onFocus',
'onBlur',
'onChange',
'onInput',
'onSubmit',
'onClick',
'onContextMenu',
'onDblClick',
'onDoubleClick',
'onDrag',
'onDragEnd',
'onDragEnter',
'onDragExit',
'onDragLeave',
'onDragOver',
'onDragStart',
'onDrop',
'onMouseDown',
'onMouseEnter',
'onMouseLeave',
'onMouseMove',
'onMouseOut',
'onMouseOver',
'onMouseUp',
'onSelect',
'onTouchCancel',
'onTouchEnd',
'onTouchMove',
'onTouchStart',
'onScroll',
'onWheel',
'onAbort',
'onCanPlay',
'onCanPlayThrough',
'onDurationChange',
'onEmptied',
'onEncrypted',
'onEnded',
'onError',
'onLoadedData',
'onLoadedMetadata',
'onLoadStart',
'onPause',
'onPlay',
'onPlaying',
'onProgress',
'onRateChange',
'onSeeked',
'onSeeking',
'onStalled',
'onSuspend',
'onTimeUpdate',
'onVolumeChange',
'onWaiting',
'onLoad',
'onError',
'onAnimationStart',
'onAnimationEnd',
'onAnimationIteration',
'onTransitionEnd',
].every(handlerName => includes(eventHandlers, handlerName)));
});
});
describe('eventHandlersByType', () => {
it('should be keyed by type', () => {
assert([
'clipboard',
'composition',
'keyboard',
'focus',
'form',
'mouse',
'selection',
'touch',
'ui',
'wheel',
'media',
'image',
'animation',
'transition',
].every(type => !!eventHandlersByType[type]));
});
});

View File

@@ -0,0 +1,71 @@
/* eslint-env mocha */
import assert from 'assert';
import { getOpeningElement } from '../helper';
import getProp from '../../src/getProp';
describe('getProp', () => {
it('should export a function', () => {
const expected = 'function';
const actual = typeof getProp;
assert.equal(expected, actual);
});
it('should return undefined if no arguments are provided', () => {
const expected = undefined;
const actual = getProp();
assert.equal(expected, actual);
});
it('should return undefined if the attribute is absent', () => {
const code = '<div />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = 'id';
const expected = undefined;
const actual = getProp(props, prop);
assert.equal(expected, actual);
});
it('should return the correct attribute if the attribute exists', () => {
const code = '<div id="foo" />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = 'id';
const expected = 'id';
const actual = getProp(props, prop).name.name;
assert.equal(expected, actual);
});
it('should return undefined if the attribute may exist in spread', () => {
const code = '<div {...props} />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = 'id';
const expected = undefined;
const actual = getProp(props, prop);
assert.equal(expected, actual);
});
it('should return undefined if the attribute is considered absent in case-sensitive mode', () => {
const code = '<div ID="foo" />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = 'id';
const options = {
ignoreCase: false,
};
const expected = undefined;
const actual = getProp(props, prop, options);
assert.equal(expected, actual);
});
});

View File

@@ -0,0 +1,468 @@
/* eslint-env mocha */
/* eslint no-template-curly-in-string: 0 */
import assert from 'assert';
import { extractProp } from '../helper';
import { getLiteralPropValue } from '../../src/getPropValue';
describe('getLiteralPropValue', () => {
it('should export a function', () => {
const expected = 'function';
const actual = typeof getLiteralPropValue;
assert.equal(expected, actual);
});
it('should return undefined when not provided with a JSXAttribute', () => {
const expected = undefined;
const actual = getLiteralPropValue(1);
assert.equal(expected, actual);
});
it('should throw error when trying to get value from unknown node type', () => {
const prop = {
type: 'JSXAttribute',
value: {
type: 'JSXExpressionContainer',
},
};
assert.throws(() => {
getLiteralPropValue(prop);
}, Error);
});
describe('Null', () => {
it('should return true when no value is given', () => {
const prop = extractProp('<div foo />');
const expected = true;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Literal', () => {
it('should return correct string if value is a string', () => {
const prop = extractProp('<div foo="bar" />');
const expected = 'bar';
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
it('should return correct string if value is a string expression', () => {
const prop = extractProp('<div foo={"bar"} />');
const expected = 'bar';
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
it('should return correct integer if value is a integer expression', () => {
const prop = extractProp('<div foo={1} />');
const expected = 1;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
it('should convert "true" to boolean type', () => {
const prop = extractProp('<div foo="true" />');
const expected = true;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
it('should convert "TrUE" to boolean type', () => {
const prop = extractProp('<div foo="TrUE" />');
const expected = true;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
it('should convert "false" to boolean type', () => {
const prop = extractProp('<div foo="false" />');
const expected = false;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
it('should convert "FaLsE" to boolean type', () => {
const prop = extractProp('<div foo="FaLsE" />');
const expected = false;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
it('should return String null when value is null', () => {
const prop = extractProp('<div foo={null} />');
const expected = 'null';
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
});
describe('JSXElement', () => {
it('should return null', () => {
const prop = extractProp('<div foo=<bar /> />');
const expected = null;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Identifier', () => {
it('should return null', () => {
const prop = extractProp('<div foo={bar} />');
const expected = null;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
it('should return undefined when identifier is literally `undefined`', () => {
const prop = extractProp('<div foo={undefined} />');
const expected = undefined;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Template literal', () => {
it('should return template literal with vars wrapped in curly braces', () => {
const prop = extractProp('<div foo={`bar ${baz}`} />');
const expected = 'bar {baz}';
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
it('should drop variables in template literals that are literally undefined', () => {
const prop = extractProp('<div foo={`bar ${undefined}`} />');
const expected = 'bar ';
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Tagged Template literal', () => {
it('should return template literal with vars wrapped in curly braces', () => {
const prop = extractProp('<div foo={noop`bar ${baz}`} />');
const expected = 'bar {baz}';
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
it('should drop variables in template literals that are literally undefined', () => {
const prop = extractProp('<div foo={noop`bar ${undefined}`} />');
const expected = 'bar ';
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Arrow function expression', () => {
it('should return null', () => {
const prop = extractProp('<div foo={ () => { return "bar"; }} />');
const expected = null;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Function expression', () => {
it('should return null', () => {
const prop = extractProp('<div foo={ function() { return "bar"; } } />');
const expected = null;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Logical expression', () => {
it('should return null for && operator', () => {
const prop = extractProp('<div foo={bar && baz} />');
const expected = null;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
it('should return null for || operator', () => {
const prop = extractProp('<div foo={bar || baz} />');
const expected = null;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Member expression', () => {
it('should return null', () => {
const prop = extractProp('<div foo={bar.baz} />');
const expected = null;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Call expression', () => {
it('should return null', () => {
const prop = extractProp('<div foo={bar()} />');
const expected = null;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Unary expression', () => {
it('should correctly evaluate an expression that prefixes with -', () => {
const prop = extractProp('<div foo={-bar} />');
// -"bar" => NaN
const expected = true;
const actual = isNaN(getLiteralPropValue(prop));
assert.equal(expected, actual);
});
it('should correctly evaluate an expression that prefixes with -', () => {
const prop = extractProp('<div foo={-42} />');
const expected = -42;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
it('should correctly evaluate an expression that prefixes with +', () => {
const prop = extractProp('<div foo={+bar} />');
// +"bar" => NaN
const expected = true;
const actual = isNaN(getLiteralPropValue(prop));
assert.equal(expected, actual);
});
it('should correctly evaluate an expression that prefixes with +', () => {
const prop = extractProp('<div foo={+42} />');
const expected = 42;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
it('should correctly evaluate an expression that prefixes with !', () => {
const prop = extractProp('<div foo={!bar} />');
const expected = false; // !"bar" === false
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
it('should correctly evaluate an expression that prefixes with ~', () => {
const prop = extractProp('<div foo={~bar} />');
const expected = -1; // ~"bar" === -1
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
it('should return true when evaluating `delete foo`', () => {
const prop = extractProp('<div foo={delete x} />');
const expected = true;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
it('should return undefined when evaluating `void foo`', () => {
const prop = extractProp('<div foo={void x} />');
const expected = undefined;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
// TODO: We should fix this to check to see if we can evaluate it.
it('should return undefined when evaluating `typeof foo`', () => {
const prop = extractProp('<div foo={typeof x} />');
const expected = undefined;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Update expression', () => {
it('should correctly evaluate an expression that prefixes with ++', () => {
const prop = extractProp('<div foo={++bar} />');
// ++"bar" => NaN
const expected = true;
const actual = isNaN(getLiteralPropValue(prop));
assert.equal(expected, actual);
});
it('should correctly evaluate an expression that prefixes with --', () => {
const prop = extractProp('<div foo={--bar} />');
// --"bar" => NaN
const expected = true;
const actual = isNaN(getLiteralPropValue(prop));
assert.equal(expected, actual);
});
it('should correctly evaluate an expression that suffixes with ++', () => {
const prop = extractProp('<div foo={bar++} />');
// "bar"++ => NaN
const expected = true;
const actual = isNaN(getLiteralPropValue(prop));
assert.equal(expected, actual);
});
it('should correctly evaluate an expression that suffixes with --', () => {
const prop = extractProp('<div foo={bar--} />');
// "bar"-- => NaN
const expected = true;
const actual = isNaN(getLiteralPropValue(prop));
assert.equal(expected, actual);
});
});
describe('This expression', () => {
it('should return null', () => {
const prop = extractProp('<div foo={this} />');
const expected = null;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Conditional expression', () => {
it('should return null', () => {
const prop = extractProp('<div foo={bar ? baz : bam} />');
const expected = null;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Binary expression', () => {
it('should return null', () => {
const prop = extractProp('<div foo={1 == "1"} />');
const expected = null;
const actual = getLiteralPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Object expression', () => {
it('should return null', () => {
const prop = extractProp('<div foo={ { bar: "baz" } } />');
const expected = null;
const actual = getLiteralPropValue(prop);
assert.deepEqual(expected, actual);
});
});
describe('New expression', () => {
it('should return null', () => {
const prop = extractProp('<div foo={new Bar()} />');
const expected = null;
const actual = getLiteralPropValue(prop);
assert.deepEqual(expected, actual);
});
});
describe('Array expression', () => {
it('should evaluate to correct representation of the the array in props', () => {
const prop = extractProp('<div foo={["bar", 42, null]} />');
const expected = ['bar', 42];
const actual = getLiteralPropValue(prop);
assert.deepEqual(expected, actual);
});
});
it('should return an empty array provided an empty array in props', () => {
const prop = extractProp('<div foo={[]} />');
const expected = [];
const actual = getLiteralPropValue(prop);
assert.deepEqual(expected, actual);
});
describe('Bind expression', () => {
it('should return null', () => {
const prop = extractProp('<div foo={::this.handleClick} />');
const expected = null;
const actual = getLiteralPropValue(prop);
assert.deepEqual(expected, actual);
});
});
});

View File

@@ -0,0 +1,837 @@
/* eslint-env mocha */
/* eslint no-template-curly-in-string: 0 */
import assert from 'assert';
import { extractProp } from '../helper';
import getPropValue from '../../src/getPropValue';
describe('getPropValue', () => {
it('should export a function', () => {
const expected = 'function';
const actual = typeof getPropValue;
assert.equal(expected, actual);
});
it('should return undefined when not provided with a JSXAttribute', () => {
const expected = undefined;
const actual = getPropValue(1);
assert.equal(expected, actual);
});
it('should throw error when trying to get value from unknown node type', () => {
const prop = {
type: 'JSXAttribute',
value: {
type: 'JSXExpressionContainer',
},
};
assert.throws(() => {
getPropValue(prop);
}, Error);
});
describe('Null', () => {
it('should return true when no value is given', () => {
const prop = extractProp('<div foo />');
const expected = true;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Literal', () => {
it('should return correct string if value is a string', () => {
const prop = extractProp('<div foo="bar" />');
const expected = 'bar';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should return correct string if value is a string expression', () => {
const prop = extractProp('<div foo={"bar"} />');
const expected = 'bar';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should return correct integer if value is a integer expression', () => {
const prop = extractProp('<div foo={1} />');
const expected = 1;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should convert "true" to boolean type', () => {
const prop = extractProp('<div foo="true" />');
const expected = true;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should convert "false" to boolean type', () => {
const prop = extractProp('<div foo="false" />');
const expected = false;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
});
describe('JSXElement', () => {
it('should return correct representation of JSX element as a string', () => {
const prop = extractProp('<div foo=<bar /> />');
const expected = '<bar />';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Identifier', () => {
it('should return string representation of variable identifier', () => {
const prop = extractProp('<div foo={bar} />');
const expected = 'bar';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should return undefined when identifier is literally `undefined`', () => {
const prop = extractProp('<div foo={undefined} />');
const expected = undefined;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should return String object when using a reserved JavaScript object', () => {
const prop = extractProp('<div foo={String} />');
const expected = String;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should return Array object when using a reserved JavaScript object', () => {
const prop = extractProp('<div foo={Array} />');
const expected = Array;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should return Date object when using a reserved JavaScript object', () => {
const prop = extractProp('<div foo={Date} />');
const expected = Date;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should return Infinity object when using a reserved JavaScript object', () => {
const prop = extractProp('<div foo={Infinity} />');
const expected = Infinity;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should return Math object when using a reserved JavaScript object', () => {
const prop = extractProp('<div foo={Math} />');
const expected = Math;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should return Number object when using a reserved JavaScript object', () => {
const prop = extractProp('<div foo={Number} />');
const expected = Number;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should return Object object when using a reserved JavaScript object', () => {
const prop = extractProp('<div foo={Object} />');
const expected = Object;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Template literal', () => {
it('should return template literal with vars wrapped in curly braces', () => {
const prop = extractProp('<div foo={`bar ${baz}`} />');
const expected = 'bar {baz}';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should drop variables in template literals that are literally undefined', () => {
const prop = extractProp('<div foo={`bar ${undefined}`} />');
const expected = 'bar ';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should return template literal with expression type wrapped in curly braces', () => {
const prop = extractProp('<div foo={`bar ${baz()}`} />');
const expected = 'bar {CallExpression}';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should ignore non-expressions in the template literal', () => {
const prop = extractProp('<div foo={`bar ${<baz />}`} />');
const expected = 'bar ';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Tagged Template literal', () => {
it('should return template literal with vars wrapped in curly braces', () => {
const prop = extractProp('<div foo={noop`bar ${baz}`} />');
const expected = 'bar {baz}';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should drop variables in template literals that are literally undefined', () => {
const prop = extractProp('<div foo={noop`bar ${undefined}`} />');
const expected = 'bar ';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should return template literal with expression type wrapped in curly braces', () => {
const prop = extractProp('<div foo={noop`bar ${baz()}`} />');
const expected = 'bar {CallExpression}';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should ignore non-expressions in the template literal', () => {
const prop = extractProp('<div foo={noop`bar ${<baz />}`} />');
const expected = 'bar ';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Arrow function expression', () => {
it('should return a function', () => {
const prop = extractProp('<div foo={ () => { return "bar"; }} />');
const expected = 'function';
const actual = getPropValue(prop);
assert.equal(expected, typeof actual);
// For code coverage ¯\_(ツ)_/¯
actual();
});
it('should handle ArrowFunctionExpression as conditional consequent', () => {
const prop = extractProp('<div foo={ (true) ? () => null : () => ({})} />');
const expected = 'function';
const actual = getPropValue(prop);
assert.equal(expected, typeof actual);
// For code coverage ¯\_(ツ)_/¯
actual();
});
});
describe('Function expression', () => {
it('should return a function', () => {
const prop = extractProp('<div foo={ function() { return "bar"; } } />');
const expected = 'function';
const actual = getPropValue(prop);
assert.equal(expected, typeof actual);
// For code coverage ¯\_(ツ)_/¯
actual();
});
});
describe('Logical expression', () => {
it('should correctly infer result of && logical expression based on derived values', () => {
const prop = extractProp('<div foo={bar && baz} />');
const expected = 'baz';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should return undefined when evaluating `undefined && undefined` ', () => {
const prop = extractProp('<div foo={undefined && undefined} />');
const expected = undefined;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should correctly infer result of || logical expression based on derived values', () => {
const prop = extractProp('<div foo={bar || baz} />');
const expected = 'bar';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should correctly infer result of || logical expression based on derived values', () => {
const prop = extractProp('<div foo={undefined || baz} />');
const expected = 'baz';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should return undefined when evaluating `undefined || undefined` ', () => {
const prop = extractProp('<div foo={undefined || undefined} />');
const expected = undefined;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Member expression', () => {
it('should return string representation of form `object.property`', () => {
const prop = extractProp('<div foo={bar.baz} />');
const expected = 'bar.baz';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Call expression', () => {
it('should return string representation of callee', () => {
const prop = extractProp('<div foo={bar()} />');
const expected = 'bar';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should return string representation of callee', () => {
const prop = extractProp('<div foo={bar.call()} />');
const expected = 'bar.call';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Unary expression', () => {
it('should correctly evaluate an expression that prefixes with -', () => {
const prop = extractProp('<div foo={-bar} />');
// -"bar" => NaN
const expected = true;
const actual = isNaN(getPropValue(prop));
assert.equal(expected, actual);
});
it('should correctly evaluate an expression that prefixes with -', () => {
const prop = extractProp('<div foo={-42} />');
const expected = -42;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should correctly evaluate an expression that prefixes with +', () => {
const prop = extractProp('<div foo={+bar} />');
// +"bar" => NaN
const expected = true;
const actual = isNaN(getPropValue(prop));
assert.equal(expected, actual);
});
it('should correctly evaluate an expression that prefixes with +', () => {
const prop = extractProp('<div foo={+42} />');
const expected = 42;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should correctly evaluate an expression that prefixes with !', () => {
const prop = extractProp('<div foo={!bar} />');
const expected = false; // !"bar" === false
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should correctly evaluate an expression that prefixes with ~', () => {
const prop = extractProp('<div foo={~bar} />');
const expected = -1; // ~"bar" === -1
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should return true when evaluating `delete foo`', () => {
const prop = extractProp('<div foo={delete x} />');
const expected = true;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should return undefined when evaluating `void foo`', () => {
const prop = extractProp('<div foo={void x} />');
const expected = undefined;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
// TODO: We should fix this to check to see if we can evaluate it.
it('should return undefined when evaluating `typeof foo`', () => {
const prop = extractProp('<div foo={typeof x} />');
const expected = undefined;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Update expression', () => {
it('should correctly evaluate an expression that prefixes with ++', () => {
const prop = extractProp('<div foo={++bar} />');
// ++"bar" => NaN
const expected = true;
const actual = isNaN(getPropValue(prop));
assert.equal(expected, actual);
});
it('should correctly evaluate an expression that prefixes with --', () => {
const prop = extractProp('<div foo={--bar} />');
const expected = true;
const actual = isNaN(getPropValue(prop));
assert.equal(expected, actual);
});
it('should correctly evaluate an expression that suffixes with ++', () => {
const prop = extractProp('<div foo={bar++} />');
// "bar"++ => NaN
const expected = true;
const actual = isNaN(getPropValue(prop));
assert.equal(expected, actual);
});
it('should correctly evaluate an expression that suffixes with --', () => {
const prop = extractProp('<div foo={bar--} />');
const expected = true;
const actual = isNaN(getPropValue(prop));
assert.equal(expected, actual);
});
});
describe('This expression', () => {
it('should return string value `this`', () => {
const prop = extractProp('<div foo={this} />');
const expected = 'this';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Conditional expression', () => {
it('should evaluate the conditional based on the derived values correctly', () => {
const prop = extractProp('<div foo={bar ? baz : bam} />');
const expected = 'baz';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should evaluate the conditional based on the derived values correctly', () => {
const prop = extractProp('<div foo={undefined ? baz : bam} />');
const expected = 'bam';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should evaluate the conditional based on the derived values correctly', () => {
const prop = extractProp('<div foo={(1 > 2) ? baz : bam} />');
const expected = 'bam';
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Binary expression', () => {
it('should evaluate the `==` operator correctly', () => {
const trueProp = extractProp('<div foo={1 == "1"} />');
const falseProp = extractProp('<div foo={1 == bar} />');
const trueVal = getPropValue(trueProp);
const falseVal = getPropValue(falseProp);
assert.equal(true, trueVal);
assert.equal(false, falseVal);
});
it('should evaluate the `!=` operator correctly', () => {
const trueProp = extractProp('<div foo={1 != "2"} />');
const falseProp = extractProp('<div foo={1 != "1"} />');
const trueVal = getPropValue(trueProp);
const falseVal = getPropValue(falseProp);
assert.equal(true, trueVal);
assert.equal(false, falseVal);
});
it('should evaluate the `===` operator correctly', () => {
const trueProp = extractProp('<div foo={1 === 1} />');
const falseProp = extractProp('<div foo={1 === "1"} />');
const trueVal = getPropValue(trueProp);
const falseVal = getPropValue(falseProp);
assert.equal(true, trueVal);
assert.equal(false, falseVal);
});
it('should evaluate the `!==` operator correctly', () => {
const trueProp = extractProp('<div foo={1 !== "1"} />');
const falseProp = extractProp('<div foo={1 !== 1} />');
const trueVal = getPropValue(trueProp);
const falseVal = getPropValue(falseProp);
assert.equal(true, trueVal);
assert.equal(false, falseVal);
});
it('should evaluate the `<` operator correctly', () => {
const trueProp = extractProp('<div foo={1 < 2} />');
const falseProp = extractProp('<div foo={1 < 0} />');
const trueVal = getPropValue(trueProp);
const falseVal = getPropValue(falseProp);
assert.equal(true, trueVal);
assert.equal(false, falseVal);
});
it('should evaluate the `>` operator correctly', () => {
const trueProp = extractProp('<div foo={1 > 0} />');
const falseProp = extractProp('<div foo={1 > 2} />');
const trueVal = getPropValue(trueProp);
const falseVal = getPropValue(falseProp);
assert.equal(true, trueVal);
assert.equal(false, falseVal);
});
it('should evaluate the `<=` operator correctly', () => {
const trueProp = extractProp('<div foo={1 <= 1} />');
const falseProp = extractProp('<div foo={1 <= 0} />');
const trueVal = getPropValue(trueProp);
const falseVal = getPropValue(falseProp);
assert.equal(true, trueVal);
assert.equal(false, falseVal);
});
it('should evaluate the `>=` operator correctly', () => {
const trueProp = extractProp('<div foo={1 >= 1} />');
const falseProp = extractProp('<div foo={1 >= 2} />');
const trueVal = getPropValue(trueProp);
const falseVal = getPropValue(falseProp);
assert.equal(true, trueVal);
assert.equal(false, falseVal);
});
it('should evaluate the `<<` operator correctly', () => {
const prop = extractProp('<div foo={1 << 2} />');
const expected = 4;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should evaluate the `>>` operator correctly', () => {
const prop = extractProp('<div foo={1 >> 2} />');
const expected = 0;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should evaluate the `>>>` operator correctly', () => {
const prop = extractProp('<div foo={2 >>> 1} />');
const expected = 1;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should evaluate the `+` operator correctly', () => {
const prop = extractProp('<div foo={1 + 1} />');
const expected = 2;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should evaluate the `-` operator correctly', () => {
const prop = extractProp('<div foo={1 - 1} />');
const expected = 0;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should evaluate the `*` operator correctly', () => {
const prop = extractProp('<div foo={10 * 10} />');
const expected = 100;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should evaluate the `/` operator correctly', () => {
const prop = extractProp('<div foo={10 / 2} />');
const expected = 5;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should evaluate the `%` operator correctly', () => {
const prop = extractProp('<div foo={10 % 3} />');
const expected = 1;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should evaluate the `|` operator correctly', () => {
const prop = extractProp('<div foo={10 | 1} />');
const expected = 11;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should evaluate the `^` operator correctly', () => {
const prop = extractProp('<div foo={10 ^ 1} />');
const expected = 11;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should evaluate the `&` operator correctly', () => {
const prop = extractProp('<div foo={10 & 1} />');
const expected = 0;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should evaluate the `in` operator correctly', () => {
const prop = extractProp('<div foo={foo in bar} />');
const expected = false;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should evaluate the `instanceof` operator correctly', () => {
const prop = extractProp('<div foo={{} instanceof Object} />');
const expected = true;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
it('should evaluate the `instanceof` operator when right side is not a function', () => {
const prop = extractProp('<div foo={"bar" instanceof Baz} />');
const expected = false;
const actual = getPropValue(prop);
assert.equal(expected, actual);
});
});
describe('Object expression', () => {
it('should evaluate to a correct representation of the object in props', () => {
const prop = extractProp('<div foo={ { bar: "baz" } } />');
const expected = { bar: 'baz' };
const actual = getPropValue(prop);
assert.deepEqual(expected, actual);
});
});
describe('New expression', () => {
it('should return a new empty object', () => {
const prop = extractProp('<div foo={new Bar()} />');
const expected = {};
const actual = getPropValue(prop);
assert.deepEqual(expected, actual);
});
});
describe('Array expression', () => {
it('should evaluate to correct representation of the the array in props', () => {
const prop = extractProp('<div foo={["bar", 42, null]} />');
const expected = ['bar', 42, null];
const actual = getPropValue(prop);
assert.deepEqual(expected, actual);
});
});
it('should return an empty array provided an empty array in props', () => {
const prop = extractProp('<div foo={[]} />');
const expected = [];
const actual = getPropValue(prop);
assert.deepEqual(expected, actual);
});
describe('Bind expression', () => {
it('should return string representation of bind function call when object is null', () => {
const prop = extractProp('<div foo={::this.handleClick} />');
const expected = 'this.handleClick.bind(this)';
const actual = getPropValue(prop);
assert.deepEqual(expected, actual);
});
it('should return string representation of bind function call when object is not null', () => {
const prop = extractProp('<div foo={foo::bar} />');
const expected = 'bar.bind(foo)';
const actual = getPropValue(prop);
assert.deepEqual(expected, actual);
});
it('should return string representation of bind function call when binding to object properties', () => {
const prop = extractProp('<div foo={a.b::c} />');
const otherProp = extractProp('<div foo={::a.b.c} />');
const expected = 'a.b.c.bind(a.b)';
const actual = getPropValue(prop);
const otherExpected = 'a.b.c.bind(a.b)';
const otherActual = getPropValue(otherProp);
assert.deepEqual(expected, actual);
assert.deepEqual(otherExpected, otherActual);
});
});
});

View File

@@ -0,0 +1,409 @@
/* eslint-env mocha */
import assert from 'assert';
import { getOpeningElement } from '../helper';
import hasProp, { hasAnyProp, hasEveryProp } from '../../src/hasProp';
describe('hasProp', () => {
it('should export a function', () => {
const expected = 'function';
const actual = typeof hasProp;
assert.equal(expected, actual);
});
it('should return false if no arguments are provided', () => {
const expected = false;
const actual = hasProp();
assert.equal(expected, actual);
});
it('should return false if the prop is absent', () => {
const code = '<div />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = 'id';
const expected = false;
const actual = hasProp(props, prop);
assert.equal(expected, actual);
});
it('should return true if the prop exists', () => {
const code = '<div id="foo" />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = 'id';
const expected = true;
const actual = hasProp(props, prop);
assert.equal(expected, actual);
});
it('should return true if the prop may exist in spread loose mode', () => {
const code = '<div {...props} />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = 'id';
const options = {
spreadStrict: false,
};
const expected = true;
const actual = hasProp(props, prop, options);
assert.equal(expected, actual);
});
it('should return false if the prop is considered absent in case-sensitive mode', () => {
const code = '<div ID="foo" />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = 'id';
const options = {
ignoreCase: false,
};
const expected = false;
const actual = hasProp(props, prop, options);
assert.equal(expected, actual);
});
});
describe('hasAnyProp tests', () => {
it('should export a function', () => {
const expected = 'function';
const actual = typeof hasAnyProp;
assert.equal(expected, actual);
});
it('should return false if no arguments are provided', () => {
const expected = false;
const actual = hasAnyProp();
assert.equal(expected, actual);
});
it('should return false if the prop is absent', () => {
const code = '<div />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = 'id';
const expected = false;
const actual = hasAnyProp(props, prop);
assert.equal(expected, actual);
});
it('should return false if all props are absent in array', () => {
const code = '<div />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const propsToCheck = ['id', 'className'];
const expected = false;
const actual = hasAnyProp(props, propsToCheck);
assert.equal(expected, actual);
});
it('should return false if all props are absent in space delimited string', () => {
const code = '<div />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const propsToCheck = 'id className';
const expected = false;
const actual = hasAnyProp(props, propsToCheck);
assert.equal(expected, actual);
});
it('should return true if the prop exists', () => {
const code = '<div id="foo" />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = 'id';
const expected = true;
const actual = hasAnyProp(props, prop);
assert.equal(expected, actual);
});
it('should return true if any prop exists in array', () => {
const code = '<div id="foo" />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = ['className', 'id'];
const expected = true;
const actual = hasAnyProp(props, prop);
assert.equal(expected, actual);
});
it('should return true if any prop exists in space delimited string', () => {
const code = '<div id="foo" />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = 'className id';
const expected = true;
const actual = hasAnyProp(props, prop);
assert.equal(expected, actual);
});
it('should return true if the prop may exist in spread loose mode', () => {
const code = '<div {...props} />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = 'id';
const options = {
spreadStrict: false,
};
const expected = true;
const actual = hasAnyProp(props, prop, options);
assert.equal(expected, actual);
});
it('should return true if any prop may exist in spread loose mode', () => {
const code = '<div {...props} />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = ['id', 'className'];
const options = {
spreadStrict: false,
};
const expected = true;
const actual = hasAnyProp(props, prop, options);
assert.equal(expected, actual);
});
it('should return false if the prop is considered absent in case-sensitive mode', () => {
const code = '<div ID="foo" />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = 'id';
const options = {
ignoreCase: false,
};
const expected = false;
const actual = hasAnyProp(props, prop, options);
assert.equal(expected, actual);
});
it('should return false if all props are considered absent in case-sensitive mode', () => {
const code = '<div ID="foo" />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = ['id', 'iD', 'className'];
const options = {
ignoreCase: false,
};
const expected = false;
const actual = hasAnyProp(props, prop, options);
assert.equal(expected, actual);
});
});
describe('hasEveryProp tests', () => {
it('should export a function', () => {
const expected = 'function';
const actual = typeof hasEveryProp;
assert.equal(expected, actual);
});
it('should return true if no arguments are provided', () => {
const expected = true;
const actual = hasEveryProp();
assert.equal(expected, actual);
});
it('should return false if the prop is absent', () => {
const code = '<div />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = 'id';
const expected = false;
const actual = hasEveryProp(props, prop);
assert.equal(expected, actual);
});
it('should return false if any props are absent in array', () => {
const code = '<div id="foo" />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const propsToCheck = ['id', 'className'];
const expected = false;
const actual = hasEveryProp(props, propsToCheck);
assert.equal(expected, actual);
});
it('should return false if all props are absent in array', () => {
const code = '<div />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const propsToCheck = ['id', 'className'];
const expected = false;
const actual = hasEveryProp(props, propsToCheck);
assert.equal(expected, actual);
});
it('should return false if any props are absent in space delimited string', () => {
const code = '<div id="foo" />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const propsToCheck = 'id className';
const expected = false;
const actual = hasEveryProp(props, propsToCheck);
assert.equal(expected, actual);
});
it('should return false if all props are absent in space delimited string', () => {
const code = '<div />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const propsToCheck = 'id className';
const expected = false;
const actual = hasEveryProp(props, propsToCheck);
assert.equal(expected, actual);
});
it('should return true if the prop exists', () => {
const code = '<div id="foo" />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = 'id';
const expected = true;
const actual = hasEveryProp(props, prop);
assert.equal(expected, actual);
});
it('should return true if all props exist in array', () => {
const code = '<div id="foo" className="box" />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = ['className', 'id'];
const expected = true;
const actual = hasEveryProp(props, prop);
assert.equal(expected, actual);
});
it('should return true if all props exist in space delimited string', () => {
const code = '<div id="foo" className="box" />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = 'className id';
const expected = true;
const actual = hasEveryProp(props, prop);
assert.equal(expected, actual);
});
it('should return true if the props may exist in spread loose mode', () => {
const code = '<div {...props} />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = 'id';
const options = {
spreadStrict: false,
};
const expected = true;
const actual = hasEveryProp(props, prop, options);
assert.equal(expected, actual);
});
it('should return true if all props may exist in spread loose mode', () => {
const code = '<div {...props} />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = ['id', 'className'];
const options = {
spreadStrict: false,
};
const expected = true;
const actual = hasEveryProp(props, prop, options);
assert.equal(expected, actual);
});
it('should return false if the prop is considered absent in case-sensitive mode', () => {
const code = '<div ID="foo" />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = 'id';
const options = {
ignoreCase: false,
};
const expected = false;
const actual = hasEveryProp(props, prop, options);
assert.equal(expected, actual);
});
it('should return false if all props are considered absent in case-sensitive mode', () => {
const code = '<div ID="foo" />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = ['id', 'iD', 'className'];
const options = {
ignoreCase: false,
};
const expected = false;
const actual = hasEveryProp(props, prop, options);
assert.equal(expected, actual);
});
it('should return true if all props are considered present in case-sensitive mode', () => {
const code = '<div ID="foo" className="box" />';
const node = getOpeningElement(code);
const { attributes: props } = node;
const prop = ['ID', 'className'];
const options = {
ignoreCase: false,
};
const expected = true;
const actual = hasEveryProp(props, prop, options);
assert.equal(expected, actual);
});
});

View File

@@ -0,0 +1,35 @@
/* eslint-env mocha */
import fs from 'fs';
import path from 'path';
import assert from 'assert';
import core from '../../src/index';
const src = fs.readdirSync(path.resolve(__dirname, '../../src'))
.filter(f => f.indexOf('.js') >= 0)
.map(f => path.basename(f, '.js'));
describe('main export', () => {
it('should export an object', () => {
const expected = 'object';
const actual = typeof core;
assert.equal(expected, actual);
});
src.filter(f => f !== 'index').forEach((f) => {
it(`should export ${f}`, () => {
assert.equal(
core[f],
require(path.join('../../src/', f)).default // eslint-disable-line
);
});
it(`should export ${f} from root`, () => {
const file = `${f}.js`;
const expected = true;
const actual = fs.statSync(path.join(path.resolve('.'), file)).isFile();
assert.equal(expected, actual);
});
});
});

View File

@@ -0,0 +1,39 @@
/* eslint-env mocha */
import assert from 'assert';
import { extractProp } from '../helper';
import propName from '../../src/propName';
describe('propName', () => {
it('should export a function', () => {
const expected = 'function';
const actual = typeof propName;
assert.equal(expected, actual);
});
it('should throw an error if the argument is missing', () => {
assert.throws(() => { propName(); }, Error);
});
it('should throw an error if the argument not a JSX node', () => {
assert.throws(() => { propName({ a: 'foo' }); }, Error);
});
it('should return correct name for normal prop', () => {
const prop = extractProp('<div foo="bar" />');
const expected = 'foo';
const actual = propName(prop);
assert.equal(expected, actual);
});
it('should return correct name for namespaced prop', () => {
const prop = extractProp('<div foo:bar="baz" />', 'foo:bar');
const expected = 'foo:bar';
const actual = propName(prop);
assert.equal(expected, actual);
});
});

View File

@@ -0,0 +1 @@
module.exports = require('./lib').elementType; // eslint-disable-line import/no-unresolved

View File

@@ -0,0 +1 @@
module.exports = require('./lib').eventHandlers; // eslint-disable-line import/no-unresolved

View File

@@ -0,0 +1 @@
module.exports = require('./lib').eventHandlersByType; // eslint-disable-line import/no-unresolved

View File

@@ -0,0 +1 @@
module.exports = require('./lib').getLiteralPropValue; // eslint-disable-line import/no-unresolved

View File

@@ -0,0 +1 @@
module.exports = require('./lib').getProp; // eslint-disable-line import/no-unresolved

View File

@@ -0,0 +1 @@
module.exports = require('./lib').getPropValue; // eslint-disable-line import/no-unresolved

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