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
49 changes: 47 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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

Expand Down
59 changes: 59 additions & 0 deletions src/DataverseAnalyzer/ObjectInitializationAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -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<DiagnosticDescriptor> 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);
}
}
6 changes: 6 additions & 0 deletions src/DataverseAnalyzer/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/DataverseAnalyzer/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,13 @@
<data name="CT0002_Description" xml:space="preserve">
<value>Enum properties should be assigned enum values rather than literal numeric values to improve code readability and maintainability.</value>
</data>
<data name="CT0003_Title" xml:space="preserve">
<value>Remove empty parentheses from object initialization</value>
</data>
<data name="CT0003_MessageFormat" xml:space="preserve">
<value>Object initialization should not use empty parentheses. Remove '()' from 'new {0}()' when using object initializer</value>
</data>
<data name="CT0003_Description" xml:space="preserve">
<value>Object initialization with empty parentheses followed by an initializer is unnecessary and should be avoided for cleaner code.</value>
</data>
</root>
205 changes: 205 additions & 0 deletions tests/DataverseAnalyzer.Tests/ObjectInitializationAnalyzerTests.cs
Original file line number Diff line number Diff line change
@@ -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<string>()
{
"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<Diagnostic[]> 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<DiagnosticAnalyzer>(analyzer));

var diagnostics = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
return diagnostics.Where(d => d.Id == "CT0003").ToArray();
}
}