Skip to content

Commit cdd0aca

Browse files
authored
Merge pull request #58316 from sharwell/nullable-refactoring
Improve usability of "enable nullable reference types" refactoring
2 parents 3e32282 + b885da9 commit cdd0aca

File tree

2 files changed

+246
-15
lines changed

2 files changed

+246
-15
lines changed

src/EditorFeatures/CSharpTest/CodeActions/EnableNullable/EnableNullableTests.cs

Lines changed: 242 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System;
66
using System.Globalization;
77
using System.Linq;
8+
using System.Text.RegularExpressions;
89
using System.Threading;
910
using System.Threading.Tasks;
1011
using Microsoft.CodeAnalysis.CSharp;
@@ -25,8 +26,8 @@ public class EnableNullableTests
2526
var project = solution.GetRequiredProject(projectId);
2627
var document = project.Documents.First();
2728

28-
// Only the input solution contains '#nullable enable'
29-
if (!document.GetTextSynchronously(CancellationToken.None).ToString().Contains("#nullable enable"))
29+
// Only the input solution contains '#nullable enable' or '#nullable enable' in the first document
30+
if (!Regex.IsMatch(document.GetTextSynchronously(CancellationToken.None).ToString(), "#nullable ?enable"))
3031
{
3132
var compilationOptions = (CSharpCompilationOptions)solution.GetRequiredProject(projectId).CompilationOptions!;
3233
solution = solution.WithProjectCompilationOptions(projectId, compilationOptions.WithNullableContextOptions(NullableContextOptions.Enable));
@@ -35,16 +36,43 @@ public class EnableNullableTests
3536
return solution;
3637
};
3738

38-
[Fact]
39-
public async Task EnabledOnNullableEnable()
39+
private static readonly Func<Solution, ProjectId, Solution> s_enableNullableInFixedSolutionFromRestoreKeyword =
40+
(solution, projectId) =>
41+
{
42+
var project = solution.GetRequiredProject(projectId);
43+
var document = project.Documents.First();
44+
45+
// Only the input solution contains '#nullable restore' or '#nullable restore' in the first document
46+
if (!Regex.IsMatch(document.GetTextSynchronously(CancellationToken.None).ToString(), "#nullable ?restore"))
47+
{
48+
var compilationOptions = (CSharpCompilationOptions)solution.GetRequiredProject(projectId).CompilationOptions!;
49+
solution = solution.WithProjectCompilationOptions(projectId, compilationOptions.WithNullableContextOptions(NullableContextOptions.Enable));
50+
}
51+
52+
return solution;
53+
};
54+
55+
private static readonly Func<Solution, ProjectId, Solution> s_enableNullableInFixedSolutionFromDisableKeyword =
56+
s_enableNullableInFixedSolutionFromRestoreKeyword;
57+
58+
[Theory]
59+
[InlineData("$$#nullable enable")]
60+
[InlineData("#$$nullable enable")]
61+
[InlineData("#null$$able enable")]
62+
[InlineData("#nullable$$ enable")]
63+
[InlineData("#nullable $$ enable")]
64+
[InlineData("#nullable $$enable")]
65+
[InlineData("#nullable ena$$ble")]
66+
[InlineData("#nullable enable$$")]
67+
public async Task EnabledOnNullableEnable(string directive)
4068
{
41-
var code1 = @"
42-
#nullable enable$$
69+
var code1 = $@"
70+
{directive}
4371
4472
class Example
45-
{
73+
{{
4674
string? value;
47-
}
75+
}}
4876
";
4977
var code2 = @"
5078
class Example2
@@ -573,17 +601,217 @@ public async Task DisabledForUnsupportedLanguageVersion(LanguageVersion language
573601
}.RunAsync();
574602
}
575603

