Skip to content
Merged
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0199](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0199.md)|Design|Do not use inheritdoc on types without inheritance source|⚠️|✔️|❌|
|[MA0200](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0200.md)|Usage|Do not use empty property patterns with non-nullable value types|ℹ️|✔️|✔️|
|[MA0201](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0201.md)|Usage|Do not use zero-valued enum flags in flag checks|⚠️|✔️|❌|
|[MA0202](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0202.md)|Design|Conditional compilation branches have identical code|⚠️|✔️|✔️|

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@
|[MA0199](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0199.md)|Design|Do not use inheritdoc on types without inheritance source|<span title='Warning'>⚠️</span>|✔️|❌|
|[MA0200](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0200.md)|Usage|Do not use empty property patterns with non-nullable value types|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0201](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0201.md)|Usage|Do not use zero-valued enum flags in flag checks|<span title='Warning'>⚠️</span>|✔️|❌|
|[MA0202](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0202.md)|Design|Conditional compilation branches have identical code|<span title='Warning'>⚠️</span>|✔️|✔️|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -815,6 +816,9 @@ dotnet_diagnostic.MA0200.severity = suggestion

# MA0201: Do not use zero-valued enum flags in flag checks
dotnet_diagnostic.MA0201.severity = warning

# MA0202: Conditional compilation branches have identical code
dotnet_diagnostic.MA0202.severity = warning
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1416,4 +1420,7 @@ dotnet_diagnostic.MA0200.severity = none

# MA0201: Do not use zero-valued enum flags in flag checks
dotnet_diagnostic.MA0201.severity = none

# MA0202: Conditional compilation branches have identical code
dotnet_diagnostic.MA0202.severity = none
```
36 changes: 36 additions & 0 deletions docs/Rules/MA0202.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# MA0202 - Conditional compilation branches have identical code
<!-- sources -->
Sources: [ConditionalCompilationBranchesAreIdenticalAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/ConditionalCompilationBranchesAreIdenticalAnalyzer.cs), [ConditionalCompilationBranchesAreIdenticalFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/ConditionalCompilationBranchesAreIdenticalFixer.cs)
<!-- sources -->

This rule reports duplicate code in a single `#if` / `#elif` / `#else` block.

If a branch has the same code as any previous branch in the same conditional compilation block, the directive is redundant and should be simplified.

The comparison ignores trivia (such as comments and whitespace), so only the actual code structure is considered.

## Non-compliant code

````csharp
// #elif and #else duplicate earlier branches
#if NET6_0_OR_GREATER
Console.WriteLine("sample");
#elif NET8_0_OR_GREATER
Console.WriteLine("sample");
#else
// same behavior as NET6_0_OR_GREATER
Console.WriteLine("sample");
#endif
````

## Compliant code

````csharp
#if NET9_0_OR_GREATER
Console.WriteLine("net9");
#elif NET8_0_OR_GREATER
Console.WriteLine("net8");
#else
Console.WriteLine("legacy");
#endif
````
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using System.Collections.Immutable;
using System.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace Meziantou.Analyzer.Rules;

[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public sealed class ConditionalCompilationBranchesAreIdenticalFixer : CodeFixProvider
{
private const string MergeTitle = "Merge duplicate conditional compilation branches";
private const string RemoveTitle = "Remove redundant conditional compilation block";

public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.ConditionalCompilationBranchesAreIdentical);

public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root is null)
return;

var sourceText = await context.Document.GetTextAsync(context.CancellationToken).ConfigureAwait(false);
if (!TryGetFixContext(root, sourceText, context.Span, out var fixContext))
return;

