diff --git a/lib/rules/no-typos.js b/lib/rules/no-typos.js index 8d1e8845fc..1e350113bb 100644 --- a/lib/rules/no-typos.js +++ b/lib/rules/no-typos.js @@ -36,6 +36,9 @@ module.exports = { }, create: Components.detect((context, components, utils) => { + let propTypesPackageName = null; + let reactPackageName = null; + function checkValidPropTypeQualfier(node) { if (node.name !== 'isRequired') { context.report({ @@ -54,38 +57,66 @@ module.exports = { } } + function isPropTypesPackage(node) { + return ( + node.type === 'Identifier' && + node.name === propTypesPackageName + ) || ( + node.type === 'MemberExpression' && + node.property.name === 'PropTypes' && + node.object.name === reactPackageName + ); + } + /* eslint-disable no-use-before-define */ + + function checkValidCallExpression(node) { + const callee = node.callee; + if (callee.type === 'MemberExpression' && callee.property.name === 'shape') { + checkValidPropObject(node.arguments[0]); + } else if (callee.type === 'MemberExpression' && callee.property.name === 'oneOfType') { + const args = node.arguments[0]; + if (args && args.type === 'ArrayExpression') { + args.elements.forEach(el => { + checkValidProp(el); + }); + } + } + } + function checkValidProp(node) { - if (node && node.type === 'MemberExpression' && node.object.type === 'MemberExpression') { - checkValidPropType(node.object.property); - checkValidPropTypeQualfier(node.property); - } else if (node && node.type === 'MemberExpression' && node.object.type === 'Identifier' && node.property.name !== 'isRequired') { - checkValidPropType(node.property); - } else if (node && ( - node.type === 'MemberExpression' && node.object.type === 'CallExpression' || node.type === 'CallExpression' - )) { - if (node.type === 'MemberExpression') { + if ((!propTypesPackageName && !reactPackageName) || !node) { + return; + } + + if (node.type === 'MemberExpression') { + if ( + node.object.type === 'MemberExpression' && + isPropTypesPackage(node.object.object) + ) { // PropTypes.myProp.isRequired + checkValidPropType(node.object.property); checkValidPropTypeQualfier(node.property); - node = node.object; - } - const callee = node.callee; - if (callee.type === 'MemberExpression' && callee.property.name === 'shape') { - checkValidPropObject(node.arguments[0]); - } else if (callee.type === 'MemberExpression' && callee.property.name === 'oneOfType') { - const args = node.arguments[0]; - if (args && args.type === 'ArrayExpression') { - args.elements.forEach(el => checkValidProp(el)); - } + } else if ( + isPropTypesPackage(node.object) && + node.property.name !== 'isRequired' + ) { // PropTypes.myProp + checkValidPropType(node.property); + } else if (node.object.type === 'CallExpression') { + checkValidPropTypeQualfier(node.property); + checkValidCallExpression(node.object); } + } else if (node.type === 'CallExpression') { + checkValidCallExpression(node); } } + /* eslint-enable no-use-before-define */ + function checkValidPropObject (node) { if (node && node.type === 'ObjectExpression') { node.properties.forEach(prop => checkValidProp(prop.value)); } } - /* eslint-enable no-use-before-define */ function reportErrorIfClassPropertyCasingTypo(node, propertyName) { if (propertyName === 'propTypes' || propertyName === 'contextTypes' || propertyName === 'childContextTypes') { @@ -114,6 +145,23 @@ module.exports = { } return { + ImportDeclaration: function(node) { + if (node.source && node.source.value === 'prop-types') { // import PropType from "prop-types" + propTypesPackageName = node.specifiers[0].local.name; + } else if (node.source && node.source.value === 'react') { // import { PropTypes } from "react" + reactPackageName = node.specifiers[0].local.name; + + if (node.specifiers.length >= 1) { + const propTypesSpecifier = node.specifiers.find(specifier => ( + specifier.imported && specifier.imported.name === 'PropTypes' + )); + if (propTypesSpecifier) { + propTypesPackageName = propTypesSpecifier.local.name; + } + } + } + }, + ClassProperty: function(node) { if (!node.static || !utils.isES6Component(node.parent.parent)) { return; diff --git a/tests/lib/rules/no-typos.js b/tests/lib/rules/no-typos.js index bd4bfc5a37..a6d50c033d 100644 --- a/tests/lib/rules/no-typos.js +++ b/tests/lib/rules/no-typos.js @@ -14,7 +14,8 @@ const parserOptions = { ecmaVersion: 6, ecmaFeatures: { jsx: true - } + }, + sourceType: 'module' }; // ----------------------------------------------------------------------------- @@ -260,81 +261,218 @@ ruleTester.run('no-typos', rule, { parser: 'babel-eslint', parserOptions: parserOptions }, { - code: `class Component extends React.Component {}; - Component.propTypes = { - a: PropTypes.number.isRequired - } + code: ` + import PropTypes from "prop-types"; + class Component extends React.Component {}; + Component.propTypes = { + a: PropTypes.number.isRequired + } `, parser: 'babel-eslint', parserOptions: parserOptions }, { - code: `class Component extends React.Component {}; - Component.propTypes = { - e: PropTypes.shape({ - ea: PropTypes.string, - }) - } + code: ` + import PropTypes from "prop-types"; + class Component extends React.Component {}; + Component.propTypes = { + e: PropTypes.shape({ + ea: PropTypes.string, + }) + } `, parser: 'babel-eslint', parserOptions: parserOptions }, { - code: `class Component extends React.Component {}; - Component.propTypes = { - a: PropTypes.string, - b: PropTypes.string.isRequired, - c: PropTypes.shape({ - d: PropTypes.string, - e: PropTypes.number.isRequired, - }).isRequired - } + code: ` + import PropTypes from "prop-types"; + class Component extends React.Component {}; + Component.propTypes = { + a: PropTypes.string, + b: PropTypes.string.isRequired, + c: PropTypes.shape({ + d: PropTypes.string, + e: PropTypes.number.isRequired, + }).isRequired + } `, parser: 'babel-eslint', parserOptions: parserOptions }, { - code: `class Component extends React.Component {}; - Component.propTypes = { - a: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number - ]) - } + code: ` + import PropTypes from "prop-types"; + class Component extends React.Component {}; + Component.propTypes = { + a: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number + ]) + } `, parser: 'babel-eslint', parserOptions: parserOptions }, { - code: `class Component extends React.Component {}; - Component.propTypes = { - a: PropTypes.oneOf([ - 'hello', - 'hi' - ]) - } + code: ` + import PropTypes from "prop-types"; + class Component extends React.Component {}; + Component.propTypes = { + a: PropTypes.oneOf([ + 'hello', + 'hi' + ]) + } `, parser: 'babel-eslint', parserOptions: parserOptions }, { - code: `class Component extends React.Component {}; - Component.childContextTypes = { - a: PropTypes.string, - b: PropTypes.string.isRequired, - c: PropTypes.shape({ - d: PropTypes.string, - e: PropTypes.number.isRequired, - }).isRequired - } + code: ` + import PropTypes from "prop-types"; + class Component extends React.Component {}; + Component.childContextTypes = { + a: PropTypes.string, + b: PropTypes.string.isRequired, + c: PropTypes.shape({ + d: PropTypes.string, + e: PropTypes.number.isRequired, + }).isRequired + } `, parser: 'babel-eslint', parserOptions: parserOptions }, { - code: `class Component extends React.Component {}; - Component.contextTypes = { - a: PropTypes.string, - b: PropTypes.string.isRequired, - c: PropTypes.shape({ - d: PropTypes.string, - e: PropTypes.number.isRequired, - }).isRequired - } + code: ` + import PropTypes from "prop-types"; + class Component extends React.Component {}; + Component.propTypes = { + a: PropTypes.oneOf([ + 'hello', + 'hi' + ]) + } + `, + parser: 'babel-eslint', + parserOptions: parserOptions + }, { + code: ` + import PropTypes from "prop-types"; + class Component extends React.Component {}; + Component.childContextTypes = { + a: PropTypes.string, + b: PropTypes.string.isRequired, + c: PropTypes.shape({ + d: PropTypes.string, + e: PropTypes.number.isRequired, + }).isRequired + } + `, + parserOptions: parserOptions + }, { + code: ` + import PropTypes from "prop-types"; + class Component extends React.Component {}; + Component.contextTypes = { + a: PropTypes.string, + b: PropTypes.string.isRequired, + c: PropTypes.shape({ + d: PropTypes.string, + e: PropTypes.number.isRequired, + }).isRequired + } + `, + parserOptions: parserOptions + }, { + code: ` + import PropTypes from 'prop-types' + import * as MyPropTypes from 'lib/my-prop-types' + class Component extends React.Component {}; + Component.propTypes = { + a: PropTypes.string, + b: MyPropTypes.MYSTRING, + c: MyPropTypes.MYSTRING.isRequired, + } + `, + parserOptions: parserOptions + }, { + code: ` + import PropTypes from "prop-types" + import * as MyPropTypes from 'lib/my-prop-types' + class Component extends React.Component {}; + Component.propTypes = { + b: PropTypes.string, + a: MyPropTypes.MYSTRING, + } + `, + parserOptions: parserOptions + }, { + code: ` + import CustomReact from "react" + class Component extends React.Component {}; + Component.propTypes = { + b: CustomReact.PropTypes.string, + } + `, + parserOptions: parserOptions + }, { + code: ` + import PropTypes from "prop-types"; + class Component extends React.Component {}; + Component.childContextTypes = { + a: PropTypes.string, + b: PropTypes.string.isRequired, + c: PropTypes.shape({ + d: PropTypes.string, + e: PropTypes.number.isRequired, + }).isRequired + } + `, + parser: 'babel-eslint', + parserOptions: parserOptions + }, { + code: ` + import PropTypes from "prop-types"; + class Component extends React.Component {}; + Component.contextTypes = { + a: PropTypes.string, + b: PropTypes.string.isRequired, + c: PropTypes.shape({ + d: PropTypes.string, + e: PropTypes.number.isRequired, + }).isRequired + } + `, + parser: 'babel-eslint', + parserOptions: parserOptions + }, { + code: ` + import PropTypes from 'prop-types' + import * as MyPropTypes from 'lib/my-prop-types' + class Component extends React.Component {}; + Component.propTypes = { + a: PropTypes.string, + b: MyPropTypes.MYSTRING, + c: MyPropTypes.MYSTRING.isRequired, + } + `, + parser: 'babel-eslint', + parserOptions: parserOptions + }, { + code: ` + import PropTypes from "prop-types" + import * as MyPropTypes from 'lib/my-prop-types' + class Component extends React.Component {}; + Component.propTypes = { + b: PropTypes.string, + a: MyPropTypes.MYSTRING, + } + `, + parser: 'babel-eslint', + parserOptions: parserOptions + }, { + code: ` + import CustomReact from "react" + class Component extends React.Component {}; + Component.propTypes = { + b: CustomReact.PropTypes.string, + } `, parser: 'babel-eslint', parserOptions: parserOptions @@ -710,6 +848,7 @@ ruleTester.run('no-typos', rule, { }] }, { code: ` + import PropTypes from "prop-types"; class Component extends React.Component {}; Component.propTypes = { a: PropTypes.Number.isRequired @@ -722,6 +861,7 @@ ruleTester.run('no-typos', rule, { }] }, { code: ` + import PropTypes from "prop-types"; class Component extends React.Component {}; Component.propTypes = { a: PropTypes.number.isrequired @@ -734,6 +874,7 @@ ruleTester.run('no-typos', rule, { }] }, { code: ` + import PropTypes from "prop-types"; class Component extends React.Component {}; Component.propTypes = { a: PropTypes.Number @@ -746,6 +887,7 @@ ruleTester.run('no-typos', rule, { }] }, { code: ` + import PropTypes from "prop-types"; class Component extends React.Component {}; Component.propTypes = { a: PropTypes.shape({ @@ -761,6 +903,7 @@ ruleTester.run('no-typos', rule, { }] }, { code: ` + import PropTypes from "prop-types"; class Component extends React.Component {}; Component.propTypes = { a: PropTypes.oneOfType([ @@ -776,6 +919,7 @@ ruleTester.run('no-typos', rule, { }] }, { code: ` + import PropTypes from "prop-types"; class Component extends React.Component {}; Component.propTypes = { a: PropTypes.bools, @@ -797,6 +941,7 @@ ruleTester.run('no-typos', rule, { }] }, { code: ` + import PropTypes from "prop-types"; class Component extends React.Component {}; Component.childContextTypes = { a: PropTypes.bools, @@ -817,35 +962,408 @@ ruleTester.run('no-typos', rule, { message: 'Typo in declared prop type: objectof' }] }, { - code: `class Component extends React.Component {}; + code: ` + import PropTypes from 'prop-types'; + class Component extends React.Component {}; + Component.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 PropTypes from 'prop-types'; + class Component extends React.Component {}; + Component.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 PropTypes from 'prop-types'; + class Component extends React.Component {}; Component.propTypes = { - a: string.isrequired, - b: shape({ - c: number + a: PropTypes.string.isrequired, + b: PropTypes.shape({ + c: PropTypes.number }).isrequired } `, + parser: 'babel-eslint', parserOptions: parserOptions, errors: [{ - message: 'Typo in declared prop type: isrequired' + message: 'Typo in prop type chain qualifier: isrequired' }, { message: 'Typo in prop type chain qualifier: isrequired' }] }, { - code: `class Component extends React.Component {}; + code: ` + import RealPropTypes from 'prop-types'; + class Component extends React.Component {}; + Component.childContextTypes = { + a: RealPropTypes.bools, + b: RealPropTypes.Array, + c: RealPropTypes.function, + d: RealPropTypes.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'; + class Component extends React.Component {}; Component.propTypes = { - a: string.isrequired, - b: shape({ - c: number + a: React.PropTypes.string.isrequired, + b: React.PropTypes.shape({ + c: React.PropTypes.number }).isrequired } `, parser: 'babel-eslint', parserOptions: parserOptions, errors: [{ - message: 'Typo in declared prop type: isrequired' + message: 'Typo in prop type chain qualifier: isrequired' }, { message: 'Typo in prop type chain qualifier: isrequired' }] + }, { + code: ` + import React from 'react'; + class Component extends React.Component {}; + Component.childContextTypes = { + a: React.PropTypes.bools, + b: React.PropTypes.Array, + c: React.PropTypes.function, + d: React.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 { PropTypes } from 'react'; + class Component extends React.Component {}; + Component.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 { PropTypes } from 'react'; + class Component extends React.Component {}; + Component.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 PropTypes from 'prop-types'; + class Component extends React.Component {}; + Component.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 PropTypes from 'prop-types'; + class Component extends React.Component {}; + Component.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 PropTypes from 'prop-types'; + class Component extends React.Component {}; + Component.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 RealPropTypes from 'prop-types'; + class Component extends React.Component {}; + Component.childContextTypes = { + a: RealPropTypes.bools, + b: RealPropTypes.Array, + c: RealPropTypes.function, + d: RealPropTypes.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'; + class Component extends React.Component {}; + Component.propTypes = { + a: React.PropTypes.string.isrequired, + b: React.PropTypes.shape({ + c: React.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'; + class Component extends React.Component {}; + Component.childContextTypes = { + a: React.PropTypes.bools, + b: React.PropTypes.Array, + c: React.PropTypes.function, + d: React.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 { PropTypes } from 'react'; + class Component extends React.Component {}; + Component.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 { PropTypes } from 'react'; + class Component extends React.Component {}; + Component.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' + }] + }] +/* +// 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' + }] }] +// --------- +// createClass tests above fail, so they're commented out +*/ });