Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

no-unused-props: not ignoring components with no used props #1303

Merged
merged 3 commits into from Jul 21, 2017

Conversation

DianaSuvorova
Copy link
Contributor

@DianaSuvorova DianaSuvorova commented Jul 14, 2017

This is tackling one of the many issues with the rule (issue #1162).

The problem was that the rule simply ignored the components that had no used properties. Once this condition was removed I had to fix some tests and remove some other (that were never actually worked correctly but were passing because of the component had no used props and thus was excluded from validation).

This change might fix few more false positives that users reported. I am planning to go through the list of open issues for the rule and close/fix some more after I get some feedback on my change.

@@ -784,54 +764,6 @@ ruleTester.run('no-unused-prop-types', rule, {
].join('\n')
}, {
code: [
'const statelessComponent = (props) => {',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all for some reason the same test were copy-pasted 4 times. I had to remove this test all together because it was never validated correctly. Current implementation is not able to detect correctly nested definition of components (subRender technically is a react component). I don't think this is a common/idiomatic react style so I assume we shouldn't support it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not the same test; I see 4 different variations: arrow, non-arrow, and destructured, and non-destructured.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Makes sense. Is ti ok if I delete these tests though. Do you agree that this rule technically doesn't support this use case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it ideally should support this use case, but our mechanism of detecting components may make it difficult.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you ask me, the fact that certain cases are not supported could also be tested. Of course, with a test name which clearly states "testing unsupported case" and assertions that clearly check for false positives or other signs of "not supported"-ness.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, that seems reasonable too.

@@ -3215,6 +3148,23 @@ ruleTester.run('no-unused-prop-types', rule, {
line: 11,
column: 8
}]
}, { // None of the props are used
code: [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the test reported for the issue

Copy link
Member

@ljharb ljharb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this!

if (usedPropTypes[id]) {
list[j].usedPropTypes = (list[j].usedPropTypes || []).concat(usedPropTypes[id]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change doesn't seem like an improvement; it increases the number of mutations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I agree it is not exactly an improvement.
Reason I added it - so usedPropTypes it is an empty array when there is no usedPropTypes instead of null. Otherwise anywhere we loop through it we would need to check if it is an array or not.
I can remove it and add few checks in the rule file - I don't mind either way. Let me know what is preferable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean. Could you point to the other lines you'd need an || [] on, if you reverted these lines?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one place actually:
lib/rules/no-unused-prop-types.js:236

    function isPropUsed(node, prop) {
      for (let i = 0, l = node.usedPropTypes.length; i < l; i++) {
        const usedProp = node.usedPropTypes[i];
        if (
...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ambivalent; my general preference is to avoid mutations, but I don't feel super strongly about it. I'll leave it up to you :-)

code: [
'var Hello = createReactClass({',
' propTypes: {',
' unused: PropTypes.string',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test case specifically had React.PropTypes, and React.createClass - are these not necessary to trigger the bug?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This didn't matter. But I updated the test so it is identical to the one reported.

if (usedPropTypes[id]) {
list[j].usedPropTypes = (list[j].usedPropTypes || []).concat(usedPropTypes[id]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean. Could you point to the other lines you'd need an || [] on, if you reverted these lines?

@@ -1387,15 +1319,16 @@ ruleTester.run('no-unused-prop-types', rule, {
code: [
'const PagingBlock = function(props) {',
' return (',
' <SomeChild {...props} />',
' <span>{...props}</span>',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to detect usage of a spread operator (that is handled in JSXSpreadChild I added to the rule) SomeChild has to be a valid react component (this is a babel-eslint thing). Originally when component was invalid, babel didn't detect any props passed to it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need to use babel-eslint for spread props; so let's revert this change and keep it on the default parser.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as far as I can tell spread works with es6 but jsx spread inside a component does not. Simply removing babel and adding parserOptions fails with : message: 'Parsing error: Unexpected token ...',

I am happy to try any suggestions you might have.

  {
    code: [
      'const PagingBlock = function(props) {',
      '  return (',
      '    <span>{...props}</span>',
      ' );',
      '};',

      'PagingBlock.propTypes = {',
      '  nextPage: PropTypes.func.isRequired,',
      '  previousPage: PropTypes.func.isRequired,',
      '};'
    ].join('\n'),
    parserOptions: Object.assign({}, parserOptions, {ecmaVersion: 2017})
    }

error:

    AssertionError: Should have no errors but had 1: [ { ruleId: null,
    fatal: true,
    severity: 2,
    source: '    <span>{...props}</span>',
    message: 'Parsing error: Unexpected token ...',
    line: 3,
    column: 12 } ]
      + expected - actual

      -1
      +0
      
      at testValidTemplate (node_modules/eslint/lib/testers/rule-tester.js:415:20)
      at Context.RuleTester.it (node_modules/eslint/lib/testers/rule-tester.js:545:25)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused… {...props} in a child context isn't anything; that shouldn't work. Spreading is a JSX attribute thing, and a JS iterable thing, and a JS object thing - but not a child thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I have to add that the reason it worked before is that the component was invalid by itself so the parser wouldn't try to parse the props of SomeChild. And that way the linter didn't know that props are actually used. That was ok before this change (where components with 0 used props were simply not validated). If we want to validate components with no props used, we need to make sure that where they are actually used, the code is syntactically valid.

In other words if I keep this test as it is it would fail with nextPage and PreviousPage are not used error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused… {...props} in a child context isn't anything; that shouldn't work. Spreading is a JSX attribute thing, and a JS iterable thing, and a JS object thing - but not a child thing.

I guess it is getting late... I am not sure I understand what you mean .. But I agree regrdless😃

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<span>{...props}</span> is invalid jsx - if babel-eslint parses it, it's a bug in babel-eslint.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, good point. Let me update the test so SomeChild is a thing then. I will look into creating the issue with babel-eslint.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix was easy. Changing type from JSXChildSpread to JSXAttribute spread...Thanks for catching this. I will go read about JSXChildSpread though... maybe it is some beta feature...

' );',
'};',

'PagingBlock.propTypes = {',
' nextPage: PropTypes.func.isRequired,',
' previousPage: PropTypes.func.isRequired,',
'};'
].join('\n')
].join('\n'),
parser: 'babel-eslint'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what in this test needs babel-eslint?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this component uses JSX spread few lines above.So it needs babel.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsx spread works in ES6 or above in the default parser with parserOptions: { jsx: true } - there's definitely no need for babel-eslint for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

responded above

' }',
'});'
].join('\n'),
parser: 'babel-eslint',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly here, what in this test needs babel-eslint? this test seems like it'd work with ES6.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, here es6 would do.

@DianaSuvorova
Copy link
Contributor Author

DianaSuvorova commented Jul 15, 2017

@ljharb, looks like this will take some more time. Neither babel nor es6 parser gives any indication that {...props} is being used. And it has nothing to do with component being a thing (thanks for catching that issue with invalid spread for babel ).

So this

 return <SomeComponent {...props} />;

is parsed into:

image
note 0 children in arguments. If I use props without spread, I see children there.

What dependency do you think this problem starts from? Would it be an eslint/espree issue?

Update: Nevermind, it shouldn't have children it should have props. I will look into that and update PR. Thanks for your help!!

@@ -1317,9 +1318,16 @@ ruleTester.run('no-unused-prop-types', rule, {
}, {
// `no-unused-prop-types` rest param props in jsx expressions - [Issue #885]
code: [
'const SomeComponent = funciton(props) {',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will be gone. Thanks.

if (usedPropTypes[id]) {
list[j].usedPropTypes = (list[j].usedPropTypes || []).concat(usedPropTypes[id]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ambivalent; my general preference is to avoid mutations, but I don't feel super strongly about it. I'll leave it up to you :-)

@@ -3148,24 +3148,7 @@ ruleTester.run('no-unused-prop-types', rule, {
line: 11,
column: 8
}]
}, { // None of the props are used
code: [
'var Hello = createReactClass({',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, the duplicated test isn't a duplicate; one imports React, and the other doesn't, and it's intentional to check both.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I will bring this back.

@@ -784,54 +764,6 @@ ruleTester.run('no-unused-prop-types', rule, {
].join('\n')
}, {
code: [
'const statelessComponent = (props) => {',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not the same test; I see 4 different variations: arrow, non-arrow, and destructured, and non-destructured.

@@ -234,8 +233,9 @@ module.exports = {
* @returns {Boolean} True if the prop is used, false if not.
*/
function isPropUsed(node, prop) {
for (let i = 0, l = node.usedPropTypes.length; i < l; i++) {
const usedProp = node.usedPropTypes[i];
const usedPropTypes = node.usedPropTypes || [];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reverted that change from Components file.

@DianaSuvorova
Copy link
Contributor Author

@ljharb sorry for so many updates. I squashed my change into 1 commit and it is ready for re-review. Thanks!

@@ -897,6 +897,13 @@ module.exports = {
}
},

JSXSpreadAttribute: function(node) {
const component = components.get(utils.getParentComponent());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems super weird to me that getParentComponent infers the node rather than taking it as an argument (not your code ofc, just thought I'd comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeap

' }',
'}',
'Comp2.propTypes = {',
' prop2: PropTypes.arrayOf(Comp1.propTypes.prop1)',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test implies that Comp1's prop1 prop should be marked as used, because Comp2 references its propType; but in fact I think it shouldn't - could you move this test to be invalid instead of just deleting it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -784,54 +764,6 @@ ruleTester.run('no-unused-prop-types', rule, {
].join('\n')
}, {
code: [
'const statelessComponent = (props) => {',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it ideally should support this use case, but our mechanism of detecting components may make it difficult.

Copy link
Member

@ljharb ljharb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for the iteration!

@DianaSuvorova
Copy link
Contributor Author

@lencioni, when would you be able to take a look at this PR? This is my first PR for the repo so I am not sure what is the normal turn around here.

Thank you,
Diana

@ljharb
Copy link
Member

ljharb commented Jul 20, 2017

I'll give it another day, then I'll merge it if there's no objections by then.

@ljharb
Copy link
Member

ljharb commented Jul 20, 2017

Sorry for the inconvenience; I've just merged a refactor to start using arrow functions. Would you mind rebasing latest master? I'll merge ASAP after that's done.

@DianaSuvorova
Copy link
Contributor Author

@ljharb, no problem. Should be all set.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants