Skip to content

Commit 8956b1e

Browse files
Copilotthomhurst
andauthored
Fix analyzer to skip required keyword enforcement for Attribute class properties (#2981)
* Initial plan * Fix analyzer to skip required keyword enforcement for Attribute class properties Co-authored-by: thomhurst <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: thomhurst <[email protected]>
1 parent 1e1fa38 commit 8956b1e

File tree

2 files changed

+149
-1
lines changed

2 files changed

+149
-1
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
using Verifier = TUnit.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier<TUnit.Analyzers.TestDataAnalyzer>;
2+
3+
namespace TUnit.Analyzers.Tests;
4+
5+
public class AttributeClassPropertyRequiredTests
6+
{
7+
[Test]
8+
public async Task Attribute_Class_Property_Without_Required_Should_Not_Flag_Error()
9+
{
10+
await Verifier
11+
.VerifyAnalyzerAsync(
12+
"""
13+
using TUnit.Core;
14+
using System;
15+
16+
public class MyCustomAttribute : System.Attribute
17+
{
18+
[ClassDataSource<string>]
19+
public string? TestProperty { get; init; } // Should NOT trigger TUnit0043
20+
}
21+
"""
22+
);
23+
}
24+
25+
[Test]
26+
public async Task Attribute_Class_Property_With_Required_Should_Not_Flag_Error()
27+
{
28+
await Verifier
29+
.VerifyAnalyzerAsync(
30+
"""
31+
using TUnit.Core;
32+
using System;
33+
34+
public class MyCustomAttribute : System.Attribute
35+
{
36+
[ClassDataSource<string>]
37+
public required string TestProperty { get; init; } // Should also NOT trigger error
38+
}
39+
"""
40+
);
41+
}
42+
43+
[Test]
44+
public async Task Regular_Test_Class_Property_Without_Required_Should_Flag_Error()
45+
{
46+
await Verifier
47+
.VerifyAnalyzerAsync(
48+
"""
49+
using TUnit.Core;
50+
51+
public class MyTestClass
52+
{
53+
[ClassDataSource<string>]
54+
public string? {|#0:TestProperty|} { get; init; } // SHOULD trigger TUnit0043
55+
56+
[Test]
57+
public void TestMethod()
58+
{
59+
}
60+
}
61+
""",
62+
63+
Verifier.Diagnostic(Rules.PropertyRequiredNotSet)
64+
.WithLocation(0)
65+
);
66+
}
67+
68+
[Test]
69+
public async Task Regular_Test_Class_Property_With_Required_Should_Not_Flag_Error()
70+
{
71+
await Verifier
72+
.VerifyAnalyzerAsync(
73+
"""
74+
using TUnit.Core;
75+
76+
public class MyTestClass
77+
{
78+
[ClassDataSource<string>]
79+
public required string TestProperty { get; init; } // Should NOT trigger error
80+
81+
[Test]
82+
public void TestMethod()
83+
{
84+
}
85+
}
86+
"""
87+
);
88+
}
89+
90+
[Test]
91+
public async Task Indirect_Attribute_Inheritance_Should_Not_Flag_Error()
92+
{
93+
await Verifier
94+
.VerifyAnalyzerAsync(
95+
"""
96+
using TUnit.Core;
97+
using System;
98+
99+
public class BaseCustomAttribute : System.Attribute
100+
{
101+
}
102+
103+
public class MyCustomAttribute : BaseCustomAttribute
104+
{
105+
[ClassDataSource<string>]
106+
public string? TestProperty { get; init; } // Should NOT trigger TUnit0043
107+
}
108+
"""
109+
);
110+
}
111+
112+
[Test]
113+
public async Task Multiple_Data_Source_Attributes_On_Attribute_Class_Property()
114+
{
115+
await Verifier
116+
.VerifyAnalyzerAsync(
117+
"""
118+
using TUnit.Core;
119+
using System;
120+
121+
public class MyCustomAttribute : System.Attribute
122+
{
123+
[Arguments("test")]
124+
public string? TestProperty { get; init; } // Should NOT trigger TUnit0043 even with Arguments
125+
}
126+
"""
127+
);
128+
}
129+
}

TUnit.Analyzers/TestDataAnalyzer.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,15 @@ private void CheckPropertyAccessor(SymbolAnalysisContext context, IPropertySymbo
265265

266266
if (propertySymbol is { IsStatic: false, IsRequired: false })
267267
{
268-
context.ReportDiagnostic(Diagnostic.Create(Rules.PropertyRequiredNotSet, propertySymbol.Locations.FirstOrDefault()));
268+
// Skip required keyword enforcement if the property is in a class that inherits from System.Attribute
269+
if (IsInAttributeClass(propertySymbol.ContainingType))
270+
{
271+
// Attribute classes don't need to enforce the required keyword for injected properties
272+
}
273+
else
274+
{
275+
context.ReportDiagnostic(Diagnostic.Create(Rules.PropertyRequiredNotSet, propertySymbol.Locations.FirstOrDefault()));
276+
}
269277
}
270278

271279
if (propertySymbol is { IsStatic: true, SetMethod: null })
@@ -274,6 +282,17 @@ private void CheckPropertyAccessor(SymbolAnalysisContext context, IPropertySymbo
274282
}
275283
}
276284

285+
private static bool IsInAttributeClass(INamedTypeSymbol? typeSymbol)
286+
{
287+
if (typeSymbol is null)
288+
{
289+
return false;
290+
}
291+
292+
// Check if the type or any of its base types is System.Attribute
293+
return typeSymbol.IsOrInherits("global::System.Attribute");
294+
}
295+
277296
private ImmutableArray<ITypeSymbol> GetTypes(ImmutableArray<IParameterSymbol> parameters, IPropertySymbol? propertySymbol)
278297
{
279298
IEnumerable<ITypeSymbol?> types = parameters.Select(x => x.Type).Concat(new[] { propertySymbol?.Type }).Where(t => t != null);

0 commit comments

Comments
 (0)