Skip to content

Commit 8eb7a4e

Browse files
Copilotmagesoe
andcommitted
Implement CT0003 rule for object initialization
Co-authored-by: magesoe <8904582+magesoe@users.noreply.github.com>
1 parent 8145b99 commit 8eb7a4e

File tree

5 files changed

+326
-2
lines changed

5 files changed

+326
-2
lines changed

README.md

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Dataverse Analyzer
22

3-
A Roslyn analyzer for .NET Core projects that enforces specific control flow braces rules.
3+
A Roslyn analyzer for .NET Core projects that enforces specific coding standards and best practices.
44

55
## Rule CT0001: Control Flow Braces Rule
66

@@ -38,9 +38,54 @@ if (condition)
3838
DoSomething();
3939
```
4040

41+
## Rule CT0002: Enum Assignment Rule
42+
43+
This analyzer prevents assigning literal values to enum properties, requiring the use of proper enum values instead.
44+
45+
### Examples
46+
47+
**Allowed**:
48+
```csharp
49+
AccountCategoryCode = AccountCategoryCode.Standard;
50+
```
51+
52+
**Not allowed**:
53+
```csharp
54+
// This will trigger CT0002
55+
AccountCategoryCode = 1;
56+
```
57+
58+
## Rule CT0003: Object Initialization Rule
59+
60+
This analyzer prevents the use of empty parentheses in object initialization when using object initializers.
61+
62+
### Examples
63+
64+
**Allowed**:
65+
```csharp
66+
var account = new Account
67+
{
68+
Name = "MoneyMan",
69+
};
70+
71+
var account = new Account(accountId)
72+
{
73+
Name = "MoneyMan",
74+
};
75+
```
76+
77+
**Not allowed**:
78+
```csharp
79+
// This will trigger CT0003
80+
var account = new Account()
81+
{
82+
Name = "MoneyMan",
83+
};
84+
```
85+
4186
## Usage
4287

43-
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.
88+
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.
4489

4590
### Building
4691

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
7+
namespace DataverseAnalyzer;
8+
9+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
10+
public sealed class ObjectInitializationAnalyzer : DiagnosticAnalyzer
11+
{
12+
public static readonly DiagnosticDescriptor Rule = new(
13+
"CT0003",
14+
Resources.CT0003_Title,
15+
Resources.CT0003_MessageFormat,
16+
"Style",
17+
DiagnosticSeverity.Warning,
18+
isEnabledByDefault: true,
19+
description: Resources.CT0003_Description);
20+
21+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
22+
23+
public override void Initialize(AnalysisContext context)
24+
{
25+
ArgumentNullException.ThrowIfNull(context);
26+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
27+
context.EnableConcurrentExecution();
28+
context.RegisterSyntaxNodeAction(AnalyzeObjectCreation, SyntaxKind.ObjectCreationExpression);
29+
}
30+
31+
private static void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context)
32+
{
33+
var objectCreation = (ObjectCreationExpressionSyntax)context.Node;
34+
35+
// Check if it has an object initializer
36+
if (objectCreation.Initializer is null)
37+
{
38+
return;
39+
}
40+
41+
// Check if it has an argument list (parentheses)
42+
if (objectCreation.ArgumentList is null)
43+
{
44+
return;
45+
}
46+
47+
// Check if the argument list is empty (no arguments between parentheses)
48+
if (objectCreation.ArgumentList.Arguments.Count > 0)
49+
{
50+
return;
51+
}
52+
53+
// Get the type name for the diagnostic message
54+
var typeName = objectCreation.Type?.ToString() ?? "object";
55+
56+
var diagnostic = Diagnostic.Create(Rule, objectCreation.ArgumentList.GetLocation(), typeName);
57+
context.ReportDiagnostic(diagnostic);
58+
}
59+
}

src/DataverseAnalyzer/Resources.Designer.cs

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/DataverseAnalyzer/Resources.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,13 @@
7979
<data name="CT0002_Description" xml:space="preserve">
8080
<value>Enum properties should be assigned enum values rather than literal numeric values to improve code readability and maintainability.</value>
8181
</data>
82+
<data name="CT0003_Title" xml:space="preserve">
83+
<value>Remove empty parentheses from object initialization</value>
84+
</data>
85+
<data name="CT0003_MessageFormat" xml:space="preserve">
86+
<value>Object initialization should not use empty parentheses. Remove '()' from 'new {0}()' when using object initializer</value>
87+
</data>
88+
<data name="CT0003_Description" xml:space="preserve">
89+
<value>Object initialization with empty parentheses followed by an initializer is unnecessary and should be avoided for cleaner code.</value>
90+
</data>
8291
</root>
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CSharp;
3+
using Microsoft.CodeAnalysis.Diagnostics;
4+
using System.Collections.Immutable;
5+
6+
namespace DataverseAnalyzer.Tests;
7+
8+
public sealed class ObjectInitializationAnalyzerTests
9+
{
10+
[Fact]
11+
public async Task ObjectCreationWithEmptyParenthesesAndInitializerShouldTrigger()
12+
{
13+
var source = """
14+
class Account
15+
{
16+
public string Name { get; set; }
17+
}
18+
19+
class TestClass
20+
{
21+
public void TestMethod()
22+
{
23+
var account = new Account()
24+
{
25+
Name = "MoneyMan",
26+
};
27+
}
28+
}
29+
""";
30+
31+
var diagnostics = await GetDiagnosticsAsync(source);
32+
Assert.Single(diagnostics);
33+
Assert.Equal("CT0003", diagnostics[0].Id);
34+
}
35+
36+
[Fact]
37+
public async Task ObjectCreationWithoutParenthesesAndInitializerShouldNotTrigger()
38+
{
39+
var source = """
40+
class Account
41+
{
42+
public string Name { get; set; }
43+
}
44+
45+
class TestClass
46+
{
47+
public void TestMethod()
48+
{
49+
var account = new Account
50+
{
51+
Name = "MoneyMan",
52+
};
53+
}
54+
}
55+
""";
56+
57+
var diagnostics = await GetDiagnosticsAsync(source);
58+
Assert.Empty(diagnostics);
59+
}
60+
61+
[Fact]
62+
public async Task ObjectCreationWithParametersAndInitializerShouldNotTrigger()
63+
{
64+
var source = """
65+
class Account
66+
{
67+
public string Name { get; set; }
68+
public Account(string id) { }
69+
}
70+
71+
class TestClass
72+
{
73+
public void TestMethod()
74+
{
75+
var accountId = "123";
76+
var account = new Account(accountId)
77+
{
78+
Name = "MoneyMan",
79+
};
80+
}
81+
}
82+
""";
83+
84+
var diagnostics = await GetDiagnosticsAsync(source);
85+
Assert.Empty(diagnostics);
86+
}
87+
88+
[Fact]
89+
public async Task ObjectCreationWithEmptyParenthesesWithoutInitializerShouldNotTrigger()
90+
{
91+
var source = """
92+
class Account
93+
{
94+
public string Name { get; set; }
95+
}
96+
97+
class TestClass
98+
{
99+
public void TestMethod()
100+
{
101+
var account = new Account();
102+
}
103+
}
104+
""";
105+
106+
var diagnostics = await GetDiagnosticsAsync(source);
107+
Assert.Empty(diagnostics);
108+
}
109+
110+
[Fact]
111+
public async Task BuiltInTypeWithEmptyParenthesesAndInitializerShouldTrigger()
112+
{
113+
var source = """
114+
using System.Collections.Generic;
115+
116+
class TestClass
117+
{
118+
public void TestMethod()
119+
{
120+
var list = new List<string>()
121+
{
122+
"item1",
123+
"item2"
124+
};
125+
}
126+
}
127+
""";
128+
129+
var diagnostics = await GetDiagnosticsAsync(source);
130+
Assert.Single(diagnostics);
131+
Assert.Equal("CT0003", diagnostics[0].Id);
132+
}
133+
134+
[Fact]
135+
public async Task AnonymousTypeCreationShouldNotTrigger()
136+
{
137+
var source = """
138+
class TestClass
139+
{
140+
public void TestMethod()
141+
{
142+
var obj = new
143+
{
144+
Name = "Test",
145+
Value = 42
146+
};
147+
}
148+
}
149+
""";
150+
151+
var diagnostics = await GetDiagnosticsAsync(source);
152+
Assert.Empty(diagnostics);
153+
}
154+
155+
[Fact]
156+
public async Task MultipleViolationsInSameMethodShouldTriggerMultiple()
157+
{
158+
var source = """
159+
class Account
160+
{
161+
public string Name { get; set; }
162+
}
163+
164+
class Person
165+
{
166+
public string FirstName { get; set; }
167+
}
168+
169+
class TestClass
170+
{
171+
public void TestMethod()
172+
{
173+
var account = new Account()
174+
{
175+
Name = "MoneyMan",
176+
};
177+
178+
var person = new Person()
179+
{
180+
FirstName = "John",
181+
};
182+
}
183+
}
184+
""";
185+
186+
var diagnostics = await GetDiagnosticsAsync(source);
187+
Assert.Equal(2, diagnostics.Length);
188+
Assert.All(diagnostics, d => Assert.Equal("CT0003", d.Id));
189+
}
190+
191+
private static async Task<Diagnostic[]> GetDiagnosticsAsync(string source)
192+
{
193+
var syntaxTree = CSharpSyntaxTree.ParseText(source);
194+
var compilation = CSharpCompilation.Create(
195+
"TestAssembly",
196+
new[] { syntaxTree },
197+
new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) });
198+
199+
var analyzer = new ObjectInitializationAnalyzer();
200+
var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create<DiagnosticAnalyzer>(analyzer));
201+
202+
var diagnostics = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
203+
return diagnostics.Where(d => d.Id == "CT0003").ToArray();
204+
}
205+
}

0 commit comments

Comments
 (0)