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 @@ -224,6 +224,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0203](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0203.md)|Design|Do not use return tag for void method|⚠️|✔️|❌|
|[MA0204](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0204.md)|Design|Remove unnecessary partial modifier|ℹ️|✔️|✔️|
|[MA0205](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0205.md)|Style|Use exclusive or operator|ℹ️|✔️|✔️|
|[MA0206](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0206.md)|Style|Remove unnecessary braces in type declaration|ℹ️|✔️|✔️|

<!-- rules -->

Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@
|[MA0203](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0203.md)|Design|Do not use return tag for void method|<span title='Warning'>⚠️</span>|✔️|❌|
|[MA0204](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0204.md)|Design|Remove unnecessary partial modifier|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0205](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0205.md)|Style|Use exclusive or operator|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0206](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0206.md)|Style|Remove unnecessary braces in type declaration|<span title='Info'>ℹ️</span>|✔️|✔️|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down
24 changes: 24 additions & 0 deletions docs/Rules/MA0206.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# MA0206 - Remove unnecessary braces in type declaration
<!-- sources -->
Sources: [RemoveUnnecessaryBracesInTypeDeclarationAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/RemoveUnnecessaryBracesInTypeDeclarationAnalyzer.cs), [RemoveUnnecessaryBracesInTypeDeclarationFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/RemoveUnnecessaryBracesInTypeDeclarationFixer.cs)
<!-- sources -->

This rule reports empty braces in type declarations that can be replaced with a semicolon.

The rule applies to positional records and to C# 12 type declarations with primary constructors when the type body contains no members, comments, or directives.

## Non-compliant code

````csharp
public record Foo(string Value1, string Value2) { }

public class Bar(string Value1, string Value2) { }
````

## Compliant code

````csharp
public record Foo(string Value1, string Value2);

public class Bar(string Value1, string Value2);
````
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using Meziantou.Analyzer.Internals;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;

namespace Meziantou.Analyzer.Rules;

[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public sealed class RemoveUnnecessaryBracesInTypeDeclarationFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.RemoveUnnecessaryBracesInTypeDeclaration);

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

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var typeDeclaration = root?.FindNode(context.Span, getInnermostNodeForTie: true).FirstAncestorOrSelf<TypeDeclarationSyntax>();
if (typeDeclaration is null || !CanRemoveBraces(typeDeclaration))
return;

const string Title = "Remove unnecessary braces";
var codeAction = CodeAction.Create(
Title,
ct => RemoveBraces(context.Document, typeDeclaration, ct),
equivalenceKey: Title);

context.RegisterCodeFix(codeAction, context.Diagnostics);
}

private static async Task<Document> RemoveBraces(Document document, TypeDeclarationSyntax typeDeclaration, CancellationToken cancellationToken)
{
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
var semicolonToken = SyntaxFactory.Token(SyntaxKind.SemicolonToken)
.WithTrailingTrivia(typeDeclaration.CloseBraceToken.TrailingTrivia);

var newNode = typeDeclaration
.WithOpenBraceToken(default)
.WithCloseBraceToken(default)
.WithSemicolonToken(semicolonToken)
.WithAdditionalAnnotations(Formatter.Annotation);

editor.ReplaceNode(typeDeclaration, newNode);
return editor.GetChangedDocument();
}

private static bool CanRemoveBraces(TypeDeclarationSyntax typeDeclaration)
{
if (!HasParameterList(typeDeclaration))
return false;

if (typeDeclaration.Members.Count != 0)
return false;

if (typeDeclaration.OpenBraceToken.IsMissing || typeDeclaration.CloseBraceToken.IsMissing || typeDeclaration.SemicolonToken.IsKind(SyntaxKind.SemicolonToken))
return false;

return !ContainsCommentOrDirectiveInBraces(typeDeclaration);
}

private static bool HasParameterList(TypeDeclarationSyntax typeDeclaration)
{
if (typeDeclaration is RecordDeclarationSyntax { ParameterList: not null })
return true;

#if CSHARP12_OR_GREATER
if (!typeDeclaration.GetCSharpLanguageVersion().IsCSharp12OrAbove())
return false;

if (typeDeclaration is ClassDeclarationSyntax { ParameterList: not null } or StructDeclarationSyntax { ParameterList: not null })
return true;
#endif

return false;
}

