Skip to content
This repository has been archived by the owner on Aug 31, 2023. It is now read-only.

useOptionalChain #2748

Closed
Tracked by #2642
ematipico opened this issue Jun 20, 2022 · 13 comments · Fixed by #3085
Closed
Tracked by #2642

useOptionalChain #2748

ematipico opened this issue Jun 20, 2022 · 13 comments · Fixed by #3085
Assignees
Labels
A-Linter Area: linter
Milestone

Comments

@ematipico
Copy link
Contributor

No description provided.

@ematipico ematipico changed the title useOptionalChainingUse optional chaining to manual checksExample fix:foo && foo.bar; -> foo?.bar; useOptionalChainingUse Jun 20, 2022
@ematipico ematipico added the A-Linter Area: linter label Jun 20, 2022
@ematipico ematipico changed the title useOptionalChainingUse useOptionalChaining Jun 21, 2022
@denbezrukov
Copy link
Contributor

Would you mind assigning me this task?🙏🏽

@ematipico ematipico assigned ematipico and denbezrukov and unassigned ematipico Jun 27, 2022
@ematipico
Copy link
Contributor Author

@denbezrukov Done!

@ematipico ematipico changed the title useOptionalChaining useOptionalChain Jun 27, 2022
@denbezrukov
Copy link
Contributor

There is a conditional which is used to check isLeftSideLowerPrecedence.

We need it for the case then it's necessary to add parentheses around left part of LogicalExpression after fixing the rule:

(await a || {}).b -> (await a)?.b

I found OperatorPrecedence enum.

pub(crate) enum OperatorPrecedence {
Comma = 0,
Yield = 1,
Assignment = 2,
Conditional = 3,
Coalesce = 4,
LogicalOr = 5,
LogicalAnd = 6,
BitwiseOr = 7,
BitwiseXor = 8,
BitwiseAnd = 9,
Equality = 10,
Relational = 11,
Shift = 12,
Additive = 13,
Multiplicative = 14,
Exponential = 15,
Unary = 16,
Update = 17,
LeftHandSide = 18,
Member = 19,
Primary = 20,
}

I was wondering if I could make it public and port getOperatorPrecedence function from typescript-eslint:

What do you think?🙏🏽

@ematipico
Copy link
Contributor Author

I was wondering if I could make it public and port getOperatorPrecedence function from typescript-eslint:

@denbzrukov yes, it makes total sense! Ideally, the precedence should be built using Rust's trait system (Ord and PartialOrd).

Inside the formatter we created an enum called FormatPrecedence, which implements methods where the precedence is created based on the context. For your solution, it seems we want to create a new method for the optional chain operator.

What we could do, is to move FormatPrecedence inside the crate rome_js_syntax, in a file called utils.rs, and rename it NodePrecedence. And then make it available to the formatter AND the analyzer. At that point, we can create a new method called with_precedence_for_optional_operator. That should work?

Let me know what you think.

@denbezrukov
Copy link
Contributor

@ematipico
Could you please explain how FormatPrecedence::with_precedence_for_parenthesis works now? Does It return FormatPrecedence::High for expressions which have more precedence over parentheses? And FormatPrecedence::Low for expressions which have less precedence over parentheses?

I guess that with_precedence_for_optional_operator should work. I see only one drawback that next time when we need to implement, for example, with_precedence_for_async_await we'll again struggle with Operator Precedence Table.

It seems that getOperatorPrecedence function from typescript-eslint can handle all cases. Because it returns OperatorPrecedence which already derives Ord and PartialOrd.

#[derive(Debug, Eq, Ord, PartialOrd, PartialEq, Copy, Clone)]
pub(crate) enum OperatorPrecedence {

We can implement this function and use it in with_precedence_for_optional_operator and with_precedence_for_parenthesis.

E.g. with_precedence_for_optional_operator:

