Skip to content

Commit 1f55b85

Browse files
authored
Fixing C# using -> F# open code fix (#15799)
1 parent 81bb862 commit 1f55b85

File tree

3 files changed

+207
-47
lines changed

3 files changed

+207
-47
lines changed

vsintegration/src/FSharp.Editor/CodeFixes/ConvertCSharpUsingToFSharpOpen.fs

Lines changed: 50 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,73 +9,76 @@ open System.Collections.Immutable
99
open Microsoft.CodeAnalysis.Text
1010
open Microsoft.CodeAnalysis.CodeFixes
1111

12+
open CancellableTasks
13+
1214
[<ExportCodeFixProvider(FSharpConstants.FSharpLanguageName, Name = CodeFix.ConvertCSharpUsingToFSharpOpen); Shared>]
1315
type internal ConvertCSharpUsingToFSharpOpenCodeFixProvider [<ImportingConstructor>] () =
1416
inherit CodeFixProvider()
1517

1618
static let title = SR.ConvertCSharpUsingToFSharpOpen()
1719
let usingLength = "using".Length
1820

19-
let isCSharpUsingShapeWithPos (context: CodeFixContext) (sourceText: SourceText) =
21+
let isCSharpUsingShapeWithPos (errorSpan: TextSpan) (sourceText: SourceText) =
2022
// Walk back until whitespace
21-
let mutable pos = context.Span.Start - 1
22-
let mutable ch = sourceText.[pos]
23+
let mutable pos = errorSpan.Start
24+
let mutable ch = sourceText[pos]
2325

2426
while pos > 0 && not (Char.IsWhiteSpace(ch)) do
2527
pos <- pos - 1
26-
ch <- sourceText.[pos]
28+
ch <- sourceText[pos]
2729

2830
// Walk back whitespace
29-
ch <- sourceText.[pos]
31+
ch <- sourceText[pos]
3032

3133
while pos > 0 && Char.IsWhiteSpace(ch) do
3234
pos <- pos - 1
33-
ch <- sourceText.[pos]
35+
ch <- sourceText[pos]
3436

3537
// Take 'using' slice and don't forget that offset because computer math is annoying
3638
let start = pos - usingLength + 1
37-
let span = TextSpan(start, usingLength)
38-
let slice = sourceText.GetSubText(span).ToString()
39-
struct (slice = "using", start)
40-
41-
let registerCodeFix (context: CodeFixContext) (str: string) (span: TextSpan) =
42-
let replacement =
43-
let str = str.Replace("using", "open").Replace(";", "")
44-
TextChange(span, str)
4539

46-
do context.RegisterFsharpFix(CodeFix.ConvertCSharpUsingToFSharpOpen, title, [| replacement |])
40+
if start < 0 then
41+
false
42+
else
43+
let span = TextSpan(start, usingLength)
44+
let slice = sourceText.GetSubText(span).ToString()
45+
slice = "using"
4746

4847
override _.FixableDiagnosticIds = ImmutableArray.Create("FS0039", "FS0201")
4948

