Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,73 +9,76 @@ open System.Collections.Immutable
open Microsoft.CodeAnalysis.Text
open Microsoft.CodeAnalysis.CodeFixes

open CancellableTasks

[<ExportCodeFixProvider(FSharpConstants.FSharpLanguageName, Name = CodeFix.ConvertCSharpUsingToFSharpOpen); Shared>]
type internal ConvertCSharpUsingToFSharpOpenCodeFixProvider [<ImportingConstructor>] () =
inherit CodeFixProvider()

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

let isCSharpUsingShapeWithPos (context: CodeFixContext) (sourceText: SourceText) =
let isCSharpUsingShapeWithPos (errorSpan: TextSpan) (sourceText: SourceText) =
// Walk back until whitespace
let mutable pos = context.Span.Start - 1
let mutable ch = sourceText.[pos]
let mutable pos = errorSpan.Start
let mutable ch = sourceText[pos]

while pos > 0 && not (Char.IsWhiteSpace(ch)) do
pos <- pos - 1
ch <- sourceText.[pos]
ch <- sourceText[pos]

// Walk back whitespace
ch <- sourceText.[pos]
ch <- sourceText[pos]

while pos > 0 && Char.IsWhiteSpace(ch) do
pos <- pos - 1
ch <- sourceText.[pos]
ch <- sourceText[pos]

// Take 'using' slice and don't forget that offset because computer math is annoying
let start = pos - usingLength + 1
let span = TextSpan(start, usingLength)
let slice = sourceText.GetSubText(span).ToString()
struct (slice = "using", start)

let registerCodeFix (context: CodeFixContext) (str: string) (span: TextSpan) =
let replacement =
let str = str.Replace("using", "open").Replace(";", "")
TextChange(span, str)

do context.RegisterFsharpFix(CodeFix.ConvertCSharpUsingToFSharpOpen, title, [| replacement |])
if start < 0 then
false
else
let span = TextSpan(start, usingLength)
let slice = sourceText.GetSubText(span).ToString()
slice = "using"

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

override _.RegisterCodeFixesAsync context =
asyncMaybe {
let! sourceText = context.Document.GetTextAsync(context.CancellationToken)

// TODO: handle single-line case?
let statementWithSemicolonSpan =
TextSpan(context.Span.Start, context.Span.Length + 1)

do! Option.guard (sourceText.Length >= statementWithSemicolonSpan.End)

let statementWithSemicolon =
sourceText.GetSubText(statementWithSemicolonSpan).ToString()

// Top of the file case -- entire line gets a diagnostic
if
(statementWithSemicolon.StartsWith("using")
&& statementWithSemicolon.EndsWith(";"))
then
registerCodeFix context statementWithSemicolon statementWithSemicolonSpan
else
// Only the identifier being opened has a diagnostic, so we try to find the rest of the statement
let struct (isCSharpUsingShape, start) =
isCSharpUsingShapeWithPos context sourceText

if isCSharpUsingShape then
let len = (context.Span.Start - start) + statementWithSemicolonSpan.Length
let fullSpan = TextSpan(start, len)
let str = sourceText.GetSubText(fullSpan).ToString()
registerCodeFix context str fullSpan
}
|> Async.Ignore
|> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken)
override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix this

override this.GetFixAllProvider() = this.RegisterFsharpFixAll()

interface IFSharpCodeFixProvider with
member _.GetCodeFixIfAppliesAsync context =
cancellableTask {
let diagnostic = context.Diagnostics[0]
let! errorText = context.GetSquigglyTextAsync()
let! sourceText = context.GetSourceTextAsync()

let isValidCase =
match diagnostic.Id with
// using is included in the squiggly
| "FS0201" when errorText.Contains("using ") -> true
// using is not included in the squiqqly
| "FS0039" when isCSharpUsingShapeWithPos context.Span sourceText -> true
| _ -> false

if isValidCase then
let lineNumber = sourceText.Lines.GetLinePositionSpan(context.Span).Start.Line
let line = sourceText.Lines[lineNumber]

let change =
TextChange(line.Span, line.ToString().Replace("using", "open").Replace(";", ""))

return
ValueSome
{
Name = CodeFix.ConvertCSharpUsingToFSharpOpen
Message = title
Changes = [ change ]
}
else
return ValueNone
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

module FSharp.Editor.Tests.CodeFixes.ConvertCSharpUsingToFSharpOpenTests

open Microsoft.VisualStudio.FSharp.Editor
open Xunit

open CodeFixTestFramework

let private codeFix = ConvertCSharpUsingToFSharpOpenCodeFixProvider()

[<Theory>]
[<InlineData "">]
[<InlineData ";">]
let ``Fixes FS0039 - simple namespace`` optionalSemicolon =
let code =
$"""
using System{optionalSemicolon}
"""

let expected =
Some
{
Message = "Convert C# 'using' to F# 'open'"
FixedCode =
"""
open System
"""
}

let actual = codeFix |> tryFix code Auto

Assert.Equal(expected, actual)

[<Theory>]
[<InlineData "">]
[<InlineData ";">]
let ``Fixes FS0039 - complex namespace`` optionalSemicolon =
let code =
$"""
using System.IO{optionalSemicolon}
"""

let expected =
Some
{
Message = "Convert C# 'using' to F# 'open'"
FixedCode =
"""
open System.IO
"""
}

let actual = codeFix |> tryFix code Auto

Assert.Equal(expected, actual)

[<Fact>]
let ``Doesn't fix random FS0039`` () =
let code = """namespa"""

let expected = None

let actual = codeFix |> tryFix code Auto

Assert.Equal(expected, actual)

[<Theory>]
[<InlineData "">]
[<InlineData ";">]
let ``Fixes FS0201 - simple namespace`` optionalSemicolon =
let code =
$"""
namespace Test

using System{optionalSemicolon}
"""

let expected =
Some
{
Message = "Convert C# 'using' to F# 'open'"
FixedCode =
"""
namespace Test

open System
"""
}

let actual = codeFix |> tryFix code Auto

Assert.Equal(expected, actual)

[<Theory>]
[<InlineData "">]
[<InlineData ";">]
let ``Fixes FS0201 - complex namespace`` optionalSemicolon =
let code =
$"""
namespace Test

using System.IO{optionalSemicolon}
"""

let expected =
Some
{
Message = "Convert C# 'using' to F# 'open'"
FixedCode =
"""
namespace Test

open System.IO
"""
}

let actual = codeFix |> tryFix code Auto

Assert.Equal(expected, actual)

[<Fact>]
let ``Doesn't fix random FS0201`` () =
let code =
"""
namespace Test

let x = 42
"""

let expected = None

let actual = codeFix |> tryFix code Auto

Assert.Equal(expected, actual)

[<Fact>]
let ``Handles spaces before semicolons`` () =
let code =
$"""
using System ;
"""

let expected =
Some
{
Message = "Convert C# 'using' to F# 'open'"
FixedCode =
"""
open System
"""
}

let actual = codeFix |> tryFix code Auto

Assert.Equal(expected, actual)
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
<Compile Include="CodeFixes\RemoveUnusedOpensTests.fs" />
<Compile Include="CodeFixes\SimplifyNameTests.fs" />
<Compile Include="CodeFixes\RenameParamToMatchSignatureTests.fs" />
<Compile Include="CodeFixes\ConvertCSharpUsingToFSharpOpenTests.fs" />
<Compile Include="Hints\HintTestFramework.fs" />
<Compile Include="Hints\OptionParserTests.fs" />
<Compile Include="Hints\InlineParameterNameHintTests.fs" />
Expand Down