Files

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

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

CHANGELOG.md

@@ -3,6 +3,171 @@
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)
@@ -23,7 +188,7 @@
* Output a warning if React version is missing in settings ([#1857][], @alexzherdev)
### Fixed
-* [`destructuring-assignment`][]: Ignore class properties ([#1909][], @alexzherdev)
+* [`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)
@@ -2331,3 +2496,4 @@
[`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,7 @@
'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'),

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();
@@ -172,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) {
@@ -192,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;
}
@@ -248,7 +247,7 @@
}
}
- if (!has(list, component) || (list[component].invalidProps || []).length) {
+ if (list[component].invalidProps && list[component].invalidProps.length > 0) {
reportInvalidNaming(list[component]);
}
});

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,277 +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 || []);
- // Used to track the type annotations in scope.
- // Necessary because babel's scopes do not track type annotations.
- let stack = null;
-
- /**
- * 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;
- }
-
- /**
- * 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;
- }
-
- /**
- * 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) || typeScope(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' && property.type !== 'SpreadElement');
-
- 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' || property.type === 'SpreadElement');
-
- 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.
@@ -319,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;
@@ -334,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}
);
}
});
@@ -351,265 +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' || property.type === 'SpreadElement') {
- 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));
- }
- });
- },
-
- TypeAlias: function(node) {
- typeScope(node.id.name, node.right);
- },
-
- Program: function() {
- stack = [{}];
- },
-
- BlockStatement: function () {
- stack.push(Object.create(typeScope()));
- },
-
- 'BlockStatement:exit': function () {
- stack.pop();
- },
-
- // Check for type annotations in stateless components
- FunctionDeclaration: handleStatelessComponent,
- ArrowFunctionExpression: handleStatelessComponent,
- FunctionExpression: handleStatelessComponent,
-
'Program:exit': function() {
- stack = null;
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

@@ -82,6 +82,17 @@
}
}
+ 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 = (
@@ -92,7 +103,7 @@
if (
isPropUsed && configuration === 'always' &&
- !(ignoreClassFields && node.parent.type === 'ClassProperty')
+ !(ignoreClassFields && isInClassProperty(node))
) {
context.report({
node: node,

lib/rules/display-name.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');
@@ -216,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;
}

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,11 +71,7 @@
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 => {
@@ -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 === '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);
}
// --------------------------------------------------------------------------

lib/rules/jsx-curly-spacing.js

@@ -331,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;
@@ -219,7 +228,7 @@
} 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-max-depth.js

@@ -6,6 +6,7 @@
const has = require('has');
const variableUtil = require('../util/variable');
+const jsxUtil = require('../util/jsx');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
@@ -39,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) {
@@ -60,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++;
}
}
@@ -82,7 +79,7 @@
});
}
- function findJSXElement(variables, name) {
+ function findJSXElementOrFragment(variables, name) {
function find(refs) {
let i = refs.length;
@@ -90,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);
}
}
@@ -105,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)) {
@@ -119,8 +116,7 @@
});
}
- return {
- JSXElement: function(node) {
+ function handleJSX(node) {
if (!isLeaf(node)) {
return;
}
@@ -129,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-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()}”`
});
}

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';
}

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

@@ -50,16 +50,15 @@
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;
@@ -215,6 +214,10 @@
});
});
}
+
+ return {
+ JSXElement: handleJSX,
+ JSXFragment: handleJSX
};
}
};

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

@@ -6,6 +6,7 @@
const variableUtil = require('../util/variable');
const docsUrl = require('../util/docsUrl');
+const propWrapperUtil = require('../util/propWrapper');
// ------------------------------------------------------------------------------
// Rule Definition
@@ -35,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
@@ -134,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

@@ -107,7 +107,7 @@
});
});
- fixers.sort((a, b) => a.range[0] < b.range[0]);
+ fixers.sort((a, b) => b.range[0] - a.range[0]);
const rangeStart = fixers[fixers.length - 1].range[0];
const rangeEnd = fixers[0].range[1];

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

@@ -7,6 +7,7 @@
const has = require('has');
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
+const pragma = require('../util/pragma');
// ------------------------------------------------------------------------------
// Rule Definition
@@ -47,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') {
@@ -59,16 +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;
}
- if (!astUtil.isFunctionLikeExpression(firstArg)) {
+ 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-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;
}

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

@@ -124,15 +124,18 @@
}
}
- function reportErrorIfClassPropertyCasingTypo(node, propertyName) {
+ function reportErrorIfPropertyCasingTypo(node, propertyName, isClassProperty) {
if (propertyName === 'propTypes' || propertyName === 'contextTypes' || propertyName === 'childContextTypes') {
checkValidPropObject(node);
}
STATIC_CLASS_PROPERTIES.forEach(CLASS_PROP => {
if (propertyName && CLASS_PROP.toLowerCase() === propertyName.toLowerCase() && CLASS_PROP !== propertyName) {
+ const message = isClassProperty
+ ? 'Typo in static class property declaration'
+ : 'Typo in property declaration';
context.report({
node: node,
- message: 'Typo in static class property declaration'
+ message: message
});
}
});
@@ -175,7 +178,7 @@
const tokens = context.getFirstTokens(node, 2);
const propertyName = tokens[1].value;
- reportErrorIfClassPropertyCasingTypo(node.value, propertyName);
+ reportErrorIfPropertyCasingTypo(node.value, propertyName, true);
},
MemberExpression: function(node) {
@@ -195,16 +198,29 @@
(utils.isES6Component(relatedComponent.node) || utils.isReturningJSX(relatedComponent.node)) &&
(node.parent && node.parent.type === 'AssignmentExpression' && node.parent.right)
) {
- reportErrorIfClassPropertyCasingTypo(node.parent.right, 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
@@ -72,7 +73,7 @@
return {
'Literal, JSXText': function(node) {
- if (node.parent.type === 'JSXElement') {
+ if (jsxUtil.isJSX(node.parent)) {
reportInvalidEntity(node);
}
}

lib/rules/no-unsafe.js

@@ -1,5 +1,5 @@
/**
- * @fileoverview Prevent usage of UNSAFE_ methods
+ * @fileoverview Prevent usage of unsafe lifecycle methods
* @author Sergei Startsev
*/
@@ -17,30 +17,63 @@
module.exports = {
meta: {
docs: {
- description: 'Prevent usage of UNSAFE_ methods',
+ description: 'Prevent usage of unsafe lifecycle methods',
category: 'Best Practices',
recommended: false,
url: docsUrl('no-unsafe')
},
- schema: []
+ 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 [
- 'UNSAFE_componentWillMount',
- 'UNSAFE_componentWillReceiveProps',
- 'UNSAFE_componentWillUpdate'
- ];
+ return Object.keys(unsafe);
}
/**
@@ -63,9 +96,13 @@
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, see https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html`
+ message: `${method} is unsafe for use in async rendering. Update the component to use ${newMethod} instead. ${details}`
});
}

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

@@ -7,23 +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 astUtil = require('../util/ast');
-const versionUtil = require('../util/version');
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
// ------------------------------------------------------------------------------
@@ -53,92 +40,12 @@
}]
},
- create: Components.detect((context, components, utils) => {
- const sourceCode = context.getSourceCode();
- const checkAsyncSafeLifeCycles = versionUtil.testReactVersion(context, '16.3.0');
+ create: Components.detect((context, components) => {
const defaults = {skipShapeProps: true, customValidators: []};
const configuration = Object.assign({}, defaults, context.options[0] || {});
const UNUSED_MESSAGE = '\'{{name}}\' PropType is defined but prop is never used';
/**
- * 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 the component must be validated
* @param {Object} component The component to process
* @returns {Boolean} True if the component must be validated, false if not.
@@ -146,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
);
}
@@ -223,229 +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;
- }
-
- /**
- * 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
- });
- }
-
- /**
* Used to recursively loop through each declared prop type
* @param {Object} component The component to process
* @param {Array} props List of props to validate
@@ -469,7 +103,7 @@
if (prop.node && !isPropUsed(component, prop)) {
context.report(
- prop.node,
+ prop.node.value || prop.node,
UNUSED_MESSAGE, {
name: prop.fullName
}
@@ -490,104 +124,20 @@
reportUnusedPropType(component, component.declaredPropTypes);
}
- /**
- * @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);
- }
- }
-
- 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 (astUtil.isFunctionLikeExpression(node)) {
- markPropTypesAsUsed(node);
- }
- });
- }
-
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
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,
-
- 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.forEach((property, i) => {
- if (i === 0) {
- markPropTypesAsUsed(node.parent);
- }
- });
- }
- },
-
'Program:exit': function() {
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;
}
- handleCustomValidators(list[component]);
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
@@ -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);
}
},

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,18 +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 docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
-// Constants
-// ------------------------------------------------------------------------------
-
-const PROPS_REGEX = /^(props|nextProps)$/;
-const DIRECT_PROPS_REGEX = /^(props|nextProps)\s*(\.|\[)/;
-
-// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
@@ -54,8 +46,7 @@
}]
},
- create: Components.detect((context, components, utils) => {
- const sourceCode = context.getSourceCode();
+ create: Components.detect((context, components) => {
const configuration = context.options[0] || {};
const ignored = configuration.ignore || [];
const skipUndeclared = configuration.skipUndeclared || false;
@@ -63,86 +54,6 @@
const MISSING_MESSAGE = '\'{{name}}\' is missing in props validation';
/**
- * 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 the prop is ignored
* @param {String} name Name of the prop to check.
* @returns {Boolean} True if the prop is ignored, false if not.
@@ -191,7 +102,7 @@
return true;
}
// Consider every children as declared
- if (propType.children === true) {
+ if (propType.children === true || propType.containsSpread) {
return true;
}
if (propType.acceptedProperties) {
@@ -248,229 +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;
- }
-
- /**
- * 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
- });
- }
-
- /**
* 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, '[]')
}
@@ -478,76 +182,17 @@
}
}
- /**
- * @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);
- }
- }
-
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
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 directDestructuring =
- destructuring &&
- PROPS_REGEX.test(node.init.name) &&
- (utils.getParentStatelessComponent() || inConstructor() || inComponentWillReceiveProps())
- ;
-
- if (!thisDestructuring && !directDestructuring) {
- return;
- }
- markPropTypesAsUsed(node);
- },
-
- FunctionDeclaration: markDestructuredFunctionArgumentsAsUsed,
-
- ArrowFunctionExpression: markDestructuredFunctionArgumentsAsUsed,
-
- FunctionExpression: function(node) {
- if (node.parent.type === 'MethodDefinition') {
- return;
- }
- markDestructuredFunctionArgumentsAsUsed(node);
- },
-
- MemberExpression: function(node) {
- if (isPropTypesUsage(node)) {
- markPropTypesAsUsed(node);
- }
- },
-
- 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);
- }
- },
-
'Program:exit': function() {
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,253 +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;
- // Used to track the type annotations in scope.
- // Necessary because babel's scopes do not track type annotations.
- let stack = null;
- /**
- * 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;
- }
-
- /**
- * 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;
- }
-
- /**
- * 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) || typeScope(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' && property.type !== 'SpreadElement');
-
- 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' || property.type === 'SpreadElement');
-
- 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}
@@ -295,352 +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' || property.type === 'SpreadElement') {
- 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));
- }
- });
- },
-
- TypeAlias: function(node) {
- typeScope(node.id.name, node.right);
- },
-
- Program: function() {
- stack = [{}];
- },
-
- BlockStatement: function () {
- stack.push(Object.create(typeScope()));
- },
-
- 'BlockStatement:exit': function () {
- stack.pop();
- },
-
- // 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() {
- stack = null;
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');
@@ -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/sort-comp.js

@@ -444,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
@@ -54,9 +55,11 @@
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);
}
@@ -247,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;

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,13 +18,21 @@
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));
}
/**
@@ -68,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 it's 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;
@@ -87,7 +96,17 @@
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;
@@ -102,11 +121,42 @@
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,
- isNodeFirstInLine: isNodeFirstInLine,
- isFunctionLikeExpression: isFunctionLikeExpression
+ isArrowFunction: isArrowFunction,
+ isClass: isClass,
+ isFunction: isFunction,
+ isFunctionLikeExpression: isFunctionLikeExpression,
+ isNodeFirstInLine: isNodeFirstInLine
};

lib/util/Components.js

@@ -4,13 +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 propTypes = require('./propTypes');
+const propTypesUtil = require('./propTypes');
+const jsxUtil = require('./jsx');
+const usedPropTypesUtil = require('./usedPropTypes');
+const defaultPropsUtil = require('./defaultProps');
function getId(node) {
return node && node.range.join(':');
@@ -122,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;
@@ -141,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;
}
@@ -166,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;
}
}
@@ -262,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'
);
@@ -300,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) {
@@ -349,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 ?
@@ -363,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
);
},
@@ -403,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
*
@@ -466,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;
@@ -486,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).
@@ -541,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;
@@ -590,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;
@@ -695,8 +730,15 @@
// Update the provided rule instructions to add the component detection
const ruleInstructions = rule(context, components, utils);
const updatedRuleInstructions = util._extend({}, ruleInstructions);
- const propTypesInstructions = propTypes(context, components, utils);
- const allKeys = new Set(Object.keys(detectionInstructions).concat(Object.keys(propTypesInstructions)));
+ 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) {
@@ -705,6 +747,12 @@
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

@@ -9,7 +9,7 @@
/**
* Checks if a node represents a DOM element.
- * @param {String} node - JSXOpeningElement to check.
+ * @param {object} node - JSXOpeningElement to check.
* @returns {boolean} Whether or not the node corresponds to a DOM element.
*/
function isDOMComponent(node) {
@@ -25,6 +25,16 @@
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
+ isDOMComponent: isDOMComponent,
+ isJSX: isJSX
};

lib/util/makeNoMethodSetStateRule.js

@@ -10,6 +10,19 @@
// Rule Definition
// ------------------------------------------------------------------------------
+function mapTitle(methodName) {
+ const map = {
+ componentDidMount: 'did-mount',
+ componentDidUpdate: 'did-update',
+ componentWillUpdate: 'will-update'
+ };
+ const title = map[methodName];
+ if (!title) {
+ throw Error(`No docsUrl for '${methodName}'`);
+ }
+ return `no-${title}-set-state`;
+}
+
function makeNoMethodSetStateRule(methodName, shouldCheckUnsafeCb) {
return {
meta: {
@@ -17,7 +30,7 @@
description: `Prevent usage of setState in ${methodName}`,
category: 'Best Practices',
recommended: false,
- url: docsUrl(methodName)
+ url: docsUrl(mapTitle(methodName))
},
schema: [{

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

@@ -7,6 +7,7 @@
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.
@@ -61,11 +62,29 @@
const key = getKeyValue(context, node);
const value = node.value;
- fn(key, 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.
@@ -76,7 +95,6 @@
const configuration = Object.assign({}, defaults, context.options[0] || {});
const customValidators = configuration.customValidators;
const sourceCode = context.getSourceCode();
- const propWrapperFunctions = new Set(context.settings.propWrapperFunctions);
/**
* Returns the full scope.
@@ -130,7 +148,7 @@
type: 'shape',
children: {}
};
- iterateProperties(context, annotation.properties, (childKey, childValue) => {
+ iterateProperties(context, annotation.properties, (childKey, childValue, propNode) => {
const fullName = [parentName, childKey].join('.');
if (!childKey && !childValue) {
containsObjectTypeSpread = true;
@@ -138,15 +156,16 @@
const types = buildTypeAnnotationDeclarationTypes(childValue, fullName, seen);
types.fullName = fullName;
types.name = childKey;
- types.node = childValue;
+ types.node = propNode;
+ types.isRequired = !childValue.optional;
shapeTypeDefinition.children[childKey] = types;
}
});
- // nested object type spread means we need to ignore/accept everything in this object
- if (containsObjectTypeSpread) {
- return {};
- }
+ // 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;
},
@@ -248,7 +268,7 @@
function declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes) {
let ignorePropsValidation = false;
- iterateProperties(context, propTypes.properties, (key, value) => {
+ iterateProperties(context, propTypes.properties, (key, value, propNode) => {
if (!value) {
ignorePropsValidation = true;
return;
@@ -257,7 +277,8 @@
const types = buildTypeAnnotationDeclarationTypes(value, key);
types.fullName = key;
types.name = key;
- types.node = value;
+ types.node = propNode;
+ types.isRequired = !propNode.optional;
declaredPropTypes[key] = types;
});
@@ -349,13 +370,15 @@
type: 'shape',
children: {}
};
- iterateProperties(context, argument.properties, (childKey, childValue) => {
+ 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 = childValue;
+ types.node = propNode;
shapeTypeDefinition.children[childKey] = types;
+ }
});
return shapeTypeDefinition;
case 'arrayOf':
@@ -435,7 +458,7 @@
ignorePropsValidation = declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes);
break;
case 'ObjectExpression':
- iterateProperties(context, propTypes.properties, (key, value) => {
+ iterateProperties(context, propTypes.properties, (key, value, propNode) => {
if (!value) {
ignorePropsValidation = true;
return;
@@ -443,7 +466,8 @@
const types = buildReactDeclarationTypes(value, key);
types.fullName = key;
types.name = key;
- types.node = value;
+ types.node = propNode;
+ types.isRequired = propsUtil.isRequiredPropType(value);
declaredPropTypes[key] = types;
});
break;
@@ -481,7 +505,8 @@
types.name = propTypes.property.name;
types.fullName = [parentProp, propTypes.property.name].join('.');
- types.node = propTypes.property;
+ types.node = propTypes.parent;
+ types.isRequired = propsUtil.isRequiredPropType(propTypes.parent.right);
curDeclaredPropTypes[propTypes.property.name] = types;
} else {
let isUsedInPropTypes = false;
@@ -515,7 +540,7 @@
break;
case 'CallExpression':
if (
- propWrapperFunctions.has(sourceCode.getText(propTypes.callee)) &&
+ propWrapperUtil.isPropWrapperFunction(context, sourceCode.getText(propTypes.callee)) &&
propTypes.arguments && propTypes.arguments[0]
) {
markPropTypesAsDeclared(node, propTypes.arguments[0]);
@@ -546,7 +571,23 @@
if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) {
return;
}
- markPropTypesAsDeclared(node, resolveTypeAnnotation(node.params[0]));
+
+ 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));
+ }
}
/**
@@ -666,13 +707,6 @@
}
},
- JSXSpreadAttribute: function(node) {
- const component = components.get(utils.getParentComponent());
- components.set(component ? component.node : node, {
- ignorePropsValidation: true
- });
- },
-
TypeAlias: function(node) {
setInTypeScope(node.id.name, node.right);
},

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,18 +4,42 @@
*/
'use strict';
-const log = require('./log');
+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) {
- log('Warning: React version not specified in eslint-plugin-react settings. ' +
- 'See https://github.com/yannickcr/eslint-plugin-react#configuration.');
+ 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;
@@ -26,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';
}
@@ -34,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.11.1",
+ "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"
+ "unit-test": "istanbul cover --dir reports/coverage node_modules/mocha/bin/_mocha tests/lib/**/*.js tests/util/**/*.js tests/index.js"
},
"files": [
"LICENSE",
@@ -28,14 +28,19 @@
"doctrine": "^2.1.0",
"has": "^1.0.3",
"jsx-ast-utils": "^2.0.1",
- "prop-types": "^15.6.2"
+ "object.fromentries": "^2.0.0",
+ "prop-types": "^15.6.2",
+ "resolve": "^1.9.0"
},
"devDependencies": {
- "babel-eslint": "^8.2.5",
- "coveralls": "^3.0.1",
+ "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.2.0"
+ "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 || ^5.0.0"

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,7 +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_` methods
+* [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`
@@ -158,6 +161,7 @@
* [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