private static bool ContainsCommentOrDirectiveInBraces(TypeDeclarationSyntax typeDeclaration)
{
return typeDeclaration.OpenBraceToken.TrailingTrivia
.Concat(typeDeclaration.CloseBraceToken.LeadingTrivia)
.Any(static trivia => trivia.IsDirective || trivia.IsKind(SyntaxKind.SingleLineCommentTrivia) || trivia.IsKind(SyntaxKind.MultiLineCommentTrivia));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -610,3 +610,6 @@ dotnet_diagnostic.MA0204.severity = error

# MA0205: Use exclusive or operator
dotnet_diagnostic.MA0205.severity = error

# MA0206: Remove unnecessary braces in type declaration
dotnet_diagnostic.MA0206.severity = error
Original file line number Diff line number Diff line change
Expand Up @@ -610,3 +610,6 @@ dotnet_diagnostic.MA0204.severity = suggestion

# MA0205: Use exclusive or operator
dotnet_diagnostic.MA0205.severity = suggestion

# MA0206: Remove unnecessary braces in type declaration
dotnet_diagnostic.MA0206.severity = suggestion
Original file line number Diff line number Diff line change
Expand Up @@ -610,3 +610,6 @@ dotnet_diagnostic.MA0204.severity = warning

# MA0205: Use exclusive or operator
dotnet_diagnostic.MA0205.severity = warning

# MA0206: Remove unnecessary braces in type declaration
dotnet_diagnostic.MA0206.severity = warning
Original file line number Diff line number Diff line change
Expand Up @@ -610,3 +610,6 @@ dotnet_diagnostic.MA0204.severity = suggestion

# MA0205: Use exclusive or operator
dotnet_diagnostic.MA0205.severity = suggestion

# MA0206: Remove unnecessary braces in type declaration
dotnet_diagnostic.MA0206.severity = suggestion
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 @@ -610,3 +610,6 @@ dotnet_diagnostic.MA0204.severity = none

# MA0205: Use exclusive or operator
dotnet_diagnostic.MA0205.severity = none

# MA0206: Remove unnecessary braces in type declaration
dotnet_diagnostic.MA0206.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 @@ -205,6 +205,7 @@ internal static class RuleIdentifiers
public const string DoNotUseReturnTagForVoidMethod = "MA0203";
public const string RemoveUnnecessaryPartialModifier = "MA0204";
public const string UseExclusiveOrOperator = "MA0205";
public const string RemoveUnnecessaryBracesInTypeDeclaration = "MA0206";

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

namespace Meziantou.Analyzer.Rules;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class RemoveUnnecessaryBracesInTypeDeclarationAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.RemoveUnnecessaryBracesInTypeDeclaration,
title: "Remove unnecessary braces in type declaration",
messageFormat: "Remove unnecessary braces in type declaration",
RuleCategories.Style,
DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.RemoveUnnecessaryBracesInTypeDeclaration));

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

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

context.RegisterSyntaxNodeAction(AnalyzeTypeDeclaration, SyntaxKind.RecordDeclaration);
#if CSHARP12_OR_GREATER
context.RegisterSyntaxNodeAction(AnalyzeTypeDeclaration, SyntaxKind.ClassDeclaration);
context.RegisterSyntaxNodeAction(AnalyzeTypeDeclaration, SyntaxKind.StructDeclaration);
#endif
}

private static void AnalyzeTypeDeclaration(SyntaxNodeAnalysisContext context)
{
var typeDeclaration = (TypeDeclarationSyntax)context.Node;
if (!CanRemoveBraces(typeDeclaration, context.Compilation.GetCSharpLanguageVersion()))
return;

context.ReportDiagnostic(Diagnostic.Create(Rule, typeDeclaration.OpenBraceToken.GetLocation()));
}

private static bool CanRemoveBraces(TypeDeclarationSyntax typeDeclaration, LanguageVersion languageVersion)
{
if (!HasParameterList(typeDeclaration, languageVersion))
return false;

if (typeDeclaration.Members.Count != 0)
return false;

if (typeDeclaration.OpenBraceToken.IsMissing || typeDeclaration.CloseBraceToken.IsMissing || typeDeclaration.SemicolonToken.IsKind(SyntaxKind.SemicolonToken))
return false;

return !ContainsCommentOrDirectiveInBraces(typeDeclaration);
}

