diff --git a/docs/rules/jsx-no-invalid-props.md b/docs/rules/jsx-no-invalid-props.md index d101eb4..94ea897 100644 --- a/docs/rules/jsx-no-invalid-props.md +++ b/docs/rules/jsx-no-invalid-props.md @@ -1,10 +1,14 @@ # JSX No invalid props (jsx-no-invalid-props) -A quick check to see if a typo has been made on a PropType which at the minimum is annoying, but can also lead to debugging red herrings +Validates all `x.propTypes = { ... }` and `x.contextTypes = { ... }` statements for proper syntax (also as class members). ## Rule Details -This rule checks propTypes on a class and validates the PropTypes against a static list from the React docs +This rule checks propTypes on a class and validates the PropTypes against a static list from the [React docs](https://github.com/facebook/react/blob/master/docs/docs/typechecking-with-proptypes.md). + +It supports arrays, shapes etc, nested to any depth. + +The validation is rather strict; hopefully it will not generate false error reports. Please [report](https://github.com/craigbilner/eslint-plugin-react-props/issues) any broken error reports you may encounter. ## Rule Options diff --git a/lib/rules/jsx-no-invalid-props.js b/lib/rules/jsx-no-invalid-props.js index 50f3268..2127d57 100644 --- a/lib/rules/jsx-no-invalid-props.js +++ b/lib/rules/jsx-no-invalid-props.js @@ -12,6 +12,15 @@ const utils = require('../utilities'); //------------------------------------------------------------------------------ module.exports = (context) => { + /* + 1 = proptype + 2 = invalid (bad spelling) + 3 = function(proptype | function) + 4 = function(jstype) + 5 = function(array-with-strings) + 6 = function(array-with-proptypes) + 7 = function(object-with-proptypes) + */ const validProps = { array: 1, bool: 1, @@ -22,59 +31,149 @@ module.exports = (context) => { object: 1, obj: 2, string: 1, + symbol: 1, any: 1, - arrayOf: 1, + arrayOf: 3, element: 1, - instanceOf: 1, + instanceOf: 4, node: 1, - objectOf: 1, - oneOf: 1, - oneOfType: 1, - shape: 1, + objectOf: 3, + oneOf: 5, + oneOfType: 6, + shape: 7, }; - function isExpectedReactPropSyntax(declaration) { - return declaration.value.object && (( - declaration.value.object.name === 'PropTypes' - ) - || ( - declaration.value.object.object.name === 'React' - && declaration.value.object.property.name === 'PropTypes' - ) - || ( - declaration.value.object.object.name === 'PropTypes' - && declaration.value.property.name === 'isRequired' - ) - || ( - declaration.value.object.object.object.name === 'React' - && declaration.value.object.object.property.name === 'PropTypes' - && declaration.value.property.name === 'isRequired' - )); - } - function getPropValue(propName) { return validProps[propName]; } - function checkProperties(declarations) { - declarations.forEach((declaration) => { - if (isExpectedReactPropSyntax(declaration)) { - let propName = ''; - let propValue = 0; - if (declaration.value.property.name === 'isRequired') { - propName = declaration.value.object.property.name; - } else { - propName = declaration.value.property.name; - } + const argType = { + 3: 'proptype or function', + 4: 'JavaScript type', + 5: 'array with strings', + 6: 'array with proptypes', + 7: 'object with proptypes as values', + }; - propValue = getPropValue(propName); + function isIdentifier(n, ident) { + return n.type === 'Identifier' && (ident === undefined || n.name === ident); + } - if (propValue !== 1) { - context.report(declaration, `${propName} is not a valid PropType`); - } - } else { - context.report(declaration, 'unknown use of PropTypes'); + function isIdentifierOrRawLiteral(n) { + if (n.type === 'Identifier') { + return n.name; + } + if (n.type === 'Literal') { + return n.raw; + } + return ''; + } + + function isPropTypesExpression(n) { + if (n.type !== 'MemberExpression') return false; + if (isIdentifier(n.object, 'PropTypes')) return true; + return n.object.type === 'MemberExpression' && isIdentifier(n.object.object, 'React') && isIdentifier(n.object.property, 'PropTypes'); + } + + function isArray(n) { + return n.type === 'ArrayExpression'; + } + + function isObject(n) { + return n.type === 'ObjectExpression'; + } + + function assertExpectedReactPropSyntax(key, origDeclaration, allowIsRequired) { + let declaration = origDeclaration; + if (declaration.type === 'MemberExpression' && + isIdentifier(declaration.property, 'isRequired')) { + if (!allowIsRequired) { + return context.report(origDeclaration, `property '${key}': please move isRequired qualifier outside the oneOfType() declaration e.g. '(React.)PropTypes.oneOfType(...).isRequired'`); + } + declaration = declaration.object; + } + if (declaration.type === 'CallExpression') { + if (!isPropTypesExpression(declaration.callee) || + !isIdentifier(declaration.callee.property)) { + return context.report(origDeclaration, `property '${key}': unsupported proptypes call syntax`); + } + const propType = declaration.callee.property.name; + const propValue = getPropValue(propType); + switch (propValue) { + case 1: return context.report(origDeclaration, `property '${key}': ${propType} is a simple prop type, should not be called like a function`); + case 2: return context.report(origDeclaration, `property '${key}': ${propType} is a misspelled prop type. Please correct spelling`); + case 3: case 4: case 5: case 6: case 7: break; + default: return context.report(origDeclaration, `property '${key}': ${propType} is not a known prop type`); + } + if (declaration.arguments.length !== 1) { + return context.report(origDeclaration, `property '${key}': ${propType} expects exactly one argument of type ${argType[propValue]}`); + } + const arg = declaration.arguments[0]; + if (((propValue === 3 || propValue === 4) && (isArray(arg) || isObject(arg))) + || ((propValue === 5 || propValue === 6) && !isArray(arg)) + || (propValue === 7 && !isObject(arg))) { + return context.report(origDeclaration, `property '${key}': ${propType} expects exactly one argument of type ${argType[propValue]}`); } + switch (propValue) { + case 3: + assertExpectedReactPropSyntax(key, arg, true); + break; + case 4: + if (arg.type === 'Literal') { + context.report(origDeclaration, `property '${key}': ${propType} argument must not be a literal`); + } + break; + case 5: + arg.elements.every((elem) => { + if (elem.type !== 'Literal') { + context.report(origDeclaration, `property '${key}': ${propType} array entries must be literals`); + return false; + } + return true; + }); + break; + case 6: + arg.elements.forEach(elem => assertExpectedReactPropSyntax(key, elem, false)); + break; + case 7: + arg.properties.every((elem) => { + if (elem.type !== 'Property') { + context.report(origDeclaration, `property '${key}': ${propType} object must only consist of properties`); + return false; + } + if (!isIdentifierOrRawLiteral(elem.key)) { + context.report(origDeclaration, `property '${key}': ${propType} properties must be identifiers or literals`); + } + assertExpectedReactPropSyntax(key, elem.value, true); + return true; + }); + break; + default: + throw new Error(`Unhandled propValue ${propValue}`); + } + } else if (isPropTypesExpression(declaration) && isIdentifier(declaration.property)) { + const propType = declaration.property.name; + const propValue = getPropValue(propType); + switch (propValue) { + case 1: return null; + case 2: return context.report(origDeclaration, `property '${key}': ${propType} is a misspelled prop type. Please correct spelling`); + case 3: case 4: case 5: case 6: case 7: return context.report(origDeclaration, `property '${key}': ${propType} expects to be called like a function with an argument of type ${argType[propValue]}`); + default: return context.report(origDeclaration, `property '${key}': ${propType} is not a valid PropType`); + } + } else if (declaration.type === 'FunctionExpression' || + declaration.type === 'ArrowFunctionExpression') { + // ok + } else { + // console.log(declaration); + return context.report(origDeclaration, `property '${key}': unknown use of PropTypes`); + } + return null; + } + + function checkProperties(declarations) { + declarations.forEach((declaration) => { + const identifierOrRawLiteral = isIdentifierOrRawLiteral(declaration.key); + assertExpectedReactPropSyntax(identifierOrRawLiteral, declaration.value, true); }); } diff --git a/lib/utilities.js b/lib/utilities.js index e64d35d..b8a9a63 100644 --- a/lib/utilities.js +++ b/lib/utilities.js @@ -2,6 +2,10 @@ Borrowed from https://github.com/yannickcr/eslint-plugin-react */ +function isTypesDecl(str) { + return str === 'propTypes' || str === 'contextTypes'; +} + /** * Checks if node is `propTypes` declaration * @param {ASTNode} node The AST node being checked. @@ -12,13 +16,13 @@ function isPropTypesDeclaration(context, node) { // (babel-eslint does not expose property name so we have to rely on tokens) if (node.type === 'ClassProperty') { const tokens = context.getFirstTokens(node, 2); - if (tokens[0].value === 'propTypes' || tokens[1].value === 'propTypes') { + if (isTypesDecl(tokens[0].value) || isTypesDecl(tokens[1].value)) { return true; } return false; } - return Boolean(node && node.name === 'propTypes'); + return Boolean(node && isTypesDecl(node.name)); } module.exports = { diff --git a/tests/lib/rules/jsx-no-invalid-props.js b/tests/lib/rules/jsx-no-invalid-props.js index 18cd242..6e139eb 100644 --- a/tests/lib/rules/jsx-no-invalid-props.js +++ b/tests/lib/rules/jsx-no-invalid-props.js @@ -1,6 +1,8 @@ 'use strict'; require('babel-eslint'); +const fs = require('fs'); +const path = require('path'); const rule = require('../../../lib/rules/jsx-no-invalid-props'); const RuleTester = require('eslint').RuleTester; @@ -10,52 +12,27 @@ const ruleTester = new RuleTester(); ruleTester.run('jsx-no-invalid-props', rule, { valid: [ { - code: [ - 'import React, {PropTypes} from "react";', - 'export default class TestComponent extends React.Component {', - 'constructor(props) {', - 'super(props);', - 'this.state = {};', - '}', - 'render() {', - 'return (', - '
', - ');', - '}', - '}', - 'TestComponent.propTypes = {', - 'a: PropTypes.array,', - 'b: PropTypes.bool,', - 'c: PropTypes.func,', - 'd: PropTypes.number,', - 'e: PropTypes.object,', - 'f: PropTypes.string,', - 'g: PropTypes.any,', - 'h: PropTypes.arrayOf,', - 'i: PropTypes.element,', - 'j: PropTypes.instanceOf,', - 'k: PropTypes.node,', - 'l: PropTypes.objectOf,', - 'm: PropTypes.oneOf,', - 'n: PropTypes.oneOfType,', - 'o: PropTypes.shape,', - 'p: PropTypes.array,', - 'q: PropTypes.bool,', - 'r: PropTypes.func,', - 's: PropTypes.number,', - 't: PropTypes.object,', - 'u: PropTypes.string,', - 'v: PropTypes.any,', - 'w: PropTypes.arrayOf,', - 'x: PropTypes.element,', - 'y: PropTypes.instanceOf,', - 'z: PropTypes.node,', - 'aa: PropTypes.objectOf,', - 'ab: PropTypes.oneOf,', - 'ac: PropTypes.oneOfType,', - 'ad: PropTypes.shape', - '};', - ].join('\n'), + code: fs.readFileSync(path.join(__dirname, 'jsx-no-invalid-props', 'valid-1.js'), 'utf-8'), + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + }, + { + code: fs.readFileSync(path.join(__dirname, 'jsx-no-invalid-props', 'valid-2.js'), 'utf-8'), + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + }, + { + code: fs.readFileSync(path.join(__dirname, 'jsx-no-invalid-props', 'ignored-1.js'), 'utf-8'), parserOptions: { ecmaVersion: 6, sourceType: 'module', @@ -67,23 +44,7 @@ ruleTester.run('jsx-no-invalid-props', rule, { ], invalid: [ { - code: [ - 'import React, {PropTypes} from "react";', - 'export default class TestComponent extends React.Component {', - 'constructor(props) {', - 'super(props);', - 'this.state = {};', - '}', - 'render() {', - 'return (', - '
', - ');', - '}', - '}', - 'TestComponent.propTypes = {', - 'a: PropTypes.arr,', - '};', - ].join('\n'), + code: fs.readFileSync(path.join(__dirname, 'jsx-no-invalid-props', 'invalid-2.js'), 'utf-8'), parserOptions: { ecmaVersion: 6, sourceType: 'module', @@ -93,31 +54,81 @@ ruleTester.run('jsx-no-invalid-props', rule, { }, errors: [ { - message: 'arr is not a valid PropType', - line: 14, - column: 1, - type: 'Property', + message: 'property \'h\': arrayOf expects to be called like a function with an argument of type proptype or function', + line: 15, + column: 6, + type: 'MemberExpression', + }, + { + message: 'property \'j\': instanceOf expects to be called like a function with an argument of type JavaScript type', + line: 17, + column: 6, + type: 'MemberExpression', + }, + { + message: 'property \'l\': objectOf expects to be called like a function with an argument of type proptype or function', + line: 19, + column: 6, + type: 'MemberExpression', + }, + { + message: 'property \'m\': oneOf expects to be called like a function with an argument of type array with strings', + line: 20, + column: 6, + type: 'MemberExpression', + }, + { + message: 'property \'n\': oneOfType expects to be called like a function with an argument of type array with proptypes', + line: 21, + column: 6, + type: 'MemberExpression', + }, + { + message: 'property \'o\': shape expects to be called like a function with an argument of type object with proptypes as values', + line: 22, + column: 6, + type: 'MemberExpression', + }, + { + message: 'property \'w\': arrayOf expects to be called like a function with an argument of type proptype or function', + line: 30, + column: 6, + type: 'MemberExpression', + }, + { + message: 'property \'y\': instanceOf expects to be called like a function with an argument of type JavaScript type', + line: 32, + column: 6, + type: 'MemberExpression', + }, + { + message: 'property \'aa\': objectOf expects to be called like a function with an argument of type proptype or function', + line: 34, + column: 7, + type: 'MemberExpression', + }, + { + message: 'property \'ab\': oneOf expects to be called like a function with an argument of type array with strings', + line: 35, + column: 7, + type: 'MemberExpression', + }, + { + message: 'property \'ac\': oneOfType expects to be called like a function with an argument of type array with proptypes', + line: 36, + column: 7, + type: 'MemberExpression', + }, + { + message: 'property \'ad\': shape expects to be called like a function with an argument of type object with proptypes as values', + line: 37, + column: 7, + type: 'MemberExpression', }, ], }, { - code: [ - 'import React, {PropTypes} from "react";', - 'export default class TestComponent extends React.Component {', - 'constructor(props) {', - 'super(props);', - 'this.state = {};', - '}', - 'render() {', - 'return (', - '
', - ');', - '}', - '}', - 'TestComponent.propTypes = {', - 'a: PropTypes,', - '};', - ].join('\n'), + code: fs.readFileSync(path.join(__dirname, 'jsx-no-invalid-props', 'invalid-1.js'), 'utf-8'), parserOptions: { ecmaVersion: 6, sourceType: 'module', @@ -127,10 +138,292 @@ ruleTester.run('jsx-no-invalid-props', rule, { }, errors: [ { - message: 'unknown use of PropTypes', + message: 'property \'badBool\': boolean is a misspelled prop type. Please correct spelling', line: 14, - column: 1, - type: 'Property', + column: 12, + type: 'MemberExpression', + }, + { + message: 'property \'badFunc\': function is a misspelled prop type. Please correct spelling', + line: 15, + column: 12, + type: 'MemberExpression', + }, + { + message: 'property \'badUnknown\': unknown is not a valid PropType', + line: 16, + column: 15, + type: 'MemberExpression', + }, + { + message: 'property \'badMessage\': instanceOf argument must not be a literal', + line: 17, + column: 15, + type: 'CallExpression', + }, + { + message: 'property \'badEnum\': oneOf array entries must be literals', + line: 18, + column: 12, + type: 'CallExpression', + }, + { + message: 'property \'badUnion\': please move isRequired qualifier outside the oneOfType() declaration e.g. \'(React.)PropTypes.oneOfType(...).isRequired\'', + line: 20, + column: 5, + type: 'MemberExpression', + }, + { + message: 'property \'badUnion\': please move isRequired qualifier outside the oneOfType() declaration e.g. \'(React.)PropTypes.oneOfType(...).isRequired\'', + line: 21, + column: 5, + type: 'MemberExpression', + }, + { + message: 'property \'badArrayOf\': unknown use of PropTypes', + line: 23, + column: 39, + type: 'Literal', + }, + { + message: 'property \'badObjectOf\': unknown use of PropTypes', + line: 24, + column: 41, + type: 'Literal', + }, + { + message: 'property \'badObjectWithShape\': unknown use of PropTypes', + line: 26, + column: 12, + type: 'Literal', + }, + { + message: 'property \'badObjectWithShape\': unknown use of PropTypes', + line: 27, + column: 15, + type: 'Literal', + }, + { + message: 'property \'nonFuncArray\': array is a simple prop type, should not be called like a function', + line: 30, + column: 17, + type: 'CallExpression', + }, + { + message: 'property \'nonFuncBool\': bool is a simple prop type, should not be called like a function', + line: 31, + column: 16, + type: 'CallExpression', + }, + { + message: 'property \'nonFuncFunc\': func is a simple prop type, should not be called like a function', + line: 32, + column: 16, + type: 'CallExpression', + }, + { + message: 'property \'nonFuncNumber\': number is a simple prop type, should not be called like a function', + line: 33, + column: 18, + type: 'CallExpression', + }, + { + message: 'property \'nonFuncObject\': object is a simple prop type, should not be called like a function', + line: 34, + column: 18, + type: 'CallExpression', + }, + { + message: 'property \'nonFuncString\': string is a simple prop type, should not be called like a function', + line: 35, + column: 18, + type: 'CallExpression', + }, + { + message: 'property \'nonFuncSymbol\': symbol is a simple prop type, should not be called like a function', + line: 36, + column: 18, + type: 'CallExpression', + }, + { + message: 'property \'nonFuncNode\': node is a simple prop type, should not be called like a function', + line: 37, + column: 16, + type: 'CallExpression', + }, + { + message: 'property \'nonFuncElement\': element is a simple prop type, should not be called like a function', + line: 38, + column: 19, + type: 'CallExpression', + }, + { + message: 'property \'nonFuncAny\': any is a simple prop type, should not be called like a function', + line: 39, + column: 15, + type: 'CallExpression', + }, + { + message: 'property \'missingArgMessage\': instanceOf expects exactly one argument of type JavaScript type', + line: 41, + column: 22, + type: 'CallExpression', + }, + { + message: 'property \'missingArgEnum\': oneOf expects exactly one argument of type array with strings', + line: 42, + column: 19, + type: 'CallExpression', + }, + { + message: 'property \'missingArgUnion\': oneOfType expects exactly one argument of type array with proptypes', + line: 43, + column: 20, + type: 'CallExpression', + }, + { + message: 'property \'missingArgArrayOf\': arrayOf expects exactly one argument of type proptype or function', + line: 44, + column: 22, + type: 'CallExpression', + }, + { + message: 'property \'missingArgObjectOf\': objectOf expects exactly one argument of type proptype or function', + line: 45, + column: 23, + type: 'CallExpression', + }, + { + message: 'property \'missingArgObjectWithShape\': shape expects exactly one argument of type object with proptypes as values', + line: 46, + column: 30, + type: 'CallExpression', + }, + { + message: 'property \'tooManyArgMessage\': instanceOf expects exactly one argument of type JavaScript type', + line: 48, + column: 22, + type: 'CallExpression', + }, + { + message: 'property \'tooManyArgEnum\': oneOf expects exactly one argument of type array with strings', + line: 49, + column: 19, + type: 'CallExpression', + }, + { + message: 'property \'tooManyArgUnion\': oneOfType expects exactly one argument of type array with proptypes', + line: 50, + column: 20, + type: 'CallExpression', + }, + { + message: 'property \'tooManyArgArrayOf\': arrayOf expects exactly one argument of type proptype or function', + line: 51, + column: 22, + type: 'CallExpression', + }, + { + message: 'property \'tooManyArgObjectOf\': objectOf expects exactly one argument of type proptype or function', + line: 52, + column: 23, + type: 'CallExpression', + }, + { + message: 'property \'tooManyArgObjectWithShape\': shape expects exactly one argument of type object with proptypes as values', + line: 53, + column: 30, + type: 'CallExpression', + }, + { + message: 'property \'badArgMessage\': instanceOf expects exactly one argument of type JavaScript type', + line: 55, + column: 18, + type: 'CallExpression', + }, + { + message: 'property \'badArgEnum\': oneOf expects exactly one argument of type array with strings', + line: 56, + column: 15, + type: 'CallExpression', + }, + { + message: 'property \'badArgUnion\': oneOfType expects exactly one argument of type array with proptypes', + line: 57, + column: 16, + type: 'CallExpression', + }, + { + message: 'property \'badArgArrayOf\': arrayOf expects exactly one argument of type proptype or function', + line: 58, + column: 18, + type: 'CallExpression', + }, + { + message: 'property \'badArgObjectOf\': objectOf expects exactly one argument of type proptype or function', + line: 59, + column: 19, + type: 'CallExpression', + }, + { + message: 'property \'badArgObjectWithShape\': shape expects exactly one argument of type object with proptypes as values', + line: 60, + column: 26, + type: 'CallExpression', + }, + ], + }, + { + code: fs.readFileSync(path.join(__dirname, 'jsx-no-invalid-props', 'invalid-3.js'), 'utf-8'), + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + errors: [ + { + message: 'property \'a\': arr is not a valid PropType', + line: 8, + column: 6, + type: 'MemberExpression', + }, + ], + }, + { + code: fs.readFileSync(path.join(__dirname, 'jsx-no-invalid-props', 'invalid-4.js'), 'utf-8'), + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + errors: [ + { + message: 'property \'a\': unknown use of PropTypes', + line: 8, + column: 6, + type: 'Identifier', + }, + ], + }, + { + code: fs.readFileSync(path.join(__dirname, 'jsx-no-invalid-props', 'invalid-5.js'), 'utf-8'), + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + errors: [ + { + message: 'property \'b\': unknown use of PropTypes', + line: 8, + column: 6, + type: 'Identifier', }, ], }, diff --git a/tests/lib/rules/jsx-no-invalid-props/ignored-1.js b/tests/lib/rules/jsx-no-invalid-props/ignored-1.js new file mode 100644 index 0000000..1e3ea5c --- /dev/null +++ b/tests/lib/rules/jsx-no-invalid-props/ignored-1.js @@ -0,0 +1,9 @@ +/* eslint import/no-extraneous-dependencies: "off", import/no-unresolved: "off" */ +/* eslint comma-dangle: "off", no-unused-vars: "off", object-curly-spacing: "off" */ +/* eslint import/extensions: "off", import/newline-after-import: "off" */ + +import React, {PropTypes} from 'react'; +const TestComponent = {}; +TestComponent.notTestedTypes = { + c: PropTypes, +}; diff --git a/tests/lib/rules/jsx-no-invalid-props/invalid-1.js b/tests/lib/rules/jsx-no-invalid-props/invalid-1.js new file mode 100644 index 0000000..5239081 --- /dev/null +++ b/tests/lib/rules/jsx-no-invalid-props/invalid-1.js @@ -0,0 +1,61 @@ +/* eslint import/no-extraneous-dependencies: "off", import/no-unresolved: "off" */ + +const React = require('react'); + +const MyComponent = {}; + +/* + The content below this comment is loosely based on one of the examples from the below url: + https://github.com/facebook/react/blob/bc86ac4380/docs/docs/typechecking-with-proptypes.md + .. which is licensed under https://creativecommons.org/licenses/by/4.0/ + This set covers most of the inavlid permutations + */ +MyComponent.propTypes = { + badBool: React.PropTypes.boolean, + badFunc: React.PropTypes.function, + badUnknown: React.PropTypes.unknown, + badMessage: React.PropTypes.instanceOf('Object'), + badEnum: React.PropTypes.oneOf([React]), + badUnion: React.PropTypes.oneOfType([ + React.PropTypes.string.isRequired, + React.PropTypes.number.isRequired, + ]), + badArrayOf: React.PropTypes.arrayOf('Array'), + badObjectOf: React.PropTypes.objectOf('Object'), + badObjectWithShape: React.PropTypes.shape({ + color: 'lol', + fontSize: 5, + }), + + nonFuncArray: React.PropTypes.array(), + nonFuncBool: React.PropTypes.bool(), + nonFuncFunc: React.PropTypes.func(), + nonFuncNumber: React.PropTypes.number(), + nonFuncObject: React.PropTypes.object(), + nonFuncString: React.PropTypes.string(), + nonFuncSymbol: React.PropTypes.symbol(), + nonFuncNode: React.PropTypes.node(), + nonFuncElement: React.PropTypes.element(), + nonFuncAny: React.PropTypes.any(), + + missingArgMessage: React.PropTypes.instanceOf(), + missingArgEnum: React.PropTypes.oneOf(), + missingArgUnion: React.PropTypes.oneOfType(), + missingArgArrayOf: React.PropTypes.arrayOf(), + missingArgObjectOf: React.PropTypes.objectOf(), + missingArgObjectWithShape: React.PropTypes.shape(), + + tooManyArgMessage: React.PropTypes.instanceOf('', ''), + tooManyArgEnum: React.PropTypes.oneOf('', ''), + tooManyArgUnion: React.PropTypes.oneOfType('', ''), + tooManyArgArrayOf: React.PropTypes.arrayOf('', ''), + tooManyArgObjectOf: React.PropTypes.objectOf('', ''), + tooManyArgObjectWithShape: React.PropTypes.shape('', ''), + + badArgMessage: React.PropTypes.instanceOf({}), + badArgEnum: React.PropTypes.oneOf({}), + badArgUnion: React.PropTypes.oneOfType({}), + badArgArrayOf: React.PropTypes.arrayOf([]), + badArgObjectOf: React.PropTypes.objectOf({}), + badArgObjectWithShape: React.PropTypes.shape([]), +}; diff --git a/tests/lib/rules/jsx-no-invalid-props/invalid-2.js b/tests/lib/rules/jsx-no-invalid-props/invalid-2.js new file mode 100644 index 0000000..6696b2d --- /dev/null +++ b/tests/lib/rules/jsx-no-invalid-props/invalid-2.js @@ -0,0 +1,38 @@ +/* eslint import/no-extraneous-dependencies: "off", import/no-unresolved: "off" */ +/* eslint comma-dangle: "off", no-unused-vars: "off", object-curly-spacing: "off" */ +/* eslint import/extensions: "off", import/newline-after-import: "off" */ + +import React, {PropTypes} from 'react'; +const TestComponent = {}; +TestComponent.propTypes = { + a: PropTypes.array, + b: PropTypes.bool, + c: PropTypes.func, + d: PropTypes.number, + e: PropTypes.object, + f: PropTypes.string, + g: PropTypes.any, + h: PropTypes.arrayOf, + i: PropTypes.element, + j: PropTypes.instanceOf, + k: PropTypes.node, + l: PropTypes.objectOf, + m: PropTypes.oneOf, + n: PropTypes.oneOfType, + o: PropTypes.shape, + p: PropTypes.array, + q: PropTypes.bool, + r: PropTypes.func, + s: PropTypes.number, + t: PropTypes.object, + u: PropTypes.string, + v: PropTypes.any, + w: PropTypes.arrayOf, + x: PropTypes.element, + y: PropTypes.instanceOf, + z: PropTypes.node, + aa: PropTypes.objectOf, + ab: PropTypes.oneOf, + ac: PropTypes.oneOfType, + ad: PropTypes.shape +}; diff --git a/tests/lib/rules/jsx-no-invalid-props/invalid-3.js b/tests/lib/rules/jsx-no-invalid-props/invalid-3.js new file mode 100644 index 0000000..3a097d7 --- /dev/null +++ b/tests/lib/rules/jsx-no-invalid-props/invalid-3.js @@ -0,0 +1,9 @@ +/* eslint import/no-extraneous-dependencies: "off", import/no-unresolved: "off" */ +/* eslint comma-dangle: "off", no-unused-vars: "off", object-curly-spacing: "off" */ +/* eslint import/extensions: "off", import/newline-after-import: "off" */ + +import React, {PropTypes} from 'react'; +const TestComponent = {}; +TestComponent.propTypes = { + a: PropTypes.arr, +}; diff --git a/tests/lib/rules/jsx-no-invalid-props/invalid-4.js b/tests/lib/rules/jsx-no-invalid-props/invalid-4.js new file mode 100644 index 0000000..1b8946b --- /dev/null +++ b/tests/lib/rules/jsx-no-invalid-props/invalid-4.js @@ -0,0 +1,9 @@ +/* eslint import/no-extraneous-dependencies: "off", import/no-unresolved: "off" */ +/* eslint comma-dangle: "off", no-unused-vars: "off", object-curly-spacing: "off" */ +/* eslint import/extensions: "off", import/newline-after-import: "off" */ + +import React, {PropTypes} from 'react'; +const TestComponent = {}; +TestComponent.propTypes = { + a: PropTypes, +}; diff --git a/tests/lib/rules/jsx-no-invalid-props/invalid-5.js b/tests/lib/rules/jsx-no-invalid-props/invalid-5.js new file mode 100644 index 0000000..150b888 --- /dev/null +++ b/tests/lib/rules/jsx-no-invalid-props/invalid-5.js @@ -0,0 +1,9 @@ +/* eslint import/no-extraneous-dependencies: "off", import/no-unresolved: "off" */ +/* eslint comma-dangle: "off", no-unused-vars: "off", object-curly-spacing: "off" */ +/* eslint import/extensions: "off", import/newline-after-import: "off" */ + +import React, {PropTypes} from 'react'; +const TestComponent = {}; +TestComponent.contextTypes = { + b: PropTypes, +}; diff --git a/tests/lib/rules/jsx-no-invalid-props/valid-1.js b/tests/lib/rules/jsx-no-invalid-props/valid-1.js new file mode 100644 index 0000000..f26da02 --- /dev/null +++ b/tests/lib/rules/jsx-no-invalid-props/valid-1.js @@ -0,0 +1,93 @@ +/* eslint import/no-extraneous-dependencies: "off", import/no-unresolved: "off", indent: "off" */ +/* eslint comma-dangle: "off", consistent-return: "off", prefer-arrow-callback: "off" */ +/* eslint func-names: "off", object-shorthand: "off", prefer-template: "off", max-len: "off" */ + +const React = require('react'); + +const MyComponent = {}; +const Message = {}; + +/* + The content below this comment is a copy of one of the examples from the below url, with + whitespace changes only: + https://github.com/facebook/react/blob/bc86ac4380/docs/docs/typechecking-with-proptypes.md + .. which is licensed under https://creativecommons.org/licenses/by/4.0/ + */ +MyComponent.propTypes = { + // You can declare that a prop is a specific JS primitive. By default, these + // are all optional. + optionalArray: React.PropTypes.array, + optionalBool: React.PropTypes.bool, + optionalFunc: React.PropTypes.func, + optionalNumber: React.PropTypes.number, + optionalObject: React.PropTypes.object, + optionalString: React.PropTypes.string, + optionalSymbol: React.PropTypes.symbol, + + // Anything that can be rendered: numbers, strings, elements or an array + // (or fragment) containing these types. + optionalNode: React.PropTypes.node, + + // A React element. + optionalElement: React.PropTypes.element, + + // You can also declare that a prop is an instance of a class. This uses + // JS's instanceof operator. + optionalMessage: React.PropTypes.instanceOf(Message), + + // You can ensure that your prop is limited to specific values by treating + // it as an enum. + optionalEnum: React.PropTypes.oneOf(['News', 'Photos']), + + // An object that could be one of many types + optionalUnion: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.number, + React.PropTypes.instanceOf(Message) + ]), + + // An array of a certain type + optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number), + + // An object with property values of a certain type + optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number), + + // An object taking on a particular shape + optionalObjectWithShape: React.PropTypes.shape({ + color: React.PropTypes.string, + fontSize: React.PropTypes.number + }), + + // You can chain any of the above with `isRequired` to make sure a warning + // is shown if the prop isn't provided. + requiredFunc: React.PropTypes.func.isRequired, + + // A value of any data type + requiredAny: React.PropTypes.any.isRequired, + + // You can also specify a custom validator. It should return an Error + // object if the validation fails. Don't `console.warn` or throw, as this + // won't work inside `oneOfType`. + customProp: function (props, propName, componentName) { + if (!/matchme/.test(props[propName])) { + return new Error( + 'Invalid prop `' + propName + '` supplied to' + + ' `' + componentName + '`. Validation failed.' + ); + } + }, + + // You can also supply a custom validator to `arrayOf` and `objectOf`. + // It should return an Error object if the validation fails. The validator + // will be called for each key in the array or object. The first two + // arguments of the validator are the array or object itself, and the + // current item's key. + customArrayProp: React.PropTypes.arrayOf(function (propValue, key, componentName, location, propFullName) { + if (!/matchme/.test(propValue[key])) { + return new Error( + 'Invalid prop `' + propFullName + '` supplied to' + + ' `' + componentName + '`. Validation failed.' + ); + } + }) +}; diff --git a/tests/lib/rules/jsx-no-invalid-props/valid-2.js b/tests/lib/rules/jsx-no-invalid-props/valid-2.js new file mode 100644 index 0000000..973124c --- /dev/null +++ b/tests/lib/rules/jsx-no-invalid-props/valid-2.js @@ -0,0 +1,61 @@ +/* eslint import/no-extraneous-dependencies: "off", import/no-unresolved: "off" */ + +const React = require('react'); + +const MyComponent = {}; +const Message = {}; + +/* + The content below this comment is loosely based on one of the examples from the below url: + https://github.com/facebook/react/blob/bc86ac4380/docs/docs/typechecking-with-proptypes.md + .. which is licensed under https://creativecommons.org/licenses/by/4.0/ + This set covers most of the remaining acceptable permutations not covered in valid-1.js + */ +MyComponent.propTypes = { + requiredArray: React.PropTypes.array.isRequired, + requiredBool: React.PropTypes.bool.isRequired, + requiredFunc: React.PropTypes.func.isRequired, + requiredNumber: React.PropTypes.number.isRequired, + requiredObject: React.PropTypes.object.isRequired, + requiredString: React.PropTypes.string.isRequired, + requiredSymbol: React.PropTypes.symbol.isRequired, + requiredNode: React.PropTypes.node.isRequired, + requiredElement: React.PropTypes.element.isRequired, + requiredMessage: React.PropTypes.instanceOf(Message).isRequired, + requiredEnum: React.PropTypes.oneOf(['News', 'Photos']).isRequired, + requiredUnion: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.number, + React.PropTypes.instanceOf(Message), + ]).isRequired, + requiredArrayOf: React.PropTypes.arrayOf(React.PropTypes.number.isRequired).isRequired, + requiredObjectOf: React.PropTypes.objectOf(React.PropTypes.number.isRequired).isRequired, + requiredObjectWithShape: React.PropTypes.shape({ + color: React.PropTypes.string.isRequired, + fontSize: React.PropTypes.number.isRequired, + }), + deepShape: React.PropTypes.shape({ + color: React.PropTypes.string.isRequired, + deep: React.PropTypes.shape({ + foo: React.PropTypes.string.isRequired, + union: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.number, + ]), + }), + }), + optionalAny: React.PropTypes.any, + customProp(props, propName, componentName) { + if (!/matchme/.test(props[propName])) { + return new Error(`error ${componentName}`); + } + return null; + }, + customObjectProp: + React.PropTypes.objectOf((propValue, key, componentName, location, propFullName) => { + if (!/matchme/.test(propValue[key])) { + return new Error(`error ${propFullName}`); + } + return null; + }), +};