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

Fix issues with multiline generic type parameters #2852

Merged
merged 17 commits into from
Jan 9, 2024

Conversation

josh-degraw
Copy link
Contributor

Closes #2706, from Amplifying F# session

@josh-degraw josh-degraw self-assigned this Apr 19, 2023
@nojaf
Copy link
Contributor

nojaf commented Apr 19, 2023

Thanks for following up on this. I need to play with this some more and we may want to have the conversation over at the style guide as well.
Once all is settled, we should target 6.1 for this.

Copy link
Contributor

@nojaf nojaf left a comment

Choose a reason for hiding this comment

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

Hi @josh-degraw, many thanks again for taking the lead in this during https://amplifying-fsharp.github.io/sessions/2023/04/14/

I think the change is sensible but this does change the style and so I would like to see this covered by https://learn.microsoft.com/en-us/dotnet/fsharp/style-guide/formatting#formatting-explicit-generic-type-arguments-and-constraints

And also to have a link for people who will open an issue claiming the additional space is a bug. (Trust me, it will be reported 🙈).

Could you open an issue in https://github.com/fsharp/fslang-design/issues so we can further discuss these implications?

And please also retarget this branch to v6.1.

Thanks!

(Position -> option<string>) *
FSharp.Compiler.Syntax.ParsedInput> =
: cmap<
DeclName,
Copy link
Contributor

Choose a reason for hiding this comment

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

This indent no longer is a multitude of the indent_size, we should look into why this is happening. Gut feeling this is related due to

+> atCurrentColumnIndent (genType rt.Type)
and not the changes here.
Nonetheless, we should investigate this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you still consider this a problem or are we okay with this indent level?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, I think I can live with this.
This sample screams "use type alias" anyway.

