Interesting Bugs Caught by ESLint's no-constant-binary-expression (2022)
eslint.org
eslint.org
In general, I find it frustrating that the eslint recommended presets don't document why they are recommended. I disable several of the rules in all my projects because they seem to be arbitrary stylistic choices (e.g. [0], [1], [2]).
[0] https://typescript-eslint.io/rules/no-empty-function/
[1] https://github.com/jsx-eslint/eslint-plugin-react/blob/maste...
[2] Specifically checkLoops of https://eslint.org/docs/latest/rules/no-constant-condition
[edit] In looking up no-empty-function, I saw a stack overflow post that provides a compelling alternative of using `() => undefined` instead of `() => {}`, which suppresses the error. That should be shown as an example on the eslint page!
That’s a good point. While this doesn’t address the root problem, I can share some context here as a former maintainer: browsing the core rules [1], you should see that recommended rules flag cases that are highly likely to be bugs or unnecessary constructs. Recommended rules should have false positives only in exceptional cases and be objective or near-universal consensus opinions.
Plugins, of course, are free to choose their own threshold for their recommended configs.
> Why isn't it in the "recommended" preset of lints?
It will be added to recommended in v9! [2] The rule was written during one of the v8 minor versions, and adding to the recommended config is always a breaking change.
0 is to prevent mistakes where you declare a function, but forget to implement it. If that's really intended I like to put a comment "do nothing" in empty functions, even in projects without the ESLint rule, so future readers know what's going on.
1 is for consistency & readability. Imagine you read someone else's code like <div children="foo" />, and you see that it's self-closing, so you expect it's an empty div. Even more confusing when you have more than just one attribute. Or say you want to modify the code to add a new child, so you remove the self-closing and add the child, breaking the old children in the process. What is the reason for doing children="foo" in the first place anyways?
2 is to prevent infinite loops, it's good practice to keep an upper bound (in a single-threaded world like JS, infinite loops can be very deadly and hard to diagnose). I like NASA's ten coding commandments (this is #2): https://devm.io/careers/power-ten-nasas-coding-commandments-...
But generally I agree that it’s overly persnickety to complain about empty functions. My use case for them is that sometimes you need to pass a function as an argument and the function may be null - to indicate you don’t want to do any work. It’s often cleaner to use an empty function as a default value rather than add null checks everywhere.
I don’t use eslint at all because of defaults like this. Having my coding style negged by a tool feels awful. I hate gofmt. I ran it once and it deleted a bunch of empty lines in my code that I had put in on purpose to aid readability. And wow, that pissed me right off! I just know I’ll hate a lot of eslint’s rules just as much. 30 years of coding experience gives you opinions. Kids these days don’t even know how to use the ternary operator properly.
You mean not using it at all? 30 years of coding experience gives you opinions.
From what I remember, being able to pass children as a prop is considered a side-effect of an implementation detail, that breaks the expected abstraction. There really isn't any reason to use it, and I think there's a chance it may even limit what the virtual dom diffing can manage?
Also this would prevent you from accidentally doing both at once:
<MyComponent children={<div>Is it me?</div>}>
<div>Or is it me?</div>
</MyComponent>
(I don't remember what React does in this situation)I like using the "children" prop explicitly because it's a lot more concise (with prettier, I can often go from five lines to one line with this style), and because it's more explicit. I'm not creating my own children, I'm not adding anything to the markup myself, I'm just passing the input children directly to another component.
This isn't really anything unusual, it's exactly what JSX compiles to under the hood. And passing JSX elements in props rather than as children is useful in other contexts as well - for example, a button that allows <Btn icon={<MyIcon />} ...>
You're right that this can cause odd situations where children are specified in two ways, but I'd rather have a lint rule explicitly handle this case than disallow using the "children" prop altogether. (In fact, I think Typescript does validate this case already? But I might be wrong there.)
I can see why this rule might be useful to have in general, but I agree with the previous poster that it's a semi-opinionated rule, and one of the sort of rules where I end up having to disable it and fiddle around with configs whenever it comes up, which in turn puts me off using ESLint in general because I know to get it to be useful I'm going to need to spend some time changing everything up.
I'm not completely sure I would take it all the way to calling it a bug, but I do appreciate a rigorous way to simplify code, because the worst case is that you've made it easier to reason about and that's a win all by itself.
(EDIT: This post also makes me feel better about my personal coding style being paranoid and doing things like using parens to force order of operations and avoiding "advanced" constructs like ?? because I don't trust myself to not shoot myself in the foot. I'm not a professional dev, so I'm happy to write verbose, inelegant code in exchange for it being so simple that I'm less likely to screw it up)
Writing clear, concise code is *hard.* Writing just clear code, conciseness be damned, is a perfectly fine middle ground.
Delete it? Version control is your backup
return X unless [complex logic]
I sometimes see this stacked inside other complex logic and it’s a nightmare sometimes to figure out the actual control flow.
So e.g. you have lines like:
const isCar = (wheels === 4) && (steeringWheels === 1) && (canDrive === true);
if (isCar) { ... }
and not lines like: if ((wheels === 4) && (steeringWheels === 1) && canDrive === true) { ... }
And similarly: const isCar = (wheels === 4) && (steeringWheels === 1) && (canDrive === true);
return isCar;
Rather than: return (wheels === 4) && (steeringWheels === 1) && (canDrive === true);
I'd actually like linters for JS and Python to enforce named conditionals and named returns, but afaik no one has done this yet.Other linters might complain about `==`, though ;)
Often a code comment can be avoided (and clarity improved) by giving a name to an intermediate value rather than letting it be a nameless expression.
I like it! But I don't think of this a no-op code and would not imagine a lint rule objecting to it. That said, a code minifier (or compiler) would be well positioned to optimize that code, so you shouldn't even have to worry about even the thought of perf implications.
I hope the take away for the reader is: If you can think of other rules that will detect useless code, you should pursue them, because they are likely more valuable than just enabling dead code elimination. They have a high probability of being able to uncover interesting bugs/mistakes as well, which is much more valuable.
I quite often write 'useless' code, just to make the intent more obvious.
Int Foo,bar = 1,5
Int baz = foo+bar
I could just make baz 6, but then I don't know why it's 6.
> The rule checks for comparisons (==, !==, etc) where the outcome cannot vary at runtime, and logical expressions (&&, ??, ||) which will either always or never short-circuit.
If you write if(baz == 6) then the if clause is useless and probably a bug.
I'd expect the compiler to optimise out
If(somedebugflag)
Not tell me off for writing buggy code.
I'll never understand why programmers don't simply put parens where they want the expression to be evaluated, rather than relying on their (sometimes incorrect) assumption about operator precedence. I want to chalk this up to hubris, but it's probably just laziness.
I’ve taken naming them as variables instead. Definitely more verbose but more readability.
const aIsB = a === b;
… aIsB ?? c … return a === b ?? c;
The intent was const bOrDefault = b ?? c;
return a === bOrDefault;
You posted the actual (buggy) evaluation. I agree that its better if that is what you intend.aIsB is guaranteed to not be null or undefined so the ?? is a no-op. You just did a comparison so it's either true or false.
For general clarify for precedence I still agree but in this case it's a silly thing to do. It would be more clear without the syntax shorthand:
const ret = a === b
if(ret == null || ret == undefined)
{
ret = c
}
return retI was just pointing out that this is a case prettier wouldn't do anything. The expression parses differently and adding or removing parentheses to any subexpression will change its meaning.
That by manually doing the transform in your example that you would realize your cody was a no-op or that the intended version was what I posted.
I like the idea that different readers of the same code base could opt for differing levels of explicitness when it comes to operator precedence. One thing that working on the project helped demonstrate for me is that adding parens around _every_ subexpression is _way_ too noisy. So, you need to draw the line somewhere. But for me, I prefer drawing that line on the noisier side.
I prefer the noisier side as well. When adding parens to indicate/force precedence, I will sometimes split my expressions into multiple lines, with indentation, as an aid to legibility, e.g. instead of ( ( a + b ) / ( ( c - d ) / e ) ) I might write:
(
( a + b )
/
(
( c - d )
/
e
)
)
I've found this not only quite legible, but also fairly easy to edit because you can easily match parens visually.I feel like if you just remove the spaces around the parens themselves, it's the most clear: ((a + b) / ((c - d) / e))
Nowhere I have worked would pass a code review for what you have proposed.
To answer your earlier question about object definitions, in cases where the objects are small, I do think more concise (single line) notation can be more readable. An example, though not a great one because it's data more than it's code:
items: [
{ type: "a", quantity: "12", price: 123.45 },
{ type: "b", quantity: "7", price: 456.78 },
{ type: "c", quantity: "3", price: 9.00 },
]
If the number of properties exceeds say 3, or the names of them are complex, I would lean toward the longer form.Another example: do you write { foo: bar, baz: bat, bing: bang } or:
{
foo: bar,
baz: bat,
bing: bang
}
If you use the latter method, why wouldn't you write your expressions the same way? Obviously not the ultra simple ones like a + b, but the longer ones.Incidentally, I used to write code with no spaces between parens, which is what you suggested: ((a + b) / ((c - d) / e)), but eventually found it much easier to always put spaces around parens: ( ( a + b ) / ( ( c - d ) / e ) ). I would say this is similar to the indent-with-spaces-vs-tabs argument, which will rage forever.
The thing for me is my text editor visually matches the braces, so the second option makes that harder.
One bug that appeared is bitwise-AND followed by a comparison with no parentheses grouping the bitwise-AND operation. Obviously developers thought the bitwise-AND had higher precedence than the comparison. They were wrong. They obviously didn't test the code either.
Javascript code doesn't twiddle bits as often as C/C++ code so this bug doesn't appear as often.
For text editor support of this problem, what may work is to show that an expression with higher precedence IS evaluated first before the expression using an operator lower on the operator precedence table.
There was a suggestion in the Vim mailing list years ago to use background color to show the nesting depth of a block.
One could use background color or a font attribute, say underline or font-size, to indicate the evaluation order of a non-parenthesized multi-operator expression.
HN posts only support italics for font formatting so I'll use that in my example - hmmm, looks like I can't use block formatting in addition - just imagine the code indented in a fixed-width font
====
if (x & 3 == 1) { // do something }
===
a=(b+c)
Because you know it's not necessary. But we're human and sometimes make mistakes when we're sure we didn't.That's also why we allow for some misunderstanding, (rather than (writing (using) (sentence trees))).
On a related note, I occasionally write expressions that both assign and test, e.g. if ( ( a = b ) == c ) and in those cases I parenthesize liberally and always write a comment that indicates yes, I intend to make an assignment in the middle of a test for equality.
while((buf = resultofstreamingoperation() != null) {
}
The ` != null ` is redundant here but you can imagine where it'd be required. if ((error = do_a_thing())) {
// some error happened, good thing we saved the error code
}
Given the standard "zero is success" paradigm, handling errors is very succinct. a = b
if ( a == c ) ...
The repetition is even worse if you replace 'a' or 'b' with a more complex expression: a[foo/bar] = b[baz/bat]
if (a[foo/bar] == c) ...
Oof indeed! Just rewrite it as: if ( ( a[foo/bar] = b[baz/bat] ) == c ) ... // Yes, assign and test!In a code review I would definitely reject this.
What I'm getting at is one person's "easy to parse" is another person's "difficult to parse", and there may be no objective answer which makes one any better than the other.
I would think to myself if I was writing one “have I made a mistake somewhere that means I have to use this?”
I don’t think it’s necessarily about “can I parse this”, it’s much more about will all the devs on the team be able to parse this.
To me, code is all about communication - to myself, but also to an audience I haven’t met with varying skill, knowledge and context levels.
I’ve been bitten too many times with code that only one or two people can work on.
I'm not sure where you get that idea. Repetition anywhere is usually a code smell.
> You write code once
And then you update it when you add new features or the requirements change or you fix bugs, etc. Having to change two symbols is more error prone than changing one. And having to parse more code is harder than parsing less code.
The assign-and-test pattern is common in several languages (e.g. C), and adding a comment that explains the logic should remove all doubt as to what is happening and why, so I see it as a win/win.
In any case, there is a trade-off between terseness and legibility, and while I usually favor more verbose code, I tend to draw the line at needless repetition. But that's my personal preference, and everybody draws that line in different places.
I'd rather parse three straightforward copies with minor differences than one chunk of dry spaghetti. And in assign & test case, splitting them makes debugging much easier.
key1 = foo/bar
key2 = baz/bat
a[key1] = b[key2]
if (a[key1] == c)
I'm sure a coder who understands the context can come up with better names than key1 and key2.edit: or better yet
key2 = baz/bat
normal_var = b[key2]
if (normal_var == c)
...
key1 = foo/bar
a[key1] = normal_varYou're also splitting what is intended to be essentially an atomic operation into multiple steps, which can be good if you want to analyze and tweak them in the future, but it's now no longer clear where the process begins and ends in relation to the code that comes before and after: you have to add more comments, or split the whole thing out into its own function.
I'm not saying your code is bad or wrong, just that there are downsides to any solution (including mine), and ultimately everybody has to pick whichever has the fewest negatives for their particular project.
Here's one example that the rule caught: https://github.com/captbaritone/vscode/blob/ab86e0229d6b4d0c...
I've been writing JS for over 10 years now, and I'm not sure I would have caught that in code review.
Huh, now I'm curious if my IDE would highlight this with a warning... will have to check!
You may be able to chalk it up to some “senior” reviewer who asks a junior developer to remove the parens because “they aren’t needed”, and the junior developer doesn’t know how to argue their case.
Left to right is fine. So is right to left. Just pick one and stick with it.
“But PEMDAS!” Ok, YOU can use parens.
This has also caught many real world bugs in C such as https://github.com/aircrack-ng/rtl8812au/issues/308
Like you say, macros often make this very hard to enforce in practice.
I regularly do things like:
if (0 && expression) ...
and: if (1 || expression) ...
to temporarily disable a conditional when I'm looking for a bug.I wrote a lot of code for the optimization step of the compiler of Racket, in particular steps to eliminate similar code. I saw those expressions and I inmediately think:
Macro expansions create a lot of similar expressions, and also more complex expressions that the compiler can reduce to trivialy looking expressions [1]. It's nice that the compiler can detect them and eliminate them, but raisng an error would break a lot of code.
My guess is that all three of us agree that the post is about linters, but it would be very bad to extend this rule to the compiler.
[1] For example, after an expansion of a macro or inlining a function you may get:
(define x (random 10))
(if (integer? x)
(display x)
(error "The number should be an integer"))Its compiler can only transpile TypeScript into JavaScript code, and the language itself does not affect runtime behavior (with the exception of enums).
There's another good reason -- by definition, it's impossible to write a test case with full decision coverage if some of the decisions are themselves impossible.
I'm a huge proponent that branch coverage is way more important than just statement coverage, because as you mentioned, it's way easier to miss these types of bugs just by getting 100% statement coverage.
Put another way, you can only get 100% statement coverage if every 'then' and 'else' branch is taken, so 100% statement coverage and 100% branch coverage seems to be saying the same thing I think a misunderstanding you, an explanation would be helpful, thanks.
if (foo() || bar()) {
a();
} else {
b();
}
Statement coverage says that a test must cover the lines of code (or statements) `if (foo() || bar())`, `a()`, and `b()`. But if `foo()` is tautological then the || short circuits, and all of your tests can pass, with 100% statement coverage, even if `bar()` causes the sun to go nova. With 100% decision coverage, you must have a test case that causes `foo()` to return false, so that you can test the behavior when `bar()` both returns true and returns false.Edit: The above isn't a correct example, because statement coverage will not hit `b()` if `foo()` is tautological. Still, you can see the point -- there's a difference between decision coverage and statement coverage.
In my experience, a fairly strict non-aesthetic linter setup makes mentoring much more efficient.
Just in time mentorship!
It’s extremely valuable to have multiple layers of verification, and fast-cheap static analysis tools like linters have a tremendously high ROI as one of those layers, especially in languages with many subtle syntax surprises.
Overall, my point with the examples was to highlight that these are mistakes that even make their way into high visibility projects built by highly competent engineering teams.
That said, looking at the issues few were in really critical paths of these projects. Often they cropped up in auxiliary areas like test harnesses or more off-the-beaten-path features. One can assume the same bugs may have existed at some point in the development cycle in other areas of the code base, but they got caught by more rigorous testing/review of those areas, or bug reports. But it's surely a time saver to identify them _as the developer saves the file_ rather than later in the process. The sooner you catch the bug, the more engineering energy you save.
I thought TypeScript was able to analyze some static code, like below:
const alwaysTrue = true
if (alwaysTrue === false) { }
// This comparison appears to be unintentional because the types 'true' and 'false' have no overlap.(2367)The back and forth on that thread seems to result in advocating for what we’ve got: compiler (TypeScript in your example) not caring, but linter caring.
The reason not to in the transpiler is it can be very helpful during testing to block or force some paths.
Eslint-typescript works well for this. I also recommend eslint-plugin-sonarjs
A typical example is string + number results in no warning. I understand that some people actually want to do it for "convenience", but often this turns out to be a mistake (e.g. passing the wrong variable). However, if you have a Map<string, number> and tries to do map.get( 3 ), that's an error even though you can absolutely do that in JavaScript -- it just always returns undefined, no error thrown.
You can find a stackoverflow thread about this. To me the current behavior does not make any sense -- I use TypeScript for static typing and avoid any implicit type conversion, and I want that to be consistently applied without exception, that's why I am using TypeScript. I am a bit disappointed that there are a number of such holes in TypeScript.
Other times
Genuinely wondering what language that would have been valid in.
Have a brain fart one day and you'll write something like this and be confused why it doesn't work
Obviously, this is runtime code, not a type declaration, but you can see how a new or tired/busy developer’s brain might spit this out in the wrong place.
states.Any(s => s is "VALID" or "IN_PROGRESS")
I'm sure there are languages where a match-able pattern is an expression, allowing the original code basically unchanged. states.find(state => state === 'VALID' || state === 'IN_PROGRESS')
Or states.find(state => ['VALID', 'IN_PROGRESS'].includes(state))Raku, but using “junctive or” | (which creates an any junction of its arguments, which “autothreads” on method calls producing a Junction of the results) instead of “tight or” ||.
Where statestype function ‘includes’ takes a lambda that’s run in states context and overrides plus operator for string to evaluate to states.contains(that string). One could, but should they?
IF X = 0 OR = 1 OR = 2
Does this Python code count? Haha.
‘VALID’ or ‘IN_PROGRESS’ in states
Does not work and I’ve seen it around or SO. Or more commonly it’s variation a == b or c states.includes('VALID' | 'IN_PROGRESS')
is effectively the same as: states.includes('VALID') || states.includes('IN_PROGRESS')
See https://docs.raku.org/type/Junction attribute:(value OR another_value)EDIT:
- pylint: using-constant-test (W0125) https://docs.pylint.org/features.html#id13
- golangci-lint: revive's constant-logical-expression https://golangci-lint.run/usage/linters/#revive
Note that this is pointing out a bug in the language spec. There are two ways to assign the precedence:
a === b ?? c
One says "Compare a to a value, b, which might be null. If it is null, compare against the guard value, c, instead." a === (b ?? c)
The other way says "Compare a to b. Consider whether the result of the comparison is NULL, and if it is, do nothing. If it isn't, do nothing then too. In all cases, return the comparison between a and b." (a === b) ?? c
There's a good reason people make the erroneous assumption that the language itself isn't trying to sabotage them. The left operand of ?? must be a nullable value; for === to bind tighter than ?? implies that it can return NULL, which in fact it can't.[1] https://github.com/typescript-eslint/typescript-eslint/blob/...
Without rigorous systems in place to catch mistakes, mistakes will [more frequently] happen.
> where developers misunderstood the precedence of operators, particularly
> unary operators like !, + and typeof.
if (!whitelist.has(specifier.imported.name) == null) {
return;
}
which is exactly why I always bracket stuff, almost to a fault. I've seen almost exactly this in a piece of shipped code which meant a crucial condition would never trigger. Cost of that? Possibly hundreds of millions or even higher. I'm not exaggerating.This is why I totally hate the tower of precedence in C-style languages.
Being a common second language is relevant, but JS also has weird truthiness rules and very first-class-feeling lists and hashmaps but not native deep comparisons, and many of the bugs you described stem from this. Those categories of bugs don't exist in Python, that has almost perfect truthiness (PSA: an epoch datetime is falsy! Wat) and deep comparisons by default.
if (a < b < c)
if (a < b & c)
if (a = b)
and statements like: for (i = 0; i < 10; ++i);
illegal.Yes. And ditto for `if (expression)` and a couple others. In developing D, I looked at what linters and coding standards did and incorporated into the language rules for troublesome things that there's just no excuse for.
For another tidbit, the JSF coding standard specifies that using a lower case 'l' as an integer literal suffix is not allowed, because it is too easily confused with '1'. D just doesn't allow it; you gotta use 'L'. No need to write a coding standard about it.
Although "real" production software would use feature flags of some kind instead of hardcoding the constant, sometimes you do just need to hide some code behind an if statement that is currently ineffectual, and linters that prevent that are extremely annoying and force me to write a confusing, convoluted expression that is too complicated for the linter to detect as constant true or constant false.
I don't think they are: binary expressions affected are expressions constructed with binary logical or comparison operator (==, ===, &&, ||, ??).
true and false each aren't binary expressions, they are simple constant values, and the lint isn't for constant conditionals.
(The rule for that is no-constant-condition.)
// takes editing tools in many situations
/* doesn't nest
Setting an if to false is easy and fast.
I've never seen a project with 100% condition tests coverage. Sure, they exist, but they're as rare as unicorns.
> By comparison, the project has 590 times as much test code [as program code].
You can either find out you have these bugs from lint errors or you can find out by finding them in code coverage reports while chasing 100% code coverage. Most people would obviously prefer the former.
And even 100% doesn't mean much -- you can easily come up with examples where you have 100% coverage but there are still bugs.