diff --git a/README.md b/README.md index 6bb0836761..8afe26911e 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ You can also specify some settings that will be shared across all the plugin rul "createClass": "createReactClass", // Regex for Component Factory to use, default to "createReactClass" "pragma": "React", // Pragma to use, default to "React" "version": "15.0" // React version, default to the latest React stable release + "flowVersion": "0.53" // Flow version }, "propWrapperFunctions": [ "forbidExtraProps" ] // The names of any functions used to wrap the propTypes object, such as `forbidExtraProps`. If this isn't set, any propTypes wrapped in a function will be skipped. } diff --git a/lib/rules/no-deprecated.js b/lib/rules/no-deprecated.js index 688046372d..0504798c5e 100644 --- a/lib/rules/no-deprecated.js +++ b/lib/rules/no-deprecated.js @@ -78,7 +78,7 @@ module.exports = { return ( deprecated && deprecated[method] && - versionUtil.test(context, deprecated[method][0]) + versionUtil.testReactVersion(context, deprecated[method][0]) ); } diff --git a/lib/rules/no-render-return-value.js b/lib/rules/no-render-return-value.js index e854481392..db38731026 100644 --- a/lib/rules/no-render-return-value.js +++ b/lib/rules/no-render-return-value.js @@ -35,11 +35,11 @@ module.exports = { } let calleeObjectName = /^ReactDOM$/; - if (versionUtil.test(context, '15.0.0')) { + if (versionUtil.testReactVersion(context, '15.0.0')) { calleeObjectName = /^ReactDOM$/; - } else if (versionUtil.test(context, '0.14.0')) { + } else if (versionUtil.testReactVersion(context, '0.14.0')) { calleeObjectName = /^React(DOM)?$/; - } else if (versionUtil.test(context, '0.13.0')) { + } else if (versionUtil.testReactVersion(context, '0.13.0')) { calleeObjectName = /^React$/; } diff --git a/lib/rules/prefer-stateless-function.js b/lib/rules/prefer-stateless-function.js index 5e89724532..e3c4ff5204 100644 --- a/lib/rules/prefer-stateless-function.js +++ b/lib/rules/prefer-stateless-function.js @@ -371,7 +371,7 @@ module.exports = { scope = scope.upper; } const isRender = blockNode && blockNode.key && blockNode.key.name === 'render'; - const allowNull = versionUtil.test(context, '15.0.0'); // Stateless components can return null since React 15 + const allowNull = versionUtil.testReactVersion(context, '15.0.0'); // Stateless components can return null since React 15 const isReturningJSX = utils.isReturningJSX(node, !allowNull); const isReturningNull = node.argument && (node.argument.value === null || node.argument.value === false); if ( diff --git a/lib/rules/prop-types.js b/lib/rules/prop-types.js index 6e7f48dfc8..13aa98e718 100644 --- a/lib/rules/prop-types.js +++ b/lib/rules/prop-types.js @@ -11,6 +11,7 @@ const has = require('has'); const Components = require('../util/Components'); const variable = require('../util/variable'); const annotations = require('../util/annotations'); +const versionUtil = require('../util/version'); // ------------------------------------------------------------------------------ // Constants @@ -173,7 +174,7 @@ module.exports = { */ function isSuperTypeParameterPropsDeclaration(node) { if (node && node.type === 'ClassDeclaration') { - if (node.superTypeParameters && node.superTypeParameters.params.length >= 2) { + if (node.superTypeParameters && node.superTypeParameters.params.length > 0) { return true; } } @@ -857,7 +858,17 @@ module.exports = { * @returns {ASTNode} The resolved type annotation for the node. */ function resolveSuperParameterPropsType(node) { - let annotation = node.superTypeParameters.params[1]; + let propsParameterPosition; + try { + // Flow <=0.52 had 3 required TypedParameters of which the second one is the Props. + // Flow >=0.53 has 2 optional TypedParameters of which the first one is the Props. + propsParameterPosition = versionUtil.testFlowVersion(context, '0.53.0') ? 0 : 1; + } catch (e) { + // In case there is no flow version defined, we can safely assume that when there are 3 Props we are dealing with version <= 0.52 + propsParameterPosition = node.superTypeParameters.params.length <= 2 ? 0 : 1; + } + + let annotation = node.superTypeParameters.params[propsParameterPosition]; while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) { annotation = annotation.typeAnnotation; } diff --git a/lib/util/version.js b/lib/util/version.js index a3847fd1a8..cbdec7d919 100644 --- a/lib/util/version.js +++ b/lib/util/version.js @@ -1,10 +1,10 @@ /** - * @fileoverview Utility functions for React version configuration + * @fileoverview Utility functions for React and Flow version configuration * @author Yannick Croissant */ 'use strict'; -function getFromContext(context) { +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) { @@ -14,8 +14,19 @@ function getFromContext(context) { return confVer.split('.').map(part => Number(part)); } -function test(context, methodVer) { - const confVer = getFromContext(context); +function getFlowVersionFromContext(context) { + let confVer = '999.999.999'; + // .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings) + if (context.settings.react && context.settings.react.flowVersion) { + confVer = context.settings.react.flowVersion; + } else { + throw 'Could not retrieve flowVersion from settings'; + } + confVer = /^[0-9]+\.[0-9]+$/.test(confVer) ? `${confVer}.0` : confVer; + return confVer.split('.').map(part => Number(part)); +} + +function test(context, methodVer, confVer) { methodVer = methodVer.split('.').map(part => Number(part)); const higherMajor = methodVer[0] < confVer[0]; const higherMinor = methodVer[0] === confVer[0] && methodVer[1] < confVer[1]; @@ -24,6 +35,15 @@ function test(context, methodVer) { return higherMajor || higherMinor || higherOrEqualPatch; } +function testReactVersion(context, methodVer) { + return test(context, methodVer, getReactVersionFromContext(context)); +} + +function testFlowVersion(context, methodVer) { + return test(context, methodVer, getFlowVersionFromContext(context)); +} + module.exports = { - test: test + testReactVersion, + testFlowVersion }; diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index da938c7929..3bcc2c61e7 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -1487,6 +1487,19 @@ ruleTester.run('prop-types', rule, { '}' ].join('\n'), parser: 'babel-eslint' + }, { + code: [ + 'type Person = {', + ' firstname: string', + '};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.firstname}
;', + ' }', + '}' + ].join('\n'), + settings: {react: {flowVersion: '0.52'}}, + parser: 'babel-eslint' }, { code: [ 'type Person = {', @@ -1507,13 +1520,22 @@ ruleTester.run('prop-types', rule, { '};', 'class Hello extends React.Component {', ' render () {', - ' const {', - ' firstname,', - ' } = this.props;', + ' const { firstname } = this.props;', ' return
Hello {firstname}
;', ' }', '}' ].join('\n'), + settings: {react: {flowVersion: '0.52'}}, + parser: 'babel-eslint' + }, { + code: [ + 'type Props = {name: {firstname: string;};};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.name.firstname}
;', + ' }', + '}' + ].join('\n'), parser: 'babel-eslint' }, { code: [ @@ -1524,6 +1546,17 @@ ruleTester.run('prop-types', rule, { ' }', '}' ].join('\n'), + settings: {react: {flowVersion: '0.52'}}, + parser: 'babel-eslint' + }, { + code: [ + 'import type Props from "fake";', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.firstname}
;', + ' }', + '}' + ].join('\n'), parser: 'babel-eslint' }, { code: [ @@ -1534,6 +1567,19 @@ ruleTester.run('prop-types', rule, { ' }', '}' ].join('\n'), + settings: {react: {flowVersion: '0.52'}}, + parser: 'babel-eslint' + }, { + code: [ + 'type Person = {', + ' firstname: string', + '};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.person.firstname}
;', + ' }', + '}' + ].join('\n'), parser: 'babel-eslint' }, { code: [ @@ -1546,6 +1592,17 @@ ruleTester.run('prop-types', rule, { ' }', '}' ].join('\n'), + settings: {react: {flowVersion: '0.52'}}, + parser: 'babel-eslint' + }, { + code: [ + 'type Props = {result?: {ok?: ?string | boolean;}|{ok?: ?number | Array}};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.result.ok}
;', + ' }', + '}' + ].join('\n'), parser: 'babel-eslint' }, { code: [ @@ -1556,6 +1613,61 @@ ruleTester.run('prop-types', rule, { ' }', '}' ].join('\n'), + settings: {react: {flowVersion: '0.52'}}, + parser: 'babel-eslint' + }, { + code: ` + type Props = { + foo: string, + }; + + class Bar extends React.Component { + render() { + return
{this.props.foo}
+ } + } + `, + parser: 'babel-eslint' + }, { + code: ` + type Props = { + foo: string, + }; + + class Bar extends React.Component { + render() { + return
{this.props.foo}
+ } + } + `, + settings: {react: {flowVersion: '0.53'}}, + parser: 'babel-eslint' + }, { + code: ` + type FancyProps = { + foo: string, + }; + + class Bar extends React.Component { + render() { + return
{this.props.foo}
+ } + } + `, + parser: 'babel-eslint' + }, { + code: ` + type FancyProps = { + foo: string, + }; + + class Bar extends React.Component { + render() { + return
{this.props.foo}
+ } + } + `, + settings: {react: {flowVersion: '0.53'}}, parser: 'babel-eslint' }, // issue #1288 @@ -2845,15 +2957,36 @@ ruleTester.run('prop-types', rule, { '};', 'class Hello extends React.Component {', ' render () {', - ' const { lastname } = this.props;', - ' return
Hello {lastname}
;', + ' return
Hello {this.props.lastname}
;', ' }', '}' ].join('\n'), + settings: {react: {flowVersion: '0.52'}}, errors: [{ message: '\'lastname\' is missing in props validation', line: 6, - column: 13, + column: 35, + type: 'Identifier' + }], + parser: 'babel-eslint' + }, { + code: [ + 'type Person = {', + ' firstname: string', + '};', + 'class Hello extends React.Component {', + ' render () {', + ' const {', + ' lastname,', + ' } = this.props;', + ' return
Hello {lastname}
;', + ' }', + '}' + ].join('\n'), + errors: [{ + message: '\'lastname\' is missing in props validation', + line: 7, + column: 7, type: 'Property' }], parser: 'babel-eslint' @@ -2871,6 +3004,7 @@ ruleTester.run('prop-types', rule, { ' }', '}' ].join('\n'), + settings: {react: {flowVersion: '0.52'}}, errors: [{ message: '\'lastname\' is missing in props validation', line: 7, @@ -2894,6 +3028,39 @@ ruleTester.run('prop-types', rule, { type: 'Identifier' }], parser: 'babel-eslint' + }, { + code: [ + 'type Props = {name: {firstname: string;};};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.name.lastname}
;', + ' }', + '}' + ].join('\n'), + settings: {react: {flowVersion: '0.52'}}, + errors: [{ + message: '\'name.lastname\' is missing in props validation', + line: 4, + column: 40, + type: 'Identifier' + }], + parser: 'babel-eslint' + }, { + code: [ + 'type Props = {result?: {ok: string | boolean;}|{ok: number | Array}};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.result.notok}
;', + ' }', + '}' + ].join('\n'), + errors: [{ + message: '\'result.notok\' is missing in props validation', + line: 4, + column: 42, + type: 'Identifier' + }], + parser: 'babel-eslint' }, { code: [ 'type Props = {result?: {ok: string | boolean;}|{ok: number | Array}};', @@ -2903,6 +3070,7 @@ ruleTester.run('prop-types', rule, { ' }', '}' ].join('\n'), + settings: {react: {flowVersion: '0.52'}}, errors: [{ message: '\'result.notok\' is missing in props validation', line: 4, @@ -2928,6 +3096,140 @@ ruleTester.run('prop-types', rule, { type: 'Identifier' }], parser: 'babel-eslint' + }, { + code: [ + 'type Person = {', + ' firstname: string', + '};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.person.lastname}
;', + ' }', + '}' + ].join('\n'), + settings: {react: {flowVersion: '0.52'}}, + errors: [{ + message: '\'person.lastname\' is missing in props validation', + line: 6, + column: 42, + type: 'Identifier' + }], + parser: 'babel-eslint' + }, { + code: ` + type Props = { + foo: string, + }; + + class Bar extends React.Component { + render() { + return
{this.props.bar}
+ } + } + `, + errors: [{ + message: '\'bar\' is missing in props validation', + line: 8, + column: 37, + type: 'Identifier' + }], + parser: 'babel-eslint' + }, { + code: ` + type Props = { + foo: string, + }; + + class Bar extends React.Component { + render() { + return
{this.props.bar}
+ } + } + `, + settings: {react: {flowVersion: '0.53'}}, + errors: [{ + message: '\'bar\' is missing in props validation', + line: 8, + column: 37, + type: 'Identifier' + }], + parser: 'babel-eslint' + }, { + code: ` + type FancyProps = { + foo: string, + }; + + class Bar extends React.Component { + render() { + return
{this.props.bar}
+ } + } + `, + errors: [{ + message: '\'bar\' is missing in props validation', + line: 8, + column: 37, + type: 'Identifier' + }], + parser: 'babel-eslint' + }, { + code: ` + type FancyProps = { + foo: string, + }; + + class Bar extends React.Component { + render() { + return
{this.props.bar}
+ } + } + `, + settings: {react: {flowVersion: '0.53'}}, + errors: [{ + message: '\'bar\' is missing in props validation', + line: 8, + column: 37, + type: 'Identifier' + }], + parser: 'babel-eslint' + }, { + code: ` + type Person = { + firstname: string + }; + class Hello extends React.Component<{ person: Person }> { + render () { + return
Hello {this.props.person.lastname}
; + } + } + `, + errors: [{ + message: '\'person.lastname\' is missing in props validation', + line: 7, + column: 50, + type: 'Identifier' + }], + parser: 'babel-eslint' + }, { + code: ` + type Person = { + firstname: string + }; + class Hello extends React.Component<{ person: Person }> { + render () { + return
Hello {this.props.person.lastname}
; + } + } + `, + settings: {react: {flowVersion: '0.53'}}, + errors: [{ + message: '\'person.lastname\' is missing in props validation', + line: 7, + column: 50, + type: 'Identifier' + }], + parser: 'babel-eslint' } ] });