@@ -3037,7 +3045,8 @@ let genType (t: Type) =
+> optSingle genIdentListNodeWithDot node.PostIdentifier
+> genSingleTextNode node.LessThen
+> addExtraSpace
+> col sepComma node.Arguments genType
+> leadingExpressionIsMultiline (colGenericTypeParameters node.Arguments) (fun isMultiline ->
Copy link
Contributor

Choose a reason for hiding this comment

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

We should add some tests where the < and > contain trivia.
Cases like:

type X =
    Teq< //
        int
     //
     >

won't be covered right now.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also please add a code comment on why we are doing this in the first place.

src/Fantomas.Core/CodePrinter.fs Outdated Show resolved Hide resolved
src/Fantomas.Core/CodePrinter.fs Outdated Show resolved Hide resolved
"""

[<Test>]
let `` Aligned bracket style in anonymous record is respected for multiple types, #2706`` () =
Copy link
Contributor

Choose a reason for hiding this comment

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

We should have the same test for the other styles.

src/Fantomas.Core.Tests/TypeAnnotationTests.fs Outdated Show resolved Hide resolved
@josh-degraw josh-degraw changed the base branch from main to v6.1 May 2, 2023 16:07
@josh-degraw josh-degraw force-pushed the aligned-anonymous-arg branch from 4d8d328 to fe504e3 Compare May 2, 2023 16:08
@nojaf nojaf force-pushed the v6.1 branch 3 times, most recently from 9329316 to 87d2a45 Compare June 20, 2023 11:23
@josh-degraw josh-degraw changed the base branch from v6.1 to main November 11, 2023 16:48
@josh-degraw josh-degraw force-pushed the aligned-anonymous-arg branch from eeb8f27 to 9056cf1 Compare November 11, 2023 18:17
@josh-degraw
Copy link
Contributor Author

Sorry it's taken me so long to finally get back around to trying to finish this up haha.

I'm not sure if something has changed on the parser level since I originally worked on this, but a test that previously succeeded is now failing and I'm having trouble figuring out the best way to address it.

I extracted the relevant bits into its own test but essentially this kind of expression (which is how my PR originally formatted this in the updated tests here):

let bv =
    unbox<
        Foo<
            'innerContextLongLongLong,
            'bb -> 'b
            >
    >
        bf

Is no longer an idempotent operation. It seems like if I increase the indent of the final > when it's output in genExpr under Expr.TypeApp so it looks like this:

let bv =
    unbox<
        Foo<
            'innerContextLongLongLong,
            'bb -> 'b
            >
     >
        bf

it becomes idempotent again, but since this worked before I rebased this PR I'm not sure if this is a bug in the AST parsing layer or just in my code here.

@nojaf
Copy link
Contributor

nojaf commented Nov 11, 2023

@josh-degraw thanks for picking this up again! Much appreciated!

I'm afraid this one kinda sucks:

image

The unbox<...> bf no longer is a function application after formatting.

So, instead of a b it becomes a ; b conceptually.

I'm not sure what we need to do to keep this as a function application.

@josh-degraw
Copy link
Contributor Author

@josh-degraw thanks for picking this up again! Much appreciated!

I'm afraid this one kinda sucks:

image

The unbox<...> bf no longer is a function application after formatting.

So, instead of a b it becomes a ; b conceptually.

I'm not sure what we need to do to keep this as a function application.

Yeah like I mentioned it seems like just increasing the indent for the closing angle bracket at least fixes the parser issue, but that's still valid code with the additional indent right? If not I guess we'd have to carve out an exemption for this scenario or wrap in parentheses or something

@nojaf
Copy link
Contributor

nojaf commented Nov 11, 2023

Ok, consider this example:

foo<
    'bar
        -> int
>
    barry

This looks like it is a SynExpr.App(foo<...>, barry) but actually it is already is two top level SynModuleDecl.Expr.

Adding a space before > makes it into a SynModuleDecl.Expr (SynExpr.App ...).
So, I think our hand is forced here to just move it one space further.

@nojaf
Copy link
Contributor

nojaf commented Nov 11, 2023

Added a workaround of let startColumn = ctx.Column + 1

@josh-degraw
Copy link
Contributor Author

I tried that but that messes with a bunch of other tests, wouldn't we want to only do this in this specific situation?

@nojaf
Copy link
Contributor

nojaf commented Nov 11, 2023

Hmm, I think the safest thing to do would be to have it all the time.
We essentially do the same thing with SynType I think.

Digging into the exact case of SynExpr.App(SynExpr.TypeApp, ...) might be quite challenging and not really worth it.

@josh-degraw
Copy link
Contributor Author

Fair enough. Looks like most of the tests that fail with that change were added in this PR anyway haha. I'll update the tests to reflect this

@nojaf
Copy link
Contributor

nojaf commented Nov 11, 2023

@josh-degraw took a quick peek if it is possible. It seems less intrusive at first glance to remove the space.

This of course can still go wrong in things like Expr.AppLongIdentAndSingleParenArg, Expr.AppSingleParenArg, Expr.AppWithLambda, Expr.NestedIndexWithoutDot, EndsWithDualListApp, EndsWithSingleListApp, ...

We should check what happens in those scenarios. If we need to update all these individually I don't think it is worth it.

@josh-degraw
Copy link
Contributor Author

This of course can still go wrong in things like Expr.AppLongIdentAndSingleParenArg, Expr.AppSingleParenArg, Expr.AppWithLambda, Expr.NestedIndexWithoutDot, EndsWithDualListApp, EndsWithSingleListApp, ...

Can you maybe help me know in what situations these nodes would apply here? I'm not quite sure exactly how to test for these just going off of the node names themselves (not quite there yet haha..)

@nojaf
Copy link
Contributor

nojaf commented Nov 20, 2023

Can you maybe help me know in what situations these nodes would apply here?

Sure thing.

SynExpr.App has historically been a tricky one to format. Because it really depends on what expressions it has before we decide how we can format it.

Some examples:

// ExprAppSingleParenArgNode
Foo(
    //
    bar
)

// ExprAppWithLambdaNode
bar(fun x -> x)

// ExprAppNode
a [ Href "some-link"] [
    str "content"
]

// ExprInfixAppNode 
x - y

// ExprSameInfixAppsNode
x * y * z

image

All of these expressions are SynExpr.App, but we transform them into different Oak shapes in ASTTransformer.

This is sometimes very convenient but at the same has its drawbacks.

A clear pro: sometimes you really need to deal with these shapes very carefully and it helps to have separation.

A clear con: the is no single entry genApp that deals with all types of applications. It can be a bit scattered.

A lot of what we have today is somewhat historical as well. Infix applications are a good example of that. Almost all of the code pre-dates the style guide and it is just what we have today.

We split the applications into different Oak nodes in:

| SynExpr.App(_,
false,
SynExpr.LongIdent(_,
SynLongIdent([ ident ], [], [ Some(IdentTrivia.OriginalNotation operatorName) ]),
_,
_),
e2,
_) when
PrettyNaming.IsValidPrefixOperatorDefinitionName(
PrettyNaming.ConvertValLogicalNameToDisplayNameCore ident.idText
)
->
ExprPrefixAppNode(stn operatorName ident.idRange, mkExpr creationAide e2, exprRange)
|> Expr.PrefixApp
| NewlineInfixApps(head, xs)
| MultipleConsInfixApps(head, xs)
| SameInfixApps(head, xs) ->
let rest = xs |> List.map (fun (operator, e) -> operator, mkExpr creationAide e)
ExprSameInfixAppsNode(mkExpr creationAide head, rest, exprRange)
|> Expr.SameInfixApps
| InfixApp(e1, operator, e2) ->
ExprInfixAppNode(mkExpr creationAide e1, operator, mkExpr creationAide e2, exprRange)
|> Expr.InfixApp
| IndexWithoutDot(identifierExpr, indexExpr) ->
ExprIndexWithoutDotNode(mkExpr creationAide identifierExpr, mkExpr creationAide indexExpr, exprRange)
|> Expr.IndexWithoutDot
| ChainExpr links ->
let chainLinks =
links
|> List.map (function
| LinkExpr.Identifier identifierExpr -> mkExpr creationAide identifierExpr |> ChainLink.Identifier
| LinkExpr.Dot mDot -> stn "." mDot |> ChainLink.Dot
| LinkExpr.Expr e -> mkExpr creationAide e |> ChainLink.Expr
| LinkExpr.AppUnit(f, mUnit) ->
LinkSingleAppUnit(mkExpr creationAide f, mkUnit mUnit, unionRanges f.Range mUnit)
|> ChainLink.AppUnit
| LinkExpr.AppParen(f, lpr, e, rpr, pr) ->
LinkSingleAppParen(
mkExpr creationAide f,
mkParenExpr creationAide lpr e rpr pr,
unionRanges f.Range pr
)
|> ChainLink.AppParen
| LinkExpr.IndexExpr e -> mkExpr creationAide e |> ChainLink.IndexExpr
| link -> failwithf "cannot map %A" link)
ExprChain(chainLinks, exprRange) |> Expr.Chain
| AppSingleParenArg(SynExpr.LongIdent(longDotId = longDotId), px) ->
ExprAppLongIdentAndSingleParenArgNode(mkSynLongIdent longDotId, mkExpr creationAide px, exprRange)
|> Expr.AppLongIdentAndSingleParenArg
| AppSingleParenArg(e, px) ->
ExprAppSingleParenArgNode(mkExpr creationAide e, mkExpr creationAide px, exprRange)
|> Expr.AppSingleParenArg
| SynExpr.App(funcExpr = App(fe, args); argExpr = ParenLambda(lpr, pats, mArrow, body, mLambda, rpr)) ->
let lambdaNode = mkLambda creationAide pats mArrow body mLambda
ExprAppWithLambdaNode(
mkExpr creationAide fe,
List.map (mkExpr creationAide) args,
stn "(" lpr,
Choice1Of2 lambdaNode,
stn ")" rpr,
exprRange
)
|> Expr.AppWithLambda
| SynExpr.App(funcExpr = fe; argExpr = ParenLambda(lpr, pats, mArrow, body, mLambda, rpr)) ->
let lambdaNode = mkLambda creationAide pats mArrow body mLambda
ExprAppWithLambdaNode(mkExpr creationAide fe, [], stn "(" lpr, Choice1Of2 lambdaNode, stn ")" rpr, exprRange)
|> Expr.AppWithLambda
| SynExpr.App(funcExpr = App(fe, args); argExpr = ParenMatchLambda(lpr, mFunction, clauses, mMatchLambda, rpr)) ->
let lambdaNode = mkMatchLambda creationAide mFunction clauses mMatchLambda
ExprAppWithLambdaNode(
mkExpr creationAide fe,
List.map (mkExpr creationAide) args,
stn "(" lpr,
Choice2Of2 lambdaNode,
stn ")" rpr,
exprRange
)
|> Expr.AppWithLambda
| SynExpr.App(funcExpr = fe; argExpr = ParenMatchLambda(lpr, mFunction, clauses, mMatchLambda, rpr)) ->
let lambdaNode = mkMatchLambda creationAide mFunction clauses mMatchLambda
ExprAppWithLambdaNode(mkExpr creationAide fe, [], stn "(" lpr, Choice2Of2 lambdaNode, stn ")" rpr, exprRange)
|> Expr.AppWithLambda
| SynExpr.App(ExprAtomicFlag.NonAtomic,
false,
SynExpr.App(ExprAtomicFlag.Atomic,
false,
identifierExpr,
SynExpr.ArrayOrListComputed(false, indexExpr, _),
_),
argExpr,
_) ->
ExprNestedIndexWithoutDotNode(
mkExpr creationAide identifierExpr,
mkExpr creationAide indexExpr,
mkExpr creationAide argExpr,
exprRange
)
|> Expr.NestedIndexWithoutDot
| SynExpr.App(ExprAtomicFlag.NonAtomic,
false,
SynExpr.App(ExprAtomicFlag.NonAtomic,
false,
identifierExpr,
(SynExpr.ArrayOrListComputed(isArray = false; expr = indexExpr) as indexArgExpr),
_),
argExpr,
_) when (RangeHelpers.isAdjacentTo identifierExpr.Range indexArgExpr.Range) ->
ExprNestedIndexWithoutDotNode(
mkExpr creationAide identifierExpr,
mkExpr creationAide indexExpr,
mkExpr creationAide argExpr,
exprRange
)
|> Expr.NestedIndexWithoutDot
| App(fe, args) ->
ExprAppNode(mkExpr creationAide fe, List.map (mkExpr creationAide) args, exprRange)
|> Expr.App

So, to answer your question, that would be the list you need to check.

On a side note, some of these nodes probably shouldn't exist because they are too focused on what we do with them inside CodePrinter. For this reason, you also have EndsWithDualListApp as an active pattern instead of a separate node. It is hard to really draw the line there.

Anyway, you should check what happens if any of these nodes can be used with ExprTypeAppNode and what happens if they do with a specific case.

I hope this helps a little.

@@ -266,25 +262,27 @@ let private asJson (arm: IArmResource) =
>
"""

let forcedLongDefnConfig =
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not a fan of this configuration.
The only thing you are trying to achieve is to spread things over multiple lines, using MaxLineLength = 30. All the other settings are irrelevant and misleading to readers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, fully agree, this latest commit was mainly a WIP

|}>
"""
config
let ``Multiline type argument with AppLongIdentAndSingleParenArg`` () =
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: lowercase unit test names.

[<Test>]
let ``type application including nested multiline function type`` () =
forcedLongDefnConfig
{ config with
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick but this doesn't comply with the pull request ground rules.

I don't really see a compelling reason to deviate from the standard unit template.
All our tests are the same, I'm not a fan of mixing that up in this PR.

@nojaf
Copy link
Contributor

nojaf commented Nov 27, 2023

Be aware that in its current form this is not working out for every SynExpr.App combination.
I did a quick test with:

[<Test>]
let ``dual list application`` () =
    formatSourceString
        """
