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

feat(lint): add rule noUselessLengthCheck #4060

Open
wants to merge 34 commits into
base: main
Choose a base branch
from

Conversation

GunseiKPaseri
Copy link
Contributor

Summary

Implement noUselessLengthCheck(unicorn/no-useless-length-check)

closes #3941

Test Plan

  • Added tests and snapshots

@github-actions github-actions bot added A-CLI Area: CLI A-Project Area: project A-Linter Area: linter L-JavaScript Language: JavaScript and super languages A-Diagnostic Area: diagnostocis labels Sep 24, 2024
Copy link

codspeed-hq bot commented Sep 24, 2024

CodSpeed Performance Report

Merging #4060 will not alter performance

Comparing GunseiKPaseri:no-useless-length-check (b8e7493) with main (6c209ef)

Summary

✅ 99 untouched benchmarks

Copy link
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

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

Looks good. The merge conflict can be fixed by rerunning codegen.

@Conaclos
Copy link
Member

Conaclos commented Oct 24, 2024

@GunseiKPaseri Could you add a changelog entry and resolve the conflicts?

Comment on lines 335 to 339
ErrorType::UselessLengthCheckWithEvery => markup! {
"The empty check is useless as `Array#every()` returns `true` for an empty array."
},
ErrorType::UselessLengthCheckWithSome => markup! {
"The non-empty check is useless as `Array#some()` returns `false` for an empty array."
Copy link
Member

@unvalley unvalley Oct 25, 2024

Choose a reason for hiding this comment

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

We can use <Emphasis> tag here to Array#every(), true, Array#some(), false

Copy link
Member

@unvalley unvalley left a comment

Choose a reason for hiding this comment

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

looks good to me overall, left nits suggestions

@GunseiKPaseri
Copy link
Contributor Author

GunseiKPaseri commented Oct 25, 2024

It was difficult to determine whether it was an unsafe fix, but is it okay to leave it as an unsafe fix? Is there any way to make it a safe fix?

Copy link
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

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

I left an early review

Comment on lines 139 to 146
&& literal.to_string().trim() == "0"
&& (operator == T![===] || operator == T![<])
{
return Some(target);
}
// .length !== 0
if matches!(expect_error, ErrorType::UselessLengthCheckWithSome)
&& (literal.to_string().trim() == "0" && (operator == T![!==] || operator == T![>])
Copy link
Member

@ematipico ematipico Oct 25, 2024

Choose a reason for hiding this comment

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

Two things to change in all the code:

Copy link
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

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

Thank you for your contribution @GunseiKPaseri

This rule is quite complex, so we wouldn't want to urge the need to merge. There are a few concerns that should be addressed before:

  • recursions: we should remove it and find an algorithm that puts less memory pressure
  • complexity: I believe the code of this rule is too complex for what it should do, and it can be simplified. For example, I can't understand why we use a Vec<> for signals.
  • documentation: since this rule requires a lot of code, you should strive to provide as much documentation as possible. We are picky on this front

.map(|token| token.kind() == operator)
.or(Some(false));
}
(!JsParenthesizedExpression::can_cast(ancestor.kind())).then_some(false)
Copy link
Member

Choose a reason for hiding this comment

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

JsParenthesizedExpression can be tough to deal with; for example, I don't think we are able to catch these cases:

if (((((array.length === 0 || array.every(Boolean))))));

The type AnyJsExpression has a method called omit_parentheses, which returns the expression inside all the parenthesis. You should use that, and see if you can catch cases like the one I shared

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 function returns whether the parent is a JsLogicalExpression. When the parent is a JsParenthesizedExpression, we want to continue searching up to its parent as well, so this function is called to check for that. Therefore, even with the current implementation, such cases can be detected. (However, since I have learned about the convenient omit_parentheses function, I would like to rewrite some parts using it. Thank you.)

.unwrap_or(false)
}

/// Extract the expressions that perform length comparisons corresponding to the errors you want to check.
Copy link
Member

Choose a reason for hiding this comment

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

I find it difficult to understand the documentation; maybe you should provide an example. Perhaps because you call the second argument "error", you should see a better name, like the one I suggested.

Also, it would be nice to provide a code example in the docs.

