diff --git a/README.md b/README.md index 2109a9a16e..2dd2cddb5a 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ Finally, enable all of the rules that you would like to use. Use [our preset](# * [react/no-redundant-should-component-update](docs/rules/no-redundant-should-component-update.md): Prevent usage of `shouldComponentUpdate` when extending React.PureComponent * [react/no-render-return-value](docs/rules/no-render-return-value.md): Prevent usage of the return value of `React.render` * [react/no-set-state](docs/rules/no-set-state.md): Prevent usage of `setState` +* [react/no-typos](docs/rules/no-typos.md): Prevent common casing typos * [react/no-string-refs](docs/rules/no-string-refs.md): Prevent using string references in `ref` attribute. * [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) diff --git a/docs/rules/no-typos.md b/docs/rules/no-typos.md new file mode 100644 index 0000000000..a7dd2b9287 --- /dev/null +++ b/docs/rules/no-typos.md @@ -0,0 +1,70 @@ +# Prevents common casing typos (react/no-typos) + +Ensure no casing typos were made declaring static class properties + +## 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 +no casing typos: + +* propTypes +* contextTypes +* childContextTypes +* defaultProps + +The following patterns are considered warnings: + +```js +class MyComponent extends React.Component { + static PropTypes = {} +} + +class MyComponent extends React.Component { + static proptypes = {} +} + +class MyComponent extends React.Component { + static ContextTypes = {} +} + +class MyComponent extends React.Component { + static contexttypes = {} +} + +class MyComponent extends React.Component { + static ChildContextTypes = {} +} + +class MyComponent extends React.Component { + static childcontexttypes = {} +} + +class MyComponent extends React.Component { + static DefaultProps = {} +} + +class MyComponent extends React.Component { + static defaultprops = {} +} +``` + +The following patterns are not considered warnings: + +```js +class MyComponent extends React.Component { + static propTypes = {} +} + +class MyComponent extends React.Component { + static contextTypes = {} +} + +class MyComponent extends React.Component { + static childContextTypes = {} +} + +class MyComponent extends React.Component { + static defaultProps = {} +} +``` diff --git a/index.js b/index.js index 3cb6f72f26..7bd88043e0 100644 --- a/index.js +++ b/index.js @@ -65,7 +65,8 @@ const allRules = { 'void-dom-elements-no-children': require('./lib/rules/void-dom-elements-no-children'), 'jsx-tag-spacing': require('./lib/rules/jsx-tag-spacing'), 'no-redundant-should-component-update': require('./lib/rules/no-redundant-should-component-update'), - 'boolean-prop-naming': require('./lib/rules/boolean-prop-naming') + 'boolean-prop-naming': require('./lib/rules/boolean-prop-naming'), + 'no-typos': require('./lib/rules/no-typos') }; function filterRules(rules, predicate) { diff --git a/lib/rules/no-typos.js b/lib/rules/no-typos.js new file mode 100644 index 0000000000..d3cce13714 --- /dev/null +++ b/lib/rules/no-typos.js @@ -0,0 +1,60 @@ +/** + * @fileoverview Prevent common casing typos + */ +'use strict'; + +const Components = require('../util/Components'); + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +const STATIC_CLASS_PROPERTIES = ['propTypes', 'contextTypes', 'childContextTypes', 'defaultProps']; + +module.exports = { + meta: { + docs: { + description: 'Prevent common casing typos', + category: 'Stylistic Issues', + recommended: false + }, + schema: [] + }, + + create: Components.detect(function(context, components, utils) { + function reportErrorIfCasingTypo(node, propertyName) { + STATIC_CLASS_PROPERTIES.forEach(function(CLASS_PROP) { + if (propertyName && CLASS_PROP.toLowerCase() === propertyName.toLowerCase() && CLASS_PROP !== propertyName) { + context.report({ + node: node, + message: 'Typo in static class property declaration' + }); + } + }); + } + + return { + ClassProperty: function(node) { + if (!node.static || !utils.isES6Component(node.parent.parent)) { + return; + } + + const tokens = context.getFirstTokens(node, 2); + const propertyName = tokens[1].value; + reportErrorIfCasingTypo(node, propertyName); + }, + + MemberExpression: function(node) { + const relatedComponent = utils.getRelatedComponent(node); + + if ( + relatedComponent && + (utils.isES6Component(relatedComponent.node) || utils.isReturningJSX(relatedComponent.node)) + ) { + const propertyName = node.property.name; + reportErrorIfCasingTypo(node, propertyName); + } + } + }; + }) +}; diff --git a/lib/util/Components.js b/lib/util/Components.js index 31f634ea3e..1346fb9ef1 100644 --- a/lib/util/Components.js +++ b/lib/util/Components.js @@ -319,13 +319,19 @@ function componentRule(rule, context) { * @param {ASTNode} ASTnode The AST node being checked */ findReturnStatement: function(node) { - if (!node.value || !node.value.body || !node.value.body.body) { + if ( + (!node.value || !node.value.body || !node.value.body.body) && + (!node.body || !node.body.body) + ) { return false; } - let i = node.value.body.body.length - 1; + + const bodyNodes = (node.value ? node.value.body.body : node.body.body); + + let i = bodyNodes.length - 1; for (; i >= 0; i--) { - if (node.value.body.body[i].type === 'ReturnStatement') { - return node.value.body.body[i]; + if (bodyNodes[i].type === 'ReturnStatement') { + return bodyNodes[i]; } } return false; diff --git a/tests/lib/rules/no-typos.js b/tests/lib/rules/no-typos.js new file mode 100644 index 0000000000..7883352453 --- /dev/null +++ b/tests/lib/rules/no-typos.js @@ -0,0 +1,371 @@ +/** + * @fileoverview Tests for no-typos + */ +'use strict'; + +// ----------------------------------------------------------------------------- +// Requirements +// ----------------------------------------------------------------------------- + +const rule = require('../../../lib/rules/no-typos'); +const RuleTester = require('eslint').RuleTester; + +const parserOptions = { + ecmaVersion: 6, + ecmaFeatures: { + jsx: true + } +}; + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +const ERROR_MESSAGE = 'Typo in static class property declaration'; + +const ruleTester = new RuleTester(); +ruleTester.run('no-typos', rule, { + + valid: [{ + code: [ + 'class First {', + ' static PropTypes = {key: "myValue"};', + ' static ContextTypes = {key: "myValue"};', + ' static ChildContextTypes = {key: "myValue"};', + ' static DefaultProps = {key: "myValue"};', + '}' + ].join('\n'), + parser: 'babel-eslint', + parserOptions: parserOptions + }, + { + code: [ + 'class First {}', + 'First.PropTypes = {key: "myValue"};', + 'First.ContextTypes = {key: "myValue"};', + 'First.ChildContextTypes = {key: "myValue"};', + 'First.DefaultProps = {key: "myValue"};' + ].join('\n'), + parserOptions: parserOptions + }, { + code: [ + 'class First extends React.Component {', + ' static propTypes = {key: "myValue"};', + ' static contextTypes = {key: "myValue"};', + ' static childContextTypes = {key: "myValue"};', + ' static defaultProps = {key: "myValue"};', + '}' + ].join('\n'), + parser: 'babel-eslint', + parserOptions: parserOptions + }, { + code: [ + 'class First extends React.Component {}', + 'First.propTypes = {key: "myValue"};', + 'First.contextTypes = {key: "myValue"};', + 'First.childContextTypes = {key: "myValue"};', + 'First.defaultProps = {key: "myValue"};' + ].join('\n'), + parserOptions: parserOptions + }, { + code: [ + 'class MyClass {', + ' propTypes = {key: "myValue"};', + ' contextTypes = {key: "myValue"};', + ' childContextTypes = {key: "myValue"};', + ' defaultProps = {key: "myValue"};', + '}' + ].join('\n'), + parser: 'babel-eslint', + parserOptions: parserOptions + }, { + code: [ + 'class MyClass {', + ' PropTypes = {key: "myValue"};', + ' ContextTypes = {key: "myValue"};', + ' ChildContextTypes = {key: "myValue"};', + ' DefaultProps = {key: "myValue"};', + '}' + ].join('\n'), + parser: 'babel-eslint', + parserOptions: parserOptions + }, { + code: [ + 'class MyClass {', + ' proptypes = {key: "myValue"};', + ' contexttypes = {key: "myValue"};', + ' childcontextypes = {key: "myValue"};', + ' defaultprops = {key: "myValue"};', + '}' + ].join('\n'), + parser: 'babel-eslint', + parserOptions: parserOptions + }, { + code: [ + 'class MyClass {', + ' static PropTypes() {};', + ' static ContextTypes() {};', + ' static ChildContextTypes() {};', + ' static DefaultProps() {};', + '}' + ].join('\n'), + parser: 'babel-eslint', + parserOptions: parserOptions + }, { + code: [ + 'class MyClass {', + ' static proptypes() {};', + ' static contexttypes() {};', + ' static childcontexttypes() {};', + ' static defaultprops() {};', + '}' + ].join('\n'), + parser: 'babel-eslint', + parserOptions: parserOptions + }, { + code: [ + 'class MyClass {}', + 'MyClass.prototype.PropTypes = function() {};', + 'MyClass.prototype.ContextTypes = function() {};', + 'MyClass.prototype.ChildContextTypes = function() {};', + 'MyClass.prototype.DefaultProps = function() {};' + ].join('\n'), + parserOptions: parserOptions + }, { + code: [ + 'class MyClass {}', + 'MyClass.PropTypes = function() {};', + 'MyClass.ContextTypes = function() {};', + 'MyClass.ChildContextTypes = function() {};', + 'MyClass.DefaultProps = function() {};' + ].join('\n'), + parserOptions: parserOptions + }, { + code: [ + 'function MyRandomFunction() {}', + 'MyRandomFunction.PropTypes = {};', + 'MyRandomFunction.ContextTypes = {};', + 'MyRandomFunction.ChildContextTypes = {};', + 'MyRandomFunction.DefaultProps = {};' + ].join('\n'), + parserOptions: parserOptions + }, { + code: [ + 'class First extends React.Component {}', + 'First["prop" + "Types"] = {};', + 'First["context" + "Types"] = {};', + 'First["childContext" + "Types"] = {};', + 'First["default" + "Props"] = {};' + ].join('\n'), + parserOptions: parserOptions + }, { + code: [ // This case is currently not supported + 'class First extends React.Component {}', + 'First["PROP" + "TYPES"] = {};', + 'First["CONTEXT" + "TYPES"] = {};', + 'First["CHILDCONTEXT" + "TYPES"] = {};', + 'First["DEFAULT" + "PROPS"] = {};' + ].join('\n'), + parserOptions: parserOptions + }, { + code: [ // This case is currently not supported + 'const propTypes = "PROPTYPES"', + 'const contextTypes = "CONTEXTTYPES"', + 'const childContextTypes = "CHILDCONTEXTTYPES"', + 'const defautProps = "DEFAULTPROPS"', + '', + 'class First extends React.Component {}', + 'First[propTypes] = {};', + 'First[contextTypes] = {};', + 'First[childContextTypes] = {};', + 'First[defautProps] = {};' + ].join('\n'), + parserOptions: parserOptions + }], + + invalid: [{ + code: [ + 'class Component extends React.Component {', + ' static PropTypes = {};', + '}' + ].join('\n'), + parser: 'babel-eslint', + parserOptions: parserOptions, + errors: [{message: ERROR_MESSAGE}] + }, { + code: [ + 'class Component extends React.Component {}', + 'Component.PropTypes = {}' + ].join('\n'), + parserOptions: parserOptions, + errors: [{message: ERROR_MESSAGE}] + }, { + code: [ + 'function MyComponent() { return (