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

Initial attempt at ReplaceReturnTypeFix. #367

Merged
merged 32 commits into from Jun 8, 2022

Conversation

nojaf
Copy link
Contributor

@nojaf nojaf commented May 4, 2022

Hello @auduchinok, this is a first attempt to add a quick fix action for the Type constraint mismatch error when a defined return type differs from the actual type of an expression.

ReplaceReturnType

Please let me know what the best next steps could be here.
I imagine unit tests and edge cases.

Comment on lines 14 to 17
let rec visitParent (expr: IFSharpExpression) =
match expr.Parent with
| :? IFSharpExpression as parent -> visitParent parent
| _ -> expr
Copy link
Member

Choose a reason for hiding this comment

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

This approach doesn't seem to be correct. Consider examples like:

let f x : int =
    match () with
    | _ -> ""
    | _ -> 1

    1
let f x : int =
    "" + 1

    1

In both cases it'll go up to the binding despite that the error is not at the "return" position for that binding.
It should only go up if the expression is at the position that a type annotation refers to. For example, if an expression is inside a sequentialExpr we should only go up if it's the last expression. Or if it's an expression in let Pat = Expr1 in Expr2 we should only go up for Expr1 and not for Expr2.

Copy link
Member

Choose a reason for hiding this comment

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

RecordCtorReference.GetFcsSymbol does something similar (though, I'm not sure if it's complete enough). It should probably be extracted into some common util method.

Copy link
Member

@auduchinok auduchinok left a comment

Choose a reason for hiding this comment

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

@nojaf Could you try to unify the tree traversal with the logic in RecordCtorReference.GetFcsSymbol?
https://github.com/JetBrains/resharper-fsharp/blob/net222/ReSharper.FSharp/src/FSharp.Psi/src/Util/FSharpExpressionUtil.cs would be a good place to put it to.

@nojaf
Copy link
Contributor Author

nojaf commented May 17, 2022

@auduchinok, I'm slowly getting the hang of this.
Let me know where you still see gaps.
I'm sure there are some more cases regarding ITypeUsage.

Copy link
Member

@auduchinok auduchinok left a comment

Choose a reason for hiding this comment

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

@nojaf The overall approach is much better now, I left a few comments here and there.

Comment on lines 73 to 74
| :? IFunctionTypeUsage as ftu ->
ftu.SetReturnTypeUsage(typeUsage) |> ignore
Copy link
Member

Choose a reason for hiding this comment

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

This probably needs to take more conditions into account. It seems for the examples below it'll only cover the first one:

let f1 _ : unit -> int =
    fun _ -> ""

let f2 _ : unit -> int =
    id

Copy link
Member

@auduchinok auduchinok left a comment

Choose a reason for hiding this comment

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

Here goes another round. 🙂

continue;
}

if (BinaryAppExprNavigator.GetByArgument(currentExpr) is { } binaryAppExpr)
Copy link
Member

Choose a reason for hiding this comment

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

We likely need to be more cautious here. Consider this:

let _: int = 1 + ""

I'd also check if the logic is the same for piping. For some reason I think there might be some quirks with binary operators and type conversions.

In general, it's usually better to not provide quick fix in cases were we aren't sure we work correctly than breaking the user code in an unexpected way. It's better to gradually enable cases later than to ship it as soon as possible and to deal with a situation where you can't easily fix all the corner cases and don't want to disable the feature too.

Comment on lines 77 to 81
if (TupleExprNavigator.GetByExpression(currentExpr) is { } tupleExpr)
{
currentExpr = tupleExpr;
continue;
}
Copy link
Member

Choose a reason for hiding this comment

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

Could you show an example where we need to go up from a tuple expression?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See test Tuple return type.fs

Copy link
Member

Choose a reason for hiding this comment

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

Thanks! Please check the samples below, they currently throw exceptions:

let x: int * string = 1

let x: int * string =
    if true then 1, "" else 1, 1

Copy link
Member

Choose a reason for hiding this comment

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

Another interesting case:

let x: int -> int = 1, 1

Copy link
Member

Choose a reason for hiding this comment

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

I think tuple (and probably lambda) expressions handling should be moved out of this method. Traversing them makes us go through parts of a type instead of going through inner expressions with the same overall type and may require keeping additional knowledge like "what item we came from", so it works in nested tuples and other complex cases. It also might be good to keep this method simple, so it could be easier to reuse it.

It's fine to move them to another method or to the quick fix code for now. Maybe we'll decide to move it back later.

Copy link
Member

Choose a reason for hiding this comment

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

We shouldn't go up here, but it's OK to go up in the quick fix (as you already seem to do).

Comment on lines 71 to 75
if (LambdaExprNavigator.GetByExpression(currentExpr) is { } lambdaExpr)
{
currentExpr = lambdaExpr;
continue;
}
Copy link
Member

Choose a reason for hiding this comment

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

Likewise, can we go up from any lambda body?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See test FunctionType 01.fs.

Copy link
Member

Choose a reason for hiding this comment

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

I see, thanks. We should carefully consider possible syntax and FCS types here:

let f: int -> double -> string = Unchecked.defaultof<_>
let g: int -> int -> string = f
let h: int * (int -> int) = 1, fun x -> ""

Copy link
Member

Choose a reason for hiding this comment

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

Both these cases don't work correctly with the current implementation. We should disable going up from lambda expressions for now.

continue;
}

if (BinaryAppExprNavigator.GetByArgument(currentExpr) is { } binaryAppExpr)
Copy link
Member

Choose a reason for hiding this comment

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

let x: int = 1 + ""

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, still on my to-do list.

