File: EnableNullable\EnableNullableTests.cs
Web Access
Project: src\src\Features\CSharpTest\Microsoft.CodeAnalysis.CSharp.Features.UnitTests.csproj (Microsoft.CodeAnalysis.CSharp.Features.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.CodeRefactorings.EnableNullable;
using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Testing;
using Roslyn.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.CodeActions.EnableNullable;
 
using VerifyCS = CSharpCodeRefactoringVerifier<EnableNullableCodeRefactoringProvider>;
 
[UseExportProvider]
public class EnableNullableTests
{
    private static readonly Func<Solution, ProjectId, Solution> s_enableNullableInFixedSolution =
        (solution, projectId) =>
        {
            var project = solution.GetRequiredProject(projectId);
            var document = project.Documents.First();
 
            // Only the input solution contains '#nullable enable' or '#nullable  enable' in the first document
            if (!Regex.IsMatch(document.GetTextSynchronously(CancellationToken.None).ToString(), "#nullable  ?enable"))
            {
                var compilationOptions = (CSharpCompilationOptions)solution.GetRequiredProject(projectId).CompilationOptions!;
                solution = solution.WithProjectCompilationOptions(projectId, compilationOptions.WithNullableContextOptions(NullableContextOptions.Enable));
            }
 
            return solution;
        };
 
    private static readonly Func<Solution, ProjectId, Solution> s_enableNullableInFixedSolutionFromRestoreKeyword =
        (solution, projectId) =>
        {
            var project = solution.GetRequiredProject(projectId);
            var document = project.Documents.First();
 
            // Only the input solution contains '#nullable restore' or '#nullable  restore' in the first document
            if (!Regex.IsMatch(document.GetTextSynchronously(CancellationToken.None).ToString(), "#nullable  ?restore"))
            {
                var compilationOptions = (CSharpCompilationOptions)solution.GetRequiredProject(projectId).CompilationOptions!;
                solution = solution.WithProjectCompilationOptions(projectId, compilationOptions.WithNullableContextOptions(NullableContextOptions.Enable));
            }
 
            return solution;
        };
 
    private static readonly Func<Solution, ProjectId, Solution> s_enableNullableInFixedSolutionFromDisableKeyword =
        s_enableNullableInFixedSolutionFromRestoreKeyword;
 
    [Theory]
    [InlineData("$$#nullable enable")]
    [InlineData("#$$nullable enable")]
    [InlineData("#null$$able enable")]
    [InlineData("#nullable$$ enable")]
    [InlineData("#nullable $$ enable")]
    [InlineData("#nullable $$enable")]
    [InlineData("#nullable ena$$ble")]
    [InlineData("#nullable enable$$")]
    public async Task EnabledOnNullableEnable(string directive)
    {
        var code1 = $@"
{directive}
 
class Example
{{
  string? value;
}}
";
        var code2 = @"
class Example2
{
  string value;
}
";
        var code3 = @"
class Example3
{
#nullable enable
  string? value;
#nullable restore
}
";
        var code4 = @"
#nullable disable
 
class Example4
{
  string value;
}
";
 
        var fixedCode1 = @"
 
class Example
{
  string? value;
}
";
        var fixedCode2 = @"
#nullable disable
 
class Example2
{
  string value;
}
";
        var fixedCode3 = @"
#nullable disable
 
class Example3
{
#nullable restore
  string? value;
#nullable disable
}
";
        var fixedCode4 = @"
#nullable disable
 
class Example4
{
  string value;
}
";
 
        await new VerifyCS.Test
        {
            TestState =
            {
                Sources =
                {
                    code1,
                    code2,
                    code3,
                    code4,
                },
            },
            FixedState =
            {
                Sources =
                {
                    fixedCode1,
                    fixedCode2,
                    fixedCode3,
                    fixedCode4,
                },
            },
            SolutionTransforms = { s_enableNullableInFixedSolution },
        }.RunAsync();
    }
 
    [Fact]
    public async Task PlacementAfterHeader()
    {
        var code1 = @"
#nullable enable$$
 
class Example
{
  string? value;
}
";
        var code2 = @"// File header line 1
// File header line 2
 
class Example2
{
  string value;
}
";
        var code3 = @"#region File Header
// File header line 1
// File header line 2
#endregion
 
class Example3
{
  string value;
}
";
 
        var fixedCode1 = @"
 
class Example
{
  string? value;
}
";
        var fixedCode2 = @"// File header line 1
// File header line 2
 
#nullable disable
 
class Example2
{
  string value;
}
";
        var fixedCode3 = @"#region File Header
// File header line 1
// File header line 2
#endregion
 
#nullable disable
 
class Example3
{
  string value;
}
";
 
        await new VerifyCS.Test
        {
            TestState =
            {
                Sources =
                {
                    code1,
                    code2,
                    code3,
                },
            },
            FixedState =
            {
                Sources =
                {
                    fixedCode1,
                    fixedCode2,
                    fixedCode3,
                },
            },
            SolutionTransforms = { s_enableNullableInFixedSolution },
        }.RunAsync();
    }
 
    [Fact]
    public async Task PlacementBeforeDocComment()
    {
        var code1 = @"
#nullable enable$$
 
class Example
{
  string? value;
}
";
        var code2 = @"// Line comment
class Example2
{
  string value;
}
";
        var code3 = @"/*
 * Block comment
 */
class Example3
{
  string value;
}
";
        var code4 = @"/// <summary>Single line doc comment</summary>
class Example4
{
  string value;
}
";
        var code5 = @"/**
 * Multi-line doc comment
 */
class Example5
{
  string value;
}
";
 
        var fixedCode1 = @"
 
class Example
{
  string? value;
}
";
        var fixedCode2 = @"// Line comment
#nullable disable
 
class Example2
{
  string value;
}
";
        var fixedCode3 = @"/*
 * Block comment
 */
#nullable disable
 
class Example3
{
  string value;
}
";
        var fixedCode4 = @"#nullable disable
 
/// <summary>Single line doc comment</summary>
class Example4
{
  string value;
}
";
        var fixedCode5 = @"#nullable disable
 
/**
 * Multi-line doc comment
 */
class Example5
{
  string value;
}
";
 
        await new VerifyCS.Test
        {
            TestState =
            {
                Sources =
                {
                    code1,
                    code2,
                    code3,
                    code4,
                    code5,
                },
            },
            FixedState =
            {
                Sources =
                {
                    fixedCode1,
                    fixedCode2,
                    fixedCode3,
                    fixedCode4,
                    fixedCode5,
                },
            },
            SolutionTransforms = { s_enableNullableInFixedSolution },
        }.RunAsync();
    }
 
    [Fact]
    public async Task OmitLeadingRestore()
    {
        var code1 = @"
#nullable enable$$
 
class Example
{
  string? value;
}
";
        var code2 = @"
#nullable enable
 
class Example2
{
  string? value;
}
";
        var code3 = @"
#nullable enable warnings
 
class Example3
{
  string value;
}
";
        var code4 = @"
#nullable enable annotations
 
class Example4
{
  string? value;
}
";
 
        var fixedCode1 = @"
 
class Example
{
  string? value;
}
";
        var fixedCode2 = @"
 
class Example2
{
  string? value;
}
";
        var fixedCode3 = @"
#nullable disable
 
#nullable restore warnings
 
class Example3
{
  string value;
}
";
        var fixedCode4 = @"
#nullable disable
 
#nullable restore annotations
 
class Example4
{
  string? value;
}
";
 
        await new VerifyCS.Test
        {
            TestState =
            {
                Sources =
                {
                    code1,
                    code2,
                    code3,
                    code4,
                },
            },
            FixedState =
            {
                Sources =
                {
                    fixedCode1,
                    fixedCode2,
                    fixedCode3,
                    fixedCode4,
                },
            },
            SolutionTransforms = { s_enableNullableInFixedSolution },
        }.RunAsync();
    }
 
    [Fact]
    public async Task IgnoreGeneratedCode()
    {
        var code1 = @"
#nullable enable$$
 
class Example
{
  string? value;
}
";
        var generatedCode1 = @"// <auto-generated/>
 
#nullable enable
 
class Example2
{
  string? value;
}
";
        var generatedCode2 = @"// <auto-generated/>
 
#nullable disable
 
class Example3
{
  string value;
}
";
        var generatedCode3 = @"// <auto-generated/>
 
#nullable restore
 
class Example4
{
  string {|#0:value|};
}
";
 
        var fixedCode1 = @"
 
class Example
{
  string? value;
}
";
 
        await new VerifyCS.Test
        {
            TestState =
            {
                Sources =
                {
                    code1,
                    generatedCode1,
                    generatedCode2,
                    generatedCode3,
                },
            },
            FixedState =
            {
                Sources =
                {
                    fixedCode1,
                    generatedCode1,
                    generatedCode2,
                    generatedCode3,
                },
                ExpectedDiagnostics =
                {
                    // /0/Test3.cs(7,10): error CS8618: Non-nullable field 'value' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.
                    DiagnosticResult.CompilerError("CS8618").WithSpan("/0/Test3.cs", 7, 10, 7, 15).WithSpan("/0/Test3.cs", 7, 10, 7, 15).WithArguments("field", "value"),
                },
            },
            SolutionTransforms = { s_enableNullableInFixedSolution },
        }.RunAsync();
    }
 
    [Theory]
    [InlineData(NullableContextOptions.Annotations)]
    [InlineData(NullableContextOptions.Warnings)]
    [InlineData(NullableContextOptions.Enable)]
    public async Task DisabledIfSetInProject(NullableContextOptions nullableContextOptions)
    {
        var code = @"
#nullable enable$$
";
 
        await new VerifyCS.Test
        {
            TestCode = code,
            FixedCode = code,
            SolutionTransforms =
            {
                (solution, projectId) =>
                {
                    var compilationOptions = (CSharpCompilationOptions)solution.GetRequiredProject(projectId).CompilationOptions!;
                    return solution.WithProjectCompilationOptions(projectId, compilationOptions.WithNullableContextOptions(nullableContextOptions));
                },
            },
        }.RunAsync();
    }
 
    [Theory]
    [InlineData(LanguageVersion.CSharp1)]
    [InlineData(LanguageVersion.CSharp2)]
    [InlineData(LanguageVersion.CSharp3)]
    [InlineData(LanguageVersion.CSharp4)]
    [InlineData(LanguageVersion.CSharp5)]
    [InlineData(LanguageVersion.CSharp6)]
    [InlineData(LanguageVersion.CSharp7)]
    [InlineData(LanguageVersion.CSharp7_1)]
    [InlineData(LanguageVersion.CSharp7_2)]
    [InlineData(LanguageVersion.CSharp7_3)]
    public async Task DisabledForUnsupportedLanguageVersion(LanguageVersion languageVersion)
    {
        var code = @"
#{|#0:nullable|} enable$$
";
 
        var error = languageVersion switch
        {
            LanguageVersion.CSharp1 => "CS8022",
            LanguageVersion.CSharp2 => "CS8023",
            LanguageVersion.CSharp3 => "CS8024",
            LanguageVersion.CSharp4 => "CS8025",
            LanguageVersion.CSharp5 => "CS8026",
            LanguageVersion.CSharp6 => "CS8059",
            LanguageVersion.CSharp7 => "CS8107",
            LanguageVersion.CSharp7_1 => "CS8302",
            LanguageVersion.CSharp7_2 => "CS8320",
            LanguageVersion.CSharp7_3 => "CS8370",
            _ => throw ExceptionUtilities.Unreachable(),
        };
 
        // /0/Test0.cs(2,2): error [error]: Feature 'nullable reference types' is not available in C# [version]. Please use language version 8.0 or greater.
        var expected = DiagnosticResult.CompilerError(error).WithLocation(0);
        if (CultureInfo.CurrentCulture.TwoLetterISOLanguageName == "en")
        {
            expected = expected.WithArguments("nullable reference types", "8.0");
        }
 
        await new VerifyCS.Test
        {
            TestCode = code,
            ExpectedDiagnostics = { expected },
            FixedCode = code,
            LanguageVersion = languageVersion,
        }.RunAsync();
    }
 
    [Theory]
    [InlineData("$$#nullable restore")]
    [InlineData("#$$nullable restore")]
    [InlineData("#null$$able restore")]
    [InlineData("#nullable$$ restore")]
    [InlineData("#nullable $$ restore")]
    [InlineData("#nullable $$restore")]
    [InlineData("#nullable res$$tore")]
    [InlineData("#nullable restore$$")]
    public async Task EnabledOnNullableRestore(string directive)
    {
        var code1 = $@"
{directive}
 
class Example
{{
  string value;
}}
";
        var code2 = @"
class Example2
{
  string value;
}
";
        var code3 = @"
class Example3
{
#nullable enable
  string? value;
#nullable restore
}
";
        var code4 = @"
#nullable disable
 
class Example4
{
  string value;
}
";
 
        var fixedDirective = directive.Replace("$$", "").Replace("restore", "disable");
 
        var fixedCode1 = $@"
{fixedDirective}
 
class Example
{{
  string value;
}}
";
        var fixedCode2 = @"
#nullable disable
 
class Example2
{
  string value;
}
";
        var fixedCode3 = @"
#nullable disable
 
class Example3
{
#nullable restore
  string? value;
#nullable disable
}
";
        var fixedCode4 = @"
#nullable disable
 
class Example4
{
  string value;
}
";
 
        await new VerifyCS.Test
        {
            TestState =
            {
                Sources =
                {
                    code1,
                    code2,
                    code3,
                    code4,
                },
            },
            FixedState =
            {
                Sources =
                {
                    fixedCode1,
                    fixedCode2,
                    fixedCode3,
                    fixedCode4,
                },
            },
            SolutionTransforms = { s_enableNullableInFixedSolutionFromRestoreKeyword },
        }.RunAsync();
    }
 
    [Theory]
    [InlineData("$$#nullable disable")]
    [InlineData("#$$nullable disable")]
    [InlineData("#null$$able disable")]
    [InlineData("#nullable$$ disable")]
    [InlineData("#nullable $$ disable")]
    [InlineData("#nullable $$disable")]
    [InlineData("#nullable dis$$able")]
    [InlineData("#nullable disable$$")]
    public async Task EnabledOnNullableDisable(string directive)
    {
        var code1 = $@"
{directive}
 
class Example
{{
  string value;
}}
 
#nullable restore
";
        var code2 = @"
class Example2
{
  string value;
}
";
        var code3 = @"
class Example3
{
#nullable enable
  string? value;
#nullable restore
}
";
        var code4 = @"
#nullable disable
 
class Example4
{
  string value;
}
";
 
        var fixedDirective = directive.Replace("$$", "");
 
        var fixedCode1 = $@"
{fixedDirective}
 
class Example
{{
  string value;
}}
 
#nullable disable
";
        var fixedCode2 = @"
#nullable disable
 
class Example2
{
  string value;
}
";
        var fixedCode3 = @"
#nullable disable
 
class Example3
{
#nullable restore
  string? value;
#nullable disable
}
";
        var fixedCode4 = @"
#nullable disable
 
class Example4
{
  string value;
}
";
 
        await new VerifyCS.Test
        {
            TestState =
            {
                Sources =
                {
                    code1,
                    code2,
                    code3,
                    code4,
                },
            },
            FixedState =
            {
                Sources =
                {
                    fixedCode1,
                    fixedCode2,
                    fixedCode3,
                    fixedCode4,
                },
            },
            SolutionTransforms = { s_enableNullableInFixedSolutionFromDisableKeyword },
        }.RunAsync();
    }
}