  1. pass JsSyntaxNode as argument.
  2. get_operator_precedence returns OperatorPrecedence for the node.
  3. compare OperatorPrecedence with OperatorPrecedence::LeftHandSide. Because optional chain has that precedence.

@ematipico
Copy link
Contributor Author

Could you please explain how FormatPrecedence::with_precedence_for_parenthesis works now? Does It return FormatPrecedence::High for expressions which have more precedence over parentheses? And FormatPrecedence::Low for expressions which have less precedence over parentheses?

Yes, but there's also the fact that this precedence has a context.

The idea is that you have two nodes, the current node and its parent, then you pass kind of the nodes to the function. The function will emit a precedence priority. Then you use these two priorities for doing different things. If the parent has more priority compared to the child, we can't remove the parenthesis.

An example:

a[(a + b) * 3]

Here the the parent of the JsParenthesizedExpression is a JsBinaryExpression. We assign a high priority to JsBinaryExpression because we can't remove the parenthesis: doing so will cause a different in the logic, breaking the code.

Instead:

const a = (3 + 4);

Here it's safe to remove the parenthesis. The parent here is a JsInitializerClause, where we assign Precedence::None, which is the lowest. Then, the parenthesis are removed.

@denbezrukov
Copy link
Contributor

denbezrukov commented Jul 12, 2022

@ematipico
Thank you for your explanation!
I have concerns that there are cases then we can omit parenthesis for your first example but current implementation keeps them.
E.g.
a[(a * b) * 3] -> a[a * b * 3]
Prettier actually does it. Playground

What do you think about:
This function is port of typescript-eslint function

#[derive(Debug, Eq, Ord, PartialOrd, PartialEq, Copy, Clone)]
pub enum OperatorPrecedence {
    Comma = 0,
    Yield = 1,
    Assignment = 2,
    Conditional = 3,
    Coalesce = 4,
    LogicalOr = 5,
    LogicalAnd = 6,
    BitwiseOr = 7,
    BitwiseXor = 8,
    BitwiseAnd = 9,
    Equality = 10,
    Relational = 11,
    Shift = 12,
    Additive = 13,
    Multiplicative = 14,
    Exponential = 15,
    Unary = 16,
    Update = 17,
    LeftHandSide = 18,
    Member = 19,
    Primary = 20,
    Group = 21,
}

pub fn expression_precedence(expression: &JsAnyExpression) -> SyntaxResult<OperatorPrecedence> {
    let precedence = match expression {
        JsAnyExpression::JsSequenceExpression(_) => OperatorPrecedence::Comma,
        JsAnyExpression::JsYieldExpression(_) => OperatorPrecedence::Yield,
        JsAnyExpression::JsConditionalExpression(_) => OperatorPrecedence::Conditional,
        JsAnyExpression::JsAssignmentExpression(_) => OperatorPrecedence::Assignment,
        JsAnyExpression::JsInExpression(_)
        | JsAnyExpression::JsInstanceofExpression(_)
        | JsAnyExpression::TsAsExpression(_) => OperatorPrecedence::Relational,
        JsAnyExpression::JsLogicalExpression(expression) => match expression.operator()? {
            JsLogicalOperator::LogicalAnd => OperatorPrecedence::LogicalAnd,
            JsLogicalOperator::LogicalOr => OperatorPrecedence::LogicalOr,
            JsLogicalOperator::NullishCoalescing => OperatorPrecedence::Coalesce,
        },
        JsAnyExpression::JsBinaryExpression(expression) => match expression.operator()? {
            JsBinaryOperator::LessThan
            | JsBinaryOperator::GreaterThan
            | JsBinaryOperator::GreaterThanOrEqual
            | JsBinaryOperator::LessThanOrEqual => OperatorPrecedence::Relational,
            JsBinaryOperator::Equality
            | JsBinaryOperator::StrictEquality
            | JsBinaryOperator::Inequality
            | JsBinaryOperator::StrictInequality => OperatorPrecedence::Equality,
            JsBinaryOperator::Minus | JsBinaryOperator::Plus => OperatorPrecedence::Additive,
            JsBinaryOperator::Remainder | JsBinaryOperator::Divide | JsBinaryOperator::Times => {
                OperatorPrecedence::Multiplicative
            }
            JsBinaryOperator::Exponent => OperatorPrecedence::Exponential,
            JsBinaryOperator::UnsignedRightShift
            | JsBinaryOperator::RightShift
            | JsBinaryOperator::LeftShift => OperatorPrecedence::Shift,
            JsBinaryOperator::BitwiseAnd => OperatorPrecedence::BitwiseAnd,
            JsBinaryOperator::BitwiseOr => OperatorPrecedence::BitwiseOr,
            JsBinaryOperator::BitwiseXor => OperatorPrecedence::BitwiseXor,
        },
        JsAnyExpression::TsTypeAssertionExpression(_)
        | JsAnyExpression::TsNonNullAssertionExpression(_)
        | JsAnyExpression::JsUnaryExpression(_)
        | JsAnyExpression::JsAwaitExpression(_) => OperatorPrecedence::Unary,
        JsAnyExpression::JsPostUpdateExpression(_) | JsAnyExpression::JsPreUpdateExpression(_) => {
            OperatorPrecedence::Update
        }
        JsAnyExpression::JsCallExpression(_)
        | JsAnyExpression::JsNewExpression(_)
        | JsAnyExpression::JsImportCallExpression(_)
        | JsAnyExpression::JsSuperExpression(_) => OperatorPrecedence::LeftHandSide,

        JsAnyExpression::JsComputedMemberExpression(_)
        | JsAnyExpression::JsStaticMemberExpression(_)
        | JsAnyExpression::ImportMeta(_)
        | JsAnyExpression::NewTarget(_) => OperatorPrecedence::Member,

        JsAnyExpression::JsThisExpression(_)
        | JsAnyExpression::JsAnyLiteralExpression(_)
        | JsAnyExpression::JsArrayExpression(_)
        | JsAnyExpression::JsArrowFunctionExpression(_)
        | JsAnyExpression::JsClassExpression(_)
        | JsAnyExpression::JsFunctionExpression(_)
        | JsAnyExpression::JsIdentifierExpression(_)
        | JsAnyExpression::JsObjectExpression(_)
        | JsAnyExpression::JsxTagExpression(_) => OperatorPrecedence::Primary,

        JsAnyExpression::JsTemplate(template) => {
            if template.tag().is_some() {
                OperatorPrecedence::Member
            } else {
                OperatorPrecedence::Primary
            }
        }

        JsAnyExpression::JsUnknownExpression(_) => OperatorPrecedence::lowest(),
        JsAnyExpression::JsParenthesizedExpression(_) => OperatorPrecedence::highest(),
    };

    Ok(precedence)
}

We can use the function for JsParenthesizedExpression context too:

