diff --git a/README.md b/README.md index e8fee4d..2219b89 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Dataverse Analyzer -A Roslyn analyzer for .NET Core projects that enforces specific control flow braces rules. +A Roslyn analyzer for .NET Core projects that enforces specific coding standards and best practices. ## Rule CT0001: Control Flow Braces Rule @@ -38,9 +38,54 @@ if (condition) DoSomething(); ``` +## Rule CT0002: Enum Assignment Rule + +This analyzer prevents assigning literal values to enum properties, requiring the use of proper enum values instead. + +### Examples + +✅ **Allowed**: +```csharp +AccountCategoryCode = AccountCategoryCode.Standard; +``` + +❌ **Not allowed**: +```csharp +// This will trigger CT0002 +AccountCategoryCode = 1; +``` + +## Rule CT0003: Object Initialization Rule + +This analyzer prevents the use of empty parentheses in object initialization when using object initializers. + +### Examples + +✅ **Allowed**: +```csharp +var account = new Account +{ + Name = "MoneyMan", +}; + +var account = new Account(accountId) +{ + Name = "MoneyMan", +}; +``` + +❌ **Not allowed**: +```csharp +// This will trigger CT0003 +var account = new Account() +{ + Name = "MoneyMan", +}; +``` + ## Usage -The analyzer is designed to be consumed as a project reference or NuGet package in other .NET projects. When integrated, it will automatically analyze your code and report violations of rule CT0001. +The analyzer is designed to be consumed as a project reference or NuGet package in other .NET projects. When integrated, it will automatically analyze your code and report violations of the rules. ### Building diff --git a/src/DataverseAnalyzer/ObjectInitializationAnalyzer.cs b/src/DataverseAnalyzer/ObjectInitializationAnalyzer.cs new file mode 100644 index 0000000..0d8d71e --- /dev/null +++ b/src/DataverseAnalyzer/ObjectInitializationAnalyzer.cs @@ -0,0 +1,59 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace DataverseAnalyzer; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ObjectInitializationAnalyzer : DiagnosticAnalyzer +{ + public static readonly DiagnosticDescriptor Rule = new( + "CT0003", + Resources.CT0003_Title, + Resources.CT0003_MessageFormat, + "Style", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: Resources.CT0003_Description); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + ArgumentNullException.ThrowIfNull(context); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeObjectCreation, SyntaxKind.ObjectCreationExpression); + } + + private static void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context) + { + var objectCreation = (ObjectCreationExpressionSyntax)context.Node; + + // Check if it has an object initializer + if (objectCreation.Initializer is null) + { + return; + } + + // Check if it has an argument list (parentheses) + if (objectCreation.ArgumentList is null) + { + return; + } + + // Check if the argument list is empty (no arguments between parentheses) + if (objectCreation.ArgumentList.Arguments.Count > 0) + { + return; + } + + // Get the type name for the diagnostic message + var typeName = objectCreation.Type?.ToString() ?? "object"; + + var diagnostic = Diagnostic.Create(Rule, objectCreation.ArgumentList.GetLocation(), typeName); + context.ReportDiagnostic(diagnostic); + } +} \ No newline at end of file diff --git a/src/DataverseAnalyzer/Resources.Designer.cs b/src/DataverseAnalyzer/Resources.Designer.cs index 78bd77c..9b562e4 100644 --- a/src/DataverseAnalyzer/Resources.Designer.cs +++ b/src/DataverseAnalyzer/Resources.Designer.cs @@ -21,5 +21,11 @@ internal static class Resources internal static string CT0002_Description => GetString(nameof(CT0002_Description)); + internal static string CT0003_Title => GetString(nameof(CT0003_Title)); + + internal static string CT0003_MessageFormat => GetString(nameof(CT0003_MessageFormat)); + + internal static string CT0003_Description => GetString(nameof(CT0003_Description)); + private static string GetString(string name) => ResourceManager.GetString(name, CultureInfo.InvariantCulture) ?? name; } \ No newline at end of file diff --git a/src/DataverseAnalyzer/Resources.resx b/src/DataverseAnalyzer/Resources.resx index 037663b..75cf30c 100644 --- a/src/DataverseAnalyzer/Resources.resx +++ b/src/DataverseAnalyzer/Resources.resx @@ -79,4 +79,13 @@ Enum properties should be assigned enum values rather than literal numeric values to improve code readability and maintainability. + + Remove empty parentheses from object initialization + + + Object initialization should not use empty parentheses. Remove '()' from 'new {0}()' when using object initializer + + + Object initialization with empty parentheses followed by an initializer is unnecessary and should be avoided for cleaner code. + \ No newline at end of file diff --git a/tests/DataverseAnalyzer.Tests/ObjectInitializationAnalyzerTests.cs b/tests/DataverseAnalyzer.Tests/ObjectInitializationAnalyzerTests.cs new file mode 100644 index 0000000..ff7f479 --- /dev/null +++ b/tests/DataverseAnalyzer.Tests/ObjectInitializationAnalyzerTests.cs @@ -0,0 +1,205 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; + +namespace DataverseAnalyzer.Tests; + +public sealed class ObjectInitializationAnalyzerTests +{ + [Fact] + public async Task ObjectCreationWithEmptyParenthesesAndInitializerShouldTrigger() + { + var source = """ + class Account + { + public string Name { get; set; } + } + + class TestClass + { + public void TestMethod() + { + var account = new Account() + { + Name = "MoneyMan", + }; + } + } + """; + + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Single(diagnostics); + Assert.Equal("CT0003", diagnostics[0].Id); + } + + [Fact] + public async Task ObjectCreationWithoutParenthesesAndInitializerShouldNotTrigger() + { + var source = """ + class Account + { + public string Name { get; set; } + } + + class TestClass + { + public void TestMethod() + { + var account = new Account + { + Name = "MoneyMan", + }; + } + } + """; + + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Empty(diagnostics); + } + + [Fact] + public async Task ObjectCreationWithParametersAndInitializerShouldNotTrigger() + { + var source = """ + class Account + { + public string Name { get; set; } + public Account(string id) { } + } + + class TestClass + { + public void TestMethod() + { + var accountId = "123"; + var account = new Account(accountId) + { + Name = "MoneyMan", + }; + } + } + """; + + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Empty(diagnostics); + } + + [Fact] + public async Task ObjectCreationWithEmptyParenthesesWithoutInitializerShouldNotTrigger() + { + var source = """ + class Account + { + public string Name { get; set; } + } + + class TestClass + { + public void TestMethod() + { + var account = new Account(); + } + } + """; + + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Empty(diagnostics); + } + + [Fact] + public async Task BuiltInTypeWithEmptyParenthesesAndInitializerShouldTrigger() + { + var source = """ + using System.Collections.Generic; + + class TestClass + { + public void TestMethod() + { + var list = new List() + { + "item1", + "item2" + }; + } + } + """; + + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Single(diagnostics); + Assert.Equal("CT0003", diagnostics[0].Id); + } + + [Fact] + public async Task AnonymousTypeCreationShouldNotTrigger() + { + var source = """ + class TestClass + { + public void TestMethod() + { + var obj = new + { + Name = "Test", + Value = 42 + }; + } + } + """; + + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Empty(diagnostics); + } + + [Fact] + public async Task MultipleViolationsInSameMethodShouldTriggerMultiple() + { + var source = """ + class Account + { + public string Name { get; set; } + } + + class Person + { + public string FirstName { get; set; } + } + + class TestClass + { + public void TestMethod() + { + var account = new Account() + { + Name = "MoneyMan", + }; + + var person = new Person() + { + FirstName = "John", + }; + } + } + """; + + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Equal(2, diagnostics.Length); + Assert.All(diagnostics, d => Assert.Equal("CT0003", d.Id)); + } + + private static async Task GetDiagnosticsAsync(string source) + { + var syntaxTree = CSharpSyntaxTree.ParseText(source); + var compilation = CSharpCompilation.Create( + "TestAssembly", + new[] { syntaxTree }, + new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }); + + var analyzer = new ObjectInitializationAnalyzer(); + var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer)); + + var diagnostics = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync(); + return diagnostics.Where(d => d.Id == "CT0003").ToArray(); + } +} \ No newline at end of file