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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0171](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0171.md)|Usage|Use pattern matching instead of inequality operators for discrete value|ℹ️|❌|✔️|
|[MA0172](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0172.md)|Usage|Both sides of the logical operation are identical|⚠️|❌|❌|
|[MA0173](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0173.md)|Design|Use LazyInitializer.EnsureInitialize|ℹ️|✔️|❌|
|[MA0174](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0174.md)|Style|Record should use explicit 'class' keyword|ℹ️|❌|❌|
|[MA0175](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0175.md)|Style|Record should not use explicit 'class' keyword|ℹ️|❌|❌|

<!-- rules -->

Expand Down
14 changes: 14 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@
|[MA0171](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0171.md)|Usage|Use pattern matching instead of inequality operators for discrete value|<span title='Info'>ℹ️</span>|❌|✔️|
|[MA0172](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0172.md)|Usage|Both sides of the logical operation are identical|<span title='Warning'>⚠️</span>|❌|❌|
|[MA0173](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0173.md)|Design|Use LazyInitializer.EnsureInitialize|<span title='Info'>ℹ️</span>|✔️|❌|
|[MA0174](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0174.md)|Style|Record should use explicit 'class' keyword|<span title='Info'>ℹ️</span>|❌|❌|
|[MA0175](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0175.md)|Style|Record should not use explicit 'class' keyword|<span title='Info'>ℹ️</span>|❌|❌|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -700,6 +702,12 @@ dotnet_diagnostic.MA0172.severity = none

# MA0173: Use LazyInitializer.EnsureInitialize
dotnet_diagnostic.MA0173.severity = suggestion

# MA0174: Record should use explicit 'class' keyword
dotnet_diagnostic.MA0174.severity = none

# MA0175: Record should not use explicit 'class' keyword
dotnet_diagnostic.MA0175.severity = none
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1220,4 +1228,10 @@ dotnet_diagnostic.MA0172.severity = none

# MA0173: Use LazyInitializer.EnsureInitialize
dotnet_diagnostic.MA0173.severity = none

# MA0174: Record should use explicit 'class' keyword
dotnet_diagnostic.MA0174.severity = none

