diff --git a/lib/rules/no-typos.js b/lib/rules/no-typos.js index 4540e5a015..47e2b7fdf1 100644 --- a/lib/rules/no-typos.js +++ b/lib/rules/no-typos.js @@ -124,15 +124,18 @@ module.exports = { } } - 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 @@ module.exports = { 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 @@ module.exports = { (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); + }); } }; }) diff --git a/tests/lib/rules/no-typos.js b/tests/lib/rules/no-typos.js index 06279e786b..3f1c472097 100644 --- a/tests/lib/rules/no-typos.js +++ b/tests/lib/rules/no-typos.js @@ -23,6 +23,7 @@ const parserOptions = { // ----------------------------------------------------------------------------- const ERROR_MESSAGE = 'Typo in static class property declaration'; +const ERROR_MESSAGE_ES5 = 'Typo in property declaration'; const ERROR_MESSAGE_LIFECYCLE_METHOD = 'Typo in component lifecycle method declaration'; const ruleTester = new RuleTester(); @@ -519,6 +520,105 @@ ruleTester.run('no-typos', rule, { `, parser: 'babel-eslint', parserOptions: parserOptions + }, { + code: ` + import React from 'react'; + import PropTypes from 'prop-types'; + const Component = React.createReactClass({ + propTypes: { + a: PropTypes.string.isRequired, + b: PropTypes.shape({ + c: PropTypes.number + }).isRequired + } + }); + `, + parserOptions: parserOptions + }, { + code: ` + import React from 'react'; + import PropTypes from 'prop-types'; + const Component = React.createReactClass({ + propTypes: { + a: PropTypes.string.isRequired, + b: PropTypes.shape({ + c: PropTypes.number + }).isRequired + } + }); + `, + parser: 'babel-eslint', + parserOptions: parserOptions + }, { + code: ` + import React from 'react'; + import PropTypes from 'prop-types'; + const Component = React.createReactClass({ + childContextTypes: { + a: PropTypes.bool, + b: PropTypes.array, + c: PropTypes.func, + d: PropTypes.object, + } + }); + `, + parserOptions: parserOptions + }, { + code: ` + import React from 'react'; + import PropTypes from 'prop-types'; + const Component = React.createReactClass({ + childContextTypes: { + a: PropTypes.bool, + b: PropTypes.array, + c: PropTypes.func, + d: PropTypes.object, + } + }); + `, + parser: 'babel-eslint', + parserOptions: parserOptions + }, { + code: ` + import React from 'react'; + const Component = React.createReactClass({ + propTypes: {}, + childContextTypes: {}, + contextTypes: {}, + componentWillMount() { }, + componentDidMount() { }, + componentWillReceiveProps() { }, + shouldComponentUpdate() { }, + componentWillUpdate() { }, + componentDidUpdate() { }, + componentWillUnmount() { }, + render() { + return
Hello {this.props.name}
; + } + }); + `, + parserOptions: parserOptions + }, { + code: ` + import React from 'react'; + const Component = React.createReactClass({ + propTypes: {}, + childContextTypes: {}, + contextTypes: {}, + componentWillMount() { }, + componentDidMount() { }, + componentWillReceiveProps() { }, + shouldComponentUpdate() { }, + componentWillUpdate() { }, + componentDidUpdate() { }, + componentWillUnmount() { }, + render() { + return
Hello {this.props.name}
; + } + }); + `, + parser: 'babel-eslint', + parserOptions: parserOptions }], invalid: [{ @@ -1369,111 +1469,207 @@ ruleTester.run('no-typos', rule, { }, { message: 'Typo in declared prop type: objectof' }] - }] - /* - // PropTypes declared on a component that is detected through JSDoc comments and is - // declared AFTER the PropTypes assignment - // Commented out since it only works with ESLint 5. - ,{ - code: ` - MyComponent.PROPTYPES = {} - \/** @extends React.Component *\/ - class MyComponent extends BaseComponent {} - `, - parserOptions: parserOptions - }, - */ - /* - // createClass tests below fail, so they're commented out - // --------- - }, { - code: ` - import React from 'react'; - import PropTypes from 'prop-types'; - const Component = React.createClass({ - propTypes: { - a: PropTypes.string.isrequired, - b: PropTypes.shape({ - c: PropTypes.number - }).isrequired - } - }); - `, - parser: 'babel-eslint', - parserOptions: parserOptions, - errors: [{ - message: 'Typo in prop type chain qualifier: isrequired' - }, { - message: 'Typo in prop type chain qualifier: isrequired' - }] - }, { - code: ` - import React from 'react'; - import PropTypes from 'prop-types'; - const Component = React.createClass({ - childContextTypes: { - a: PropTypes.bools, - b: PropTypes.Array, - c: PropTypes.function, - d: PropTypes.objectof, - } - }); - `, - parser: 'babel-eslint', - parserOptions: parserOptions, - errors: [{ - message: 'Typo in declared prop type: bools' - }, { - message: 'Typo in declared prop type: Array' - }, { - message: 'Typo in declared prop type: function' - }, { - message: 'Typo in declared prop type: objectof' - }] - }, { - code: ` - import React from 'react'; - import PropTypes from 'prop-types'; - const Component = React.createClass({ - propTypes: { - a: PropTypes.string.isrequired, - b: PropTypes.shape({ - c: PropTypes.number - }).isrequired - } - }); - `, - parserOptions: parserOptions, - errors: [{ - message: 'Typo in prop type chain qualifier: isrequired' - }, { - message: 'Typo in prop type chain qualifier: isrequired' - }] - }, { - code: ` - import React from 'react'; - import PropTypes from 'prop-types'; - const Component = React.createClass({ - childContextTypes: { - a: PropTypes.bools, - b: PropTypes.Array, - c: PropTypes.function, - d: PropTypes.objectof, - } - }); - `, - parserOptions: parserOptions, - errors: [{ - message: 'Typo in declared prop type: bools' - }, { - message: 'Typo in declared prop type: Array' - }, { - message: 'Typo in declared prop type: function' - }, { - message: 'Typo in declared prop type: objectof' - }] + }, { + code: ` + import React from 'react'; + import PropTypes from 'prop-types'; + const Component = React.createReactClass({ + propTypes: { + a: PropTypes.string.isrequired, + b: PropTypes.shape({ + c: PropTypes.number + }).isrequired + } + }); + `, + parser: 'babel-eslint', + parserOptions: parserOptions, + errors: [{ + message: 'Typo in prop type chain qualifier: isrequired' + }, { + message: 'Typo in prop type chain qualifier: isrequired' }] - // --------- - // createClass tests above fail, so they're commented out - */ + }, { + code: ` + import React from 'react'; + import PropTypes from 'prop-types'; + const Component = React.createReactClass({ + childContextTypes: { + a: PropTypes.bools, + b: PropTypes.Array, + c: PropTypes.function, + d: PropTypes.objectof, + } + }); + `, + parser: 'babel-eslint', + parserOptions: parserOptions, + errors: [{ + message: 'Typo in declared prop type: bools' + }, { + message: 'Typo in declared prop type: Array' + }, { + message: 'Typo in declared prop type: function' + }, { + message: 'Typo in declared prop type: objectof' + }] + }, { + code: ` + import React from 'react'; + import PropTypes from 'prop-types'; + const Component = React.createReactClass({ + propTypes: { + a: PropTypes.string.isrequired, + b: PropTypes.shape({ + c: PropTypes.number + }).isrequired + } + }); + `, + parserOptions: parserOptions, + errors: [{ + message: 'Typo in prop type chain qualifier: isrequired' + }, { + message: 'Typo in prop type chain qualifier: isrequired' + }] + }, { + code: ` + import React from 'react'; + import PropTypes from 'prop-types'; + const Component = React.createReactClass({ + childContextTypes: { + a: PropTypes.bools, + b: PropTypes.Array, + c: PropTypes.function, + d: PropTypes.objectof, + } + }); + `, + parserOptions: parserOptions, + errors: [{ + message: 'Typo in declared prop type: bools' + }, { + message: 'Typo in declared prop type: Array' + }, { + message: 'Typo in declared prop type: function' + }, { + message: 'Typo in declared prop type: objectof' + }] + }, { + code: ` + import React from 'react'; + const Component = React.createReactClass({ + proptypes: {}, + childcontexttypes: {}, + contexttypes: {}, + ComponentWillMount() { }, + ComponentDidMount() { }, + ComponentWillReceiveProps() { }, + ShouldComponentUpdate() { }, + ComponentWillUpdate() { }, + ComponentDidUpdate() { }, + ComponentWillUnmount() { }, + render() { + return
Hello {this.props.name}
; + } + }); + `, + parserOptions: parserOptions, + errors: [{ + message: ERROR_MESSAGE_ES5, + type: 'ObjectExpression' + }, { + message: ERROR_MESSAGE_ES5, + type: 'ObjectExpression' + }, { + message: ERROR_MESSAGE_ES5, + type: 'ObjectExpression' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'Property' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'Property' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'Property' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'Property' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'Property' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'Property' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'Property' + }] + }, { + code: ` + import React from 'react'; + const Component = React.createReactClass({ + proptypes: {}, + childcontexttypes: {}, + contexttypes: {}, + ComponentWillMount() { }, + ComponentDidMount() { }, + ComponentWillReceiveProps() { }, + ShouldComponentUpdate() { }, + ComponentWillUpdate() { }, + ComponentDidUpdate() { }, + ComponentWillUnmount() { }, + render() { + return
Hello {this.props.name}
; + } + }); + `, + parser: 'babel-eslint', + parserOptions: parserOptions, + errors: [{ + message: ERROR_MESSAGE_ES5, + type: 'ObjectExpression' + }, { + message: ERROR_MESSAGE_ES5, + type: 'ObjectExpression' + }, { + message: ERROR_MESSAGE_ES5, + type: 'ObjectExpression' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'Property' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'Property' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'Property' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'Property' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'Property' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'Property' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'Property' + }] + /* + // PropTypes declared on a component that is detected through JSDoc comments and is + // declared AFTER the PropTypes assignment + // Commented out since it only works with ESLint 5. + ,{ + code: ` + MyComponent.PROPTYPES = {} + \/** @extends React.Component *\/ + class MyComponent extends BaseComponent {} + `, + parserOptions: parserOptions + }, + */ + }] });