jsx-eslint / eslint-plugin-react

React-specific linting rules for ESLint
MIT License
8.93k stars 2.77k forks source link

Feature: Disallow non-boolean type for inline If with logical && operator #2073

Closed cburgmer closed 2 years ago

cburgmer commented 5 years ago

We are making use of inline conditional guard statements a lot like:

    <div>
      {unreadMessages.length > 0 && <h2>Hey</h2>}
    </div>

We often run into bugs where a string literal 0 is rendered, because a short hand notion was used, which react does not allow:

    <div>
      {unreadMessages.length && <h2>Hey</h2>}
    </div>

React will not render a boolean value, while it does for integer values like 0 and 1.

If possible, I'd like to detect this pattern and flag it early. Apologies if this already exists, I checked for "conditional" and "inline" and couldn't find a match.

ljharb commented 5 years ago

This is a tricky one, because if it’s any falsy value besides a number, it will work properly.

Once the nullish coalescing operator lands, and that’s a better alternative, it might make more sense to have a rule - right now, such a rule would probably be more noisy than helpful, I’m afraid.

krawaller commented 5 years ago

I managed to miss this issue, and implemented a rule as suggested here by @cburgmer in PR #2077.

It protects solely against using the truthiness of a length prop as guard, which as @ljharb said in the PR might be too narrow to be useful.

I don’t see an immediately obvious way of expanding the rule to cover more numeric guard cases, but my gut feeling is that the .length case is common enough to justify the rule.

Then again, it might be that I want to believe that just so I’m not alone in being a schmuck (did exactly this mistake in production code twice)...

ljharb commented 5 years ago

Relying on the implicit truthiness of .length anywhere imo is a bad idea (for this reason as well as many others) - if you always do a strict number comparison with .length, you won't run into the problem there at all (which the airbnb styleguide requires, ftr).

krawaller commented 5 years ago

Hmm. Interesting. I was focused on the fact that implicit length truthiness is especially dangerous in JSX, earning me the title ”0 of the week” on the main company whiteboard.

image

But I see your point - you’ll live a happier life if you never rely on it, jsx or not.

Still, in most other environments it’s more arrogant than unsafe to do it, no? There I’d classify this rule as ”coding style”, while in JSX it’s a ”potential error” (actually pretty much a guaranteed one).

@ljharb, would you consider this rule if we add a ”forbidEverywhere” option, disallowing boolean casting of dot length anywhere as per the airbnb styleguide?

krawaller commented 5 years ago

Another way of making the rule more useful could be to allow the user to add props other than .length to also be included in the check. Maybe I'm using lots of Set:s in my codebase and could thus add .size, for example.

ljharb commented 5 years ago

I don't think it's really something a linter can do - there's no guarantee a length property is even a number (not that I can think of a use case for that). I think at some point the solution is always going to boil down to actual tests.

krawaller commented 5 years ago

A linter would've saved me, twice! But then again, so would writing better code. :)

I agree there's no guarantee, but I think the argument is that there doesn't need to be, and that erroneous casting of .length is common enough to warrant a rule.

Also, protecting against this particular mistake with tests seems a bit off - would you really write a test to see that you didn't render a zero?

But I'm liking the idea of enforcing the AirBnB rule of never casting length to boolean, and so we'll probably widen the rule to do that and deploy it to our linting suite outside of the React package.

Thank you for the input!

cburgmer commented 5 years ago

I might have misunderstood, the only linting rule I've sound so far is https://github.com/sindresorhus/eslint-plugin-unicorn/blob/master/docs/rules/explicit-length-check.md, which is not included in the Airbnb rule set.

To reproduce:

$ npm install eslint-config-airbnb-base
$ npx install-peerdeps --dev eslint-config-airbnb-base
$ cat > h.js << EOF
heredoc> const list = [];
if (list.length) {
  list.pop();
}
heredoc> EOF
$ npx eslint .
$
krawaller commented 5 years ago

@ljharb: I want to make a last attempt at convincing you!

In the 2.5 months passed I've seen the arr.length && <Something/> mistake thrice more in the wild. Yes, a rule that could catch all numeric guards would be better, and a rule that could catch all non-boolean guards would be better still. But I think the flawed arr.length usage is common enough to warrant a dedicated rule. It might not be a broad rule, but I argue it'll save more than enough grief to pay for the inclusion.

