Files

Return to Package Diff Home.
Brought to you by Intrinsic.

Package Diff: eslint-plugin-react @ 7.8.2 .. 7.12.4

CHANGELOG.md

@@ -3,6 +3,294 @@
This project adheres to [Semantic Versioning](http://semver.org/).
This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com).
+## [7.12.4] - 2019-01-16
+
+### Fixed
+* [`no-unused-prop-types`][]: avoid a crash ([#2131][], @ljharb)
+* [`prop-types`][]: avoid further crashes from nonexistent nodes in unusedPropTypes ([#2127][], @ljharb)
+* [`prop-types`][]: Read name of callee object ([#2125][], @CrOrc)
+* [`prop-types`][]: Ignore reassignments when matching props declarations with components ([#2051][], [#1957][], @yannickcr)
+* [`prop-types`][], [`no-unused-prop-types`][], [`require-default-props`][]: Detect components with return statement in switch/case ([#2118][], @yannickcr)
+
+### Changed
+* [`prop-types`][], [`no-typos`][]: add passing test cases ([#2123][], [#2128][], [#2136][], [#2134][], @ljharb)
+
+[#2136]: https://github.com/yannickcr/eslint-plugin-react/issues/2136
+[#2134]: https://github.com/yannickcr/eslint-plugin-react/issues/2134
+[#2131]: https://github.com/yannickcr/eslint-plugin-react/issues/2131
+[#2128]: https://github.com/yannickcr/eslint-plugin-react/issues/2128
+[#2127]: https://github.com/yannickcr/eslint-plugin-react/issues/2127
+[#2125]: https://github.com/yannickcr/eslint-plugin-react/pull/2125
+[#2123]: https://github.com/yannickcr/eslint-plugin-react/issues/2123
+[#2118]: https://github.com/yannickcr/eslint-plugin-react/issues/2118
+[#2051]: https://github.com/yannickcr/eslint-plugin-react/issues/2051
+[#1957]: https://github.com/yannickcr/eslint-plugin-react/issues/1957
+
+## [7.12.3] - 2019-01-04
+
+### Fixed
+* [`jsx-indent`][]: Prevent crash on valueless props ([#2120][], @jomasti)
+* [`jsx-fragments`][]: avoid crashing on self-closing fragments ([#2113][], @alexzherdev)
+* [`no-unused-prop-types`][]: Fix propType detection inside class bodies ([#2115][], @drx)
+* [`no-unused-prop-types`][]: fix issue with propTypes misclassifying props ([#2111][], @drx)
+* [`display-name`][]: fix false positive for `React.memo` ([#2109][], @jomasti)
+
+### Changed
+* [Docs] add a missing comma in the JSON settings ([#2117][], @haideralsh)
+* [Docs] update README to document React version detection ([#2114][], @mohsinulhaq)
+
+[#2120]: https://github.com/yannickcr/eslint-plugin-react/issues/2120
+[#2117]: https://github.com/yannickcr/eslint-plugin-react/issues/2117
+[#2115]: https://github.com/yannickcr/eslint-plugin-react/issues/2115
+[#2114]: https://github.com/yannickcr/eslint-plugin-react/issues/2114
+[#2113]: https://github.com/yannickcr/eslint-plugin-react/issues/2113
+[#2111]: https://github.com/yannickcr/eslint-plugin-react/issues/2111
+[#2109]: https://github.com/yannickcr/eslint-plugin-react/issues/2109
+
+## [7.12.2] - 2019-01-02
+
+### Fixed
+* [`prop-types`][]: avoid crash on used prevProps ([#2095][], @ljharb)
+* Version warning: Link does not end with '.' ([#2103][], @yoyo837))
+* [`forbid-prop-types`][]: fix crash with propWrapper check on MemberExpressions ([#2104][], @ljharb)
+
+[#2104]: https://github.com/yannickcr/eslint-plugin-react/issues/2104
+[#2103]: https://github.com/yannickcr/eslint-plugin-react/pull/2103
+[#2095]: https://github.com/yannickcr/eslint-plugin-react/issues/2095
+
+## [7.12.1] - 2019-01-01
+
+### Fixed
+* [`no-unused-state`][]: Fix crash with class fields ([#2098][], @jomasti)
+* [`prop-types`][]: Fix false positives inside lifecycle methods ([#2099][], @jomasti)
+* [`jsx-max-depth`][]: avoid a crash ([#2102][], @ljharb)
+* [`jsx-wrap-multilines`][]: avoid crash when no trailing newline ([#2100][], @ljharb)
+
+### Changed
+* Fix CHANGELOG.md ([#2097][], @alexzherdev)
+
+[#2102]: https://github.com/yannickcr/eslint-plugin-react/issues/2102
+[#2100]: https://github.com/yannickcr/eslint-plugin-react/issues/2100
+[#2099]: https://github.com/yannickcr/eslint-plugin-react/pull/2099
+[#2098]: https://github.com/yannickcr/eslint-plugin-react/pull/2098
+[#2097]: https://github.com/yannickcr/eslint-plugin-react/pull/2097
+
+## [7.12.0] - 2018-12-27
+
+### Added
+* [`no-typos`]: Support createClass ([#1828][], @alexzherdev)
+* Support detecting React.forwardRef/React.memo ([#2089][], @jomasti)
+* [`jsx-indent`][]: add `checkAttributes` option for JSX attribute indentation ([#2086][], @jomasti)
+* Change allowed `propWrapperFunctions` setting values ([#2065][], @jomasti)
+* add [`jsx-fragments`][] rule to enforce fragment syntax ([#1994][], @alexzherdev)
+* Support "detect" option for React version setting ([#1978][], @alexzherdev)
+* Support shorthand fragment syntax in many rules ([#1956][], @alexzherdev)
+* [`jsx-no-literals`][]: print node value in warning message ([#2008][], @jlgonzalezdev)
+
+### Fixed
+* [`jsx-max-depth`][]: Fix depth of JSX siblings in a JSXEpressionContainer ([#1824][], @alexzherdev)
+* [`no-array-index-key`][]: fix in React.Children methods ([#2085][], @himynameisdave)
+* [`no-unused-state`][]: handle functional setState ([#2084][], @jomasti)
+* version errors should log to stderr, not stdout ([#2082][], @ljharb)
+* [`no-deprecated`][]: Disable legacy lifecycle methods linting for now ([#2069][], @sergei-startsev)
+* ensure that react and flow versions can be numbers ([#2056][], @ljharb)
+* [`forbid-foreign-prop-types`][]: ensure `allowInPropTypes` option applies to class fields ([#2040][], @Sheile)
+* [`jsx-wrap-multilines`][]: catch single missing newlines ([#1984][], @MrHen)
+* [`jsx-first-prop-new-line`][]: Fix for parsers (like TypeScript) ([#2026][], @HauptmannEck)
+* [`sort-comp`][]: Fix fixer in case of more than 10 props ([#2012][], @tihonove)
+* [`no-unused-state`][] Don't depend on state parameter name ([#1829][], @alexzherdev)
+* [`no-this-in-sfc`][] fix for class properties ([#1995][], @sergei-startsev)
+* [`no-this-in-sfc`][] fix rule behavior for arrow functions inside a class field ([#1989][], @sergei-startsev)
+* [`destructuring-assignment`][]: handle nested props usage ([#1983][], @alexzherdev)
+* [`sort-prop-types`][]: fix string property order ([#1977][], @metreniuk)
+* [`jsx-no-target-blank`][]: don’t crash when there’s no value ([#1949][], @ljharb)
+* [`prop-types`][], [`no-unused-prop-types`][]: better handle object spread ([#1939][], @alexzherdev)
+
+### Changed
+* [`jsx-fragments`][]: improve message text ([#2032][], @alexzherdev)
+* [`no-unsafe`][]: handle all unsafe life-cycle methods ([#2075][], @sergei-startsev)
+* [`require-default-props`][]: Change error message naming from singular defaultProp to plural defaultProps ([#2064][], @jseminck)
+* [Refactor] Extract used `propTypes` detection ([#1946][], @alexzherdev)
+* [Refactor] Extract `defaultProps` detection ([#1942][], @alexzherdev)
+* [Refactor] Extract required `propTypes` detection ([#2001][], @alexzherdev)
+* [Docs] [`no-did-mount-set-state`][], [`no-did-update-set-state`][], [`no-will-update-set-state`][]: fix docs URLs ([#2090][], @JBallin)
+* [Docs] Remove statement on GC in jsx-no-bind ([#2067][], @rickhanlonii)
+* [Docs] [`jsx-sort-props`][]: Fix small mistake ([#2044][], @dimitarnestorov)
+* [Docs] [`no-unescaped-entities`][]: add more escape examples ([#2015][], @stevemao)
+* [Docs] [`display-name`][]: mention default `ignoreTranspilerName` value ([#2002][], @OliverJAsh)
+* [Docs] [`jsx-no-target-blank`][]: Add full example ([#1988][], @atomcorp)
+* [Docs] Update [`jsx-no-target-blank`][].md ([#1953][], @brunocoelho)
+* [Changelog] fix "Ignore class properties" contributor ([#1941][], @alexzherdev)
+* [Tests] Remove redundant `require('babel-eslint')` from tests ([#2004][], @sergei-startsev)
+* [Tests] [`prop-types`][]: Add tests for prop-types destructuring ([#2029][], @sstern6)
+* [Tests] [`display-name`][]: add false positive component detection for destructured createElement ([#1098][], @arian)
+
+[#2090]: https://github.com/yannickcr/eslint-plugin-react/pull/2090
+[#2089]: https://github.com/yannickcr/eslint-plugin-react/pull/2089
+[#2086]: https://github.com/yannickcr/eslint-plugin-react/pull/2086
+[#2085]: https://github.com/yannickcr/eslint-plugin-react/pull/2085
+[#2084]: https://github.com/yannickcr/eslint-plugin-react/pull/2084
+[#2082]: https://github.com/yannickcr/eslint-plugin-react/issues/2082
+[#2075]: https://github.com/yannickcr/eslint-plugin-react/pull/2075
+[#2069]: https://github.com/yannickcr/eslint-plugin-react/pull/2069
+[#2067]: https://github.com/yannickcr/eslint-plugin-react/pull/2067
+[#2065]: https://github.com/yannickcr/eslint-plugin-react/pull/2065
+[#2064]: https://github.com/yannickcr/eslint-plugin-react/pull/2064
+[#2056]: https://github.com/yannickcr/eslint-plugin-react/issues/2056
+[#2044]: https://github.com/yannickcr/eslint-plugin-react/pull/2044
+[#2040]: https://github.com/yannickcr/eslint-plugin-react/pull/2040
+[#2032]: https://github.com/yannickcr/eslint-plugin-react/pull/2032
+[#2029]: https://github.com/yannickcr/eslint-plugin-react/pull/2029
+[#2026]: https://github.com/yannickcr/eslint-plugin-react/pull/2026
+[#2015]: https://github.com/yannickcr/eslint-plugin-react/pull/2015
+[#2012]: https://github.com/yannickcr/eslint-plugin-react/pull/2012
+[#2008]: https://github.com/yannickcr/eslint-plugin-react/pull/2008
+[#2004]: https://github.com/yannickcr/eslint-plugin-react/pull/2004
+[#2002]: https://github.com/yannickcr/eslint-plugin-react/pull/2002
+[#2001]: https://github.com/yannickcr/eslint-plugin-react/pull/2001
+[#1995]: https://github.com/yannickcr/eslint-plugin-react/pull/1995
+[#1994]: https://github.com/yannickcr/eslint-plugin-react/pull/1994
+[#1989]: https://github.com/yannickcr/eslint-plugin-react/pull/1989
+[#1988]: https://github.com/yannickcr/eslint-plugin-react/pull/1988
+[#1984]: https://github.com/yannickcr/eslint-plugin-react/pull/1984
+[#1983]: https://github.com/yannickcr/eslint-plugin-react/pull/1983
+[#1978]: https://github.com/yannickcr/eslint-plugin-react/pull/1978
+[#1977]: https://github.com/yannickcr/eslint-plugin-react/pull/1977
+[#1956]: https://github.com/yannickcr/eslint-plugin-react/pull/1956
+[#1953]: https://github.com/yannickcr/eslint-plugin-react/pull/1953
+[#1949]: https://github.com/yannickcr/eslint-plugin-react/issues/1949
+[#1946]: https://github.com/yannickcr/eslint-plugin-react/pull/1946
+[#1942]: https://github.com/yannickcr/eslint-plugin-react/pull/1942
+[#1941]: https://github.com/yannickcr/eslint-plugin-react/pull/1941
+[#1939]: https://github.com/yannickcr/eslint-plugin-react/pull/1939
+[#1829]: https://github.com/yannickcr/eslint-plugin-react/pull/1829
+[#1828]: https://github.com/yannickcr/eslint-plugin-react/pull/1828
+[#1824]: https://github.com/yannickcr/eslint-plugin-react/pull/1824
+[#1098]: https://github.com/yannickcr/eslint-plugin-react/pull/1098
+
+## [7.11.1] - 2018-08-14
+### Fixed
+* stop crashing when assigning to propTypes ([#1932][], @alexzherdev)
+
+### Changed
+* Fix changelog links ([#1926][], @ferhatelmas)
+* Fix changelog links ([#1929][], @alexzherdev)
+
+[#1932]: https://github.com/yannickcr/eslint-plugin-react/pull/1932
+[#1929]: https://github.com/yannickcr/eslint-plugin-react/pull/1929
+[#1926]: https://github.com/yannickcr/eslint-plugin-react/pull/1926
+
+## [7.11.0] - 2018-08-13
+### Added
+* [`jsx-one-expression-per-line`][]: add "allow" option ([#1924][], @alexzherdev)
+* [`sort-prop-types`][]: add autofix ([#1891][], @finnp)
+* [`jsx-no-bind`][]: Add ignoreDOMComponents option ([#1868][], @alexzherdev)
+* Output a warning if React version is missing in settings ([#1857][], @alexzherdev)
+
+### Fixed
+* [`destructuring-assignment`][]: Ignore class properties ([#1909][], @alexandernanberg)
+* [`destructuring-assignment`][], component detection: ignore components with confidence = 0 ([#1907][], @alexzherdev)
+* [`boolean-prop-naming`][]: Handle inline Flow type ([#1905][], @alexzherdev)
+* [`jsx-props-no-multi-spaces`][]: Handle member expressions ([#1890][], @alexzherdev)
+* [`sort-comp`][]: Allow methods to belong to any matching group ([#1858][], @nosilleg)
+* [`jsx-sort-props`][]: Fix `reservedFirst` ([#1883][], @fleischie)
+* [`prop-types`][]: (flow) Stop crashing on undefined or null properties ([#1860][], @nicholas-l)
+* [`no-unknown-property`][]: Make attribute "charset" valid ([#1863][], @silvenon)
+* [`no-deprecated`][]: report identifier AST node instead of the class node ([#1854][], @jsnajdr)
+* [`button-has-type`][]: Account for pragma ([#1851][], @alexzherdev)
+* [`button-has-type`][]: improve error message when an identifier is used as the value ([#1874][], @ljharb)
+* support JSXText nodes alongside Literal nodes (@ljharb)
+
+### Changed
+* Extract propTypes detection code ([#1911][], @alexzherdev)
+* Fix broken links in changelog ([#1849][], @alexzherdev)
+* [`no-unused-state`][]: combine spread visitors (@ljharb)
+* [`jsx-one-expression-per-line`][]: Fix JSX Syntax in docs ([#1867][], @peter-mouland)
+* [`jsx-max-depth`][], [`jsx-sort-default-props`][]: add missing docs urls ([#1880][], @flyerhzm)
+* [`jsx-indent`][]: add test cases ([#1892][], @alexzherdev)
+* [`prop-types`][]: add test cases ([#1898][], @alexzherdev)
+* Add a helper function for determining function-like expressions ([#1914][], @alexzherdev)
+* [`jsx-props-no-multi-spaces`][]: update docs ([#1918][], @BenRichter)
+
+[#1924]: https://github.com/yannickcr/eslint-plugin-react/pull/1924
+[#1918]: https://github.com/yannickcr/eslint-plugin-react/pull/1918
+[#1914]: https://github.com/yannickcr/eslint-plugin-react/pull/1914
+[#1911]: https://github.com/yannickcr/eslint-plugin-react/pull/1911
+[#1909]: https://github.com/yannickcr/eslint-plugin-react/pull/1909
+[#1907]: https://github.com/yannickcr/eslint-plugin-react/pull/1907
+[#1905]: https://github.com/yannickcr/eslint-plugin-react/pull/1905
+[#1898]: https://github.com/yannickcr/eslint-plugin-react/pull/1898
+[#1892]: https://github.com/yannickcr/eslint-plugin-react/pull/1892
+[#1891]: https://github.com/yannickcr/eslint-plugin-react/pull/1891
+[#1890]: https://github.com/yannickcr/eslint-plugin-react/pull/1890
+[#1883]: https://github.com/yannickcr/eslint-plugin-react/pull/1883
+[#1880]: https://github.com/yannickcr/eslint-plugin-react/pull/1880
+[#1874]: https://github.com/yannickcr/eslint-plugin-react/issues/1874
+[#1868]: https://github.com/yannickcr/eslint-plugin-react/pull/1868
+[#1867]: https://github.com/yannickcr/eslint-plugin-react/pull/1867
+[#1863]: https://github.com/yannickcr/eslint-plugin-react/pull/1863
+[#1860]: https://github.com/yannickcr/eslint-plugin-react/pull/1860
+[#1858]: https://github.com/yannickcr/eslint-plugin-react/pull/1858
+[#1857]: https://github.com/yannickcr/eslint-plugin-react/pull/1857
+[#1854]: https://github.com/yannickcr/eslint-plugin-react/pull/1854
+[#1851]: https://github.com/yannickcr/eslint-plugin-react/pull/1851
+[#1849]: https://github.com/yannickcr/eslint-plugin-react/pull/1849
+
+## [7.10.0] - 2018-06-24
+### Added
+* Allow eslint ^5 ([#1843][] @papandreou, @ljharb)
+* [`no-unsafe`][] rule ([#1831][], [#1830][] @sergei-startsev)
+* [`no-will-update-set-state`][]: Account for `UNSAFE_` methods ([#1845][], [#1844][] @alexzherdev)
+
+### Fixed
+* [`no-typos`][]: Fix static propTypes handling ([#1827][], [#1677][] @alexzherdev)
+* [`destructuring-assignment`][]: Allow LHS ([#1825][], [#1728][] @alexzherdev)
+* [`no-unused-prop-types`][]: Fix crash when encountering mixed union and intersection flow types ([#1806][] @yannickcr)
+
+### Changed
+* Typo fixes in [`jsx-no-target-blank`][] ([#1805][] @ferhatelmas))
+
+[#1845]: https://github.com/yannickcr/eslint-plugin-react/pull/1845
+[#1844]: https://github.com/yannickcr/eslint-plugin-react/issues/1844
+[#1843]: https://github.com/yannickcr/eslint-plugin-react/pull/1843
+[#1831]: https://github.com/yannickcr/eslint-plugin-react/pull/1831
+[#1830]: https://github.com/yannickcr/eslint-plugin-react/issues/1830
+[#1827]: https://github.com/yannickcr/eslint-plugin-react/pull/1827
+[#1825]: https://github.com/yannickcr/eslint-plugin-react/pull/1825
+[#1806]: https://github.com/yannickcr/eslint-plugin-react/issues/1806
+[#1805]: https://github.com/yannickcr/eslint-plugin-react/pull/1805
+[#1728]: https://github.com/yannickcr/eslint-plugin-react/issues/1728
+[#1677]: https://github.com/yannickcr/eslint-plugin-react/issues/1677
+
+## [7.9.1] - 2018-06-03
+* Nothing was fixed; this is a republish with some updated deps. ([#1804][] @ljharb)
+
+[#1804]: https://github.com/yannickcr/eslint-plugin-react/issues/1804
+
+## [7.9.0] - 2018-06-03
+### Added
+* Add [`jsx-props-no-multi-spaces`][] rule ([#1755][] @ThiefMaster)
+* Add `first` option to [`jsx-indent-props`][] ([#398][] @ThiefMaster)
+* Add `enforceDynamicLinks` option to [`jsx-no-target-blank`][] ([#1737][] @kenearley)
+
+### Fixed
+* Fix static lifecycle methods validation in [`sort-comp`][] ([#1793][] @lynxtaa)
+* Fix crash in [`no-typos`][] when encountering anonymous react imports ([#1796][] @jsg2021)
+* Fix ESLint 3 support ([#1779][])
+
+### Changed
+* Documentation improvements ([#1794][] @lencioni)
+* Update Travis CI configuration to test on multiple ESLint verions
+
+[7.9.0]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.8.2...v7.9.0
+[#1755]: https://github.com/yannickcr/eslint-plugin-react/pull/1755
+[#398]: https://github.com/yannickcr/eslint-plugin-react/issues/398
+[#1737]: https://github.com/yannickcr/eslint-plugin-react/issues/1737
+[#1793]: https://github.com/yannickcr/eslint-plugin-react/issues/1793
+[#1796]: https://github.com/yannickcr/eslint-plugin-react/pull/1796
+[#1779]: https://github.com/yannickcr/eslint-plugin-react/issues/1779
+[#1794]: https://github.com/yannickcr/eslint-plugin-react/pull/1794
+
## [7.8.2] - 2018-05-13
### Fixed
* Fix crash in [`boolean-prop-naming`][] when encountering a required shape prop type ([#1791][] @pcorpet)
@@ -77,6 +365,7 @@
[#1670]: https://github.com/yannickcr/eslint-plugin-react/pull/1670
[#1669]: https://github.com/yannickcr/eslint-plugin-react/pull/1669
[#1666]: https://github.com/yannickcr/eslint-plugin-react/pull/1666
+[#1665]: https://github.com/yannickcr/eslint-plugin-react/pull/1665
[#1655]: https://github.com/yannickcr/eslint-plugin-react/pull/1655
[#1610]: https://github.com/yannickcr/eslint-plugin-react/pull/1610
[#1414]: https://github.com/yannickcr/eslint-plugin-react/pull/1414
@@ -2205,3 +2494,6 @@
[`no-this-in-sfc`]: docs/rules/no-this-in-sfc.md
[`jsx-sort-default-props`]: docs/rules/jsx-sort-default-props.md
[`jsx-max-depth`]: docs/rules/jsx-max-depth.md
+[`jsx-props-no-multi-spaces`]: docs/rules/jsx-props-no-multi-spaces.md
+[`no-unsafe`]: docs/rules/no-unsafe.md
+[`jsx-fragments`]: docs/rules/jsx-fragments.md

index.js

@@ -36,6 +36,8 @@
'jsx-no-undef': require('./lib/rules/jsx-no-undef'),
'jsx-curly-brace-presence': require('./lib/rules/jsx-curly-brace-presence'),
'jsx-pascal-case': require('./lib/rules/jsx-pascal-case'),
+ 'jsx-fragments': require('./lib/rules/jsx-fragments'),
+ 'jsx-props-no-multi-spaces': require('./lib/rules/jsx-props-no-multi-spaces'),
'jsx-sort-default-props': require('./lib/rules/jsx-sort-default-props'),
'jsx-sort-props': require('./lib/rules/jsx-sort-props'),
'jsx-space-before-closing': require('./lib/rules/jsx-space-before-closing'),
@@ -63,6 +65,7 @@
'no-typos': require('./lib/rules/no-typos'),
'no-unescaped-entities': require('./lib/rules/no-unescaped-entities'),
'no-unknown-property': require('./lib/rules/no-unknown-property'),
+ 'no-unsafe': require('./lib/rules/no-unsafe'),
'no-unused-prop-types': require('./lib/rules/no-unused-prop-types'),
'no-unused-state': require('./lib/rules/no-unused-state'),
'no-will-update-set-state': require('./lib/rules/no-will-update-set-state'),
@@ -138,6 +141,7 @@
'react/no-string-refs': 2,
'react/no-unescaped-entities': 2,
'react/no-unknown-property': 2,
+ 'react/no-unsafe': 0,
'react/prop-types': 2,
'react/react-in-jsx-scope': 2,
'react/require-render-return': 2

lib/rules/boolean-prop-naming.js

@@ -4,10 +4,10 @@
*/
'use strict';
-const has = require('has');
const Components = require('../util/Components');
const propsUtil = require('../util/props');
const docsUrl = require('../util/docsUrl');
+const propWrapperUtil = require('../util/propWrapper');
// ------------------------------------------------------------------------------
// Rule Definition
@@ -52,7 +52,6 @@
const config = context.options[0] || {};
const rule = config.rule ? new RegExp(config.rule) : null;
const propTypeNames = config.propTypeNames || ['bool'];
- const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
// Remembers all Flowtype object definitions
const objectTypeAnnotations = new Map();
@@ -68,9 +67,10 @@
* @param {Object} node The node we're getting the name of
*/
function getPropKey(node) {
- // Check for `ExperimentalSpreadProperty` so we can skip validation of those fields.
- // Otherwise it will look for `node.value.property` which doesn't exist and breaks Eslint.
- if (node.type === 'ExperimentalSpreadProperty') {
+ // Check for `ExperimentalSpreadProperty` (ESLint 3/4) and `SpreadElement` (ESLint 5)
+ // so we can skip validation of those fields.
+ // Otherwise it will look for `node.value.property` which doesn't exist and breaks ESLint.
+ if (node.type === 'ExperimentalSpreadProperty' || node.type === 'SpreadElement') {
return null;
}
if (node.value.property) {
@@ -171,7 +171,7 @@
if (!rule || !propsUtil.isPropTypesDeclaration(node)) {
return;
}
- if (node.value && node.value.type === 'CallExpression' && propWrapperFunctions.has(sourceCode.getText(node.value.callee))) {
+ if (node.value && node.value.type === 'CallExpression' && propWrapperUtil.isPropWrapperFunction(context, sourceCode.getText(node.value.callee))) {
checkPropWrapperArguments(node, node.value.arguments);
}
if (node.value && node.value.properties) {
@@ -191,7 +191,7 @@
return;
}
const right = node.parent.right;
- if (right.type === 'CallExpression' && propWrapperFunctions.has(sourceCode.getText(right.callee))) {
+ if (right.type === 'CallExpression' && propWrapperUtil.isPropWrapperFunction(context, sourceCode.getText(right.callee))) {
checkPropWrapperArguments(component.node, right.arguments);
return;
}
@@ -234,13 +234,20 @@
list[component].node.params[0].typeAnnotation
) {
const typeNode = list[component].node.params[0].typeAnnotation;
- const propType = objectTypeAnnotations.get(typeNode.typeAnnotation.id.name);
+ const annotation = typeNode.typeAnnotation;
+
+ let propType;
+ if (annotation.type === 'GenericTypeAnnotation') {
+ propType = objectTypeAnnotations.get(annotation.id.name);
+ } else if (annotation.type === 'ObjectTypeAnnotation') {
+ propType = annotation;
+ }
if (propType) {
validatePropNaming(list[component].node, propType.properties);
}
}
- if (!has(list, component) || (list[component].invalidProps || []).length) {
+ if (list[component].invalidProps && list[component].invalidProps.length > 0) {
reportInvalidNaming(list[component]);
}
});

lib/rules/button-has-type.js

@@ -7,15 +7,19 @@
const getProp = require('jsx-ast-utils/getProp');
const getLiteralPropValue = require('jsx-ast-utils/getLiteralPropValue');
const docsUrl = require('../util/docsUrl');
+const pragmaUtil = require('../util/pragma');
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
-function isCreateElement(node) {
+function isCreateElement(node, context) {
+ const pragma = pragmaUtil.getFromContext(context);
return node.callee
&& node.callee.type === 'MemberExpression'
&& node.callee.property.name === 'createElement'
+ && node.callee.object
+ && node.callee.object.name === pragma
&& node.arguments.length > 0;
}
@@ -67,16 +71,17 @@
});
}
- function checkValue(node, value) {
+ function checkValue(node, value, quoteFn) {
+ const q = quoteFn || (x => `"${x}"`);
if (!(value in configuration)) {
context.report({
node: node,
- message: `"${value}" is an invalid value for button type attribute`
+ message: `${q(value)} is an invalid value for button type attribute`
});
} else if (!configuration[value]) {
context.report({
node: node,
- message: `"${value}" is a forbidden value for button type attribute`
+ message: `${q(value)} is a forbidden value for button type attribute`
});
}
}
@@ -94,10 +99,15 @@
return;
}
- checkValue(node, getLiteralPropValue(typeProp));
+ const propValue = getLiteralPropValue(typeProp);
+ if (!propValue && typeProp.value && typeProp.value.expression) {
+ checkValue(node, typeProp.value.expression.name, x => `\`${x}\``);
+ } else {
+ checkValue(node, propValue);
+ }
},
CallExpression: function(node) {
- if (!isCreateElement(node)) {
+ if (!isCreateElement(node, context)) {
return;
}

lib/rules/default-props-match-prop-types.js

@@ -5,12 +5,7 @@
*/
'use strict';
-const has = require('has');
const Components = require('../util/Components');
-const variableUtil = require('../util/variable');
-const annotations = require('../util/annotations');
-const astUtil = require('../util/ast');
-const propsUtil = require('../util/props');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
@@ -37,258 +32,9 @@
}]
},
- create: Components.detect((context, components, utils) => {
+ create: Components.detect((context, components) => {
const configuration = context.options[0] || {};
const allowRequiredDefaults = configuration.allowRequiredDefaults || false;
- const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
-
- /**
- * Try to resolve the node passed in to a variable in the current scope. If the node passed in is not
- * an Identifier, then the node is simply returned.
- * @param {ASTNode} node The node to resolve.
- * @returns {ASTNode|null} Return null if the value could not be resolved, ASTNode otherwise.
- */
- function resolveNodeValue(node) {
- if (node.type === 'Identifier') {
- return variableUtil.findVariableByName(context, node.name);
- }
- if (
- node.type === 'CallExpression' &&
- 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 variableUtil.findVariableByName(context, 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: propsUtil.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 = variableUtil.findVariableByName(context, annotation.id.name);
- }
-
- if (!annotation || !annotation.properties) {
- return properties;
- }
-
- 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 = variableUtil.findVariableByName(context, 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 (astUtil.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.
@@ -300,12 +46,13 @@
// 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) {
+ if (defaultProps === 'unresolved' || !propTypes || Object.keys(propTypes).length === 0) {
return;
}
- defaultProps.forEach(defaultProp => {
- const prop = propFromName(propTypes, defaultProp.name);
+ Object.keys(defaultProps).forEach(defaultPropName => {
+ const defaultProp = defaultProps[defaultPropName];
+ const prop = propTypes[defaultPropName];
if (prop && (allowRequiredDefaults || !prop.isRequired)) {
return;
@@ -315,13 +62,13 @@
context.report(
defaultProp.node,
'defaultProp "{{name}}" defined for isRequired propType.',
- {name: defaultProp.name}
+ {name: defaultPropName}
);
} else {
context.report(
defaultProp.node,
'defaultProp "{{name}}" has no corresponding propTypes declaration.',
- {name: defaultProp.name}
+ {name: defaultPropName}
);
}
});
@@ -332,248 +79,16 @@
// --------------------------------------------------------------------------
return {
- MemberExpression: function(node) {
- const isPropType = propsUtil.isPropTypesDeclaration(node);
- const isDefaultProp = propsUtil.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: propsUtil.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 = propsUtil.isPropTypesDeclaration(node);
- const isDefaultProp = propsUtil.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 = astUtil.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 = propsUtil.isPropTypesDeclaration(property);
- const isDefaultProp = propsUtil.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;
- }
-
+ Object.keys(list).filter(component => list[component].defaultProps).forEach(component => {
reportInvalidDefaultProps(
- list[component].propTypes,
+ list[component].declaredPropTypes,
list[component].defaultProps || {}
);
- }
+ });
}
};
})

lib/rules/destructuring-assignment.js

@@ -22,12 +22,20 @@
'always',
'never'
]
+ }, {
+ type: 'object',
+ properties: {
+ ignoreClassFields: {
+ type: 'boolean'
+ }
+ },
+ additionalProperties: false
}]
},
create: Components.detect((context, components, utils) => {
const configuration = context.options[0] || DEFAULT_OPTION;
-
+ const ignoreClassFields = context.options[1] && context.options[1].ignoreClassFields === true || false;
/**
* Checks if a prop is being assigned a value props.bar = 'bar'
@@ -74,14 +82,29 @@
}
}
+ function isInClassProperty(node) {
+ let curNode = node.parent;
+ while (curNode) {
+ if (curNode.type === 'ClassProperty') {
+ return true;
+ }
+ curNode = curNode.parent;
+ }
+ return false;
+ }
+
function handleClassUsage(node) {
// this.props.Aprop || this.context.aProp || this.state.aState
const isPropUsed = (
node.object.type === 'MemberExpression' && node.object.object.type === 'ThisExpression' &&
- (node.object.property.name === 'props' || node.object.property.name === 'context' || node.object.property.name === 'state')
+ (node.object.property.name === 'props' || node.object.property.name === 'context' || node.object.property.name === 'state') &&
+ !isAssignmentToProp(node)
);
- if (isPropUsed && configuration === 'always') {
+ if (
+ isPropUsed && configuration === 'always' &&
+ !(ignoreClassFields && isInClassProperty(node))
+ ) {
context.report({
node: node,
message: `Must use destructuring ${node.object.property.name} assignment`
@@ -127,7 +150,10 @@
});
}
- if (classComponent && destructuringClass && configuration === 'never') {
+ if (
+ classComponent && destructuringClass && configuration === 'never' &&
+ !(ignoreClassFields && node.parent.type === 'ClassProperty')
+ ) {
context.report({
node: node,
message: `Must never use destructuring ${node.init.property.name} assignment`

lib/rules/display-name.js

@@ -4,8 +4,8 @@
*/
'use strict';
-const has = require('has');
const Components = require('../util/Components');
+const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
@@ -116,7 +116,7 @@
);
const namedFunctionExpression = (
- (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') &&
+ astUtil.isFunctionLikeExpression(node) &&
node.parent &&
(node.parent.type === 'VariableDeclarator' || node.parent.method === true) &&
(!node.parent.parent || !utils.isES5Component(node.parent.parent))
@@ -215,12 +215,9 @@
'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;
- }
+ Object.keys(list).filter(component => !list[component].hasDisplayName).forEach(component => {
reportMissingDisplayName(list[component]);
- }
+ });
}
};
})

lib/rules/forbid-foreign-prop-types.js

@@ -52,6 +52,18 @@
return null;
}
+ function findParentClassProperty(node) {
+ let parent = node.parent;
+
+ while (parent && parent.type !== 'Program') {
+ if (parent.type === 'ClassProperty') {
+ return parent;
+ }
+ parent = parent.parent;
+ }
+ return null;
+ }
+
function isAllowedAssignment(node) {
if (!allowInPropTypes) {
return false;
@@ -67,6 +79,16 @@
) {
return true;
}
+
+ const classProperty = findParentClassProperty(node);
+
+ if (
+ classProperty &&
+ classProperty.key &&
+ classProperty.key.name === 'propTypes'
+ ) {
+ return true;
+ }
return false;
}
@@ -81,7 +103,7 @@
!isLeftSideOfAssignment(node) &&
!isAllowedAssignment(node)
) || (
- node.property.type === 'Literal' &&
+ (node.property.type === 'Literal' || node.property.type === 'JSXText') &&
node.property.value === 'propTypes' &&
!isLeftSideOfAssignment(node) &&
!isAllowedAssignment(node)

lib/rules/forbid-prop-types.js

@@ -7,6 +7,7 @@
const propsUtil = require('../util/props');
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
+const propWrapperUtil = require('../util/propWrapper');
// ------------------------------------------------------------------------------
// Constants
@@ -48,7 +49,6 @@
},
create: function(context) {
- const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
const configuration = context.options[0] || {};
const checkContextTypes = configuration.checkContextTypes || false;
const checkChildContextTypes = configuration.checkChildContextTypes || false;
@@ -125,7 +125,7 @@
break;
case 'CallExpression':
const innerNode = node.arguments && node.arguments[0];
- if (propWrapperFunctions.has(node.callee.name) && innerNode) {
+ if (propWrapperUtil.isPropWrapperFunction(context, context.getSource(node.callee)) && innerNode) {
checkNode(innerNode);
}
break;

lib/rules/jsx-child-element-spacing.js

@@ -56,6 +56,9 @@
]
},
create: function (context) {
+ const TEXT_FOLLOWING_ELEMENT_PATTERN = /^\s*\n\s*\S/;
+ const TEXT_PRECEDING_ELEMENT_PATTERN = /\S\s*\n\s*$/;
+
const elementName = node => (
node.openingElement &&
node.openingElement.name &&
@@ -68,18 +71,14 @@
INLINE_ELEMENTS.has(elementName(node))
);
- const TEXT_FOLLOWING_ELEMENT_PATTERN = /^\s*\n\s*\S/;
- const TEXT_PRECEDING_ELEMENT_PATTERN = /\S\s*\n\s*$/;
-
- return {
- JSXElement: function(node) {
+ const handleJSX = node => {
let lastChild = null;
let child = null;
(node.children.concat([null])).forEach(nextChild => {
if (
(lastChild || nextChild) &&
(!lastChild || isInlineElement(lastChild)) &&
- (child && child.type === 'Literal') &&
+ (child && (child.type === 'Literal' || child.type === 'JSXText')) &&
(!nextChild || isInlineElement(nextChild)) &&
true
) {
@@ -100,7 +99,11 @@
lastChild = child;
child = nextChild;
});
- }
+ };
+
+ return {
+ JSXElement: handleJSX,
+ JSXFragment: handleJSX
};
}
};

lib/rules/jsx-closing-tag-location.js

@@ -22,13 +22,12 @@
},
create: function(context) {
- return {
- JSXClosingElement: function(node) {
+ function handleClosingElement(node) {
if (!node.parent) {
return;
}
- const opening = node.parent.openingElement;
+ const opening = node.parent.openingElement || node.parent.openingFragment;
if (opening.loc.start.line === node.loc.start.line) {
return;
}
@@ -61,6 +60,10 @@
}
});
}
+
+ return {
+ JSXClosingElement: handleClosingElement,
+ JSXClosingFragment: handleClosingElement
};
}
};

lib/rules/jsx-curly-brace-presence.js

@@ -6,6 +6,7 @@
'use strict';
const docsUrl = require('../util/docsUrl');
+const jsxUtil = require('../util/jsx');
// ------------------------------------------------------------------------------
// Constants
@@ -168,13 +169,12 @@
function lintUnnecessaryCurly(JSXExpressionNode) {
const expression = JSXExpressionNode.expression;
const expressionType = expression.type;
- const parentType = JSXExpressionNode.parent.type;
if (
- expressionType === 'Literal' &&
+ (expressionType === 'Literal' || expressionType === 'JSXText') &&
typeof expression.value === 'string' &&
!needToEscapeCharacterForJSX(expression.raw) && (
- parentType === 'JSXElement' ||
+ jsxUtil.isJSX(JSXExpressionNode.parent) ||
!containsQuoteCharacters(expression.value)
)
) {
@@ -183,7 +183,7 @@
expressionType === 'TemplateLiteral' &&
expression.expressions.length === 0 &&
!needToEscapeCharacterForJSX(expression.quasis[0].value.raw) && (
- parentType === 'JSXElement' ||
+ jsxUtil.isJSX(JSXExpressionNode.parent) ||
!containsQuoteCharacters(expression.quasis[0].value.cooked)
)
) {
@@ -191,24 +191,22 @@
}
}
- function areRuleConditionsSatisfied(parentType, config, ruleCondition) {
+ function areRuleConditionsSatisfied(parent, config, ruleCondition) {
return (
- parentType === 'JSXAttribute' &&
+ parent.type === 'JSXAttribute' &&
typeof config.props === 'string' &&
config.props === ruleCondition
) || (
- parentType === 'JSXElement' &&
+ jsxUtil.isJSX(parent) &&
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) {
+ if (jsxUtil.isJSX(parent) && parent.children.length !== 1) {
return false;
}
@@ -220,7 +218,7 @@
return false;
}
- return areRuleConditionsSatisfied(parentType, config, OPTION_NEVER);
+ return areRuleConditionsSatisfied(parent, config, OPTION_NEVER);
}
function shouldCheckForMissingCurly(parent, config) {
@@ -232,7 +230,7 @@
return false;
}
- return areRuleConditionsSatisfied(parent.type, config, OPTION_ALWAYS);
+ return areRuleConditionsSatisfied(parent, config, OPTION_ALWAYS);
}
// --------------------------------------------------------------------------
@@ -246,7 +244,7 @@
}
},
- Literal: node => {
+ 'Literal, JSXText': node => {
if (shouldCheckForMissingCurly(node.parent, userConfig)) {
reportMissingCurly(node);
}

lib/rules/jsx-curly-spacing.js

@@ -228,7 +228,16 @@
message: `There should be no space after '${token.value}'`,
fix: function(fixer) {
const nextToken = sourceCode.getTokenAfter(token);
- const nextComment = sourceCode.getCommentsAfter(token);
+ let nextComment;
+
+ // ESLint >=4.x
+ if (sourceCode.getCommentsAfter) {
+ nextComment = sourceCode.getCommentsAfter(token);
+ // ESLint 3.x
+ } else {
+ const potentialComment = sourceCode.getTokenAfter(token, {includeComments: true});
+ nextComment = nextToken === potentialComment ? [] : [potentialComment];
+ }
// Take comments into consideration to narrow the fix range to what is actually affected. (See #1414)
if (nextComment.length > 0) {
@@ -253,7 +262,16 @@
message: `There should be no space before '${token.value}'`,
fix: function(fixer) {
const previousToken = sourceCode.getTokenBefore(token);
- const previousComment = sourceCode.getCommentsBefore(token);
+ let previousComment;
+
+ // ESLint >=4.x
+ if (sourceCode.getCommentsBefore) {
+ previousComment = sourceCode.getCommentsBefore(token);
+ // ESLint 3.x
+ } else {
+ const potentialComment = sourceCode.getTokenBefore(token, {includeComments: true});
+ previousComment = previousToken === potentialComment ? [] : [potentialComment];
+ }
// Take comments into consideration to narrow the fix range to what is actually affected. (See #1414)
if (previousComment.length > 0) {
@@ -313,6 +331,7 @@
break;
case 'JSXElement':
+ case 'JSXFragment':
config = childrenConfig;
break;

lib/rules/jsx-filename-extension.js

@@ -43,19 +43,14 @@
},
create: function(context) {
- function getExtensionsConfig() {
- return context.options[0] && context.options[0].extensions || DEFAULTS.extensions;
- }
-
let invalidExtension;
let invalidNode;
- // --------------------------------------------------------------------------
- // Public
- // --------------------------------------------------------------------------
+ function getExtensionsConfig() {
+ return context.options[0] && context.options[0].extensions || DEFAULTS.extensions;
+ }
- return {
- JSXElement: function(node) {
+ function handleJSX(node) {
const filename = context.getFilename();
if (filename === '<text>') {
return;
@@ -74,7 +69,15 @@
invalidNode = node;
invalidExtension = path.extname(filename);
- },
+ }
+
+ // --------------------------------------------------------------------------
+ // Public
+ // --------------------------------------------------------------------------
+
+ return {
+ JSXElement: handleJSX,
+ JSXFragment: handleJSX,
'Program:exit': function() {
if (!invalidNode) {

lib/rules/jsx-first-prop-new-line.js

@@ -45,7 +45,7 @@
node: decl,
message: 'Property should be placed on a new line',
fix: function(fixer) {
- return fixer.replaceTextRange([node.name.end, decl.range[0]], '\n');
+ return fixer.replaceTextRange([node.name.range[1], decl.range[0]], '\n');
}
});
}
@@ -58,7 +58,7 @@
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.range[0]], ' ');
+ return fixer.replaceTextRange([node.name.range[1], firstNode.range[0]], ' ');
}
});
return;

lib/rules/jsx-fragments.js

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

lib/rules/jsx-indent.js

@@ -50,6 +50,14 @@
}, {
type: 'integer'
}]
+ }, {
+ type: 'object',
+ properties: {
+ checkAttributes: {
+ type: 'boolean'
+ }
+ },
+ additionalProperties: false
}]
},
@@ -73,6 +81,8 @@
}
const indentChar = indentType === 'space' ? ' ' : '\t';
+ const options = context.options[1] || {};
+ const checkAttributes = options.checkAttributes || false;
/**
* Responsible for fixing the indentation issue fix
@@ -205,8 +215,7 @@
}
}
- return {
- JSXOpeningElement: function(node) {
+ function handleOpeningElement(node) {
let prevToken = sourceCode.getTokenBefore(node);
if (!prevToken) {
return;
@@ -214,12 +223,12 @@
// Use the parent in a list or an array
if (prevToken.type === 'JSXText' || prevToken.type === 'Punctuator' && prevToken.value === ',') {
prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
- prevToken = prevToken.type === 'Literal' ? prevToken.parent : prevToken;
+ prevToken = prevToken.type === 'Literal' || prevToken.type === 'JSXText' ? prevToken.parent : prevToken;
// Use the first non-punctuator token in a conditional expression
} else if (prevToken.type === 'Punctuator' && prevToken.value === ':') {
do {
prevToken = sourceCode.getTokenBefore(prevToken);
- } while (prevToken.type === 'Punctuator');
+ } while (prevToken.type === 'Punctuator' && prevToken.value !== '/');
prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
while (prevToken.parent && prevToken.parent.type !== 'ConditionalExpression') {
prevToken = prevToken.parent;
@@ -234,14 +242,33 @@
isAlternateInConditionalExp(node)
) ? 0 : indentSize;
checkNodesIndent(node, parentElementIndent + indent);
- },
- JSXClosingElement: function(node) {
+ }
+
+ function handleClosingElement(node) {
if (!node.parent) {
return;
}
- const peerElementIndent = getNodeIndent(node.parent.openingElement);
+ const peerElementIndent = getNodeIndent(node.parent.openingElement || node.parent.openingFragment);
checkNodesIndent(node, peerElementIndent);
- },
+ }
+
+ function handleAttribute(node) {
+ if (!checkAttributes || (!node.value || node.value.type !== 'JSXExpressionContainer')) {
+ return;
+ }
+ const nameIndent = getNodeIndent(node.name);
+ const lastToken = sourceCode.getLastToken(node.value);
+ const firstInLine = astUtil.getFirstNodeInLine(context, lastToken);
+ const indent = node.name.loc.start.line === firstInLine.loc.start.line ? 0 : nameIndent;
+ checkNodesIndent(firstInLine, indent);
+ }
+
+ return {
+ JSXOpeningElement: handleOpeningElement,
+ JSXOpeningFragment: handleOpeningElement,
+ JSXClosingElement: handleClosingElement,
+ JSXClosingFragment: handleClosingElement,
+ JSXAttribute: handleAttribute,
JSXExpressionContainer: function(node) {
if (!node.parent) {
return;

lib/rules/jsx-indent-props.js

@@ -47,7 +47,7 @@
schema: [{
oneOf: [{
- enum: ['tab']
+ enum: ['tab', 'first']
}, {
type: 'integer'
}]
@@ -64,7 +64,10 @@
const sourceCode = context.getSourceCode();
if (context.options.length) {
- if (context.options[0] === 'tab') {
+ if (context.options[0] === 'first') {
+ indentSize = 'first';
+ indentType = 'space';
+ } else if (context.options[0] === 'tab') {
indentSize = 1;
indentType = 'tab';
} else if (typeof context.options[0] === 'number') {
@@ -78,9 +81,8 @@
* @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) {
+ function report(node, needed, gotten) {
const msgContext = {
needed: needed,
type: indentType,
@@ -88,14 +90,6 @@
gotten: gotten
};
- if (loc) {
- context.report({
- node: node,
- loc: loc,
- message: MESSAGE,
- data: msgContext
- });
- } else {
context.report({
node: node,
message: MESSAGE,
@@ -106,34 +100,22 @@
}
});
}
- }
/**
* 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;
-
+ function getNodeIndent(node) {
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}]+`);
+ regExp = /^[ ]+/;
} else {
- regExp = new RegExp(`^[\t${skip}]+`);
+ regExp = /^[\t]+/;
}
const indent = regExp.exec(src);
@@ -146,9 +128,9 @@
* @param {Number} indent needed indent
* @param {Boolean} excludeCommas skip comma on start of line
*/
- function checkNodesIndent(nodes, indent, excludeCommas) {
+ function checkNodesIndent(nodes, indent) {
nodes.forEach(node => {
- const nodeIndent = getNodeIndent(node, false, excludeCommas);
+ const nodeIndent = getNodeIndent(node);
if (
node.type !== 'ArrayExpression' && node.type !== 'ObjectExpression' &&
nodeIndent !== indent && astUtil.isNodeFirstInLine(context, node)
@@ -160,8 +142,18 @@
return {
JSXOpeningElement: function(node) {
+ if (!node.attributes.length) {
+ return;
+ }
+ let propIndent;
+ if (indentSize === 'first') {
+ const firstPropNode = node.attributes[0];
+ propIndent = firstPropNode.loc.start.column;
+ } else {
const elementIndent = getNodeIndent(node);
- checkNodesIndent(node.attributes, elementIndent + indentSize);
+ propIndent = elementIndent + indentSize;
+ }
+ checkNodesIndent(node.attributes, propIndent);
}
};
}

lib/rules/jsx-max-depth.js

@@ -6,6 +6,8 @@
const has = require('has');
const variableUtil = require('../util/variable');
+const jsxUtil = require('../util/jsx');
+const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
@@ -15,7 +17,8 @@
docs: {
description: 'Validate JSX maximum depth',
category: 'Stylistic Issues',
- recommended: false
+ recommended: false,
+ url: docsUrl('jsx-max-depth')
},
schema: [
{
@@ -37,16 +40,12 @@
const option = context.options[0] || {};
const maxDepth = has(option, 'max') ? option.max : DEFAULT_DEPTH;
- function isJSXElement(node) {
- return node.type === 'JSXElement';
- }
-
function isExpression(node) {
return node.type === 'JSXExpressionContainer';
}
function hasJSX(node) {
- return isJSXElement(node) || isExpression(node) && isJSXElement(node.expression);
+ return jsxUtil.isJSX(node) || isExpression(node) && jsxUtil.isJSX(node.expression);
}
function isLeaf(node) {
@@ -58,9 +57,9 @@
function getDepth(node) {
let count = 0;
- while (isJSXElement(node.parent) || isExpression(node.parent)) {
+ while (jsxUtil.isJSX(node.parent) || isExpression(node.parent)) {
node = node.parent;
- if (isJSXElement(node)) {
+ if (jsxUtil.isJSX(node)) {
count++;
}
}
@@ -80,7 +79,7 @@
});
}
- function findJSXElement(variables, name) {
+ function findJSXElementOrFragment(variables, name) {
function find(refs) {
let i = refs.length;
@@ -88,10 +87,10 @@
if (has(refs[i], 'writeExpr')) {
const writeExpr = refs[i].writeExpr;
- return isJSXElement(writeExpr)
+ return jsxUtil.isJSX(writeExpr)
&& writeExpr
- || writeExpr.type === 'Identifier'
- && findJSXElement(variables, writeExpr.name);
+ || (writeExpr && writeExpr.type === 'Identifier')
+ && findJSXElementOrFragment(variables, writeExpr.name);
}
}
@@ -103,12 +102,12 @@
}
function checkDescendant(baseDepth, children) {
- children.forEach(node => {
+ baseDepth++;
+ (children || []).forEach(node => {
if (!hasJSX(node)) {
return;
}
- baseDepth++;
if (baseDepth > maxDepth) {
report(node, baseDepth);
} else if (!isLeaf(node)) {
@@ -117,8 +116,7 @@
});
}
- return {
- JSXElement: function(node) {
+ function handleJSX(node) {
if (!isLeaf(node)) {
return;
}
@@ -127,14 +125,19 @@
if (depth > maxDepth) {
report(node, depth);
}
- },
+ }
+
+ return {
+ JSXElement: handleJSX,
+ JSXFragment: handleJSX,
+
JSXExpressionContainer: function(node) {
if (node.expression.type !== 'Identifier') {
return;
}
const variables = variableUtil.variablesInScope(context);
- const element = findJSXElement(variables, node.expression.name);
+ const element = findJSXElementOrFragment(variables, node.expression.name);
if (element) {
const baseDepth = getDepth(node);

lib/rules/jsx-no-bind.js

@@ -9,6 +9,7 @@
const propName = require('jsx-ast-utils/propName');
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
+const jsxUtil = require('../util/jsx');
// -----------------------------------------------------------------------------
// Rule Definition
@@ -48,6 +49,10 @@
ignoreRefs: {
default: false,
type: 'boolean'
+ },
+ ignoreDOMComponents: {
+ default: false,
+ type: 'boolean'
}
},
additionalProperties: false
@@ -165,6 +170,10 @@
if (isRef || !node.value || !node.value.expression) {
return;
}
+ const isDOMComponent = jsxUtil.isDOMComponent(node.parent);
+ if (configuration.ignoreDOMComponents && isDOMComponent) {
+ return;
+ }
const valueNode = node.value.expression;
const valueNodeType = valueNode.type;
const nodeViolationType = getNodeViolationType(valueNode);

lib/rules/jsx-no-literals.js

@@ -33,6 +33,7 @@
create: function(context) {
const isNoStrings = context.options[0] ? context.options[0].noStrings : false;
+ const sourceCode = context.getSourceCode();
const message = isNoStrings ?
'Strings not allowed in JSX files' :
@@ -41,7 +42,7 @@
function reportLiteralNode(node) {
context.report({
node: node,
- message: message
+ message: `${message}: “${sourceCode.getText(node).trim()}”`
});
}
@@ -75,6 +76,12 @@
if (getValidation(node)) {
reportLiteralNode(node);
}
+ },
+
+ JSXText: function(node) {
+ if (getValidation(node)) {
+ reportLiteralNode(node);
+ }
},
TemplateLiteral: function(node) {

lib/rules/jsx-no-target-blank.js

@@ -11,7 +11,9 @@
// ------------------------------------------------------------------------------
function isTargetBlank(attr) {
- return attr.name.name === 'target' &&
+ return attr.name &&
+ attr.name.name === 'target' &&
+ attr.value &&
attr.value.type === 'Literal' &&
attr.value.value.toLowerCase() === '_blank';
}
@@ -23,6 +25,12 @@
/^(?:\w+:|\/\/)/.test(attr.value.value));
}
+function hasDynamicLink(element) {
+ return element.attributes.some(attr => attr.name &&
+ attr.name.name === 'href' &&
+ attr.value.type === 'JSXExpressionContainer');
+}
+
function hasSecureRel(element) {
return element.attributes.find(attr => {
if (attr.type === 'JSXAttribute' && attr.name.name === 'rel') {
@@ -41,21 +49,28 @@
recommended: true,
url: docsUrl('jsx-no-target-blank')
},
- schema: []
+ schema: [{
+ type: 'object',
+ properties: {
+ enforceDynamicLinks: {
+ enum: ['always', 'never']
+ }
+ },
+ additionalProperties: false
+ }]
},
create: function(context) {
+ const configuration = context.options[0] || {};
+ const enforceDynamicLinks = configuration.enforceDynamicLinks || 'always';
+
return {
JSXAttribute: function(node) {
- if (node.parent.name.name !== 'a') {
+ if (node.parent.name.name !== 'a' || !isTargetBlank(node) || hasSecureRel(node.parent)) {
return;
}
- if (
- isTargetBlank(node) &&
- hasExternalLink(node.parent) &&
- !hasSecureRel(node.parent)
- ) {
+ if (hasExternalLink(node.parent) || (enforceDynamicLinks === 'always' && hasDynamicLink(node.parent))) {
context.report(node, 'Using target="_blank" without rel="noopener noreferrer" ' +
'is a security risk: see https://mathiasbynens.github.io/rel-noopener');
}

lib/rules/jsx-no-undef.js

@@ -6,16 +6,7 @@
'use strict';
const docsUrl = require('../util/docsUrl');
-
-/**
- * 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);
-}
+const jsxUtil = require('../util/jsx');
// ------------------------------------------------------------------------------
// Rule Definition
@@ -95,10 +86,10 @@
JSXOpeningElement: function(node) {
switch (node.name.type) {
case 'JSXIdentifier':
- node = node.name;
- if (isTagName(node.name)) {
+ if (jsxUtil.isDOMComponent(node)) {
return;
}
+ node = node.name;
break;
case 'JSXMemberExpression':
node = node.name;

lib/rules/jsx-one-expression-per-line.js

@@ -11,6 +11,10 @@
// Rule Definition
// ------------------------------------------------------------------------------
+const optionDefaults = {
+ allow: 'none'
+};
+
module.exports = {
meta: {
docs: {
@@ -20,10 +24,22 @@
url: docsUrl('jsx-one-expression-per-line')
},
fixable: 'whitespace',
- schema: []
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ allow: {
+ enum: ['none', 'literal', 'single-child']
+ }
+ },
+ default: optionDefaults,
+ additionalProperties: false
+ }
+ ]
},
create: function (context) {
+ const options = Object.assign({}, optionDefaults, context.options[0]);
const sourceCode = context.getSourceCode();
function nodeKey (node) {
@@ -34,18 +50,37 @@
return n.openingElement ? n.openingElement.name.name : sourceCode.getText(n).replace(/\n/g, '');
}
- return {
- JSXElement: function (node) {
+ function handleJSX(node) {
const children = node.children;
if (!children || !children.length) {
return;
}
- const openingElement = node.openingElement;
- const closingElement = node.closingElement;
+ const openingElement = node.openingElement || node.openingFragment;
+ const closingElement = node.closingElement || node.closingFragment;
+ const openingElementStartLine = openingElement.loc.start.line;
const openingElementEndLine = openingElement.loc.end.line;
const closingElementStartLine = closingElement.loc.start.line;
+ const closingElementEndLine = closingElement.loc.end.line;
+
+ if (children.length === 1) {
+ const child = children[0];
+ if (
+ openingElementStartLine === openingElementEndLine &&
+ openingElementEndLine === closingElementStartLine &&
+ closingElementStartLine === closingElementEndLine &&
+ closingElementEndLine === child.loc.start.line &&
+ child.loc.start.line === child.loc.end.line
+ ) {
+ if (
+ options.allow === 'single-child' ||
+ options.allow === 'literal' && (child.type === 'Literal' || child.type === 'JSXText')
+ ) {
+ return;
+ }
+ }
+ }
const childrenGroupedByLine = {};
const fixDetailsByNode = {};
@@ -54,7 +89,7 @@
let countNewLinesBeforeContent = 0;
let countNewLinesAfterContent = 0;
- if (child.type === 'Literal') {
+ if (child.type === 'Literal' || child.type === 'JSXText') {
if (/^\s*$/.test(child.raw)) {
return;
}
@@ -110,14 +145,14 @@
}
function spaceBetweenPrev () {
- return (prevChild.type === 'Literal' && / $/.test(prevChild.raw)) ||
- (child.type === 'Literal' && /^ /.test(child.raw)) ||
+ return ((prevChild.type === 'Literal' || prevChild.type === 'JSXText') && / $/.test(prevChild.raw)) ||
+ ((child.type === 'Literal' || child.type === 'JSXText') && /^ /.test(child.raw)) ||
sourceCode.isSpaceBetweenTokens(prevChild, child);
}
function spaceBetweenNext () {
- return (nextChild.type === 'Literal' && /^ /.test(nextChild.raw)) ||
- (child.type === 'Literal' && / $/.test(child.raw)) ||
+ return ((nextChild.type === 'Literal' || nextChild.type === 'JSXText') && /^ /.test(nextChild.raw)) ||
+ ((child.type === 'Literal' || child.type === 'JSXText') && / $/.test(child.raw)) ||
sourceCode.isSpaceBetweenTokens(child, nextChild);
}
@@ -179,6 +214,10 @@
});
});
}
+
+ return {
+ JSXElement: handleJSX,
+ JSXFragment: handleJSX
};
}
};

lib/rules/jsx-pascal-case.js

@@ -7,13 +7,13 @@
const elementType = require('jsx-ast-utils/elementType');
const docsUrl = require('../util/docsUrl');
+const jsxUtil = require('../util/jsx');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const PASCAL_CASE_REGEX = /^([A-Z0-9]|[A-Z0-9]+[a-z0-9]+(?:[A-Z0-9]+[a-z0-9]*)*)$/;
-const COMPAT_TAG_REGEX = /^[a-z]|\-/;
const ALL_CAPS_TAG_REGEX = /^[A-Z0-9]+$/;
// ------------------------------------------------------------------------------
@@ -60,7 +60,7 @@
}
const isPascalCase = PASCAL_CASE_REGEX.test(name);
- const isCompatTag = COMPAT_TAG_REGEX.test(name);
+ const isCompatTag = jsxUtil.isDOMComponent(node);
const isAllowedAllCaps = allowAllCaps && ALL_CAPS_TAG_REGEX.test(name);
const isIgnored = ignore.indexOf(name) !== -1;

lib/rules/jsx-props-no-multi-spaces.js

@@ -0,0 +1,67 @@
+/**
+ * @fileoverview Disallow multiple spaces between inline JSX props
+ * @author Adrian Moennich
+ */
+
+'use strict';
+
+const docsUrl = require('../util/docsUrl');
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ docs: {
+ description: 'Disallow multiple spaces between inline JSX props',
+ category: 'Stylistic Issues',
+ recommended: false,
+ url: docsUrl('jsx-props-no-multi-spaces')
+ },
+ fixable: 'code',
+ schema: []
+ },
+
+ create: function (context) {
+ const sourceCode = context.getSourceCode();
+
+ function getPropName(propNode) {
+ switch (propNode.type) {
+ case 'JSXSpreadAttribute':
+ return sourceCode.getText(propNode.argument);
+ case 'JSXIdentifier':
+ return propNode.name;
+ case 'JSXMemberExpression':
+ return `${getPropName(propNode.object)}.${propNode.property.name}`;
+ default:
+ return propNode.name.name;
+ }
+ }
+
+ function checkSpacing(prev, node) {
+ if (prev.loc.end.line !== node.loc.end.line) {
+ return;
+ }
+ const between = sourceCode.text.slice(prev.range[1], node.range[0]);
+ if (between !== ' ') {
+ context.report({
+ node: node,
+ message: `Expected only one space between "${getPropName(prev)}" and "${getPropName(node)}"`,
+ fix: function(fixer) {
+ return fixer.replaceTextRange([prev.range[1], node.range[0]], ' ');
+ }
+ });
+ }
+ }
+
+ return {
+ JSXOpeningElement: function (node) {
+ node.attributes.reduce((prev, prop) => {
+ checkSpacing(prev, prop);
+ return prop;
+ }, node.name);
+ }
+ };
+ }
+};

lib/rules/jsx-sort-default-props.js

@@ -5,6 +5,8 @@
'use strict';
const variableUtil = require('../util/variable');
+const docsUrl = require('../util/docsUrl');
+const propWrapperUtil = require('../util/propWrapper');
// ------------------------------------------------------------------------------
// Rule Definition
@@ -15,7 +17,8 @@
docs: {
description: 'Enforce default props alphabetical sorting',
category: 'Stylistic Issues',
- recommended: false
+ recommended: false,
+ url: docsUrl('jsx-sort-default-props')
},
schema: [{
@@ -33,7 +36,6 @@
const sourceCode = context.getSourceCode();
const configuration = context.options[0] || {};
const ignoreCase = configuration.ignoreCase || false;
- const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
/**
* Get properties name
@@ -132,7 +134,7 @@
break;
case 'CallExpression':
const innerNode = node.arguments && node.arguments[0];
- if (propWrapperFunctions.has(node.callee.name) && innerNode) {
+ if (propWrapperUtil.isPropWrapperFunction(context, node.callee.name) && innerNode) {
checkNode(innerNode);
}
break;

lib/rules/jsx-sort-props.js

@@ -4,9 +4,9 @@
*/
'use strict';
-const elementType = require('jsx-ast-utils/elementType');
const propName = require('jsx-ast-utils/propName');
const docsUrl = require('../util/docsUrl');
+const jsxUtil = require('../util/jsx');
// ------------------------------------------------------------------------------
// Rule Definition
@@ -16,20 +16,6 @@
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',
@@ -107,19 +93,30 @@
return function(fixer) {
const fixers = [];
+ let source = sourceCode.getText();
// Replace each unsorted attribute with the sorted one.
sortableAttributeGroups.forEach((sortableGroup, ii) => {
sortableGroup.forEach((attr, jj) => {
const sortedAttr = sortedAttributeGroups[ii][jj];
const sortedAttrText = sourceCode.getText(sortedAttr);
- fixers.push(
- fixer.replaceTextRange([attr.range[0], attr.range[1]], sortedAttrText)
- );
+ fixers.push({
+ range: [attr.range[0], attr.range[1]],
+ text: sortedAttrText
+ });
});
});
- return fixers;
+ fixers.sort((a, b) => b.range[0] - a.range[0]);
+
+ const rangeStart = fixers[fixers.length - 1].range[0];
+ const rangeEnd = fixers[0].range[1];
+
+ fixers.forEach(fix => {
+ source = `${source.substr(0, fix.range[0])}${fix.text}${source.substr(fix.range[1])}`;
+ });
+
+ return fixer.replaceTextRange([rangeStart, rangeEnd], source.substr(rangeStart, rangeEnd - rangeStart));
};
};
@@ -218,7 +215,7 @@
return {
JSXOpeningElement: function(node) {
// `dangerouslySetInnerHTML` is only "reserved" on DOM components
- if (reservedFirst && !isDOMComponent(node)) {
+ if (reservedFirst && !jsxUtil.isDOMComponent(node)) {
reservedList = reservedList.filter(prop => prop !== 'dangerouslySetInnerHTML');
}
@@ -248,15 +245,8 @@
const previousIsReserved = isReservedPropName(previousPropName, reservedList);
const currentIsReserved = isReservedPropName(currentPropName, reservedList);
- if ((previousIsReserved && currentIsReserved) || (!previousIsReserved && !currentIsReserved)) {
- if (!noSortAlphabetically && currentPropName < previousPropName) {
- context.report({
- node: decl,
- message: 'Props should be sorted alphabetically',
- fix: generateFixerFunction(node, context, reservedList)
- });
- return memo;
- }
+ if (previousIsReserved && !currentIsReserved) {
+ return decl;
}
if (!previousIsReserved && currentIsReserved) {
context.report({
@@ -264,8 +254,8 @@
message: 'Reserved props must be listed before all other props',
fix: generateFixerFunction(node, context, reservedList)
});
+ return memo;
}
- return decl;
}
if (callbacksLast) {
@@ -313,7 +303,7 @@
context.report({
node: decl,
message: 'Props should be sorted alphabetically',
- fix: generateFixerFunction(node, context)
+ fix: generateFixerFunction(node, context, reservedList)
});
return memo;
}

lib/rules/jsx-space-before-closing.js

@@ -7,6 +7,7 @@
const getTokenBeforeClosingBracket = require('../util/getTokenBeforeClosingBracket');
const docsUrl = require('../util/docsUrl');
+const log = require('../util/log');
let isWarnedForDeprecation = false;
@@ -75,15 +76,13 @@
},
Program: function() {
- if (isWarnedForDeprecation || /\=-(f|-format)=/.test(process.argv.join('='))) {
+ if (isWarnedForDeprecation) {
return;
}
- /* eslint-disable no-console */
- console.log('The react/jsx-space-before-closing rule is deprecated. ' +
+ 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;
}
};

lib/rules/jsx-uses-react.js

@@ -25,16 +25,16 @@
create: function(context) {
const pragma = pragmaUtil.getFromContext(context);
+ function handleOpeningElement() {
+ context.markVariableAsUsed(pragma);
+ }
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
-
- JSXOpeningElement: function() {
- context.markVariableAsUsed(pragma);
- }
-
+ JSXOpeningElement: handleOpeningElement,
+ JSXOpeningFragment: handleOpeningElement
};
}
};

lib/rules/jsx-wrap-multilines.js

@@ -6,6 +6,7 @@
const has = require('has');
const docsUrl = require('../util/docsUrl');
+const jsxUtil = require('../util/jsx');
// ------------------------------------------------------------------------------
// Constants
@@ -93,13 +94,32 @@
nextToken.value === ')' && nextToken.range[0] >= node.range[1];
}
- function needsNewLines(node) {
+ function needsOpeningNewLine(node) {
const previousToken = sourceCode.getTokenBefore(node);
+
+ if (!isParenthesised(node)) {
+ return false;
+ }
+
+ if (previousToken.loc.end.line === node.loc.start.line) {
+ return true;
+ }
+
+ return false;
+ }
+
+ function needsClosingNewLine(node) {
const nextToken = sourceCode.getTokenAfter(node);
- return isParenthesised(node) &&
- previousToken.loc.end.line === node.loc.start.line &&
- node.loc.end.line === nextToken.loc.end.line;
+ if (!isParenthesised(node)) {
+ return false;
+ }
+
+ if (node.loc.end.line === nextToken.loc.end.line) {
+ return true;
+ }
+
+ return false;
}
function isMultilines(node) {
@@ -122,7 +142,7 @@
}
function check(node, type) {
- if (!node || node.type !== 'JSXElement') {
+ if (!node || !jsxUtil.isJSX(node)) {
return;
}
@@ -142,15 +162,29 @@
node,
MISSING_PARENS,
fixer => fixer.replaceTextRange(
- [tokenBefore.range[0], tokenAfter.range[0]],
+ [tokenBefore.range[0], tokenAfter ? tokenAfter.range[0] : node.range[1]],
`${trimTokenBeforeNewline(node, tokenBefore)}(\n${sourceCode.getText(node)}\n)`
)
);
} else {
report(node, MISSING_PARENS, fixer => fixer.replaceText(node, `(\n${sourceCode.getText(node)}\n)`));
}
- } else if (needsNewLines(node)) {
- report(node, PARENS_NEW_LINES, fixer => fixer.replaceText(node, `\n${sourceCode.getText(node)}\n`));
+ } else {
+ const needsOpening = needsOpeningNewLine(node);
+ const needsClosing = needsClosingNewLine(node);
+ if (needsOpening || needsClosing) {
+ report(node, PARENS_NEW_LINES, fixer => {
+ const text = sourceCode.getText(node);
+ let fixed = text;
+ if (needsOpening) {
+ fixed = `\n${fixed}`;
+ }
+ if (needsClosing) {
+ fixed = `${fixed}\n`;
+ }
+ return fixer.replaceText(node, fixed);
+ });
+ }
}
}
}

lib/rules/no-array-index-key.js

@@ -5,7 +5,9 @@
'use strict';
const has = require('has');
+const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
+const pragma = require('../util/pragma');
// ------------------------------------------------------------------------------
// Rule Definition
@@ -46,6 +48,32 @@
&& indexParamNames.indexOf(node.name) !== -1;
}
+ function isUsingReactChildren(node) {
+ const callee = node.callee;
+ if (
+ !callee
+ || !callee.property
+ || !callee.object
+ ) {
+ return null;
+ }
+
+ const isReactChildMethod = ['map', 'forEach'].indexOf(callee.property.name) > -1;
+ if (!isReactChildMethod) {
+ return null;
+ }
+
+ const obj = callee.object;
+ if (obj && obj.name === 'Children') {
+ return true;
+ }
+ if (obj && obj.object && obj.object.name === pragma.getFromContext(context)) {
+ return true;
+ }
+
+ return false;
+ }
+
function getMapIndexParamName(node) {
const callee = node.callee;
if (callee.type !== 'MemberExpression') {
@@ -58,20 +86,19 @@
return null;
}
- const firstArg = node.arguments[0];
- if (!firstArg) {
+ const callbackArg = isUsingReactChildren(node)
+ ? node.arguments[1]
+ : node.arguments[0];
+
+ if (!callbackArg) {
return null;
}
- const isFunction = [
- 'ArrowFunctionExpression',
- 'FunctionExpression'
- ].indexOf(firstArg.type) !== -1;
- if (!isFunction) {
+ if (!astUtil.isFunctionLikeExpression(callbackArg)) {
return null;
}
- const params = firstArg.params;
+ const params = callbackArg.params;
const indexParamPosition = iteratorFunctionsToIndexParamPosition[callee.property.name];
if (params.length < indexParamPosition + 1) {

lib/rules/no-danger.js

@@ -5,6 +5,7 @@
'use strict';
const docsUrl = require('../util/docsUrl');
+const jsxUtil = require('../util/jsx');
// ------------------------------------------------------------------------------
// Constants
@@ -26,16 +27,6 @@
// ------------------------------------------------------------------------------
/**
- * 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.
@@ -63,7 +54,7 @@
return {
JSXAttribute: function(node) {
- if (isTagName(node.parent.name.name) && isDangerous(node.name.name)) {
+ if (jsxUtil.isDOMComponent(node.parent) && isDangerous(node.name.name)) {
context.report({
node: node,
message: DANGEROUS_MESSAGE,

lib/rules/no-danger-with-children.js

@@ -36,7 +36,7 @@
return node.properties.find(prop => {
if (prop.type === 'Property') {
return prop.key.name === propName;
- } else if (prop.type === 'ExperimentalSpreadProperty') {
+ } else if (prop.type === 'ExperimentalSpreadProperty' || prop.type === 'SpreadElement') {
const variable = findSpreadVariable(prop.argument.name);
if (variable && variable.defs.length && variable.defs[0].node.init) {
if (seenProps.indexOf(prop.argument.name) > -1) {
@@ -74,7 +74,7 @@
* @returns {Boolean} True if node is a line break, false if not
*/
function isLineBreak(node) {
- const isLiteral = node.type === 'Literal';
+ const isLiteral = node.type === 'Literal' || node.type === 'JSXText';
const isMultiline = node.loc.start.line !== node.loc.end.line;
const isWhiteSpaces = /^\s*$/.test(node.value);

lib/rules/no-deprecated.js

@@ -76,21 +76,27 @@
deprecated[`${pragma}.PropTypes`] = ['15.5.0', 'the npm module prop-types'];
// 15.6.0
deprecated[`${pragma}.DOM`] = ['15.6.0', 'the npm module react-dom-factories'];
- // 16.3.0
+ // 16.999.0
+ // For now the following life-cycle methods are just legacy, not deprecated:
+ // `componentWillMount`, `componentWillReceiveProps`, `componentWillUpdate`
+ // https://github.com/yannickcr/eslint-plugin-react/pull/1750#issuecomment-425975934
deprecated.componentWillMount = [
- '16.3.0',
+ '16.999.0',
'UNSAFE_componentWillMount',
- 'https://reactjs.org/docs/react-component.html#unsafe_componentwillmount'
+ 'https://reactjs.org/docs/react-component.html#unsafe_componentwillmount. ' +
+ 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.'
];
deprecated.componentWillReceiveProps = [
- '16.3.0',
+ '16.999.0',
'UNSAFE_componentWillReceiveProps',
- 'https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops'
+ 'https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops. ' +
+ 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.'
];
deprecated.componentWillUpdate = [
- '16.3.0',
+ '16.999.0',
'UNSAFE_componentWillUpdate',
- 'https://reactjs.org/docs/react-component.html#unsafe_componentwillupdate'
+ 'https://reactjs.org/docs/react-component.html#unsafe_componentwillupdate. ' +
+ 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.'
];
return deprecated;
}
@@ -106,19 +112,19 @@
);
}
- function checkDeprecation(node, method) {
- if (!isDeprecated(method)) {
+ function checkDeprecation(node, methodName, methodNode) {
+ if (!isDeprecated(methodName)) {
return;
}
const deprecated = getDeprecated();
- const version = deprecated[method][0];
- const newMethod = deprecated[method][1];
- const refs = deprecated[method][2];
+ const version = deprecated[methodName][0];
+ const newMethod = deprecated[methodName][1];
+ const refs = deprecated[methodName][2];
context.report({
- node: node,
+ node: methodNode || node,
message: DEPRECATED_MESSAGE,
data: {
- oldMethod: method,
+ oldMethod: methodName,
version,
newMethod: newMethod ? `, use ${newMethod} instead` : '',
refs: refs ? `, see ${refs}` : ''
@@ -150,7 +156,10 @@
*/
function getLifeCycleMethods(node) {
const properties = astUtil.getComponentProperties(node);
- return properties.map(property => astUtil.getPropertyName(property));
+ return properties.map(property => ({
+ name: astUtil.getPropertyName(property),
+ node: astUtil.getPropertyNameNode(property)
+ }));
}
/**
@@ -160,7 +169,7 @@
function checkLifeCycleMethods(node) {
if (utils.isES5Component(node) || utils.isES6Component(node)) {
const methods = getLifeCycleMethods(node);
- methods.forEach(method => checkDeprecation(node, method));
+ methods.forEach(method => checkDeprecation(node, method.name, method.node));
}
}

lib/rules/no-multi-comp.js

@@ -4,7 +4,6 @@
*/
'use strict';
-const has = require('has');
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
@@ -59,17 +58,15 @@
}
const list = components.list();
- let i = 0;
- for (const component in list) {
- if (!has(list, component) || isIgnored(list[component]) || ++i === 1) {
- continue;
- }
+ Object.keys(list).filter(component => !isIgnored(list[component])).forEach((component, i) => {
+ if (i >= 1) {
context.report({
node: list[component].node,
message: MULTI_COMP_MESSAGE
});
}
+ });
}
};
})

lib/rules/no-set-state.js

@@ -4,7 +4,6 @@
*/
'use strict';
-const has = require('has');
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
@@ -74,12 +73,9 @@
'Program:exit': function() {
const list = components.list();
- for (const component in list) {
- if (!has(list, component) || isValid(list[component])) {
- continue;
- }
+ Object.keys(list).filter(component => !isValid(list[component])).forEach(component => {
reportSetStateUsages(list[component]);
- }
+ });
}
};
})

lib/rules/no-this-in-sfc.js

@@ -29,11 +29,11 @@
create: Components.detect((context, components, utils) => ({
MemberExpression(node) {
+ if (node.object.type === 'ThisExpression') {
const component = components.get(utils.getParentStatelessComponent());
if (!component) {
return;
}
- if (node.object.type === 'ThisExpression') {
context.report({
node: node,
message: ERROR_MESSAGE

lib/rules/no-typos.js

@@ -45,7 +45,7 @@
let propTypesPackageName = null;
let reactPackageName = null;
- function checkValidPropTypeQualfier(node) {
+ function checkValidPropTypeQualifier(node) {
if (node.name !== 'isRequired') {
context.report({
node: node,
@@ -101,14 +101,14 @@
isPropTypesPackage(node.object.object)
) { // PropTypes.myProp.isRequired
checkValidPropType(node.object.property);
- checkValidPropTypeQualfier(node.property);
+ checkValidPropTypeQualifier(node.property);
} else if (
isPropTypesPackage(node.object) &&
node.property.name !== 'isRequired'
) { // PropTypes.myProp
checkValidPropType(node.property);
} else if (node.object.type === 'CallExpression') {
- checkValidPropTypeQualfier(node.property);
+ checkValidPropTypeQualifier(node.property);
checkValidCallExpression(node.object);
}
} else if (node.type === 'CallExpression') {
@@ -124,16 +124,18 @@
}
}
- function reportErrorIfClassPropertyCasingTypo(node, propertyName) {
+ function reportErrorIfPropertyCasingTypo(node, propertyName, isClassProperty) {
if (propertyName === 'propTypes' || propertyName === 'contextTypes' || propertyName === 'childContextTypes') {
- const propsNode = node && node.parent && node.parent.type === 'AssignmentExpression' && node.parent.right;
- checkValidPropObject(propsNode);
+ checkValidPropObject(node);
}
STATIC_CLASS_PROPERTIES.forEach(CLASS_PROP => {
if (propertyName && CLASS_PROP.toLowerCase() === propertyName.toLowerCase() && CLASS_PROP !== propertyName) {
+ const message = isClassProperty
+ ? 'Typo in static class property declaration'
+ : 'Typo in property declaration';
context.report({
node: node,
- message: 'Typo in static class property declaration'
+ message: message
});
}
});
@@ -155,8 +157,9 @@
if (node.source && node.source.value === 'prop-types') { // import PropType from "prop-types"
propTypesPackageName = node.specifiers[0].local.name;
} else if (node.source && node.source.value === 'react') { // import { PropTypes } from "react"
- reactPackageName = node.specifiers[0].local.name;
-
+ if (node.specifiers.length > 0) {
+ reactPackageName = node.specifiers[0].local.name; // guard against accidental anonymous `import "react"`
+ }
if (node.specifiers.length >= 1) {
const propTypesSpecifier = node.specifiers.find(specifier => (
specifier.imported && specifier.imported.name === 'PropTypes'
@@ -175,7 +178,7 @@
const tokens = context.getFirstTokens(node, 2);
const propertyName = tokens[1].value;
- reportErrorIfClassPropertyCasingTypo(node, propertyName);
+ reportErrorIfPropertyCasingTypo(node.value, propertyName, true);
},
MemberExpression: function(node) {
@@ -192,18 +195,32 @@
if (
relatedComponent &&
- (utils.isES6Component(relatedComponent.node) || utils.isReturningJSX(relatedComponent.node))
+ (utils.isES6Component(relatedComponent.node) || utils.isReturningJSX(relatedComponent.node)) &&
+ (node.parent && node.parent.type === 'AssignmentExpression' && node.parent.right)
) {
- reportErrorIfClassPropertyCasingTypo(node, propertyName);
+ reportErrorIfPropertyCasingTypo(node.parent.right, propertyName, true);
}
},
- MethodDefinition: function (node) {
+ MethodDefinition: function(node) {
if (!utils.isES6Component(node.parent.parent)) {
return;
}
reportErrorIfLifecycleMethodCasingTypo(node);
+ },
+
+ ObjectExpression: function(node) {
+ const component = utils.isES5Component(node) && components.get(node);
+
+ if (!component) {
+ return;
+ }
+
+ node.properties.forEach(property => {
+ reportErrorIfPropertyCasingTypo(property.value, property.key.name, false);
+ reportErrorIfLifecycleMethodCasingTypo(property);
+ });
}
};
})

lib/rules/no-unescaped-entities.js

@@ -5,6 +5,7 @@
'use strict';
const docsUrl = require('../util/docsUrl');
+const jsxUtil = require('../util/jsx');
// ------------------------------------------------------------------------------
// Rule Definition
@@ -71,8 +72,8 @@
}
return {
- Literal: function(node) {
- if (node.type === 'Literal' && node.parent.type === 'JSXElement') {
+ 'Literal, JSXText': function(node) {
+ if (jsxUtil.isJSX(node.parent)) {
reportInvalidEntity(node);
}
}

lib/rules/no-unknown-property.js

@@ -117,7 +117,7 @@
const DOM_PROPERTY_NAMES = [
// Standard
'acceptCharset', 'accessKey', 'allowFullScreen', 'allowTransparency', 'autoComplete', 'autoFocus', 'autoPlay',
- 'cellPadding', 'cellSpacing', 'charSet', 'classID', 'className', 'colSpan', 'contentEditable', 'contextMenu',
+ 'cellPadding', 'cellSpacing', 'classID', 'className', 'colSpan', 'contentEditable', 'contextMenu',
'dateTime', 'encType', 'formAction', 'formEncType', 'formMethod', 'formNoValidate', 'formTarget',
'frameBorder', 'hrefLang', 'htmlFor', 'httpEquiv', 'inputMode', 'keyParams', 'keyType', 'marginHeight', 'marginWidth',
'maxLength', 'mediaGroup', 'minLength', 'noValidate', 'onAnimationEnd', 'onAnimationIteration', 'onAnimationStart',

lib/rules/no-unsafe.js

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

lib/rules/no-unused-prop-types.js

@@ -7,25 +7,10 @@
// As for exceptions for props.children or props.className (and alike) look at
// https://github.com/yannickcr/eslint-plugin-react/issues/7
-const has = require('has');
const Components = require('../util/Components');
-const variable = require('../util/variable');
-const annotations = require('../util/annotations');
-const versionUtil = require('../util/version');
-const propsUtil = require('../util/props');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
-// Constants
-// ------------------------------------------------------------------------------
-
-const DIRECT_PROPS_REGEX = /^props\s*(\.|\[)/;
-const DIRECT_NEXT_PROPS_REGEX = /^nextProps\s*(\.|\[)/;
-const DIRECT_PREV_PROPS_REGEX = /^prevProps\s*(\.|\[)/;
-const LIFE_CYCLE_METHODS = ['componentWillReceiveProps', 'shouldComponentUpdate', 'componentWillUpdate', 'componentDidUpdate'];
-const ASYNC_SAFE_LIFE_CYCLE_METHODS = ['getDerivedStateFromProps', 'getSnapshotBeforeUpdate', 'UNSAFE_componentWillReceiveProps', 'UNSAFE_componentWillUpdate'];
-
-// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
@@ -55,187 +40,12 @@
}]
},
- create: Components.detect((context, components, utils) => {
- const defaults = {skipShapeProps: true};
- const sourceCode = context.getSourceCode();
+ create: Components.detect((context, components) => {
+ const defaults = {skipShapeProps: true, customValidators: []};
const configuration = Object.assign({}, defaults, context.options[0] || {});
- const skipShapeProps = configuration.skipShapeProps;
- const customValidators = configuration.customValidators || [];
- const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
- const checkAsyncSafeLifeCycles = versionUtil.testReactVersion(context, '16.3.0');
-
- // Used to track the type annotations in scope.
- // Necessary because babel's scopes do not track type annotations.
- let stack = null;
-
const UNUSED_MESSAGE = '\'{{name}}\' PropType is defined but prop is never used';
/**
- * Helper for accessing the current scope in the stack.
- * @param {string} key The name of the identifier to access. If omitted, returns the full scope.
- * @param {ASTNode} value If provided sets the new value for the identifier.
- * @returns {Object|ASTNode} Either the whole scope or the ASTNode associated with the given identifier.
- */
- function typeScope(key, value) {
- if (arguments.length === 0) {
- return stack[stack.length - 1];
- } else if (arguments.length === 1) {
- return stack[stack.length - 1][key];
- }
- stack[stack.length - 1][key] = value;
- return value;
- }
-
- /**
- * Check if we are in a lifecycle method
- * @return {boolean} true if we are in a class constructor, false if not
- **/
- function inLifeCycleMethod() {
- let scope = context.getScope();
- while (scope) {
- if (scope.block && scope.block.parent && scope.block.parent.key) {
- const name = scope.block.parent.key.name;
-
- if (LIFE_CYCLE_METHODS.indexOf(name) >= 0) {
- return true;
- } else if (checkAsyncSafeLifeCycles && ASYNC_SAFE_LIFE_CYCLE_METHODS.indexOf(name) >= 0) {
- return true;
- }
- }
- scope = scope.upper;
- }
- return false;
- }
-
- /**
- * Check if the current node is in a setState updater method
- * @return {boolean} true if we are in a setState updater, false if not
- */
- function inSetStateUpdater() {
- let scope = context.getScope();
- while (scope) {
- if (
- scope.block && scope.block.parent
- && scope.block.parent.type === 'CallExpression'
- && scope.block.parent.callee.property
- && scope.block.parent.callee.property.name === 'setState'
- // Make sure we are in the updater not the callback
- && scope.block.parent.arguments[0].start === scope.block.start
- ) {
- return true;
- }
- scope = scope.upper;
- }
- return false;
- }
-
- function isPropArgumentInSetStateUpdater(node) {
- let scope = context.getScope();
- while (scope) {
- if (
- scope.block && scope.block.parent
- && scope.block.parent.type === 'CallExpression'
- && scope.block.parent.callee.property
- && scope.block.parent.callee.property.name === 'setState'
- // Make sure we are in the updater not the callback
- && scope.block.parent.arguments[0].start === scope.block.start
- && scope.block.parent.arguments[0].params
- && scope.block.parent.arguments[0].params.length > 1
- ) {
- return scope.block.parent.arguments[0].params[1].name === node.object.name;
- }
- scope = scope.upper;
- }
- return false;
- }
-
- /**
- * Checks if we are using a prop
- * @param {ASTNode} node The AST node being checked.
- * @returns {Boolean} True if we are using a prop, false if not.
- */
- function isPropTypesUsage(node) {
- const isClassUsage = (
- (utils.getParentES6Component() || utils.getParentES5Component()) &&
- ((node.object.type === 'ThisExpression' && node.property.name === 'props')
- || isPropArgumentInSetStateUpdater(node))
- );
- const isStatelessFunctionUsage = node.object.name === 'props';
- return isClassUsage || isStatelessFunctionUsage || inLifeCycleMethod();
- }
-
- /**
- * Checks if we are declaring a `props` class property 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 isAnnotatedClassPropsDeclaration(node) {
- if (node && node.type === 'ClassProperty') {
- const tokens = context.getFirstTokens(node, 2);
- if (
- node.typeAnnotation && (
- tokens[0].value === 'props' ||
- (tokens[1] && tokens[1].value === 'props')
- )
- ) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Resolve the type annotation for a given class declaration node with superTypeParameters.
- *
- * @param {ASTNode} node The annotation or a node containing the type annotation.
- * @returns {ASTNode} The resolved type annotation for the node.
- */
- function resolveSuperParameterPropsType(node) {
- let propsParameterPosition;
- try {
- // Flow <=0.52 had 3 required TypedParameters of which the second one is the Props.
- // Flow >=0.53 has 2 optional TypedParameters of which the first one is the Props.
- propsParameterPosition = versionUtil.testFlowVersion(context, '0.53.0') ? 0 : 1;
- } catch (e) {
- // In case there is no flow version defined, we can safely assume that when there are 3 Props we are dealing with version <= 0.52
- propsParameterPosition = node.superTypeParameters.params.length <= 2 ? 0 : 1;
- }
-
- let annotation = node.superTypeParameters.params[propsParameterPosition];
- while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) {
- annotation = annotation.typeAnnotation;
- }
- if (annotation.type === 'GenericTypeAnnotation' && typeScope(annotation.id.name)) {
- return typeScope(annotation.id.name);
- }
- return annotation;
- }
-
- /**
- * Checks if we are declaring a props as a generic type in a flow-annotated class.
- *
- * @param {ASTNode} node the AST node being checked.
- * @returns {Boolean} True if the node is a class with generic prop types, false if not.
- */
- function isSuperTypeParameterPropsDeclaration(node) {
- if (node && node.type === 'ClassDeclaration') {
- if (node.superTypeParameters && node.superTypeParameters.params.length > 0) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Checks if prop should be validated by plugin-react-proptypes
- * @param {String} validator Name of validator to check.
- * @returns {Boolean} True if validator should be checked by custom validator.
- */
- function hasCustomValidator(validator) {
- return customValidators.indexOf(validator) !== -1;
- }
-
- /**
* Checks if the component must be validated
* @param {Object} component The component to process
* @returns {Boolean} True if the component must be validated, false if not.
@@ -243,57 +53,7 @@
function mustBeValidated(component) {
return Boolean(
component &&
- !component.ignorePropsValidation
- );
- }
-
- /**
- * Returns true if the given node is a React Component lifecycle method
- * @param {ASTNode} node The AST node being checked.
- * @return {Boolean} True if the node is a lifecycle method
- */
- function isNodeALifeCycleMethod(node) {
- const nodeKeyName = (node.key || {}).name;
-
- if (node.kind === 'constructor') {
- return true;
- } else if (LIFE_CYCLE_METHODS.indexOf(nodeKeyName) >= 0) {
- return true;
- } else if (checkAsyncSafeLifeCycles && ASYNC_SAFE_LIFE_CYCLE_METHODS.indexOf(nodeKeyName) >= 0) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Returns true if the given node is inside a React Component lifecycle
- * method.
- * @param {ASTNode} node The AST node being checked.
- * @return {Boolean} True if the node is inside a lifecycle method
- */
- function isInLifeCycleMethod(node) {
- if ((node.type === 'MethodDefinition' || node.type === 'Property') && isNodeALifeCycleMethod(node)) {
- return true;
- }
-
- if (node.parent) {
- return isInLifeCycleMethod(node.parent);
- }
-
- return false;
- }
-
- /**
- * Checks if a prop init name matches common naming patterns
- * @param {ASTNode} node The AST node being checked.
- * @returns {Boolean} True if the prop name matches
- */
- function isPropAttributeName (node) {
- return (
- node.init.name === 'props' ||
- node.init.name === 'nextProps' ||
- node.init.name === 'prevProps'
+ !component.ignoreUnusedPropTypesValidation
);
}
@@ -320,568 +80,6 @@
}
/**
- * Checks if the prop has spread operator.
- * @param {ASTNode} node The AST node being marked.
- * @returns {Boolean} True if the prop has spread operator, false if not.
- */
- function hasSpreadOperator(node) {
- const tokens = sourceCode.getTokens(node);
- return tokens.length && tokens[0].value === '...';
- }
-
- /**
- * Removes quotes from around an identifier.
- * @param {string} the identifier to strip
- */
- function stripQuotes(string) {
- return string.replace(/^\'|\'$/g, '');
- }
-
- /**
- * Retrieve the name of a key node
- * @param {ASTNode} node The AST node with the key.
- * @return {string} the name of the key
- */
- function getKeyValue(node) {
- if (node.type === 'ObjectTypeProperty') {
- const tokens = context.getFirstTokens(node, 2);
- return (tokens[0].value === '+' || tokens[0].value === '-'
- ? tokens[1].value
- : stripQuotes(tokens[0].value)
- );
- }
- const key = node.key || node.argument;
- return key.type === 'Identifier' ? key.name : key.value;
- }
-
- /**
- * Iterates through a properties node, like a customized forEach.
- * @param {Object[]} properties Array of properties to iterate.
- * @param {Function} fn Function to call on each property, receives property key
- and property value. (key, value) => void
- */
- function iterateProperties(properties, fn) {
- if (properties.length && typeof fn === 'function') {
- for (let i = 0, j = properties.length; i < j; i++) {
- const node = properties[i];
- const key = getKeyValue(node);
-
- const value = node.value;
- fn(key, value);
- }
- }
- }
-
- /**
- * Creates the representation of the React propTypes for the component.
- * The representation is used to verify nested used properties.
- * @param {ASTNode} value Node of the PropTypes for the desired property
- * @param {String} parentName Name of the parent prop node.
- * @return {Object|Boolean} The representation of the declaration, true means
- * the property is declared without the need for further analysis.
- */
- function buildReactDeclarationTypes(value, parentName) {
- if (
- value &&
- value.callee &&
- value.callee.object &&
- hasCustomValidator(value.callee.object.name)
- ) {
- return {};
- }
-
- if (
- value &&
- value.type === 'MemberExpression' &&
- value.property &&
- value.property.name &&
- value.property.name === 'isRequired'
- ) {
- value = value.object;
- }
-
- // Verify PropTypes that are functions
- if (
- value &&
- value.type === 'CallExpression' &&
- value.callee &&
- value.callee.property &&
- value.callee.property.name &&
- value.arguments &&
- value.arguments.length > 0
- ) {
- const callName = value.callee.property.name;
- const argument = value.arguments[0];
- switch (callName) {
- case 'shape':
- if (skipShapeProps) {
- return {};
- }
-
- if (argument.type !== 'ObjectExpression') {
- // Invalid proptype or cannot analyse statically
- return {};
- }
- const shapeTypeDefinition = {
- type: 'shape',
- children: []
- };
- iterateProperties(argument.properties, (childKey, childValue) => {
- const fullName = [parentName, childKey].join('.');
- const types = buildReactDeclarationTypes(childValue, fullName);
- types.fullName = fullName;
- types.name = childKey;
- types.node = childValue;
- shapeTypeDefinition.children.push(types);
- });
- return shapeTypeDefinition;
- case 'arrayOf':
- case 'objectOf':
- const fullName = [parentName, '*'].join('.');
- const child = buildReactDeclarationTypes(argument, fullName);
- child.fullName = fullName;
- child.name = '__ANY_KEY__';
- child.node = argument;
- return {
- type: 'object',
- children: [child]
- };
- case 'oneOfType':
- if (
- !argument.elements ||
- !argument.elements.length
- ) {
- // Invalid proptype or cannot analyse statically
- return {};
- }
- const unionTypeDefinition = {
- type: 'union',
- children: []
- };
- for (let i = 0, j = argument.elements.length; i < j; i++) {
- const type = buildReactDeclarationTypes(argument.elements[i], parentName);
- // keep only complex type
- if (Object.keys(type).length > 0) {
- if (type.children === true) {
- // every child is accepted for one type, abort type analysis
- unionTypeDefinition.children = true;
- return unionTypeDefinition;
- }
- }
-
- unionTypeDefinition.children.push(type);
- }
- if (unionTypeDefinition.length === 0) {
- // no complex type found, simply accept everything
- return {};
- }
- return unionTypeDefinition;
- case 'instanceOf':
- return {
- type: 'instance',
- // Accept all children because we can't know what type they are
- children: true
- };
- case 'oneOf':
- default:
- return {};
- }
- }
- // Unknown property or accepts everything (any, object, ...)
- return {};
- }
-
- /**
- * Creates the representation of the React props type annotation for the component.
- * The representation is used to verify nested used properties.
- * @param {ASTNode} annotation Type annotation for the props class property.
- * @param {String} parentName Name of the parent prop node.
- * @return {Object} The representation of the declaration, an empty object means
- * the property is declared without the need for further analysis.
- */
- function buildTypeAnnotationDeclarationTypes(annotation, parentName) {
- switch (annotation.type) {
- case 'GenericTypeAnnotation':
- if (typeScope(annotation.id.name)) {
- return buildTypeAnnotationDeclarationTypes(typeScope(annotation.id.name), parentName);
- }
- return {};
- case 'ObjectTypeAnnotation':
- if (skipShapeProps) {
- return {};
- }
- const shapeTypeDefinition = {
- type: 'shape',
- children: []
- };
- iterateProperties(annotation.properties, (childKey, childValue) => {
- const fullName = [parentName, childKey].join('.');
- const types = buildTypeAnnotationDeclarationTypes(childValue, fullName);
- types.fullName = fullName;
- types.name = childKey;
- types.node = childValue;
- shapeTypeDefinition.children.push(types);
- });
- return shapeTypeDefinition;
- case 'UnionTypeAnnotation':
- const unionTypeDefinition = {
- type: 'union',
- children: []
- };
- for (let i = 0, j = annotation.types.length; i < j; i++) {
- const type = buildTypeAnnotationDeclarationTypes(annotation.types[i], parentName);
- // keep only complex type
- if (Object.keys(type).length > 0) {
- if (type.children === true) {
- // every child is accepted for one type, abort type analysis
- unionTypeDefinition.children = true;
- return unionTypeDefinition;
- }
- }
-
- unionTypeDefinition.children.push(type);
- }
- if (unionTypeDefinition.children.length === 0) {
- // no complex type found
- return {};
- }
- return unionTypeDefinition;
- case 'ArrayTypeAnnotation':
- const fullName = [parentName, '*'].join('.');
- const child = buildTypeAnnotationDeclarationTypes(annotation.elementType, fullName);
- child.fullName = fullName;
- child.name = '__ANY_KEY__';
- child.node = annotation;
- return {
- type: 'object',
- children: [child]
- };
- default:
- // Unknown or accepts everything.
- return {};
- }
- }
-
- /**
- * Check if we are in a class constructor
- * @return {boolean} true if we are in a class constructor, false if not
- */
- function inConstructor() {
- let scope = context.getScope();
- while (scope) {
- if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') {
- return true;
- }
- scope = scope.upper;
- }
- return false;
- }
-
- /**
- * Retrieve the name of a property node
- * @param {ASTNode} node The AST node with the property.
- * @return {string} the name of the property or undefined if not found
- */
- function getPropertyName(node) {
- const isDirectProp = DIRECT_PROPS_REGEX.test(sourceCode.getText(node));
- const isDirectNextProp = DIRECT_NEXT_PROPS_REGEX.test(sourceCode.getText(node));
- const isDirectPrevProp = DIRECT_PREV_PROPS_REGEX.test(sourceCode.getText(node));
- const isDirectSetStateProp = isPropArgumentInSetStateUpdater(node);
- const isInClassComponent = utils.getParentES6Component() || utils.getParentES5Component();
- const isNotInConstructor = !inConstructor(node);
- const isNotInLifeCycleMethod = !inLifeCycleMethod();
- const isNotInSetStateUpdater = !inSetStateUpdater();
- if ((isDirectProp || isDirectNextProp || isDirectPrevProp || isDirectSetStateProp)
- && isInClassComponent
- && isNotInConstructor
- && isNotInLifeCycleMethod
- && isNotInSetStateUpdater
- ) {
- return void 0;
- }
- if (!isDirectProp && !isDirectNextProp && !isDirectPrevProp && !isDirectSetStateProp) {
- node = node.parent;
- }
- const property = node.property;
- if (property) {
- switch (property.type) {
- case 'Identifier':
- if (node.computed) {
- return '__COMPUTED_PROP__';
- }
- return property.name;
- case 'MemberExpression':
- return void 0;
- case 'Literal':
- // Accept computed properties that are literal strings
- if (typeof property.value === 'string') {
- return property.value;
- }
- // falls through
- default:
- if (node.computed) {
- return '__COMPUTED_PROP__';
- }
- break;
- }
- }
- return void 0;
- }
-
- /**
- * Mark a prop type as used
- * @param {ASTNode} node The AST node being marked.
- */
- function markPropTypesAsUsed(node, parentNames) {
- parentNames = parentNames || [];
- let type;
- let name;
- let allNames;
- let properties;
- switch (node.type) {
- case 'MemberExpression':
- name = getPropertyName(node);
- if (name) {
- allNames = parentNames.concat(name);
- if (node.parent.type === 'MemberExpression') {
- markPropTypesAsUsed(node.parent, allNames);
- }
- // Do not mark computed props as used.
- type = name !== '__COMPUTED_PROP__' ? 'direct' : null;
- } else if (
- node.parent.id &&
- node.parent.id.properties &&
- node.parent.id.properties.length &&
- getKeyValue(node.parent.id.properties[0])
- ) {
- type = 'destructuring';
- properties = node.parent.id.properties;
- }
- break;
- case 'ArrowFunctionExpression':
- case 'FunctionDeclaration':
- case 'FunctionExpression':
- if (node.params.length === 0) {
- break;
- }
- type = 'destructuring';
- properties = node.params[0].properties;
- if (inSetStateUpdater()) {
- properties = node.params[1].properties;
- }
- break;
- case 'VariableDeclarator':
- for (let i = 0, j = node.id.properties.length; i < j; i++) {
- // let {props: {firstname}} = this
- const thisDestructuring = (
- node.id.properties[i].key && (
- (node.id.properties[i].key.name === 'props' || node.id.properties[i].key.value === 'props') &&
- node.id.properties[i].value.type === 'ObjectPattern'
- )
- );
- // let {firstname} = props
- const genericDestructuring = isPropAttributeName(node) && (
- utils.getParentStatelessComponent() ||
- isInLifeCycleMethod(node)
- );
-
- if (thisDestructuring) {
- properties = node.id.properties[i].value.properties;
- } else if (genericDestructuring) {
- properties = node.id.properties;
- } else {
- continue;
- }
- type = 'destructuring';
- break;
- }
- break;
- default:
- throw new Error(`${node.type} ASTNodes are not handled by markPropTypesAsUsed`);
- }
-
- const component = components.get(utils.getParentComponent());
- const usedPropTypes = component && component.usedPropTypes || [];
- let ignorePropsValidation = component && component.ignorePropsValidation || false;
-
- switch (type) {
- case 'direct':
- // Ignore Object methods
- if (Object.prototype[name]) {
- break;
- }
-
- usedPropTypes.push({
- name: name,
- allNames: allNames
- });
- break;
- case 'destructuring':
- for (let k = 0, l = (properties || []).length; k < l; k++) {
- if (hasSpreadOperator(properties[k]) || properties[k].computed) {
- ignorePropsValidation = true;
- break;
- }
- const propName = getKeyValue(properties[k]);
-
- let currentNode = node;
- allNames = [];
- while (currentNode.property && currentNode.property.name !== 'props') {
- allNames.unshift(currentNode.property.name);
- currentNode = currentNode.object;
- }
- allNames.push(propName);
-
- if (propName) {
- usedPropTypes.push({
- allNames: allNames,
- name: propName
- });
- }
- }
- break;
- default:
- break;
- }
-
- components.set(component ? component.node : node, {
- usedPropTypes: usedPropTypes,
- ignorePropsValidation: ignorePropsValidation
- });
- }
-
- /**
- * Marks all props found inside ObjectTypeAnnotaiton as declared.
- *
- * Modifies the declaredProperties object
- * @param {ASTNode} propTypes
- * @param {Object} declaredPropTypes
- * @returns {Boolean} True if propTypes should be ignored (e.g. when a type can't be resolved, when it is imported)
- */
- function declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes) {
- let ignorePropsValidation = false;
-
- iterateProperties(propTypes.properties, (key, value) => {
- if (!value) {
- ignorePropsValidation = true;
- return;
- }
-
- const types = buildTypeAnnotationDeclarationTypes(value, key);
- types.fullName = key;
- types.name = key;
- types.node = value;
- declaredPropTypes.push(types);
- });
-
- return ignorePropsValidation;
- }
-
- /**
- * Marks all props found inside IntersectionTypeAnnotation as declared.
- * Since InterSectionTypeAnnotations can be nested, this handles recursively.
- *
- * Modifies the declaredPropTypes object
- * @param {ASTNode} propTypes
- * @param {Object} declaredPropTypes
- * @returns {Boolean} True if propTypes should be ignored (e.g. when a type can't be resolved, when it is imported)
- */
- function declarePropTypesForIntersectionTypeAnnotation(propTypes, declaredPropTypes) {
- return propTypes.types.some(annotation => {
- if (annotation.type === 'ObjectTypeAnnotation') {
- return declarePropTypesForObjectTypeAnnotation(annotation, declaredPropTypes);
- }
-
- if (annotation.type === 'UnionTypeAnnotation') {
- return true;
- }
-
- const typeNode = typeScope(annotation.id.name);
-
- if (!typeNode) {
- return true;
- } else if (typeNode.type === 'IntersectionTypeAnnotation') {
- return declarePropTypesForIntersectionTypeAnnotation(typeNode, declaredPropTypes);
- }
-
- return declarePropTypesForObjectTypeAnnotation(typeNode, declaredPropTypes);
- });
- }
-
- /**
- * Mark a prop type as declared
- * @param {ASTNode} node The AST node being checked.
- * @param {propTypes} node The AST node containing the proptypes
- */
- function markPropTypesAsDeclared(node, propTypes) {
- const component = components.get(node);
- const declaredPropTypes = component && component.declaredPropTypes || [];
- let ignorePropsValidation = component && component.ignorePropsValidation || false;
-
- switch (propTypes && propTypes.type) {
- case 'ObjectTypeAnnotation':
- ignorePropsValidation = declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes);
- break;
- case 'ObjectExpression':
- iterateProperties(propTypes.properties, (key, value) => {
- if (!value) {
- ignorePropsValidation = true;
- return;
- }
- const types = buildReactDeclarationTypes(value, key);
- types.fullName = key;
- types.name = key;
- types.node = value;
- declaredPropTypes.push(types);
- // Handle custom prop validators using props inside
- if (
- value.type === 'ArrowFunctionExpression'
- || value.type === 'FunctionExpression'
- ) {
- markPropTypesAsUsed(value);
- }
- });
- break;
- case 'MemberExpression':
- break;
- case 'Identifier':
- const variablesInScope = variable.variablesInScope(context);
- for (let i = 0, j = variablesInScope.length; i < j; i++) {
- if (variablesInScope[i].name !== propTypes.name) {
- continue;
- }
- const defInScope = variablesInScope[i].defs[variablesInScope[i].defs.length - 1];
- markPropTypesAsDeclared(node, defInScope.node && defInScope.node.init);
- return;
- }
- ignorePropsValidation = true;
- break;
- case 'CallExpression':
- if (
- propWrapperFunctions.has(propTypes.callee.name) &&
- propTypes.arguments && propTypes.arguments[0]
- ) {
- markPropTypesAsDeclared(node, propTypes.arguments[0]);
- return;
- }
- break;
- case 'IntersectionTypeAnnotation':
- ignorePropsValidation = declarePropTypesForIntersectionTypeAnnotation(propTypes, declaredPropTypes);
- break;
- case null:
- break;
- default:
- ignorePropsValidation = true;
- break;
- }
-
- components.set(node, {
- declaredPropTypes: declaredPropTypes,
- ignorePropsValidation: ignorePropsValidation
- });
- }
-
- /**
* Used to recursively loop through each declared prop type
* @param {Object} component The component to process
* @param {Array} props List of props to validate
@@ -892,15 +90,20 @@
return;
}
- (props || []).forEach(prop => {
+ Object.keys(props || {}).forEach(key => {
+ const prop = props[key];
// Skip props that check instances
if (prop === true) {
return;
}
+ if (prop.type === 'shape' && configuration.skipShapeProps) {
+ return;
+ }
+
if (prop.node && !isPropUsed(component, prop)) {
context.report(
- prop.node,
+ prop.node.value || prop.node,
UNUSED_MESSAGE, {
name: prop.fullName
}
@@ -921,204 +124,20 @@
reportUnusedPropType(component, component.declaredPropTypes);
}
- /**
- * Resolve the type annotation for a given node.
- * Flow annotations are sometimes wrapped in outer `TypeAnnotation`
- * and `NullableTypeAnnotation` nodes which obscure the annotation we're
- * interested in.
- * This method also resolves type aliases where possible.
- *
- * @param {ASTNode} node The annotation or a node containing the type annotation.
- * @returns {ASTNode} The resolved type annotation for the node.
- */
- function resolveTypeAnnotation(node) {
- let annotation = node.typeAnnotation || node;
- while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) {
- annotation = annotation.typeAnnotation;
- }
- if (annotation.type === 'GenericTypeAnnotation' && typeScope(annotation.id.name)) {
- return typeScope(annotation.id.name);
- }
- return annotation;
- }
-
- /**
- * @param {ASTNode} node We expect either an ArrowFunctionExpression,
- * FunctionDeclaration, or FunctionExpression
- */
- function markDestructuredFunctionArgumentsAsUsed(node) {
- const destructuring = node.params && node.params[0] && node.params[0].type === 'ObjectPattern';
- if (destructuring && components.get(node)) {
- markPropTypesAsUsed(node);
- }
- }
-
- /**
- * @param {ASTNode} node We expect either an ArrowFunctionExpression,
- * FunctionDeclaration, or FunctionExpression
- */
- function markAnnotatedFunctionArgumentsAsDeclared(node) {
- if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) {
- return;
- }
- markPropTypesAsDeclared(node, resolveTypeAnnotation(node.params[0]));
- }
-
- function handleSetStateUpdater(node) {
- if (!node.params || node.params.length < 2 || !inSetStateUpdater()) {
- return;
- }
- markPropTypesAsUsed(node);
- }
-
- /**
- * Handle both stateless functions and setState updater functions.
- * @param {ASTNode} node We expect either an ArrowFunctionExpression,
- * FunctionDeclaration, or FunctionExpression
- */
- function handleFunctionLikeExpressions(node) {
- handleSetStateUpdater(node);
- markDestructuredFunctionArgumentsAsUsed(node);
- markAnnotatedFunctionArgumentsAsDeclared(node);
- }
-
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
- ClassDeclaration: function(node) {
- if (isSuperTypeParameterPropsDeclaration(node)) {
- markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node));
- }
- },
-
- ClassProperty: function(node) {
- if (isAnnotatedClassPropsDeclaration(node)) {
- markPropTypesAsDeclared(node, resolveTypeAnnotation(node));
- } else if (propsUtil.isPropTypesDeclaration(node)) {
- markPropTypesAsDeclared(node, node.value);
- }
- },
-
- VariableDeclarator: function(node) {
- const destructuring = node.init && node.id && node.id.type === 'ObjectPattern';
- // let {props: {firstname}} = this
- const thisDestructuring = destructuring && node.init.type === 'ThisExpression';
- // let {firstname} = props
- const statelessDestructuring = destructuring && isPropAttributeName(node) && (
- utils.getParentStatelessComponent() ||
- isInLifeCycleMethod(node)
- );
-
- if (!thisDestructuring && !statelessDestructuring) {
- return;
- }
- markPropTypesAsUsed(node);
- },
-
- FunctionDeclaration: handleFunctionLikeExpressions,
-
- ArrowFunctionExpression: handleFunctionLikeExpressions,
-
- FunctionExpression: handleFunctionLikeExpressions,
-
- MemberExpression: function(node) {
- let type;
- if (isPropTypesUsage(node)) {
- type = 'usage';
- } else if (propsUtil.isPropTypesDeclaration(node)) {
- type = 'declaration';
- }
-
- switch (type) {
- case 'usage':
- markPropTypesAsUsed(node);
- break;
- case 'declaration':
- const component = utils.getRelatedComponent(node);
- if (!component) {
- return;
- }
- markPropTypesAsDeclared(component.node, node.parent.right || node.parent);
- break;
- default:
- break;
- }
- },
-
- JSXSpreadAttribute: function(node) {
- const component = components.get(utils.getParentComponent());
- components.set(component ? component.node : node, {
- ignorePropsValidation: true
- });
- },
-
- MethodDefinition: function(node) {
- if (!propsUtil.isPropTypesDeclaration(node)) {
- return;
- }
-
- let i = node.value.body.body.length - 1;
- for (; i >= 0; i--) {
- if (node.value.body.body[i].type === 'ReturnStatement') {
- break;
- }
- }
-
- if (i >= 0) {
- markPropTypesAsDeclared(node, node.value.body.body[i].argument);
- }
- },
-
- ObjectPattern: function(node) {
- // If the object pattern is a destructured props object in a lifecycle
- // method -- mark it for used props.
- if (isNodeALifeCycleMethod(node.parent.parent)) {
- node.properties.forEach((property, i) => {
- if (i === 0) {
- markPropTypesAsUsed(node.parent);
- }
- });
- }
- },
-
- ObjectExpression: function(node) {
- // Search for the proptypes declaration
- node.properties.forEach(property => {
- if (!propsUtil.isPropTypesDeclaration(property)) {
- return;
- }
- markPropTypesAsDeclared(node, property.value);
- });
- },
-
- TypeAlias: function(node) {
- typeScope(node.id.name, node.right);
- },
-
- Program: function() {
- stack = [{}];
- },
-
- BlockStatement: function () {
- stack.push(Object.create(typeScope()));
- },
-
- 'BlockStatement:exit': function () {
- stack.pop();
- },
-
'Program:exit': function() {
- stack = null;
const list = components.list();
// Report undeclared proptypes for all classes
- for (const component in list) {
- if (!has(list, component) || !mustBeValidated(list[component])) {
- continue;
+ Object.keys(list).filter(component => mustBeValidated(list[component])).forEach(component => {
+ if (!mustBeValidated(list[component])) {
+ return;
}
reportUnusedPropTypes(list[component]);
- }
+ });
}
};
})

lib/rules/no-unused-state.js

@@ -59,6 +59,14 @@
};
}
+function isSetStateCall(node) {
+ return (
+ node.callee.type === 'MemberExpression' &&
+ isThisExpression(node.callee.object) &&
+ getName(node.callee.property) === 'setState'
+ );
+}
+
module.exports = {
meta: {
docs: {
@@ -77,7 +85,38 @@
// 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`, `prevState` or `nextState`.
+ function isStateParameterReference(node) {
+ const classMethods = [
+ 'shouldComponentUpdate',
+ 'componentWillUpdate',
+ 'UNSAFE_componentWillUpdate',
+ 'getSnapshotBeforeUpdate',
+ 'componentDidUpdate'
+ ];
+
+ let scope = context.getScope();
+ while (scope) {
+ const parent = scope.block && scope.block.parent;
+ if (
+ parent &&
+ parent.type === 'MethodDefinition' && (
+ parent.static && parent.key.name === 'getDerivedStateFromProps' ||
+ classMethods.indexOf(parent.key.name !== -1)
+ ) &&
+ parent.value.type === 'FunctionExpression' &&
+ parent.value.params[1] &&
+ parent.value.params[1].name === node.name
+ ) {
+ return true;
+ }
+ scope = scope.upper;
+ }
+
+ return false;
+ }
+
+ // Returns true if the given node is possibly a reference to `this.state` or the state parameter of
+ // a lifecycle method.
function isStateReference(node) {
node = uncast(node);
@@ -91,15 +130,7 @@
classInfo.aliases &&
classInfo.aliases.has(node.name);
- const isPrevStateReference =
- node.type === 'Identifier' &&
- node.name === 'prevState';
-
- const isNextStateReference =
- node.type === 'Identifier' &&
- node.name === 'nextState';
-
- return isDirectStateReference || isAliasedStateReference || isPrevStateReference || isNextStateReference;
+ return isDirectStateReference || isAliasedStateReference || isStateParameterReference(node);
}
// Takes an ObjectExpression node and adds all named Property nodes to the
@@ -136,7 +167,7 @@
if (prop.type === 'Property') {
addUsedStateField(prop.key);
} else if (
- prop.type === 'ExperimentalRestProperty' &&
+ (prop.type === 'ExperimentalRestProperty' || prop.type === 'RestElement') &&
classInfo.aliases
) {
classInfo.aliases.add(getName(prop.argument));
@@ -223,13 +254,21 @@
// 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' &&
+ isSetStateCall(node) &&
node.arguments.length > 0 &&
node.arguments[0].type === 'ObjectExpression'
) {
addStateFields(node.arguments[0]);
+ } else if (
+ isSetStateCall(node) &&
+ node.arguments.length > 0 &&
+ node.arguments[0].type === 'ArrowFunctionExpression' &&
+ node.arguments[0].body.type === 'ObjectExpression'
+ ) {
+ if (node.arguments[0].params.length > 0 && classInfo.aliases) {
+ classInfo.aliases.add(getName(node.arguments[0].params[0]));
+ }
+ addStateFields(node.arguments[0].body);
}
},
@@ -374,7 +413,7 @@
}
},
- ExperimentalSpreadProperty(node) {
+ 'ExperimentalSpreadProperty, SpreadElement'(node) {
if (classInfo && isStateReference(node.argument)) {
classInfo = null;
}

lib/rules/no-will-update-set-state.js

@@ -5,5 +5,9 @@
'use strict';
const makeNoMethodSetStateRule = require('../util/makeNoMethodSetStateRule');
+const versionUtil = require('../util/version');
-module.exports = makeNoMethodSetStateRule('componentWillUpdate');
+module.exports = makeNoMethodSetStateRule(
+ 'componentWillUpdate',
+ context => versionUtil.testReactVersion(context, '16.3.0')
+);

lib/rules/prefer-stateless-function.js

@@ -6,7 +6,6 @@
*/
'use strict';
-const has = require('has');
const Components = require('../util/Components');
const versionUtil = require('../util/version');
const astUtil = require('../util/ast');
@@ -357,9 +356,8 @@
'Program:exit': function() {
const list = components.list();
- for (const component in list) {
+ Object.keys(list).forEach(component => {
if (
- !has(list, component) ||
hasOtherProperties(list[component].node) ||
list[component].useThis ||
list[component].useRef ||
@@ -368,17 +366,17 @@
list[component].useDecorators ||
(!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node))
) {
- continue;
+ return;
}
if (list[component].hasSCU && list[component].usePropsOrContext) {
- continue;
+ return;
}
context.report({
node: list[component].node,
message: 'Component should be written as a pure function'
});
- }
+ });
}
};
})

lib/rules/prop-types.js

@@ -7,22 +7,10 @@
// As for exceptions for props.children or props.className (and alike) look at
// https://github.com/yannickcr/eslint-plugin-react/issues/7
-const has = require('has');
const Components = require('../util/Components');
-const variable = require('../util/variable');
-const annotations = require('../util/annotations');
-const versionUtil = require('../util/version');
-const propsUtil = require('../util/props');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
-// Constants
-// ------------------------------------------------------------------------------
-
-const PROPS_REGEX = /^(props|nextProps)$/;
-const DIRECT_PROPS_REGEX = /^(props|nextProps)\s*(\.|\[)/;
-
-// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
@@ -58,152 +46,14 @@
}]
},
- create: Components.detect((context, components, utils) => {
- const sourceCode = context.getSourceCode();
+ create: Components.detect((context, components) => {
const configuration = context.options[0] || {};
- const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
const ignored = configuration.ignore || [];
- const customValidators = configuration.customValidators || [];
const skipUndeclared = configuration.skipUndeclared || false;
- // Used to track the type annotations in scope.
- // Necessary because babel's scopes do not track type annotations.
- let stack = null;
- const classExpressions = [];
const MISSING_MESSAGE = '\'{{name}}\' is missing in props validation';
/**
- * Helper for accessing the current scope in the stack.
- * @param {string} key The name of the identifier to access. If omitted, returns the full scope.
- * @param {ASTNode} value If provided sets the new value for the identifier.
- * @returns {Object|ASTNode} Either the whole scope or the ASTNode associated with the given identifier.
- */
- function typeScope(key, value) {
- if (arguments.length === 0) {
- return stack[stack.length - 1];
- } else if (arguments.length === 1) {
- return stack[stack.length - 1][key];
- }
- stack[stack.length - 1][key] = value;
- return value;
- }
-
- /**
- * Check if we are in a class constructor
- * @return {boolean} true if we are in a class constructor, false if not
- */
- function inConstructor() {
- let scope = context.getScope();
- while (scope) {
- if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') {
- return true;
- }
- scope = scope.upper;
- }
- return false;
- }
-
- /**
- * Check if we are in a class constructor
- * @return {boolean} true if we are in a class constructor, false if not
- */
- function inComponentWillReceiveProps() {
- let scope = context.getScope();
- while (scope) {
- if (
- scope.block && scope.block.parent &&
- scope.block.parent.key && scope.block.parent.key.name === 'componentWillReceiveProps'
- ) {
- return true;
- }
- scope = scope.upper;
- }
- return false;
- }
-
- /**
- * Check if we are in a class constructor
- * @return {boolean} true if we are in a class constructor, false if not
- */
- function inShouldComponentUpdate() {
- let scope = context.getScope();
- while (scope) {
- if (
- scope.block && scope.block.parent &&
- scope.block.parent.key && scope.block.parent.key.name === 'shouldComponentUpdate'
- ) {
- return true;
- }
- scope = scope.upper;
- }
- return false;
- }
-
- /**
- * Checks if a prop is being assigned a value props.bar = 'bar'
- * @param {ASTNode} node The AST node being checked.
- * @returns {Boolean}
- */
-
- function isAssignmentToProp(node) {
- return (
- node.parent &&
- node.parent.type === 'AssignmentExpression' &&
- node.parent.left === node
- );
- }
-
- /**
- * Checks if we are using a prop
- * @param {ASTNode} node The AST node being checked.
- * @returns {Boolean} True if we are using a prop, false if not.
- */
- function isPropTypesUsage(node) {
- const isClassUsage = (
- (utils.getParentES6Component() || utils.getParentES5Component()) &&
- node.object.type === 'ThisExpression' && node.property.name === 'props'
- );
- const isStatelessFunctionUsage = node.object.name === 'props' && !isAssignmentToProp(node);
- const isNextPropsUsage = node.object.name === 'nextProps' && (inComponentWillReceiveProps() || inShouldComponentUpdate());
- return isClassUsage || isStatelessFunctionUsage || isNextPropsUsage;
- }
-
- /**
- * Checks if we are declaring a `props` class property 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 isAnnotatedClassPropsDeclaration(node) {
- if (node && node.type === 'ClassProperty') {
- const tokens = context.getFirstTokens(node, 2);
- if (
- node.typeAnnotation && (
- tokens[0].value === 'props' ||
- (tokens[1] && tokens[1].value === 'props')
- )
- ) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Checks if we are declaring a props as a generic type in a flow-annotated class.
- *
- * @param {ASTNode} node the AST node being checked.
- * @returns {Boolean} True if the node is a class with generic prop types, false if not.
- */
- function isSuperTypeParameterPropsDeclaration(node) {
- if (node && (node.type === 'ClassDeclaration' || node.type === 'ClassExpression')) {
- if (node.superTypeParameters && node.superTypeParameters.params.length > 0) {
- return true;
- }
- }
- return false;
- }
-
- /**
* Checks if the prop is ignored
* @param {String} name Name of the prop to check.
* @returns {Boolean} True if the prop is ignored, false if not.
@@ -213,15 +63,6 @@
}
/**
- * Checks if prop should be validated by plugin-react-proptypes
- * @param {String} validator Name of validator to check.
- * @returns {Boolean} True if validator should be checked by custom validator.
- */
- function hasCustomValidator(validator) {
- return customValidators.indexOf(validator) !== -1;
- }
-
- /**
* Checks if the component must be validated
* @param {Object} component The component to process
* @returns {Boolean} True if the component must be validated, false if not.
@@ -258,11 +98,11 @@
// If it's a computed property, we can't make any further analysis, but is valid
return key === '__COMPUTED_PROP__';
}
- if (typeof propType === 'object' && Object.keys(propType).length === 0) {
+ if (typeof propType === 'object' && !propType.type) {
return true;
}
// Consider every children as declared
- if (propType.children === true) {
+ if (propType.children === true || propType.containsSpread) {
return true;
}
if (propType.acceptedProperties) {
@@ -319,579 +159,22 @@
}
/**
- * Checks if the prop has spread operator.
- * @param {ASTNode} node The AST node being marked.
- * @returns {Boolean} True if the prop has spread operator, false if not.
- */
- function hasSpreadOperator(node) {
- const tokens = sourceCode.getTokens(node);
- return tokens.length && tokens[0].value === '...';
- }
-
- /**
- * Removes quotes from around an identifier.
- * @param {string} the identifier to strip
- */
- function stripQuotes(string) {
- return string.replace(/^\'|\'$/g, '');
- }
-
- /**
- * Retrieve the name of a key node
- * @param {ASTNode} node The AST node with the key.
- * @return {string} the name of the key
- */
- function getKeyValue(node) {
- if (node.type === 'ObjectTypeProperty') {
- const tokens = context.getFirstTokens(node, 2);
- return (tokens[0].value === '+' || tokens[0].value === '-'
- ? tokens[1].value
- : stripQuotes(tokens[0].value)
- );
- }
- const key = node.key || node.argument;
- return key.type === 'Identifier' ? key.name : key.value;
- }
-
- /**
- * Iterates through a properties node, like a customized forEach.
- * @param {Object[]} properties Array of properties to iterate.
- * @param {Function} fn Function to call on each property, receives property key
- and property value. (key, value) => void
- */
- function iterateProperties(properties, fn) {
- if (properties.length && typeof fn === 'function') {
- for (let i = 0, j = properties.length; i < j; i++) {
- const node = properties[i];
- const key = getKeyValue(node);
-
- const value = node.value;
- fn(key, value);
- }
- }
- }
-
- /**
- * Creates the representation of the React propTypes for the component.
- * The representation is used to verify nested used properties.
- * @param {ASTNode} value Node of the PropTypes for the desired property
- * @return {Object} The representation of the declaration, empty object means
- * the property is declared without the need for further analysis.
- */
- function buildReactDeclarationTypes(value) {
- if (
- value &&
- value.callee &&
- value.callee.object &&
- hasCustomValidator(value.callee.object.name)
- ) {
- return {};
- }
-
- if (
- value &&
- value.type === 'MemberExpression' &&
- value.property &&
- value.property.name &&
- value.property.name === 'isRequired'
- ) {
- value = value.object;
- }
-
- // Verify PropTypes that are functions
- if (
- value &&
- value.type === 'CallExpression' &&
- value.callee &&
- value.callee.property &&
- value.callee.property.name &&
- value.arguments &&
- value.arguments.length > 0
- ) {
- const callName = value.callee.property.name;
- const argument = value.arguments[0];
- switch (callName) {
- case 'shape':
- if (argument.type !== 'ObjectExpression') {
- // Invalid proptype or cannot analyse statically
- return {};
- }
- const shapeTypeDefinition = {
- type: 'shape',
- children: {}
- };
- iterateProperties(argument.properties, (childKey, childValue) => {
- shapeTypeDefinition.children[childKey] = buildReactDeclarationTypes(childValue);
- });
- return shapeTypeDefinition;
- case 'arrayOf':
- case 'objectOf':
- return {
- type: 'object',
- children: {
- __ANY_KEY__: buildReactDeclarationTypes(argument)
- }
- };
- case 'oneOfType':
- if (
- !argument.elements ||
- !argument.elements.length
- ) {
- // Invalid proptype or cannot analyse statically
- return {};
- }
- const unionTypeDefinition = {
- type: 'union',
- children: []
- };
- for (let i = 0, j = argument.elements.length; i < j; i++) {
- const type = buildReactDeclarationTypes(argument.elements[i]);
- // keep only complex type
- if (Object.keys(type).length > 0) {
- if (type.children === true) {
- // every child is accepted for one type, abort type analysis
- unionTypeDefinition.children = true;
- return unionTypeDefinition;
- }
- }
-
- unionTypeDefinition.children.push(type);
- }
- if (unionTypeDefinition.length === 0) {
- // no complex type found, simply accept everything
- return {};
- }
- return unionTypeDefinition;
- case 'instanceOf':
- return {
- type: 'instance',
- // Accept all children because we can't know what type they are
- children: true
- };
- case 'oneOf':
- default:
- return {};
- }
- }
- // Unknown property or accepts everything (any, object, ...)
- return {};
- }
-
- /**
- * Creates the representation of the React props type annotation for the component.
- * The representation is used to verify nested used properties.
- * @param {ASTNode} annotation Type annotation for the props class property.
- * @return {Object} The representation of the declaration, empty object means
- * the property is declared without the need for further analysis.
- */
- function buildTypeAnnotationDeclarationTypes(annotation, seen) {
- if (typeof seen === 'undefined') {
- // Keeps track of annotations we've already seen to
- // prevent problems with recursive types.
- seen = new Set();
- }
- if (seen.has(annotation)) {
- // This must be a recursive type annotation, so just accept anything.
- return {};
- }
- seen.add(annotation);
-
- switch (annotation.type) {
- case 'GenericTypeAnnotation':
- if (typeScope(annotation.id.name)) {
- return buildTypeAnnotationDeclarationTypes(typeScope(annotation.id.name), seen);
- }
- return {};
- case 'ObjectTypeAnnotation':
- let containsObjectTypeSpread = false;
- const shapeTypeDefinition = {
- type: 'shape',
- children: {}
- };
- iterateProperties(annotation.properties, (childKey, childValue) => {
- if (!childKey && !childValue) {
- containsObjectTypeSpread = true;
- } else {
- shapeTypeDefinition.children[childKey] = buildTypeAnnotationDeclarationTypes(childValue, seen);
- }
- });
-
- // nested object type spread means we need to ignore/accept everything in this object
- if (containsObjectTypeSpread) {
- return {};
- }
- return shapeTypeDefinition;
- case 'UnionTypeAnnotation':
- const unionTypeDefinition = {
- type: 'union',
- children: []
- };
- for (let i = 0, j = annotation.types.length; i < j; i++) {
- const type = buildTypeAnnotationDeclarationTypes(annotation.types[i], seen);
- // keep only complex type
- if (Object.keys(type).length > 0) {
- if (type.children === true) {
- // every child is accepted for one type, abort type analysis
- unionTypeDefinition.children = true;
- return unionTypeDefinition;
- }
- }
-
- unionTypeDefinition.children.push(type);
- }
- if (unionTypeDefinition.children.length === 0) {
- // no complex type found, simply accept everything
- return {};
- }
- return unionTypeDefinition;
- case 'ArrayTypeAnnotation':
- return {
- type: 'object',
- children: {
- __ANY_KEY__: buildTypeAnnotationDeclarationTypes(annotation.elementType, seen)
- }
- };
- default:
- // Unknown or accepts everything.
- return {};
- }
- }
-
- /**
- * Retrieve the name of a property node
- * @param {ASTNode} node The AST node with the property.
- * @return {string} the name of the property or undefined if not found
- */
- function getPropertyName(node) {
- const isDirectProp = DIRECT_PROPS_REGEX.test(sourceCode.getText(node));
- const isInClassComponent = utils.getParentES6Component() || utils.getParentES5Component();
- const isNotInConstructor = !inConstructor();
- const isNotInComponentWillReceiveProps = !inComponentWillReceiveProps();
- const isNotInShouldComponentUpdate = !inShouldComponentUpdate();
- if (isDirectProp && isInClassComponent && isNotInConstructor && isNotInComponentWillReceiveProps
- && isNotInShouldComponentUpdate) {
- return void 0;
- }
- if (!isDirectProp) {
- node = node.parent;
- }
- const property = node.property;
- if (property) {
- switch (property.type) {
- case 'Identifier':
- if (node.computed) {
- return '__COMPUTED_PROP__';
- }
- return property.name;
- case 'MemberExpression':
- return void 0;
- case 'Literal':
- // Accept computed properties that are literal strings
- if (typeof property.value === 'string') {
- return property.value;
- }
- // falls through
- default:
- if (node.computed) {
- return '__COMPUTED_PROP__';
- }
- break;
- }
- }
- return void 0;
- }
-
- /**
- * Mark a prop type as used
- * @param {ASTNode} node The AST node being marked.
- */
- function markPropTypesAsUsed(node, parentNames) {
- parentNames = parentNames || [];
- let type;
- let name;
- let allNames;
- let properties;
- switch (node.type) {
- case 'MemberExpression':
- name = getPropertyName(node);
- if (name) {
- allNames = parentNames.concat(name);
- if (node.parent.type === 'MemberExpression') {
- markPropTypesAsUsed(node.parent, allNames);
- }
- // Do not mark computed props as used.
- type = name !== '__COMPUTED_PROP__' ? 'direct' : null;
- } else if (
- node.parent.id &&
- node.parent.id.properties &&
- node.parent.id.properties.length &&
- getKeyValue(node.parent.id.properties[0])
- ) {
- type = 'destructuring';
- properties = node.parent.id.properties;
- }
- break;
- case 'ArrowFunctionExpression':
- case 'FunctionDeclaration':
- case 'FunctionExpression':
- type = 'destructuring';
- properties = node.params[0].properties;
- break;
- case 'MethodDefinition':
- const destructuring = node.value && node.value.params && node.value.params[0] && node.value.params[0].type === 'ObjectPattern';
- if (destructuring) {
- type = 'destructuring';
- properties = node.value.params[0].properties;
- break;
- } else {
- return;
- }
- case 'VariableDeclarator':
- for (let i = 0, j = node.id.properties.length; i < j; i++) {
- // let {props: {firstname}} = this
- const thisDestructuring = (
- !hasSpreadOperator(node.id.properties[i]) &&
- (PROPS_REGEX.test(node.id.properties[i].key.name) || PROPS_REGEX.test(node.id.properties[i].key.value)) &&
- node.id.properties[i].value.type === 'ObjectPattern'
- );
- // let {firstname} = props
- const directDestructuring =
- PROPS_REGEX.test(node.init.name) &&
- (utils.getParentStatelessComponent() || inConstructor() || inComponentWillReceiveProps())
- ;
-
- if (thisDestructuring) {
- properties = node.id.properties[i].value.properties;
- } else if (directDestructuring) {
- properties = node.id.properties;
- } else {
- continue;
- }
- type = 'destructuring';
- break;
- }
- break;
- default:
- throw new Error(`${node.type} ASTNodes are not handled by markPropTypesAsUsed`);
- }
-
- const component = components.get(utils.getParentComponent());
- const usedPropTypes = (component && component.usedPropTypes || []).slice();
-
- switch (type) {
- case 'direct':
- // Ignore Object methods
- if (Object.prototype[name]) {
- break;
- }
-
- const isDirectProp = DIRECT_PROPS_REGEX.test(sourceCode.getText(node));
-
- usedPropTypes.push({
- name: name,
- allNames: allNames,
- node: (
- !isDirectProp && !inConstructor() && !inComponentWillReceiveProps() ?
- node.parent.property :
- node.property
- )
- });
- break;
- case 'destructuring':
- for (let k = 0, l = properties.length; k < l; k++) {
- if (hasSpreadOperator(properties[k]) || properties[k].computed) {
- continue;
- }
- const propName = getKeyValue(properties[k]);
-
- let currentNode = node;
- allNames = [];
- while (currentNode.property && !PROPS_REGEX.test(currentNode.property.name)) {
- allNames.unshift(currentNode.property.name);
- currentNode = currentNode.object;
- }
- allNames.push(propName);
-
- if (propName) {
- usedPropTypes.push({
- name: propName,
- allNames: allNames,
- node: properties[k]
- });
- }
- }
- break;
- default:
- break;
- }
-
- components.set(node, {
- usedPropTypes: usedPropTypes
- });
- }
-
- /**
- * Marks all props found inside ObjectTypeAnnotaiton as declared.
- *
- * Modifies the declaredProperties object
- * @param {ASTNode} propTypes
- * @param {Object} declaredPropTypes
- * @returns {Boolean} True if propTypes should be ignored (e.g. when a type can't be resolved, when it is imported)
- */
- function declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes) {
- let ignorePropsValidation = false;
-
- iterateProperties(propTypes.properties, (key, value) => {
- if (!value) {
- ignorePropsValidation = true;
- return;
- }
-
- declaredPropTypes[key] = buildTypeAnnotationDeclarationTypes(value);
- });
-
- return ignorePropsValidation;
- }
-
- /**
- * Marks all props found inside IntersectionTypeAnnotation as declared.
- * Since InterSectionTypeAnnotations can be nested, this handles recursively.
- *
- * Modifies the declaredPropTypes object
- * @param {ASTNode} propTypes
- * @param {Object} declaredPropTypes
- * @returns {Boolean} True if propTypes should be ignored (e.g. when a type can't be resolved, when it is imported)
- */
- function declarePropTypesForIntersectionTypeAnnotation(propTypes, declaredPropTypes) {
- return propTypes.types.some(annotation => {
- if (annotation.type === 'ObjectTypeAnnotation') {
- return declarePropTypesForObjectTypeAnnotation(annotation, declaredPropTypes);
- }
-
- if (annotation.type === 'UnionTypeAnnotation') {
- return true;
- }
-
- const typeNode = typeScope(annotation.id.name);
-
- if (!typeNode) {
- return true;
- } else if (typeNode.type === 'IntersectionTypeAnnotation') {
- return declarePropTypesForIntersectionTypeAnnotation(typeNode, declaredPropTypes);
- }
-
- return declarePropTypesForObjectTypeAnnotation(typeNode, declaredPropTypes);
- });
- }
-
- /**
- * Mark a prop type as declared
- * @param {ASTNode} node The AST node being checked.
- * @param {propTypes} node The AST node containing the proptypes
- */
- function markPropTypesAsDeclared(node, propTypes) {
- let componentNode = node;
- while (componentNode && !components.get(componentNode)) {
- componentNode = componentNode.parent;
- }
- const component = components.get(componentNode);
- const declaredPropTypes = component && component.declaredPropTypes || {};
- let ignorePropsValidation = false;
-
- switch (propTypes && propTypes.type) {
- case 'ObjectTypeAnnotation':
- ignorePropsValidation = declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes);
- break;
- case 'ObjectExpression':
- iterateProperties(propTypes.properties, (key, value) => {
- if (!value) {
- ignorePropsValidation = true;
- return;
- }
- declaredPropTypes[key] = buildReactDeclarationTypes(value);
- });
- break;
- case 'MemberExpression':
- let curDeclaredPropTypes = declaredPropTypes;
- // Walk the list of properties, until we reach the assignment
- // ie: ClassX.propTypes.a.b.c = ...
- while (
- propTypes &&
- propTypes.parent &&
- propTypes.parent.type !== 'AssignmentExpression' &&
- propTypes.property &&
- curDeclaredPropTypes
- ) {
- const propName = propTypes.property.name;
- if (propName in curDeclaredPropTypes) {
- curDeclaredPropTypes = curDeclaredPropTypes[propName].children;
- propTypes = propTypes.parent;
- } else {
- // This will crash at runtime because we haven't seen this key before
- // stop this and do not declare it
- propTypes = null;
- }
- }
- if (propTypes && propTypes.parent && propTypes.property) {
- curDeclaredPropTypes[propTypes.property.name] =
- buildReactDeclarationTypes(propTypes.parent.right);
- } else {
- ignorePropsValidation = true;
- }
- break;
- case 'Identifier':
- const variablesInScope = variable.variablesInScope(context);
- for (let i = 0, j = variablesInScope.length; i < j; i++) {
- if (variablesInScope[i].name !== propTypes.name) {
- continue;
- }
- const defInScope = variablesInScope[i].defs[variablesInScope[i].defs.length - 1];
- markPropTypesAsDeclared(node, defInScope.node && defInScope.node.init);
- return;
- }
- ignorePropsValidation = true;
- break;
- case 'CallExpression':
- if (
- propWrapperFunctions.has(sourceCode.getText(propTypes.callee)) &&
- propTypes.arguments && propTypes.arguments[0]
- ) {
- markPropTypesAsDeclared(node, propTypes.arguments[0]);
- return;
- }
- break;
- case 'IntersectionTypeAnnotation':
- ignorePropsValidation = declarePropTypesForIntersectionTypeAnnotation(propTypes, declaredPropTypes);
- break;
- case null:
- break;
- default:
- ignorePropsValidation = true;
- break;
- }
-
- components.set(node, {
- declaredPropTypes: declaredPropTypes,
- ignorePropsValidation: ignorePropsValidation
- });
- }
-
- /**
* Reports undeclared proptypes for a given component
* @param {Object} component The component to process
*/
function reportUndeclaredPropTypes(component) {
- let allNames;
for (let i = 0, j = component.usedPropTypes.length; i < j; i++) {
- allNames = component.usedPropTypes[i].allNames;
+ const allNames = component.usedPropTypes[i].allNames;
+ const node = component.usedPropTypes[i].node;
if (
isIgnored(allNames[0]) ||
- isDeclaredInComponent(component.node, allNames)
+ isDeclaredInComponent(component.node, allNames) ||
+ !node
) {
continue;
}
context.report(
- component.usedPropTypes[i].node,
+ node,
MISSING_MESSAGE, {
name: allNames.join('.').replace(/\.__COMPUTED_PROP__/g, '[]')
}
@@ -899,240 +182,17 @@
}
}
- /**
- * Resolve the type annotation for a given node.
- * Flow annotations are sometimes wrapped in outer `TypeAnnotation`
- * and `NullableTypeAnnotation` nodes which obscure the annotation we're
- * interested in.
- * This method also resolves type aliases where possible.
- *
- * @param {ASTNode} node The annotation or a node containing the type annotation.
- * @returns {ASTNode} The resolved type annotation for the node.
- */
- function resolveTypeAnnotation(node) {
- let annotation = node.typeAnnotation || node;
- while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) {
- annotation = annotation.typeAnnotation;
- }
- if (annotation.type === 'GenericTypeAnnotation' && typeScope(annotation.id.name)) {
- return typeScope(annotation.id.name);
- }
- return annotation;
- }
-
- /**
- * Resolve the type annotation for a given class declaration node with superTypeParameters.
- *
- * @param {ASTNode} node The annotation or a node containing the type annotation.
- * @returns {ASTNode} The resolved type annotation for the node.
- */
- function resolveSuperParameterPropsType(node) {
- let propsParameterPosition;
- try {
- // Flow <=0.52 had 3 required TypedParameters of which the second one is the Props.
- // Flow >=0.53 has 2 optional TypedParameters of which the first one is the Props.
- propsParameterPosition = versionUtil.testFlowVersion(context, '0.53.0') ? 0 : 1;
- } catch (e) {
- // In case there is no flow version defined, we can safely assume that when there are 3 Props we are dealing with version <= 0.52
- propsParameterPosition = node.superTypeParameters.params.length <= 2 ? 0 : 1;
- }
-
- let annotation = node.superTypeParameters.params[propsParameterPosition];
- while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) {
- annotation = annotation.typeAnnotation;
- }
-
- if (annotation.type === 'GenericTypeAnnotation' && typeScope(annotation.id.name)) {
- return typeScope(annotation.id.name);
- }
- return annotation;
- }
-
- /**
- * @param {ASTNode} node We expect either an ArrowFunctionExpression,
- * FunctionDeclaration, or FunctionExpression
- */
- function markDestructuredFunctionArgumentsAsUsed(node) {
- const destructuring = node.params && node.params[0] && node.params[0].type === 'ObjectPattern';
- if (destructuring && components.get(node)) {
- markPropTypesAsUsed(node);
- }
- }
-
- /**
- * @param {ASTNode} node We expect either an ArrowFunctionExpression,
- * FunctionDeclaration, or FunctionExpression
- */
- function markAnnotatedFunctionArgumentsAsDeclared(node) {
- if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) {
- return;
- }
- markPropTypesAsDeclared(node, resolveTypeAnnotation(node.params[0]));
- }
-
- /**
- * @param {ASTNode} node We expect either an ArrowFunctionExpression,
- * FunctionDeclaration, or FunctionExpression
- */
- function handleStatelessComponent(node) {
- markDestructuredFunctionArgumentsAsUsed(node);
- markAnnotatedFunctionArgumentsAsDeclared(node);
- }
-
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
- ClassDeclaration: function(node) {
- if (isSuperTypeParameterPropsDeclaration(node)) {
- markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node));
- }
- },
-
- ClassExpression: function(node) {
- // TypeParameterDeclaration need to be added to typeScope in order to handle ClassExpressions.
- // This visitor is executed before TypeParameterDeclaration are scoped, therefore we postpone
- // processing class expressions until when the program exists.
- classExpressions.push(node);
- },
-
- ClassProperty: function(node) {
- if (isAnnotatedClassPropsDeclaration(node)) {
- markPropTypesAsDeclared(node, resolveTypeAnnotation(node));
- } else if (propsUtil.isPropTypesDeclaration(node)) {
- markPropTypesAsDeclared(node, node.value);
- }
- },
-
- VariableDeclarator: function(node) {
- const destructuring = node.init && node.id && node.id.type === 'ObjectPattern';
- // let {props: {firstname}} = this
- const thisDestructuring = destructuring && node.init.type === 'ThisExpression';
- // let {firstname} = props
- const directDestructuring =
- destructuring &&
- PROPS_REGEX.test(node.init.name) &&
- (utils.getParentStatelessComponent() || inConstructor() || inComponentWillReceiveProps())
- ;
-
- if (!thisDestructuring && !directDestructuring) {
- return;
- }
- markPropTypesAsUsed(node);
- },
-
- FunctionDeclaration: handleStatelessComponent,
-
- ArrowFunctionExpression: handleStatelessComponent,
-
- FunctionExpression: function(node) {
- if (node.parent.type === 'MethodDefinition') {
- return;
- }
- handleStatelessComponent(node);
- },
-
- MemberExpression: function(node) {
- let type;
- if (isPropTypesUsage(node)) {
- type = 'usage';
- } else if (propsUtil.isPropTypesDeclaration(node)) {
- type = 'declaration';
- }
-
- switch (type) {
- case 'usage':
- markPropTypesAsUsed(node);
- break;
- case 'declaration':
- const component = utils.getRelatedComponent(node);
- if (!component) {
- return;
- }
- markPropTypesAsDeclared(component.node, node.parent.right || node.parent);
- break;
- default:
- break;
- }
- },
-
- MethodDefinition: function(node) {
- const destructuring = node.value && node.value.params && node.value.params[0] && node.value.params[0].type === 'ObjectPattern';
- if (node.key.name === 'componentWillReceiveProps' && destructuring) {
- markPropTypesAsUsed(node);
- }
-
- if (node.key.name === 'shouldComponentUpdate' && destructuring) {
- markPropTypesAsUsed(node);
- }
-
- if (!node.static || node.kind !== 'get' || !propsUtil.isPropTypesDeclaration(node)) {
- return;
- }
-
- let i = node.value.body.body.length - 1;
- for (; i >= 0; i--) {
- if (node.value.body.body[i].type === 'ReturnStatement') {
- break;
- }
- }
-
- if (i >= 0) {
- markPropTypesAsDeclared(node, node.value.body.body[i].argument);
- }
- },
-
- ObjectExpression: function(node) {
- // Search for the proptypes declaration
- node.properties.forEach(property => {
- if (!propsUtil.isPropTypesDeclaration(property)) {
- return;
- }
- markPropTypesAsDeclared(node, property.value);
- });
- },
-
- TypeAlias: function(node) {
- typeScope(node.id.name, node.right);
- },
-
- TypeParameterDeclaration: function(node) {
- const identifier = node.params[0];
-
- if (identifier.typeAnnotation) {
- typeScope(identifier.name, identifier.typeAnnotation.typeAnnotation);
- }
- },
-
- Program: function() {
- stack = [{}];
- },
-
- BlockStatement: function () {
- stack.push(Object.create(typeScope()));
- },
-
- 'BlockStatement:exit': function () {
- stack.pop();
- },
-
'Program:exit': function() {
- classExpressions.forEach(node => {
- if (isSuperTypeParameterPropsDeclaration(node)) {
- markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node));
- }
- });
-
- stack = null;
const list = components.list();
// Report undeclared proptypes for all classes
- for (const component in list) {
- if (!has(list, component) || !mustBeValidated(list[component])) {
- continue;
- }
+ Object.keys(list).filter(component => mustBeValidated(list[component])).forEach(component => {
reportUndeclaredPropTypes(list[component]);
- }
+ });
}
};
})

lib/rules/require-default-props.js

@@ -4,15 +4,9 @@
*/
'use strict';
-const has = require('has');
const Components = require('../util/Components');
-const variableUtil = require('../util/variable');
-const annotations = require('../util/annotations');
-const astUtil = require('../util/ast');
-const propsUtil = require('../util/props');
const docsUrl = require('../util/docsUrl');
-const QUOTES_REGEX = /^["']|["']$/g;
// ------------------------------------------------------------------------------
// Rule Definition
@@ -37,234 +31,13 @@
}]
},
- create: Components.detect((context, components, utils) => {
- const sourceCode = context.getSourceCode();
- const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
+ create: Components.detect((context, components) => {
const configuration = context.options[0] || {};
const forbidDefaultForRequired = configuration.forbidDefaultForRequired || false;
- /**
- * Try to resolve the node passed in to a variable in the current scope. If the node passed in is not
- * an Identifier, then the node is simply returned.
- * @param {ASTNode} node The node to resolve.
- * @returns {ASTNode|null} Return null if the value could not be resolved, ASTNode otherwise.
- */
- function resolveNodeValue(node) {
- if (node.type === 'Identifier') {
- return variableUtil.findVariableByName(context, node.name);
- }
- if (
- node.type === 'CallExpression' &&
- 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 variableUtil.findVariableByName(context, 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: propsUtil.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 = variableUtil.findVariableByName(context, 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 (astUtil.getPropertyName(node) === 'props' && !!node.typeAnnotation);
- }
-
- /**
- * Reports all propTypes passed in that don't have a defaultProp counterpart.
+ * Reports all propTypes passed in that don't have a defaultProps counterpart.
* @param {Object[]} propTypes List of propTypes to check.
* @param {Object} defaultProps Object of defaultProps to check. Keys are the props names.
* @return {void}
@@ -276,335 +49,45 @@
return;
}
- propTypes.forEach(prop => {
+ Object.keys(propTypes).forEach(propName => {
+ const prop = propTypes[propName];
if (prop.isRequired) {
- if (forbidDefaultForRequired && defaultProps[prop.name]) {
+ if (forbidDefaultForRequired && defaultProps[propName]) {
context.report(
prop.node,
- 'propType "{{name}}" is required and should not have a defaultProp declaration.',
- {name: prop.name}
+ 'propType "{{name}}" is required and should not have a defaultProps declaration.',
+ {name: propName}
);
}
return;
}
- if (defaultProps[prop.name]) {
+ if (defaultProps[propName]) {
return;
}
context.report(
prop.node,
- 'propType "{{name}}" is not required, but has no corresponding defaultProp declaration.',
- {name: prop.name}
+ 'propType "{{name}}" is not required, but has no corresponding defaultProps declaration.',
+ {name: propName}
);
});
}
- /**
- * Extracts a PropType from a TypeAnnotation contained in generic node.
- * @param {ASTNode} node TypeAnnotation node.
- * @returns {Object[]} Array of PropType object representations, to be consumed by `addPropTypesToComponent`.
- */
- function getPropTypesFromGeneric(node) {
- let annotation = resolveGenericTypeAnnotation(node);
-
- if (annotation && annotation.id) {
- annotation = variableUtil.findVariableByName(context, annotation.id.name);
- }
-
- const properties = annotation ? (annotation.properties || []) : [];
-
- 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
- };
- });
- }
-
- function hasPropTypesAsGeneric(node) {
- return node && node.parent && node.parent.type === 'ClassDeclaration';
- }
-
- function handlePropTypesAsGeneric(node) {
- const component = components.get(utils.getParentES6Component());
- if (!component) {
- return;
- }
-
- if (node.params[0]) {
- addPropTypesToComponent(component, getPropTypesFromGeneric(node.params[0], context));
- }
- }
-
// --------------------------------------------------------------------------
// Public API
// --------------------------------------------------------------------------
return {
- MemberExpression: function(node) {
- const isPropType = propsUtil.isPropTypesDeclaration(node);
- const isDefaultProp = propsUtil.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: propsUtil.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 = propsUtil.isPropTypesDeclaration(node);
- const isDefaultProp = propsUtil.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 = astUtil.getPropertyName(node) === 'propTypes';
- const isDefaultProp = astUtil.getPropertyName(node) === 'defaultProps' || astUtil.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 = propsUtil.isPropTypesDeclaration(property);
- const isDefaultProp = propsUtil.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));
- }
- });
- },
-
- // e.g.:
- // type HelloProps = {
- // foo?: string
- // };
- // class Hello extends React.Component<HelloProps> {
- // static defaultProps = {
- // foo: 'default'
- // }
- // render() {
- // return <div>{this.props.foo}</div>;
- // }
- // };
- TypeParameterInstantiation: function(node) {
- if (hasPropTypesAsGeneric(node)) {
- handlePropTypesAsGeneric(node);
- return;
- }
- },
-
- // 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;
- }
-
+ Object.keys(list).filter(component => list[component].declaredPropTypes).forEach(component => {
reportPropTypesWithoutDefault(
- list[component].propTypes,
+ list[component].declaredPropTypes,
list[component].defaultProps || {}
);
- }
+ });
}
};
})

lib/rules/require-optimization.js

@@ -4,7 +4,6 @@
*/
'use strict';
-const has = require('has');
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
@@ -221,12 +220,9 @@
const list = components.list();
// Report missing shouldComponentUpdate for all components
- for (const component in list) {
- if (!has(list, component) || list[component].hasSCU) {
- continue;
- }
+ Object.keys(list).filter(component => !list[component].hasSCU).forEach(component => {
reportMissingOptimization(list[component]);
- }
+ });
}
};
})

lib/rules/require-render-return.js

@@ -4,7 +4,6 @@
*/
'use strict';
-const has = require('has');
const Components = require('../util/Components');
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
@@ -46,7 +45,7 @@
if (astUtil.getPropertyName(properties[i]) !== 'render' || !properties[i].value) {
continue;
}
- return /FunctionExpression$/.test(properties[i].value.type);
+ return astUtil.isFunctionLikeExpression(properties[i].value);
}
return false;
}
@@ -79,20 +78,19 @@
'Program:exit': function() {
const list = components.list();
- for (const component in list) {
+ Object.keys(list).forEach(component => {
if (
- !has(list, component) ||
!hasRenderMethod(list[component].node) ||
list[component].hasReturnStatement ||
(!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node))
) {
- continue;
+ return;
}
context.report({
node: list[component].node,
message: 'Your render method should have return statement'
});
- }
+ });
}
};
})

lib/rules/self-closing-comp.js

@@ -5,6 +5,7 @@
'use strict';
const docsUrl = require('../util/docsUrl');
+const jsxUtil = require('../util/jsx');
// ------------------------------------------------------------------------------
// Rule Definition
@@ -39,20 +40,15 @@
},
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);
+ return node.name && node.name.type === 'JSXIdentifier' && !jsxUtil.isDOMComponent(node);
}
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, ''))
+ (childrens.length === 1 && (childrens[0].type === 'Literal' || childrens[0].type === 'JSXText') && !childrens[0].value.replace(/(?!\xA0)\s/g, ''))
) {
return false;
}
@@ -63,7 +59,7 @@
const configuration = Object.assign({}, optionDefaults, context.options[0]);
return (
configuration.component && isComponent(node) ||
- configuration.html && isTagName(node.name.name)
+ configuration.html && jsxUtil.isDOMComponent(node)
) && !node.selfClosing && !hasChildren(node);
}

lib/rules/sort-comp.js

@@ -8,6 +8,7 @@
const util = require('util');
const Components = require('../util/Components');
+const arrayIncludes = require('array-includes');
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
@@ -131,84 +132,91 @@
* @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 = [];
+ const methodGroupIndexes = [];
+ methodsOrder.forEach((currentGroup, groupIndex) => {
+ if (currentGroup === 'getters') {
if (method.getter) {
- const getterIndex = methodsOrder.indexOf('getters');
- if (getterIndex >= 0) {
- indexes.push(getterIndex);
+ methodGroupIndexes.push(groupIndex);
}
- }
-
+ } else if (currentGroup === 'setters') {
if (method.setter) {
- const setterIndex = methodsOrder.indexOf('setters');
- if (setterIndex >= 0) {
- indexes.push(setterIndex);
+ methodGroupIndexes.push(groupIndex);
}
- }
-
+ } else if (currentGroup === 'type-annotations') {
if (method.typeAnnotation) {
- const annotationIndex = methodsOrder.indexOf('type-annotations');
- if (annotationIndex >= 0) {
- indexes.push(annotationIndex);
+ methodGroupIndexes.push(groupIndex);
}
- }
-
- 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);
- }
- }
- }
-
+ } else if (currentGroup === 'static-methods') {
if (method.static) {
- const staticIndex = methodsOrder.indexOf('static-methods');
- if (staticIndex >= 0) {
- indexes.push(staticIndex);
+ methodGroupIndexes.push(groupIndex);
}
+ } else if (currentGroup === 'instance-variables') {
+ if (method.instanceVariable) {
+ methodGroupIndexes.push(groupIndex);
}
-
- if (indexes.length === 0 && method.instanceVariable) {
- const annotationIndex = methodsOrder.indexOf('instance-variables');
- if (annotationIndex >= 0) {
- indexes.push(annotationIndex);
+ } else if (currentGroup === 'instance-methods') {
+ if (method.instanceMethod) {
+ methodGroupIndexes.push(groupIndex);
+ }
+ } else if (arrayIncludes([
+ 'displayName',
+ 'propTypes',
+ 'contextTypes',
+ 'childContextTypes',
+ 'mixins',
+ 'statics',
+ 'defaultProps',
+ 'constructor',
+ 'getDefaultProps',
+ 'state',
+ 'getInitialState',
+ 'getChildContext',
+ 'getDerivedStateFromProps',
+ 'componentWillMount',
+ 'UNSAFE_componentWillMount',
+ 'componentDidMount',
+ 'componentWillReceiveProps',
+ 'UNSAFE_componentWillReceiveProps',
+ 'shouldComponentUpdate',
+ 'componentWillUpdate',
+ 'UNSAFE_componentWillUpdate',
+ 'getSnapshotBeforeUpdate',
+ 'componentDidUpdate',
+ 'componentDidCatch',
+ 'componentWillUnmount',
+ 'render'
+ ], currentGroup)) {
+ if (currentGroup === method.name) {
+ methodGroupIndexes.push(groupIndex);
}
+ } else {
+ // Is the group a regex?
+ const isRegExp = currentGroup.match(regExpRegExp);
+ if (isRegExp) {
+ const isMatching = new RegExp(isRegExp[1], isRegExp[2]).test(method.name);
+ if (isMatching) {
+ methodGroupIndexes.push(groupIndex);
}
-
- if (indexes.length === 0 && method.instanceMethod) {
- const annotationIndex = methodsOrder.indexOf('instance-methods');
- if (annotationIndex >= 0) {
- indexes.push(annotationIndex);
+ } else if (currentGroup === method.name) {
+ methodGroupIndexes.push(groupIndex);
}
}
+ });
// 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;
- }
- }
- }
+ if (methodGroupIndexes.length === 0) {
+ const everythingElseIndex = methodsOrder.indexOf('everything-else');
+ if (everythingElseIndex !== -1) {
+ methodGroupIndexes.push(everythingElseIndex);
+ } else {
// No matching pattern and no 'everything-else' group
- if (indexes.length === 0) {
- indexes.push(Infinity);
+ methodGroupIndexes.push(Infinity);
+ }
}
- return indexes;
+ return methodGroupIndexes;
}
/**
@@ -385,13 +393,11 @@
instanceVariable: !node.static &&
node.type === 'ClassProperty' &&
node.value &&
- node.value.type !== 'ArrowFunctionExpression' &&
- node.value.type !== 'FunctionExpression',
+ !astUtil.isFunctionLikeExpression(node.value),
instanceMethod: !node.static &&
node.type === 'ClassProperty' &&
node.value &&
- (node.value.type === 'ArrowFunctionExpression' ||
- node.value.type === 'FunctionExpression'),
+ (astUtil.isFunctionLikeExpression(node.value)),
typeAnnotation: !!node.typeAnnotation && node.value === null
}));
@@ -409,6 +415,10 @@
// Loop around the properties a second time (for comparison)
for (k = 0, l = propertiesInfos.length; k < l; k++) {
+ if (i === k) {
+ continue;
+ }
+
propB = propertiesInfos[k];
// Compare the properties order
@@ -434,13 +444,10 @@
return {
'Program:exit': function() {
const list = components.list();
- for (const component in list) {
- if (!has(list, component)) {
- continue;
- }
+ Object.keys(list).forEach(component => {
const properties = astUtil.getComponentProperties(list[component].node);
checkPropsOrder(properties);
- }
+ });
reportErrors();
}

lib/rules/sort-prop-types.js

@@ -6,6 +6,7 @@
const variableUtil = require('../util/variable');
const propsUtil = require('../util/props');
const docsUrl = require('../util/docsUrl');
+const propWrapperUtil = require('../util/propWrapper');
// ------------------------------------------------------------------------------
// Rule Definition
@@ -20,6 +21,8 @@
url: docsUrl('sort-prop-types')
},
+ fixable: 'code',
+
schema: [{
type: 'object',
properties: {
@@ -32,6 +35,10 @@
ignoreCase: {
type: 'boolean'
},
+ // Whether alphabetical sorting should be enforced
+ noSortAlphabetically: {
+ type: 'boolean'
+ },
sortShapeProp: {
type: 'boolean'
}
@@ -46,10 +53,13 @@
const requiredFirst = configuration.requiredFirst || false;
const callbacksLast = configuration.callbacksLast || false;
const ignoreCase = configuration.ignoreCase || false;
+ const noSortAlphabetically = configuration.noSortAlphabetically || false;
const sortShapeProp = configuration.sortShapeProp || false;
- const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
function getKey(node) {
+ if (node.key && node.key.value) {
+ return node.key.value;
+ }
return sourceCode.getText(node.key || node.argument);
}
@@ -71,6 +81,46 @@
);
}
+ function getShapeProperties (node) {
+ return node.arguments && node.arguments[0] && node.arguments[0].properties;
+ }
+
+ function sorter(a, b) {
+ let aKey = getKey(a);
+ let bKey = getKey(b);
+ if (requiredFirst) {
+ if (isRequiredProp(a) && !isRequiredProp(b)) {
+ return -1;
+ }
+ if (!isRequiredProp(a) && isRequiredProp(b)) {
+ return 1;
+ }
+ }
+
+ if (callbacksLast) {
+ if (isCallbackPropName(aKey) && !isCallbackPropName(bKey)) {
+ return 1;
+ }
+ if (!isCallbackPropName(aKey) && isCallbackPropName(bKey)) {
+ return -1;
+ }
+ }
+
+ if (ignoreCase) {
+ aKey = aKey.toLowerCase();
+ bKey = bKey.toLowerCase();
+ }
+
+ if (aKey < bKey) {
+ return -1;
+ }
+ if (aKey > bKey) {
+ return 1;
+ }
+ return 0;
+ }
+
+
/**
* Checks if propTypes declarations are sorted
* @param {Array} declarations The array of AST nodes being checked.
@@ -83,8 +133,50 @@
return;
}
+ function fix(fixer) {
+ function sortInSource(allNodes, source) {
+ const originalSource = source;
+ const nodeGroups = allNodes.reduce((acc, curr) => {
+ if (curr.type === 'ExperimentalSpreadProperty' || curr.type === 'SpreadElement') {
+ acc.push([]);
+ } else {
+ acc[acc.length - 1].push(curr);
+ }
+ return acc;
+ }, [[]]);
+
+ nodeGroups.forEach(nodes => {
+ const sortedAttributes = nodes.slice().sort(sorter);
+
+ for (let i = nodes.length - 1; i >= 0; i--) {
+ const sortedAttr = sortedAttributes[i];
+ const attr = nodes[i];
+ let sortedAttrText = sourceCode.getText(sortedAttr);
+ if (sortShapeProp && isShapeProp(sortedAttr.value)) {
+ const shape = getShapeProperties(sortedAttr.value);
+ if (shape) {
+ const attrSource = sortInSource(
+ shape,
+ originalSource
+ );
+ sortedAttrText = attrSource.slice(sortedAttr.range[0], sortedAttr.range[1]);
+ }
+ }
+ source = `${source.slice(0, attr.range[0])}${sortedAttrText}${source.slice(attr.range[1])}`;
+ }
+ });
+ return source;
+ }
+
+ const source = sortInSource(declarations, context.getSourceCode().getText());
+
+ const rangeStart = declarations[0].range[0];
+ const rangeEnd = declarations[declarations.length - 1].range[1];
+ return fixer.replaceTextRange([rangeStart, rangeEnd], source.slice(rangeStart, rangeEnd));
+ }
+
declarations.reduce((prev, curr, idx, decls) => {
- if (/SpreadProperty$/.test(curr.type)) {
+ if (curr.type === 'ExperimentalSpreadProperty' || curr.type === 'SpreadElement') {
return decls[idx + 1];
}
@@ -109,7 +201,8 @@
// 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'
+ message: 'Required prop types must be listed before all other prop types',
+ fix
});
return curr;
}
@@ -124,16 +217,18 @@
// 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'
+ message: 'Callback prop types must be listed after all other prop types',
+ fix
});
return prev;
}
}
- if (currentPropName < prevPropName) {
+ if (!noSortAlphabetically && currentPropName < prevPropName) {
context.report({
node: curr,
- message: 'Prop types declarations should be sorted alphabetically'
+ message: 'Prop types declarations should be sorted alphabetically',
+ fix
});
return prev;
}
@@ -155,7 +250,7 @@
break;
case 'CallExpression':
const innerNode = node.arguments && node.arguments[0];
- if (propWrapperFunctions.has(node.callee.name) && innerNode) {
+ if (propWrapperUtil.isPropWrapperFunction(context, node.callee.name) && innerNode) {
checkNode(innerNode);
}
break;
@@ -166,7 +261,7 @@
return {
CallExpression: function(node) {
- if (!sortShapeProp || !isShapeProp(node) || (!node.arguments && !node.arguments[0])) {
+ if (!sortShapeProp || !isShapeProp(node) || !(node.arguments && node.arguments[0])) {
return;
}
checkSorted(node.arguments[0].properties);

lib/rules/void-dom-elements-no-children.js

@@ -99,7 +99,7 @@
return;
}
- if (!utils.isReactCreateElement(node)) {
+ if (!utils.isCreateElement(node)) {
return;
}

lib/util/ast.js

@@ -18,27 +18,45 @@
const bodyNodes = (node.value ? node.value.body.body : node.body.body);
- let i = bodyNodes.length - 1;
+ return (function loopNodes(nodes) {
+ let i = nodes.length - 1;
for (; i >= 0; i--) {
- if (bodyNodes[i].type === 'ReturnStatement') {
- return bodyNodes[i];
+ if (nodes[i].type === 'ReturnStatement') {
+ return nodes[i];
+ }
+ if (nodes[i].type === 'SwitchStatement') {
+ let j = nodes[i].cases.length - 1;
+ for (; j >= 0; j--) {
+ return loopNodes(nodes[i].cases[j].consequent);
+ }
}
}
return false;
+ }(bodyNodes));
}
/**
- * Get properties name
+ * Get node with property's name
* @param {Object} node - Property.
- * @returns {String} Property name.
+ * @returns {Object} Property name node.
*/
-function getPropertyName(node) {
+function getPropertyNameNode(node) {
if (node.key || ['MethodDefinition', 'Property'].indexOf(node.type) !== -1) {
- return node.key.name;
+ return node.key;
} else if (node.type === 'MemberExpression') {
- return node.property.name;
+ return node.property;
}
- return '';
+ return null;
+}
+
+/**
+ * Get properties name
+ * @param {Object} node - Property.
+ * @returns {String} Property name.
+ */
+function getPropertyName(node) {
+ const nameNode = getPropertyNameNode(node);
+ return nameNode ? nameNode.name : '';
}
/**
@@ -52,7 +70,6 @@
case 'ClassExpression':
return node.body.body;
case 'ObjectExpression':
- // return node.properties;
return node.properties;
default:
return [];
@@ -59,13 +76,14 @@
}
}
+
/**
- * Checks if the node is the first in its line, excluding whitespace.
+ * Gets the first node in a line from the initial node, excluding whitespace.
* @param {Object} context The node to check
* @param {ASTNode} node The node to check
- * @return {Boolean} true if its the first node in its line
+ * @return {ASTNode} the first node in the line
*/
-function isNodeFirstInLine(context, node) {
+function getFirstNodeInLine(context, node) {
const sourceCode = context.getSourceCode();
let token = node;
let lines;
@@ -78,16 +96,67 @@
token.type === 'JSXText' &&
/^\s*$/.test(lines[lines.length - 1])
);
+ return token;
+}
+/**
+ * Checks if the node is the first in its line, excluding whitespace.
+ * @param {Object} context The node to check
+ * @param {ASTNode} node The node to check
+ * @return {Boolean} true if it's the first node in its line
+ */
+function isNodeFirstInLine(context, node) {
+ const token = getFirstNodeInLine(context, node);
const startLine = node.loc.start.line;
const endLine = token ? token.loc.end.line : -1;
return startLine !== endLine;
}
+/**
+ * Checks if the node is a function or arrow function expression.
+ * @param {Object} context The node to check
+ * @return {Boolean} true if it's a function-like expression
+ */
+function isFunctionLikeExpression(node) {
+ return node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression';
+}
+
+/**
+ * Checks if the node is a function.
+ * @param {Object} context The node to check
+ * @return {Boolean} true if it's a function
+ */
+function isFunction(node) {
+ return node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration';
+}
+
+/**
+ * Checks if the node is an arrow function.
+ * @param {Object} context The node to check
+ * @return {Boolean} true if it's an arrow function
+ */
+function isArrowFunction(node) {
+ return node.type === 'ArrowFunctionExpression';
+}
+
+/**
+ * Checks if the node is a class.
+ * @param {Object} context The node to check
+ * @return {Boolean} true if it's a class
+ */
+function isClass(node) {
+ return node.type === 'ClassDeclaration' || node.type === 'ClassExpression';
+}
module.exports = {
findReturnStatement: findReturnStatement,
+ getFirstNodeInLine: getFirstNodeInLine,
getPropertyName: getPropertyName,
+ getPropertyNameNode: getPropertyNameNode,
getComponentProperties: getComponentProperties,
+ isArrowFunction: isArrowFunction,
+ isClass: isClass,
+ isFunction: isFunction,
+ isFunctionLikeExpression: isFunctionLikeExpression,
isNodeFirstInLine: isNodeFirstInLine
};

lib/util/Components.js

@@ -4,12 +4,17 @@
*/
'use strict';
-const has = require('has');
const util = require('util');
const doctrine = require('doctrine');
+const arrayIncludes = require('array-includes');
+
const variableUtil = require('./variable');
const pragmaUtil = require('./pragma');
const astUtil = require('./ast');
+const propTypesUtil = require('./propTypes');
+const jsxUtil = require('./jsx');
+const usedPropTypesUtil = require('./usedPropTypes');
+const defaultPropsUtil = require('./defaultProps');
function getId(node) {
return node && node.range.join(':');
@@ -75,12 +80,15 @@
* 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
+ * @returns {Object} Component object, undefined if the component is not found or has confidence value of 0.
*/
get(node) {
const id = getId(node);
+ if (this._list[id] && this._list[id].confidence >= 1) {
return this._list[id];
}
+ return null;
+ }
/**
* Update a component in the list
@@ -119,10 +127,7 @@
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;
- }
+ Object.keys(this._list).filter(i => this._list[i].confidence < 2).forEach(i => {
let component = null;
let node = null;
node = this._list[i].node;
@@ -138,21 +143,19 @@
const newUsedProps = (this._list[i].usedPropTypes || []).filter(propType => !propType.node || propType.node.kind !== 'init');
const componentId = getId(component.node);
- usedPropTypes[componentId] = (usedPropTypes[componentId] || []).concat(newUsedProps);
- }
+
+ usedPropTypes[componentId] = mergeUsedPropTypes(usedPropTypes[componentId] || [], 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;
- }
+ Object.keys(this._list).filter(j => this._list[j].confidence >= 2).forEach(j => {
const id = getId(this._list[j].node);
list[j] = this._list[j];
if (usedPropTypes[id]) {
- list[j].usedPropTypes = (list[j].usedPropTypes || []).concat(usedPropTypes[id]);
- }
+ list[j].usedPropTypes = mergeUsedPropTypes(list[j].usedPropTypes || [], usedPropTypes[id]);
}
+ });
return list;
}
@@ -163,14 +166,7 @@
* @returns {Number} Components list length
*/
length() {
- let length = 0;
- for (const i in this._list) {
- if (!has(this._list, i) || this._list[i].confidence < 2) {
- continue;
- }
- length++;
- }
- return length;
+ return Object.keys(this._list).filter(i => this._list[i].confidence >= 2).length;
}
}
@@ -259,34 +255,33 @@
},
/**
- * Check if createElement is destructured from React import
+ * Check if variable is destructured from pragma import
*
- * @returns {Boolean} True if createElement is destructured from React
+ * @param {variable} String The variable name to check
+ * @returns {Boolean} True if createElement is destructured from the pragma
*/
- hasDestructuredReactCreateElement: function() {
+ isDestructuredFromPragmaImport: function(variable) {
const variables = variableUtil.variablesInScope(context);
- const variable = variableUtil.getVariable(variables, 'createElement');
- if (variable) {
- const map = variable.scope.set;
- if (map.has('React')) {
- return true;
- }
+ const variableInScope = variableUtil.getVariable(variables, variable);
+ if (variableInScope) {
+ const map = variableInScope.scope.set;
+ return map.has(pragma);
}
return false;
},
/**
- * Checks to see if node is called within React.createElement
+ * Checks to see if node is called within createElement from pragma
*
* @param {ASTNode} node The AST node being checked.
- * @returns {Boolean} True if React.createElement called
+ * @returns {Boolean} True if createElement called from pragma
*/
- isReactCreateElement: function(node) {
- const calledOnReact = (
+ isCreateElement: function(node) {
+ const calledOnPragma = (
node &&
node.callee &&
node.callee.object &&
- node.callee.object.name === 'React' &&
+ node.callee.object.name === pragma &&
node.callee.property &&
node.callee.property.name === 'createElement'
);
@@ -297,10 +292,10 @@
node.callee.name === 'createElement'
);
- if (this.hasDestructuredReactCreateElement()) {
- return calledDirectly || calledOnReact;
+ if (this.isDestructuredFromPragmaImport('createElement')) {
+ return calledDirectly || calledOnPragma;
}
- return calledOnReact;
+ return calledOnPragma;
},
getReturnPropertyAndNode(ASTnode) {
@@ -346,12 +341,12 @@
const returnsConditionalJSXConsequent =
node[property] &&
node[property].type === 'ConditionalExpression' &&
- node[property].consequent.type === 'JSXElement'
+ jsxUtil.isJSX(node[property].consequent)
;
const returnsConditionalJSXAlternate =
node[property] &&
node[property].type === 'ConditionalExpression' &&
- node[property].alternate.type === 'JSXElement'
+ jsxUtil.isJSX(node[property].alternate)
;
const returnsConditionalJSX =
strict ?
@@ -360,14 +355,14 @@
const returnsJSX =
node[property] &&
- node[property].type === 'JSXElement'
+ jsxUtil.isJSX(node[property])
;
- const returnsReactCreateElement = this.isReactCreateElement(node[property]);
+ const returnsPragmaCreateElement = this.isCreateElement(node[property]);
return Boolean(
returnsConditionalJSX ||
returnsJSX ||
- returnsReactCreateElement
+ returnsPragmaCreateElement
);
},
@@ -400,6 +395,18 @@
return utils.isReturningJSX(ASTNode, strict) || utils.isReturningNull(ASTNode);
},
+ isPragmaComponentWrapper(node) {
+ if (node.type !== 'CallExpression') {
+ return false;
+ }
+ const propertyNames = ['forwardRef', 'memo'];
+ const calleeObject = node.callee.object;
+ if (calleeObject && node.callee.property) {
+ return arrayIncludes(propertyNames, node.callee.property.name) && calleeObject.name === pragma;
+ }
+ return arrayIncludes(propertyNames, node.callee.name) && this.isDestructuredFromPragmaImport(node.callee.name);
+ },
+
/**
* Find a return statment in the current node
*
@@ -463,12 +470,18 @@
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 isArrowFunction = astUtil.isArrowFunction(node);
+ const enclosingScope = isArrowFunction ? utils.getArrowFunctionScope(scope) : scope;
+ const enclosingScopeParent = enclosingScope && enclosingScope.block.parent;
+ const isClass = enclosingScope && astUtil.isClass(enclosingScope.block);
+ const isMethod = enclosingScopeParent && enclosingScopeParent.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';
+ if (isFunction && node.parent && this.isPragmaComponentWrapper(node.parent)) {
+ return node.parent;
+ }
// Stop moving up if we reach a class or an argument (like a callback)
if (isClass || isArgument) {
return null;
@@ -483,6 +496,22 @@
},
/**
+ * Get an enclosing scope used to find `this` value by an arrow function
+ * @param {Scope} scope Current scope
+ * @returns {Scope} An enclosing scope used by an arrow function
+ */
+ getArrowFunctionScope(scope) {
+ scope = scope.upper;
+ while (scope) {
+ if (astUtil.isFunction(scope.block) || astUtil.isClass(scope.block)) {
+ return scope;
+ }
+ scope = scope.upper;
+ }
+ return null;
+ },
+
+ /**
* Get the related component from a node
*
* @param {ASTNode} node The AST node being checked (must be a MemberExpression).
@@ -538,7 +567,7 @@
}
if (refId.type === 'MemberExpression') {
componentNode = refId.parent.right;
- } else if (refId.parent && refId.parent.type === 'VariableDeclarator') {
+ } else if (refId.parent && refId.parent.type === 'VariableDeclarator' && refId.parent.init.type !== 'Identifier') {
componentNode = refId.parent.init;
}
break;
@@ -587,6 +616,15 @@
// Component detection instructions
const detectionInstructions = {
+ CallExpression: function(node) {
+ if (!utils.isPragmaComponentWrapper(node)) {
+ return;
+ }
+ if (node.arguments.length > 0 && astUtil.isFunctionLikeExpression(node.arguments[0])) {
+ components.add(node, 2);
+ }
+ },
+
ClassExpression: function(node) {
if (!utils.isES6Component(node)) {
return;
@@ -692,9 +730,29 @@
// 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 => {
+ const propTypesInstructions = propTypesUtil(context, components, utils);
+ const usedPropTypesInstructions = usedPropTypesUtil(context, components, utils);
+ const defaultPropsInstructions = defaultPropsUtil(context, components, utils);
+ const allKeys = new Set(Object.keys(detectionInstructions).concat(
+ Object.keys(propTypesInstructions),
+ Object.keys(usedPropTypesInstructions),
+ Object.keys(defaultPropsInstructions)
+ ));
+
+ allKeys.forEach(instruction => {
updatedRuleInstructions[instruction] = function(node) {
+ if (instruction in detectionInstructions) {
detectionInstructions[instruction](node);
+ }
+ if (instruction in propTypesInstructions) {
+ propTypesInstructions[instruction](node);
+ }
+ if (instruction in usedPropTypesInstructions) {
+ usedPropTypesInstructions[instruction](node);
+ }
+ if (instruction in defaultPropsInstructions) {
+ defaultPropsInstructions[instruction](node);
+ }
return ruleInstructions[instruction] ? ruleInstructions[instruction](node) : void 0;
};
});

lib/util/defaultProps.js

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

lib/util/error.js

@@ -0,0 +1,14 @@
+'use strict';
+
+/**
+ * Logs out a message if there is no format option set.
+ * @param {String} message - Message to log.
+ */
+function error(message) {
+ if (!/\=-(f|-format)=/.test(process.argv.join('='))) {
+ // eslint-disable-next-line no-console
+ console.error(message);
+ }
+}
+
+module.exports = error;

lib/util/jsx.js

@@ -0,0 +1,40 @@
+/**
+ * @fileoverview Utility functions for JSX
+ */
+'use strict';
+
+const elementType = require('jsx-ast-utils/elementType');
+
+const COMPAT_TAG_REGEX = /^[a-z]|\-/;
+
+/**
+ * Checks if a node represents a DOM element.
+ * @param {object} node - JSXOpeningElement to check.
+ * @returns {boolean} Whether or not the node corresponds to a DOM element.
+ */
+function isDOMComponent(node) {
+ let name = elementType(node);
+
+ // Get namespace if the type is JSXNamespacedName or JSXMemberExpression
+ if (name.indexOf(':') > -1) {
+ name = name.slice(0, name.indexOf(':'));
+ } else if (name.indexOf('.') > -1) {
+ name = name.slice(0, name.indexOf('.'));
+ }
+
+ return COMPAT_TAG_REGEX.test(name);
+}
+
+/**
+ * Checks if a node represents a JSX element or fragment.
+ * @param {object} node - node to check.
+ * @returns {boolean} Whether or not the node if a JSX element or fragment.
+ */
+function isJSX(node) {
+ return node && ['JSXElement', 'JSXFragment'].indexOf(node.type) >= 0;
+}
+
+module.exports = {
+ isDOMComponent: isDOMComponent,
+ isJSX: isJSX
+};

lib/util/log.js

@@ -0,0 +1,14 @@
+'use strict';
+
+/**
+ * Logs out a message if there is no format option set.
+ * @param {String} message - Message to log.
+ */
+function log(message) {
+ if (!/\=-(f|-format)=/.test(process.argv.join('='))) {
+ // eslint-disable-next-line no-console
+ console.log(message);
+ }
+}
+
+module.exports = log;

lib/util/makeNoMethodSetStateRule.js

@@ -10,14 +10,27 @@
// Rule Definition
// ------------------------------------------------------------------------------
-function makeNoMethodSetStateRule(methodName) {
+function mapTitle(methodName) {
+ const map = {
+ componentDidMount: 'did-mount',
+ componentDidUpdate: 'did-update',
+ componentWillUpdate: 'will-update'
+ };
+ const title = map[methodName];
+ if (!title) {
+ throw Error(`No docsUrl for '${methodName}'`);
+ }
+ return `no-${title}-set-state`;
+}
+
+function makeNoMethodSetStateRule(methodName, shouldCheckUnsafeCb) {
return {
meta: {
docs: {
description: `Prevent usage of setState in ${methodName}`,
category: 'Best Practices',
recommended: false,
- url: docsUrl(methodName)
+ url: docsUrl(mapTitle(methodName))
},
schema: [{
@@ -28,6 +41,18 @@
create: function(context) {
const mode = context.options[0] || 'allow-in-func';
+ function nameMatches(name) {
+ if (name === methodName) {
+ return true;
+ }
+
+ if (typeof shouldCheckUnsafeCb === 'function' && shouldCheckUnsafeCb(context)) {
+ return name === `UNSAFE_${methodName}`;
+ }
+
+ return false;
+ }
+
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
@@ -46,19 +71,20 @@
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)) {
+ const ancestor = ancestors[i];
+ if (/Function(Expression|Declaration)$/.test(ancestor.type)) {
depth++;
}
if (
- (ancestors[i].type !== 'Property' && ancestors[i].type !== 'MethodDefinition') ||
- ancestors[i].key.name !== methodName ||
+ (ancestor.type !== 'Property' && ancestor.type !== 'MethodDefinition') ||
+ !nameMatches(ancestor.key.name) ||
(mode !== 'disallow-in-func' && depth > 1)
) {
continue;
}
context.report({
node: callee,
- message: `Do not use setState in ${methodName}`
+ message: `Do not use setState in ${ancestor.key.name}`
});
break;
}

lib/util/pragma.js

@@ -21,6 +21,18 @@
return pragma;
}
+function getFragmentFromContext(context) {
+ let pragma = 'Fragment';
+ // .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings)
+ if (context.settings.react && context.settings.react.fragment) {
+ pragma = context.settings.react.fragment;
+ }
+ if (!JS_IDENTIFIER_REGEX.test(pragma)) {
+ throw new Error(`Fragment pragma ${pragma} is not a valid identifier`);
+ }
+ return pragma;
+}
+
function getFromContext(context) {
let pragma = 'React';
@@ -43,5 +55,6 @@
module.exports = {
getCreateClassFromContext: getCreateClassFromContext,
+ getFragmentFromContext: getFragmentFromContext,
getFromContext: getFromContext
};

lib/util/propTypes.js

@@ -0,0 +1,742 @@
+/**
+ * @fileoverview Common propTypes detection functionality.
+ */
+'use strict';
+
+const annotations = require('./annotations');
+const propsUtil = require('./props');
+const variableUtil = require('./variable');
+const versionUtil = require('./version');
+const propWrapperUtil = require('../util/propWrapper');
+
+/**
+ * Checks if we are declaring a props as a generic type in a flow-annotated class.
+ *
+ * @param {ASTNode} node the AST node being checked.
+ * @returns {Boolean} True if the node is a class with generic prop types, false if not.
+ */
+function isSuperTypeParameterPropsDeclaration(node) {
+ if (node && (node.type === 'ClassDeclaration' || node.type === 'ClassExpression')) {
+ if (node.superTypeParameters && node.superTypeParameters.params.length > 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Removes quotes from around an identifier.
+ * @param {string} the identifier to strip
+ */
+function stripQuotes(string) {
+ return string.replace(/^\'|\'$/g, '');
+}
+
+/**
+ * Retrieve the name of a key node
+ * @param {ASTNode} node The AST node with the key.
+ * @return {string} the name of the key
+ */
+function getKeyValue(context, node) {
+ if (node.type === 'ObjectTypeProperty') {
+ const tokens = context.getFirstTokens(node, 2);
+ return (tokens[0].value === '+' || tokens[0].value === '-'
+ ? tokens[1].value
+ : stripQuotes(tokens[0].value)
+ );
+ }
+ const key = node.key || node.argument;
+ return key.type === 'Identifier' ? key.name : key.value;
+}
+
+/**
+ * Iterates through a properties node, like a customized forEach.
+ * @param {Object[]} properties Array of properties to iterate.
+ * @param {Function} fn Function to call on each property, receives property key
+ and property value. (key, value) => void
+ */
+function iterateProperties(context, properties, fn) {
+ if (properties && properties.length && typeof fn === 'function') {
+ for (let i = 0, j = properties.length; i < j; i++) {
+ const node = properties[i];
+ const key = getKeyValue(context, node);
+
+ const value = node.value;
+ fn(key, value, node);
+ }
+ }
+}
+
+/**
+ * Checks if a node is inside a class body.
+ *
+ * @param {ASTNode} node the AST node being checked.
+ * @returns {Boolean} True if the node has a ClassBody ancestor, false if not.
+ */
+function isInsideClassBody(node) {
+ let parent = node.parent;
+ while (parent) {
+ if (parent.type === 'ClassBody') {
+ return true;
+ }
+ parent = parent.parent;
+ }
+
+ return false;
+}
+
+module.exports = function propTypesInstructions(context, components, utils) {
+ // Used to track the type annotations in scope.
+ // Necessary because babel's scopes do not track type annotations.
+ let stack = null;
+
+ const classExpressions = [];
+ const defaults = {customValidators: []};
+ const configuration = Object.assign({}, defaults, context.options[0] || {});
+ const customValidators = configuration.customValidators;
+ const sourceCode = context.getSourceCode();
+
+ /**
+ * Returns the full scope.
+ * @returns {Object} The whole scope.
+ */
+ function typeScope() {
+ return stack[stack.length - 1];
+ }
+
+ /**
+ * Gets a node from the scope.
+ * @param {string} key The name of the identifier to access.
+ * @returns {ASTNode} The ASTNode associated with the given identifier.
+ */
+ function getInTypeScope(key) {
+ return stack[stack.length - 1][key];
+ }
+
+ /**
+ * Sets the new value in the scope.
+ * @param {string} key The name of the identifier to access
+ * @param {ASTNode} value The new value for the identifier.
+ * @returns {ASTNode} The ASTNode associated with the given identifier.
+ */
+ function setInTypeScope(key, value) {
+ stack[stack.length - 1][key] = value;
+ return value;
+ }
+
+ /**
+ * Checks if prop should be validated by plugin-react-proptypes
+ * @param {String} validator Name of validator to check.
+ * @returns {Boolean} True if validator should be checked by custom validator.
+ */
+ function hasCustomValidator(validator) {
+ return customValidators.indexOf(validator) !== -1;
+ }
+
+ /* eslint-disable no-use-before-define */
+ const typeDeclarationBuilders = {
+ GenericTypeAnnotation: function(annotation, parentName, seen) {
+ if (getInTypeScope(annotation.id.name)) {
+ return buildTypeAnnotationDeclarationTypes(getInTypeScope(annotation.id.name), parentName, seen);
+ }
+ return {};
+ },
+
+ ObjectTypeAnnotation: function(annotation, parentName, seen) {
+ let containsObjectTypeSpread = false;
+ const shapeTypeDefinition = {
+ type: 'shape',
+ children: {}
+ };
+ iterateProperties(context, annotation.properties, (childKey, childValue, propNode) => {
+ const fullName = [parentName, childKey].join('.');
+ if (!childKey && !childValue) {
+ containsObjectTypeSpread = true;
+ } else {
+ const types = buildTypeAnnotationDeclarationTypes(childValue, fullName, seen);
+ types.fullName = fullName;
+ types.name = childKey;
+ types.node = propNode;
+ types.isRequired = !childValue.optional;
+ shapeTypeDefinition.children[childKey] = types;
+ }
+ });
+
+ // Mark if this shape has spread. We will know to consider all props from this shape as having propTypes,
+ // but still have the ability to detect unused children of this shape.
+ shapeTypeDefinition.containsSpread = containsObjectTypeSpread;
+
+ return shapeTypeDefinition;
+ },
+
+ UnionTypeAnnotation: function(annotation, parentName, seen) {
+ const unionTypeDefinition = {
+ type: 'union',
+ children: []
+ };
+ for (let i = 0, j = annotation.types.length; i < j; i++) {
+ const type = buildTypeAnnotationDeclarationTypes(annotation.types[i], parentName, seen);
+ // keep only complex type
+ if (type.type) {
+ if (type.children === true) {
+ // every child is accepted for one type, abort type analysis
+ unionTypeDefinition.children = true;
+ return unionTypeDefinition;
+ }
+ }
+
+ unionTypeDefinition.children.push(type);
+ }
+ if (unionTypeDefinition.children.length === 0) {
+ // no complex type found, simply accept everything
+ return {};
+ }
+ return unionTypeDefinition;
+ },
+
+ ArrayTypeAnnotation: function(annotation, parentName, seen) {
+ const fullName = [parentName, '*'].join('.');
+ const child = buildTypeAnnotationDeclarationTypes(annotation.elementType, fullName, seen);
+ child.fullName = fullName;
+ child.name = '__ANY_KEY__';
+ child.node = annotation;
+ return {
+ type: 'object',
+ children: {
+ __ANY_KEY__: child
+ }
+ };
+ }
+ };
+ /* eslint-enable no-use-before-define */
+
+ /**
+ * Resolve the type annotation for a given node.
+ * Flow annotations are sometimes wrapped in outer `TypeAnnotation`
+ * and `NullableTypeAnnotation` nodes which obscure the annotation we're
+ * interested in.
+ * This method also resolves type aliases where possible.
+ *
+ * @param {ASTNode} node The annotation or a node containing the type annotation.
+ * @returns {ASTNode} The resolved type annotation for the node.
+ */
+ function resolveTypeAnnotation(node) {
+ let annotation = node.typeAnnotation || node;
+ while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) {
+ annotation = annotation.typeAnnotation;
+ }
+ if (annotation.type === 'GenericTypeAnnotation' && getInTypeScope(annotation.id.name)) {
+ return getInTypeScope(annotation.id.name);
+ }
+
+ return annotation;
+ }
+
+ /**
+ * Creates the representation of the React props type annotation for the component.
+ * The representation is used to verify nested used properties.
+ * @param {ASTNode} annotation Type annotation for the props class property.
+ * @return {Object} The representation of the declaration, empty object means
+ * the property is declared without the need for further analysis.
+ */
+ function buildTypeAnnotationDeclarationTypes(annotation, parentName, seen) {
+ if (typeof seen === 'undefined') {
+ // Keeps track of annotations we've already seen to
+ // prevent problems with recursive types.
+ seen = new Set();
+ }
+ if (seen.has(annotation)) {
+ // This must be a recursive type annotation, so just accept anything.
+ return {};
+ }
+ seen.add(annotation);
+
+ if (annotation.type in typeDeclarationBuilders) {
+ return typeDeclarationBuilders[annotation.type](annotation, parentName, seen);
+ }
+ return {};
+ }
+
+ /**
+ * Marks all props found inside ObjectTypeAnnotaiton as declared.
+ *
+ * Modifies the declaredProperties object
+ * @param {ASTNode} propTypes
+ * @param {Object} declaredPropTypes
+ * @returns {Boolean} True if propTypes should be ignored (e.g. when a type can't be resolved, when it is imported)
+ */
+ function declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes) {
+ let ignorePropsValidation = false;
+
+ iterateProperties(context, propTypes.properties, (key, value, propNode) => {
+ if (!value) {
+ ignorePropsValidation = true;
+ return;
+ }
+
+ const types = buildTypeAnnotationDeclarationTypes(value, key);
+ types.fullName = key;
+ types.name = key;
+ types.node = propNode;
+ types.isRequired = !propNode.optional;
+ declaredPropTypes[key] = types;
+ });
+
+ return ignorePropsValidation;
+ }
+
+ /**
+ * Marks all props found inside IntersectionTypeAnnotation as declared.
+ * Since InterSectionTypeAnnotations can be nested, this handles recursively.
+ *
+ * Modifies the declaredPropTypes object
+ * @param {ASTNode} propTypes
+ * @param {Object} declaredPropTypes
+ * @returns {Boolean} True if propTypes should be ignored (e.g. when a type can't be resolved, when it is imported)
+ */
+ function declarePropTypesForIntersectionTypeAnnotation(propTypes, declaredPropTypes) {
+ return propTypes.types.some(annotation => {
+ if (annotation.type === 'ObjectTypeAnnotation') {
+ return declarePropTypesForObjectTypeAnnotation(annotation, declaredPropTypes);
+ }
+
+ if (annotation.type === 'UnionTypeAnnotation') {
+ return true;
+ }
+
+ // Type can't be resolved
+ if (!annotation.id) {
+ return true;
+ }
+
+ const typeNode = getInTypeScope(annotation.id.name);
+
+ if (!typeNode) {
+ return true;
+ } else if (typeNode.type === 'IntersectionTypeAnnotation') {
+ return declarePropTypesForIntersectionTypeAnnotation(typeNode, declaredPropTypes);
+ }
+
+ return declarePropTypesForObjectTypeAnnotation(typeNode, declaredPropTypes);
+ });
+ }
+
+ /**
+ * Creates the representation of the React propTypes for the component.
+ * The representation is used to verify nested used properties.
+ * @param {ASTNode} value Node of the PropTypes for the desired property
+ * @return {Object} The representation of the declaration, empty object means
+ * the property is declared without the need for further analysis.
+ */
+ function buildReactDeclarationTypes(value, parentName) {
+ if (
+ value &&
+ value.callee &&
+ value.callee.object &&
+ hasCustomValidator(value.callee.object.name)
+ ) {
+ return {};
+ }
+
+ if (
+ value &&
+ value.type === 'MemberExpression' &&
+ value.property &&
+ value.property.name &&
+ value.property.name === 'isRequired'
+ ) {
+ value = value.object;
+ }
+
+ // Verify PropTypes that are functions
+ if (
+ value &&
+ value.type === 'CallExpression' &&
+ value.callee &&
+ value.callee.property &&
+ value.callee.property.name &&
+ value.arguments &&
+ value.arguments.length > 0
+ ) {
+ const callName = value.callee.property.name;
+ const argument = value.arguments[0];
+ switch (callName) {
+ case 'shape':
+ if (argument.type !== 'ObjectExpression') {
+ // Invalid proptype or cannot analyse statically
+ return {};
+ }
+ const shapeTypeDefinition = {
+ type: 'shape',
+ children: {}
+ };
+ iterateProperties(context, argument.properties, (childKey, childValue, propNode) => {
+ if (childValue) { // skip spread propTypes
+ const fullName = [parentName, childKey].join('.');
+ const types = buildReactDeclarationTypes(childValue, fullName);
+ types.fullName = fullName;
+ types.name = childKey;
+ types.node = propNode;
+ shapeTypeDefinition.children[childKey] = types;
+ }
+ });
+ return shapeTypeDefinition;
+ case 'arrayOf':
+ case 'objectOf':
+ const fullName = [parentName, '*'].join('.');
+ const child = buildReactDeclarationTypes(argument, fullName);
+ child.fullName = fullName;
+ child.name = '__ANY_KEY__';
+ child.node = argument;
+ return {
+ type: 'object',
+ children: {
+ __ANY_KEY__: child
+ }
+ };
+ case 'oneOfType':
+ if (
+ !argument.elements ||
+ !argument.elements.length
+ ) {
+ // Invalid proptype or cannot analyse statically
+ return {};
+ }
+ const unionTypeDefinition = {
+ type: 'union',
+ children: []
+ };
+ for (let i = 0, j = argument.elements.length; i < j; i++) {
+ const type = buildReactDeclarationTypes(argument.elements[i], parentName);
+ // keep only complex type
+ if (type.type) {
+ if (type.children === true) {
+ // every child is accepted for one type, abort type analysis
+ unionTypeDefinition.children = true;
+ return unionTypeDefinition;
+ }
+ }
+
+ unionTypeDefinition.children.push(type);
+ }
+ if (unionTypeDefinition.length === 0) {
+ // no complex type found, simply accept everything
+ return {};
+ }
+ return unionTypeDefinition;
+ case 'instanceOf':
+ return {
+ type: 'instance',
+ // Accept all children because we can't know what type they are
+ children: true
+ };
+ case 'oneOf':
+ default:
+ return {};
+ }
+ }
+ // Unknown property or accepts everything (any, object, ...)
+ return {};
+ }
+
+
+ /**
+ * Mark a prop type as declared
+ * @param {ASTNode} node The AST node being checked.
+ * @param {propTypes} node The AST node containing the proptypes
+ */
+ function markPropTypesAsDeclared(node, propTypes) {
+ let componentNode = node;
+ while (componentNode && !components.get(componentNode)) {
+ componentNode = componentNode.parent;
+ }
+ const component = components.get(componentNode);
+ const declaredPropTypes = component && component.declaredPropTypes || {};
+ let ignorePropsValidation = component && component.ignorePropsValidation || false;
+ switch (propTypes && propTypes.type) {
+ case 'ObjectTypeAnnotation':
+ ignorePropsValidation = declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes);
+ break;
+ case 'ObjectExpression':
+ iterateProperties(context, propTypes.properties, (key, value, propNode) => {
+ if (!value) {
+ ignorePropsValidation = true;
+ return;
+ }
+ const types = buildReactDeclarationTypes(value, key);
+ types.fullName = key;
+ types.name = key;
+ types.node = propNode;
+ types.isRequired = propsUtil.isRequiredPropType(value);
+ declaredPropTypes[key] = types;
+ });
+ break;
+ case 'MemberExpression':
+ let curDeclaredPropTypes = declaredPropTypes;
+ // Walk the list of properties, until we reach the assignment
+ // ie: ClassX.propTypes.a.b.c = ...
+ while (
+ propTypes &&
+ propTypes.parent &&
+ propTypes.parent.type !== 'AssignmentExpression' &&
+ propTypes.property &&
+ curDeclaredPropTypes
+ ) {
+ const propName = propTypes.property.name;
+ if (propName in curDeclaredPropTypes) {
+ curDeclaredPropTypes = curDeclaredPropTypes[propName].children;
+ propTypes = propTypes.parent;
+ } else {
+ // This will crash at runtime because we haven't seen this key before
+ // stop this and do not declare it
+ propTypes = null;
+ }
+ }
+ if (propTypes && propTypes.parent && propTypes.property) {
+ if (!(propTypes === propTypes.parent.left && propTypes.parent.left.object)) {
+ ignorePropsValidation = true;
+ break;
+ }
+ const parentProp = context.getSource(propTypes.parent.left.object).replace(/^.*\.propTypes\./, '');
+ const types = buildReactDeclarationTypes(
+ propTypes.parent.right,
+ parentProp
+ );
+
+ types.name = propTypes.property.name;
+ types.fullName = [parentProp, propTypes.property.name].join('.');
+ types.node = propTypes.parent;
+ types.isRequired = propsUtil.isRequiredPropType(propTypes.parent.right);
+ curDeclaredPropTypes[propTypes.property.name] = types;
+ } else {
+ let isUsedInPropTypes = false;
+ let n = propTypes;
+ while (n) {
+ if (n.type === 'AssignmentExpression' && propsUtil.isPropTypesDeclaration(n.left) ||
+ (n.type === 'ClassProperty' || n.type === 'Property') && propsUtil.isPropTypesDeclaration(n)) {
+ // Found a propType used inside of another propType. This is not considered usage, we'll still validate
+ // this component.
+ isUsedInPropTypes = true;
+ break;
+ }
+ n = n.parent;
+ }
+ if (!isUsedInPropTypes) {
+ ignorePropsValidation = true;
+ }
+ }
+ break;
+ case 'Identifier':
+ const variablesInScope = variableUtil.variablesInScope(context);
+ for (let i = 0, j = variablesInScope.length; i < j; i++) {
+ if (variablesInScope[i].name !== propTypes.name) {
+ continue;
+ }
+ const defInScope = variablesInScope[i].defs[variablesInScope[i].defs.length - 1];
+ markPropTypesAsDeclared(node, defInScope.node && defInScope.node.init);
+ return;
+ }
+ ignorePropsValidation = true;
+ break;
+ case 'CallExpression':
+ if (
+ propWrapperUtil.isPropWrapperFunction(context, sourceCode.getText(propTypes.callee)) &&
+ propTypes.arguments && propTypes.arguments[0]
+ ) {
+ markPropTypesAsDeclared(node, propTypes.arguments[0]);
+ return;
+ }
+ break;
+ case 'IntersectionTypeAnnotation':
+ ignorePropsValidation = declarePropTypesForIntersectionTypeAnnotation(propTypes, declaredPropTypes);
+ break;
+ case null:
+ break;
+ default:
+ ignorePropsValidation = true;
+ break;
+ }
+
+ components.set(node, {
+ declaredPropTypes: declaredPropTypes,
+ ignorePropsValidation: ignorePropsValidation
+ });
+ }
+
+ /**
+ * @param {ASTNode} node We expect either an ArrowFunctionExpression,
+ * FunctionDeclaration, or FunctionExpression
+ */
+ function markAnnotatedFunctionArgumentsAsDeclared(node) {
+ if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) {
+ return;
+ }
+
+ if (isInsideClassBody(node)) {
+ return;
+ }
+
+ const param = node.params[0];
+ if (param.typeAnnotation && param.typeAnnotation.typeAnnotation && param.typeAnnotation.typeAnnotation.type === 'UnionTypeAnnotation') {
+ param.typeAnnotation.typeAnnotation.types.forEach(annotation => {
+ if (annotation.type === 'GenericTypeAnnotation') {
+ markPropTypesAsDeclared(node, resolveTypeAnnotation(annotation));
+ } else {
+ markPropTypesAsDeclared(node, annotation);
+ }
+ });
+ } else {
+ markPropTypesAsDeclared(node, resolveTypeAnnotation(param));
+ }
+ }
+
+ /**
+ * Resolve the type annotation for a given class declaration node with superTypeParameters.
+ *
+ * @param {ASTNode} node The annotation or a node containing the type annotation.
+ * @returns {ASTNode} The resolved type annotation for the node.
+ */
+ function resolveSuperParameterPropsType(node) {
+ let propsParameterPosition;
+ try {
+ // Flow <=0.52 had 3 required TypedParameters of which the second one is the Props.
+ // Flow >=0.53 has 2 optional TypedParameters of which the first one is the Props.
+ propsParameterPosition = versionUtil.testFlowVersion(context, '0.53.0') ? 0 : 1;
+ } catch (e) {
+ // In case there is no flow version defined, we can safely assume that when there are 3 Props we are dealing with version <= 0.52
+ propsParameterPosition = node.superTypeParameters.params.length <= 2 ? 0 : 1;
+ }
+
+ let annotation = node.superTypeParameters.params[propsParameterPosition];
+ while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) {
+ annotation = annotation.typeAnnotation;
+ }
+
+ if (annotation.type === 'GenericTypeAnnotation' && getInTypeScope(annotation.id.name)) {
+ return getInTypeScope(annotation.id.name);
+ }
+ return annotation;
+ }
+
+ /**
+ * Checks if we are declaring a `props` class property 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 isAnnotatedClassPropsDeclaration(node) {
+ if (node && node.type === 'ClassProperty') {
+ const tokens = context.getFirstTokens(node, 2);
+ if (
+ node.typeAnnotation && (
+ tokens[0].value === 'props' ||
+ (tokens[1] && tokens[1].value === 'props')
+ )
+ ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ return {
+ ClassExpression: function(node) {
+ // TypeParameterDeclaration need to be added to typeScope in order to handle ClassExpressions.
+ // This visitor is executed before TypeParameterDeclaration are scoped, therefore we postpone
+ // processing class expressions until when the program exists.
+ classExpressions.push(node);
+ },
+
+ ClassDeclaration: function(node) {
+ if (isSuperTypeParameterPropsDeclaration(node)) {
+ markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node));
+ }
+ },
+
+ ClassProperty: function(node) {
+ if (isAnnotatedClassPropsDeclaration(node)) {
+ markPropTypesAsDeclared(node, resolveTypeAnnotation(node));
+ } else if (propsUtil.isPropTypesDeclaration(node)) {
+ markPropTypesAsDeclared(node, node.value);
+ }
+ },
+
+ ObjectExpression: function(node) {
+ // Search for the proptypes declaration
+ node.properties.forEach(property => {
+ if (!propsUtil.isPropTypesDeclaration(property)) {
+ return;
+ }
+ markPropTypesAsDeclared(node, property.value);
+ });
+ },
+
+ FunctionExpression: function(node) {
+ if (node.parent.type !== 'MethodDefinition') {
+ markAnnotatedFunctionArgumentsAsDeclared(node);
+ }
+ },
+
+ FunctionDeclaration: markAnnotatedFunctionArgumentsAsDeclared,
+
+ ArrowFunctionExpression: markAnnotatedFunctionArgumentsAsDeclared,
+
+ MemberExpression: function(node) {
+ if (propsUtil.isPropTypesDeclaration(node)) {
+ const component = utils.getRelatedComponent(node);
+ if (!component) {
+ return;
+ }
+ markPropTypesAsDeclared(component.node, node.parent.right || node.parent);
+ }
+ },
+
+ MethodDefinition: function(node) {
+ if (!node.static || node.kind !== 'get' || !propsUtil.isPropTypesDeclaration(node)) {
+ return;
+ }
+
+ let i = node.value.body.body.length - 1;
+ for (; i >= 0; i--) {
+ if (node.value.body.body[i].type === 'ReturnStatement') {
+ break;
+ }
+ }
+
+ if (i >= 0) {
+ markPropTypesAsDeclared(node, node.value.body.body[i].argument);
+ }
+ },
+
+ TypeAlias: function(node) {
+ setInTypeScope(node.id.name, node.right);
+ },
+
+ TypeParameterDeclaration: function(node) {
+ const identifier = node.params[0];
+
+ if (identifier.typeAnnotation) {
+ setInTypeScope(identifier.name, identifier.typeAnnotation.typeAnnotation);
+ }
+ },
+
+ Program: function() {
+ stack = [{}];
+ },
+
+ BlockStatement: function () {
+ stack.push(Object.create(typeScope()));
+ },
+
+ 'BlockStatement:exit': function () {
+ stack.pop();
+ },
+
+ 'Program:exit': function() {
+ classExpressions.forEach(node => {
+ if (isSuperTypeParameterPropsDeclaration(node)) {
+ markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node));
+ }
+ });
+ }
+ };
+};

lib/util/propWrapper.js

@@ -0,0 +1,27 @@
+/**
+ * @fileoverview Utility functions for propWrapperFunctions setting
+ */
+'use strict';
+
+function getPropWrapperFunctions(context) {
+ return new Set(context.settings.propWrapperFunctions || []);
+}
+
+function isPropWrapperFunction(context, name) {
+ if (typeof name !== 'string') {
+ return false;
+ }
+ const propWrapperFunctions = getPropWrapperFunctions(context);
+ const splitName = name.split('.');
+ return Array.from(propWrapperFunctions).some(func => {
+ if (splitName.length === 2 && func.object === splitName[0] && func.property === splitName[1]) {
+ return true;
+ }
+ return name === func || func.property === name;
+ });
+}
+
+module.exports = {
+ getPropWrapperFunctions: getPropWrapperFunctions,
+ isPropWrapperFunction: isPropWrapperFunction
+};

lib/util/usedPropTypes.js

@@ -0,0 +1,525 @@
+/**
+ * @fileoverview Common used propTypes detection functionality.
+ */
+'use strict';
+
+const astUtil = require('./ast');
+const versionUtil = require('./version');
+
+// ------------------------------------------------------------------------------
+// Constants
+// ------------------------------------------------------------------------------
+
+const DIRECT_PROPS_REGEX = /^props\s*(\.|\[)/;
+const DIRECT_NEXT_PROPS_REGEX = /^nextProps\s*(\.|\[)/;
+const DIRECT_PREV_PROPS_REGEX = /^prevProps\s*(\.|\[)/;
+const LIFE_CYCLE_METHODS = ['componentWillReceiveProps', 'shouldComponentUpdate', 'componentWillUpdate', 'componentDidUpdate'];
+const ASYNC_SAFE_LIFE_CYCLE_METHODS = ['getDerivedStateFromProps', 'getSnapshotBeforeUpdate', 'UNSAFE_componentWillReceiveProps', 'UNSAFE_componentWillUpdate'];
+
+/**
+ * Checks if a prop init name matches common naming patterns
+ * @param {ASTNode} node The AST node being checked.
+ * @returns {Boolean} True if the prop name matches
+ */
+function isPropAttributeName (node) {
+ return (
+ node.init.name === 'props' ||
+ node.init.name === 'nextProps' ||
+ node.init.name === 'prevProps'
+ );
+}
+
+/**
+ * Checks if the component must be validated
+ * @param {Object} component The component to process
+ * @returns {Boolean} True if the component must be validated, false if not.
+ */
+function mustBeValidated(component) {
+ return !!(component && !component.ignorePropsValidation);
+}
+
+module.exports = function usedPropTypesInstructions(context, components, utils) {
+ const sourceCode = context.getSourceCode();
+ const checkAsyncSafeLifeCycles = versionUtil.testReactVersion(context, '16.3.0');
+
+ /**
+ * Check if we are in a class constructor
+ * @return {boolean} true if we are in a class constructor, false if not
+ */
+ function inComponentWillReceiveProps() {
+ let scope = context.getScope();
+ while (scope) {
+ if (
+ scope.block
+ && scope.block.parent
+ && scope.block.parent.key
+ && scope.block.parent.key.name === 'componentWillReceiveProps'
+ ) {
+ return true;
+ }
+ scope = scope.upper;
+ }
+ return false;
+ }
+
+ /**
+ * Check if we are in a lifecycle method
+ * @return {boolean} true if we are in a class constructor, false if not
+ **/
+ function inLifeCycleMethod() {
+ let scope = context.getScope();
+ while (scope) {
+ if (scope.block && scope.block.parent && scope.block.parent.key) {
+ const name = scope.block.parent.key.name;
+
+ if (LIFE_CYCLE_METHODS.indexOf(name) >= 0) {
+ return true;
+ }
+ if (checkAsyncSafeLifeCycles && ASYNC_SAFE_LIFE_CYCLE_METHODS.indexOf(name) >= 0) {
+ return true;
+ }
+ }
+ scope = scope.upper;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the given node is a React Component lifecycle method
+ * @param {ASTNode} node The AST node being checked.
+ * @return {Boolean} True if the node is a lifecycle method
+ */
+ function isNodeALifeCycleMethod(node) {
+ const nodeKeyName = (node.key || {}).name;
+
+ if (node.kind === 'constructor') {
+ return true;
+ }
+ if (LIFE_CYCLE_METHODS.indexOf(nodeKeyName) >= 0) {
+ return true;
+ }
+ if (checkAsyncSafeLifeCycles && ASYNC_SAFE_LIFE_CYCLE_METHODS.indexOf(nodeKeyName) >= 0) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the given node is inside a React Component lifecycle
+ * method.
+ * @param {ASTNode} node The AST node being checked.
+ * @return {Boolean} True if the node is inside a lifecycle method
+ */
+ function isInLifeCycleMethod(node) {
+ if ((node.type === 'MethodDefinition' || node.type === 'Property') && isNodeALifeCycleMethod(node)) {
+ return true;
+ }
+
+ if (node.parent) {
+ return isInLifeCycleMethod(node.parent);
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if the current node is in a setState updater method
+ * @return {boolean} true if we are in a setState updater, false if not
+ */
+ function inSetStateUpdater() {
+ let scope = context.getScope();
+ while (scope) {
+ if (
+ scope.block && scope.block.parent
+ && scope.block.parent.type === 'CallExpression'
+ && scope.block.parent.callee.property
+ && scope.block.parent.callee.property.name === 'setState'
+ // Make sure we are in the updater not the callback
+ && scope.block.parent.arguments[0].start === scope.block.start
+ ) {
+ return true;
+ }
+ scope = scope.upper;
+ }
+ return false;
+ }
+
+ function isPropArgumentInSetStateUpdater(node) {
+ let scope = context.getScope();
+ while (scope) {
+ if (
+ scope.block && scope.block.parent
+ && scope.block.parent.type === 'CallExpression'
+ && scope.block.parent.callee.property
+ && scope.block.parent.callee.property.name === 'setState'
+ // Make sure we are in the updater not the callback
+ && scope.block.parent.arguments[0].start === scope.block.start
+ && scope.block.parent.arguments[0].params
+ && scope.block.parent.arguments[0].params.length > 1
+ ) {
+ return scope.block.parent.arguments[0].params[1].name === node.object.name;
+ }
+ scope = scope.upper;
+ }
+ return false;
+ }
+
+ /**
+ * Checks if the prop has spread operator.
+ * @param {ASTNode} node The AST node being marked.
+ * @returns {Boolean} True if the prop has spread operator, false if not.
+ */
+ function hasSpreadOperator(node) {
+ const tokens = sourceCode.getTokens(node);
+ return tokens.length && tokens[0].value === '...';
+ }
+
+ /**
+ * Removes quotes from around an identifier.
+ * @param {string} the identifier to strip
+ */
+ function stripQuotes(string) {
+ return string.replace(/^\'|\'$/g, '');
+ }
+
+ /**
+ * Retrieve the name of a key node
+ * @param {ASTNode} node The AST node with the key.
+ * @return {string} the name of the key
+ */
+ function getKeyValue(node) {
+ if (node.type === 'ObjectTypeProperty') {
+ const tokens = context.getFirstTokens(node, 2);
+ return (tokens[0].value === '+' || tokens[0].value === '-'
+ ? tokens[1].value
+ : stripQuotes(tokens[0].value)
+ );
+ }
+ const key = node.key || node.argument;
+ return key.type === 'Identifier' ? key.name : key.value;
+ }
+
+ /**
+ * Check if we are in a class constructor
+ * @return {boolean} true if we are in a class constructor, false if not
+ */
+ function inConstructor() {
+ let scope = context.getScope();
+ while (scope) {
+ if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') {
+ return true;
+ }
+ scope = scope.upper;
+ }
+ return false;
+ }
+
+ /**
+ * Retrieve the name of a property node
+ * @param {ASTNode} node The AST node with the property.
+ * @return {string} the name of the property or undefined if not found
+ */
+ function getPropertyName(node) {
+ const isDirectProp = DIRECT_PROPS_REGEX.test(sourceCode.getText(node));
+ const isDirectNextProp = DIRECT_NEXT_PROPS_REGEX.test(sourceCode.getText(node));
+ const isDirectPrevProp = DIRECT_PREV_PROPS_REGEX.test(sourceCode.getText(node));
+ const isDirectSetStateProp = isPropArgumentInSetStateUpdater(node);
+ const isInClassComponent = utils.getParentES6Component() || utils.getParentES5Component();
+ const isNotInConstructor = !inConstructor(node);
+ const isNotInLifeCycleMethod = !inLifeCycleMethod();
+ const isNotInSetStateUpdater = !inSetStateUpdater();
+ if ((isDirectProp || isDirectNextProp || isDirectPrevProp || isDirectSetStateProp)
+ && isInClassComponent
+ && isNotInConstructor
+ && isNotInLifeCycleMethod
+ && isNotInSetStateUpdater
+ ) {
+ return void 0;
+ }
+ if (!isDirectProp && !isDirectNextProp && !isDirectPrevProp && !isDirectSetStateProp) {
+ node = node.parent;
+ }
+ const property = node.property;
+ if (property) {
+ switch (property.type) {
+ case 'Identifier':
+ if (node.computed) {
+ return '__COMPUTED_PROP__';
+ }
+ return property.name;
+ case 'MemberExpression':
+ return void 0;
+ case 'Literal':
+ // Accept computed properties that are literal strings
+ if (typeof property.value === 'string') {
+ return property.value;
+ }
+ // falls through
+ default:
+ if (node.computed) {
+ return '__COMPUTED_PROP__';
+ }
+ break;
+ }
+ }
+ return void 0;
+ }
+
+ /**
+ * Checks if a prop is being assigned a value props.bar = 'bar'
+ * @param {ASTNode} node The AST node being checked.
+ * @returns {Boolean}
+ */
+ function isAssignmentToProp(node) {
+ return (
+ node.parent &&
+ node.parent.type === 'AssignmentExpression' &&
+ node.parent.left === node
+ );
+ }
+
+ /**
+ * Checks if we are using a prop
+ * @param {ASTNode} node The AST node being checked.
+ * @returns {Boolean} True if we are using a prop, false if not.
+ */
+ function isPropTypesUsage(node) {
+ const isThisPropsUsage = node.object.type === 'ThisExpression' && node.property.name === 'props';
+ const isPropsUsage = isThisPropsUsage || node.object.name === 'nextProps' || node.object.name === 'prevProps';
+ const isClassUsage = (
+ (utils.getParentES6Component() || utils.getParentES5Component()) &&
+ (isThisPropsUsage || isPropArgumentInSetStateUpdater(node))
+ );
+ const isStatelessFunctionUsage = node.object.name === 'props' && !isAssignmentToProp(node);
+ return isClassUsage || isStatelessFunctionUsage || (isPropsUsage && inLifeCycleMethod());
+ }
+
+ /**
+ * Mark a prop type as used
+ * @param {ASTNode} node The AST node being marked.
+ */
+ function markPropTypesAsUsed(node, parentNames) {
+ parentNames = parentNames || [];
+ let type;
+ let name;
+ let allNames;
+ let properties;
+ switch (node.type) {
+ case 'MemberExpression':
+ name = getPropertyName(node);
+ if (name) {
+ allNames = parentNames.concat(name);
+ if (node.parent.type === 'MemberExpression') {
+ markPropTypesAsUsed(node.parent, allNames);
+ }
+ // Do not mark computed props as used.
+ type = name !== '__COMPUTED_PROP__' ? 'direct' : null;
+ } else if (
+ node.parent.id &&
+ node.parent.id.properties &&
+ node.parent.id.properties.length &&
+ getKeyValue(node.parent.id.properties[0])
+ ) {
+ type = 'destructuring';
+ properties = node.parent.id.properties;
+ }
+ break;
+ case 'ArrowFunctionExpression':
+ case 'FunctionDeclaration':
+ case 'FunctionExpression':
+ if (node.params.length === 0) {
+ break;
+ }
+ type = 'destructuring';
+ properties = node.params[0].properties;
+ if (inSetStateUpdater()) {
+ properties = node.params[1].properties;
+ }
+ break;
+ case 'VariableDeclarator':
+ for (let i = 0, j = node.id.properties.length; i < j; i++) {
+ // let {props: {firstname}} = this
+ const thisDestructuring = (
+ node.id.properties[i].key && (
+ (node.id.properties[i].key.name === 'props' || node.id.properties[i].key.value === 'props') &&
+ node.id.properties[i].value.type === 'ObjectPattern'
+ )
+ );
+ // let {firstname} = props
+ const genericDestructuring = isPropAttributeName(node) && (
+ utils.getParentStatelessComponent() ||
+ isInLifeCycleMethod(node)
+ );
+
+ if (thisDestructuring) {
+ properties = node.id.properties[i].value.properties;
+ } else if (genericDestructuring) {
+ properties = node.id.properties;
+ } else {
+ continue;
+ }
+ type = 'destructuring';
+ break;
+ }
+ break;
+ default:
+ throw new Error(`${node.type} ASTNodes are not handled by markPropTypesAsUsed`);
+ }
+
+ const component = components.get(utils.getParentComponent());
+ const usedPropTypes = component && component.usedPropTypes || [];
+ let ignoreUnusedPropTypesValidation = component && component.ignoreUnusedPropTypesValidation || false;
+
+ switch (type) {
+ case 'direct':
+ // Ignore Object methods
+ if (name in Object.prototype) {
+ break;
+ }
+
+ const nodeSource = sourceCode.getText(node);
+ const isDirectProp = DIRECT_PROPS_REGEX.test(nodeSource)
+ || DIRECT_NEXT_PROPS_REGEX.test(nodeSource)
+ || DIRECT_PREV_PROPS_REGEX.test(nodeSource);
+ const reportedNode = (
+ !isDirectProp && !inConstructor() && !inComponentWillReceiveProps() ?
+ node.parent.property :
+ node.property
+ );
+ usedPropTypes.push({
+ name: name,
+ allNames: allNames,
+ node: reportedNode
+ });
+ break;
+ case 'destructuring':
+ for (let k = 0, l = (properties || []).length; k < l; k++) {
+ if (hasSpreadOperator(properties[k]) || properties[k].computed) {
+ ignoreUnusedPropTypesValidation = true;
+ break;
+ }
+ const propName = getKeyValue(properties[k]);
+
+ let currentNode = node;
+ allNames = [];
+ while (currentNode.property && currentNode.property.name !== 'props') {
+ allNames.unshift(currentNode.property.name);
+ currentNode = currentNode.object;
+ }
+ allNames.push(propName);
+ if (propName) {
+ usedPropTypes.push({
+ allNames: allNames,
+ name: propName,
+ node: properties[k]
+ });
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ components.set(component ? component.node : node, {
+ usedPropTypes: usedPropTypes,
+ ignoreUnusedPropTypesValidation: ignoreUnusedPropTypesValidation
+ });
+ }
+
+ /**
+ * @param {ASTNode} node We expect either an ArrowFunctionExpression,
+ * FunctionDeclaration, or FunctionExpression
+ */
+ function markDestructuredFunctionArgumentsAsUsed(node) {
+ const destructuring = node.params && node.params[0] && node.params[0].type === 'ObjectPattern';
+ if (destructuring && (components.get(node) || components.get(node.parent))) {
+ markPropTypesAsUsed(node);
+ }
+ }
+
+ function handleSetStateUpdater(node) {
+ if (!node.params || node.params.length < 2 || !inSetStateUpdater()) {
+ return;
+ }
+ markPropTypesAsUsed(node);
+ }
+
+ /**
+ * Handle both stateless functions and setState updater functions.
+ * @param {ASTNode} node We expect either an ArrowFunctionExpression,
+ * FunctionDeclaration, or FunctionExpression
+ */
+ function handleFunctionLikeExpressions(node) {
+ handleSetStateUpdater(node);
+ markDestructuredFunctionArgumentsAsUsed(node);
+ }
+
+ function handleCustomValidators(component) {
+ const propTypes = component.declaredPropTypes;
+ if (!propTypes) {
+ return;
+ }
+
+ Object.keys(propTypes).forEach(key => {
+ const node = propTypes[key].node;
+
+ if (node.value && astUtil.isFunctionLikeExpression(node.value)) {
+ markPropTypesAsUsed(node.value);
+ }
+ });
+ }
+
+ return {
+ VariableDeclarator: function(node) {
+ const destructuring = node.init && node.id && node.id.type === 'ObjectPattern';
+ // let {props: {firstname}} = this
+ const thisDestructuring = destructuring && node.init.type === 'ThisExpression';
+ // let {firstname} = props
+ const statelessDestructuring = destructuring && isPropAttributeName(node) && (
+ utils.getParentStatelessComponent() ||
+ isInLifeCycleMethod(node)
+ );
+
+ if (!thisDestructuring && !statelessDestructuring) {
+ return;
+ }
+ markPropTypesAsUsed(node);
+ },
+
+ FunctionDeclaration: handleFunctionLikeExpressions,
+
+ ArrowFunctionExpression: handleFunctionLikeExpressions,
+
+ FunctionExpression: handleFunctionLikeExpressions,
+
+ JSXSpreadAttribute: function(node) {
+ const component = components.get(utils.getParentComponent());
+ components.set(component ? component.node : node, {
+ ignoreUnusedPropTypesValidation: true
+ });
+ },
+
+ MemberExpression: function(node) {
+ if (isPropTypesUsage(node)) {
+ markPropTypesAsUsed(node);
+ }
+ },
+
+ ObjectPattern: function(node) {
+ // If the object pattern is a destructured props object in a lifecycle
+ // method -- mark it for used props.
+ if (isNodeALifeCycleMethod(node.parent.parent) && node.properties.length > 0) {
+ markPropTypesAsUsed(node.parent);
+ }
+ },
+
+ 'Program:exit': function() {
+ const list = components.list();
+
+ Object.keys(list).filter(component => mustBeValidated(list[component])).forEach(component => {
+ handleCustomValidators(list[component]);
+ });
+ }
+ };
+};

lib/util/version.js

@@ -4,11 +4,43 @@
*/
'use strict';
+const resolve = require('resolve');
+const error = require('./error');
+
+let warnedForMissingVersion = false;
+
+function detectReactVersion() {
+ try {
+ const reactPath = resolve.sync('react', {basedir: process.cwd()});
+ const react = require(reactPath);
+ return react.version;
+ } catch (e) {
+ if (e.code === 'MODULE_NOT_FOUND') {
+ error('Warning: React version was set to "detect" in eslint-plugin-react settings, ' +
+ 'but the "react" package is not installed. Assuming latest React version for linting.');
+ return '999.999.999';
+ }
+ throw e;
+ }
+}
+
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;
+ let settingsVersion = context.settings.react.version;
+ if (settingsVersion === 'detect') {
+ settingsVersion = detectReactVersion();
+ }
+ if (typeof settingsVersion !== 'string') {
+ error('Warning: React version specified in eslint-plugin-react-settings must be a string; ' +
+ `got “${typeof settingsVersion}”`);
+ }
+ confVer = String(settingsVersion);
+ } else if (!warnedForMissingVersion) {
+ error('Warning: React version not specified in eslint-plugin-react settings. ' +
+ 'See https://github.com/yannickcr/eslint-plugin-react#configuration .');
+ warnedForMissingVersion = true;
}
confVer = /^[0-9]+\.[0-9]+$/.test(confVer) ? `${confVer}.0` : confVer;
return confVer.split('.').map(part => Number(part));
@@ -18,7 +50,12 @@
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;
+ const flowVersion = context.settings.react.flowVersion;
+ if (typeof flowVersion !== 'string') {
+ error('Warning: Flow version specified in eslint-plugin-react-settings must be a string; ' +
+ `got “${typeof flowVersion}”`);
+ }
+ confVer = String(flowVersion);
} else {
throw 'Could not retrieve flowVersion from settings';
}
@@ -26,11 +63,18 @@
return confVer.split('.').map(part => Number(part));
}
+function normalizeParts(parts) {
+ return Array.from({length: 3}, (_, i) => (parts[i] || 0));
+}
+
function test(context, methodVer, confVer) {
- methodVer = String(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];
+ const methodVers = normalizeParts(String(methodVer || '').split('.').map(part => Number(part)));
+ const confVers = normalizeParts(confVer);
+ const higherMajor = methodVers[0] < confVers[0];
+ const higherMinor = methodVers[0] === confVers[0] && methodVers[1] < confVers[1];
+ const higherOrEqualPatch = methodVers[0] === confVers[0]
+ && methodVers[1] === confVers[1]
+ && methodVers[2] <= confVers[2];
return higherMajor || higherMinor || higherOrEqualPatch;
}

package.json

@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-react",
- "version": "7.8.2",
+ "version": "7.12.4",
"author": "Yannick Croissant <yannick.croissant+npm@gmail.com>",
"description": "React specific linting rules for ESLint",
"main": "index.js",
@@ -9,7 +9,7 @@
"lint": "eslint ./",
"pretest": "npm run lint",
"test": "npm run unit-test",
- "unit-test": "istanbul cover --dir reports/coverage node_modules/mocha/bin/_mocha tests/**/*.js -- --reporter dot"
+ "unit-test": "istanbul cover --dir reports/coverage node_modules/mocha/bin/_mocha tests/lib/**/*.js tests/util/**/*.js tests/index.js"
},
"files": [
"LICENSE",
@@ -24,20 +24,26 @@
"homepage": "https://github.com/yannickcr/eslint-plugin-react",
"bugs": "https://github.com/yannickcr/eslint-plugin-react/issues",
"dependencies": {
- "doctrine": "^2.0.2",
- "has": "^1.0.1",
+ "array-includes": "^3.0.3",
+ "doctrine": "^2.1.0",
+ "has": "^1.0.3",
"jsx-ast-utils": "^2.0.1",
- "prop-types": "^15.6.0"
+ "object.fromentries": "^2.0.0",
+ "prop-types": "^15.6.2",
+ "resolve": "^1.9.0"
},
"devDependencies": {
- "babel-eslint": "^8.2.1",
- "coveralls": "^3.0.0",
- "eslint": "^4.18.0",
+ "babel-eslint": "^8.2.6",
+ "coveralls": "^3.0.2",
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0",
"istanbul": "^0.4.5",
- "mocha": "^5.0.1"
+ "mocha": "^5.2.0",
+ "sinon": "^7.2.2",
+ "typescript": "^3.2.2",
+ "typescript-eslint-parser": "^20.1.1"
},
"peerDependencies": {
- "eslint": "^3.0.0 || ^4.0.0"
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0"
},
"engines": {
"node": ">=4"

README.md

@@ -39,13 +39,16 @@
"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
+ "version": "detect", // React version. "detect" automatically picks the version you have installed.
+ // You can also use `16.0`, `16.3`, etc, if you want to override the detected value.
"flowVersion": "0.53" // Flow version
},
- "propWrapperFunctions": [ "forbidExtraProps" ] // The names of any functions used to wrap the
- // propTypes object, e.g. `forbidExtraProps`.
- // If this isn't set, any propTypes wrapped in
- // a function will be skipped.
+ "propWrapperFunctions": [
+ // The names of any function used to wrap propTypes, e.g. `forbidExtraProps`. If this isn't set, any propTypes wrapped in a function will be skipped.
+ "forbidExtraProps",
+ {"property": "freeze", "object": "Object"},
+ {"property": "myFavoriteWrapper"}
+ ]
}
}
```
@@ -117,6 +120,7 @@
* [react/no-this-in-sfc](docs/rules/no-this-in-sfc.md): Prevent using `this` in stateless functional components
* [react/no-unescaped-entities](docs/rules/no-unescaped-entities.md): Prevent invalid characters from appearing in markup
* [react/no-unknown-property](docs/rules/no-unknown-property.md): Prevent usage of unknown DOM property (fixable)
+* [react/no-unsafe](docs/rules/no-unsafe.md): Prevent usage of unsafe lifecycle methods
* [react/no-unused-prop-types](docs/rules/no-unused-prop-types.md): Prevent definitions of unused prop types
* [react/no-unused-state](docs/rules/no-unused-state.md): Prevent definitions of unused state properties
* [react/no-will-update-set-state](docs/rules/no-will-update-set-state.md): Prevent usage of `setState` in `componentWillUpdate`
@@ -157,7 +161,9 @@
* [react/jsx-no-undef](docs/rules/jsx-no-undef.md): Disallow undeclared variables in JSX
* [react/jsx-one-expression-per-line](docs/rules/jsx-one-expression-per-line.md): Limit to one expression per line in JSX
* [react/jsx-curly-brace-presence](docs/rules/jsx-curly-brace-presence.md): Enforce curly braces or disallow unnecessary curly braces in JSX
+* [react/jsx-fragments](docs/rules/jsx-fragments.md): Enforce shorthand or standard form for React fragments
* [react/jsx-pascal-case](docs/rules/jsx-pascal-case.md): Enforce PascalCase for user-defined JSX components
+* [react/jsx-props-no-multi-spaces](docs/rules/jsx-props-no-multi-spaces.md): Disallow multiple spaces between inline JSX props (fixable)
* [react/jsx-sort-default-props](docs/rules/jsx-sort-default-props.md): Enforce default props alphabetical sorting
* [react/jsx-sort-props](docs/rules/jsx-sort-props.md): Enforce props alphabetical sorting (fixable)
* [react/jsx-space-before-closing](docs/rules/jsx-space-before-closing.md): Validate spacing before closing bracket in JSX (fixable)