# MA0175: Record should not use explicit 'class' keyword
dotnet_diagnostic.MA0175.severity = none
```
12 changes: 12 additions & 0 deletions docs/Rules/MA0174.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# MA0174 - Record should use explicit 'class' keyword

This rule suggests adding the explicit `class` keyword to record declarations that don't specify it.

```csharp
public sealed record Customer; // non-compliant
public sealed record class Customer; // compliant
```

## Related rules

- [MA0175](MA0175.md) - Record should not use explicit 'class' keyword (opposite rule)
12 changes: 12 additions & 0 deletions docs/Rules/MA0175.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# MA0175 - Record should not use explicit 'class' keyword

This rule suggests adding the explicit `class` keyword to record declarations that don't specify it.

```csharp
public sealed record class Customer; // non-compliant
public sealed record Customer; // compliant
```

## Related rules

- [MA0174](MA0174.md) - Record should use explicit 'class' keyword (opposite rule)
Original file line number Diff line number Diff line change
Expand Up @@ -517,3 +517,9 @@ dotnet_diagnostic.MA0172.severity = none

# MA0173: Use LazyInitializer.EnsureInitialize
dotnet_diagnostic.MA0173.severity = suggestion

# MA0174: Record should use explicit 'class' keyword
dotnet_diagnostic.MA0174.severity = none

# MA0175: Record should not use explicit 'class' keyword
dotnet_diagnostic.MA0175.severity = none
6 changes: 6 additions & 0 deletions src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -517,3 +517,9 @@ dotnet_diagnostic.MA0172.severity = none

# MA0173: Use LazyInitializer.EnsureInitialize
dotnet_diagnostic.MA0173.severity = none

# MA0174: Record should use explicit 'class' keyword
dotnet_diagnostic.MA0174.severity = none

# MA0175: Record should not use explicit 'class' keyword
dotnet_diagnostic.MA0175.severity = none
2 changes: 2 additions & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ internal static class RuleIdentifiers
public const string UsePatternMatchingInsteadOfHasvalue = "MA0171";
public const string BothSideOfTheConditionAreIdentical = "MA0172";
public const string UseLazyInitializerEnsureInitialize = "MA0173";
public const string RecordClassDeclarationShouldBeExplicit = "MA0174";
public const string RecordClassDeclarationShouldBeImplicit = "MA0175";

public static string GetHelpUri(string identifier)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#if CSHARP10_OR_GREATER
using System.Collections.Immutable;
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 RecordClassDeclarationShouldBeExplicitAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.RecordClassDeclarationShouldBeExplicit,
title: "Record should use explicit 'class' keyword",
messageFormat: "Record should be declared with explicit 'class' keyword",
RuleCategories.Style,
DiagnosticSeverity.Info,
isEnabledByDefault: false,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.RecordClassDeclarationShouldBeExplicit));

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

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterSyntaxNodeAction(AnalyzeRecordDeclaration, SyntaxKind.RecordDeclaration);
}

private static void AnalyzeRecordDeclaration(SyntaxNodeAnalysisContext context)
{
var recordDeclaration = (RecordDeclarationSyntax)context.Node;

// Check if this is a record without the explicit 'class' keyword
// RecordDeclarationSyntax.ClassOrStructKeyword will be null/missing for implicit record classes
// and will contain 'class' or 'struct' for explicit ones
if (recordDeclaration.ClassOrStructKeyword.IsKind(SyntaxKind.None))
{
// This is an implicit record class (no 'class' or 'struct' keyword)
// Report diagnostic on the record keyword
context.ReportDiagnostic(Rule, recordDeclaration.Keyword);
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#if CSHARP10_OR_GREATER
using System.Collections.Immutable;
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 RecordClassDeclarationShouldBeImplicitAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.RecordClassDeclarationShouldBeImplicit,
title: "Record should not use explicit 'class' keyword",
messageFormat: "Record should not be declared with explicit 'class' keyword",
RuleCategories.Style,
DiagnosticSeverity.Info,
isEnabledByDefault: false,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.RecordClassDeclarationShouldBeImplicit));

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

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterSyntaxNodeAction(AnalyzeRecordDeclaration, SyntaxKind.RecordDeclaration);
}

private static void AnalyzeRecordDeclaration(SyntaxNodeAnalysisContext context)
{
var recordDeclaration = (RecordDeclarationSyntax)context.Node;

// Check if this is a record with the explicit 'class' keyword
if (recordDeclaration.ClassOrStructKeyword.IsKind(SyntaxKind.ClassKeyword))
{
// This is an explicit record class - report diagnostic on the 'class' keyword
context.ReportDiagnostic(Rule, recordDeclaration.ClassOrStructKeyword);
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#if CSHARP10_OR_GREATER
using Meziantou.Analyzer.Rules;
using Meziantou.Analyzer.Test.Helpers;
using TestHelper;

namespace Meziantou.Analyzer.Test.Rules;

public sealed class RecordClassDeclarationShouldBeExplicitAnalyzerTests
{
private static ProjectBuilder CreateProjectBuilder()
{
return new ProjectBuilder()
.WithAnalyzer<RecordClassDeclarationShouldBeExplicitAnalyzer>()
.WithTargetFramework(TargetFramework.NetLatest);
}

[Fact]
public async Task ImplicitRecordClass_ShouldReportDiagnostic()
{

await CreateProjectBuilder()
.WithSourceCode("""
public [|record|] Target { }
""")
.ValidateAsync();
}

[Fact]
public async Task ImplicitRecordClass_WithModifiers_ShouldReportDiagnostic()
{

await CreateProjectBuilder()
.WithSourceCode("""
public sealed [|record|] Target { }
""")
.ValidateAsync();
}

[Fact]
public async Task ExplicitRecordClass_ShouldNotReportDiagnostic()
{

await CreateProjectBuilder()
.WithSourceCode("""
public record class Target { }
""")
.ValidateAsync();
}

[Fact]
public async Task ExplicitRecordStruct_ShouldNotReportDiagnostic()
{

await CreateProjectBuilder()
.WithSourceCode("""
public record struct Target { }
""")
.ValidateAsync();
}

[Fact]
public async Task RegularClass_ShouldNotReportDiagnostic()
{

await CreateProjectBuilder()
.WithSourceCode("""
public class Target { }
""")
.ValidateAsync();
}

[Fact]
public async Task RegularStruct_ShouldNotReportDiagnostic()
{

await CreateProjectBuilder()
.WithSourceCode("""
public struct Target { }
""")
.ValidateAsync();
}

[Fact]
public async Task ImplicitRecordClass_WithParameters_ShouldReportDiagnostic()
{

await CreateProjectBuilder()
.WithSourceCode("""
public [|record|] Target(int Id) { }
""")
.ValidateAsync();
}

[Fact]
public async Task ExplicitRecordClass_WithParameters_ShouldNotReportDiagnostic()
{

await CreateProjectBuilder()
.WithSourceCode("""
public record class Target(int Id) { }
""")
.ValidateAsync();
}

[Fact]
public async Task ImplicitRecordClass_Generic_ShouldReportDiagnostic()
{

await CreateProjectBuilder()
.WithSourceCode("""
public [|record|] Target<T> { }
""")
.ValidateAsync();
}

[Fact]
public async Task ImplicitRecordClass_InNamespace_ShouldReportDiagnostic()
{

await CreateProjectBuilder()
.WithSourceCode("""
namespace MyNamespace
{
public [|record|] Target { }
}
""")
.ValidateAsync();
}

[Fact]
public async Task ImplicitRecordClass_WithInheritance_ShouldReportDiagnostic()
{

await CreateProjectBuilder()
.WithSourceCode("""
public abstract [|record|] BaseRecord { }
public [|record|] Target : BaseRecord { }
""")
.ValidateAsync();
}

[Fact]
public async Task RxplicitRecordClass_WithInheritance_ShouldNotReportDiagnostic()
{

await CreateProjectBuilder()
.WithSourceCode("""
public abstract record class BaseRecord { }
public record class Target : BaseRecord { }
""")
.ValidateAsync();
}
}
#endif
Loading