To me the bottom line is this: we have a very common pattern in normal JS that might be frowned upon by some, but it is still widely used and perfectly safe. In JSX it suddenly isn't safe anymore.

So yes, you could argue that users should use a general explicit-length-check rule like the one linked by @cburgmer above. But to me that rule enforces an opinion, which falls in a different category. The proposed jsx rule catches the usage in JSX specifically, which is always a mistake.

krawaller commented 4 years ago

Half a year later, new assignment, ran this rule on the (large) codebase. Found 10 violations, all of which were real dangers of rendering a 0 to screen. Codebase is otherwise in very good shape with sound linting, and the devs are experienced and driven.

I'm even more convinced than before that this rule deserves a place in a React-dedicated rules package.

MuYunyun commented 4 years ago

I managed to miss this issue, and implemented a rule as suggested here by @cburgmer in PR #2077.

By the way, how to config make it take effect? :smile:

marhaupe commented 3 years ago

I'd like a rule for that as well. I'm not sure if this package treats react-native as first class citizen, but if it does, another point to consider is that a lint rule like this would actually prevent errors in react-native. Unlike in the browser, you can't render text outside of <Text/> components. If you do, your app crashes. Another source of errors are cases like this:

{  
  props.text && <Text>{props.text}</Text>
}

This would just render an empty string and just won't affect a react app. In react native however, we'd be trying to render a string outside of a Text component too.

pke commented 3 years ago

Half a year later, new assignment, ran this rule on the (large) codebase.

Where is the rule? You refer to the length one?

Will a more broader rule be considered? Especially in the react-native env such rule would help to prevent serious bugs in the code base. Maybe it could only be implemented as a typescript rule, because the TS compiler knows about types (and RN projects are usually type script projects).

cburgmer commented 3 years ago

I've seen eslint rules in conjunction with TypeScript. Maybe the rule I'm looking will benefit from the type information. Would such a rule still be welcome here?

ljharb commented 3 years ago

@cburgmer as long as it's still useful with no type information, absolutely.

phaux commented 3 years ago

One solution is to use "@typescript-eslint/strict-boolean-expressions": ["error", { "allowNumber": false }].

Alternative solution which doesn't require types would be to create a rule which disallows a && b in JSX altogether and always autofixes it to a ? b : null

ljharb commented 3 years ago

that wouldn’t be appropriate tho when a is a boolean type :-/

phaux commented 3 years ago

Why though?

I often use ternary in JSX even when the else branch is just null. I like it because it's consistent. It also kinda forces you to at least consider displaying some message like "No data to show" which is usually a nice UX. I once had to refactor a bunch of components to show these empty states in UI and if I used ternaries from the beginning it would be much easier. It would be useful rule for enforcing style, and if you want actual bug-catching rule then there's strict-boolean-expressions.

ljharb commented 3 years ago

"a nice UX" depends very much on the context.

cburgmer commented 3 years ago

a ? b : null

Just to share a painful learning from today: The null here is rather important, and "" should be avoided, although it looks the same to the user. What happens is that for "" React will render an empty text node, which in our case broke our xpath query we are using for our browser-based tests. //span[contains(text(), "my caption"] will not behave as expected if an empty text node is created alongside the caption:

The following JSX

