-
Notifications
You must be signed in to change notification settings - Fork 789
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
Analyzer & code fix for ~IDE0047~ FS3583: remove unnecessary parentheses #16079
Conversation
When it comes to strings and other constants, there actually are a lot of scenarios where the value in the untyped AST doesn't reflect what the user wrote. The way we deal with this in Fantomas is to get the original text from
I'm in favour of extending Nice work @brianrourkeboll! |
Hmm yes, I tried the following and it does work, although it pollutes the signature/callsite a bit for what seems like a rather small and uncommon thing (e.g., parens are not needed in val getUnnecessaryParentheses : getTextAtRange: (range -> string) -> parsedInput: ParsedInput -> Async<range seq> let getTextAtRange m = sourceText.ToString(RoslynHelpers.FSharpRangeToTextSpan(sourceText, m))
let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses getTextAtRange parseResults.ParseTree Do you think it's worth the addition? We could pass in the Roslyn |
I think having it as Overall it is a trade-off for sure. Passing in |
Hmm, I'm not totally against using
As for (1), As for (2a), I don't really foresee that this particular function ( type ISourceText =
abstract Item: index: int -> char with get
abstract GetLineString: lineIndex: int -> string
abstract GetLineCount: unit -> int
abstract GetLastCharacterPosition: unit -> int * int
abstract GetSubTextString: start: int * length: int -> string
abstract SubTextEquals: target: string * startIndex: int -> bool
abstract Length: int
abstract ContentEquals: sourceText: ISourceText -> bool
abstract CopyTo: sourceIndex: int * destination: char[] * destinationIndex: int * count: int -> unit With let! unnecessaryParentheses =
UnnecessaryParentheses.getUnnecessaryParentheses
sourceText.GetSubTextFromRange
parseResults.ParseTree As for (2b), if we ever wanted to reap that benefit (although I realize that an type IGetSubtext =
abstract GetSubtext: range: range -> ReadOnlySpan<char> A caller with an let getTextFromRange =
{ new IGetSubtext with
member _.GetSubtext range =
(sourceText.GetSubTextFromRange range).AsSpan() }
let! unnecessaryParentheses =
UnnecessaryParentheses.getUnnecessaryParentheses
getTextFromRange
parseResults.ParseTree …All of that to say: I vote that we either:
Footnotes
|
vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs
Outdated
Show resolved
Hide resolved
vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs
Outdated
Show resolved
Hide resolved
* Use `getSourceLineStr` and enable handling sensitive indentation inside parens.
This uses the API exposed in FSC in dotnet/fsharp#16079.
* Add unnecessary parens analyzer & codefix This uses the API exposed in FSC in dotnet/fsharp#16079. * Comments * Use nicer APIs * Fix * Better there * Fmt * Disambiguate AsSpan overload * Remove debug fail * Remove redundant diag filter
* For background, see: * dotnet#35591 * dotnet/fsharp#16078, * dotnet/fsharp#16079 (comment) * dotnet/fsharp#16079 (review) * dotnet/fsharp#16211
Resolves #16078.
I have an analyzer and code fix for IDE0047: remove unnecessary parentheses for expressions and patterns (adding it for type signatures can be done but will require updating
SyntaxTraversal.Traverse
to traverse types in many more places).Extra.parens.2023-10-04.210059.mp4
I shoehorned the call to the analysis logic into
DocumentDiagnosticAnalyzer.fs
. This lets us avoid making any upstream changes to Roslyn that then need to flow back here, although eventually just hooking every new analyzer intoDocumentDiagnosticAnalyzer
seems like it would become unsustainable or at least undesirable, so we should probably come up with some kind of longer-term plan at some point.I've added a fair number of obvious tests, but, due to the combinatorially-explosive nature of the problem at hand, they are by no means exhaustive. I'd be happy to hear suggestions for ways to improve them.
Top-level test summary
Questions
The fix-all/bulk-fix functionality doesn't seem to trigger. Does anyone see an obvious reason why it wouldn't?
I assume I need to add
IFSharpUnnecessaryParenthesesDiagnosticAnalyzer
andFSharpIDEDiagnosticIds.RemoveUnnecessaryParentheses
to Roslyn.Might that also be why the bulk-fix functionality is not working for this (because Roslyn is not aware that IDE0047 is supported for F#)?What are the logistics for getting that into Roslyn so that it is available to use here? Edit: it sounds like we don't actually want to do this, at least right now, but I at least moved the diagnostic creation logic into its own file in a2c9fe2.When merged, should the analyzer be enabled by default in VS? It is for C#/VB, and I think it should be for F#, too (assuming the perf is acceptable). Either way, should it have a VS setting/be toggleable? As of right now, it will be enabled by default and will not be toggleable.
The current implementation independently traverses the entire untyped syntax tree using a creative call to the code underlying
SyntaxTraversal.Traverse
. Is this OK, or even the right way or place at all? In theory the diagnostics could be emitted inCheckExpressions.fs
,CheckPatterns.fs
, etc., during the type-checking pass, although it's debatable whether that's the right place to emit an "IDE" diagnostic. Or is there another way to avoid/amortize/consolidate full AST traversals across analyzers (if that's even desirable—I can see not wanting one misbehaving analyzer to be able to block others)?I made a couple small updates to
SyntaxTraversal.Traverse
to handle some (seemingly) obvious omissions, namely:parsedData
of the firstSynExpr.Lambda
in a lambda sequence instead of the simple pats of each lambda in the sequence.SynExpr.Lambda
in a lambda sequence instead of the body of each lambda in the sequence.Is this OK?
The only public API addition is
Is this the signature we want?
I can think of other scenarios where we might additionally want to expose another function along the lines of
It's not currently possible to apply the code fix to types (signatures or annotations, e.g.,
x : (int)
→x : int
), sinceSyntaxTraversal.Traverse
does not traverse mostSynType
uses (and it explicitly skipsSynType.Paren
nodes altogether anyway). While certainly less pressing than expressions and patterns, would it ever make sense to handle types? (Edit: this should probably be done in one or more separate PRs.)It would be nice if we could assert that the AST never changed (at all? meaningfully?) upon application of the code fix in response to generated inputs, perhaps using something like FsCheck (compare https://blog.emillon.org/posts/2020-08-03-fuzzing-ocamlformat-with-afl-and-crowbar.html). If we did this, we'd need to take care to avoid any potential flakiness (or not run it on every commit, etc.). We could alternatively at least make such an assertion for the existing (non-generated) tests.
I'm not currently handling the ML infix operators (
mod
,land
…), since all (except formod
) now emit warnings. Should I? Edit: it seems reasonable not to handle them, since the default behavior will simply be not to suggest that parens can be removed even if they theoretically could, which I suspect is for the best, since I don't think many F# programmers without an OCaml background would know their precedence off the top of their head.When ready
Remaining work
Potential followups