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
1 change: 1 addition & 0 deletions src/Orleans.Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
ORLEANS0026 | Usage | Error | Invalid invokable base type mapping
ORLEANS0014 | Usage | Warning | ConfigureAwaitAnalyzer, Grain code should not use ConfigureAwait(false) or ConfigureAwait without ContinueOnCapturedContext
ORLEANS0016 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Grain interface not declared in OrleansContracts.txt
ORLEANS0017 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Grain interface version mismatch between code and file
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,46 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Orleans.CodeGenerator;

namespace Orleans.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class GrainInterfaceMethodReturnTypeDiagnosticAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "ORLEANS0009";
public const string Title = "Grain interfaces methods must return a compatible type";
public const string MessageFormat = $"Grain interfaces methods must return a compatible type, such as Task, Task<T>, ValueTask, ValueTask<T>, or void";
public const string Title = "Grain interface methods must return a registered grain-call return type";
public const string MessageFormat = "Grain interface methods must return a registered grain-call return type";
public const string Category = "Usage";
public const string InvalidMappingDiagnosticId = "ORLEANS0026";

private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true);
private static readonly DiagnosticDescriptor Rule = new(
DiagnosticId,
new LocalizableResourceString(nameof(Resources.GrainInterfaceMethodReturnTypeTitle), Resources.ResourceManager, typeof(Resources)),
new LocalizableResourceString(nameof(Resources.GrainInterfaceMethodReturnTypeMessageFormat), Resources.ResourceManager, typeof(Resources)),
Category,
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: new LocalizableResourceString(
nameof(Resources.GrainInterfaceMethodReturnTypeDescription),
Resources.ResourceManager,
typeof(Resources)));
private static readonly DiagnosticDescriptor InvalidMappingRule = new(
InvalidMappingDiagnosticId,
new LocalizableResourceString(nameof(Resources.InvalidInvokableBaseTypeMappingTitle), Resources.ResourceManager, typeof(Resources)),
new LocalizableResourceString(nameof(Resources.InvalidInvokableBaseTypeMappingMessageFormat), Resources.ResourceManager, typeof(Resources)),
Category,
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: new LocalizableResourceString(
nameof(Resources.InvalidInvokableBaseTypeMappingDescription),
Resources.ResourceManager,
typeof(Resources)));

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [Rule];
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [Rule, InvalidMappingRule];

public override void Initialize(AnalysisContext context)
{
Expand All @@ -30,53 +53,173 @@ public override void Initialize(AnalysisContext context)
return;
}

var builder = ImmutableHashSet.CreateBuilder<ITypeSymbol>(SymbolEqualityComparer.Default);

