Skip to content

Commit 5831eec

Browse files
committed
Added analyzer and codefix for methods with UseDelegateFromConstructor with partial keyword
1 parent 2a4a7b4 commit 5831eec

File tree

6 files changed

+190
-18
lines changed

6 files changed

+190
-18
lines changed

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/CodeFixes/ThinktectureRuntimeExtensionsCodeFixProvider.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace Thinktecture.CodeAnalysis.CodeFixes;
99
public sealed class ThinktectureRuntimeExtensionsCodeFixProvider : CodeFixProvider
1010
{
1111
private const string _MAKE_PARTIAL = "Make the type partial";
12+
private const string _MAKE_METHOD_PARTIAL = "Make the method partial";
1213
private const string _MAKE_MEMBER_PUBLIC = "Make the member public";
1314
private const string _MAKE_FIELD_READONLY = "Make the field read-only";
1415
private const string _REMOVE_PROPERTY_SETTER = "Remove property setter";
@@ -36,6 +37,7 @@ public sealed class ThinktectureRuntimeExtensionsCodeFixProvider : CodeFixProvid
3637
DiagnosticsDescriptors.ComplexValueObjectWithStringMembersNeedsDefaultEqualityComparer.Id,
3738
DiagnosticsDescriptors.ExplicitComparerWithoutEqualityComparer.Id,
3839
DiagnosticsDescriptors.ExplicitEqualityComparerWithoutComparer.Id,
40+
DiagnosticsDescriptors.MethodWithUseDelegateFromConstructorMustBePartial.Id,
3941
];
4042

4143
/// <inheritdoc />
@@ -109,6 +111,10 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
109111
{
110112
context.RegisterCodeFix(CodeAction.Create(_DEFINE_VALUE_OBJECT_COMPARER, t => AddValueObjectKeyMemberComparerAttributeAsync(context.Document, root, GetCodeFixesContext().TypeDeclaration, t), _DEFINE_VALUE_OBJECT_COMPARER), diagnostic);
111113
}
114+
else if (diagnostic.Id == DiagnosticsDescriptors.MethodWithUseDelegateFromConstructorMustBePartial.Id)
115+
{
116+
context.RegisterCodeFix(CodeAction.Create(_MAKE_METHOD_PARTIAL, _ => AddTypeModifierAsync(context.Document, root, GetCodeFixesContext().MethodDeclaration, SyntaxKind.PartialKeyword), _MAKE_METHOD_PARTIAL), diagnostic);
117+
}
112118
}
113119
}
114120

@@ -412,6 +418,9 @@ private sealed class CodeFixesContext
412418
private PropertyDeclarationSyntax? _propertyDeclaration;
413419
public PropertyDeclarationSyntax? PropertyDeclaration => _propertyDeclaration ??= GetDeclaration<PropertyDeclarationSyntax>();
414420

421+
private MethodDeclarationSyntax? _methodDeclaration;
422+
public MethodDeclarationSyntax? MethodDeclaration => _methodDeclaration ??= GetDeclaration<MethodDeclarationSyntax>();
423+
415424
public CodeFixesContext(Diagnostic diagnostic, SyntaxNode root)
416425
{
417426
_diagnostic = diagnostic;

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ public sealed class ThinktectureRuntimeExtensionsAnalyzer : DiagnosticAnalyzer
4747
DiagnosticsDescriptors.StringBasedValueObjectNeedsEqualityComparer,
4848
DiagnosticsDescriptors.ComplexValueObjectWithStringMembersNeedsDefaultEqualityComparer,
4949
DiagnosticsDescriptors.ExplicitComparerWithoutEqualityComparer,
50-
DiagnosticsDescriptors.ExplicitEqualityComparerWithoutComparer
50+
DiagnosticsDescriptors.ExplicitEqualityComparerWithoutComparer,
51+
DiagnosticsDescriptors.MethodWithUseDelegateFromConstructorMustBePartial
5152
];
5253