unbox<Foo<'innerContextLongLongLong,'bb -> 'b>> [
    ClassName "meh"
] [
    str "foo"
]
"""
        { config with
            MaxLineLength = 12
            MultilineBracketStyle = Stroustrup }
    |> prepend newline
    |> should
        equal
        """
unbox<
    Foo<
        'innerContextLongLongLong,
        'bb
            -> 'b
     >
> [
    ClassName 
        "meh"
] [
    str 
        "foo"
]
"""

@josh-degraw
Copy link
Contributor Author

Be aware that in its current form this is not working out for every SynExpr.App combination.

Yeah I know, I'm still having a little trouble pinpointing the spot to fix some of these

@nojaf
Copy link
Contributor

nojaf commented Nov 27, 2023

Don't hesitate to reach out if you wanna pair on this sometime!

@josh-degraw
Copy link
Contributor Author

Don't hesitate to reach out if you wanna pair on this sometime!

That might be helpful. I felt like a few I found right where I thought I needed to make changes to fix some of these issues but it didn't work haha

@josh-degraw josh-degraw force-pushed the aligned-anonymous-arg branch from 66fdc2d to 0209297 Compare January 2, 2024 16:57
Copy link
Contributor

@nojaf nojaf left a comment

Choose a reason for hiding this comment

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

Alright, I think this is worth merging in and release as an alpha.
Thank you for your endurance here @josh-degraw!

@nojaf nojaf enabled auto-merge (squash) January 9, 2024 17:01
@nojaf nojaf merged commit 3a10e88 into fsprojects:main Jan 9, 2024
5 checks passed
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.

Aligned bracket style in anonymous record is not respected
2 participants