Skip to content
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
207 changes: 141 additions & 66 deletions src/Orleans.Analyzers/AliasClashAttributeAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,102 +2,178 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Globalization;

namespace Orleans.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public partial class AliasClashAttributeAnalyzer : DiagnosticAnalyzer
public class AliasClashAttributeAnalyzer : DiagnosticAnalyzer
{
private readonly record struct AliasBag(string Name, Location Location);
private readonly record struct TypeAliasInfo(string TypeName, Location Location);

public const string RuleId = "ORLEANS0011";

private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
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)));
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)),
helpLinkUri: null,
customTags: [WellKnownDiagnosticTags.CompilationEnd]);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule];

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(
GeneratedCodeAnalysisFlags.Analyze |
GeneratedCodeAnalysisFlags.ReportDiagnostics);

context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.RegisterSyntaxNodeAction(CheckSyntaxNode, SyntaxKind.InterfaceDeclaration);

context.RegisterCompilationStartAction(compilationContext =>
{
var aliasMap = new ConcurrentDictionary<string, ConcurrentBag<TypeAliasInfo>>();

compilationContext.RegisterSyntaxNodeAction(
nodeContext => CollectTypeAliases(nodeContext, aliasMap),
SyntaxKind.EnumDeclaration,
SyntaxKind.ClassDeclaration,
SyntaxKind.StructDeclaration,
SyntaxKind.RecordDeclaration,
SyntaxKind.InterfaceDeclaration,
SyntaxKind.RecordStructDeclaration);

// We can immediately check duplicate method‐aliases in grain interfaces.
compilationContext.RegisterSyntaxNodeAction(
nodeContext => CheckMethodAliases(nodeContext, aliasMap),
SyntaxKind.InterfaceDeclaration);

// Only at the very end, we do one single‐threaded scan for type‐alias clashes only.
compilationContext.RegisterCompilationEndAction(endContext =>
{
foreach (var kvp in aliasMap)
{
var alias = kvp.Key;
var infos = kvp.Value;

var distinctTypes = infos
.Select(i => i.TypeName)
.Distinct()
.ToList();

if (distinctTypes.Count <= 1)
{
continue; // If more than one different type claimed it.
}

var firstType = distinctTypes[0];
foreach (var info in infos.Where(i => i.TypeName != firstType))
{
endContext.ReportDiagnostic(Diagnostic.Create(Rule, info.Location, alias, firstType));
}
}
});
});
}

private void CheckSyntaxNode(SyntaxNodeAnalysisContext context)
private static void CollectTypeAliases(
SyntaxNodeAnalysisContext context,
ConcurrentDictionary<string, ConcurrentBag<TypeAliasInfo>> aliasMap)
{
var interfaceDeclaration = (InterfaceDeclarationSyntax)context.Node;
if (!interfaceDeclaration.ExtendsGrainInterface(context.SemanticModel))
if (context.Node is not BaseTypeDeclarationSyntax decl)
return;

var semanticModel = context.SemanticModel;
var typeSymbol = semanticModel.GetDeclaredSymbol(decl);
if (typeSymbol == null)
{
return;
}

List<AttributeArgumentBag<string>> bags = new();
foreach (var methodDeclaration in interfaceDeclaration.Members.OfType<MethodDeclarationSyntax>())
if (decl is InterfaceDeclarationSyntax iface && !iface.ExtendsGrainInterface(semanticModel))
{
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);
}
}
return; // Skip interfaces that dont extend IAddressable
}

var duplicateAliases = bags
.GroupBy(alias => alias.Value)
.Where(group => group.Count() > 1)
.Select(group => group.Key);
var attrs = decl.AttributeLists.GetAttributeSyntaxes(Constants.AliasAttributeName);
foreach (var attr in attrs)
{
var alias = attr.GetArgumentValue(semanticModel);
if (string.IsNullOrEmpty(alias))
continue;

var info = new TypeAliasInfo(typeSymbol.ToDisplayString(), attr.GetLocation());

if (!duplicateAliases.Any())
aliasMap.AddOrUpdate(
key: alias,
addValueFactory: _ => new ConcurrentBag<TypeAliasInfo>([info]),
updateValueFactory: (_, bag) =>
{
bag.Add(info);
return bag;
});
}
}

private static void CheckMethodAliases(
SyntaxNodeAnalysisContext context,
ConcurrentDictionary<string, ConcurrentBag<TypeAliasInfo>> aliasMap)
{
if (context.Node is not InterfaceDeclarationSyntax interfaceDecl)
{
return;
}

foreach (var duplicateAlias in duplicateAliases)
{
var filteredBags = bags.Where(x => x.Value == duplicateAlias);
var duplicateCount = filteredBags.Count();
var semanticModel = context.SemanticModel;
var methodBags = new List<(string Alias, Location Location)>();

if (duplicateCount > 1)
foreach (var method in interfaceDecl.Members.OfType<MethodDeclarationSyntax>())
{
var methodAttrs = method.AttributeLists.GetAttributeSyntaxes(Constants.AliasAttributeName);
foreach (var attr in methodAttrs)
{
var (prefix, suffix) = ParsePrefixAndNumericSuffix(duplicateAlias);

filteredBags = filteredBags.Skip(1);

foreach (var bag in filteredBags)
var alias = attr.GetArgumentValue(semanticModel);
if (!string.IsNullOrEmpty(alias))
{
string newAlias;
do
{
++suffix;
newAlias = $"{prefix}{suffix}";
} while (bags.Exists(x => x.Value.Equals(newAlias, StringComparison.Ordinal)));

var builder = ImmutableDictionary.CreateBuilder<string, string>();
methodBags.Add((alias, attr.GetLocation()));
}
}
}

builder.Add("AliasName", prefix);
builder.Add("AliasSuffix", suffix.ToString(System.Globalization.CultureInfo.InvariantCulture));
// Find duplicate aliases within the interface's methods.
var duplicateMethodAliases = methodBags
.GroupBy(x => x.Alias)
.Where(g => g.Count() > 1);

context.ReportDiagnostic(Diagnostic.Create(
descriptor: Rule,
location: bag.Location,
properties: builder.ToImmutable()));
foreach (var group in duplicateMethodAliases)
{
var duplicates = group.Skip(1).ToList();
var (prefix, suffix) = ParsePrefixAndNumericSuffix(group.Key);

foreach (var duplicate in duplicates)
{
string newAlias;
do
{
suffix++;
newAlias = $"{prefix}{suffix}";
}
while (aliasMap.ContainsKey(newAlias) || methodBags.Any(b => b.Alias == newAlias));

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

properties.Add("AliasName", prefix);
properties.Add("AliasSuffix", suffix.ToString(CultureInfo.InvariantCulture));

context.ReportDiagnostic(Diagnostic.Create(Rule, duplicate.Location, properties, group.Key));
}
}
}
Expand All @@ -110,22 +186,21 @@ private static (string Prefix, ulong Suffix) ParsePrefixAndNumericSuffix(string
return (input, 0);
}

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

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

++suffixLength;
suffixLength++;
}

return suffixLength;
}
}
}
14 changes: 11 additions & 3 deletions src/Orleans.Analyzers/AliasClashAttributeCodeFix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,21 @@ 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"];
if (!diagnostic.Properties.TryGetValue("AliasName", out var aliasName))
{
return;
}

if (!diagnostic.Properties.TryGetValue("AliasSuffix", out var aliasSuffix))
{
return;
}

context.RegisterCodeFix(
CodeAction.Create(
Expand All @@ -48,4 +56,4 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
equivalenceKey: AliasClashAttributeAnalyzer.RuleId),
diagnostic);
}
}
}
21 changes: 20 additions & 1 deletion src/Orleans.Analyzers/SyntaxHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,5 +223,24 @@ public static IEnumerable<AttributeSyntax> GetAttributeSyntaxes(this SyntaxList<
attributeLists
.SelectMany(attributeList => attributeList.Attributes)
.Where(attribute => attribute.IsAttribute(attributeName));

public static string GetArgumentValue(this AttributeSyntax attribute, SemanticModel semanticModel)
{
if (attribute?.ArgumentList == null || attribute.ArgumentList.Arguments.Count == 0)
{
return null;
}

var symbolInfo = semanticModel.GetSymbolInfo(attribute);
if (symbolInfo.Symbol == null && symbolInfo.CandidateSymbols.Length == 0)
{
return null;
}

var argumentExpression = attribute.ArgumentList.Arguments[0].Expression;
var constant = semanticModel.GetConstantValue(argumentExpression);

return constant.HasValue ? constant.Value?.ToString() : null;
}
}
}
}
Loading