AddIfNotNull(builder, context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task"));
AddIfNotNull(builder, context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1"));
AddIfNotNull(builder, context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask"));
AddIfNotNull(builder, context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask`1"));
AddIfNotNull(builder, context.Compilation.GetTypeByMetadataName("System.Collections.Generic.IAsyncEnumerable`1"));
AddIfNotNull(builder, context.Compilation.GetSpecialType(SpecialType.System_Void));

context.RegisterSymbolAction(context => AnalyzeMethod(context, baseInterface, builder.ToImmutable()), SymbolKind.Method);
var generateMethodSerializersAttribute = context.Compilation.GetTypeByMetadataName("Orleans.GenerateMethodSerializersAttribute");
var proxyContexts = GetProxyContexts(
context.Compilation.Assembly.GlobalNamespace,
baseInterface,
generateMethodSerializersAttribute);
var resolver = new InvokableBaseTypeResolver(context.Compilation);
context.RegisterSymbolAction(
context => AnalyzeMethod(context, proxyContexts, resolver),
SymbolKind.Method);
});
}

private static void AnalyzeMethod(
SymbolAnalysisContext context,
ImmutableArray<ProxyContext> proxyContexts,
InvokableBaseTypeResolver resolver)
{
var symbol = (IMethodSymbol)context.Symbol;
if (symbol.ContainingType.TypeKind != TypeKind.Interface || symbol.IsStatic)
{
return;
}

static void AddIfNotNull(ImmutableHashSet<ITypeSymbol>.Builder builder, INamedTypeSymbol? symbol)
ResolverDiagnostic? diagnostic = null;
foreach (var proxyContext in proxyContexts)
{
if (symbol is not null)
if (!TryGetContextMethod(proxyContext.InterfaceType, symbol, out var contextMethod))
{
continue;
}

if (resolver.TryResolve(
proxyContext.ProxyBaseType,
contextMethod,
proxyContext.InterfaceType,
out _,
out diagnostic))
{
builder.Add(symbol);
continue;
}

break;
}

if (diagnostic is null)
{
return;
}

var syntaxReference = symbol.DeclaringSyntaxReferences[0];
if (diagnostic.Kind == ResolverDiagnosticKind.InvalidMapping)
{
context.ReportDiagnostic(Diagnostic.Create(
InvalidMappingRule,
diagnostic.Location ?? Location.Create(syntaxReference.SyntaxTree, syntaxReference.Span),
diagnostic.Message));
return;
}

context.ReportDiagnostic(Diagnostic.Create(
Rule,
Location.Create(syntaxReference.SyntaxTree, syntaxReference.Span)));
}

private static void AnalyzeMethod(SymbolAnalysisContext context, INamedTypeSymbol baseInterface, ImmutableHashSet<ITypeSymbol> supportedTypes)
private static bool TryGetContextMethod(
INamedTypeSymbol proxyInterface,
IMethodSymbol method,
out IMethodSymbol contextMethod)
{
var symbol = (IMethodSymbol)context.Symbol;
if (SymbolEqualityComparer.Default.Equals(
proxyInterface.OriginalDefinition,
method.ContainingType.OriginalDefinition))
{
contextMethod = method;
return true;
}

if (symbol.ContainingType.TypeKind != TypeKind.Interface) return;
foreach (var inheritedInterface in proxyInterface.AllInterfaces)
{
if (!SymbolEqualityComparer.Default.Equals(
inheritedInterface.OriginalDefinition,
method.ContainingType.OriginalDefinition))
{
continue;
}

// allow static interface methods to return any type
if (symbol.IsStatic)
return;
foreach (var candidate in inheritedInterface.GetMembers(method.Name).OfType<IMethodSymbol>())
{
if (SymbolEqualityComparer.Default.Equals(candidate.OriginalDefinition, method.OriginalDefinition))
{
contextMethod = candidate;
return true;
}
}
}

contextMethod = null!;
return false;
}

var isIAddressableInterface = false;
foreach (var implementedInterface in symbol.ContainingType.AllInterfaces)
private static ImmutableArray<ProxyContext> GetProxyContexts(
INamespaceSymbol globalNamespace,
INamedTypeSymbol baseInterface,
INamedTypeSymbol? generateMethodSerializersAttribute)
{
if (generateMethodSerializersAttribute is null)
{
if (implementedInterface.Equals(baseInterface, SymbolEqualityComparer.Default))
return [];
}

var result = new List<ProxyContext>();
AddNamespace(globalNamespace);
return [.. result
.OrderBy(static entry => entry.SourceOrderGroup)
.ThenBy(static entry => entry.FilePath, StringComparer.Ordinal)
.ThenBy(static entry => entry.Position)
.ThenBy(
static entry => entry.InterfaceType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
StringComparer.Ordinal)];

void AddNamespace(INamespaceSymbol @namespace)
{
foreach (var member in @namespace.GetMembers())
{
isIAddressableInterface = true;
break;
if (member is INamespaceSymbol childNamespace)
{
AddNamespace(childNamespace);
}
else if (member is INamedTypeSymbol type)
{
AddType(type);
}
}
}

if (!isIAddressableInterface || supportedTypes.Contains(symbol.ReturnType.OriginalDefinition))
return;
void AddType(INamedTypeSymbol type)
{
if (type.TypeKind == TypeKind.Interface
&& type.AllInterfaces.Any(implemented =>
SymbolEqualityComparer.Default.Equals(implemented, baseInterface))
&& InvokableBaseTypeResolver.TryGetProxyBaseType(
type,
generateMethodSerializersAttribute,
out var proxyBaseType,
out _))
{
var location = type.Locations.FirstOrDefault(static candidate => candidate.IsInSource);
result.Add(new ProxyContext(
type,
proxyBaseType,
location is null ? 1 : 0,
location?.SourceTree?.FilePath ?? string.Empty,
location?.SourceSpan.Start ?? int.MaxValue));
}

var syntaxReference = symbol.DeclaringSyntaxReferences;
context.ReportDiagnostic(Diagnostic.Create(Rule, Location.Create(syntaxReference[0].SyntaxTree, syntaxReference[0].Span)));
foreach (var nestedType in type.GetTypeMembers())
{
AddType(nestedType);
}
}
}

private sealed record ProxyContext(
INamedTypeSymbol InterfaceType,
INamedTypeSymbol ProxyBaseType,
int SourceOrderGroup,
string FilePath,
int Position);
}
}
4 changes: 4 additions & 0 deletions src/Orleans.Analyzers/Orleans.Analyzers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
<PackageReference Update="NETStandard.Library" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\Orleans.CodeGenerator.Shared\InvokableBaseTypeResolver.cs" Link="InvokableBaseTypeResolver.cs" />
</ItemGroup>

<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
Expand Down
54 changes: 54 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.

18 changes: 18 additions & 0 deletions src/Orleans.Analyzers/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,24 @@
<data name="AliasClashDetectedTitle" xml:space="preserve">
<value>Rename duplicated [Alias]</value>
</data>
<data name="GrainInterfaceMethodReturnTypeDescription" xml:space="preserve">
<value>Grain interface methods must use a return type registered for the selected proxy base.</value>
</data>
<data name="GrainInterfaceMethodReturnTypeMessageFormat" xml:space="preserve">
<value>Grain interface methods must return a registered grain-call return type</value>
</data>
<data name="GrainInterfaceMethodReturnTypeTitle" xml:space="preserve">
<value>Grain interface methods must return a registered grain-call return type</value>
</data>
<data name="InvalidInvokableBaseTypeMappingDescription" xml:space="preserve">
<value>Invokable base type registrations must identify a valid base type for the grain-call return type and proxy base.</value>
</data>
<data name="InvalidInvokableBaseTypeMappingMessageFormat" xml:space="preserve">
<value>{0}</value>
</data>
<data name="InvalidInvokableBaseTypeMappingTitle" xml:space="preserve">
<value>Invalid invokable base type mapping</value>
</data>
<data name="IdClashDetectedDescription" xml:space="preserve">
<value>The [Id] attribute must be unique to each members of the declaring type.</value>
</data>
Expand Down
Loading
Loading