5354
/// <inheritdoc />
@@ -60,12 +61,50 @@ public override void Initialize(AnalysisContext context)
6061
context.RegisterOperationAction(AnalyzeValueObject, OperationKind.Attribute);
6162
context.RegisterOperationAction(AnalyzeAdHocUnion, OperationKind.Attribute);
6263
context.RegisterOperationAction(AnalyzeUnion, OperationKind.Attribute);
64+
context.RegisterOperationAction(AnalyzeMethodWithUseDelegateFromConstructor, OperationKind.Attribute);
6365

6466
context.RegisterOperationAction(AnalyzeMethodCall, OperationKind.Invocation);
6567
context.RegisterOperationAction(AnalyzeDefaultValueAssignment, OperationKind.DefaultValue);
6668
context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation);
6769
}
6870

71+
private static void AnalyzeMethodWithUseDelegateFromConstructor(OperationAnalysisContext context)
72+
{
73+
if (context.ContainingSymbol.Kind != SymbolKind.Method
74+
|| context.Operation is not IAttributeOperation { Operation: IObjectCreationOperation attrCreation }
75+
|| !attrCreation.Type.IsUseDelegateFromConstructorAttribute()
76+
|| context.ContainingSymbol is not IMethodSymbol method)
77+
{
78+
return;
79+
}
80+
81+
try
82+
{
83+
if (method.DeclaringSyntaxReferences.IsDefaultOrEmpty)
84+
return;
85+
86+
var syntaxRef = method.DeclaringSyntaxReferences.Single();
87+
88+
if (syntaxRef.GetSyntax(context.CancellationToken) is not MethodDeclarationSyntax mds)
89+
return;
90+
91+
if (!mds.IsPartial())
92+
{
93+
ReportDiagnostic(
94+
context,
95+
DiagnosticsDescriptors.MethodWithUseDelegateFromConstructorMustBePartial,
96+
mds.Identifier.GetLocation(),
97+
method.Name);
98+
}
99+
}
100+
catch (Exception ex)
101+
{
102+
context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.ErrorDuringCodeAnalysis,
103+
Location.None,
104+
method.ToDisplayString(), ex.ToString()));
105+
}
106+
}
107+
69108
private void AnalyzeObjectCreation(OperationAnalysisContext context)
70109
{
71110
var operation = (IObjectCreationOperation)context.Operation;
@@ -231,7 +270,8 @@ private static void AnalyzeSwitchMap(
231270
private static void AnalyzeSmartEnum(OperationAnalysisContext context)
232271
{
233272
if (context.ContainingSymbol.Kind != SymbolKind.NamedType
234-
|| context.Operation is not IAttributeOperation { Operation: IObjectCreationOperation attrCreation } || !attrCreation.Type.IsSmartEnumAttribute()
273+
|| context.Operation is not IAttributeOperation { Operation: IObjectCreationOperation attrCreation }
274+
|| !attrCreation.Type.IsSmartEnumAttribute()
235275
|| context.ContainingSymbol is not INamedTypeSymbol type
236276
|| type.TypeKind == TypeKind.Error)
237277
{

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiagnosticsDescriptors.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ internal static class DiagnosticsDescriptors
2727
public static readonly DiagnosticDescriptor PrimaryConstructorNotAllowed = new("TTRESG043", "Primary constructor is not allowed", "Primary constructor is not allowed in object of type '{0}'", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
2828
public static readonly DiagnosticDescriptor CustomKeyMemberImplementationNotFound = new("TTRESG044", "Custom implementation of the key member not found", $"Provide a custom implementation of the key member. Implement a field or property '{{0}}'. Use '{Constants.Attributes.Properties.KEY_MEMBER_NAME}' to change the name.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
2929
public static readonly DiagnosticDescriptor CustomKeyMemberImplementationTypeMismatch = new("TTRESG045", "Key member type mismatch", "The type of the key member '{0}' must be '{2}' instead of '{1}'", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
30-
public static readonly DiagnosticDescriptor IndexBasedSwitchAndMapMustUseNamedParameters = new("TTRESG046", "The arguments of \"Switch\" and \"Map\" must named", "Not all arguments of \"Switch/Map\" on type '{0}' are named", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
30+
public static readonly DiagnosticDescriptor IndexBasedSwitchAndMapMustUseNamedParameters = new("TTRESG046", "The arguments of 'Switch' and 'Map' must named", "Not all arguments of 'Switch/Map' on type '{0}' are named", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
3131
public static readonly DiagnosticDescriptor VariableMustBeInitializedWithNonDefaultValue = new("TTRESG047", "Variable must be initialed with non-default value", "Variable of type '{0}' must be initialized with non default value", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
3232
public static readonly DiagnosticDescriptor StringBasedValueObjectNeedsEqualityComparer = new("TTRESG048", "String-based Value Object needs equality comparer", "String-based Value Object needs equality comparer. Use ValueObjectKeyMemberEqualityComparerAttribute<TAccessor, TKey> to defined the equality comparer. Example: [ValueObjectKeyMemberEqualityComparer<ComparerAccessors.StringOrdinalIgnoreCase, string>].", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true);
3333
public static readonly DiagnosticDescriptor ComplexValueObjectWithStringMembersNeedsDefaultEqualityComparer = new("TTRESG049", "Complex Value Object with string members needs equality comparer", "Complex Value Object with string members needs equality comparer. Use ValueObjectKeyMemberEqualityComparerAttribute<TAccessor, TKey> to defined the equality comparer. Example: [ValueObjectKeyMemberEqualityComparer<ComparerAccessors.StringOrdinalIgnoreCase, string>].", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true);
34+
public static readonly DiagnosticDescriptor MethodWithUseDelegateFromConstructorMustBePartial = new("TTRESG050", "Method with 'UseDelegateFromConstructorAttribute' must be partial", "The method '{0}' with 'UseDelegateFromConstructorAttribute' must be marked as partial", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
3435

3536
public static readonly DiagnosticDescriptor ErrorDuringCodeAnalysis = new("TTRESG098", "Error during code analysis", "Error during code analysis of '{0}': '{1}'", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true);
3637
public static readonly DiagnosticDescriptor ErrorDuringGeneration = new("TTRESG099", "Error during code generation", "Error during code generation for '{0}': '{1}'", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Microsoft.CodeAnalysis.CSharp;
2+
using Microsoft.CodeAnalysis.CSharp.Syntax;
3+
4+
namespace Thinktecture;
5+
6+
internal static class MemberDeclarationSyntaxExtensions
7+
{
8+
public static bool IsPartial(this MemberDeclarationSyntax tds)
9+
{
10+
if (tds is null)
11+
throw new ArgumentNullException(nameof(tds));
12+
13+
for (var i = 0; i < tds.Modifiers.Count; i++)
14+
{
15+
if (tds.Modifiers[i].IsKind(SyntaxKind.PartialKeyword))
16+
return true;
17+
}
18+
19+
return false;
20+
}
21+
}

src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/TypeDeclarationSyntaxExtensions.cs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,9 @@
1-
using Microsoft.CodeAnalysis.CSharp;
21
using Microsoft.CodeAnalysis.CSharp.Syntax;
32

43
namespace Thinktecture;
54

65
internal static class TypeDeclarationSyntaxExtensions
76
{
8-
public static bool IsPartial(this TypeDeclarationSyntax tds)
9-
{
10-
if (tds is null)
11-
throw new ArgumentNullException(nameof(tds));
12-
13-
for (var i = 0; i < tds.Modifiers.Count; i++)
14-
{
15-
if (tds.Modifiers[i].IsKind(SyntaxKind.PartialKeyword))
16-
return true;
17-
}
18-
19-
return false;
20-
}
21-
227
public static bool IsGeneric(this TypeDeclarationSyntax tds)
238
{
249
if (tds is null)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using System.Threading.Tasks;
2+
using Verifier = Thinktecture.Runtime.Tests.Verifiers.CodeFixVerifier<Thinktecture.CodeAnalysis.Diagnostics.ThinktectureRuntimeExtensionsAnalyzer, Thinktecture.CodeAnalysis.CodeFixes.ThinktectureRuntimeExtensionsCodeFixProvider>;
3+
4+
namespace Thinktecture.Runtime.Tests.AnalyzerAndCodeFixTests;
5+
6+
// ReSharper disable InconsistentNaming
7+
public class TTRESG050_MethodWithUseDelegateFromConstructorMustBePartial
8+
{
9+
private const string _DIAGNOSTIC_ID = "TTRESG050";
10+
11+
[Fact]
12+
public async Task Should_trigger_on_non_partial_method()
13+
{
14+
var code = """
15+
using System;
16+
using Thinktecture;
17+
18+
namespace TestNamespace
19+
{
20+
[SmartEnum<int>]
21+
public partial class TestEnum
22+
{
23+
public static readonly TestEnum Item1 = default;
24+
25+
[UseDelegateFromConstructor]
26+
public void {|#0:Do|}();
27+
28+
private static void DoItem1() { }
29+
}
30+
}
31+
""";
32+
33+
var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("Do");
34+
var toIgnore = Verifier.Diagnostic("CS0501", "'TestEnum.Do()' must declare a body because it is not marked abstract, extern, or partial").WithSpan(12, 19, 12, 21).WithArguments("TestNamespace.TestEnum.Do()");
35+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UseDelegateFromConstructorAttribute).Assembly], [toIgnore, expected]);
36+
}
37+
38+
[Fact]
39+
public async Task Should_not_trigger_on_partial_method()
40+
{
41+
var code = """
42+
using System;
43+
using Thinktecture;
44+
45+
namespace TestNamespace
46+
{
47+
[SmartEnum<int>]
48+
public partial class TestEnum
49+
{
50+
public static readonly TestEnum Item1 = default;
51+
52+
[UseDelegateFromConstructor]
53+
partial void Do();
54+
55+
private static void DoItem1() { }
56+
}
57+
}
58+
""";
59+
60+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UseDelegateFromConstructorAttribute).Assembly]);
61+
}
62+
63+
[Fact]
64+
public async Task Should_work_with_methods_that_have_return_types()
65+
{
66+
var code = """
67+
using System;
68+
using Thinktecture;
69+
70+
namespace TestNamespace
71+
{
72+
[SmartEnum<int>]
73+
public partial class TestEnum
74+
{
75+
public static readonly TestEnum Item1 = default;
76+
77+
[UseDelegateFromConstructor]
78+
public string {|#0:Get|}();
79+
80+
private static string GetItem1() => "Item1";
81+
}
82+
}
83+
""";
84+
85+
var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("Get");
86+
var toIgnore = Verifier.Diagnostic("CS0501", "'TestEnum.Get()' must declare a body because it is not marked abstract, extern, or partial").WithSpan(12, 21, 12, 24).WithArguments("TestNamespace.TestEnum.Get()");
87+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UseDelegateFromConstructorAttribute).Assembly], [toIgnore, expected]);
88+
}
89+
90+
[Fact]
91+
public async Task Should_work_with_methods_that_have_parameters()
92+
{
93+
var code = """
94+
using System;
95+
using Thinktecture;
96+
97+
namespace TestNamespace
98+
{
99+
[SmartEnum<int>]
100+
public partial class TestEnum
101+
{
102+
public static readonly TestEnum Item1 = default;
103+
104+
[UseDelegateFromConstructor]
105+
public void {|#0:Do|}(int value);
106+
107+
private static void ProcessItem1(int value) { }
108+
}
109+
}
110+
""";
111+
112+
var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("Do");
113+
var toIgnore = Verifier.Diagnostic("CS0501", "'TestEnum.Do(int)' must declare a body because it is not marked abstract, extern, or partial").WithSpan(12, 19, 12, 21).WithArguments("TestNamespace.TestEnum.Do(int)");
114+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UseDelegateFromConstructorAttribute).Assembly], [toIgnore, expected]);
115+
}
116+
}

0 commit comments

Comments
 (0)