private static bool HasParameterList(TypeDeclarationSyntax typeDeclaration, LanguageVersion languageVersion)
{
if (typeDeclaration is RecordDeclarationSyntax { ParameterList: not null })
return true;

#if CSHARP12_OR_GREATER
if (!languageVersion.IsCSharp12OrAbove())
return false;

if (typeDeclaration is ClassDeclarationSyntax { ParameterList: not null } or StructDeclarationSyntax { ParameterList: not null })
return true;
#endif

return false;
}

private static bool ContainsCommentOrDirectiveInBraces(TypeDeclarationSyntax typeDeclaration)
{
return typeDeclaration.OpenBraceToken.TrailingTrivia
.Concat(typeDeclaration.CloseBraceToken.LeadingTrivia)
.Any(static trivia => trivia.IsDirective || trivia.IsKind(SyntaxKind.SingleLineCommentTrivia) || trivia.IsKind(SyntaxKind.MultiLineCommentTrivia));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using Meziantou.Analyzer.Rules;
using Microsoft.CodeAnalysis.CSharp;
using TestHelper;

namespace Meziantou.Analyzer.Test.Rules;

public sealed class RemoveUnnecessaryBracesInTypeDeclarationAnalyzerTests
{
private static ProjectBuilder CreateProjectBuilder()
{
return new ProjectBuilder()
.WithAnalyzer<RemoveUnnecessaryBracesInTypeDeclarationAnalyzer>()
.WithCodeFixProvider<RemoveUnnecessaryBracesInTypeDeclarationFixer>();
}

[Fact]
public async Task PositionalRecord_WithEmptyBraces()
{
await CreateProjectBuilder()
.WithLanguageVersion(LanguageVersion.CSharp9)
.WithSourceCode("""
public record Foo(string Value1, string Value2) [|{|]}
""")
.ValidateAsync();
}

[Fact]
public async Task PositionalRecord_CodeFix()
{
await CreateProjectBuilder()
.WithLanguageVersion(LanguageVersion.CSharp9)
.WithSourceCode("""
public record Foo(string Value1, string Value2) [|{|]}
""")
.ShouldFixCodeWith("""
public record Foo(string Value1, string Value2);
""")
.ValidateAsync();
}

[Fact]
public async Task PositionalRecord_WithSemicolon()
{
await CreateProjectBuilder()
.WithLanguageVersion(LanguageVersion.CSharp9)
.WithSourceCode("""
public record Foo(string Value1, string Value2);
""")
.ValidateAsync();
}

[Fact]
public async Task PositionalRecord_WithMember()
{
await CreateProjectBuilder()
.WithLanguageVersion(LanguageVersion.CSharp9)
.WithSourceCode("""
public record Foo(string Value1, string Value2)
{
public string Value3 { get; init; } = "";
}
""")
.ValidateAsync();
}

[Fact]
public async Task PositionalRecord_WithComment()
{
await CreateProjectBuilder()
.WithLanguageVersion(LanguageVersion.CSharp9)
.WithSourceCode("""
public record Foo(string Value1, string Value2)
{
// Keep this comment
}
""")
.ValidateAsync();
}

[Fact]
public async Task RecordWithoutParameterList()
{
await CreateProjectBuilder()
.WithLanguageVersion(LanguageVersion.CSharp9)
.WithSourceCode("""
public record Foo
{
}
""")
.ValidateAsync();
}

#if CSHARP12_OR_GREATER
[Fact]
public async Task ClassPrimaryConstructor_WithEmptyBraces()
{
await CreateProjectBuilder()
.WithLanguageVersion(LanguageVersion.CSharp12)
.WithSourceCode("""
public class Foo(string Value1, string Value2) [|{|]}
""")
.ValidateAsync();
}

[Fact]
public async Task ClassPrimaryConstructor_CodeFix()
{
await CreateProjectBuilder()
.WithLanguageVersion(LanguageVersion.CSharp12)
.WithSourceCode("""
public class Foo(string Value1, string Value2) [|{|]}
""")
.ShouldFixCodeWith("""
public class Foo(string Value1, string Value2);
""")
.ValidateAsync();
}

[Fact]
public async Task StructPrimaryConstructor_CodeFix()
{
await CreateProjectBuilder()
.WithLanguageVersion(LanguageVersion.CSharp12)
.WithSourceCode("""
public struct Foo(string Value1, string Value2) [|{|]}
""")
.ShouldFixCodeWith("""
public struct Foo(string Value1, string Value2);
""")
.ValidateAsync();
}
#endif
}