diff --git a/docs/rules/no-typos.md b/docs/rules/no-typos.md index a7dd2b9287..c61cb70496 100644 --- a/docs/rules/no-typos.md +++ b/docs/rules/no-typos.md @@ -1,11 +1,13 @@ # Prevents common casing typos (react/no-typos) -Ensure no casing typos were made declaring static class properties +Ensure no casing typos were made declaring static class properties and lifecycle methods. ## Rule Details -This rule checks whether the declared static class properties related to React components -do not contain any typos. It currently makes sure that the following class properties have +This rule checks whether the declared static class properties and lifecycle methods related to React components +do not contain any typos. + +It currently makes sure that the following class properties have no casing typos: * propTypes @@ -13,6 +15,18 @@ no casing typos: * childContextTypes * defaultProps +and the following react lifecycle methods: + +* componentWillMount +* componentDidMount +* componentWillReceiveProps +* shouldComponentUpdate +* componentWillUpdate +* componentDidUpdate +* componentWillUnmount +* render + + The following patterns are considered warnings: ```js @@ -47,6 +61,18 @@ class MyComponent extends React.Component { class MyComponent extends React.Component { static defaultprops = {} } + +class MyComponent extends React.Component { + componentwillMount() {} +} + +class MyComponent extends React.Component { + ComponentWillReceiveProps() {} +} + +class MyComponent extends React.Component { + componentdidupdate() {} +} ``` The following patterns are not considered warnings: @@ -67,4 +93,16 @@ class MyComponent extends React.Component { class MyComponent extends React.Component { static defaultProps = {} } + +class MyComponent extends React.Component { + componentWillMount() {} +} + +class MyComponent extends React.Component { + componentWillReceiveProps() {} +} + +class MyComponent extends React.Component { + componentDidUpdate() {} +} ``` diff --git a/lib/rules/no-typos.js b/lib/rules/no-typos.js index b4e8bdf2af..60426c4ed6 100644 --- a/lib/rules/no-typos.js +++ b/lib/rules/no-typos.js @@ -10,6 +10,16 @@ const Components = require('../util/Components'); // ------------------------------------------------------------------------------ const STATIC_CLASS_PROPERTIES = ['propTypes', 'contextTypes', 'childContextTypes', 'defaultProps']; +const LIFECYCLE_METHODS = [ + 'componentWillMount', + 'componentDidMount', + 'componentWillReceiveProps', + 'shouldComponentUpdate', + 'componentWillUpdate', + 'componentDidUpdate', + 'componentWillUnmount', + 'render' +]; module.exports = { meta: { @@ -22,7 +32,7 @@ module.exports = { }, create: Components.detect((context, components, utils) => { - function reportErrorIfCasingTypo(node, propertyName) { + function reportErrorIfClassPropertyCasingTypo(node, propertyName) { STATIC_CLASS_PROPERTIES.forEach(CLASS_PROP => { if (propertyName && CLASS_PROP.toLowerCase() === propertyName.toLowerCase() && CLASS_PROP !== propertyName) { context.report({ @@ -33,6 +43,17 @@ module.exports = { }); } + function reportErrorIfLifecycleMethodCasingTypo(node) { + LIFECYCLE_METHODS.forEach(method => { + if (method.toLowerCase() === node.key.name.toLowerCase() && method !== node.key.name) { + context.report({ + node: node, + message: 'Typo in component lifecycle method declaration' + }); + } + }); + } + return { ClassProperty: function(node) { if (!node.static || !utils.isES6Component(node.parent.parent)) { @@ -41,7 +62,7 @@ module.exports = { const tokens = context.getFirstTokens(node, 2); const propertyName = tokens[1].value; - reportErrorIfCasingTypo(node, propertyName); + reportErrorIfClassPropertyCasingTypo(node, propertyName); }, MemberExpression: function(node) { @@ -52,8 +73,16 @@ module.exports = { (utils.isES6Component(relatedComponent.node) || utils.isReturningJSX(relatedComponent.node)) ) { const propertyName = node.property.name; - reportErrorIfCasingTypo(node, propertyName); + reportErrorIfClassPropertyCasingTypo(node, propertyName); + } + }, + + MethodDefinition: function (node) { + if (!utils.isES6Component(node.parent.parent)) { + return; } + + reportErrorIfLifecycleMethodCasingTypo(node); } }; }) diff --git a/tests/lib/rules/no-typos.js b/tests/lib/rules/no-typos.js index 7883352453..1e0efa87c5 100644 --- a/tests/lib/rules/no-typos.js +++ b/tests/lib/rules/no-typos.js @@ -22,6 +22,7 @@ const parserOptions = { // ----------------------------------------------------------------------------- const ERROR_MESSAGE = 'Typo in static class property declaration'; +const ERROR_MESSAGE_LIFECYCLE_METHOD = 'Typo in component lifecycle method declaration'; const ruleTester = new RuleTester(); ruleTester.run('no-typos', rule, { @@ -181,6 +182,64 @@ ruleTester.run('no-typos', rule, { 'First[defautProps] = {};' ].join('\n'), parserOptions: parserOptions + }, { + code: [ + 'class Hello extends React.Component {', + ' componentWillMount() { }', + ' componentDidMount() { }', + ' componentWillReceiveProps() { }', + ' shouldComponentUpdate() { }', + ' componentWillUpdate() { }', + ' componentDidUpdate() { }', + ' componentWillUnmount() { }', + ' render() {', + ' return
Hello {this.props.name}
;', + ' }', + '}' + ].join('\n'), + parserOptions: parserOptions + }, { + code: [ + 'class MyClass {', + ' componentWillMount() { }', + ' componentDidMount() { }', + ' componentWillReceiveProps() { }', + ' shouldComponentUpdate() { }', + ' componentWillUpdate() { }', + ' componentDidUpdate() { }', + ' componentWillUnmount() { }', + ' render() { }', + '}' + ].join('\n'), + parserOptions: parserOptions + }, { + code: [ + 'class MyClass {', + ' componentwillmount() { }', + ' componentdidmount() { }', + ' componentwillreceiveprops() { }', + ' shouldcomponentupdate() { }', + ' componentwillupdate() { }', + ' componentdidupdate() { }', + ' componentwillUnmount() { }', + ' render() { }', + '}' + ].join('\n'), + parserOptions: parserOptions + }, { + code: [ + 'class MyClass {', + ' Componentwillmount() { }', + ' Componentdidmount() { }', + ' Componentwillreceiveprops() { }', + ' Shouldcomponentupdate() { }', + ' Componentwillupdate() { }', + ' Componentdidupdate() { }', + ' ComponentwillUnmount() { }', + ' Render() { }', + '}' + ].join('\n'), + parserOptions: parserOptions }], invalid: [{ @@ -367,5 +426,122 @@ ruleTester.run('no-typos', rule, { ].join('\n'), parserOptions: parserOptions, errors: [{message: ERROR_MESSAGE}] + }, { + code: [ + 'class Hello extends React.Component {', + ' ComponentWillMount() { }', + ' ComponentDidMount() { }', + ' ComponentWillReceiveProps() { }', + ' ShouldComponentUpdate() { }', + ' ComponentWillUpdate() { }', + ' ComponentDidUpdate() { }', + ' ComponentWillUnmount() { }', + ' render() {', + ' return
Hello {this.props.name}
;', + ' }', + '}' + ].join('\n'), + parserOptions: parserOptions, + errors: [{ + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }] + }, { + code: [ + 'class Hello extends React.Component {', + ' Componentwillmount() { }', + ' Componentdidmount() { }', + ' Componentwillreceiveprops() { }', + ' Shouldcomponentupdate() { }', + ' Componentwillupdate() { }', + ' Componentdidupdate() { }', + ' Componentwillunmount() { }', + ' Render() {', + ' return
Hello {this.props.name}
;', + ' }', + '}' + ].join('\n'), + parserOptions: parserOptions, + errors: [{ + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }] + }, { + code: [ + 'class Hello extends React.Component {', + ' componentwillmount() { }', + ' componentdidmount() { }', + ' componentwillreceiveprops() { }', + ' shouldcomponentupdate() { }', + ' componentwillupdate() { }', + ' componentdidupdate() { }', + ' componentwillunmount() { }', + ' render() {', + ' return
Hello {this.props.name}
;', + ' }', + '}' + ].join('\n'), + parserOptions: parserOptions, + errors: [{ + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }, { + message: ERROR_MESSAGE_LIFECYCLE_METHOD, + type: 'MethodDefinition' + }] }] });