Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add analyzer "Unnecessary null coalescing" ([RCS1269](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1269)) ([#1826](https://github.com/dotnet/roslynator/pull/1826))
- Add `roslyn5.0` NuGet package flavor (`analyzers/dotnet/roslyn5.0/cs`) ([PR](https://github.com/dotnet/roslynator/pull/1787))

### Breaking
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Roslynator.CodeFixes;
using Roslynator.CSharp.Refactorings;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace Roslynator.CSharp.CodeFixes;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UnnecessaryNullCoalescingCodeFixProvider))]
[Shared]
public sealed class UnnecessaryNullCoalescingCodeFixProvider : BaseCodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds
{
get { return ImmutableArray.Create(DiagnosticIdentifiers.UnnecessaryNullCoalescing); }
}

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
SyntaxNode root = await context.GetSyntaxRootAsync().ConfigureAwait(false);

if (!TryFindFirstAncestorOrSelf(
root,
context.Span,
out SyntaxNode node,
predicate: f => f.IsKind(SyntaxKind.CoalesceExpression, SyntaxKind.CoalesceAssignmentExpression)))
{
return;
}

Diagnostic diagnostic = context.Diagnostics[0];
Document document = context.Document;

CodeAction codeAction = CodeAction.Create(
"Remove unnecessary null coalescing",
ct => RefactorAsync(document, node, ct),
GetEquivalenceKey(diagnostic));

context.RegisterCodeFix(codeAction, diagnostic);
}

private static Task<Document> RefactorAsync(Document document, SyntaxNode node, CancellationToken cancellationToken)
{
if (node is BinaryExpressionSyntax coalesceExpression)
{
return SimplifyCoalesceExpressionRefactoring.RefactorAsync(
document,
coalesceExpression,
coalesceExpression.Right,
cancellationToken);
}

var assignment = (AssignmentExpressionSyntax)node;

if (assignment.Parent is ExpressionStatementSyntax expressionStatement)
{
if (expressionStatement.IsEmbedded())
{
return document.ReplaceNodeAsync(
expressionStatement,
Block().WithTriviaFrom(expressionStatement).WithFormatterAnnotation(),
cancellationToken);
}

return document.RemoveStatementAsync(expressionStatement, cancellationToken);
}

return ReplaceAssignmentWithLeftAsync(document, assignment, cancellationToken);
}