576-
[Fact]
577-
public async Task DisabledOnNullableDisable()
604+
[Theory]
605+
[InlineData("$$#nullable restore")]
606+
[InlineData("#$$nullable restore")]
607+
[InlineData("#null$$able restore")]
608+
[InlineData("#nullable$$ restore")]
609+
[InlineData("#nullable $$ restore")]
610+
[InlineData("#nullable $$restore")]
611+
[InlineData("#nullable res$$tore")]
612+
[InlineData("#nullable restore$$")]
613+
public async Task EnabledOnNullableRestore(string directive)
578614
{
579-
var code = @"
580-
#nullable disable$$
615+
var code1 = $@"
616+
{directive}
617+
618+
class Example
619+
{{
620+
string value;
621+
}}
622+
";
623+
var code2 = @"
624+
class Example2
625+
{
626+
string value;
627+
}
628+
";
629+
var code3 = @"
630+
class Example3
631+
{
632+
#nullable enable
633+
string? value;
634+
#nullable restore
635+
}
636+
";
637+
var code4 = @"
638+
#nullable disable
639+
640+
class Example4
641+
{
642+
string value;
643+
}
644+
";
645+
646+
var fixedDirective = directive.Replace("$$", "").Replace("restore", "disable");
647+
648+
var fixedCode1 = $@"
649+
{fixedDirective}
650+
651+
class Example
652+
{{
653+
string value;
654+
}}
655+
";
656+
var fixedCode2 = @"
657+
#nullable disable
658+
659+
class Example2
660+
{
661+
string value;
662+
}
663+
";
664+
var fixedCode3 = @"
665+
#nullable disable
666+
667+
class Example3
668+
{
669+
#nullable restore
670+
string? value;
671+
#nullable disable
672+
}
673+
";
674+
var fixedCode4 = @"
675+
#nullable disable
676+
677+
class Example4
678+
{
679+
string value;
680+
}
581681
";
582682

583683
await new VerifyCS.Test
584684
{
585-
TestCode = code,
586-
FixedCode = code,
685+
TestState =
686+
{
687+
Sources =
688+
{
689+
code1,
690+
code2,
691+
code3,
692+
code4,
693+
},
694+
},
695+
FixedState =
696+
{
697+
Sources =
698+
{
699+
fixedCode1,
700+
fixedCode2,
701+
fixedCode3,
702+
fixedCode4,
703+
},
704+
},
705+
SolutionTransforms = { s_enableNullableInFixedSolutionFromRestoreKeyword },
706+
}.RunAsync();
707+
}
708+
709+
[Theory]
710+
[InlineData("$$#nullable disable")]
711+
[InlineData("#$$nullable disable")]
712+
[InlineData("#null$$able disable")]
713+
[InlineData("#nullable$$ disable")]
714+
[InlineData("#nullable $$ disable")]
715+
[InlineData("#nullable $$disable")]
716+
[InlineData("#nullable dis$$able")]
717+
[InlineData("#nullable disable$$")]
718+
public async Task EnabledOnNullableDisable(string directive)
719+
{
720+
var code1 = $@"
721+
{directive}
722+
723+
class Example
724+
{{
725+
string value;
726+
}}
727+
728+
#nullable restore
729+
";
730+
var code2 = @"
731+
class Example2
732+
{
733+
string value;
734+
}
735+
";
736+
var code3 = @"
737+
class Example3
738+
{
739+
#nullable enable
740+
string? value;
741+
#nullable restore
742+
}
743+
";
744+
var code4 = @"
745+
#nullable disable
746+
747+
class Example4
748+
{
749+
string value;
750+
}
751+
";
752+
753+
var fixedDirective = directive.Replace("$$", "");
754+
755+
var fixedCode1 = $@"
756+
{fixedDirective}
757+
758+
class Example
759+
{{
760+
string value;
761+
}}
762+
763+
#nullable disable
764+
";
765+
var fixedCode2 = @"
766+
#nullable disable
767+
768+
class Example2
769+
{
770+
string value;
771+
}
772+
";
773+
var fixedCode3 = @"
774+
#nullable disable
775+
776+
class Example3
777+
{
778+
#nullable restore
779+
string? value;
780+
#nullable disable
781+
}
782+
";
783+
var fixedCode4 = @"
784+
#nullable disable
785+
786+
class Example4
787+
{
788+
string value;
789+
}
790+
";
791+
792+
await new VerifyCS.Test
793+
{
794+
TestState =
795+
{
796+
Sources =
797+
{
798+
code1,
799+
code2,
800+
code3,
801+
code4,
802+
},
803+
},
804+
FixedState =
805+
{
806+
Sources =
807+
{
808+
fixedCode1,
809+
fixedCode2,
810+
fixedCode3,
811+
fixedCode4,
812+
},
813+
},
814+
SolutionTransforms = { s_enableNullableInFixedSolutionFromDisableKeyword },
587815
}.RunAsync();
588816
}
589817
}

src/Features/CSharp/Portable/CodeRefactorings/EnableNullable/EnableNullableCodeRefactoringProvider.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,11 @@ public override async Task ComputeRefactoringsAsync(CodeRefactoringContext conte
5252
if (token.IsKind(SyntaxKind.EndOfDirectiveToken))
5353
token = root.FindToken(textSpan.Start - 1, findInsideTrivia: true);
5454

55-
if (!token.IsKind(SyntaxKind.EnableKeyword) || !token.Parent.IsKind(SyntaxKind.NullableDirectiveTrivia))
55+
if (!token.IsKind(SyntaxKind.EnableKeyword, SyntaxKind.RestoreKeyword, SyntaxKind.DisableKeyword, SyntaxKind.NullableKeyword, SyntaxKind.HashToken)
56+
|| !token.Parent.IsKind(SyntaxKind.NullableDirectiveTrivia, out NullableDirectiveTriviaSyntax? nullableDirectiveTrivia))
57+
{
5658
return;
59+
}
5760

5861
context.RegisterRefactoring(
5962
new MyCodeAction((purpose, cancellationToken) => EnableNullableReferenceTypesAsync(document.Project, purpose, cancellationToken)));

0 commit comments

Comments
 (0)