-
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 Analyzer and CodeFix for duplicate method aliases (ORLEANS0011)
- Loading branch information
1 parent
876d4ec
commit 4335591
Showing
16 changed files
with
926 additions
and
27 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,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; | ||
} | ||
} |
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
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.