Copy link
Member

Choose a reason for hiding this comment

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

Please remove this if for now 🙂

@@ -525,5 +525,18 @@
<Behavour overlapResolvePolicy="NONE"/>
<QuickFix>ReplaceReturnTypeFix</QuickFix>
</Error>


<Error staticGroup="FSharpErrors" name="TypeMisMatchTuplesHaveDifferingLengths" ID="FS0001: Type mismatch. Expecting">
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we probably should check this inside the quick fix's IsAvailable method instead of creating different error types. I'll need to think about it.

Parsing the error also seem a too ad-hoc and a bit dangerous, it would be much better to work with FCS's FSharpType instead. It's not always possible to get that view of the expected type, though.

@nojaf
Copy link
Contributor Author

nojaf commented May 19, 2022

Would it be an option to put some more logic into the override this.IsAvailable member to skip the more complicated scenarios?

@auduchinok
Copy link
Member

Yes, that sounds fine.

Copy link
Member

@auduchinok auduchinok 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 a few comments about cases that don't work as expected and that we should fix or disable. Otherwise, it seems to work great for many cases now!

Comment on lines 67 to 70
let returnType =
match binding.ReturnTypeInfo.ReturnType.IgnoreParentParens() with
| :? IParenTypeUsage as ptu -> ptu.InnerTypeUsage
| returnType -> returnType
Copy link
Member

Choose a reason for hiding this comment

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

Probably this is what you want here:

Suggested change
let returnType =
match binding.ReturnTypeInfo.ReturnType.IgnoreParentParens() with
| :? IParenTypeUsage as ptu -> ptu.InnerTypeUsage
| returnType -> returnType
let returnType = binding.ReturnTypeInfo.ReturnType.IgnoreInnerParens()

The current code takes the outermost type from the return node, then tries to go up through paren types as much as it could (but it's already at the top type usage, so it doesn't go anywhere) and then tries to go down through parens once. That doesn't look completely correct and might not work with nested paren types.

Comment on lines +198 to +200
| Regex typeMisMatchTupleLengths [expectedType; actualType] ->
let expr = nodeSelectionProvider.GetExpressionInRange(fsFile, range, false, null)
TypeMisMatchTuplesHaveDifferingLengthsError(expectedType, actualType, expr, error.Message)
Copy link
Member

Choose a reason for hiding this comment

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

There's something wrong with reported types when item count mismatch:

let x: int * int * int = 1, ""

Screenshot 2022-05-24 at 17 23 33

Let's disable this case until the error is fixed?

Comment on lines +57 to +74
let refPat = binding.HeadPattern.As<IReferencePat>()
if isNull refPat then () else

let symbolUse = refPat.GetFcsSymbolUse()
if isNull symbolUse then () else
Copy link
Member

Choose a reason for hiding this comment

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

I seems that we don't use the symbol right now (though, we probably should it future), so perhaps we don't need to require a specific kind of pattern for now.

if isNotNull binding.ReturnTypeInfo
&& isNotNull binding.ReturnTypeInfo.ReturnType then
let factory = binding.CreateElementFactory()
let typeUsage = factory.CreateTypeUsage(replacementTypeName)
Copy link
Member

Choose a reason for hiding this comment

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

I consider this creation of a type usage from a text parsed in an error text to be a bit dangerous but we can try to use it for the time being.

match mostOuterParentExpr with
| :? ITupleExpr ->
ModificationUtil.ReplaceChild(ftu, typeUsage) |> ignore
| _ -> ftu.SetReturnTypeUsage(typeUsage) |> ignore
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't handle this case correctly:

let x = 1, 2
let f: int -> int = x 

Comment on lines 40 to 41
// TODO: change name
public static IFSharpExpression GetOuterMostParentExpression(this IFSharpExpression expression)
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we use something like GetOutermostParentExpressionFromItsReturn for now (though, I find it too long for to be good).

Comment on lines 71 to 75
if (LambdaExprNavigator.GetByExpression(currentExpr) is { } lambdaExpr)
{
currentExpr = lambdaExpr;
continue;
}
Copy link
Member

Choose a reason for hiding this comment

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

Both these cases don't work correctly with the current implementation. We should disable going up from lambda expressions for now.

Comment on lines 77 to 81
if (TupleExprNavigator.GetByExpression(currentExpr) is { } tupleExpr)
{
currentExpr = tupleExpr;
continue;
}
Copy link
Member

Choose a reason for hiding this comment

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

We shouldn't go up here, but it's OK to go up in the quick fix (as you already seem to do).

continue;
}

if (BinaryAppExprNavigator.GetByArgument(currentExpr) is { } binaryAppExpr)
Copy link
Member

Choose a reason for hiding this comment

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

Please remove this if for now 🙂

@nojaf
Copy link
Contributor Author

nojaf commented Jun 1, 2022

@auduchinok I've added some additional checks inside the quick fix to not enable it when the return type or expression cannot be processed in a reliable manner.
Let me know if this is what you had in mind and what to do with the corresponding tests.

Copy link
Member

@auduchinok auduchinok left a comment

Choose a reason for hiding this comment

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

@nojaf Thanks for this, this is a very nice quick fix to have!

@nojaf nojaf marked this pull request as ready for review June 8, 2022 12:35
@nojaf
Copy link
Contributor Author

nojaf commented Jun 8, 2022

My pleasure, thank you for all the guidance along the way!

@auduchinok auduchinok merged commit 5a433da into JetBrains:net222 Jun 8, 2022
auduchinok pushed a commit that referenced this pull request Jun 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants