Skip to content
8 changes: 6 additions & 2 deletions docs/rules/jsx-no-invalid-props.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
181 changes: 140 additions & 41 deletions lib/rules/jsx-no-invalid-props.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 '<unknown>';
}

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);
});
}

Expand Down
8 changes: 6 additions & 2 deletions lib/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 = {
Expand Down
Loading