var currentBranch = fixContext.Group.Branches[fixContext.CurrentBranchIndex];
var duplicateBranch = fixContext.Group.Branches[fixContext.DuplicateBranchIndex];
if (currentBranch.Kind == ConditionalCompilationBranchesAreIdenticalCommon.BranchKind.Else &&
duplicateBranch.Kind == ConditionalCompilationBranchesAreIdenticalCommon.BranchKind.If &&
fixContext.Group.Branches.Count == 2 &&
fixContext.CurrentBranchIndex == 1)
{
context.RegisterCodeFix(
CodeAction.Create(
RemoveTitle,
ct => RemoveRedundantConditionalCompilationAsync(context.Document, context.Diagnostics[0], ct),
equivalenceKey: RemoveTitle),
context.Diagnostics);
}
else if (currentBranch.Kind == ConditionalCompilationBranchesAreIdenticalCommon.BranchKind.Elif &&
duplicateBranch.Kind is ConditionalCompilationBranchesAreIdenticalCommon.BranchKind.If or ConditionalCompilationBranchesAreIdenticalCommon.BranchKind.Elif &&
currentBranch.ConditionText is not null &&
duplicateBranch.ConditionText is not null)
{
context.RegisterCodeFix(
CodeAction.Create(
MergeTitle,
ct => MergeDuplicateConditionalCompilationBranchAsync(context.Document, context.Diagnostics[0], ct),
equivalenceKey: MergeTitle),
context.Diagnostics);
}
}

private static async Task<Document> RemoveRedundantConditionalCompilationAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
if (root is null)
return document;

var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
if (!TryGetFixContext(root, sourceText, diagnostic.Location.SourceSpan, out var fixContext))
return document;

var firstBranch = fixContext.Group.Branches[0];
var replacementText = sourceText.ToString(firstBranch.ContentSpan);
var replacementSpan = TextSpan.FromBounds(fixContext.Group.FirstIfDirective.FullSpan.Start, fixContext.Group.EndIfDirective.FullSpan.End);
var updatedText = sourceText.WithChanges(new TextChange(replacementSpan, replacementText));
return document.WithText(updatedText);
}

private static async Task<Document> MergeDuplicateConditionalCompilationBranchAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
if (root is null)
return document;

var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
if (!TryGetFixContext(root, sourceText, diagnostic.Location.SourceSpan, out var fixContext))
return document;

var currentBranch = fixContext.Group.Branches[fixContext.CurrentBranchIndex];
var duplicateBranch = fixContext.Group.Branches[fixContext.DuplicateBranchIndex];
if (currentBranch.Kind != ConditionalCompilationBranchesAreIdenticalCommon.BranchKind.Elif ||
currentBranch.ConditionText is null ||
duplicateBranch.ConditionText is null)
{
return document;
}

var mergedCondition = $"({duplicateBranch.ConditionText}) || ({currentBranch.ConditionText})";
var mergedDirective = duplicateBranch.Kind switch
{
ConditionalCompilationBranchesAreIdenticalCommon.BranchKind.If => "#if " + mergedCondition,
ConditionalCompilationBranchesAreIdenticalCommon.BranchKind.Elif => "#elif " + mergedCondition,
_ => null,
};

if (mergedDirective is null)
return document;

var removeBranchSpan = TextSpan.FromBounds(currentBranch.StartDirective.FullSpan.Start, currentBranch.NextDirective.FullSpan.Start);
var updatedText = sourceText.WithChanges(
new TextChange(duplicateBranch.StartDirective.Span, mergedDirective),
new TextChange(removeBranchSpan, string.Empty));

return document.WithText(updatedText);
}

private static bool TryGetFixContext(SyntaxNode root, SourceText sourceText, TextSpan span, out FixContext fixContext)
{
var directive = FindDirective(root, span);
if (directive is null)
{
fixContext = default;
return false;
}

if (!ConditionalCompilationBranchesAreIdenticalCommon.TryCreateBranchGroup(directive, sourceText, out var branchGroup))
{
fixContext = default;
return false;
}

var currentBranchIndex = branchGroup.GetBranchIndex(directive);
if (currentBranchIndex <= 0)
{
fixContext = default;
return false;
}

var duplicateBranchIndex = branchGroup.FindPreviousDuplicateBranchIndex(currentBranchIndex);
if (duplicateBranchIndex < 0)
{
fixContext = default;
return false;
}

fixContext = new FixContext(branchGroup, currentBranchIndex, duplicateBranchIndex);
return true;
}

private static DirectiveTriviaSyntax? FindDirective(SyntaxNode root, TextSpan span)
{
foreach (var trivia in root.DescendantTrivia(descendIntoTrivia: true))
{
if (!trivia.HasStructure || !trivia.FullSpan.Contains(span.Start))
continue;

if (trivia.GetStructure() is DirectiveTriviaSyntax directive)
return directive;
}

return null;
}

