-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add CodeFix to rename duplicate [Alias(...)] values
- Loading branch information
1 parent
f29d7e4
commit f52ca9f
Showing
16 changed files
with
911 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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++; | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 })); | ||
} | ||
} | ||
} |
Oops, something went wrong.