Comment on lines 297 to 300
for err_type in [
ErrorType::UselessLengthCheckWithEvery,
ErrorType::UselessLengthCheckWithSome,
] {
Copy link
Member

Choose a reason for hiding this comment

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

This solution isn't maintainable, because if one day we add a new variant to ErrorType, we need to tweak business logic.

The thing is, the rule knows it always needs to check for Array.every() and Array.some(), so I don't understand why we need to pass err_type as a parameter.

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 naming might have been misleading. This actually refers to both &&||and Some・Every, and it is intended to specify searching only one of them in the recursion. Since most of the code is the same, I consolidated it.

};
let left = logical_exp.left().ok()?;
let left_replacer = (logical_exp.clone(), logical_exp.right().ok()?, left.range());
search_logical_exp(
Copy link
Member

Choose a reason for hiding this comment

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

This is a recursion, and when you want to avoid it. Usually, recursions are fine, but they aren't fit for working with trees, like an AST/CST.

You should refactor this code and try to use a simple loop, or maybe you could evaluate an iterator.

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 want to search through the entire logical expression. However, I couldn't think of a method other than recursion to extract A, B, C from A || B || C or A, B, C, D from (A || B) || (C || D). If it encounters something other than a logical expression or a different operator, the implementation is designed to terminate the process. Therefore, I believe the search will not become heavier than the logical expression being passed. If there is a good utility for this, I would like to use it.

Comment on lines 162 to 166
any_exp: &AnyJsExpression,
replacer: Option<Replacer>,
expect_error: &ErrorType,
comparing_zeros: &mut HashMap<String, Vec<Replacer>>,
array_tokens_used_api: &mut HashSet<String>,
Copy link
Member

@ematipico ematipico Oct 25, 2024

Choose a reason for hiding this comment

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

There are too many arguments here, I think. But it's fine. However, you should document them and explain what they are. What's any_exp, for example? It's difficult to understand what it is, especially in relation to the name to search_local_exp. Maybe you want to give a better name, like expression_to_search.

Also, replacer is a bit cryptic. Usually, you should try to give your parameters a name based on its business logic, not on its type.


impl Rule for NoUselessLengthCheck {
type Query = Ast<JsLogicalExpression>;
type State = (ErrorType, Replacer);
Copy link
Member

Choose a reason for hiding this comment

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

I believe it's best to use a struct for the state, so you can document every field. As it is, it's difficult to understand what is it.

@ematipico
Copy link
Member

ematipico commented Oct 27, 2024

Here's a generic comment based on the responses I had from the other comments.

I believe I understand more or less what you're trying to do; here's some advice based on my understanding of what you're trying to do.


When you submit a PR that has some heavy business logic, you should at least provide a technical explanation of how you solve a problem. This is important because it reduces the cognitive load of the reviewers. I understand that the solution is in your head, but the solution isn't in the head of the reviewers, so you need to explain your solution to the others. This happened before, so please try to follow it.


I still believe you don't need multiple signals, and still believe this rule can be simplified.

  1. You're querying a JsBinaryExpression, and that's why it gets very complex. I think we should query JsCallExpression, check if it's a known function (Array.some and Array.every). Once we do that, we know in advance which operator we need to check. && -> Array.every and || -> Array.some. This removes the for loop we use at the very beginning. Plus, it will remove the recursion and the vector of signals.

    if (array.length === 0 && array.some(Boolean)) {}
    //	                      ^^^^^^^^^^^^^^^^ query this instead

    Then, navigate the ancestors, maybe using the semantic model to check that the array used inside a branch of the logical expression is the same as the one used in the other branch.

  2. You assume that we need to search every logical expresssion branch and notify all violations, however I haven't managed to find a test where this is needed. If there is, please help me by providing a good description of the PR and maybe a good code example. Please don't use things like A || B || C because they don't help without an actual code example. This isn't what a rule needs to do. A rule, in the vast majority of cases, needs to find the first violation, and that's it. If you still believe we need to emit multiple signals from this one rule, then provide a code example that requires it.

  3. The recursion can be fixed with an iterator. An iterator is better because it's lazy, so it doesn't put too much pressure on memory. You can just create a type that holds to the any_expr, which gets updated every time you call next().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-CLI Area: CLI A-Diagnostic Area: diagnostocis A-Linter Area: linter A-Project Area: project L-JavaScript Language: JavaScript and super languages
Projects
None yet
Development

Successfully merging this pull request may close these issues.

📎 Implement noUselessLengthCheck - unicorn/no-useless-length-check
5 participants