private readonly record struct FixContext(ConditionalCompilationBranchesAreIdenticalCommon.BranchGroup Group, int CurrentBranchIndex, int DuplicateBranchIndex);
}
Original file line number Diff line number Diff line change
Expand Up @@ -598,3 +598,6 @@ dotnet_diagnostic.MA0200.severity = error

# MA0201: Do not use zero-valued enum flags in flag checks
dotnet_diagnostic.MA0201.severity = error

# MA0202: Conditional compilation branches have identical code
dotnet_diagnostic.MA0202.severity = error
Original file line number Diff line number Diff line change
Expand Up @@ -598,3 +598,6 @@ dotnet_diagnostic.MA0200.severity = suggestion

# MA0201: Do not use zero-valued enum flags in flag checks
dotnet_diagnostic.MA0201.severity = suggestion

# MA0202: Conditional compilation branches have identical code
dotnet_diagnostic.MA0202.severity = suggestion
Original file line number Diff line number Diff line change
Expand Up @@ -598,3 +598,6 @@ dotnet_diagnostic.MA0200.severity = warning

# MA0201: Do not use zero-valued enum flags in flag checks
dotnet_diagnostic.MA0201.severity = warning

# MA0202: Conditional compilation branches have identical code
dotnet_diagnostic.MA0202.severity = warning
Original file line number Diff line number Diff line change
Expand Up @@ -598,3 +598,6 @@ dotnet_diagnostic.MA0200.severity = suggestion

# MA0201: Do not use zero-valued enum flags in flag checks
dotnet_diagnostic.MA0201.severity = warning

# MA0202: Conditional compilation branches have identical code
dotnet_diagnostic.MA0202.severity = warning
3 changes: 3 additions & 0 deletions src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -598,3 +598,6 @@ dotnet_diagnostic.MA0200.severity = none

# MA0201: Do not use zero-valued enum flags in flag checks
dotnet_diagnostic.MA0201.severity = none

# MA0202: Conditional compilation branches have identical code
dotnet_diagnostic.MA0202.severity = none
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ internal static class RuleIdentifiers
public const string InheritdocShouldHaveSourceOnTypes = "MA0199";
public const string DoNotUseEmptyPropertyPatternOnNonNullableValueType = "MA0200";
public const string DoNotUseZeroValuedEnumFlagsInFlagChecks = "MA0201";
public const string ConditionalCompilationBranchesAreIdentical = "MA0202";

public static string GetHelpUri(string identifier)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Collections.Immutable;
using Meziantou.Analyzer.Internals;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Meziantou.Analyzer.Rules;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ConditionalCompilationBranchesAreIdenticalAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.ConditionalCompilationBranchesAreIdentical,
title: "Conditional compilation branches have identical code",
messageFormat: "Conditional compilation branches have identical code",
RuleCategories.Design,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.ConditionalCompilationBranchesAreIdentical));

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

context.RegisterSyntaxTreeAction(AnalyzeSyntaxTree);
}

private static void AnalyzeSyntaxTree(SyntaxTreeAnalysisContext context)
{
var root = context.Tree.GetRoot(context.CancellationToken);
var sourceText = context.Tree.GetText(context.CancellationToken);
foreach (var trivia in root.DescendantTrivia(descendIntoTrivia: true))
{
if (!trivia.HasStructure)
continue;

if (trivia.GetStructure() is IfDirectiveTriviaSyntax ifDirective &&
ConditionalCompilationBranchesAreIdenticalCommon.TryCreateBranchGroup(ifDirective, sourceText, out var group))
{
AnalyzeDirectiveGroup(context, group);
}
}
}

private static void AnalyzeDirectiveGroup(SyntaxTreeAnalysisContext context, ConditionalCompilationBranchesAreIdenticalCommon.BranchGroup group)
{
Dictionary<string, DirectiveTriviaSyntax> previousBranchBySignature = new(StringComparer.Ordinal);
foreach (var branch in group.Branches)
{
if (previousBranchBySignature.ContainsKey(branch.Signature))
{
context.ReportDiagnostic(Diagnostic.Create(Rule, branch.StartDirective.GetLocation()));
}
else
{
previousBranchBySignature.Add(branch.Signature, branch.StartDirective);
}
}
}
}
Loading
Loading