Skip to content

Commit 3a0be29

Browse files
committed
Merge pull request #127 from Cellule/prop_types_computed_props
Add support for computed string format in prop-types
2 parents 228548f + 144d45c commit 3a0be29

File tree

2 files changed

+164
-25
lines changed

2 files changed

+164
-25
lines changed

lib/rules/prop-types.js

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,13 @@ module.exports = function(context) {
147147
/**
148148
* Checks if the prop is declared
149149
* @param {Object} component The component to process
150-
* @param {String} name Dot separated name of the prop to check.
150+
* @param {String[]} names List of names of the prop to check.
151151
* @returns {Boolean} True if the prop is declared, false if not.
152152
*/
153-
function isDeclaredInComponent(component, name) {
153+
function isDeclaredInComponent(component, names) {
154154
return _isDeclaredInComponent(
155155
component.declaredPropTypes || {},
156-
name.split('.')
156+
names
157157
);
158158
}
159159

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

170+
/**
171+
* Retrieve the name of a key node
172+
* @param {ASTNode} node The AST node with the key.
173+
* @return {string} the name of the key
174+
*/
170175
function getKeyValue(node) {
171176
var key = node.key;
172-
if (key) {
173-
if (key.type === 'Identifier') {
174-
return key.name;
175-
}
176-
return key.value;
177-
}
177+
return key.type === 'Identifier' ? key.name : key.value;
178178
}
179179

180180
/**
@@ -324,22 +324,51 @@ module.exports = function(context) {
324324
return true;
325325
}
326326

327+
/**
328+
* Retrieve the name of a property node
329+
* @param {ASTNode} node The AST node with the property.
330+
* @return {string} the name of the property or undefined if not found
331+
*/
332+
function getPropertyName(node) {
333+
var property = node.property;
334+
if (property) {
335+
switch (property.type) {
336+
case 'Identifier':
337+
if (node.computed) {
338+
return '__COMPUTED_PROP__';
339+
}
340+
return property.name;
341+
case 'Literal':
342+
// Accept computed properties that are literal strings
343+
if (typeof property.value === 'string') {
344+
return property.value;
345+
}
346+
// falls through
347+
default:
348+
if (node.computed) {
349+
return '__COMPUTED_PROP__';
350+
}
351+
break;
352+
}
353+
}
354+
}
355+
327356
/**
328357
* Mark a prop type as used
329358
* @param {ASTNode} node The AST node being marked.
330359
*/
331-
function markPropTypesAsUsed(node, parentName) {
360+
function markPropTypesAsUsed(node, parentNames) {
361+
parentNames = parentNames || [];
332362
var type;
333-
var name = node.parent.computed ?
334-
'__COMPUTED_PROP__'
335-
: node.parent.property && node.parent.property.name;
336-
var fullName = parentName ? parentName + '.' + name : name;
337-
338-
if (node.parent.type === 'MemberExpression') {
339-
markPropTypesAsUsed(node.parent, fullName);
340-
}
341-
if (name && !node.parent.computed) {
342-
type = 'direct';
363+
var name = getPropertyName(node.parent);
364+
var allNames;
365+
if (name) {
366+
allNames = parentNames.concat(name);
367+
if (node.parent.type === 'MemberExpression') {
368+
markPropTypesAsUsed(node.parent, allNames);
369+
}
370+
// Do not mark computed props as used.
371+
type = name !== '__COMPUTED_PROP__' ? 'direct' : null;
343372
} else if (
344373
node.parent.parent.declarations &&
345374
node.parent.parent.declarations[0].id.properties &&
@@ -357,7 +386,8 @@ module.exports = function(context) {
357386
break;
358387
}
359388
usedPropTypes.push({
360-
name: fullName,
389+
name: name,
390+
allNames: allNames,
361391
node: node.parent.property
362392
});
363393
break;
@@ -371,6 +401,7 @@ module.exports = function(context) {
371401
if (propName) {
372402
usedPropTypes.push({
373403
name: propName,
404+
allNames: [propName],
374405
node: properties[i]
375406
});
376407
}
@@ -444,19 +475,20 @@ module.exports = function(context) {
444475
* @param {Object} component The component to process
445476
*/
446477
function reportUndeclaredPropTypes(component) {
447-
var name;
478+
var allNames, name;
448479
for (var i = 0, j = component.usedPropTypes.length; i < j; i++) {
449480
name = component.usedPropTypes[i].name;
481+
allNames = component.usedPropTypes[i].allNames;
450482
if (
451-
isIgnored(name.split('.').pop()) ||
452-
isDeclaredInComponent(component, name)
483+
isIgnored(name) ||
484+
isDeclaredInComponent(component, allNames)
453485
) {
454486
continue;
455487
}
456488
context.report(
457489
component.usedPropTypes[i].node,
458490
component.name === componentUtil.DEFAULT_COMPONENT_NAME ? MISSING_MESSAGE : MISSING_MESSAGE_NAMED_COMP, {
459-
name: name.replace(/\.__COMPUTED_PROP__/g, '[]'),
491+
name: allNames.join('.').replace(/\.__COMPUTED_PROP__/g, '[]'),
460492
component: component.name
461493
}
462494
);

tests/lib/rules/prop-types.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,56 @@ eslintTester.addRuleTest('lib/rules/prop-types', {
443443
'};'
444444
].join('\n'),
445445
parser: 'babel-eslint'
446+
}, {
447+
code: [
448+
'class Hello extends React.Component {',
449+
' render() {',
450+
' this.props["some.value"];',
451+
' return <div>Hello</div>;',
452+
' }',
453+
'}',
454+
'Hello.propTypes = {',
455+
' "some.value": React.PropTypes.string',
456+
'};'
457+
].join('\n'),
458+
ecmaFeatures: {
459+
classes: true,
460+
jsx: true
461+
}
462+
}, {
463+
code: [
464+
'class Hello extends React.Component {',
465+
' render() {',
466+
' this.props["arr"][1];',
467+
' return <div>Hello</div>;',
468+
' }',
469+
'}',
470+
'Hello.propTypes = {',
471+
' "arr": React.PropTypes.array',
472+
'};'
473+
].join('\n'),
474+
ecmaFeatures: {
475+
classes: true,
476+
jsx: true
477+
}
478+
}, {
479+
code: [
480+
'class Hello extends React.Component {',
481+
' render() {',
482+
' this.props["arr"][1]["some.value"];',
483+
' return <div>Hello</div>;',
484+
' }',
485+
'}',
486+
'Hello.propTypes = {',
487+
' "arr": React.PropTypes.arrayOf(',
488+
' React.PropTypes.shape({"some.value": React.PropTypes.string})',
489+
' )',
490+
'};'
491+
].join('\n'),
492+
ecmaFeatures: {
493+
classes: true,
494+
jsx: true
495+
}
446496
}
447497
],
448498

@@ -783,6 +833,63 @@ eslintTester.addRuleTest('lib/rules/prop-types', {
783833
errors: [
784834
{message: '\'propX\' is missing in props validation for Hello'}
785835
]
836+
}, {
837+
code: [
838+
'class Hello extends React.Component {',
839+
' render() {',
840+
' this.props["some.value"];',
841+
' return <div>Hello</div>;',
842+
' }',
843+
'}',
844+
'Hello.propTypes = {',
845+
'};'
846+
].join('\n'),
847+
ecmaFeatures: {
848+
classes: true,
849+
jsx: true
850+
},
851+
errors: [
852+
{message: '\'some.value\' is missing in props validation for Hello'}
853+
]
854+
}, {
855+
code: [
856+
'class Hello extends React.Component {',
857+
' render() {',
858+
' this.props["arr"][1];',
859+
' return <div>Hello</div>;',
860+
' }',
861+
'}',
862+
'Hello.propTypes = {',
863+
'};'
864+
].join('\n'),
865+
ecmaFeatures: {
866+
classes: true,
867+
jsx: true
868+
},
869+
errors: [
870+
{message: '\'arr\' is missing in props validation for Hello'}
871+
]
872+
}, {
873+
code: [
874+
'class Hello extends React.Component {',
875+
' render() {',
876+
' this.props["arr"][1]["some.value"];',
877+
' return <div>Hello</div>;',
878+
' }',
879+
'}',
880+
'Hello.propTypes = {',
881+
' "arr": React.PropTypes.arrayOf(',
882+
' React.PropTypes.shape({})',
883+
' )',
884+
'};'
885+
].join('\n'),
886+
ecmaFeatures: {
887+
classes: true,
888+
jsx: true
889+
},
890+
errors: [
891+
{message: '\'arr[].some.value\' is missing in props validation for Hello'}
892+
]
786893
}
787894
]
788895
});

0 commit comments

Comments
 (0)