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
Conversation
let rec visitParent (expr: IFSharpExpression) = | ||
match expr.Parent with | ||
| :? IFSharpExpression as parent -> visitParent parent | ||
| _ -> expr |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
ReSharper.FSharp/src/FSharp.Psi.Intentions/src/QuickFixes/ReplaceReturnTypeFix.fs
Outdated
Show resolved
Hide resolved
d2dbdda
to
d7d8519
Compare
There was a problem hiding this 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.
ReSharper.FSharp/src/FSharp.Psi.Intentions/src/QuickFixes/ReplaceReturnTypeFix.fs
Outdated
Show resolved
Hide resolved
8b1b57c
to
af9a74e
Compare
@auduchinok, I'm slowly getting the hang of this. |
There was a problem hiding this 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.
ReSharper.FSharp/src/FSharp.Psi/src/Util/FSharpExpressionUtil.cs
Outdated
Show resolved
Hide resolved
ReSharper.FSharp/src/FSharp.Psi/src/Util/FSharpExpressionUtil.cs
Outdated
Show resolved
Hide resolved
ReSharper.FSharp/src/FSharp.Psi/src/Util/FSharpExpressionUtil.cs
Outdated
Show resolved
Hide resolved
ReSharper.FSharp/src/FSharp.Psi/src/Util/FSharpExpressionUtil.cs
Outdated
Show resolved
Hide resolved
ReSharper.FSharp/src/FSharp.Psi/src/Util/FSharpExpressionUtil.cs
Outdated
Show resolved
Hide resolved
ReSharper.FSharp/test/data/features/quickFixes/replaceReturnType/Replace type with attribute.fs
Outdated
Show resolved
Hide resolved
....FSharp/test/data/features/quickFixes/replaceReturnType/Replace type in infix application.fs
Outdated
Show resolved
Hide resolved
| :? IFunctionTypeUsage as ftu -> | ||
ftu.SetReturnTypeUsage(typeUsage) |> ignore |
There was a problem hiding this comment.
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
ReSharper.FSharp/src/FSharp.Psi.Intentions/src/QuickFixes/ReplaceReturnTypeFix.fs
Show resolved
Hide resolved
There was a problem hiding this 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. 🙂
ReSharper.FSharp/src/FSharp.Psi/src/Util/FSharpExpressionUtil.cs
Outdated
Show resolved
Hide resolved
ReSharper.FSharp/src/FSharp.Psi/src/Util/FSharpExpressionUtil.cs
Outdated
Show resolved
Hide resolved
continue; | ||
} | ||
|
||
if (BinaryAppExprNavigator.GetByArgument(currentExpr) is { } binaryAppExpr) |
There was a problem hiding this comment.
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.
ReSharper.FSharp/test/src/FSharp.Intentions.Tests/src/QuickFixes/ReplaceReturnTypeTest.fs
Outdated
Show resolved
Hide resolved
ReSharper.FSharp/test/src/FSharp.Intentions.Tests/src/QuickFixes/ReplaceReturnTypeTest.fs
Outdated
Show resolved
Hide resolved
if (TupleExprNavigator.GetByExpression(currentExpr) is { } tupleExpr) | ||
{ | ||
currentExpr = tupleExpr; | ||
continue; | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
if (LambdaExprNavigator.GetByExpression(currentExpr) is { } lambdaExpr) | ||
{ | ||
currentExpr = lambdaExpr; | ||
continue; | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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 -> ""
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let x: int = 1 + ""
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
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.
Would it be an option to put some more logic into the |
Yes, that sounds fine. |
There was a problem hiding this 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!
let returnType = | ||
match binding.ReturnTypeInfo.ReturnType.IgnoreParentParens() with | ||
| :? IParenTypeUsage as ptu -> ptu.InnerTypeUsage | ||
| returnType -> returnType |
There was a problem hiding this comment.
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:
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.
| Regex typeMisMatchTupleLengths [expectedType; actualType] -> | ||
let expr = nodeSelectionProvider.GetExpressionInRange(fsFile, range, false, null) | ||
TypeMisMatchTuplesHaveDifferingLengthsError(expectedType, actualType, expr, error.Message) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let refPat = binding.HeadPattern.As<IReferencePat>() | ||
if isNull refPat then () else | ||
|
||
let symbolUse = refPat.GetFcsSymbolUse() | ||
if isNull symbolUse then () else |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
// TODO: change name | ||
public static IFSharpExpression GetOuterMostParentExpression(this IFSharpExpression expression) |
There was a problem hiding this comment.
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).
if (LambdaExprNavigator.GetByExpression(currentExpr) is { } lambdaExpr) | ||
{ | ||
currentExpr = lambdaExpr; | ||
continue; | ||
} |
There was a problem hiding this comment.
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.
if (TupleExprNavigator.GetByExpression(currentExpr) is { } tupleExpr) | ||
{ | ||
currentExpr = tupleExpr; | ||
continue; | ||
} |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 🙂
f79fc38
to
67a8e7e
Compare
@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. |
…le' does not match the type 'int' as well.
707abf0
to
88396a2
Compare
There was a problem hiding this 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!
My pleasure, thank you for all the guidance along the way! |
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.Please let me know what the best next steps could be here.
I imagine unit tests and edge cases.