diff --git a/ChangeLog.md b/ChangeLog.md index 472c375eb9..885098b015 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix analyzer [RCS0049](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS0049) ([PR](https://github.com/dotnet/roslynator/pull/1386)) - Fix analyzer [RCS1159](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1159) ([PR](https://github.com/dotnet/roslynator/pull/1390)) +- Fix code fix for [CS8600](https://josefpihrt.github.io/docs/roslynator/fixes/CS8600) changing the wrong type when casts or `var` are involved ([PR](https://github.com/dotnet/roslynator/pull/1393) by @jroessel) ## [4.10.0] - 2024-01-24 diff --git a/src/CodeFixes/CSharp/CodeFixes/DeclareAsNullableCodeFixProvider.cs b/src/CodeFixes/CSharp/CodeFixes/DeclareAsNullableCodeFixProvider.cs index 2339f5968e..6dd1104900 100644 --- a/src/CodeFixes/CSharp/CodeFixes/DeclareAsNullableCodeFixProvider.cs +++ b/src/CodeFixes/CSharp/CodeFixes/DeclareAsNullableCodeFixProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Composition; +using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; @@ -30,7 +31,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (!IsEnabled(diagnostic.Id, CodeFixIdentifiers.AddNullableAnnotation, context.Document, root.SyntaxTree)) return; - if (!TryFindFirstAncestorOrSelf(root, context.Span, out SyntaxNode node, predicate: f => f.IsKind(SyntaxKind.EqualsValueClause, SyntaxKind.DeclarationExpression, SyntaxKind.SimpleAssignmentExpression))) + if (!TryFindFirstAncestorOrSelf(root, context.Span, out SyntaxNode node, predicate: f => f.IsKind(SyntaxKind.EqualsValueClause, SyntaxKind.DeclarationExpression, SyntaxKind.SimpleAssignmentExpression, SyntaxKind.CastExpression))) return; if (node is EqualsValueClauseSyntax equalsValueClause) @@ -67,6 +68,10 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) } } } + else if (node is CastExpressionSyntax castExpression) + { + TryRegisterCodeFixForCast(context, diagnostic, castExpression.Type); + } } private static void TryRegisterCodeFix(CodeFixContext context, Diagnostic diagnostic, TypeSyntax type) @@ -85,4 +90,46 @@ private static void TryRegisterCodeFix(CodeFixContext context, Diagnostic diagno context.RegisterCodeFix(codeAction, diagnostic); } + + private static void TryRegisterCodeFixForCast(CodeFixContext context, Diagnostic diagnostic, TypeSyntax type) + { + if (type.IsKind(SyntaxKind.NullableType)) + return; + + CodeAction codeAction = CodeAction.Create( + "Declare as nullable", + async ct => + { + NullableTypeSyntax newType = SyntaxFactory.NullableType(type.WithoutTrivia()).WithTriviaFrom(type); + + // This could be in a variable declaration whose type we also may have to change + if (type.Parent?.Parent is EqualsValueClauseSyntax + { + Parent: VariableDeclaratorSyntax + { + Parent: VariableDeclarationSyntax + { + Variables.Count: 1, + Type: { IsVar: false } declarationType + } variableDeclaration + } + } + && !declarationType.IsKind(SyntaxKind.NullableType)) + { + NullableTypeSyntax newDeclarationType = SyntaxFactory.NullableType(declarationType.WithoutTrivia()).WithTriviaFrom(declarationType); + VariableDeclarationSyntax newVariableDeclaration = variableDeclaration + .ReplaceNode(type, newType) + .WithType(newDeclarationType); + + return await context.Document.ReplaceNodeAsync(variableDeclaration, newVariableDeclaration, ct).ConfigureAwait(false); + } + else + { + return await context.Document.ReplaceNodeAsync(type, newType, ct).ConfigureAwait(false); + } + }, + GetEquivalenceKey(diagnostic)); + + context.RegisterCodeFix(codeAction, diagnostic); + } } diff --git a/src/Tests/CodeFixes.Tests/CS8600ConvertingNullLiteralOrPossibleNullValueToNonNullableTypeTests.cs b/src/Tests/CodeFixes.Tests/CS8600ConvertingNullLiteralOrPossibleNullValueToNonNullableTypeTests.cs index f2c592a2e3..5022c012c8 100644 --- a/src/Tests/CodeFixes.Tests/CS8600ConvertingNullLiteralOrPossibleNullValueToNonNullableTypeTests.cs +++ b/src/Tests/CodeFixes.Tests/CS8600ConvertingNullLiteralOrPossibleNullValueToNonNullableTypeTests.cs @@ -42,6 +42,40 @@ void M() ", equivalenceKey: EquivalenceKey.Create(DiagnosticId)); } + [Fact, Trait(Traits.CodeFix, CompilerDiagnosticIdentifiers.CS8600_ConvertingNullLiteralOrPossibleNullValueToNonNullableType)] + public async Task Test_LocalDeclarationWithCast() + { + await VerifyFixAsync(@" +using System; +#nullable enable + +public class C +{ + private object? Get() => null; + + void M() + { + var s = (string) Get(); + string s2 = (string) Get(); + } +} +", @" +using System; +#nullable enable + +public class C +{ + private object? Get() => null; + + void M() + { + var s = (string?) Get(); + string? s2 = (string?) Get(); + } +} +", equivalenceKey: EquivalenceKey.Create(DiagnosticId)); + } + [Fact, Trait(Traits.CodeFix, CompilerDiagnosticIdentifiers.CS8600_ConvertingNullLiteralOrPossibleNullValueToNonNullableType)] public async Task Test_DeclarationExpression() {