private static Task<Document> ReplaceAssignmentWithLeftAsync(
Document document,
AssignmentExpressionSyntax assignment,
CancellationToken cancellationToken)
{
IEnumerable<SyntaxTrivia> trivia = assignment.DescendantTrivia(
TextSpan.FromBounds(assignment.OperatorToken.FullSpan.Start, assignment.Right.FullSpan.End));

ExpressionSyntax newNode = assignment.Left
.WithTrailingTrivia(trivia)
.Parenthesize()
.WithFormatterAnnotation();

return document.ReplaceNodeAsync(assignment, newNode, cancellationToken);
}
}
59 changes: 59 additions & 0 deletions src/Analyzers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7338,6 +7338,65 @@ string s = """
</Sample>
</Samples>
</Analyzer>
<Analyzer>
<Id>RCS1269</Id>
<Identifier>UnnecessaryNullCoalescing</Identifier>
<Title>Unnecessary null coalescing</Title>
<DefaultSeverity>Info</DefaultSeverity>
<IsEnabledByDefault>true</IsEnabledByDefault>
<SupportsFadeOut>true</SupportsFadeOut>
<MinLanguageVersion>8.0</MinLanguageVersion>
<Samples>
<Sample>
<Before><![CDATA[#nullable enable

class C
{
C(string[] errors)
{
Errors = errors ?? [];
}

string[] Errors { get; }
}]]></Before>
<After><![CDATA[#nullable enable

class C
{
C(string[] errors)
{
Errors = errors;
}

string[] Errors { get; }
}]]></After>
</Sample>
<Sample>
<Before><![CDATA[#nullable enable

class C
{
string _context = "";

void M(string completionContext)
{
_context ??= completionContext;
}
}]]></Before>
<After><![CDATA[#nullable enable

class C
{
string _context = "";

void M(string completionContext)
{
}
}]]></After>
</Sample>
</Samples>
<Remarks>This analyzer uses nullable flow analysis. A non-nullable field or property used with `??=` for lazy initialization should be declared as `T?`.</Remarks>
</Analyzer>
<Analyzer>
<Id>RCS9001</Id>
<Identifier>UsePatternMatching</Identifier>
Expand Down
166 changes: 166 additions & 0 deletions src/Analyzers/CSharp/Analysis/UnnecessaryNullCoalescingAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Immutable;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;

namespace Roslynator.CSharp.Analysis;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class UnnecessaryNullCoalescingAnalyzer : BaseDiagnosticAnalyzer
{
private static ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics;

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
get
{
if (_supportedDiagnostics.IsDefault)
Immutable.InterlockedInitialize(ref _supportedDiagnostics, DiagnosticRules.UnnecessaryNullCoalescing);

return _supportedDiagnostics;
}
}

public override void Initialize(AnalysisContext context)
{
base.Initialize(context);

context.RegisterCompilationStartAction(startContext =>
{
if (((CSharpCompilation)startContext.Compilation).LanguageVersion < LanguageVersion.CSharp8)
return;

startContext.RegisterSyntaxNodeAction(f => AnalyzeCoalesceExpression(f), SyntaxKind.CoalesceExpression);
startContext.RegisterSyntaxNodeAction(f => AnalyzeCoalesceAssignmentExpression(f), SyntaxKind.CoalesceAssignmentExpression);
});
}

private static void AnalyzeCoalesceExpression(SyntaxNodeAnalysisContext context)
{
var coalesceExpression = (BinaryExpressionSyntax)context.Node;

if (coalesceExpression.SpanContainsDirectives())
return;

ExpressionSyntax left = coalesceExpression.Left;
ExpressionSyntax right = coalesceExpression.Right;

if (left?.IsMissing != false || right?.IsMissing != false)
return;

if (!IsNullableAnnotationsEnabled(context))
return;

ExpressionSyntax coalesced = coalesceExpression.WalkUpParentheses();

if (coalesced.Parent is BinaryExpressionSyntax outerCoalesce
&& outerCoalesce.IsKind(SyntaxKind.CoalesceExpression)
&& outerCoalesce.Right == coalesced
&& HasNotNullReferenceFlow(outerCoalesce.Left, context.SemanticModel, context.CancellationToken))
{
return;
}

if (!IsUnnecessaryNullCoalescingLeft(left, context.SemanticModel, context.CancellationToken))
return;

ReportDiagnostic(context, coalesceExpression.SyntaxTree, coalesceExpression.OperatorToken, right);
}

private static void AnalyzeCoalesceAssignmentExpression(SyntaxNodeAnalysisContext context)
{
var assignment = (AssignmentExpressionSyntax)context.Node;

if (assignment.SpanContainsDirectives())
return;

ExpressionSyntax left = assignment.Left;
ExpressionSyntax right = assignment.Right;

if (left?.IsMissing != false || right?.IsMissing != false)
return;

if (!IsNullableAnnotationsEnabled(context))
return;

if (!IsUnnecessaryNullCoalescingLeft(left, context.SemanticModel, context.CancellationToken))
return;

ReportDiagnostic(context, assignment.SyntaxTree, assignment.OperatorToken, right);
}

private static bool IsNullableAnnotationsEnabled(SyntaxNodeAnalysisContext context)
{
return (context.SemanticModel.GetNullableContext(context.Node.SpanStart) & NullableContext.AnnotationsEnabled) != 0;
}

private static bool IsUnnecessaryNullCoalescingLeft(
ExpressionSyntax left,
SemanticModel semanticModel,
CancellationToken cancellationToken)
{
ExpressionSyntax walkedLeft = left.WalkDownParentheses();

if (IsHandledBySimplifyCoalesceExpression(walkedLeft, semanticModel, cancellationToken))
return false;

return HasNotNullReferenceFlow(left, semanticModel, cancellationToken);
}

private static bool HasNotNullReferenceFlow(
ExpressionSyntax expression,
SemanticModel semanticModel,
CancellationToken cancellationToken)
{
TypeInfo typeInfo = semanticModel.GetTypeInfo(expression, cancellationToken);

if (typeInfo.Nullability.FlowState != NullableFlowState.NotNull)
return false;

ITypeSymbol type = typeInfo.Type;

return type?.IsErrorType() == false
&& type.IsReferenceType;
}

private static bool IsHandledBySimplifyCoalesceExpression(
ExpressionSyntax left,
SemanticModel semanticModel,
CancellationToken cancellationToken)
{
switch (left.Kind())
{
case SyntaxKind.ObjectCreationExpression:
case SyntaxKind.AnonymousObjectCreationExpression:
case SyntaxKind.ArrayCreationExpression:
case SyntaxKind.ImplicitArrayCreationExpression:
case SyntaxKind.InterpolatedStringExpression:
case SyntaxKind.ThisExpression:
case SyntaxKind.StringLiteralExpression:
case SyntaxKind.TypeOfExpression:
return true;
}

Optional<object> optional = semanticModel.GetConstantValue(left, cancellationToken);

return optional.HasValue
&& optional.Value is not null;
}

private static void ReportDiagnostic(
SyntaxNodeAnalysisContext context,
SyntaxTree syntaxTree,
SyntaxToken operatorToken,
ExpressionSyntax right)
{
DiagnosticHelpers.ReportDiagnostic(
context,
DiagnosticRules.UnnecessaryNullCoalescing,
Location.Create(syntaxTree, TextSpan.FromBounds(operatorToken.SpanStart, right.Span.End)));
}
}
1 change: 1 addition & 0 deletions src/Common/DiagnosticIdentifiers.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,5 +280,6 @@ public static partial class DiagnosticIdentifiers
public const string UseRawStringLiteral = "RCS1266";
public const string UseStringInterpolationInsteadOfStringConcat = "RCS1267";
public const string SimplifyNumericComparison = "RCS1268";
public const string UnnecessaryNullCoalescing = "RCS1269";
}
}
12 changes: 12 additions & 0 deletions src/Common/DiagnosticRules.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3323,5 +3323,17 @@ public static partial class DiagnosticRules
helpLinkUri: DiagnosticIdentifiers.SimplifyNumericComparison,
customTags: []);

/// <summary>RCS1269</summary>
public static readonly DiagnosticDescriptor UnnecessaryNullCoalescing = DiagnosticDescriptorFactory.Create(
id: DiagnosticIdentifiers.UnnecessaryNullCoalescing,
title: "Unnecessary null coalescing",
messageFormat: "Unnecessary null coalescing",
category: DiagnosticCategories.Roslynator,
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: null,
helpLinkUri: DiagnosticIdentifiers.UnnecessaryNullCoalescing,
customTags: WellKnownDiagnosticTags.Unnecessary);

}
}
Loading
Loading