From 8d3f52e4a9e87dc0762926038eb9c7a38c56b532 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 30 Aug 2025 10:56:18 +0200 Subject: [PATCH 01/16] Cleanup AbstractPropertiesCannotBeSerializedAnalyzer --- ...actPropertiesCannotBeSerializedAnalyzer.cs | 67 ++++++++++--------- .../SerializationAttributesHelper.cs | 10 +++ src/Orleans.Analyzers/SymbolHelpers.cs | 20 ++++++ ...ropertiesCannotBeSerializedAnalyzerTest.cs | 4 +- 4 files changed, 69 insertions(+), 32 deletions(-) create mode 100644 src/Orleans.Analyzers/SymbolHelpers.cs diff --git a/src/Orleans.Analyzers/AbstractPropertiesCannotBeSerializedAnalyzer.cs b/src/Orleans.Analyzers/AbstractPropertiesCannotBeSerializedAnalyzer.cs index aaf36f7355f..183887ab253 100644 --- a/src/Orleans.Analyzers/AbstractPropertiesCannotBeSerializedAnalyzer.cs +++ b/src/Orleans.Analyzers/AbstractPropertiesCannotBeSerializedAnalyzer.cs @@ -1,7 +1,6 @@ using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; using System.Collections.Immutable; namespace Orleans.Analyzers @@ -16,44 +15,52 @@ public class AbstractPropertiesCannotBeSerializedAnalyzer : DiagnosticAnalyzer internal static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(RuleId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true); - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + public override ImmutableArray SupportedDiagnostics { get; } = [Rule]; public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - context.RegisterSyntaxNodeAction(CheckSyntaxNode, SyntaxKind.ClassDeclaration, SyntaxKind.StructDeclaration); - } - - private void CheckSyntaxNode(SyntaxNodeAnalysisContext context) - { - if (context.Node is TypeDeclarationSyntax declaration && SerializationAttributesHelper.ShouldGenerateSerializer(declaration)) + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.RegisterCompilationStartAction(context => { - var analysis = SerializationAttributesHelper.AnalyzeTypeDeclaration(declaration); - foreach (var member in analysis.AnnotatedMembers) + var idAttribute = context.Compilation.GetTypeByMetadataName("Orleans.IdAttribute"); + var generateSerializerAttributeSymbol = context.Compilation.GetTypeByMetadataName("Orleans.GenerateSerializerAttribute"); + if (idAttribute is null || generateSerializerAttributeSymbol is null) { - string modifier = null; - if (member.IsAbstract()) - { - modifier = "abstract"; - } - else if (member.IsStatic()) + return; + } + + context.RegisterSymbolStartAction(context => + { + if (SerializationAttributesHelper.ShouldGenerateSerializer((INamedTypeSymbol)context.Symbol, generateSerializerAttributeSymbol)) { - modifier = "static"; + context.RegisterOperationAction(context => AnalyzeAttribute(context, idAttribute), OperationKind.Attribute); } + }, SymbolKind.NamedType); + }); + } - if (modifier is not null) - { - var location = member.GetLocation(); - if (member.TryGetAttribute(Constants.IdAttributeName, out var attribute)) - { - location = attribute.GetLocation(); - } + private static void AnalyzeAttribute(OperationAnalysisContext context, INamedTypeSymbol idAttribute) + { + var attributeOperation = (IAttributeOperation)context.Operation; + string modifier; + if (context.ContainingSymbol.IsAbstract) + { + modifier = "abstract"; + } + else if (context.ContainingSymbol.IsStatic) + { + modifier = "static"; + } + else + { + return; + } - var name = member.GetMemberNameOrDefault(); - context.ReportDiagnostic(Diagnostic.Create(Rule, location, name, modifier)); - } - } + if (attributeOperation.Operation is IObjectCreationOperation objectCreationOperation && + idAttribute.Equals(objectCreationOperation.Constructor.ContainingType, SymbolEqualityComparer.Default)) + { + context.ReportDiagnostic(Diagnostic.Create(Rule, attributeOperation.Syntax.GetLocation(), context.ContainingSymbol.Name, modifier)); } } } diff --git a/src/Orleans.Analyzers/SerializationAttributesHelper.cs b/src/Orleans.Analyzers/SerializationAttributesHelper.cs index e14c04b57fd..3583cba8393 100644 --- a/src/Orleans.Analyzers/SerializationAttributesHelper.cs +++ b/src/Orleans.Analyzers/SerializationAttributesHelper.cs @@ -19,6 +19,16 @@ public static bool ShouldGenerateSerializer(TypeDeclarationSyntax declaration) return false; } + public static bool ShouldGenerateSerializer(INamedTypeSymbol symbol, INamedTypeSymbol generateSerializerAttributeSymbol) + { + if (!symbol.IsStatic && symbol.HasAttribute(generateSerializerAttributeSymbol)) + { + return true; + } + + return false; + } + public readonly record struct TypeAnalysis { public List UnannotatedMembers { get; init; } diff --git a/src/Orleans.Analyzers/SymbolHelpers.cs b/src/Orleans.Analyzers/SymbolHelpers.cs new file mode 100644 index 00000000000..e930dd18197 --- /dev/null +++ b/src/Orleans.Analyzers/SymbolHelpers.cs @@ -0,0 +1,20 @@ +using Microsoft.CodeAnalysis; + +namespace Orleans.Analyzers +{ + internal static class SymbolHelpers + { + public static bool HasAttribute(this ISymbol symbol, INamedTypeSymbol attributeSymbol) + { + foreach (var attribute in symbol.GetAttributes()) + { + if (attributeSymbol.Equals(attribute.AttributeClass, SymbolEqualityComparer.Default)) + { + return true; + } + } + + return false; + } + } +} diff --git a/test/Analyzers.Tests/AbstractPropertiesCannotBeSerializedAnalyzerTest.cs b/test/Analyzers.Tests/AbstractPropertiesCannotBeSerializedAnalyzerTest.cs index ce1517ec412..406be36906e 100644 --- a/test/Analyzers.Tests/AbstractPropertiesCannotBeSerializedAnalyzerTest.cs +++ b/test/Analyzers.Tests/AbstractPropertiesCannotBeSerializedAnalyzerTest.cs @@ -1,4 +1,4 @@ -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; using Orleans.Analyzers; using Xunit; @@ -67,4 +67,4 @@ public abstract class D { [GenericAttribute] [Id(0)] public abstract int F { get; set; } } """); -} \ No newline at end of file +} From 291ea104019cb801a4a802fd111feb59c79b6eb7 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 30 Aug 2025 11:15:37 +0200 Subject: [PATCH 02/16] More cleanup --- .../AliasClashAttributeAnalyzer.cs | 82 +++++++++---------- src/Orleans.Analyzers/SyntaxHelpers.cs | 5 ++ 2 files changed, 42 insertions(+), 45 deletions(-) diff --git a/src/Orleans.Analyzers/AliasClashAttributeAnalyzer.cs b/src/Orleans.Analyzers/AliasClashAttributeAnalyzer.cs index f57dc7f67a9..8645b314726 100644 --- a/src/Orleans.Analyzers/AliasClashAttributeAnalyzer.cs +++ b/src/Orleans.Analyzers/AliasClashAttributeAnalyzer.cs @@ -28,36 +28,29 @@ public class AliasClashAttributeAnalyzer : DiagnosticAnalyzer helpLinkUri: null, customTags: [WellKnownDiagnosticTags.CompilationEnd]); - public override ImmutableArray SupportedDiagnostics => [Rule]; + public override ImmutableArray SupportedDiagnostics { get; } = [Rule]; public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis( - GeneratedCodeAnalysisFlags.Analyze | - GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); - context.RegisterCompilationStartAction(compilationContext => + context.RegisterCompilationStartAction(context => { var aliasMap = new ConcurrentDictionary>(); - - compilationContext.RegisterSyntaxNodeAction( - nodeContext => CollectTypeAliases(nodeContext, aliasMap), - SyntaxKind.EnumDeclaration, - SyntaxKind.ClassDeclaration, - SyntaxKind.StructDeclaration, - SyntaxKind.RecordDeclaration, - SyntaxKind.InterfaceDeclaration, - SyntaxKind.RecordStructDeclaration); + var aliasAttributeSymbol = context.Compilation.GetTypeByMetadataName("Orleans.AliasAttribute"); + context.RegisterSymbolAction( + context => CollectTypeAliases(context, aliasMap, aliasAttributeSymbol), + SymbolKind.NamedType); // We can immediately check duplicate method‐aliases in grain interfaces. - compilationContext.RegisterSyntaxNodeAction( - nodeContext => CheckMethodAliases(nodeContext, aliasMap), - SyntaxKind.InterfaceDeclaration); + context.RegisterSymbolAction( + context => CheckMethodAliases(context, aliasMap, aliasAttributeSymbol), + SymbolKind.NamedType); // Only at the very end, we do one single‐threaded scan for type‐alias clashes only. - compilationContext.RegisterCompilationEndAction(endContext => + context.RegisterCompilationEndAction(context => { foreach (var kvp in aliasMap) { @@ -77,7 +70,7 @@ public override void Initialize(AnalysisContext context) var firstType = distinctTypes[0]; foreach (var info in infos.Where(i => i.TypeName != firstType)) { - endContext.ReportDiagnostic(Diagnostic.Create(Rule, info.Location, alias, firstType)); + context.ReportDiagnostic(Diagnostic.Create(Rule, info.Location, alias, firstType)); } } }); @@ -85,32 +78,27 @@ public override void Initialize(AnalysisContext context) } private static void CollectTypeAliases( - SyntaxNodeAnalysisContext context, - ConcurrentDictionary> aliasMap) + SymbolAnalysisContext context, + ConcurrentDictionary> aliasMap, + INamedTypeSymbol aliasAttributeSymbol) { - if (context.Node is not BaseTypeDeclarationSyntax decl) - return; - - var semanticModel = context.SemanticModel; - var typeSymbol = semanticModel.GetDeclaredSymbol(decl); - if (typeSymbol == null) - { - return; - } - - if (decl is InterfaceDeclarationSyntax iface && !iface.ExtendsGrainInterface(semanticModel)) + var typeSymbol = (INamedTypeSymbol)context.Symbol; + + if (typeSymbol.TypeKind == TypeKind.Interface && !typeSymbol.ExtendsGrainInterface()) { return; // Skip interfaces that dont extend IAddressable } - var attrs = decl.AttributeLists.GetAttributeSyntaxes(Constants.AliasAttributeName); - foreach (var attr in attrs) + foreach (var attr in typeSymbol.GetAttributes()) { - var alias = attr.GetArgumentValue(semanticModel); + if (!aliasAttributeSymbol.Equals(attr.AttributeClass, SymbolEqualityComparer.Default)) + continue; + + var alias = attr.ConstructorArguments.FirstOrDefault().Value as string; if (string.IsNullOrEmpty(alias)) continue; - var info = new TypeAliasInfo(typeSymbol.ToDisplayString(), attr.GetLocation()); + var info = new TypeAliasInfo(typeSymbol.ToDisplayString(), attr.ApplicationSyntaxReference.GetSyntax().GetLocation()); aliasMap.AddOrUpdate( key: alias, @@ -124,26 +112,30 @@ private static void CollectTypeAliases( } private static void CheckMethodAliases( - SyntaxNodeAnalysisContext context, - ConcurrentDictionary> aliasMap) + SymbolAnalysisContext context, + ConcurrentDictionary> aliasMap, + INamedTypeSymbol aliasAttributeSymbol) { - if (context.Node is not InterfaceDeclarationSyntax interfaceDecl) + if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Interface } interfaceSymbol) { return; } - var semanticModel = context.SemanticModel; var methodBags = new List<(string Alias, Location Location)>(); - foreach (var method in interfaceDecl.Members.OfType()) + foreach (var method in interfaceSymbol.GetMembers().OfType()) { - var methodAttrs = method.AttributeLists.GetAttributeSyntaxes(Constants.AliasAttributeName); - foreach (var attr in methodAttrs) + foreach (var attr in method.GetAttributes()) { - var alias = attr.GetArgumentValue(semanticModel); + if (!aliasAttributeSymbol.Equals(attr.AttributeClass, SymbolEqualityComparer.Default)) + { + continue; + } + + var alias = attr.ConstructorArguments.FirstOrDefault().Value as string; if (!string.IsNullOrEmpty(alias)) { - methodBags.Add((alias, attr.GetLocation())); + methodBags.Add((alias, attr.ApplicationSyntaxReference.GetSyntax().GetLocation())); } } } diff --git a/src/Orleans.Analyzers/SyntaxHelpers.cs b/src/Orleans.Analyzers/SyntaxHelpers.cs index 7a43b1ea59f..1c1de525171 100644 --- a/src/Orleans.Analyzers/SyntaxHelpers.cs +++ b/src/Orleans.Analyzers/SyntaxHelpers.cs @@ -155,6 +155,11 @@ public static bool ExtendsGrainInterface(this InterfaceDeclarationSyntax interfa } var symbol = semanticModel.GetDeclaredSymbol(interfaceDeclaration); + return symbol.ExtendsGrainInterface(); + } + + public static bool ExtendsGrainInterface(this INamedTypeSymbol symbol) + { if (symbol is null || symbol.TypeKind != TypeKind.Interface) { return false; From db3a46f61ddd3debba4fbeb45a7af683b4ae7f5d Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 30 Aug 2025 11:17:51 +0200 Subject: [PATCH 03/16] Cleanup AlwaysInterleaveDiagnosticAnalyzer --- .../AlwaysInterleaveDiagnosticAnalyzer.cs | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Orleans.Analyzers/AlwaysInterleaveDiagnosticAnalyzer.cs b/src/Orleans.Analyzers/AlwaysInterleaveDiagnosticAnalyzer.cs index 0421b0fad49..60b9ac7ccca 100644 --- a/src/Orleans.Analyzers/AlwaysInterleaveDiagnosticAnalyzer.cs +++ b/src/Orleans.Analyzers/AlwaysInterleaveDiagnosticAnalyzer.cs @@ -24,25 +24,27 @@ public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); - context.RegisterSyntaxNodeAction( - AnalyzeSyntax, - SyntaxKind.MethodDeclaration); + context.RegisterCompilationStartAction(context => + { + var alwaysInterleaveAttributeSymbol = context.Compilation.GetTypeByMetadataName(AlwaysInterleaveAttributeName); + if (alwaysInterleaveAttributeSymbol is not null) + { + context.RegisterSymbolAction(context => AnalyzeMethod(context, alwaysInterleaveAttributeSymbol), SymbolKind.Method); + } + }); } - private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + private static void AnalyzeMethod(SymbolAnalysisContext context, INamedTypeSymbol alwaysInterleaveAttribute) { - var alwaysInterleaveAttribute = context.Compilation.GetTypeByMetadataName(AlwaysInterleaveAttributeName); - - var syntax = (MethodDeclarationSyntax)context.Node; - var symbol = context.SemanticModel.GetDeclaredSymbol(syntax, context.CancellationToken); + var methodSymbol = (IMethodSymbol)context.Symbol; - if (symbol.ContainingType.TypeKind == TypeKind.Interface) + if (methodSymbol.ContainingType.TypeKind == TypeKind.Interface) { // TODO: Check that interface inherits from IGrain return; } - foreach (var attribute in symbol.GetAttributes()) + foreach (var attribute in methodSymbol.GetAttributes()) { if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, alwaysInterleaveAttribute)) { From bd82954990d2bb300b42bfd976c77d6ce636bca0 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 30 Aug 2025 11:25:40 +0200 Subject: [PATCH 04/16] Cleanup AtMostOneOrleansConstructorAnalyzer --- .../AtMostOneOrleansConstructorAnalyzer.cs | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/Orleans.Analyzers/AtMostOneOrleansConstructorAnalyzer.cs b/src/Orleans.Analyzers/AtMostOneOrleansConstructorAnalyzer.cs index 45b1d111d1f..61cd22286bb 100644 --- a/src/Orleans.Analyzers/AtMostOneOrleansConstructorAnalyzer.cs +++ b/src/Orleans.Analyzers/AtMostOneOrleansConstructorAnalyzer.cs @@ -1,8 +1,6 @@ +using System.Collections.Immutable; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -using System.Collections.Immutable; namespace Orleans.Analyzers { @@ -21,18 +19,35 @@ public class AtMostOneOrleansConstructorAnalyzer : DiagnosticAnalyzer public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - context.RegisterSyntaxNodeAction(CheckSyntaxNode, SyntaxKind.ClassDeclaration, SyntaxKind.StructDeclaration); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.RegisterCompilationStartAction(context => + { + var generateSerializerAttributeSymbol = context.Compilation.GetTypeByMetadataName("Orleans.GenerateSerializerAttribute"); + if (generateSerializerAttributeSymbol is not null) + { + context.RegisterSymbolAction(context => AnalyzeNamedType(context, generateSerializerAttributeSymbol), SymbolKind.NamedType); + } + }); } - private void CheckSyntaxNode(SyntaxNodeAnalysisContext context) + private void AnalyzeNamedType(SymbolAnalysisContext context, INamedTypeSymbol generateSerializerAttributeSymbol) { - if (context.Node is TypeDeclarationSyntax declaration && SerializationAttributesHelper.ShouldGenerateSerializer(declaration)) + var symbol = (INamedTypeSymbol)context.Symbol; + if (SerializationAttributesHelper.ShouldGenerateSerializer(symbol, generateSerializerAttributeSymbol)) { - var analysis = SerializationAttributesHelper.AnalyzeTypeDeclaration(declaration); - if (analysis.AnnotatedConstructorCount > 1) + var foundAttribute = false; + foreach (var constructor in symbol.Constructors) { - context.ReportDiagnostic(Diagnostic.Create(Rule, declaration.GetLocation())); + if (constructor.HasAttribute(generateSerializerAttributeSymbol)) + { + if (foundAttribute) + { + context.ReportDiagnostic(Diagnostic.Create(Rule, symbol.Locations[0])); + return; + } + + foundAttribute = true; + } } } } From 7bc6c830b6e3bd9d4549c1d1606b6a430f9eb635 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 30 Aug 2025 12:04:24 +0200 Subject: [PATCH 05/16] Cleanup GenerateAliasAttributesAnalyzer --- .../GenerateAliasAttributesAnalyzer.cs | 53 +++++++++++-------- src/Orleans.Analyzers/SymbolHelpers.cs | 16 ++++++ src/Orleans.Analyzers/SyntaxHelpers.cs | 46 ---------------- 3 files changed, 48 insertions(+), 67 deletions(-) diff --git a/src/Orleans.Analyzers/GenerateAliasAttributesAnalyzer.cs b/src/Orleans.Analyzers/GenerateAliasAttributesAnalyzer.cs index ca7fdb52017..85c34ce956c 100644 --- a/src/Orleans.Analyzers/GenerateAliasAttributesAnalyzer.cs +++ b/src/Orleans.Analyzers/GenerateAliasAttributesAnalyzer.cs @@ -25,27 +25,38 @@ public class GenerateAliasAttributesAnalyzer : DiagnosticAnalyzer public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); - context.RegisterSyntaxNodeAction(CheckSyntaxNode, - SyntaxKind.InterfaceDeclaration, - SyntaxKind.ClassDeclaration, - SyntaxKind.StructDeclaration, - SyntaxKind.RecordDeclaration, - SyntaxKind.RecordStructDeclaration); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.RegisterCompilationStartAction(context => + { + var aliasAttributeSymbol = context.Compilation.GetTypeByMetadataName("Orleans.AliasAttribute"); + var generateSerializerAttributeSymbol = context.Compilation.GetTypeByMetadataName("Orleans.GenerateSerializerAttribute"); + var grainSymbol = context.Compilation.GetTypeByMetadataName("Orleans.Grain"); + if (aliasAttributeSymbol is not null && generateSerializerAttributeSymbol is not null) + { + context.RegisterSymbolAction(context => AnalyzeNamedType(context, aliasAttributeSymbol, generateSerializerAttributeSymbol, grainSymbol), SymbolKind.NamedType); + } + }); } - private void CheckSyntaxNode(SyntaxNodeAnalysisContext context) + private void AnalyzeNamedType( + SymbolAnalysisContext context, + INamedTypeSymbol aliasAttributeSymbol, + INamedTypeSymbol generateSerializerAttributeSymbol, + INamedTypeSymbol grainSymbol) { + var symbol = (INamedTypeSymbol)context.Symbol; + // Interface types and their methods - if (context.Node is InterfaceDeclarationSyntax { } interfaceDeclaration) + if (symbol.TypeKind == TypeKind.Interface) { - if (!interfaceDeclaration.ExtendsGrainInterface(context.SemanticModel)) + if (!symbol.ExtendsGrainInterface()) { return; } - if (!interfaceDeclaration.HasAttribute(Constants.AliasAttributeName)) + if (!symbol.HasAttribute(aliasAttributeSymbol)) { + var interfaceDeclaration = (InterfaceDeclarationSyntax)symbol.DeclaringSyntaxReferences[0].GetSyntax(); ReportFor( context, interfaceDeclaration.GetLocation(), @@ -54,16 +65,16 @@ private void CheckSyntaxNode(SyntaxNodeAnalysisContext context) GetNamespaceAndNesting(interfaceDeclaration)); } - foreach (var methodDeclaration in interfaceDeclaration.Members.OfType()) + foreach (var methodSymbol in symbol.GetMembers().OfType()) { - if (methodDeclaration.IsStatic()) + if (methodSymbol.IsStatic) { continue; } - if (!methodDeclaration.HasAttribute(Constants.AliasAttributeName)) + if (!methodSymbol.HasAttribute(aliasAttributeSymbol)) { - ReportFor(context, methodDeclaration.GetLocation(), methodDeclaration.Identifier.ToString(), arity: 0, namespaceAndNesting: null); + ReportFor(context, methodSymbol.DeclaringSyntaxReferences[0].GetSyntax().GetLocation(), methodSymbol.Name, arity: 0, namespaceAndNesting: null); } } @@ -71,24 +82,24 @@ private void CheckSyntaxNode(SyntaxNodeAnalysisContext context) } // Rest of types: class, struct, record - if (context.Node is TypeDeclarationSyntax { } typeDeclaration) + if (symbol.TypeKind is TypeKind.Class or TypeKind.Struct) { - if (typeDeclaration is ClassDeclarationSyntax classDeclaration && - classDeclaration.InheritsGrainClass(context.SemanticModel)) + if (symbol.DerivesFrom(grainSymbol)) { return; } - if (!typeDeclaration.HasAttribute(Constants.GenerateSerializerAttributeName)) + if (!symbol.HasAttribute(generateSerializerAttributeSymbol)) { return; } - if (typeDeclaration.HasAttribute(Constants.AliasAttributeName)) + if (symbol.HasAttribute(aliasAttributeSymbol)) { return; } + var typeDeclaration = (TypeDeclarationSyntax)symbol.DeclaringSyntaxReferences[0].GetSyntax(); ReportFor( context, typeDeclaration.GetLocation(), @@ -144,7 +155,7 @@ private static string GetNamespaceAndNesting(TypeDeclarationSyntax typeDeclarati return sb.ToString(); } - private static void ReportFor(SyntaxNodeAnalysisContext context, Location location, string typeName, int arity, string namespaceAndNesting) + private static void ReportFor(SymbolAnalysisContext context, Location location, string typeName, int arity, string namespaceAndNesting) { var builder = ImmutableDictionary.CreateBuilder(); diff --git a/src/Orleans.Analyzers/SymbolHelpers.cs b/src/Orleans.Analyzers/SymbolHelpers.cs index e930dd18197..db5bcc1fbb5 100644 --- a/src/Orleans.Analyzers/SymbolHelpers.cs +++ b/src/Orleans.Analyzers/SymbolHelpers.cs @@ -16,5 +16,21 @@ public static bool HasAttribute(this ISymbol symbol, INamedTypeSymbol attributeS return false; } + + public static bool DerivesFrom(this ITypeSymbol symbol, ITypeSymbol candidateBaseType) + { + var baseType = symbol.BaseType; + while (baseType is not null) + { + if (baseType.Equals(candidateBaseType, SymbolEqualityComparer.Default)) + { + return true; + } + + baseType = baseType.BaseType; + } + + return false; + } } } diff --git a/src/Orleans.Analyzers/SyntaxHelpers.cs b/src/Orleans.Analyzers/SyntaxHelpers.cs index 1c1de525171..ea33308c429 100644 --- a/src/Orleans.Analyzers/SyntaxHelpers.cs +++ b/src/Orleans.Analyzers/SyntaxHelpers.cs @@ -65,24 +65,8 @@ public static bool TryGetAttribute(this MemberDeclarationSyntax member, string a return false; } - public static string GetMemberNameOrDefault(this MemberDeclarationSyntax member) - { - if (member is PropertyDeclarationSyntax property) - { - return property.ChildTokens().FirstOrDefault(token => token.IsKind(SyntaxKind.IdentifierToken)).ValueText; - } - else if (member is FieldDeclarationSyntax field) - { - return field.ChildNodes().OfType().FirstOrDefault()?.ChildNodes().OfType().FirstOrDefault()?.Identifier.ValueText; - } - - return null; - } - public static bool IsAbstract(this MemberDeclarationSyntax member) => member.HasModifier(SyntaxKind.AbstractKeyword); - public static bool IsStatic(this MemberDeclarationSyntax member) => member.HasModifier(SyntaxKind.StaticKeyword); - public static bool HasModifier(this MemberDeclarationSyntax member, SyntaxKind modifierKind) { foreach (var modifier in member.Modifiers) @@ -147,17 +131,6 @@ public static bool IsFieldOrAutoProperty(this MemberDeclarationSyntax member) return isFieldOrAutoProperty; } - public static bool ExtendsGrainInterface(this InterfaceDeclarationSyntax interfaceDeclaration, SemanticModel semanticModel) - { - if (interfaceDeclaration is null) - { - return false; - } - - var symbol = semanticModel.GetDeclaredSymbol(interfaceDeclaration); - return symbol.ExtendsGrainInterface(); - } - public static bool ExtendsGrainInterface(this INamedTypeSymbol symbol) { if (symbol is null || symbol.TypeKind != TypeKind.Interface) @@ -228,24 +201,5 @@ public static IEnumerable GetAttributeSyntaxes(this SyntaxList< attributeLists .SelectMany(attributeList => attributeList.Attributes) .Where(attribute => attribute.IsAttribute(attributeName)); - - public static string GetArgumentValue(this AttributeSyntax attribute, SemanticModel semanticModel) - { - if (attribute?.ArgumentList == null || attribute.ArgumentList.Arguments.Count == 0) - { - return null; - } - - var symbolInfo = semanticModel.GetSymbolInfo(attribute); - if (symbolInfo.Symbol == null && symbolInfo.CandidateSymbols.Length == 0) - { - return null; - } - - var argumentExpression = attribute.ArgumentList.Arguments[0].Expression; - var constant = semanticModel.GetConstantValue(argumentExpression); - - return constant.HasValue ? constant.Value?.ToString() : null; - } } } From 3f4bf6237ed11171d5eec3efbabb100691aefa5e Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 30 Aug 2025 12:13:56 +0200 Subject: [PATCH 06/16] Cleanup no longer used helper --- src/Orleans.Analyzers/SerializationAttributesHelper.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Orleans.Analyzers/SerializationAttributesHelper.cs b/src/Orleans.Analyzers/SerializationAttributesHelper.cs index 3583cba8393..d5e4c95d960 100644 --- a/src/Orleans.Analyzers/SerializationAttributesHelper.cs +++ b/src/Orleans.Analyzers/SerializationAttributesHelper.cs @@ -9,16 +9,6 @@ namespace Orleans.Analyzers { internal static class SerializationAttributesHelper { - public static bool ShouldGenerateSerializer(TypeDeclarationSyntax declaration) - { - if (!declaration.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword)) && declaration.HasAttribute(Constants.GenerateSerializerAttributeName)) - { - return true; - } - - return false; - } - public static bool ShouldGenerateSerializer(INamedTypeSymbol symbol, INamedTypeSymbol generateSerializerAttributeSymbol) { if (!symbol.IsStatic && symbol.HasAttribute(generateSerializerAttributeSymbol)) From c83ae3cbcb9375d9cb9d14dabe66032c1f72101f Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 30 Aug 2025 12:14:06 +0200 Subject: [PATCH 07/16] Cleanup GenerateGenerateSerializerAttributeAnalyzer --- ...rateGenerateSerializerAttributeAnalyzer.cs | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/Orleans.Analyzers/GenerateGenerateSerializerAttributeAnalyzer.cs b/src/Orleans.Analyzers/GenerateGenerateSerializerAttributeAnalyzer.cs index 27b0bbc1669..566634bfc0a 100644 --- a/src/Orleans.Analyzers/GenerateGenerateSerializerAttributeAnalyzer.cs +++ b/src/Orleans.Analyzers/GenerateGenerateSerializerAttributeAnalyzer.cs @@ -1,9 +1,6 @@ using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using System.Collections.Immutable; -using System.Linq; namespace Orleans.Analyzers { @@ -18,23 +15,31 @@ public class GenerateGenerateSerializerAttributeAnalyzer : DiagnosticAnalyzer internal static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(RuleId, Title, MessageFormat, Category, DiagnosticSeverity.Info, isEnabledByDefault: true, description: Description); - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + public override ImmutableArray SupportedDiagnostics { get; } = [Rule]; public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - context.RegisterSyntaxNodeAction(CheckSyntaxNode, SyntaxKind.ClassDeclaration, SyntaxKind.StructDeclaration, SyntaxKind.RecordDeclaration, SyntaxKind.RecordStructDeclaration); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.RegisterCompilationStartAction(context => + { + var serializableAttributeSymbol = context.Compilation.GetTypeByMetadataName("System.SerializableAttribute"); + var generateSerializerAttributeSymbol = context.Compilation.GetTypeByMetadataName("Orleans.GenerateSerializerAttribute"); + if (serializableAttributeSymbol is not null && generateSerializerAttributeSymbol is not null) + { + context.RegisterSymbolAction(context => AnalyzeNamedType(context, serializableAttributeSymbol, generateSerializerAttributeSymbol), SymbolKind.NamedType); + } + }); } - private void CheckSyntaxNode(SyntaxNodeAnalysisContext context) + private static void AnalyzeNamedType(SymbolAnalysisContext context, INamedTypeSymbol serializableAttributeSymbol, INamedTypeSymbol generateSerializerAttributeSymbol) { - if (context.Node is TypeDeclarationSyntax declaration && !declaration.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword))) + var symbol = (INamedTypeSymbol)context.Symbol; + if (!symbol.IsStatic) { - if (declaration.TryGetAttribute(Constants.SerializableAttributeName, out var attribute) && !declaration.HasAttribute(Constants.GenerateSerializerAttributeName)) + if (symbol.HasAttribute(serializableAttributeSymbol) && !symbol.HasAttribute(generateSerializerAttributeSymbol)) { - - context.ReportDiagnostic(Diagnostic.Create(Rule, attribute.GetLocation(), new object[] { declaration.Identifier.ToString() })); + context.ReportDiagnostic(Diagnostic.Create(Rule, symbol.Locations[0], symbol.Name)); } } } From 81e110d02dfaf149c83c82253e35ac974e82c1c0 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 30 Aug 2025 12:26:56 +0200 Subject: [PATCH 08/16] Cleanup GrainInterfaceMethodReturnTypeDiagnosticAnalyzer --- ...rfaceMethodReturnTypeDiagnosticAnalyzer.cs | 82 ++++++++----------- 1 file changed, 33 insertions(+), 49 deletions(-) diff --git a/src/Orleans.Analyzers/GrainInterfaceMethodReturnTypeDiagnosticAnalyzer.cs b/src/Orleans.Analyzers/GrainInterfaceMethodReturnTypeDiagnosticAnalyzer.cs index 049f0cab5f9..8194798645a 100644 --- a/src/Orleans.Analyzers/GrainInterfaceMethodReturnTypeDiagnosticAnalyzer.cs +++ b/src/Orleans.Analyzers/GrainInterfaceMethodReturnTypeDiagnosticAnalyzer.cs @@ -11,15 +11,7 @@ namespace Orleans.Analyzers public class GrainInterfaceMethodReturnTypeDiagnosticAnalyzer : DiagnosticAnalyzer { private const string BaseInterfaceName = "Orleans.Runtime.IAddressable"; - private static readonly (string[] Namespace, string MetadataName)[] SupportedReturnTypes = new[] - { - (new [] { "System", "Threading", "Tasks" }, "Task"), - (new [] { "System", "Threading", "Tasks" }, "Task`1"), - (new [] { "System", "Threading", "Tasks" }, "ValueTask"), - (new [] { "System", "Threading", "Tasks" }, "ValueTask`1"), - (new [] { "System", "Collections", "Generic" }, "IAsyncEnumerable`1"), - (new [] { "System" }, "Void") - }; + public const string DiagnosticId = "ORLEANS0009"; public const string Title = "Grain interfaces methods must return a compatible type"; public const string MessageFormat = $"Grain interfaces methods must return a compatible type, such as Task, Task, ValueTask, ValueTask, or void"; @@ -27,20 +19,42 @@ private static readonly (string[] Namespace, string MetadataName)[] SupportedRet private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true); - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + public override ImmutableArray SupportedDiagnostics { get; } = [Rule]; public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); - context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.MethodDeclaration); + context.RegisterCompilationStartAction(context => + { + if (context.Compilation.GetTypeByMetadataName(BaseInterfaceName) is not { } baseInterface) + { + return; + } + + var builder = ImmutableHashSet.CreateBuilder(SymbolEqualityComparer.Default); + + AddIfNotNull(builder, context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")); + AddIfNotNull(builder, context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1")); + AddIfNotNull(builder, context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask")); + AddIfNotNull(builder, context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask`1")); + AddIfNotNull(builder, context.Compilation.GetTypeByMetadataName("System.Collections.Generic.IAsyncEnumerable`1")); + AddIfNotNull(builder, context.Compilation.GetSpecialType(SpecialType.System_Void)); + + context.RegisterSymbolAction(context => AnalyzeMethod(context, baseInterface, builder.ToImmutable()), SymbolKind.Method); + }); + + + static void AddIfNotNull(ImmutableHashSet.Builder builder, INamedTypeSymbol symbol) + { + if (symbol is not null) + builder.Add(symbol); + } } - private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + private static void AnalyzeMethod(SymbolAnalysisContext context, INamedTypeSymbol baseInterface, ImmutableHashSet supportedTypes) { - if (context.Node is not MethodDeclarationSyntax syntax) return; - - var symbol = context.SemanticModel.GetDeclaredSymbol(syntax, context.CancellationToken); + var symbol = (IMethodSymbol)context.Symbol; if (symbol.ContainingType.TypeKind != TypeKind.Interface) return; @@ -51,48 +65,18 @@ private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context) var isIAddressableInterface = false; foreach (var implementedInterface in symbol.ContainingType.AllInterfaces) { - if (BaseInterfaceName.Equals(implementedInterface.ToDisplayString(NullableFlowState.None), StringComparison.Ordinal)) + if (implementedInterface.Equals(baseInterface, SymbolEqualityComparer.Default)) { isIAddressableInterface = true; break; } } - if (!isIAddressableInterface) return; - - var isSupportedType = false; - var returnType = symbol.ReturnType switch - { - INamedTypeSymbol { IsGenericType: true } generic => generic.ConstructedFrom, - { } type => type, - }; - - if (returnType.ContainingNamespace is { } returnTypeNs) - { - foreach (var allowedReturnType in SupportedReturnTypes) - { - var (ns, metadataName) = allowedReturnType; - if (metadataName.Equals(returnType.MetadataName, StringComparison.Ordinal) && NamespacesEqual(returnTypeNs, ns.AsSpan())) - { - isSupportedType = true; - break; - } - } - } - - if (isSupportedType) return; + if (!isIAddressableInterface || supportedTypes.Contains(symbol.ReturnType.OriginalDefinition)) + return; var syntaxReference = symbol.DeclaringSyntaxReferences; context.ReportDiagnostic(Diagnostic.Create(Rule, Location.Create(syntaxReference[0].SyntaxTree, syntaxReference[0].Span))); } - - private static bool NamespacesEqual(INamespaceSymbol left, ReadOnlySpan right) - { - if (right.Length == 0) return left.IsGlobalNamespace; - if (left.IsGlobalNamespace) return false; - if (!string.Equals(left.Name, right[right.Length - 1], StringComparison.Ordinal)) return false; - - return NamespacesEqual(left.ContainingNamespace, right.Slice(0, right.Length - 1)); - } } } From 4428e85c85a3814fad6d53410561c642e8a4d757 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 30 Aug 2025 12:28:31 +0200 Subject: [PATCH 09/16] Cleanup dead code --- src/Orleans.Analyzers/SerializationAttributesHelper.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Orleans.Analyzers/SerializationAttributesHelper.cs b/src/Orleans.Analyzers/SerializationAttributesHelper.cs index d5e4c95d960..6ab89f29b1c 100644 --- a/src/Orleans.Analyzers/SerializationAttributesHelper.cs +++ b/src/Orleans.Analyzers/SerializationAttributesHelper.cs @@ -22,17 +22,13 @@ public static bool ShouldGenerateSerializer(INamedTypeSymbol symbol, INamedTypeS public readonly record struct TypeAnalysis { public List UnannotatedMembers { get; init; } - public List AnnotatedMembers { get; init; } public uint NextAvailableId { get; init; } - public uint AnnotatedConstructorCount { get; init; } } public static TypeAnalysis AnalyzeTypeDeclaration(TypeDeclarationSyntax declaration) { uint nextId = 0; - uint annotatedConstructorCount = 0; var unannotatedSerializableMembers = new List(); - var annotatedSerializableMembers = new List(); foreach (var member in declaration.Members) { // Skip members with existing [Id(x)] attributes, but record the highest value of x so that newly added attributes can begin from that value. @@ -53,13 +49,11 @@ public static TypeAnalysis AnalyzeTypeDeclaration(TypeDeclarationSyntax declarat } } - annotatedSerializableMembers.Add(member); continue; } if (member is ConstructorDeclarationSyntax constructorDeclaration && constructorDeclaration.HasAttribute(Constants.GenerateSerializerAttributeName)) { - annotatedConstructorCount++; continue; } @@ -75,9 +69,7 @@ public static TypeAnalysis AnalyzeTypeDeclaration(TypeDeclarationSyntax declarat return new TypeAnalysis { UnannotatedMembers = unannotatedSerializableMembers, - AnnotatedMembers = annotatedSerializableMembers, NextAvailableId = nextId, - AnnotatedConstructorCount = annotatedConstructorCount }; } } From cf61f14a873dabf13ff625549c782e81a04abc36 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 30 Aug 2025 12:37:41 +0200 Subject: [PATCH 10/16] Cleanup IncorrectAttributeUseAnalyzer --- .../IncorrectAttributeUseAnalyzer.cs | 39 +++++++++++-------- src/Orleans.Analyzers/SymbolHelpers.cs | 7 +++- src/Orleans.Analyzers/SyntaxHelpers.cs | 30 -------------- 3 files changed, 28 insertions(+), 48 deletions(-) diff --git a/src/Orleans.Analyzers/IncorrectAttributeUseAnalyzer.cs b/src/Orleans.Analyzers/IncorrectAttributeUseAnalyzer.cs index 6ec726d8585..865cb59bc79 100644 --- a/src/Orleans.Analyzers/IncorrectAttributeUseAnalyzer.cs +++ b/src/Orleans.Analyzers/IncorrectAttributeUseAnalyzer.cs @@ -1,6 +1,4 @@ using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using System.Collections.Immutable; @@ -25,33 +23,40 @@ public class IncorrectAttributeUseAnalyzer : DiagnosticAnalyzer public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); - context.RegisterSyntaxNodeAction(CheckSyntaxNode, SyntaxKind.ClassDeclaration); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterCompilationStartAction(context => + { + var aliasAttributeSymbol = context.Compilation.GetTypeByMetadataName("Orleans.AliasAttribute"); + var grainSymbol = context.Compilation.GetTypeByMetadataName("Orleans.Grain"); + var generateSerializerAttributeSymbol = context.Compilation.GetTypeByMetadataName("Orleans.GenerateSerializerAttribute"); + if (aliasAttributeSymbol is not null && grainSymbol is not null) + { + context.RegisterSymbolAction(context => AnalyzeNamedType(context, aliasAttributeSymbol, grainSymbol, generateSerializerAttributeSymbol), SymbolKind.NamedType); + } + }); } - private void CheckSyntaxNode(SyntaxNodeAnalysisContext context) + private static void AnalyzeNamedType(SymbolAnalysisContext context, INamedTypeSymbol aliasAttributeSymbol, INamedTypeSymbol grainSymbol, INamedTypeSymbol generateSerializerAttributeSymbol) { - if (context.Node is not ClassDeclarationSyntax) return; - - var classDeclaration = (ClassDeclarationSyntax)context.Node; - - if (!classDeclaration.InheritsGrainClass(context.SemanticModel)) + var symbol = (INamedTypeSymbol)context.Symbol; + if (!symbol.DerivesFrom(grainSymbol)) { return; } - TryReportFor(Constants.AliasAttributeName, context, classDeclaration); - TryReportFor(Constants.GenerateSerializerAttributeName, context, classDeclaration); + TryReportFor(aliasAttributeSymbol, context, symbol); + TryReportFor(generateSerializerAttributeSymbol, context, symbol); } - private static void TryReportFor(string attributeTypeName, SyntaxNodeAnalysisContext context, ClassDeclarationSyntax classDeclaration) + private static void TryReportFor(INamedTypeSymbol attributeSymbol, SymbolAnalysisContext context, INamedTypeSymbol symbol) { - if (classDeclaration.TryGetAttribute(attributeTypeName, out var attribute)) + if (symbol.HasAttribute(attributeSymbol, out var location)) { context.ReportDiagnostic(Diagnostic.Create( descriptor: Rule, - location: attribute.GetLocation(), - messageArgs: new object[] { attributeTypeName })); + location: location, + messageArgs: new object[] { attributeSymbol.Name })); } } -} \ No newline at end of file +} diff --git a/src/Orleans.Analyzers/SymbolHelpers.cs b/src/Orleans.Analyzers/SymbolHelpers.cs index db5bcc1fbb5..e4a54d33c77 100644 --- a/src/Orleans.Analyzers/SymbolHelpers.cs +++ b/src/Orleans.Analyzers/SymbolHelpers.cs @@ -4,19 +4,24 @@ namespace Orleans.Analyzers { internal static class SymbolHelpers { - public static bool HasAttribute(this ISymbol symbol, INamedTypeSymbol attributeSymbol) + public static bool HasAttribute(this ISymbol symbol, INamedTypeSymbol attributeSymbol, out Location location) { foreach (var attribute in symbol.GetAttributes()) { if (attributeSymbol.Equals(attribute.AttributeClass, SymbolEqualityComparer.Default)) { + location = attribute.ApplicationSyntaxReference.GetSyntax().GetLocation(); return true; } } + location = null; return false; } + public static bool HasAttribute(this ISymbol symbol, INamedTypeSymbol attributeSymbol) + => symbol.HasAttribute(attributeSymbol, out _); + public static bool DerivesFrom(this ITypeSymbol symbol, ITypeSymbol candidateBaseType) { var baseType = symbol.BaseType; diff --git a/src/Orleans.Analyzers/SyntaxHelpers.cs b/src/Orleans.Analyzers/SyntaxHelpers.cs index ea33308c429..ff451b5d27b 100644 --- a/src/Orleans.Analyzers/SyntaxHelpers.cs +++ b/src/Orleans.Analyzers/SyntaxHelpers.cs @@ -149,36 +149,6 @@ public static bool ExtendsGrainInterface(this INamedTypeSymbol symbol) return false; } - public static bool InheritsGrainClass(this ClassDeclarationSyntax declaration, SemanticModel semanticModel) - { - var baseTypes = declaration.BaseList?.Types; - if (baseTypes is null) - { - return false; - } - - foreach (var baseTypeSyntax in baseTypes) - { - var baseTypeSymbol = semanticModel.GetTypeInfo(baseTypeSyntax.Type).Type; - if (baseTypeSymbol is INamedTypeSymbol currentTypeSymbol) - { - if (currentTypeSymbol.IsGenericType && - currentTypeSymbol.TypeParameters.Length == 1 && - currentTypeSymbol.BaseType is { } baseBaseTypeSymbol) - { - currentTypeSymbol = baseBaseTypeSymbol; - } - - if (Constants.GrainBaseFullyQualifiedName.Equals(currentTypeSymbol.ToDisplayString(NullableFlowState.None), StringComparison.Ordinal)) - { - return true; - } - } - } - - return false; - } - public static AttributeArgumentBag GetArgumentBag(this AttributeSyntax attribute, SemanticModel semanticModel) { if (attribute is null) From ac2b819f3296c6fb0805e5a422649e1fae41b13e Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 30 Aug 2025 12:39:57 +0200 Subject: [PATCH 11/16] Cleanup SyntaxHelpers --- src/Orleans.Analyzers/SyntaxHelpers.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Orleans.Analyzers/SyntaxHelpers.cs b/src/Orleans.Analyzers/SyntaxHelpers.cs index ff451b5d27b..7157521ba1a 100644 --- a/src/Orleans.Analyzers/SyntaxHelpers.cs +++ b/src/Orleans.Analyzers/SyntaxHelpers.cs @@ -12,7 +12,7 @@ namespace Orleans.Analyzers internal static class SyntaxHelpers { - public static bool TryGetTypeName(this AttributeSyntax attributeSyntax, out string typeName) + private static bool TryGetTypeName(this AttributeSyntax attributeSyntax, out string typeName) { typeName = attributeSyntax.Name switch { @@ -67,7 +67,7 @@ public static bool TryGetAttribute(this MemberDeclarationSyntax member, string a public static bool IsAbstract(this MemberDeclarationSyntax member) => member.HasModifier(SyntaxKind.AbstractKeyword); - public static bool HasModifier(this MemberDeclarationSyntax member, SyntaxKind modifierKind) + private static bool HasModifier(this MemberDeclarationSyntax member, SyntaxKind modifierKind) { foreach (var modifier in member.Modifiers) { From a3dbbbaba6d19ad84b84d1ec456000376ff30ca6 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 30 Aug 2025 12:53:50 +0200 Subject: [PATCH 12/16] Cleanup IdClashAttributeAnalyzer --- .../IdClashAttributeAnalyzer.cs | 41 +++++++++++-------- src/Orleans.Analyzers/SyntaxHelpers.cs | 23 ----------- 2 files changed, 25 insertions(+), 39 deletions(-) diff --git a/src/Orleans.Analyzers/IdClashAttributeAnalyzer.cs b/src/Orleans.Analyzers/IdClashAttributeAnalyzer.cs index 438076fd07a..5345596d9ef 100644 --- a/src/Orleans.Analyzers/IdClashAttributeAnalyzer.cs +++ b/src/Orleans.Analyzers/IdClashAttributeAnalyzer.cs @@ -29,32 +29,41 @@ public class IdClashAttributeAnalyzer : DiagnosticAnalyzer public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); - context.RegisterSyntaxNodeAction(CheckSyntaxNode, - SyntaxKind.ClassDeclaration, - SyntaxKind.StructDeclaration, - SyntaxKind.RecordDeclaration, - SyntaxKind.RecordStructDeclaration); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.RegisterCompilationStartAction(context => + { + var generateSerializerAttribute = context.Compilation.GetTypeByMetadataName("Orleans.GenerateSerializerAttribute"); + var idAttribute = context.Compilation.GetTypeByMetadataName("Orleans.IdAttribute"); + if (generateSerializerAttribute is not null && idAttribute is not null) + { + context.RegisterSymbolAction(context => AnalyzeNamedType(context, generateSerializerAttribute, idAttribute), SymbolKind.NamedType); + } + }); } - private void CheckSyntaxNode(SyntaxNodeAnalysisContext context) + private static void AnalyzeNamedType(SymbolAnalysisContext context, INamedTypeSymbol generateSerializerAttribute, INamedTypeSymbol idAttribute) { - var typeDeclaration = context.Node as TypeDeclarationSyntax; - if (!typeDeclaration.HasAttribute(Constants.GenerateSerializerAttributeName)) + var typeSymbol = (INamedTypeSymbol)context.Symbol; + if (!typeSymbol.HasAttribute(generateSerializerAttribute)) { return; } - List> bags = []; - foreach (var memberDeclaration in typeDeclaration.Members.OfType()) + List> bags = []; + foreach (var member in typeSymbol.GetMembers()) { - var attributes = memberDeclaration.AttributeLists.GetAttributeSyntaxes(Constants.IdAttributeName); - foreach (var attribute in attributes) + foreach (var attribute in member.GetAttributes()) { - var bag = attribute.GetArgumentBag(context.SemanticModel); - if (bag != default) + if (!idAttribute.Equals(attribute.AttributeClass, SymbolEqualityComparer.Default)) + { + continue; + } + + if (attribute.ConstructorArguments.Length == 1 && + attribute.ConstructorArguments[0].Value is uint idValue) { - bags.Add(bag); + var attributeSyntax = (AttributeSyntax)attribute.ApplicationSyntaxReference.GetSyntax(); + bags.Add(new AttributeArgumentBag(idValue, attributeSyntax.GetLocation())); } } } diff --git a/src/Orleans.Analyzers/SyntaxHelpers.cs b/src/Orleans.Analyzers/SyntaxHelpers.cs index 7157521ba1a..6d78c1a77b0 100644 --- a/src/Orleans.Analyzers/SyntaxHelpers.cs +++ b/src/Orleans.Analyzers/SyntaxHelpers.cs @@ -148,28 +148,5 @@ public static bool ExtendsGrainInterface(this INamedTypeSymbol symbol) return false; } - - public static AttributeArgumentBag GetArgumentBag(this AttributeSyntax attribute, SemanticModel semanticModel) - { - if (attribute is null) - { - return default; - } - - var argument = attribute.ArgumentList?.Arguments.FirstOrDefault(); - if (argument is null || argument.Expression is not { } expression) - { - return default; - } - - var constantValue = semanticModel.GetConstantValue(expression); - return constantValue.HasValue && constantValue.Value is T value ? - new(value, attribute.GetLocation()) : default; - } - - public static IEnumerable GetAttributeSyntaxes(this SyntaxList attributeLists, string attributeName) => - attributeLists - .SelectMany(attributeList => attributeList.Attributes) - .Where(attribute => attribute.IsAttribute(attributeName)); } } From c9b67d5a1f08de097757f8d20a87312c7f40a696 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 30 Aug 2025 12:58:29 +0200 Subject: [PATCH 13/16] Cleanup NoRefParamsDiagnosticAnalyzer --- .../NoRefParamsDiagnosticAnalyzer.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Orleans.Analyzers/NoRefParamsDiagnosticAnalyzer.cs b/src/Orleans.Analyzers/NoRefParamsDiagnosticAnalyzer.cs index 1b420223796..e9e1c5664a5 100644 --- a/src/Orleans.Analyzers/NoRefParamsDiagnosticAnalyzer.cs +++ b/src/Orleans.Analyzers/NoRefParamsDiagnosticAnalyzer.cs @@ -10,7 +10,6 @@ namespace Orleans.Analyzers [DiagnosticAnalyzer(LanguageNames.CSharp)] public class NoRefParamsDiagnosticAnalyzer : DiagnosticAnalyzer { - private const string BaseInterfaceName = "IAddressable"; public const string DiagnosticId = "ORLEANS0002"; public const string Title = "Reference parameter modifiers are not allowed"; public const string MessageFormat = Title; @@ -18,20 +17,25 @@ public class NoRefParamsDiagnosticAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true); - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + public override ImmutableArray SupportedDiagnostics { get; } = [Rule]; public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); - context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.MethodDeclaration); + context.RegisterCompilationStartAction(context => + { + var baseInterface = context.Compilation.GetTypeByMetadataName("Orleans.Runtime.IAddressable"); + if (baseInterface is not null) + { + context.RegisterSymbolAction(context => AnalyzeMethodSymbol(context, baseInterface), SymbolKind.Method); + } + }); } - private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + private static void AnalyzeMethodSymbol(SymbolAnalysisContext context, INamedTypeSymbol baseInterface) { - if (!(context.Node is MethodDeclarationSyntax syntax)) return; - - var symbol = context.SemanticModel.GetDeclaredSymbol(syntax, context.CancellationToken); + var symbol = (IMethodSymbol)context.Symbol; if (symbol.ContainingType.TypeKind != TypeKind.Interface) return; @@ -41,7 +45,7 @@ private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context) var implementedInterfaces = symbol.ContainingType .AllInterfaces .Select(interfaceDef => interfaceDef.Name); - if (!implementedInterfaces.Contains(BaseInterfaceName)) return; + if (!symbol.ContainingType.AllInterfaces.Contains(baseInterface)) return; foreach(var param in symbol.Parameters) { From cd62c9dbb488635458b37c721bd1b6ae740fa3da Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Wed, 1 Oct 2025 14:42:59 -0700 Subject: [PATCH 14/16] Apply suggestion from @ReubenBond --- .../GrainInterfaceMethodReturnTypeDiagnosticAnalyzer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Orleans.Analyzers/GrainInterfaceMethodReturnTypeDiagnosticAnalyzer.cs b/src/Orleans.Analyzers/GrainInterfaceMethodReturnTypeDiagnosticAnalyzer.cs index 8194798645a..4f1563b2ac6 100644 --- a/src/Orleans.Analyzers/GrainInterfaceMethodReturnTypeDiagnosticAnalyzer.cs +++ b/src/Orleans.Analyzers/GrainInterfaceMethodReturnTypeDiagnosticAnalyzer.cs @@ -48,7 +48,9 @@ public override void Initialize(AnalysisContext context) static void AddIfNotNull(ImmutableHashSet.Builder builder, INamedTypeSymbol symbol) { if (symbol is not null) + { builder.Add(symbol); + } } } From d2b232b22c63d9e6b0b83cae9ae9e7d6c0a04a9b Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Sat, 14 Feb 2026 16:13:29 -0800 Subject: [PATCH 15/16] Guard GenerateAlias analyzer syntax access Avoid indexing DeclaringSyntaxReferences for symbols without source declarations and add a regression test for referenced grain interfaces. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../GenerateAliasAttributesAnalyzer.cs | 26 ++++++- .../GenerateAliasAttributesAnalyzerTest.cs | 77 +++++++++++++++++++ 2 files changed, 100 insertions(+), 3 deletions(-) diff --git a/src/Orleans.Analyzers/GenerateAliasAttributesAnalyzer.cs b/src/Orleans.Analyzers/GenerateAliasAttributesAnalyzer.cs index 85c34ce956c..563ce7fea70 100644 --- a/src/Orleans.Analyzers/GenerateAliasAttributesAnalyzer.cs +++ b/src/Orleans.Analyzers/GenerateAliasAttributesAnalyzer.cs @@ -56,7 +56,11 @@ private void AnalyzeNamedType( if (!symbol.HasAttribute(aliasAttributeSymbol)) { - var interfaceDeclaration = (InterfaceDeclarationSyntax)symbol.DeclaringSyntaxReferences[0].GetSyntax(); + if (!TryGetDeclarationSyntax(symbol, out InterfaceDeclarationSyntax interfaceDeclaration)) + { + return; + } + ReportFor( context, interfaceDeclaration.GetLocation(), @@ -74,7 +78,12 @@ private void AnalyzeNamedType( if (!methodSymbol.HasAttribute(aliasAttributeSymbol)) { - ReportFor(context, methodSymbol.DeclaringSyntaxReferences[0].GetSyntax().GetLocation(), methodSymbol.Name, arity: 0, namespaceAndNesting: null); + if (!TryGetDeclarationSyntax(methodSymbol, out MethodDeclarationSyntax methodDeclaration)) + { + continue; + } + + ReportFor(context, methodDeclaration.GetLocation(), methodSymbol.Name, arity: 0, namespaceAndNesting: null); } } @@ -99,7 +108,11 @@ private void AnalyzeNamedType( return; } - var typeDeclaration = (TypeDeclarationSyntax)symbol.DeclaringSyntaxReferences[0].GetSyntax(); + if (!TryGetDeclarationSyntax(symbol, out TypeDeclarationSyntax typeDeclaration)) + { + return; + } + ReportFor( context, typeDeclaration.GetLocation(), @@ -155,6 +168,13 @@ private static string GetNamespaceAndNesting(TypeDeclarationSyntax typeDeclarati return sb.ToString(); } + private static bool TryGetDeclarationSyntax(ISymbol symbol, out TSyntax syntax) + where TSyntax : SyntaxNode + { + syntax = symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as TSyntax; + return syntax is not null; + } + private static void ReportFor(SymbolAnalysisContext context, Location location, string typeName, int arity, string namespaceAndNesting) { var builder = ImmutableDictionary.CreateBuilder(); diff --git a/test/Analyzers.Tests/GenerateAliasAttributesAnalyzerTest.cs b/test/Analyzers.Tests/GenerateAliasAttributesAnalyzerTest.cs index 0028b4bdfc7..b3c27f1a037 100644 --- a/test/Analyzers.Tests/GenerateAliasAttributesAnalyzerTest.cs +++ b/test/Analyzers.Tests/GenerateAliasAttributesAnalyzerTest.cs @@ -1,4 +1,8 @@ +using System.Collections.Immutable; +using System.Reflection; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; using Orleans.Analyzers; using Xunit; @@ -95,6 +99,24 @@ public interface I return VerifyHasNoDiagnostic(code); } + [Fact] + public async Task ReferencedGrainInterfaceWithoutAliasAttribute_ShouldNotCrashAnalyzer() + { + const string referencedSource = """ + using Orleans; + using System.Threading.Tasks; + + public interface IReferencedGrain : IGrainWithGuidKey + { + Task M1(); + } + """; + + var diagnostics = await GetDiagnosticsWithReferencedAssemblyAsync("public class C {}", referencedSource); + + Assert.Empty(diagnostics); + } + #endregion #region Classes, Structs, Records @@ -168,4 +190,59 @@ public Task RecordStructWithoutAliasAttribute_AndWithoutGenerateSerializerAttrib => VerifyHasNoDiagnostic("public record struct RS {}"); #endregion + + private static async Task GetDiagnosticsWithReferencedAssemblyAsync(string source, string referencedSource) + { + static CSharpCompilation CreateCompilation(string assemblyName, string sourceText, IEnumerable references) + => CSharpCompilation.Create( + assemblyName, + [CSharpSyntaxTree.ParseText(sourceText)], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var references = GetMetadataReferences(); + var referencedCompilation = CreateCompilation("ReferencedAssembly", referencedSource, references); + + using var stream = new MemoryStream(); + var emitResult = referencedCompilation.Emit(stream); + Assert.True(emitResult.Success, string.Join(Environment.NewLine, emitResult.Diagnostics)); + + var compilation = CreateCompilation("TestProject", source, [.. references, MetadataReference.CreateFromImage(stream.ToArray())]); + var analyzer = new GenerateAliasAttributesAnalyzer(); + var compilationWithAnalyzers = compilation + .WithOptions( + compilation.Options.WithSpecificDiagnosticOptions( + analyzer.SupportedDiagnostics.ToDictionary(d => d.Id, d => ReportDiagnostic.Default))) + .WithAnalyzers([analyzer]); + + return (await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync()).ToArray(); + } + + private static IReadOnlyCollection GetMetadataReferences() + { + var assemblies = new[] + { + typeof(Task).Assembly, + typeof(Orleans.IGrain).Assembly, + typeof(Orleans.Grain).Assembly, + typeof(Attribute).Assembly, + typeof(int).Assembly, + typeof(object).Assembly, + }; + + var metadataReferences = assemblies + .SelectMany(x => x.GetReferencedAssemblies().Select(Assembly.Load)) + .Concat(assemblies) + .Distinct() + .Select(x => MetadataReference.CreateFromFile(x.Location)) + .ToList(); + + var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location); + metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath!, "mscorlib.dll"))); + metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.dll"))); + metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Core.dll"))); + metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll"))); + + return metadataReferences; + } } From 71d8e66704bba778cef083be4a56d29c395722d7 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Sat, 14 Feb 2026 16:30:52 -0800 Subject: [PATCH 16/16] Add missing analyzer test coverage Add a dedicated test suite for AtMostOneOrleansConstructorAnalyzer and expand GenerateGenerateSerializerAttributeAnalyzer edge-case coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...AtMostOneOrleansConstructorAnalyzerTest.cs | 165 ++++++++++++++++++ ...GenerateSerializerAttributeAnalyzerTest.cs | 16 ++ 2 files changed, 181 insertions(+) create mode 100644 test/Analyzers.Tests/AtMostOneOrleansConstructorAnalyzerTest.cs diff --git a/test/Analyzers.Tests/AtMostOneOrleansConstructorAnalyzerTest.cs b/test/Analyzers.Tests/AtMostOneOrleansConstructorAnalyzerTest.cs new file mode 100644 index 00000000000..6241a13a3ff --- /dev/null +++ b/test/Analyzers.Tests/AtMostOneOrleansConstructorAnalyzerTest.cs @@ -0,0 +1,165 @@ +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Orleans.Analyzers; +using Xunit; + +namespace Analyzers.Tests; + +[TestCategory("BVT"), TestCategory("Analyzer")] +public class AtMostOneOrleansConstructorAnalyzerTest : DiagnosticAnalyzerTestBase +{ + [Fact] + public async Task TypeWithMultipleAttributedConstructors_ShouldTriggerDiagnostic() + { + var diagnostics = await GetDiagnosticsAsync( + """ + [Orleans.GenerateSerializer] + public class C + { + [Orleans.GenerateSerializer] + public C() + { + } + + [Orleans.GenerateSerializer] + public C(int value) + { + } + } + """); + + Assert.NotEmpty(diagnostics); + Assert.Single(diagnostics); + Assert.Equal(AtMostOneOrleansConstructorAnalyzer.RuleId, diagnostics[0].Id); + Assert.Equal(DiagnosticSeverity.Error, diagnostics[0].Severity); + } + + [Fact] + public async Task TypeWithSingleAttributedConstructor_ShouldNotTriggerDiagnostic() + { + var diagnostics = await GetDiagnosticsAsync( + """ + [Orleans.GenerateSerializer] + public class C + { + [Orleans.GenerateSerializer] + public C() + { + } + + public C(int value) + { + } + } + """); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task TypeWithoutGenerateSerializerAttribute_ShouldNotTriggerDiagnostic() + { + var diagnostics = await GetDiagnosticsAsync( + """ + public class C + { + [Orleans.GenerateSerializer] + public C() + { + } + + [Orleans.GenerateSerializer] + public C(int value) + { + } + } + """); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task GeneratedCodeWithMultipleAttributedConstructors_ShouldNotTriggerDiagnostic() + { + var diagnostics = await GetDiagnosticsAsync( + """ + // + [Orleans.GenerateSerializer] + public class C + { + [Orleans.GenerateSerializer] + public C() + { + } + + [Orleans.GenerateSerializer] + public C(int value) + { + } + } + """); + + Assert.Empty(diagnostics); + } + + private static async Task GetDiagnosticsAsync(string source) + { + const string attributeDefinition = """ + namespace Orleans; + + [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct | System.AttributeTargets.Constructor)] + public sealed class GenerateSerializerAttribute : System.Attribute + { + } + """; + + var compilation = CreateCompilation( + "TestProject", + [CSharpSyntaxTree.ParseText(attributeDefinition), CSharpSyntaxTree.ParseText(source)], + GetMetadataReferences()); + + var analyzer = new AtMostOneOrleansConstructorAnalyzer(); + var diagnostics = await compilation + .WithOptions( + compilation.Options.WithSpecificDiagnosticOptions( + analyzer.SupportedDiagnostics.ToDictionary(d => d.Id, d => ReportDiagnostic.Default))) + .WithAnalyzers([analyzer]) + .GetAnalyzerDiagnosticsAsync(); + + return diagnostics.ToArray(); + } + + private static CSharpCompilation CreateCompilation(string assemblyName, IEnumerable syntaxTrees, IEnumerable references) + => CSharpCompilation.Create( + assemblyName, + syntaxTrees, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + private static IReadOnlyCollection GetMetadataReferences() + { + var assemblies = new[] + { + typeof(object).Assembly, + typeof(Attribute).Assembly, + typeof(Enumerable).Assembly, + }; + + var metadataReferences = assemblies + .SelectMany(x => x.GetReferencedAssemblies().Select(Assembly.Load)) + .Concat(assemblies) + .Distinct() + .Select(x => MetadataReference.CreateFromFile(x.Location)) + .ToList(); + + var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location); + metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath!, "mscorlib.dll"))); + metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.dll"))); + metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Core.dll"))); + metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll"))); + + return metadataReferences; + } +} diff --git a/test/Analyzers.Tests/GenerateGenerateSerializerAttributeAnalyzerTest.cs b/test/Analyzers.Tests/GenerateGenerateSerializerAttributeAnalyzerTest.cs index 6495803cdc2..bed67e45803 100644 --- a/test/Analyzers.Tests/GenerateGenerateSerializerAttributeAnalyzerTest.cs +++ b/test/Analyzers.Tests/GenerateGenerateSerializerAttributeAnalyzerTest.cs @@ -25,6 +25,12 @@ private async Task VerifyGeneratedDiagnostic(string code) Assert.Equal(DiagnosticSeverity.Info, diagnostic.Severity); } + private async Task VerifyNoDiagnostic(string code) + { + var (diagnostics, _) = await GetDiagnosticsAsync(code, new string[0]); + Assert.Empty(diagnostics); + } + /// /// Verifies that the analyzer detects when a class uses [Serializable] attribute /// and suggests using [GenerateSerializer] instead for better Orleans serialization performance. @@ -56,4 +62,14 @@ public Task SerializableRecord() [Fact] public Task SerializableRecordStruct() => VerifyGeneratedDiagnostic(@"[System.Serializable] public record struct D { }"); + + [Fact] + public Task SerializableClassWithGenerateSerializer_ShouldNotTriggerDiagnostic() + => VerifyNoDiagnostic( + """ + [System.Serializable, GenerateSerializer] + public class D + { + } + """); }