Skip to content

Commit

Permalink
Merge pull request #127 from Cellule/prop_types_computed_props
Browse files Browse the repository at this point in the history
Add support for computed string format in prop-types
  • Loading branch information
yannickcr committed Jun 21, 2015
2 parents 228548f + 144d45c commit 3a0be29
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 25 deletions.
82 changes: 57 additions & 25 deletions lib/rules/prop-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,13 @@ module.exports = function(context) {
/**
* Checks if the prop is declared
* @param {Object} component The component to process
* @param {String} name Dot separated name of the prop to check.
* @param {String[]} names List of names of the prop to check.
* @returns {Boolean} True if the prop is declared, false if not.
*/
function isDeclaredInComponent(component, name) {
function isDeclaredInComponent(component, names) {
return _isDeclaredInComponent(
component.declaredPropTypes || {},
name.split('.')
names
);
}

Expand All @@ -167,14 +167,14 @@ module.exports = function(context) {
return tokens.length && tokens[0].value === '...';
}

/**
* Retrieve the name of a key node
* @param {ASTNode} node The AST node with the key.
* @return {string} the name of the key
*/
function getKeyValue(node) {
var key = node.key;
if (key) {
if (key.type === 'Identifier') {
return key.name;
}
return key.value;
}
return key.type === 'Identifier' ? key.name : key.value;
}

/**
Expand Down Expand Up @@ -324,22 +324,51 @@ module.exports = function(context) {
return true;
}

/**
* Retrieve the name of a property node
* @param {ASTNode} node The AST node with the property.
* @return {string} the name of the property or undefined if not found
*/
function getPropertyName(node) {
var property = node.property;
if (property) {
switch (property.type) {
case 'Identifier':
if (node.computed) {
return '__COMPUTED_PROP__';
}
return property.name;
case 'Literal':
// Accept computed properties that are literal strings
if (typeof property.value === 'string') {
return property.value;
}
// falls through
default:
if (node.computed) {
return '__COMPUTED_PROP__';
}
break;
}
}
}

/**
* Mark a prop type as used
* @param {ASTNode} node The AST node being marked.
*/
function markPropTypesAsUsed(node, parentName) {
function markPropTypesAsUsed(node, parentNames) {
parentNames = parentNames || [];
var type;
var name = node.parent.computed ?
'__COMPUTED_PROP__'
: node.parent.property && node.parent.property.name;
var fullName = parentName ? parentName + '.' + name : name;

if (node.parent.type === 'MemberExpression') {
markPropTypesAsUsed(node.parent, fullName);
}
if (name && !node.parent.computed) {
type = 'direct';
var name = getPropertyName(node.parent);
var allNames;
if (name) {
allNames = parentNames.concat(name);
if (node.parent.type === 'MemberExpression') {
markPropTypesAsUsed(node.parent, allNames);
}
// Do not mark computed props as used.
type = name !== '__COMPUTED_PROP__' ? 'direct' : null;
} else if (
node.parent.parent.declarations &&
node.parent.parent.declarations[0].id.properties &&
Expand All @@ -357,7 +386,8 @@ module.exports = function(context) {
break;
}
usedPropTypes.push({
name: fullName,
name: name,
allNames: allNames,
node: node.parent.property
});
break;
Expand All @@ -371,6 +401,7 @@ module.exports = function(context) {
if (propName) {
usedPropTypes.push({
name: propName,
allNames: [propName],
node: properties[i]
});
}
Expand Down Expand Up @@ -444,19 +475,20 @@ module.exports = function(context) {
* @param {Object} component The component to process
*/
function reportUndeclaredPropTypes(component) {
var name;
var allNames, name;
for (var i = 0, j = component.usedPropTypes.length; i < j; i++) {
name = component.usedPropTypes[i].name;
allNames = component.usedPropTypes[i].allNames;
if (
isIgnored(name.split('.').pop()) ||
isDeclaredInComponent(component, name)
isIgnored(name) ||
isDeclaredInComponent(component, allNames)
) {
continue;
}
context.report(
component.usedPropTypes[i].node,
component.name === componentUtil.DEFAULT_COMPONENT_NAME ? MISSING_MESSAGE : MISSING_MESSAGE_NAMED_COMP, {
name: name.replace(/\.__COMPUTED_PROP__/g, '[]'),
name: allNames.join('.').replace(/\.__COMPUTED_PROP__/g, '[]'),
component: component.name
}
);
Expand Down
107 changes: 107 additions & 0 deletions tests/lib/rules/prop-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,56 @@ eslintTester.addRuleTest('lib/rules/prop-types', {
'};'
].join('\n'),
parser: 'babel-eslint'
}, {
code: [
'class Hello extends React.Component {',
' render() {',
' this.props["some.value"];',
' return <div>Hello</div>;',
' }',
'}',
'Hello.propTypes = {',
' "some.value": React.PropTypes.string',
'};'
].join('\n'),
ecmaFeatures: {
classes: true,
jsx: true
}
}, {
code: [
'class Hello extends React.Component {',
' render() {',
' this.props["arr"][1];',
' return <div>Hello</div>;',
' }',
'}',
'Hello.propTypes = {',
' "arr": React.PropTypes.array',
'};'
].join('\n'),
ecmaFeatures: {
classes: true,
jsx: true
}
}, {
code: [
'class Hello extends React.Component {',
' render() {',
' this.props["arr"][1]["some.value"];',
' return <div>Hello</div>;',
' }',
'}',
'Hello.propTypes = {',
' "arr": React.PropTypes.arrayOf(',
' React.PropTypes.shape({"some.value": React.PropTypes.string})',
' )',
'};'
].join('\n'),
ecmaFeatures: {
classes: true,
jsx: true
}
}
],

Expand Down Expand Up @@ -783,6 +833,63 @@ eslintTester.addRuleTest('lib/rules/prop-types', {
errors: [
{message: '\'propX\' is missing in props validation for Hello'}
]
}, {
code: [
'class Hello extends React.Component {',
' render() {',
' this.props["some.value"];',
' return <div>Hello</div>;',
' }',
'}',
'Hello.propTypes = {',
'};'
].join('\n'),
ecmaFeatures: {
classes: true,
jsx: true
},
errors: [
{message: '\'some.value\' is missing in props validation for Hello'}
]
}, {
code: [
'class Hello extends React.Component {',
' render() {',
' this.props["arr"][1];',
' return <div>Hello</div>;',
' }',
'}',
'Hello.propTypes = {',
'};'
].join('\n'),
ecmaFeatures: {
classes: true,
jsx: true
},
errors: [
{message: '\'arr\' is missing in props validation for Hello'}
]
}, {
code: [
'class Hello extends React.Component {',
' render() {',
' this.props["arr"][1]["some.value"];',
' return <div>Hello</div>;',
' }',
'}',
'Hello.propTypes = {',
' "arr": React.PropTypes.arrayOf(',
' React.PropTypes.shape({})',
' )',
'};'
].join('\n'),
ecmaFeatures: {
classes: true,
jsx: true
},
errors: [
{message: '\'arr[].some.value\' is missing in props validation for Hello'}
]
}
]
});

0 comments on commit 3a0be29

Please sign in to comment.