Skip to content

Commit 144d45c

Browse files
committed
Added support for prop-types check of type this.props['this-format'].
Used list of nested names instead of dot-separated names to avoid conflict with properties like `this.props['this.format']`
1 parent deb0447 commit 144d45c

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
@@ -144,13 +144,13 @@ module.exports = function(context) {
144144
/**
145145
* Checks if the prop is declared
146146
* @param {Object} component The component to process
147-
* @param {String} name Dot separated name of the prop to check.
147+
* @param {String[]} names List of names of the prop to check.
148148
* @returns {Boolean} True if the prop is declared, false if not.
149149
*/
150-
function isDeclaredInComponent(component, name) {
150+
function isDeclaredInComponent(component, names) {
151151
return _isDeclaredInComponent(
152152
component.declaredPropTypes || {},
153-
name.split('.')
153+
names
154154
);
155155
}
156156

@@ -164,14 +164,14 @@ module.exports = function(context) {
164164
return tokens.length && tokens[0].value === '...';
165165
}
166166

167+
/**
168+
* Retrieve the name of a key node
169+
* @param {ASTNode} node The AST node with the key.
170+
* @return {string} the name of the key
171+
*/
167172
function getKeyValue(node) {
168173
var key = node.key;
169-
if (key) {
170-
if (key.type === 'Identifier') {
171-
return key.name;
172-
}
173-
return key.value;
174-
}
174+
return key.type === 'Identifier' ? key.name : key.value;
175175
}
176176

177177
/**
@@ -321,22 +321,51 @@ module.exports = function(context) {
321321
return true;
322322
}
323323

324+
/**
325+
* Retrieve the name of a property node
326+
* @param {ASTNode} node The AST node with the property.
327+
* @return {string} the name of the property or undefined if not found
328+
*/
329+
function getPropertyName(node) {
330+
var property = node.property;
331+
if (property) {
332+
switch (property.type) {
333+
case 'Identifier':
334+
if (node.computed) {
335+
return '__COMPUTED_PROP__';
336+
}
337+
return property.name;
338+
case 'Literal':
339+
// Accept computed properties that are literal strings
340+
if (typeof property.value === 'string') {
341+
return property.value;
342+
}
343+
// falls through
344+
default:
345+
if (node.computed) {
346+
return '__COMPUTED_PROP__';
347+
}
348+
break;
349+
}
350+
}
351+
}
352+
324353
/**
325354
* Mark a prop type as used
326355
* @param {ASTNode} node The AST node being marked.
327356
*/
328-
function markPropTypesAsUsed(node, parentName) {
357+
function markPropTypesAsUsed(node, parentNames) {
358+
parentNames = parentNames || [];
329359
var type;
330-
var name = node.parent.computed ?
331-
'__COMPUTED_PROP__'
332-
: node.parent.property && node.parent.property.name;
333-
var fullName = parentName ? parentName + '.' + name : name;
334-
335-
if (node.parent.type === 'MemberExpression') {
336-
markPropTypesAsUsed(node.parent, fullName);
337-
}
338-
if (name && !node.parent.computed) {
339-
type = 'direct';
360+
var name = getPropertyName(node.parent);
361+
var allNames;
362+
if (name) {
363+
allNames = parentNames.concat(name);
364+
if (node.parent.type === 'MemberExpression') {
365+
markPropTypesAsUsed(node.parent, allNames);
366+
}
367+
// Do not mark computed props as used.
368+
type = name !== '__COMPUTED_PROP__' ? 'direct' : null;
340369
} else if (
341370
node.parent.parent.declarations &&
342371
node.parent.parent.declarations[0].id.properties &&
@@ -354,7 +383,8 @@ module.exports = function(context) {
354383
break;
355384
}
356385
usedPropTypes.push({
357-
name: fullName,
386+
name: name,
387+
allNames: allNames,
358388
node: node.parent.property
359389
});
360390
break;
@@ -368,6 +398,7 @@ module.exports = function(context) {
368398
if (propName) {
369399
usedPropTypes.push({
370400
name: propName,
401+
allNames: [propName],
371402
node: properties[i]
372403
});
373404
}
@@ -441,19 +472,20 @@ module.exports = function(context) {
441472
* @param {Object} component The component to process
442473
*/
443474
function reportUndeclaredPropTypes(component) {
444-
var name;
475+
var allNames, name;
445476
for (var i = 0, j = component.usedPropTypes.length; i < j; i++) {
446477
name = component.usedPropTypes[i].name;
478+
allNames = component.usedPropTypes[i].allNames;
447479
if (
448-
isIgnored(name.split('.').pop()) ||
449-
isDeclaredInComponent(component, name)
480+
isIgnored(name) ||
481+
isDeclaredInComponent(component, allNames)
450482
) {
451483
continue;
452484
}
453485
context.report(
454486
component.usedPropTypes[i].node,
455487
component.name === componentUtil.DEFAULT_COMPONENT_NAME ? MISSING_MESSAGE : MISSING_MESSAGE_NAMED_COMP, {
456-
name: name.replace(/\.__COMPUTED_PROP__/g, '[]'),
488+
name: allNames.join('.').replace(/\.__COMPUTED_PROP__/g, '[]'),
457489
component: component.name
458490
}
459491
);

tests/lib/rules/prop-types.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,56 @@ eslintTester.addRuleTest('lib/rules/prop-types', {
432432
'};'
433433
].join('\n'),
434434
parser: 'babel-eslint'
435+
}, {
436+
code: [
437+
'class Hello extends React.Component {',
438+
' render() {',
439+
' this.props["some.value"];',
440+
' return <div>Hello</div>;',
441+
' }',
442+
'}',
443+
'Hello.propTypes = {',
444+
' "some.value": React.PropTypes.string',
445+
'};'
446+
].join('\n'),
447+
ecmaFeatures: {
448+
classes: true,
449+
jsx: true
450+
}
451+
}, {
452+
code: [
453+
'class Hello extends React.Component {',
454+
' render() {',
455+
' this.props["arr"][1];',
456+
' return <div>Hello</div>;',
457+
' }',
458+
'}',
459+
'Hello.propTypes = {',
460+
' "arr": React.PropTypes.array',
461+
'};'
462+
].join('\n'),
463+
ecmaFeatures: {
464+
classes: true,
465+
jsx: true
466+
}
467+
}, {
468+
code: [
469+
'class Hello extends React.Component {',
470+
' render() {',
471+
' this.props["arr"][1]["some.value"];',
472+
' return <div>Hello</div>;',
473+
' }',
474+
'}',
475+
'Hello.propTypes = {',
476+
' "arr": React.PropTypes.arrayOf(',
477+
' React.PropTypes.shape({"some.value": React.PropTypes.string})',
478+
' )',
479+
'};'
480+
].join('\n'),
481+
ecmaFeatures: {
482+
classes: true,
483+
jsx: true
484+
}
435485
}
436486
],
437487

@@ -772,6 +822,63 @@ eslintTester.addRuleTest('lib/rules/prop-types', {
772822
errors: [
773823
{message: '\'propX\' is missing in props validation for Hello'}
774824
]
825+
}, {
826+
code: [
827+
'class Hello extends React.Component {',
828+
' render() {',
829+
' this.props["some.value"];',
830+
' return <div>Hello</div>;',
831+
' }',
832+
'}',
833+
'Hello.propTypes = {',
834+
'};'
835+
].join('\n'),
836+
ecmaFeatures: {
837+
classes: true,
838+
jsx: true
839+
},
840+
errors: [
841+
{message: '\'some.value\' is missing in props validation for Hello'}
842+
]
843+
}, {
844+
code: [
845+
'class Hello extends React.Component {',
846+
' render() {',
847+
' this.props["arr"][1];',
848+
' return <div>Hello</div>;',
849+
' }',
850+
'}',
851+
'Hello.propTypes = {',
852+
'};'
853+
].join('\n'),
854+
ecmaFeatures: {
855+
classes: true,
856+
jsx: true
857+
},
858+
errors: [
859+
{message: '\'arr\' is missing in props validation for Hello'}
860+
]
861+
}, {
862+
code: [
863+
'class Hello extends React.Component {',
864+
' render() {',
865+
' this.props["arr"][1]["some.value"];',
866+
' return <div>Hello</div>;',
867+
' }',
868+
'}',
869+
'Hello.propTypes = {',
870+
' "arr": React.PropTypes.arrayOf(',
871+
' React.PropTypes.shape({})',
872+
' )',
873+
'};'
874+
].join('\n'),
875+
ecmaFeatures: {
876+
classes: true,
877+
jsx: true
878+
},
879+
errors: [
880+
{message: '\'arr[].some.value\' is missing in props validation for Hello'}
881+
]
775882
}
776883
]
777884
});

0 commit comments

Comments
 (0)