<span>
  {false ? "whatever : ""}
  "my caption"
<span>

will result in this HTML (quotes used to highlight the text nodes)

<span>
  ""
  "my caption"
</span>
ljharb commented 3 years ago

@cburgmer to be fair tho, relying on XPath queries like that is inherently brittle, so the flaw there isn't use of the empty string.

nathggns commented 3 years ago

Would very much like a rule that enforces ternary usage over && for conditional rendering in JSX.

phaux commented 3 years ago

To enforce ternary in JSX you can use no-restricted-syntax like this:


{
  "no-restricted-syntax": [
    "error",
    {
      "selector": "JSXElement > JSXExpressionContainer > LogicalExpression[operator!='??']",
      "message": "Please use ternary operator instead"
    }
  ]
}
marneborn commented 3 years ago

Just adding to @phaux's suggestion above:

{
  "no-restricted-syntax": [
    "error",
    {
      "selector": "JSXElement,JSXFragment > JSXExpressionContainer > LogicalExpression[operator!='??']",
      "message": "Please use ternary operator instead"
    }
  ]
}
bobaaaaa commented 3 years ago

@marneborn your example does not work. Try this:

{
  "no-restricted-syntax": [
    "error",
    {
      "selector": ":matches(JSXElement, JSXFragment) > JSXExpressionContainer > LogicalExpression[operator!='??']",
      "message": "Please use ternary operator instead"
    }
  ]
}
alesmit commented 3 years ago

It would still be great to have a rule to detect non-boolean type for inline rendering, without needing to enforce ternary expressions. I thought the goal was to spot code like this:

<div>
  {unreadMessages.length && <h2>Hey</h2>}
</div>

rather than banning any logical expression with && in JSX. Is there any way to achieve this with custom rules?

krawaller commented 3 years ago

@alesmit I have an implementation of exactly that in #2077 (which was shot down)

ljharb commented 3 years ago

The goal is to spot any chance for a 0 to be rendered. .length isn't the only one, and it isn't a reliably detectable one without type information.

kachkaev commented 3 years ago

Implementing a proper boolean check is quite problematic indeed. It requires both TypeScript and React contexts.

In the meantime, I've added this rule to our .eslintrc.js, which is an enhanced version for the previous suggestions [1], [2]:

"no-restricted-syntax": [
  "error",

  ...otherRules,

  // Two rules below help us avoid this common point of confusion: https://stackoverflow.com/q/53048037
  // The selectors are inspired by https://github.com/yannickcr/eslint-plugin-react/issues/2073#issuecomment-844344470
  {
    selector:
      ":matches(JSXElement, JSXFragment) > JSXExpressionContainer > LogicalExpression[operator='&&']",
    message:
      "Please use `condition ? <Jsx /> : undefined`. Otherwise, there is a chance of rendering '0' instead of '' in some cases. Context: https://stackoverflow.com/q/53048037",
  },
  {
    selector:
      ":matches(JSXElement, JSXFragment) > JSXExpressionContainer > LogicalExpression[operator='||']",
    message:
      "Please use `value ?? fallbackValue`. Otherwise, there is a chance of rendering '0' instead of '' in some cases. Context: https://stackoverflow.com/q/53048037",
  },
],

The messages are meant to help folks who are relatively new in React or JS as a whole. Showing the expected syntax and explaining the pitfall makes the rule easier to understand.

Hope it helps!

lindboe commented 3 years ago

For what it's worth, this is an even more severe issue on React Native. If you accidentally return a value like 0 where there's supposed to be a component, it throws a (confusing) error that breaks your app instead of just rendering an unexpected value (for example, see https://stackoverflow.com/questions/52368342/invariant-violation-text-strings-must-be-rendered-within-a-text-component/59108109#59108109)

ljharb commented 3 years ago

If we can come up with a rule that by default assumes React, but has an option to go into "React Native" mode - and that rule provides no false positives, and still provides some value - then I think we might have a candidate here.

Can someone sum up what that might look like?

hluisson commented 2 years ago

Ended up encountering this issue and writing a rule for it. For those using TypeScript, you can try using jsx-expressions/strict-logical-expressions

rijk commented 2 years ago

@hpersson this is fantastic!! I've been looking for a rule for this for a long time and was about to start writing it myself. It looks like a minor issue but the mistake is so easily made, and in React Native it can even cause a crash due to the Text strings must be rendered within a <Text> component error. Thank you!

ljharb commented 2 years ago

@hpersson if your rule can be made to match the expectations in https://github.com/yannickcr/eslint-plugin-react/issues/2073#issuecomment-901577684, a PR would be great.

bogas04 commented 2 years ago

Created a babel plugin to automatically fix this error in a codebase

steps to use

MichaelDeBoey commented 2 years ago

I want to reference @kentcdodds' article about this too, as I think it can be helpful in this discussion

https://kentcdodds.com/blog/use-ternaries-rather-than-and-and-in-jsx

remcohaszing commented 2 years ago

I think it should be fairly simple to create a rule which:

  1. Disallows logical expressions using || or && direcly inside a JSX expression node (basically https://github.com/yannickcr/eslint-plugin-react/issues/2073#issuecomment-864168062 with an autofix)
    <div>
     {foo && bar}
     {foo || bar}
    <div>

    autofix:

    <div>
     {foo ? bar : null}
     {foo ? null : bar}
    <div>
  2. Disallows JSX syntax as a direct child of a logical expression using || or &&
    const foo = bar && <div />
    const foo = bar || <div />

    autofix:

    const foo = bar ? <div /> : null
    const foo = bar ? null : <div />

The rule could be named react/jsx-no-logical-expressions

Of course this doesn’t catch all scenarios, but I think it catches a significant amount.

If @ljharb is open to this, I’ll create a pull request.

kachkaev commented 2 years ago

It might be better to use undefined instead of null in autofixes for compatibility with unicorn/no-null.

<div>
- {foo ? bar : null}
+ {foo ? bar : undefined}
- {foo ? null : bar}
+ {foo ? undefined : bar}
<div>
ljharb commented 2 years ago

No, that wouldn't be better; "no null" is an absurd rule and philosophy.

@remcohaszing I don't think that's a reasonable approach, since {foo && bar} and {foo || bar} are both perfectly reasonable and acceptable if you're using react web, and if foo is not a zero.

kachkaev commented 2 years ago

No, that wouldn't be better; "no null" is an absurd rule and philosophy.

Hmm I’m not sure if ‘absurd’ is the right term for this opinion in code style... Could you please elaborate?

According to official React docs, null and undefined are equally valid:

Screenshot 2022-01-31 at 18 46 00

Advantages of not using null are summarised in unicorn/no-nullWhy? section.

ljharb commented 2 years ago

@kachkaev i'm talking about the "unicorn" rule.

Also, undefined is only a valid return from components in very recent versions of React; null is the safer choice for backwards compatibility.

I disagree with all of those advantages, including the "intent" post, and that's what I'm saying is absurd.

kachkaev commented 2 years ago

Thanks for sharing your thoughts, but I’m still not convinced 😅 Repeating ’this is absurd’ but not providing any links or arguments does help others find flaws in their thinking. You don’t have to prove anything to me, but folks in this thread may be interested in finding out more on this subject 👀

Regardless of unicorn/no-null, usage of undefined in JSX ternaries still seems to have merit. Perhaps, there is scope for an option in the new potential rule.

ljharb commented 2 years ago

@kachkaev null is an important part of the language. undefined triggers defauls, null doesn't. I will repeat that it is absurd to suggest not using a critical language primitive that has distinct semantics.

Since I don't think the "change to ternaries" approach is viable anyways, it's not really worth the time to debate null vs undefined in it here.

MichaelDeBoey commented 2 years ago

We can always implement what @remcohaszing suggested in https://github.com/yannickcr/eslint-plugin-react/issues/2073#issuecomment-1025782557, but we don't need to make it recommended.

People can than enable the rule if they want to.

ljharb commented 2 years ago

@MichaelDeBoey "what's recommended" isn't particularly relevant; providing a rule at all is a tacit endorsement of enabling it, so rules that shouldn't exist, are best not implemented (require-await, for example, being a core rule whose mere existence harms the ecosystem).

MichaelDeBoey commented 2 years ago

This new rule could be enabled if they want to make sure they never make the mistake with a 0 rendered without them knowing, so I think this rule can be beneficial for a lot of people tbh.

ljharb commented 2 years ago

@MichaelDeBoey i understand the value, but i think it's too blunt an instrument.

If we could use propType or TS type information to know with certainty when a zero is possible, that might be worth looking into.

Such a rule would also need an option for react native users, to widen the list of "bad values" from zero to whatever RN is unable to render.

kachkaev commented 2 years ago

There is https://github.com/yannickcr/eslint-plugin-react/issues/2073#issuecomment-945025341 for TS users already. AFAIU this package cannot leverage typings because it is JS-only.

ljharb commented 2 years ago

@kachkaev that's right, but it can leverage propTypes, which are much more powerful than TS is for determining the type of a thing (altho the limits of static analysis do constrain what we can do here)

rijk commented 2 years ago

FWIW the Typescript rule works like a charm. So I'm not sure which limits you are referring to