Skip to content

Commit

Permalink
Add CodeFix to rename duplicate [Alias(...)] values
Browse files Browse the repository at this point in the history
  • Loading branch information
Ledjon Behluli authored and ReubenBond committed Nov 12, 2023
1 parent f29d7e4 commit f52ca9f
Show file tree
Hide file tree
Showing 16 changed files with 911 additions and 26 deletions.
112 changes: 112 additions & 0 deletions src/Orleans.Analyzers/AliasClashAttributeAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -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<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 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<string, string>();

builder.Add("AliasName", duplicateAlias);
builder.Add("AliasSuffix", suffix.ToString());

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

suffix++;
}
}
}
}
}
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);
}
}
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 @@ -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,
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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()));
}
}
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

0 comments on commit f52ca9f

Please sign in to comment.