diff --git a/src/EditorFeatures/CSharpTest/Diagnostics/Nullable/CSharpDeclareAsNullableCodeFixTests.cs b/src/EditorFeatures/CSharpTest/Diagnostics/Nullable/CSharpDeclareAsNullableCodeFixTests.cs
new file mode 100644
index 0000000000000000000000000000000000000000..38f043af6d066a7de27cca54390720f225a4521e
--- /dev/null
+++ b/src/EditorFeatures/CSharpTest/Diagnostics/Nullable/CSharpDeclareAsNullableCodeFixTests.cs
@@ -0,0 +1,261 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.CodeFixes.DeclareAsNullable;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Test.Utilities;
+using Roslyn.Test.Utilities;
+using Xunit;
+
+namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics.DeclareAsNullable
+{
+ [Trait(Traits.Feature, Traits.Features.CodeActionsDeclareAsNullable)]
+ public class CSharpDeclareAsNullableCodeFixTests : AbstractCSharpDiagnosticProviderBasedUserDiagnosticTest
+ {
+ internal override (DiagnosticAnalyzer, CodeFixProvider) CreateDiagnosticProviderAndFixer(Workspace workspace)
+ => (null, new CSharpDeclareAsNullableCodeFixProvider());
+
+ private static readonly TestParameters s_nullableFeature = new TestParameters(parseOptions: new CSharpParseOptions(LanguageVersion.CSharp8));
+
+ [Fact]
+ public async Task FixAll()
+ {
+ await TestInRegularAndScript1Async(
+@"class Program
+{
+ static string M()
+ {
+ return {|FixAllInDocument:null|};
+ }
+ static string M2(bool b)
+ {
+ if (b)
+ return null;
+ else
+ return null;
+ }
+}",
+@"class Program
+{
+ static string? M()
+ {
+ return null;
+ }
+ static string? M2(bool b)
+ {
+ if (b)
+ return null;
+ else
+ return null;
+ }
+}", parameters: s_nullableFeature);
+ }
+
+ [Fact]
+ public async Task FixReturnType()
+ {
+ await TestInRegularAndScript1Async(
+@"class Program
+{
+ static string M()
+ {
+ return [|null|];
+ }
+}",
+@"class Program
+{
+ static string? M()
+ {
+ return null;
+ }
+}", parameters: s_nullableFeature);
+ }
+
+ [Fact]
+ public async Task FixReturnType_WithTrivia()
+ {
+ await TestInRegularAndScript1Async(
+@"class Program
+{
+ static /*before*/ string /*after*/ M()
+ {
+ return [|null|];
+ }
+}",
+@"class Program
+{
+ static /*before*/ string? /*after*/ M()
+ {
+ return null;
+ }
+}", parameters: s_nullableFeature);
+ }
+
+ [Fact]
+ public async Task FixReturnType_ArrowBody()
+ {
+ await TestInRegularAndScript1Async(
+@"class Program
+{
+ static string M() => [|null|];
+}",
+@"class Program
+{
+ static string? M() => null;
+}", parameters: s_nullableFeature);
+ }
+
+ [Fact]
+ [WorkItem(26639, "https://github.com/dotnet/roslyn/issues/26639")]
+ public async Task FixReturnType_LocalFunction_ArrowBody()
+ {
+ await TestMissingInRegularAndScriptAsync(
+@"class Program
+{
+ static void M()
+ {
+ string local() => [|null|];
+ }
+}", parameters: s_nullableFeature);
+ }
+
+ [Fact]
+ [WorkItem(26639, "https://github.com/dotnet/roslyn/issues/26639")]
+ public async Task FixLocalFunctionReturnType()
+ {
+ await TestMissingInRegularAndScriptAsync(
+@"class Program
+{
+ void M()
+ {
+ string local()
+ {
+ return [|null|];
+ }
+ }
+}", parameters: s_nullableFeature);
+ }
+
+ [Fact]
+ public async Task NoFixAlreadyNullableReturnType()
+ {
+ await TestMissingInRegularAndScriptAsync(
+@"class Program
+{
+ static string? M()
+ {
+ return [|null|];
+ }
+}", parameters: s_nullableFeature);
+ }
+
+ [Fact]
+ [WorkItem(26628, "https://github.com/dotnet/roslyn/issues/26628")]
+ public async Task FixField()
+ {
+ await TestMissingInRegularAndScriptAsync(
+@"class Program
+{
+ string x = [|null|];
+}", parameters: s_nullableFeature);
+ }
+
+ [Fact]
+ public async Task FixLocalDeclaration()
+ {
+ await TestInRegularAndScript1Async(
+@"class Program
+{
+ static void M()
+ {
+ string x = [|null|];
+ }
+}",
+@"class Program
+{
+ static void M()
+ {
+ string? x = null;
+ }
+}", parameters: s_nullableFeature);
+ }
+
+ [Fact]
+ public async Task FixLocalDeclaration_WithVar()
+ {
+ await TestMissingInRegularAndScriptAsync(
+@"class Program
+{
+ static void M()
+ {
+ var x = [|null|];
+ }
+}", parameters: s_nullableFeature);
+ }
+
+ [Fact]
+ public async Task NoFixMultiDeclaration()
+ {
+ await TestMissingInRegularAndScriptAsync(
+@"class Program
+{
+ static void M()
+ {
+ string x = [|null|], y = null;
+ }
+}", parameters: s_nullableFeature);
+ }
+
+ [Fact]
+ [WorkItem(26628, "https://github.com/dotnet/roslyn/issues/26628")]
+ public async Task FixPropertyDeclaration()
+ {
+ await TestMissingInRegularAndScriptAsync(
+@"class Program
+{
+ string x { get; set; } = [|null|];
+}", parameters: s_nullableFeature);
+ }
+
+ [Fact]
+ public async Task FixPropertyDeclaration_WithReturnNull()
+ {
+ await TestInRegularAndScript1Async(
+@"class Program
+{
+ string x { get { return [|null|]; } }
+}",
+@"class Program
+{
+ string? x { get { return null; } }
+}", parameters: s_nullableFeature);
+ }
+
+ [Fact]
+ public async Task FixPropertyDeclaration_ArrowBody()
+ {
+ await TestInRegularAndScript1Async(
+@"class Program
+{
+ string x => [|null|];
+}",
+@"class Program
+{
+ string? x => null;
+}", parameters: s_nullableFeature);
+ }
+
+ [Fact]
+ [WorkItem(26626, "https://github.com/dotnet/roslyn/issues/26626")]
+ public async Task FixOptionalParameter()
+ {
+ await TestMissingInRegularAndScriptAsync(
+@"class Program
+{
+ static void M(string x = [|null|]) { }
+}", parameters: s_nullableFeature);
+ }
+ }
+}
diff --git a/src/Features/CSharp/Portable/CSharpFeaturesResources.Designer.cs b/src/Features/CSharp/Portable/CSharpFeaturesResources.Designer.cs
index 881a84f93d3b272b3b85815c7bad9e8ef4ea61bc..45ceef98ee92d82bf87fc4b5927a7031988a4d7e 100644
--- a/src/Features/CSharp/Portable/CSharpFeaturesResources.Designer.cs
+++ b/src/Features/CSharp/Portable/CSharpFeaturesResources.Designer.cs
@@ -314,6 +314,15 @@ internal class CSharpFeaturesResources {
}
}
+ ///
+ /// Looks up a localized string similar to Declare as nullable.
+ ///
+ internal static string Declare_as_nullable {
+ get {
+ return ResourceManager.GetString("Declare_as_nullable", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to deconstruction.
///
diff --git a/src/Features/CSharp/Portable/CSharpFeaturesResources.resx b/src/Features/CSharp/Portable/CSharpFeaturesResources.resx
index dc26cb8b8afb6015bec020922f4a33121f564d58..8f5cd39808120c7ceb6db648c4025c47d55ee48b 100644
--- a/src/Features/CSharp/Portable/CSharpFeaturesResources.resx
+++ b/src/Features/CSharp/Portable/CSharpFeaturesResources.resx
@@ -162,6 +162,9 @@
Remove Unnecessary Cast
+
+ Declare as nullable
+
Cast is redundant
diff --git a/src/Features/CSharp/Portable/CodeFixes/Nullable/CSharpDeclareAsNullableCodeFixProvider.cs b/src/Features/CSharp/Portable/CodeFixes/Nullable/CSharpDeclareAsNullableCodeFixProvider.cs
new file mode 100644
index 0000000000000000000000000000000000000000..36ab46636f470d8766c17c4c2fddbf991ab7dbdc
--- /dev/null
+++ b/src/Features/CSharp/Portable/CodeFixes/Nullable/CSharpDeclareAsNullableCodeFixProvider.cs
@@ -0,0 +1,155 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp.Extensions;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Editing;
+using Microsoft.CodeAnalysis.PooledObjects;
+using Microsoft.CodeAnalysis.Shared.Extensions;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.CSharp.CodeFixes.DeclareAsNullable
+{
+ [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.DeclareAsNullable), Shared]
+ internal class CSharpDeclareAsNullableCodeFixProvider : SyntaxEditorBasedCodeFixProvider
+ {
+ // warning CS8625: Cannot convert null literal to non-nullable reference or unconstrained type parameter.
+ // warning CS8600: Converting null literal or possible null value to non-nullable type.
+ public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("CS8625", "CS8600");
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var diagnostic = context.Diagnostics.First();
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true);
+
+ var declarationTypeToFix = TryGetDeclarationTypeToFix(node);
+ if (declarationTypeToFix == null)
+ {
+ return;
+ }
+
+ context.RegisterCodeFix(new MyCodeAction(
+ c => FixAsync(context.Document, diagnostic, c)),
+ context.Diagnostics);
+ }
+
+ protected override Task FixAllAsync(
+ Document document, ImmutableArray diagnostics,
+ SyntaxEditor editor, CancellationToken cancellationToken)
+ {
+ var root = editor.OriginalRoot;
+
+ // a method can have multiple `return null;` statements, but we should only fix its return type once
+ var alreadyHandled = PooledHashSet.GetInstance();
+
+ foreach (var diagnostic in diagnostics)
+ {
+ var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken);
+ MakeDeclarationNullable(document, editor, node, alreadyHandled);
+ }
+
+ alreadyHandled.Free();
+ return Task.CompletedTask;
+ }
+
+ private static void MakeDeclarationNullable(Document document, SyntaxEditor editor, SyntaxNode node, HashSet alreadyHandled)
+ {
+ var declarationTypeToFix = TryGetDeclarationTypeToFix(node);
+ if (declarationTypeToFix != null && alreadyHandled.Add(declarationTypeToFix))
+ {
+ var fixedDeclaration = SyntaxFactory.NullableType(declarationTypeToFix.WithoutTrivia()).WithTriviaFrom(declarationTypeToFix);
+ editor.ReplaceNode(declarationTypeToFix, fixedDeclaration);
+ }
+ }
+
+ private static TypeSyntax TryGetDeclarationTypeToFix(SyntaxNode node)
+ {
+ if (!node.IsKind(SyntaxKind.NullLiteralExpression))
+ {
+ return null;
+ }
+
+ if (node.IsParentKind(SyntaxKind.ReturnStatement))
+ {
+ var containingMember = node.GetAncestors().FirstOrDefault(a => a.IsKind(
+ SyntaxKind.MethodDeclaration, SyntaxKind.PropertyDeclaration, SyntaxKind.ParenthesizedLambdaExpression, SyntaxKind.SimpleLambdaExpression,
+ SyntaxKind.LocalFunctionStatement, SyntaxKind.AnonymousMethodExpression, SyntaxKind.ConstructorDeclaration, SyntaxKind.DestructorDeclaration,
+ SyntaxKind.OperatorDeclaration, SyntaxKind.IndexerDeclaration, SyntaxKind.EventDeclaration));
+
+ if (containingMember == null)
+ {
+ return null;
+ }
+
+ switch (containingMember)
+ {
+ case MethodDeclarationSyntax method:
+ // string M() { return null; }
+ return method.ReturnType;
+
+ case PropertyDeclarationSyntax property:
+ // string x { get { return null; } }
+ return property.Type;
+
+ default:
+ return null;
+ }
+ }
+
+ // string x = null;
+ if (node.Parent?.Parent?.IsParentKind(SyntaxKind.VariableDeclaration) == true)
+ {
+ var variableDeclaration = (VariableDeclarationSyntax)node.Parent.Parent.Parent;
+ if (variableDeclaration.Variables.Count != 1)
+ {
+ // string x = null, y = null;
+ return null;
+ }
+
+ return variableDeclaration.Type;
+ }
+
+ // string x { get; set; } = null;
+ if (node.Parent.IsParentKind(SyntaxKind.PropertyDeclaration) == true)
+ {
+ var propertyDeclaration = (PropertyDeclarationSyntax)node.Parent.Parent;
+ return propertyDeclaration.Type;
+ }
+
+ // void M(string x = null) { }
+ if (node.Parent.IsParentKind(SyntaxKind.Parameter) == true)
+ {
+ var parameter = (ParameterSyntax)node.Parent.Parent;
+ return parameter.Type;
+ }
+
+ // static string M() => null;
+ if (node.IsParentKind(SyntaxKind.ArrowExpressionClause) && node.Parent.IsParentKind(SyntaxKind.MethodDeclaration))
+ {
+ var arrowMethod = (MethodDeclarationSyntax)node.Parent.Parent;
+ return arrowMethod.ReturnType;
+ }
+
+ return null;
+ }
+
+ private class MyCodeAction : CodeAction.DocumentChangeAction
+ {
+ public MyCodeAction(Func> createChangedDocument) :
+ base(CSharpFeaturesResources.Declare_as_nullable,
+ createChangedDocument,
+ CSharpFeaturesResources.Declare_as_nullable)
+ {
+ }
+ }
+ }
+}
diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.cs.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.cs.xlf
index f8761d71a9d41367d115e079390870469e4effd4..e73a09533c0f0ca3930f7110e28cd02666ab1465 100644
--- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.cs.xlf
+++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.cs.xlf
@@ -642,6 +642,11 @@
Convert to 'for'
+
+ Declare as nullable
+ Declare as nullable
+
+