From 587004740f706777294fc14127cf9ee13aa299cf Mon Sep 17 00:00:00 2001 From: Luke Zapart Date: Wed, 2 Jan 2019 18:29:02 -0800 Subject: [PATCH 1/6] Add prefer-read-only-props rule --- lib/rules/prefer-read-only-props.js | 74 +++++++ tests/lib/rules/prefer-read-only-props.js | 251 ++++++++++++++++++++++ 2 files changed, 325 insertions(+) create mode 100644 lib/rules/prefer-read-only-props.js create mode 100644 tests/lib/rules/prefer-read-only-props.js diff --git a/lib/rules/prefer-read-only-props.js b/lib/rules/prefer-read-only-props.js new file mode 100644 index 0000000000..ed0a06fbe7 --- /dev/null +++ b/lib/rules/prefer-read-only-props.js @@ -0,0 +1,74 @@ +/** + * @fileoverview Require component props to be typed as read-only. + * @author Luke Zapart + */ +'use strict'; + +const Components = require('../util/Components'); +const docsUrl = require('../util/docsUrl'); + +function isFlowPropertyType(node) { + return node.type === 'ObjectTypeProperty'; +} + +function isCovariant(node) { + return node.variance && node.variance.kind === 'plus'; +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: 'Require read-only props.', + category: 'Stylistic Issues', + recommended: false, + url: docsUrl('prefer-read-only-props') + }, + fixable: 'code', + schema: [] + }, + + create: Components.detect((context, components) => ({ + 'Program:exit': function () { + const list = components.list(); + + Object.keys(list).forEach(key => { + const component = list[key]; + + if (!component.declaredPropTypes) { + return; + } + + Object.keys(component.declaredPropTypes).forEach(propName => { + const prop = component.declaredPropTypes[propName]; + + if (!isFlowPropertyType(prop.node)) { + return; + } + + if (!isCovariant(prop.node)) { + context.report({ + node: prop.node, + message: 'Prop \'{{propName}}\' should be read-only.', + data: { + propName + }, + fix: fixer => { + if (!prop.node.variance) { + // Insert covariance + return fixer.insertTextBefore(prop.node, '+'); + } else { + // Replace contravariance with covariance + return fixer.replaceText(prop.node.variance, '+'); + } + } + }); + } + }); + }); + } + })) +}; diff --git a/tests/lib/rules/prefer-read-only-props.js b/tests/lib/rules/prefer-read-only-props.js new file mode 100644 index 0000000000..61bd2f6933 --- /dev/null +++ b/tests/lib/rules/prefer-read-only-props.js @@ -0,0 +1,251 @@ +/** + * @fileoverview Require component props to be typed as read-only. + * @author Luke Zapart + */ +'use strict'; + + +// ----------------------------------------------------------------------------- +// Requirements +// ----------------------------------------------------------------------------- + +const rule = require('../../../lib/rules/prefer-read-only-props'); +const RuleTester = require('eslint').RuleTester; + +const parserOptions = { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true + } +}; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({parserOptions}); +ruleTester.run('prefer-read-only-props', rule, { + + valid: [ + { + // Class component with type parameter + code: ` + type Props = { + +name: string, + } + + class Hello extends React.Component { + render () { + return
Hello {this.props.name}
; + } + } + `, + parser: 'babel-eslint' + }, + { + // Class component with typed props property + code: ` + class Hello extends React.Component { + props: { + +name: string, + } + + render () { + return
Hello {this.props.name}
; + } + } + `, + parser: 'babel-eslint' + }, + { + // Functional component with typed props argument + code: ` + function Hello(props: {+name: string}) { + return
Hello {props.name}
; + } + `, + parser: 'babel-eslint' + }, + { + // Functional component with type intersection + code: ` + type PropsA = {+firstName: string}; + type PropsB = {+lastName: string}; + type Props = PropsA & PropsB; + + function Hello({firstName, lastName}: Props) { + return
Hello {firstName} {lastName}
; + } + `, + parser: 'babel-eslint' + }, + { + // Arrow function component + code: ` + const Hello = (props: {+name: string}) => ( +
Hello {props.name}
+ ); + `, + parser: 'babel-eslint' + }, + { + // Destructured props + code: ` + const Hello = ({name}: {+name: string}) => ( +
Hello {props.name}
+ ); + `, + parser: 'babel-eslint' + }, + { + // No error because this is not a component + code: ` + const notAComponent = (props: {n: number}) => { + return props.n + 1; + }; + `, + parser: 'babel-eslint' + }, + { + // No error, because there is no Props flow type + code: ` + class Hello extends React.Component { + render () { + return
Hello {this.props.name}
; + } + } + ` + }, + { + // No error, because PropTypes do not support variance + code: ` + class Hello extends React.Component { + render () { + return
Hello {this.props.name}
; + } + } + Hello.propTypes = { + name: PropTypes.string, + }; + ` + } + ], + + invalid: [ + { + // Props.name is not read-only + code: ` + type Props = { + name: string, + } + + class Hello extends React.Component { + render () { + return
Hello {this.props.name}
; + } + } + `, + parser: 'babel-eslint', + errors: [{ + message: 'Prop \'name\' should be read-only.' + }] + }, + { + // Props.name is contravariant + code: ` + type Props = { + -name: string, + } + + class Hello extends React.Component { + render () { + return
Hello {this.props.name}
; + } + } + `, + parser: 'babel-eslint', + errors: [{ + message: 'Prop \'name\' should be read-only.' + }] + }, + { + code: ` + class Hello extends React.Component { + props: { + name: string, + } + + render () { + return
Hello {this.props.name}
; + } + } + `, + parser: 'babel-eslint', + errors: [{ + message: 'Prop \'name\' should be read-only.' + }] + }, + { + code: ` + function Hello(props: {name: string}) { + return
Hello {props.name}
; + } + `, + parser: 'babel-eslint', + errors: [{ + message: 'Prop \'name\' should be read-only.' + }] + }, + { + code: ` + function Hello(props: {|name: string|}) { + return
Hello {props.name}
; + } + `, + parser: 'babel-eslint', + errors: [{ + message: 'Prop \'name\' should be read-only.' + }] + }, + { + code: ` + function Hello({name}: {name: string}) { + return
Hello {props.name}
; + } + `, + parser: 'babel-eslint', + errors: [{ + message: 'Prop \'name\' should be read-only.' + }] + }, + { + code: ` + type PropsA = {firstName: string}; + type PropsB = {lastName: string}; + type Props = PropsA & PropsB; + + function Hello({firstName, lastName}: Props) { + return
Hello {firstName} {lastName}
; + } + `, + parser: 'babel-eslint', + errors: [{ + message: 'Prop \'firstName\' should be read-only.' + }, { + message: 'Prop \'lastName\' should be read-only.' + }] + }, + { + code: ` + const Hello = (props: {-name: string}) => ( +
Hello {props.name}
+ ); + `, + parser: 'babel-eslint', + errors: [{ + message: 'Prop \'name\' should be read-only.' + }] + } + ] +}); From 8bd4f74161934f6ae05a0fd24fb5f080fb9e274f Mon Sep 17 00:00:00 2001 From: Luke Zapart Date: Wed, 2 Jan 2019 18:29:21 -0800 Subject: [PATCH 2/6] Add docs for prefer-read-only-props --- docs/rules/prefer-read-only-props.md | 47 ++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/rules/prefer-read-only-props.md diff --git a/docs/rules/prefer-read-only-props.md b/docs/rules/prefer-read-only-props.md new file mode 100644 index 0000000000..44e1d59353 --- /dev/null +++ b/docs/rules/prefer-read-only-props.md @@ -0,0 +1,47 @@ +# Enforce that props are read-only (react/prefer-read-only-props) + +Using Flow, one can define types for props. This rule enforces that prop types are read-only (covariant). + +## Rule Details + +The following patterns are considered warnings: + +```jsx +type Props = { + name: string, +} +class Hello extends React.Component { + render () { + return
Hello {this.props.name}
; + } +} + +function Hello(props: {-name: string}) { + return
Hello {props.name}
; +} + +const Hello = (props: {|name: string|}) => ( +
Hello {props.name}
+); +``` + +The following patterns are **not** considered warnings: + +```jsx +type Props = { + +name: string, +} +class Hello extends React.Component { + render () { + return
Hello {this.props.name}
; + } +} + +function Hello(props: {+name: string}) { + return
Hello {props.name}
; +} + +const Hello = (props: {|+name: string|}) => ( +
Hello {props.name}
+); +``` From ee962d38c3c16cad63002501cc3597f6a40d2977 Mon Sep 17 00:00:00 2001 From: Luke Zapart Date: Wed, 2 Jan 2019 18:29:43 -0800 Subject: [PATCH 3/6] Add rule to index.js --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index 25faa607a6..b4aba79ee1 100644 --- a/index.js +++ b/index.js @@ -70,6 +70,7 @@ const allRules = { 'no-unused-state': require('./lib/rules/no-unused-state'), 'no-will-update-set-state': require('./lib/rules/no-will-update-set-state'), 'prefer-es6-class': require('./lib/rules/prefer-es6-class'), + 'prefer-read-only-props': require('./lib/rules/prefer-read-only-props'), 'prefer-stateless-function': require('./lib/rules/prefer-stateless-function'), 'prop-types': require('./lib/rules/prop-types'), 'react-in-jsx-scope': require('./lib/rules/react-in-jsx-scope'), From a2f1ce6d76bf1e691a9203505b73ffd3d9d91663 Mon Sep 17 00:00:00 2001 From: Luke Zapart Date: Wed, 2 Jan 2019 19:45:22 -0800 Subject: [PATCH 4/6] Fix comments --- tests/lib/rules/prefer-read-only-props.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/lib/rules/prefer-read-only-props.js b/tests/lib/rules/prefer-read-only-props.js index 61bd2f6933..0bf068d60d 100644 --- a/tests/lib/rules/prefer-read-only-props.js +++ b/tests/lib/rules/prefer-read-only-props.js @@ -81,7 +81,7 @@ ruleTester.run('prefer-read-only-props', rule, { parser: 'babel-eslint' }, { - // Arrow function component + // Arrow function code: ` const Hello = (props: {+name: string}) => (
Hello {props.name}
@@ -99,7 +99,7 @@ ruleTester.run('prefer-read-only-props', rule, { parser: 'babel-eslint' }, { - // No error because this is not a component + // No error, because this is not a component code: ` const notAComponent = (props: {n: number}) => { return props.n + 1; From 456f485c8c226d4661047fbb2bd4f5155da53873 Mon Sep 17 00:00:00 2001 From: Luke Zapart Date: Wed, 2 Jan 2019 23:19:04 -0800 Subject: [PATCH 5/6] Lint --- lib/rules/prefer-read-only-props.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/rules/prefer-read-only-props.js b/lib/rules/prefer-read-only-props.js index ed0a06fbe7..31d9993b78 100644 --- a/lib/rules/prefer-read-only-props.js +++ b/lib/rules/prefer-read-only-props.js @@ -60,10 +60,10 @@ module.exports = { if (!prop.node.variance) { // Insert covariance return fixer.insertTextBefore(prop.node, '+'); - } else { - // Replace contravariance with covariance - return fixer.replaceText(prop.node.variance, '+'); } + + // Replace contravariance with covariance + return fixer.replaceText(prop.node.variance, '+'); } }); } From a1deb95f96365448f3f99dcd836c7baf9deae9f9 Mon Sep 17 00:00:00 2001 From: Luke Zapart Date: Wed, 2 Jan 2019 23:27:56 -0800 Subject: [PATCH 6/6] Whitespace --- tests/lib/rules/prefer-read-only-props.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/lib/rules/prefer-read-only-props.js b/tests/lib/rules/prefer-read-only-props.js index 0bf068d60d..fabe2aa05b 100644 --- a/tests/lib/rules/prefer-read-only-props.js +++ b/tests/lib/rules/prefer-read-only-props.js @@ -4,7 +4,6 @@ */ 'use strict'; - // ----------------------------------------------------------------------------- // Requirements // -----------------------------------------------------------------------------