Dependencies:
"eslint": "^6.7.2""eslint-plugin-react": "^7.17.0".eslintrc.json
{
...
"react/jsx-props-no-spreading": [
"error",
{
"exceptions": [
"AsyncSelect",
"Select",
"components.Group",
"components.ClearIndicator",
"components.DropdownIndicator"
]
}
]
...
}
What happens?
The following patterns are considered warnings regardless they are in the exception list:
<components.Group {...groupProps} />
<components.DropdownIndicator {...props} />
<components.ClearIndicator {...props} />
The following patterns are not considered warnings:
<AsyncSelect {...props} />
<Select {...getCustomProps({})} />
What I noticed is that sub-components don't have a JSXIdentifier object instead they have JSXMemberExpression. Being said that the tagName ends up being undefined. For a better reference:
Select:
<Select {...getCustomProps({})} />
Node {
type: 'JSXSpreadAttribute',
start: 1918,
end: 2314,
loc:
SourceLocation {
start: Position { line: 64, column: 14 },
end: Position { line: 74, column: 17 } },
range: [ 1918, 2314 ],
argument:
Node {
type: 'CallExpression',
start: 1922,
end: 2313,
loc: SourceLocation { start: [Position], end: [Position] },
range: [ 1922, 2313 ],
callee:
Node {
type: 'Identifier',
start: 1922,
end: 1933,
loc: [SourceLocation],
range: [Array],
name: 'getCustomProps',
_babelType: 'Identifier',
parent: [Circular] },
arguments: [ [Node] ],
_babelType: 'CallExpression',
parent: [Circular] },
_babelType: 'JSXSpreadAttribute',
parent:
Node {
type: 'JSXOpeningElement',
start: 1896,
end: 2329,
loc: SourceLocation { start: [Position], end: [Position] },
range: [ 1896, 2329 ],
name:
Node {
type: 'JSXIdentifier',
start: 1897,
end: 1903,
loc: [SourceLocation],
range: [Array],
name: 'Select',
_babelType: 'JSXIdentifier',
parent: [Circular] },
attributes: [ [Circular] ],
selfClosing: true,
_babelType: 'JSXOpeningElement',
parent:
Node {
type: 'JSXElement',
start: 1896,
end: 2329,
loc: [SourceLocation],
range: [Array],
openingElement: [Circular],
closingElement: null,
children: [],
_babelType: 'JSXElement',
parent: [Node] } } }
components.Group
<components.Group {...groupProps} />
Node {
type: 'JSXSpreadAttribute',
start: 1065,
end: 1080,
loc:
SourceLocation {
start: Position { line: 32, column: 24 },
end: Position { line: 32, column: 39 } },
range: [ 1065, 1080 ],
argument:
Node {
type: 'Identifier',
start: 1069,
end: 1079,
loc:
SourceLocation {
start: [Position],
end: [Position],
identifierName: 'groupProps' },
range: [ 1069, 1079 ],
name: 'groupProps',
_babelType: 'Identifier',
parent: [Circular] },
_babelType: 'JSXSpreadAttribute',
parent:
Node {
type: 'JSXOpeningElement',
start: 1047,
end: 1083,
loc: SourceLocation { start: [Position], end: [Position] },
range: [ 1047, 1083 ],
name:
Node {
type: 'JSXMemberExpression',
start: 1048,
end: 1064,
loc: [SourceLocation],
range: [Array],
object: [Node],
property: [Node],
_babelType: 'JSXMemberExpression',
parent: [Circular] },
attributes: [ [Circular] ],
selfClosing: true,
_babelType: 'JSXOpeningElement',
parent:
Node {
type: 'JSXElement',
start: 1047,
end: 1083,
loc: [SourceLocation],
range: [Array],
openingElement: [Circular],
closingElement: null,
children: [],
_babelType: 'JSXElement',
parent: [Node] } } }
I'm not that familiar with eslint rules but I can get it done if anyone points me in the right direction
The rule implementation is here: https://github.com/yannickcr/eslint-plugin-react/blob/master/lib/rules/jsx-props-no-spreading.js
This line: https://github.com/yannickcr/eslint-plugin-react/blob/master/lib/rules/jsx-props-no-spreading.js#L86 probably needs to be adjusted to account for namespaced custom components? I'd start by adding test cases, and then play with the rule impl starting at that line.
Thanks for your prompt response @ljharb
As I mentioned before, tagName is undefined when analyzing sub-components, and it is according to the constant definition
const tagName = node.parent.name.name;
it expects name property at the JSXIdentifier but in sub-components the object structure contains a JSXMemberExpression instead of the Identifier, which doesn't have the expected prop to get the tagName.
So, I don't think it would make too much sense to start from line 86
const isCustomTag = tagName && tagName[0] === tagName[0].toUpperCase();
until we find a way to identify sub-components pattern structure or at least how to get their tagName
@ljharb
This rule gets the tagName from the name property of the following structure:
Node {
type: 'JSXIdentifier',
start: 1897,
end: 1903,
loc: [SourceLocation],
range: [Array],
name: 'Select',
_babelType: 'JSXIdentifier',
parent: [Circular]
}
However, when analyzing sub-components the object at this exact level is represented by the following structure:
Node {
type: 'JSXMemberExpression',
start: 1048,
end: 1064,
loc: [SourceLocation],
range: [Array],
object: [Node],
property: [Node],
_babelType: 'JSXMemberExpression',
parent: [Circular]
}
Sounds great, a PR would be most appreciated.
Awesome, I'll work on it
@ljharb I have added some tests in here, could you please review them prior to proceeding to design my solution?
For better traceability, I will be adding some references to external resources that might help to solve this issue.
According to JSXOpeningElement, name property is required and should be of any of the following types:
property property which is required and should be of type JSXIdentifier. What I could find out through some tests is that when analyzing components (html or custom) the parser returns a JSXIdentifier, whereas analyzing sub-components (i.e. components.Group, Nav.Tab) the parser returns JSXMemberExpression which contains a JSXIdentifier Node with the following structure:
Prop Node {
type: 'JSXIdentifier',
start: 1059,
end: 1064,
loc: SourceLocation {
start: Position { line: 32, column: 18 },
end: Position { line: 32, column: 23 }
},
range: [ 1059, 1064 ],
name: 'Group',
_babelType: 'JSXIdentifier',
parent: Node {
type: 'JSXMemberExpression',
start: 1048,
end: 1064,
loc: SourceLocation { start: [Position], end: [Position] },
range: [ 1048, 1064 ],
object: Node {
type: 'JSXIdentifier',
start: 1048,
end: 1058,
loc: [SourceLocation],
range: [Array],
name: 'components',
_babelType: 'JSXIdentifier',
parent: [Circular]
},
property: [Circular],
_babelType: 'JSXMemberExpression',
parent: Node {
type: 'JSXOpeningElement',
start: 1047,
end: 1083,
loc: [SourceLocation],
range: [Array],
name: [Circular],
attributes: [Array],
selfClosing: true,
_babelType: 'JSXOpeningElement',
parent: [Node]
}
}
}
Those tests look great to me!
Note that the rule needs to work with the default eslint parser, the babel parser in babel-eslint, and the typescript parser, and your links only deal with the babel parser. It would be helpful to duplicate each of the test cases, and make sure each one runs on each of these parsers.