50-
override _.RegisterCodeFixesAsync context =
51-
asyncMaybe {
52-
let! sourceText = context.Document.GetTextAsync(context.CancellationToken)
53-
54-
// TODO: handle single-line case?
55-
let statementWithSemicolonSpan =
56-
TextSpan(context.Span.Start, context.Span.Length + 1)
57-
58-
do! Option.guard (sourceText.Length >= statementWithSemicolonSpan.End)
59-
60-
let statementWithSemicolon =
61-
sourceText.GetSubText(statementWithSemicolonSpan).ToString()
62-
63-
// Top of the file case -- entire line gets a diagnostic
64-
if
65-
(statementWithSemicolon.StartsWith("using")
66-
&& statementWithSemicolon.EndsWith(";"))
67-
then
68-
registerCodeFix context statementWithSemicolon statementWithSemicolonSpan
69-
else
70-
// Only the identifier being opened has a diagnostic, so we try to find the rest of the statement
71-
let struct (isCSharpUsingShape, start) =
72-
isCSharpUsingShapeWithPos context sourceText
73-
74-
if isCSharpUsingShape then
75-
let len = (context.Span.Start - start) + statementWithSemicolonSpan.Length
76-
let fullSpan = TextSpan(start, len)
77-
let str = sourceText.GetSubText(fullSpan).ToString()
78-
registerCodeFix context str fullSpan
79-
}
80-
|> Async.Ignore
81-
|> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken)
49+
override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix this
50+
51+
override this.GetFixAllProvider() = this.RegisterFsharpFixAll()
52+
53+
interface IFSharpCodeFixProvider with
54+
member _.GetCodeFixIfAppliesAsync context =
55+
cancellableTask {
56+
let diagnostic = context.Diagnostics[0]
57+
let! errorText = context.GetSquigglyTextAsync()
58+
let! sourceText = context.GetSourceTextAsync()
59+
60+
let isValidCase =
61+
match diagnostic.Id with
62+
// using is included in the squiggly
63+
| "FS0201" when errorText.Contains("using ") -> true
64+
// using is not included in the squiqqly
65+
| "FS0039" when isCSharpUsingShapeWithPos context.Span sourceText -> true
66+
| _ -> false
67+
68+
if isValidCase then
69+
let lineNumber = sourceText.Lines.GetLinePositionSpan(context.Span).Start.Line
70+
let line = sourceText.Lines[lineNumber]
71+
72+
let change =
73+
TextChange(line.Span, line.ToString().Replace("using", "open").Replace(";", ""))
74+
75+
return
76+
ValueSome
77+
{
78+
Name = CodeFix.ConvertCSharpUsingToFSharpOpen
79+
Message = title
80+
Changes = [ change ]
81+
}
82+
else
83+
return ValueNone
84+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
2+
3+
module FSharp.Editor.Tests.CodeFixes.ConvertCSharpUsingToFSharpOpenTests
4+
5+
open Microsoft.VisualStudio.FSharp.Editor
6+
open Xunit
7+
8+
open CodeFixTestFramework
9+
10+
let private codeFix = ConvertCSharpUsingToFSharpOpenCodeFixProvider()
11+
12+
[<Theory>]
13+
[<InlineData "">]
14+
[<InlineData ";">]
15+
let ``Fixes FS0039 - simple namespace`` optionalSemicolon =
16+
let code =
17+
$"""
18+
using System{optionalSemicolon}
19+
"""
20+
21+
let expected =
22+
Some
23+
{
24+
Message = "Convert C# 'using' to F# 'open'"
25+
FixedCode =
26+
"""
27+
open System
28+
"""
29+
}
30+
31+
let actual = codeFix |> tryFix code Auto
32+
33+
Assert.Equal(expected, actual)
34+
35+
[<Theory>]
36+
[<InlineData "">]
37+
[<InlineData ";">]
38+
let ``Fixes FS0039 - complex namespace`` optionalSemicolon =
39+
let code =
40+
$"""
41+
using System.IO{optionalSemicolon}
42+
"""
43+
44+
let expected =
45+
Some
46+
{
47+
Message = "Convert C# 'using' to F# 'open'"
48+
FixedCode =
49+
"""
50+
open System.IO
51+
"""
52+
}
53+
54+
let actual = codeFix |> tryFix code Auto
55+
56+
Assert.Equal(expected, actual)
57+
58+
[<Fact>]
59+
let ``Doesn't fix random FS0039`` () =
60+
let code = """namespa"""
61+
62+
let expected = None
63+
64+
let actual = codeFix |> tryFix code Auto
65+
66+
Assert.Equal(expected, actual)
67+
68+
[<Theory>]
69+
[<InlineData "">]
70+
[<InlineData ";">]
71+
let ``Fixes FS0201 - simple namespace`` optionalSemicolon =
72+
let code =
73+
$"""
74+
namespace Test
75+
76+
using System{optionalSemicolon}
77+
"""
78+
79+
let expected =
80+
Some
81+
{
82+
Message = "Convert C# 'using' to F# 'open'"
83+
FixedCode =
84+
"""
85+
namespace Test
86+
87+
open System
88+
"""
89+
}
90+
91+
let actual = codeFix |> tryFix code Auto
92+
93+
Assert.Equal(expected, actual)
94+
95+
[<Theory>]
96+
[<InlineData "">]
97+
[<InlineData ";">]
98+
let ``Fixes FS0201 - complex namespace`` optionalSemicolon =
99+
let code =
100+
$"""
101+
namespace Test
102+
103+
using System.IO{optionalSemicolon}
104+
"""
105+
106+
let expected =
107+
Some
108+
{
109+
Message = "Convert C# 'using' to F# 'open'"
110+
FixedCode =
111+
"""
112+
namespace Test
113+
114+
open System.IO
115+
"""
116+
}
117+
118+
let actual = codeFix |> tryFix code Auto
119+
120+
Assert.Equal(expected, actual)
121+
122+
[<Fact>]
123+
let ``Doesn't fix random FS0201`` () =
124+
let code =
125+
"""
126+
namespace Test
127+
128+
let x = 42
129+
"""
130+
131+
let expected = None
132+
133+
let actual = codeFix |> tryFix code Auto
134+
135+
Assert.Equal(expected, actual)
136+
137+
[<Fact>]
138+
let ``Handles spaces before semicolons`` () =
139+
let code =
140+
$"""
141+
using System ;
142+
"""
143+
144+
let expected =
145+
Some
146+
{
147+
Message = "Convert C# 'using' to F# 'open'"
148+
FixedCode =
149+
"""
150+
open System
151+
"""
152+
}
153+
154+
let actual = codeFix |> tryFix code Auto
155+
156+
Assert.Equal(expected, actual)

vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
<Compile Include="CodeFixes\RemoveUnusedOpensTests.fs" />
6363
<Compile Include="CodeFixes\SimplifyNameTests.fs" />
6464
<Compile Include="CodeFixes\RenameParamToMatchSignatureTests.fs" />
65+
<Compile Include="CodeFixes\ConvertCSharpUsingToFSharpOpenTests.fs" />
6566
<Compile Include="Hints\HintTestFramework.fs" />
6667
<Compile Include="Hints\OptionParserTests.fs" />
6768
<Compile Include="Hints\InlineParameterNameHintTests.fs" />

0 commit comments

Comments
 (0)