Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Analyzer and CodeFix for duplicate method aliases (ORLEANS0011) #8662

Merged
merged 1 commit into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions src/Orleans.Analyzers/AliasClashAttributeAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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 partial 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<DiagnosticDescriptor> 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<AttributeArgumentBag<string>> bags = new();
foreach (var methodDeclaration in interfaceDeclaration.Members.OfType<MethodDeclarationSyntax>())
{
var attributes = methodDeclaration.AttributeLists.GetAttributeSyntaxes(Constants.AliasAttributeName);
foreach (var attribute in attributes)
{
var bag = attribute.GetArgumentBag<string>(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 (prefix, suffix) = ParsePrefixAndNumericSuffix(duplicateAlias);

filteredBags = filteredBags.Skip(1);

foreach (var bag in filteredBags)
{
string newAlias;
do
{
++suffix;
newAlias = $"{prefix}{suffix}";
} while (bags.Any(x => x.Value.Equals(newAlias, StringComparison.Ordinal)));

var builder = ImmutableDictionary.CreateBuilder<string, string>();

builder.Add("AliasName", prefix);
builder.Add("AliasSuffix", suffix.ToString(System.Globalization.CultureInfo.InvariantCulture));

context.ReportDiagnostic(Diagnostic.Create(
descriptor: Rule,
location: bag.Location,
properties: builder.ToImmutable()));

suffix++;
}
}
}
}

private static (string Prefix, ulong Suffix) ParsePrefixAndNumericSuffix(string input)
{
var suffixLength = GetNumericSuffixLength(input);
if (suffixLength == 0)
{
return (input, 0);
}

return (input.Substring(0, input.Length - suffixLength), ulong.Parse(input.Substring(input.Length - suffixLength)));
}

private static int GetNumericSuffixLength(string input)
{
var suffixLength = 0;
for (var c = input.Length - 1; c > 0; --c)
{
if (!char.IsDigit(input[c]))
{
break;
}

++suffixLength;
}

return suffixLength;
}
}
51 changes: 51 additions & 0 deletions src/Orleans.Analyzers/AliasClashAttributeCodeFix.cs
Original file line number Diff line number Diff line change
@@ -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<string> 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);
}
}
3 changes: 3 additions & 0 deletions src/Orleans.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ ORLEANS0007 | Usage | Error |
ORLEANS0008 | Usage | Error | Grain interfaces cannot have properties
ORLEANS0009 | Usage | Error | Grain interface methods must return a compatible type
ORLEANS0010 | Usage | Info | Add missing [Alias] attribute
ORLEANS0011 | Usage | Error | The [Alias] attribute must be unique to the declaring type
ORLEANS0012 | Usage | Error | The [Id] attribute must be unique to each members of the declaring type
ORLEANS0013 | Usage | Error | This attribute should not be used on grain implementations

### Removed Rules

Expand Down
1 change: 1 addition & 0 deletions src/Orleans.Analyzers/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
18 changes: 11 additions & 7 deletions src/Orleans.Analyzers/GenerateAliasAttributesAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,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,
Expand All @@ -39,9 +39,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;
}
Expand Down Expand Up @@ -75,6 +73,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;
Expand Down Expand Up @@ -148,8 +152,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()));
}
}
88 changes: 88 additions & 0 deletions src/Orleans.Analyzers/IdClashAttributeAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -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<DiagnosticDescriptor> 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<AttributeArgumentBag<int>> bags = new();
foreach (var memberDeclaration in typeDeclaration.Members.OfType<MemberDeclarationSyntax>())
{
var attributes = memberDeclaration.AttributeLists.GetAttributeSyntaxes(Constants.IdAttributeName);
foreach (var attribute in attributes)
{
var bag = attribute.GetArgumentBag<int>(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));
}
}
}
}
}
57 changes: 57 additions & 0 deletions src/Orleans.Analyzers/IncorrectAttributeUseAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -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<DiagnosticDescriptor> 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 }));
}
}
}
Loading
Loading