From f52ca9fb2e28d430c2105baca228ad60a8b53a8f Mon Sep 17 00:00:00 2001 From: Ledjon Behluli Date: Tue, 10 Oct 2023 14:49:39 +0200 Subject: [PATCH] Add CodeFix to rename duplicate [Alias(...)] values --- .../AliasClashAttributeAnalyzer.cs | 112 ++++++++++++++ .../AliasClashAttributeCodeFix.cs | 51 +++++++ src/Orleans.Analyzers/Constants.cs | 1 + .../GenerateAliasAttributesAnalyzer.cs | 18 ++- .../IdClashAttributeAnalyzer.cs | 88 +++++++++++ .../IncorrectAttributeUseAnalyzer.cs | 57 ++++++++ .../IncorrectAttributeUseCodeFix.cs | 41 ++++++ src/Orleans.Analyzers/Resources.Designer.cs | 87 ++++++++++- src/Orleans.Analyzers/Resources.resx | 33 ++++- src/Orleans.Analyzers/SyntaxHelpers.cs | 69 ++++++++- .../AliasClashAttributeAnalyzerTest.cs | 75 ++++++++++ .../DiagnosticAnalyzerTestBase.cs | 11 ++ .../GenerateAliasAttributesAnalyzerTest.cs | 11 -- .../IdClashAttributeAnalyzerTest.cs | 138 ++++++++++++++++++ .../IncorrectAttributeUseAnalyzerTest.cs | 137 +++++++++++++++++ .../Properties/launchSettings.json | 8 + 16 files changed, 911 insertions(+), 26 deletions(-) create mode 100644 src/Orleans.Analyzers/AliasClashAttributeAnalyzer.cs create mode 100644 src/Orleans.Analyzers/AliasClashAttributeCodeFix.cs create mode 100644 src/Orleans.Analyzers/IdClashAttributeAnalyzer.cs create mode 100644 src/Orleans.Analyzers/IncorrectAttributeUseAnalyzer.cs create mode 100644 src/Orleans.Analyzers/IncorrectAttributeUseCodeFix.cs create mode 100644 test/Analyzers.Tests/AliasClashAttributeAnalyzerTest.cs create mode 100644 test/Analyzers.Tests/IdClashAttributeAnalyzerTest.cs create mode 100644 test/Analyzers.Tests/IncorrectAttributeUseAnalyzerTest.cs create mode 100644 test/DistributedTests/DistributedTests.Client/Properties/launchSettings.json diff --git a/src/Orleans.Analyzers/AliasClashAttributeAnalyzer.cs b/src/Orleans.Analyzers/AliasClashAttributeAnalyzer.cs new file mode 100644 index 00000000000..0a381258d44 --- /dev/null +++ b/src/Orleans.Analyzers/AliasClashAttributeAnalyzer.cs @@ -0,0 +1,112 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Orleans.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class AliasClashAttributeAnalyzer : DiagnosticAnalyzer +{ + private readonly record struct AliasBag(string Name, Location Location); + + public const string RuleId = "ORLEANS0011"; + + private static readonly DiagnosticDescriptor Rule = new( + id: RuleId, + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + title: new LocalizableResourceString(nameof(Resources.AliasClashDetectedTitle), Resources.ResourceManager, typeof(Resources)), + messageFormat: new LocalizableResourceString(nameof(Resources.AliasClashDetectedMessageFormat), Resources.ResourceManager, typeof(Resources)), + description: new LocalizableResourceString(nameof(Resources.AliasClashDetectedDescription), Resources.ResourceManager, typeof(Resources))); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.RegisterSyntaxNodeAction(CheckSyntaxNode, SyntaxKind.InterfaceDeclaration); + } + + private void CheckSyntaxNode(SyntaxNodeAnalysisContext context) + { + var interfaceDeclaration = (InterfaceDeclarationSyntax)context.Node; + if (!interfaceDeclaration.ExtendsGrainInterface(context.SemanticModel)) + { + return; + } + + List> bags = new(); + foreach (var methodDeclaration in interfaceDeclaration.Members.OfType()) + { + var attributes = methodDeclaration.AttributeLists.GetAttributeSyntaxes(Constants.AliasAttributeName); + foreach (var attribute in attributes) + { + var bag = attribute.GetArgumentBag(context.SemanticModel); + if (bag != default) + { + bags.Add(bag); + } + } + } + + var duplicateAliases = bags + .GroupBy(alias => alias.Value) + .Where(group => group.Count() > 1) + .Select(group => group.Key); + + if (!duplicateAliases.Any()) + { + return; + } + + foreach (var duplicateAlias in duplicateAliases) + { + var filteredBags = bags.Where(x => x.Value == duplicateAlias); + var duplicateCount = filteredBags.Count(); + + if (duplicateCount > 1) + { + var suffix = 1; + filteredBags = filteredBags.Skip(1); + + foreach (var bag in filteredBags) + { + var condition = true; + while (condition) + { + var newAlias = $"{duplicateAlias}{suffix}"; + var exists = bags.Any(x => x.Value.Equals(newAlias, StringComparison.Ordinal)); + + if (exists) + { + suffix++; + } + else + { + condition = false; + } + } + + var builder = ImmutableDictionary.CreateBuilder(); + + builder.Add("AliasName", duplicateAlias); + builder.Add("AliasSuffix", suffix.ToString()); + + context.ReportDiagnostic(Diagnostic.Create( + descriptor: Rule, + location: bag.Location, + properties: builder.ToImmutable())); + + suffix++; + } + } + } + } +} \ No newline at end of file diff --git a/src/Orleans.Analyzers/AliasClashAttributeCodeFix.cs b/src/Orleans.Analyzers/AliasClashAttributeCodeFix.cs new file mode 100644 index 00000000000..cd728ce5642 --- /dev/null +++ b/src/Orleans.Analyzers/AliasClashAttributeCodeFix.cs @@ -0,0 +1,51 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading.Tasks; +using System.Threading; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Orleans.Analyzers; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(GenerateAliasAttributesCodeFix)), Shared] +public class AliasClashAttributeCodeFix : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AliasClashAttributeAnalyzer.RuleId); + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var diagnostic = context.Diagnostics.First(); + if (root.FindNode(diagnostic.Location.SourceSpan) is not AttributeSyntax attribute) + { + return; + } + + var aliasName = diagnostic.Properties["AliasName"]; + var aliasSuffix = diagnostic.Properties["AliasSuffix"]; + + context.RegisterCodeFix( + CodeAction.Create( + Resources.AliasClashDetectedTitle, + createChangedDocument: _ => + { + var newAliasName = $"{aliasName}{aliasSuffix}"; + var newAttribute = attribute.ReplaceNode( + attribute.ArgumentList.Arguments[0].Expression, + LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(newAliasName))); + + var newRoot = root.ReplaceNode(attribute, newAttribute); + var newDocument = context.Document.WithSyntaxRoot(newRoot); + + return Task.FromResult(newDocument); + }, + equivalenceKey: AliasClashAttributeAnalyzer.RuleId), + diagnostic); + } +} \ No newline at end of file diff --git a/src/Orleans.Analyzers/Constants.cs b/src/Orleans.Analyzers/Constants.cs index b092cdc1e24..c61794f2a26 100644 --- a/src/Orleans.Analyzers/Constants.cs +++ b/src/Orleans.Analyzers/Constants.cs @@ -5,6 +5,7 @@ internal static class Constants public const string SystemNamespace = "System"; public const string IAddressibleFullyQualifiedName = "Orleans.Runtime.IAddressable"; + public const string GrainBaseFullyQualifiedName = "Orleans.Grain"; public const string IdAttributeName = "Id"; public const string IdAttributeFullyQualifiedName = "global::Orleans.IdAttribute"; diff --git a/src/Orleans.Analyzers/GenerateAliasAttributesAnalyzer.cs b/src/Orleans.Analyzers/GenerateAliasAttributesAnalyzer.cs index a5f5bb61018..cd6c2de3619 100644 --- a/src/Orleans.Analyzers/GenerateAliasAttributesAnalyzer.cs +++ b/src/Orleans.Analyzers/GenerateAliasAttributesAnalyzer.cs @@ -26,7 +26,7 @@ public class GenerateAliasAttributesAnalyzer : DiagnosticAnalyzer public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.RegisterSyntaxNodeAction(CheckSyntaxNode, SyntaxKind.InterfaceDeclaration, SyntaxKind.ClassDeclaration, @@ -40,9 +40,7 @@ private void CheckSyntaxNode(SyntaxNodeAnalysisContext context) // Interface types and their methods if (context.Node is InterfaceDeclarationSyntax { } interfaceDeclaration) { - if (!context.SemanticModel - .GetDeclaredSymbol(interfaceDeclaration, context.CancellationToken) - .ExtendsGrainInterface()) + if (!interfaceDeclaration.ExtendsGrainInterface(context.SemanticModel)) { return; } @@ -76,6 +74,12 @@ private void CheckSyntaxNode(SyntaxNodeAnalysisContext context) // Rest of types: class, struct, record if (context.Node is TypeDeclarationSyntax { } typeDeclaration) { + if (typeDeclaration is ClassDeclarationSyntax classDeclaration && + classDeclaration.InheritsGrainClass(context.SemanticModel)) + { + return; + } + if (!typeDeclaration.HasAttribute(Constants.GenerateSerializerAttributeName)) { return; @@ -149,8 +153,8 @@ private static void ReportFor(SyntaxNodeAnalysisContext context, Location locati builder.Add("Arity", arity.ToString(System.Globalization.CultureInfo.InvariantCulture)); context.ReportDiagnostic(Diagnostic.Create( - descriptor: Rule, - location: location, - properties: builder.ToImmutable())); + descriptor: Rule, + location: location, + properties: builder.ToImmutable())); } } diff --git a/src/Orleans.Analyzers/IdClashAttributeAnalyzer.cs b/src/Orleans.Analyzers/IdClashAttributeAnalyzer.cs new file mode 100644 index 00000000000..96a3df2bb51 --- /dev/null +++ b/src/Orleans.Analyzers/IdClashAttributeAnalyzer.cs @@ -0,0 +1,88 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Orleans.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class IdClashAttributeAnalyzer : DiagnosticAnalyzer +{ + private readonly record struct AliasBag(string Name, Location Location); + + public const string RuleId = "ORLEANS0012"; + + private static readonly DiagnosticDescriptor Rule = new( + id: RuleId, + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + title: new LocalizableResourceString(nameof(Resources.IdClashDetectedTitle), Resources.ResourceManager, typeof(Resources)), + messageFormat: new LocalizableResourceString(nameof(Resources.IdClashDetectedMessageFormat), Resources.ResourceManager, typeof(Resources)), + description: new LocalizableResourceString(nameof(Resources.IdClashDetectedDescription), Resources.ResourceManager, typeof(Resources))); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + 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); + } + + private void CheckSyntaxNode(SyntaxNodeAnalysisContext context) + { + var typeDeclaration = context.Node as TypeDeclarationSyntax; + if (!typeDeclaration.HasAttribute(Constants.GenerateSerializerAttributeName)) + { + return; + } + + List> bags = new(); + foreach (var memberDeclaration in typeDeclaration.Members.OfType()) + { + var attributes = memberDeclaration.AttributeLists.GetAttributeSyntaxes(Constants.IdAttributeName); + foreach (var attribute in attributes) + { + var bag = attribute.GetArgumentBag(context.SemanticModel); + if (bag != default) + { + bags.Add(bag); + } + } + } + + var duplicateIds = bags + .GroupBy(id => id.Value) + .Where(group => group.Count() > 1) + .Select(group => group.Key); + + if (!duplicateIds.Any()) + { + return; + } + + foreach (var duplicateId in duplicateIds) + { + var filteredBags = bags.Where(x => x.Value == duplicateId); + var duplicateCount = filteredBags.Count(); + + if (duplicateCount > 1) + { + foreach (var bag in filteredBags) + { + context.ReportDiagnostic(Diagnostic.Create( + descriptor: Rule, + location: bag.Location)); + } + } + } + } +} \ No newline at end of file diff --git a/src/Orleans.Analyzers/IncorrectAttributeUseAnalyzer.cs b/src/Orleans.Analyzers/IncorrectAttributeUseAnalyzer.cs new file mode 100644 index 00000000000..5cb3a941c92 --- /dev/null +++ b/src/Orleans.Analyzers/IncorrectAttributeUseAnalyzer.cs @@ -0,0 +1,57 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; + +namespace Orleans.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class IncorrectAttributeUseAnalyzer : DiagnosticAnalyzer +{ + public const string RuleId = "ORLEANS0013"; + + private static readonly DiagnosticDescriptor Rule = new( + id: RuleId, + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + title: new LocalizableResourceString(nameof(Resources.IncorrectAttributeUseTitle), Resources.ResourceManager, typeof(Resources)), + messageFormat: new LocalizableResourceString(nameof(Resources.IncorrectAttributeUseMessageFormat), Resources.ResourceManager, typeof(Resources)), + description: new LocalizableResourceString(nameof(Resources.IncorrectAttributeUseTitleDescription), Resources.ResourceManager, typeof(Resources))); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.RegisterSyntaxNodeAction(CheckSyntaxNode, SyntaxKind.ClassDeclaration); + } + + private void CheckSyntaxNode(SyntaxNodeAnalysisContext context) + { + if (context.Node is not ClassDeclarationSyntax) return; + + var classDeclaration = (ClassDeclarationSyntax)context.Node; + + if (!classDeclaration.InheritsGrainClass(context.SemanticModel)) + { + return; + } + + TryReportFor(Constants.AliasAttributeName, context, classDeclaration); + TryReportFor(Constants.GenerateSerializerAttributeName, context, classDeclaration); + } + + private static void TryReportFor(string attributeTypeName, SyntaxNodeAnalysisContext context, ClassDeclarationSyntax classDeclaration) + { + if (classDeclaration.TryGetAttribute(attributeTypeName, out var attribute)) + { + context.ReportDiagnostic(Diagnostic.Create( + descriptor: Rule, + location: attribute.GetLocation(), + messageArgs: new object[] { attributeTypeName })); + } + } +} \ No newline at end of file diff --git a/src/Orleans.Analyzers/IncorrectAttributeUseCodeFix.cs b/src/Orleans.Analyzers/IncorrectAttributeUseCodeFix.cs new file mode 100644 index 00000000000..9874ab11643 --- /dev/null +++ b/src/Orleans.Analyzers/IncorrectAttributeUseCodeFix.cs @@ -0,0 +1,41 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using System.Collections.Immutable; +using System.Threading.Tasks; +using System.Composition; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Linq; + +namespace Orleans.Analyzers; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(IncorrectAttributeUseCodeFix)), Shared] +public class IncorrectAttributeUseCodeFix : CodeFixProvider +{ + public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(IncorrectAttributeUseAnalyzer.RuleId); + public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var diagnostic = context.Diagnostics.First(); + + if (root.FindNode(diagnostic.Location.SourceSpan) is not AttributeSyntax node) + { + return; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: Resources.IncorrectAttributeUseTitle, + createChangedDocument: token => + { + var newRoot = root.RemoveNode(node.Parent, SyntaxRemoveOptions.KeepEndOfLine); + return Task.FromResult(context.Document.WithSyntaxRoot(newRoot)); + + }, + equivalenceKey: IncorrectAttributeUseAnalyzer.RuleId), + diagnostic); + } + +} \ No newline at end of file diff --git a/src/Orleans.Analyzers/Resources.Designer.cs b/src/Orleans.Analyzers/Resources.Designer.cs index 608bc774684..642c20272a2 100644 --- a/src/Orleans.Analyzers/Resources.Designer.cs +++ b/src/Orleans.Analyzers/Resources.Designer.cs @@ -79,7 +79,7 @@ internal static string AbstractOrStaticMembersCannotBeSerializedTitle { } /// - /// Looks up a localized string similar to Add [Alias] attributes to specify well-known names that can be used to identify types or methods. + /// Looks up a localized string similar to Add [Alias] to specify well-known names that can be used to identify types or methods. /// internal static string AddAliasAttributesDescription { get { @@ -88,7 +88,7 @@ internal static string AddAliasAttributesDescription { } /// - /// Looks up a localized string similar to Add missing alias attributes. + /// Looks up a localized string similar to Add missing [Alias]. /// internal static string AddAliasAttributesTitle { get { @@ -97,7 +97,7 @@ internal static string AddAliasAttributesTitle { } /// - /// Looks up a localized string similar to Add missing alias attributes. + /// Looks up a localized string similar to Add missing [Alias]. /// internal static string AddAliasMessageFormat { get { @@ -159,6 +159,33 @@ internal static string AddSerializationAttributesTitle { } } + /// + /// Looks up a localized string similar to The [Alias] attribute must be unique to the declaring type. + /// + internal static string AliasClashDetectedDescription { + get { + return ResourceManager.GetString("AliasClashDetectedDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Rename duplicated [Alias]. + /// + internal static string AliasClashDetectedMessageFormat { + get { + return ResourceManager.GetString("AliasClashDetectedMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Rename duplicated [Alias]. + /// + internal static string AliasClashDetectedTitle { + get { + return ResourceManager.GetString("AliasClashDetectedTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to A single type is not allowed to have multiple constructors annotated with the [OrleansConstructor] attribute. /// @@ -176,5 +203,59 @@ internal static string AtMostOneOrleansConstructorTitle { return ResourceManager.GetString("AtMostOneOrleansConstructorTitle", resourceCulture); } } + + /// + /// Looks up a localized string similar to The [Id] attribute must be unique to each members of the declaring type. + /// + internal static string IdClashDetectedDescription { + get { + return ResourceManager.GetString("IdClashDetectedDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Substitute duplicated [Id] value with the correct unique identity of this member. + /// + internal static string IdClashDetectedMessageFormat { + get { + return ResourceManager.GetString("IdClashDetectedMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Substitute duplicated [Id] value with the correct unique identity of this member. + /// + internal static string IdClashDetectedTitle { + get { + return ResourceManager.GetString("IdClashDetectedTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove attribute [{0}]. + /// + internal static string IncorrectAttributeUseMessageFormat { + get { + return ResourceManager.GetString("IncorrectAttributeUseMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove attribute. + /// + internal static string IncorrectAttributeUseTitle { + get { + return ResourceManager.GetString("IncorrectAttributeUseTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This attribute should not be used on grain implementations. + /// + internal static string IncorrectAttributeUseTitleDescription { + get { + return ResourceManager.GetString("IncorrectAttributeUseTitleDescription", resourceCulture); + } + } } } diff --git a/src/Orleans.Analyzers/Resources.resx b/src/Orleans.Analyzers/Resources.resx index 206f9b97300..d6db50c4c07 100644 --- a/src/Orleans.Analyzers/Resources.resx +++ b/src/Orleans.Analyzers/Resources.resx @@ -148,12 +148,39 @@ A single type is not allowed to have multiple constructors annotated with the [OrleansConstructor] attribute - Add [Alias] attributes to specify well-known names that can be used to identify types or methods + Add [Alias] to specify well-known names that can be used to identify types or methods - Add missing alias attributes + Add missing [Alias] - Add missing alias attributes + Add missing [Alias] + + + The [Alias] attribute must be unique to the declaring type + + + Rename duplicated [Alias] + + + Rename duplicated [Alias] + + + The [Id] attribute must be unique to each members of the declaring type + + + Substitute duplicated [Id] value with the correct unique identity of this member + + + Substitute duplicated [Id] value with the correct unique identity of this member + + + Remove attribute [{0}] + + + Remove attribute + + + This attribute should not be used on grain implementations \ No newline at end of file diff --git a/src/Orleans.Analyzers/SyntaxHelpers.cs b/src/Orleans.Analyzers/SyntaxHelpers.cs index 89718480b96..1c5c798257c 100644 --- a/src/Orleans.Analyzers/SyntaxHelpers.cs +++ b/src/Orleans.Analyzers/SyntaxHelpers.cs @@ -2,11 +2,14 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System; +using System.Collections.Generic; using System.Linq; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Orleans.Analyzers { + internal readonly record struct AttributeArgumentBag(T Value, Location Location); + internal static class SyntaxHelpers { public static bool TryGetTypeName(this AttributeSyntax attributeSyntax, out string typeName) @@ -144,9 +147,18 @@ public static bool IsFieldOrAutoProperty(this MemberDeclarationSyntax member) return isFieldOrAutoProperty; } - public static bool ExtendsGrainInterface(this INamedTypeSymbol symbol) + public static bool ExtendsGrainInterface(this InterfaceDeclarationSyntax interfaceDeclaration, SemanticModel semanticModel) { - if (symbol.TypeKind != TypeKind.Interface) return false; + if (interfaceDeclaration is null) + { + return false; + } + + var symbol = semanticModel.GetDeclaredSymbol(interfaceDeclaration); + if (symbol is null || symbol.TypeKind != TypeKind.Interface) + { + return false; + } foreach (var interfaceSymbol in symbol.AllInterfaces) { @@ -158,5 +170,58 @@ 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) + { + 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)); } } \ No newline at end of file diff --git a/test/Analyzers.Tests/AliasClashAttributeAnalyzerTest.cs b/test/Analyzers.Tests/AliasClashAttributeAnalyzerTest.cs new file mode 100644 index 00000000000..e4f83f39b69 --- /dev/null +++ b/test/Analyzers.Tests/AliasClashAttributeAnalyzerTest.cs @@ -0,0 +1,75 @@ +using Microsoft.CodeAnalysis; +using Orleans.Analyzers; +using Xunit; + +namespace Analyzers.Tests; + +[TestCategory("BVT"), TestCategory("Analyzer")] +public class AliasClashAttributeAnalyzerTest : DiagnosticAnalyzerTestBase +{ + private async Task VerifyHasDiagnostic(string code) + { + var (diagnostics, _) = await GetDiagnosticsAsync(code, Array.Empty()); + + Assert.NotEmpty(diagnostics); + var diagnostic = diagnostics.First(); + + Assert.Equal(AliasClashAttributeAnalyzer.RuleId, diagnostic.Id); + Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity); + } + + private async Task VerifyHasNoDiagnostic(string code) + { + var (diagnostics, _) = await GetDiagnosticsAsync(code, Array.Empty()); + Assert.Empty(diagnostics); + } + + [Theory] + [MemberData(nameof(GrainInterfaces))] + public Task SameAliasWithinDeclaringType_ShouldTriggerDiagnostic(string grainInterface) + { + var code = $$""" + public interface I : {{grainInterface}} + { + [Alias("Void")] Task Void(int a); + [Alias("Void")] Task Void(long a); + } + """; + + return VerifyHasDiagnostic(code); + } + + [Theory] + [MemberData(nameof(GrainInterfaces))] + public Task DifferentAliasWithinDeclaringType_ShouldNotTriggerDiagnostic(string grainInterface) + { + var code = $$""" + public interface I : {{grainInterface}} + { + [Alias("Void")] Task Void(int a); + [Alias("Void1")] Task Void(long a); + } + """; + + return VerifyHasNoDiagnostic(code); + } + + [Theory] + [MemberData(nameof(GrainInterfaces))] + public Task SameAliasOutsideDeclaringType_ShouldNotTriggerDiagnostic(string grainInterface) + { + var code = $$""" + public interface I1 : {{grainInterface}} + { + [Alias("Void")] Task Void(string a); + } + + public interface I2 + { + [Alias("Void")] Task Void(string a); + } + """; + + return VerifyHasNoDiagnostic(code); + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/DiagnosticAnalyzerTestBase.cs b/test/Analyzers.Tests/DiagnosticAnalyzerTestBase.cs index 6fd99b18ae5..61d9b4e2ef8 100644 --- a/test/Analyzers.Tests/DiagnosticAnalyzerTestBase.cs +++ b/test/Analyzers.Tests/DiagnosticAnalyzerTestBase.cs @@ -24,6 +24,17 @@ public abstract class DiagnosticAnalyzerTestBase "Orleans" }; + public static IEnumerable GrainInterfaces => + new List + { + new object[] { "Orleans.IGrain" }, + new object[] { "Orleans.IGrainWithStringKey" }, + new object[] { "Orleans.IGrainWithGuidKey" }, + new object[] { "Orleans.IGrainWithGuidCompoundKey" }, + new object[] { "Orleans.IGrainWithIntegerKey" }, + new object[] { "Orleans.IGrainWithIntegerCompoundKey" } + }; + protected virtual DiagnosticAnalyzer CreateDiagnosticAnalyzer() => new TDiagnosticAnalyzer(); protected async Task AssertNoDiagnostics(string source, params string[] extraUsings) diff --git a/test/Analyzers.Tests/GenerateAliasAttributesAnalyzerTest.cs b/test/Analyzers.Tests/GenerateAliasAttributesAnalyzerTest.cs index 9d6f83a67d6..f435f80d240 100644 --- a/test/Analyzers.Tests/GenerateAliasAttributesAnalyzerTest.cs +++ b/test/Analyzers.Tests/GenerateAliasAttributesAnalyzerTest.cs @@ -79,17 +79,6 @@ public interface I return VerifyHasNoDiagnostic(code); } - public static IEnumerable GrainInterfaces => - new List - { - new object[] { "Orleans.IGrain" }, - new object[] { "Orleans.IGrainWithStringKey" }, - new object[] { "Orleans.IGrainWithGuidKey" }, - new object[] { "Orleans.IGrainWithGuidCompoundKey" }, - new object[] { "Orleans.IGrainWithIntegerKey" }, - new object[] { "Orleans.IGrainWithIntegerCompoundKey" } - }; - #endregion #region Classes, Structs, Records diff --git a/test/Analyzers.Tests/IdClashAttributeAnalyzerTest.cs b/test/Analyzers.Tests/IdClashAttributeAnalyzerTest.cs new file mode 100644 index 00000000000..308981f02c9 --- /dev/null +++ b/test/Analyzers.Tests/IdClashAttributeAnalyzerTest.cs @@ -0,0 +1,138 @@ +using Microsoft.CodeAnalysis; +using Orleans.Analyzers; +using Xunit; + +namespace Analyzers.Tests; + +[TestCategory("BVT"), TestCategory("Analyzer")] +public class IdClashAttributeAnalyzerTest : DiagnosticAnalyzerTestBase +{ + private async Task VerifyHasDiagnostic(string code, int diagnosticsCount) + { + var (diagnostics, _) = await GetDiagnosticsAsync(code, Array.Empty()); + + Assert.NotEmpty(diagnostics); + Assert.Equal(diagnosticsCount, diagnostics.Length); + + var diagnostic = diagnostics.First(); + + Assert.Equal(IdClashAttributeAnalyzer.RuleId, diagnostic.Id); + Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity); + } + + private async Task VerifyHasNoDiagnostic(string code) + { + var (diagnostics, _) = await GetDiagnosticsAsync(code, Array.Empty()); + Assert.Empty(diagnostics); + } + + [Fact] + public Task TypesWithGenerateSerializerAndDuplicatedIds_ShouldTriggerDiagnostic() + { + var code = """ + [GenerateSerializer] + public class C + { + [Id(0)] public string P1 { get; set; } + [Id(1)] public string P2 { get; set; } + [Id(1)] public string P3 { get; set; } + } + + [GenerateSerializer] + public struct S + { + [Id(0)] public string P1 { get; set; } + [Id(1)] public string P2 { get; set; } + [Id(1)] public string P3 { get; set; } + } + + [GenerateSerializer] + public record R(string P1, string P2, string P3) + { + [Id(0)] public string P4 { get; set; } + [Id(0)] public string P5 { get; set; } + } + + [GenerateSerializer] + public record struct RS(string P1, string P2, string P3) + { + [Id(0)] public string P4 { get; set; } + [Id(0)] public string P5 { get; set; } + } + """; + + return VerifyHasDiagnostic(code, 8); + } + + [Fact] + public Task TypesWithGenerateSerializerAndNoDuplicatedIds_ShouldNoTriggerDiagnostic() + { + var code = """ + [GenerateSerializer] + public class C + { + [Id(0)] public string P1 { get; set; } + [Id(1)] public string P2 { get; set; } + [Id(2)] public string P3 { get; set; } + } + + [GenerateSerializer] + public struct S + { + [Id(0)] public string P1 { get; set; } + [Id(1)] public string P2 { get; set; } + [Id(2)] public string P3 { get; set; } + } + + [GenerateSerializer] + public record R(string P1, string P2, string P3) + { + [Id(0)] public string P4 { get; set; } + [Id(1)] public string P5 { get; set; } + } + + [GenerateSerializer] + public record struct RS(string P1, string P2, string P3) + { + [Id(0)] public string P4 { get; set; } + [Id(1)] public string P5 { get; set; } + } + """; + + return VerifyHasNoDiagnostic(code); + } + + [Fact] + public Task TypesWithoutGenerateSerializerAndDuplicatedIds_ShouldNotTriggerDiagnostic() + { + var code = """ + public class C + { + [Id(0)] public string P1 { get; set; } + [Id(1)] public string P2 { get; set; } + [Id(1)] public string P3 { get; set; } + } + + public struct S + { + [Id(0)] public string P1 { get; set; } + [Id(1)] public string P2 { get; set; } + [Id(1)] public string P3 { get; set; } + } + + public record R(string P1, string P2, string P3) + { + [Id(0)] public string P4 { get; set; } + [Id(0)] public string P5 { get; set; } + } + + public record struct RS(string P1, string P2, string P3) + { + [Id(0)] public string P4 { get; set; } + [Id(0)] public string P5 { get; set; } + } + """; + + return VerifyHasNoDiagnostic(code); + } +} diff --git a/test/Analyzers.Tests/IncorrectAttributeUseAnalyzerTest.cs b/test/Analyzers.Tests/IncorrectAttributeUseAnalyzerTest.cs new file mode 100644 index 00000000000..0776ce751a2 --- /dev/null +++ b/test/Analyzers.Tests/IncorrectAttributeUseAnalyzerTest.cs @@ -0,0 +1,137 @@ +using Microsoft.CodeAnalysis; +using Orleans.Analyzers; +using Xunit; + +namespace Analyzers.Tests; + +[TestCategory("BVT"), TestCategory("Analyzer")] +public class IncorrectAttributeUseAnalyzerTest : DiagnosticAnalyzerTestBase +{ + private async Task VerifyHasDiagnostic(string code) + { + var (diagnostics, _) = await GetDiagnosticsAsync(code, Array.Empty()); + + Assert.NotEmpty(diagnostics); + var diagnostic = diagnostics.First(); + + Assert.Equal(IncorrectAttributeUseAnalyzer.RuleId, diagnostic.Id); + Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity); + } + + private async Task VerifyHasNoDiagnostic(string code) + { + var (diagnostics, _) = await GetDiagnosticsAsync(code, Array.Empty()); + Assert.Empty(diagnostics); + } + + #region Grain + + [Theory] + [MemberData(nameof(Attributes))] + public Task ClassInheritingFromGrain_HavingAttributeApplied_ShouldTriggerDiagnostic(string attribute) + { + var code = $$""" + [{{attribute}}] + public class C : Grain + { + + } + """; + + return VerifyHasDiagnostic(code); + } + + [Theory] + [MemberData(nameof(Attributes))] + public Task ClassNotInheritingFromGrain_HavingAttributeApplied_ShouldNotTriggerDiagnostic(string attribute) + { + var code = $$""" + [{{attribute}}] + public class C + { + + } + """; + + return VerifyHasNoDiagnostic(code); + } + + [Fact] + public Task ClassInheritingFromGrain_NotHavingAttributeApplied_ShouldNotTriggerDiagnostic() + { + var code = """ + public class C : Grain + { + + } + """; + + return VerifyHasNoDiagnostic(code); + } + + #endregion + + #region Grain + + [Theory] + [MemberData(nameof(Attributes))] + public Task ClassInheritingFromGenericGrain_HavingAttributeApplied_ShouldTriggerDiagnostic(string attribute) + { + var code = $$""" + [{{attribute}}] + public class C : Grain + { + + } + + public class S + { + + } + """; + + return VerifyHasDiagnostic(code); + } + + [Theory] + [MemberData(nameof(Attributes))] + public Task ClassNotInheritingFromGenericGrain_HavingAttributeApplied_ShouldNotTriggerDiagnostic(string attribute) + { + var code = $$""" + [{{attribute}}] + public class C + { + + } + """; + + return VerifyHasNoDiagnostic(code); + } + + [Fact] + public Task ClassInheritingFromGenericGrain_NotHavingAttributeApplied_ShouldNotTriggerDiagnostic() + { + var code = """ + public class C : Grain + { + + } + + public class S + { + + } + """; + + return VerifyHasNoDiagnostic(code); + } + + #endregion + + public static IEnumerable Attributes => + new List + { + new object[] { "Alias(\"alias\")" }, + new object[] { "GenerateSerializer" } + }; +} diff --git a/test/DistributedTests/DistributedTests.Client/Properties/launchSettings.json b/test/DistributedTests/DistributedTests.Client/Properties/launchSettings.json new file mode 100644 index 00000000000..33504c948ad --- /dev/null +++ b/test/DistributedTests/DistributedTests.Client/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "WSL": { + "commandName": "WSL2", + "distributionName": "" + } + } +} \ No newline at end of file