Skip to content

Commit

Permalink
ORLEANS0010 (#8643)
Browse files Browse the repository at this point in the history
* Interface & interface methods analyzer working so far

* changed logic for attribute finding to be consistent with the other analyzers

* Analyzer working correctly

* Code fixer working

* removed sandbox for testing analyzers

* Add generic arity and namespace/nesting

---------

Co-authored-by: Ledjon Behluli <[email protected]>
Co-authored-by: ReubenBond <[email protected]>
  • Loading branch information
3 people authored Nov 12, 2023
1 parent 8f73d43 commit f29d7e4
Show file tree
Hide file tree
Showing 9 changed files with 475 additions and 3 deletions.
13 changes: 11 additions & 2 deletions src/Orleans.Analyzers/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ namespace Orleans.Analyzers
{
internal static class Constants
{
public const string SystemNamespace = "System";

public const string IAddressibleFullyQualifiedName = "Orleans.Runtime.IAddressable";

public const string IdAttributeName = "Id";
public const string IdAttributeFullyQualifiedName = "global::Orleans.IdAttribute";

public const string GenerateSerializerAttributeName = "GenerateSerializer";
public const string GenerateSerializerAttributeFullyQualifiedName = "global::Orleans.GenerateSerializerAttribute";

public const string SerializableAttributeName = "Serializable";

public const string NonSerializedAttribute = "NonSerialized";
public const string NonSerializedAttributeFullyQualifiedName = "global::System.NonSerializedAttribute";
public const string SystemNamespace = "System";

public const string AliasAttributeName = "Alias";
public const string AliasAttributeFullyQualifiedName = "global::Orleans.AliasAttribute";
}
}
}
156 changes: 156 additions & 0 deletions src/Orleans.Analyzers/GenerateAliasAttributesAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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;
using System.Net.NetworkInformation;
using System.Text;

namespace Orleans.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class GenerateAliasAttributesAnalyzer : DiagnosticAnalyzer
{
public const string RuleId = "ORLEANS0010";
private const string Category = "Usage";
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AddAliasAttributesTitle), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.AddAliasMessageFormat), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.AddAliasAttributesDescription), Resources.ResourceManager, typeof(Resources));

private static readonly DiagnosticDescriptor Rule = new(RuleId, Title, MessageFormat, Category, DiagnosticSeverity.Info, isEnabledByDefault: true, description: Description);

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

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
context.RegisterSyntaxNodeAction(CheckSyntaxNode,
SyntaxKind.InterfaceDeclaration,
SyntaxKind.ClassDeclaration,
SyntaxKind.StructDeclaration,
SyntaxKind.RecordDeclaration,
SyntaxKind.RecordStructDeclaration);
}

private void CheckSyntaxNode(SyntaxNodeAnalysisContext context)
{
// Interface types and their methods
if (context.Node is InterfaceDeclarationSyntax { } interfaceDeclaration)
{
if (!context.SemanticModel
.GetDeclaredSymbol(interfaceDeclaration, context.CancellationToken)
.ExtendsGrainInterface())
{
return;
}

if (!interfaceDeclaration.HasAttribute(Constants.AliasAttributeName))
{
ReportFor(
context,
interfaceDeclaration.GetLocation(),
interfaceDeclaration.Identifier.ToString(),
GetArity(interfaceDeclaration),
GetNamespaceAndNesting(interfaceDeclaration));
}

foreach (var methodDeclaration in interfaceDeclaration.Members.OfType<MethodDeclarationSyntax>())
{
if (methodDeclaration.IsStatic())
{
continue;
}

if (!methodDeclaration.HasAttribute(Constants.AliasAttributeName))
{
ReportFor(context, methodDeclaration.GetLocation(), methodDeclaration.Identifier.ToString(), arity: 0, namespaceAndNesting: null);
}
}

return;
}

// Rest of types: class, struct, record
if (context.Node is TypeDeclarationSyntax { } typeDeclaration)
{
if (!typeDeclaration.HasAttribute(Constants.GenerateSerializerAttributeName))
{
return;
}

if (typeDeclaration.HasAttribute(Constants.AliasAttributeName))
{
return;
}

ReportFor(
context,
typeDeclaration.GetLocation(),
typeDeclaration.Identifier.ToString(),
GetArity(typeDeclaration),
GetNamespaceAndNesting(typeDeclaration));
}
}

private static int GetArity(TypeDeclarationSyntax typeDeclarationSyntax)
{
var node = typeDeclarationSyntax;
int arity = 0;
while (node is TypeDeclarationSyntax type)
{
arity += type.Arity;
node = type.Parent as TypeDeclarationSyntax;
}

return arity;
}

private static string GetNamespaceAndNesting(TypeDeclarationSyntax typeDeclarationSyntax)
{
SyntaxNode node = typeDeclarationSyntax.Parent;
StringBuilder sb = new();
Stack<string> segments = new();
while (node is not null)
{
if (node is TypeDeclarationSyntax type)
{
segments.Push(type.Identifier.ToString());
}
else if (node is NamespaceDeclarationSyntax ns)
{
segments.Push(ns.Name.ToString());
}

node = node.Parent;
}

foreach (var segment in segments)
{
if (sb.Length > 0)
{
sb.Append('.');
}

sb.Append(segment);
}

return sb.ToString();
}

private static void ReportFor(SyntaxNodeAnalysisContext context, Location location, string typeName, int arity, string namespaceAndNesting)
{
var builder = ImmutableDictionary.CreateBuilder<string, string>();

builder.Add("TypeName", typeName);
builder.Add("NamespaceAndNesting", namespaceAndNesting);
builder.Add("Arity", arity.ToString(System.Globalization.CultureInfo.InvariantCulture));

context.ReportDiagnostic(Diagnostic.Create(
descriptor: Rule,
location: location,
properties: builder.ToImmutable()));
}
}
81 changes: 81 additions & 0 deletions src/Orleans.Analyzers/GenerateAliasAttributesCodeFix.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System.Collections.Immutable;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using System.Composition;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Simplification;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace Orleans.Analyzers;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(GenerateAliasAttributesCodeFix)), Shared]
public class GenerateAliasAttributesCodeFix : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(GenerateAliasAttributesAnalyzer.RuleId);
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

foreach (var diagnostic in context.Diagnostics)
{
var node = root.FindNode(diagnostic.Location.SourceSpan);
if (node != null)
{
// Check if its an interface method
var methodDeclaration = node.FirstAncestorOrSelf<MethodDeclarationSyntax>();
if (methodDeclaration != null)
{
await FixFor(context, diagnostic, methodDeclaration);
continue;
}

// Check if its a type declaration (interface itself, class, struct, record)
var typeDeclaration = node.FirstAncestorOrSelf<TypeDeclarationSyntax>();
if (typeDeclaration != null)
{
await FixFor(context, diagnostic, typeDeclaration);
continue;
}
}
}
}

private static async Task FixFor(CodeFixContext context, Diagnostic diagnostic, SyntaxNode declaration)
{
var documentEditor = await DocumentEditor.CreateAsync(context.Document, context.CancellationToken);

var arityString = diagnostic.Properties["Arity"] switch
{
null or "0" => "",
string value => $"`{value}"
};
var typeName = diagnostic.Properties["TypeName"];
var ns = diagnostic.Properties["NamespaceAndNesting"] switch
{
{ Length: > 0 } value => $"{value}.",
_ => ""
};

var aliasAttribute =
Attribute(
ParseName(Constants.AliasAttributeFullyQualifiedName))
.WithArgumentList(
ParseAttributeArgumentList($"(\"{ns}{typeName}{arityString}\")"))
.WithAdditionalAnnotations(Simplifier.Annotation);

documentEditor.AddAttribute(declaration, aliasAttribute);
var updatedDocument = documentEditor.GetChangedDocument();

context.RegisterCodeFix(
action: CodeAction.Create(
Resources.AddAliasAttributesTitle,
createChangedDocument: ct => Task.FromResult(updatedDocument),
equivalenceKey: GenerateAliasAttributesAnalyzer.RuleId),
diagnostic: diagnostic);
}
}
27 changes: 27 additions & 0 deletions src/Orleans.Analyzers/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Orleans.Analyzers/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,13 @@
<data name="AtMostOneOrleansConstructorMessageFormat" xml:space="preserve">
<value>A single type is not allowed to have multiple constructors annotated with the [OrleansConstructor] attribute</value>
</data>
<data name="AddAliasAttributesDescription" xml:space="preserve">
<value>Add [Alias] attributes to specify well-known names that can be used to identify types or methods</value>
</data>
<data name="AddAliasAttributesTitle" xml:space="preserve">
<value>Add missing alias attributes</value>
</data>
<data name="AddAliasMessageFormat" xml:space="preserve">
<value>Add missing alias attributes</value>
</data>
</root>
16 changes: 15 additions & 1 deletion src/Orleans.Analyzers/SyntaxHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,5 +144,19 @@ public static bool IsFieldOrAutoProperty(this MemberDeclarationSyntax member)
return isFieldOrAutoProperty;
}

public static bool ExtendsGrainInterface(this INamedTypeSymbol symbol)
{
if (symbol.TypeKind != TypeKind.Interface) return false;

foreach (var interfaceSymbol in symbol.AllInterfaces)
{
if (Constants.IAddressibleFullyQualifiedName.Equals(interfaceSymbol.ToDisplayString(NullableFlowState.None), StringComparison.Ordinal))
{
return true;
}
}

return false;
}
}
}
}
Loading

0 comments on commit f29d7e4

Please sign in to comment.