  1. Get a precedence for JsParenthesizedExpression expression.
  2. Try to cast parent of JsParenthesizedExpression to JsAnyExpression and get precedence.
  3. Compare precedence and decide to keep or remove parenthesis.

@ematipico
Copy link
Contributor Author

@denbezrukov I think there's some misunderstanding, or I don't really understand some point. The parenthesis logic that we have in the formatter doesn't have any correlation with what we want to do inside the linter, I was just suggesting an alternative solution of how we created a precedence ordering based on the context.

If I understood correctly, you wanted to use what we already have in the formatter for this rule? I strongly suggest to not use it inside the linter. Let's just use the port from eslint and create the parenthesis expression using the mutation API.

Of course we can use the OperatorPrecedence, which already has baked in the correct precedence.

@denbezrukov
Copy link
Contributor

@ematipico Sorry😅
I hope that we can use the function which returns precedence in formatter and lint crate. Because it can handle cases which are inconsistent with prettier now. E.g. a[(1 * 4) * 1]

Maybe it's better to port the function in another PR?

@ematipico
Copy link
Contributor Author

Maybe it's better to port the function in another PR?

Yes, I suppose so. We can start by implementing the rule by itself, without "dependencies". We can check afterwards what we can do with the formatter. The formatter has still an open issue around parenthesis, so it's not fixed/ready yet.

@ematipico ematipico added this to the 0.9.0 milestone Aug 3, 2022
@denbezrukov
Copy link
Contributor

denbezrukov commented Aug 3, 2022

Hi,

I've handle cases with nullish coalescing and logical or:

https://github.com/denbezrukov/tools/blob/feature/use-optional-chaining/crates/rome_js_analyze/tests/specs/js/useOptionalChain/nullishAndLogicalOr.ts.snap

Now I'm working on:
foo && foo.bar && foo.bar.zoo

@denbezrukov
Copy link
Contributor

denbezrukov commented Aug 16, 2022

Hi,

I've handle cases with logical and.

https://github.com/denbezrukov/tools/blob/feature/use-optional-chaining/crates/rome_js_analyze/tests/specs/js/useOptionalChain/logicalAndCases.js.snap

I'm going to double check test cases, clean code and make MR🥳

@IWANABETHATGUY
Copy link
Contributor

Hi,

I've handle cases with logical and.

https://github.com/denbezrukov/tools/blob/feature/use-optional-chaining/crates/rome_js_analyze/tests/specs/js/useOptionalChain/logicalAndCases.js.snap.new

I'm going to double check test cases, clean code and make MR🥳

Glad to see that!

denbezrukov pushed a commit to denbezrukov/tools that referenced this issue Aug 19, 2022
denbezrukov pushed a commit to denbezrukov/tools that referenced this issue Aug 19, 2022
denbezrukov pushed a commit to denbezrukov/tools that referenced this issue Aug 19, 2022
denbezrukov pushed a commit to denbezrukov/tools that referenced this issue Aug 19, 2022
denbezrukov pushed a commit to denbezrukov/tools that referenced this issue Aug 23, 2022
fix docs and rename new constructor to from_expression
denbezrukov pushed a commit to denbezrukov/tools that referenced this issue Aug 24, 2022
denbezrukov pushed a commit to denbezrukov/tools that referenced this issue Aug 25, 2022
add logical and cases with comments
denbezrukov pushed a commit to denbezrukov/tools that referenced this issue Aug 26, 2022
move is_empty inside collect node fn
denbezrukov pushed a commit to denbezrukov/tools that referenced this issue Aug 26, 2022
suppress suggestion if we have syntax error
denbezrukov pushed a commit to denbezrukov/tools that referenced this issue Sep 2, 2022
denbezrukov pushed a commit to denbezrukov/tools that referenced this issue Sep 2, 2022
@ematipico ematipico modified the milestones: 0.9.0, 0.10.0 Sep 6, 2022
denbezrukov added a commit to denbezrukov/tools that referenced this issue Sep 12, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
A-Linter Area: linter
Projects
Status: Done
Development

Successfully merging a pull request may close this issue.

3 participants