Conversation
|
this should be documented on a rfc and approved first before creating a new linter rule i think |
Contribute suggests rfcs only for language changes, this wouldn't do that, and an issue has been created, though maybe some more time should have passed between issue and pr. On the rule in general, I think it's extremely useful for devs unfamiliar with lua. Nearly every other language has at least 0 as false, and many have a concept of falsey for things like empty strings or tables. As lua and by extension luau doesn't have a broad falsey concept, this lint would catch a very common footgun for newer users. |
|
Can't comment on all the cases, but I like the idea. I think this is also valuable for a case where you have a function |
The linter is not part of the language itself, and frankly will probably be sunset in favor of the linter we're writing in lute since that allows programmers to author their own lints in luau instead of writing C++ to lint luau. Even if it continues to exist as well, it's not something we've treated as requiring an RFC. |
| { | ||
| const char * msg; | ||
| bool negated; | ||
| if (!checkCondition(node->condition, &msg, &negated)) |
There was a problem hiding this comment.
TODO: also check the first argument to assert()
There was a problem hiding this comment.
Or maybe don't. An assert evaluating false is usually understood to be a bug. And the typechecker should often be able to catch the same bugs (at leasts the asserts that assert not nil). So nearly every time this linter rule flagged an assert, it would be a false warning.
also, asserts are pretty specialized. this linter rule is meant to catch common, stupid errors. And I don't think the linter even could distinguish between "asserts that meant to check a boolean but forgot a comparison operator" and "asserts that are checking for non-nil and the type-checker agrees". Maybe checking for intersection types, but either way, it's different enough that it should be a different linter rule
There was a problem hiding this comment.
An assert evaluating false is usually understood to be a bug.
I don't think this is true at all. We use things like assert(false) or assert("this message to read about an unreachable assumption" && false) all the time to mark branches that we expect to be unreachable.
There was a problem hiding this comment.
I added assert() to the list of expressions the linter checks: 29817e7
| } | ||
| }; | ||
|
|
||
| class LintMisleadingCondition : AstVisitor |
There was a problem hiding this comment.
6 month status report:
I'm still finding this linter rule useful enough that I keep bothering to:
- rebase it on new luau versions
- keep building luau-lsp and it's vscode plugin locally so I can use it
- Maintain this branch as luau moves forward
@aatxe mentioned above #2184 (comment) that this rule should be migrated to the lute linter. I'd be happy to do that at some point. However, it's lower priority to me than teaching lute to use definitions files. Until then, lute is useless to me.
There was a problem hiding this comment.
lute check now knows about definitions files, so porting this to lute is now higher priority for me
aatxe
left a comment
There was a problem hiding this comment.
The overall implementation here feels messy and full of bugs and unexplained inconsistencies. At a high level, a lint for conditions that are both unintended and likely unexpectedly wrong is a good thing, but the realization of that here isn't up to our quality bar.
| bool visit(AstExprCall* node) override | ||
| { | ||
| const char * msg; | ||
| bool negated; | ||
| if (const auto* global = node->func->as<AstExprGlobal>()) | ||
| if (strcmp(global->name.value, "assert") == 0) | ||
| if (node->args.size >= 1) | ||
| if (!checkCondition(node->args.data[0], &msg, &negated)) | ||
| emitWarning(*context, LintWarning::Code_MisleadingCondition, node->location, "%s", msg); | ||
|
|
||
| return true; | ||
| } | ||
| }; |
There was a problem hiding this comment.
As mentioned when you were only considering it, I do not think this is a reasonable addition to this lint. It is commonplace to use assert(false) for unreachable, and it will produce this lint as noise on every such instance of that.
There was a problem hiding this comment.
assert(false) does not trigger a warning. It doesn't meet my definition of misleading. there's even a unit test to make sure it doesn't
Valid conditions include one of the following as a union member:
- boolean
- true
- false
- nil
If those are all missing, it is labeled misleading
Things like assert(0) or assert("response") would trigger a warning
| const char * msg; | ||
| bool negated; | ||
| if (const auto* global = node->func->as<AstExprGlobal>()) | ||
| if (strcmp(global->name.value, "assert") == 0) |
There was a problem hiding this comment.
This matches every global function named assert, including shadowed ones, and you modified a test to mask this.
There was a problem hiding this comment.
It also is again using strcmp instead of an ordinary comparison. global->name == "assert".
There was a problem hiding this comment.
I'll remove the strcmp
I wonder if the typechecker can identify the global assert function. I would assume not. I assume the typechecker would just see the name assert and the type <T>(T, string?): T. Maybe I could identify it by MagicAssert
| *negated = false; | ||
| if (const auto* unary = cond->as<AstExprUnary>()) | ||
| if (unary-> op == AstExprUnary::Op::Not) | ||
| { | ||
| *negated = true; | ||
| cond = unary->expr; | ||
| } |
There was a problem hiding this comment.
This hard-codes exactly one not, which means weird things will happen for any chains of not, like if not not 0 then.
There was a problem hiding this comment.
I'll add unit tests for this case and see what happens
|
|
||
| if (isNumber(*type)) | ||
| { | ||
| *msg = *negated ? "(not num) is always false; did you mean (num == 0)?" : "(num) is always true; did you mean (num ~= 0)?"; |
There was a problem hiding this comment.
I don't think any of these error messages are acceptable in their current form. We don't generally produce errors that use metavariables (like you're doing here with num, and like you're doing across all of the errors). More effective errors would refer to things the user actually wrote instead.
There was a problem hiding this comment.
Ok. I could not figure out the correct incantation to get the original source code at the expression start...end. I'll look again and try to do that
There was a problem hiding this comment.
oh. I also was very unsure how to both
- allocate the space for a dynamic string, and especially
- clean up the space some time later for a dynamic string
All the other lint rules used statically-allocated strings, which don't need cleanup, so I did too
| if if 1 then 2 else 3 then | ||
| elseif if 1 then 2 else 3 then | ||
| elseif if 0 then 5 else 4 then | ||
| a = true; b = true; c = false; d = true; e = false; f = true | ||
| if if b then c else d then | ||
| elseif if b then c else d then | ||
| elseif if a then f else e then |
There was a problem hiding this comment.
You shouldn't be changing this test either.
There was a problem hiding this comment.
I think this change would be unnecessary if number | true was treated as a valid condition, as requested in #2184 (comment)
There was a problem hiding this comment.
I reverted this test, and added --!nolint: a3c91d9. It fails MisleadingCondition 6 times, correctly in my opinion:
121205
| _ = (true and true) and true | ||
| _ = (true and true) or true | ||
| _ = (true and false) and (42 and false) | ||
| _ = (true and false) and (42 and false) -- also fails MisleadingCondition |
There was a problem hiding this comment.
This is an indication that misleading condition, as you have written it, does not work. We shouldn't be disabling the lint in our tests to mask bugs in the lint. Our users will actually experience the lints.
There was a problem hiding this comment.
I think this change would be unnecessary if number | true was treated as a valid condition, as requested in #2184 (comment)
There was a problem hiding this comment.
hmm. I think this should actually fail misleading condition. 42 is not a supertype of nil, boolean, true, or false.
and in this case is only really acting as an expression seperator, to unconditionally change the expression value. If this is idiomatic usage, maybe "the left side of and" should not be checked by MisleadingCondition
There was a problem hiding this comment.
in this specific expression (42 and false) will never actually be evaluated, because the left side of the top-level and ((true and false)) is always false. The linter isn't that smart though
…everted the unit test changes where this was the only issue
c774702 to
6509373
Compare
The linter now complains about condition expressions that are not a supertype of any of:
booleanniltruefalseThe linter considers a condition expression to be one of:
ifstatementifexpressionandoperatororoperatorassert()function callSee the unit test for error messages
Example: