Merge pull request #64731 from raulsntos/dotnet6-variant-generics-analyzer

C#: Add `MustBeVariant` attribute and analyzer
This commit is contained in:
Rémi Verschelde 2022-08-25 07:34:05 +02:00 committed by GitHub
commit 6ffbec9e49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 227 additions and 18 deletions

View File

@ -1,6 +1,7 @@
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Godot.SourceGenerators
{
@ -19,7 +20,7 @@ namespace Godot.SourceGenerators
"must be declared with the partial modifier.";
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(id: "GODOT-G0001",
new DiagnosticDescriptor(id: "GD0001",
title: message,
messageFormat: message,
category: "Usage",
@ -51,7 +52,7 @@ namespace Godot.SourceGenerators
"containing types must be declared with the partial modifier.";
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(id: "GODOT-G0002",
new DiagnosticDescriptor(id: "GD0002",
title: message,
messageFormat: message,
category: "Usage",
@ -78,7 +79,7 @@ namespace Godot.SourceGenerators
" Remove the 'static' modifier or the '[Export]' attribute.";
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(id: "GODOT-G0101",
new DiagnosticDescriptor(id: "GD0101",
title: message,
messageFormat: message,
category: "Usage",
@ -104,7 +105,7 @@ namespace Godot.SourceGenerators
string description = $"{message}. Use a supported type or remove the '[Export]' attribute.";
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(id: "GODOT-G0102",
new DiagnosticDescriptor(id: "GD0102",
title: message,
messageFormat: message,
category: "Usage",
@ -132,7 +133,7 @@ namespace Godot.SourceGenerators
$"{message}. Exported properties must be writable.";
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(id: "GODOT-G0103",
new DiagnosticDescriptor(id: "GD0103",
title: message,
messageFormat: message,
category: "Usage",
@ -156,7 +157,7 @@ namespace Godot.SourceGenerators
string description = $"{message}. Exported properties must be readable.";
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(id: "GODOT-G0104",
new DiagnosticDescriptor(id: "GD0104",
title: message,
messageFormat: message,
category: "Usage",
@ -181,7 +182,7 @@ namespace Godot.SourceGenerators
string description = $"{message}. Rename the delegate accordingly or remove the '[Signal]' attribute.";
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(id: "GODOT-G0201",
new DiagnosticDescriptor(id: "GD0201",
title: message,
messageFormat: message,
category: "Usage",
@ -205,7 +206,7 @@ namespace Godot.SourceGenerators
string description = $"{message}. Use supported types only or remove the '[Signal]' attribute.";
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(id: "GODOT-G0202",
new DiagnosticDescriptor(id: "GD0202",
title: message,
messageFormat: message,
category: "Usage",
@ -229,7 +230,7 @@ namespace Godot.SourceGenerators
string description = $"{message}. Return void or remove the '[Signal]' attribute.";
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(id: "GODOT-G0203",
new DiagnosticDescriptor(id: "GD0203",
title: message,
messageFormat: message,
category: "Usage",
@ -239,5 +240,97 @@ namespace Godot.SourceGenerators
location,
location?.SourceTree?.FilePath));
}
public static readonly DiagnosticDescriptor GenericTypeArgumentMustBeVariantRule =
new DiagnosticDescriptor(id: "GD0301",
title: "The generic type argument must be a Variant compatible type",
messageFormat: "The generic type argument must be a Variant compatible type: {0}",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The generic type argument must be a Variant compatible type. Use a Variant compatible type as the generic type argument.");
public static void ReportGenericTypeArgumentMustBeVariant(
SyntaxNodeAnalysisContext context,
SyntaxNode typeArgumentSyntax,
ISymbol typeArgumentSymbol)
{
string message = "The generic type argument " +
$"must be a Variant compatible type: '{typeArgumentSymbol.ToDisplayString()}'";
string description = $"{message}. Use a Variant compatible type as the generic type argument.";
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(id: "GD0301",
title: message,
messageFormat: message,
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description),
typeArgumentSyntax.GetLocation(),
typeArgumentSyntax.SyntaxTree.FilePath));
}
public static readonly DiagnosticDescriptor GenericTypeParameterMustBeVariantAnnotatedRule =
new DiagnosticDescriptor(id: "GD0302",
title: "The generic type parameter must be annotated with the MustBeVariant attribute",
messageFormat: "The generic type argument must be a Variant type: {0}",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The generic type argument must be a Variant type. Use a Variant type as the generic type argument.");
public static void ReportGenericTypeParameterMustBeVariantAnnotated(
SyntaxNodeAnalysisContext context,
SyntaxNode typeArgumentSyntax,
ISymbol typeArgumentSymbol)
{
string message = "The generic type parameter must be annotated with the MustBeVariant attribute";
string description = $"{message}. Add the MustBeVariant attribute to the generic type parameter.";
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(id: "GD0302",
title: message,
messageFormat: message,
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description),
typeArgumentSyntax.GetLocation(),
typeArgumentSyntax.SyntaxTree.FilePath));
}
public static readonly DiagnosticDescriptor TypeArgumentParentSymbolUnhandledRule =
new DiagnosticDescriptor(id: "GD0303",
title: "The generic type parameter must be annotated with the MustBeVariant attribute",
messageFormat: "The generic type argument must be a Variant type: {0}",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The generic type argument must be a Variant type. Use a Variant type as the generic type argument.");
public static void ReportTypeArgumentParentSymbolUnhandled(
SyntaxNodeAnalysisContext context,
SyntaxNode typeArgumentSyntax,
ISymbol parentSymbol)
{
string message = $"Symbol '{parentSymbol.ToDisplayString()}' parent of a type argument " +
"that must be Variant compatible was not handled.";
string description = $"{message}. Handle type arguments that are children of the unhandled symbol type.";
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(id: "GD0303",
title: message,
messageFormat: message,
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description),
typeArgumentSyntax.GetLocation(),
typeArgumentSyntax.SyntaxTree.FilePath));
}
}
}

View File

@ -177,6 +177,9 @@ namespace Godot.SourceGenerators
public static bool IsGodotSignalAttribute(this INamedTypeSymbol symbol)
=> symbol.ToString() == GodotClasses.SignalAttr;
public static bool IsGodotMustBeVariantAttribute(this INamedTypeSymbol symbol)
=> symbol.ToString() == GodotClasses.MustBeVariantAttr;
public static bool IsGodotClassNameAttribute(this INamedTypeSymbol symbol)
=> symbol.ToString() == GodotClasses.GodotClassNameAttr;

View File

@ -9,6 +9,7 @@ namespace Godot.SourceGenerators
public const string ExportGroupAttr = "Godot.ExportGroupAttribute";
public const string ExportSubgroupAttr = "Godot.ExportSubgroupAttribute";
public const string SignalAttr = "Godot.SignalAttribute";
public const string MustBeVariantAttr = "Godot.MustBeVariantAttribute";
public const string GodotClassNameAttr = "Godot.GodotClassName";
public const string SystemFlagsAttr = "System.FlagsAttribute";
}

View File

@ -11,11 +11,11 @@ namespace Godot.SourceGenerators
{
public INamedTypeSymbol GodotObjectType { get; }
public TypeCache(GeneratorExecutionContext context)
public TypeCache(Compilation compilation)
{
INamedTypeSymbol GetTypeByMetadataNameOrThrow(string fullyQualifiedMetadataName)
{
return context.Compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) ??
return compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) ??
throw new InvalidOperationException("Type not found: " + fullyQualifiedMetadataName);
}

View File

@ -0,0 +1,100 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Godot.SourceGenerators
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MustBeVariantAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
=> ImmutableArray.Create(
Common.GenericTypeArgumentMustBeVariantRule,
Common.GenericTypeParameterMustBeVariantAnnotatedRule,
Common.TypeArgumentParentSymbolUnhandledRule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.TypeArgumentList);
}
private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
var typeArgListSyntax = (TypeArgumentListSyntax)context.Node;
// Method invocation or variable declaration that contained the type arguments
var parentSyntax = context.Node.Parent;
Debug.Assert(parentSyntax != null);
var sm = context.SemanticModel;
var typeCache = new MarshalUtils.TypeCache(context.Compilation);
for (int i = 0; i < typeArgListSyntax.Arguments.Count; i++)
{
var typeSyntax = typeArgListSyntax.Arguments[i];
var typeSymbol = sm.GetSymbolInfo(typeSyntax).Symbol as ITypeSymbol;
Debug.Assert(typeSymbol != null);
var parentSymbol = sm.GetSymbolInfo(parentSyntax).Symbol;
if (!ShouldCheckTypeArgument(context, parentSyntax, parentSymbol, typeSyntax, typeSymbol, i))
{
return;
}
if (typeSymbol is ITypeParameterSymbol typeParamSymbol)
{
if (!typeParamSymbol.GetAttributes().Any(a => a.AttributeClass?.IsGodotMustBeVariantAttribute() ?? false))
{
Common.ReportGenericTypeParameterMustBeVariantAnnotated(context, typeSyntax, typeSymbol);
}
continue;
}
var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(typeSymbol, typeCache);
if (marshalType == null)
{
Common.ReportGenericTypeArgumentMustBeVariant(context, typeSyntax, typeSymbol);
continue;
}
}
}
/// <summary>
/// Check if the given type argument is being used in a type parameter that contains
/// the <c>MustBeVariantAttribute</c>; otherwise, we ignore the attribute.
/// </summary>
/// <param name="context">Context for a syntax node action.</param>
/// <param name="parentSyntax">The parent node syntax that contains the type node syntax.</param>
/// <param name="parentSymbol">The symbol retrieved for the parent node syntax.</param>
/// <param name="typeArgumentSyntax">The type node syntax of the argument type to check.</param>
/// <param name="typeArgumentSymbol">The symbol retrieved for the type node syntax.</param>
/// <returns><see langword="true"/> if the type must be variant and must be analyzed.</returns>
private bool ShouldCheckTypeArgument(SyntaxNodeAnalysisContext context, SyntaxNode parentSyntax, ISymbol parentSymbol, TypeSyntax typeArgumentSyntax, ITypeSymbol typeArgumentSymbol, int typeArgumentIndex)
{
var typeParamSymbol = parentSymbol switch
{
IMethodSymbol methodSymbol => methodSymbol.TypeParameters[typeArgumentIndex],
INamedTypeSymbol typeSymbol => typeSymbol.TypeParameters[typeArgumentIndex],
_ => null,
};
if (typeParamSymbol == null)
{
Common.ReportTypeArgumentParentSymbolUnhandled(context, typeArgumentSyntax, parentSymbol);
return false;
}
return typeParamSymbol.GetAttributes()
.Any(a => a.AttributeClass?.IsGodotMustBeVariantAttribute() ?? false);
}
}
}

View File

@ -49,7 +49,7 @@ namespace Godot.SourceGenerators
if (godotClasses.Length > 0)
{
var typeCache = new MarshalUtils.TypeCache(context);
var typeCache = new MarshalUtils.TypeCache(context.Compilation);
foreach (var godotClass in godotClasses)
{

View File

@ -49,7 +49,7 @@ namespace Godot.SourceGenerators
if (godotClasses.Length > 0)
{
var typeCache = new MarshalUtils.TypeCache(context);
var typeCache = new MarshalUtils.TypeCache(context.Compilation);
foreach (var godotClass in godotClasses)
{

View File

@ -49,7 +49,7 @@ namespace Godot.SourceGenerators
if (godotClasses.Length > 0)
{
var typeCache = new MarshalUtils.TypeCache(context);
var typeCache = new MarshalUtils.TypeCache(context.Compilation);
foreach (var godotClass in godotClasses)
{

View File

@ -49,7 +49,7 @@ namespace Godot.SourceGenerators
if (godotClasses.Length > 0)
{
var typeCache = new MarshalUtils.TypeCache(context);
var typeCache = new MarshalUtils.TypeCache(context.Compilation);
foreach (var godotClass in godotClasses)
{

View File

@ -56,7 +56,7 @@ namespace Godot.SourceGenerators
if (godotClasses.Length > 0)
{
var typeCache = new MarshalUtils.TypeCache(context);
var typeCache = new MarshalUtils.TypeCache(context.Compilation);
foreach (var godotClass in godotClasses)
{

View File

@ -483,7 +483,7 @@ namespace Godot.Collections
/// <typeparam name="T">The type of the array.</typeparam>
[SuppressMessage("ReSharper", "RedundantExtendsListEntry")]
[SuppressMessage("Naming", "CA1710", MessageId = "Identifiers should have correct suffix")]
public sealed class Array<T> :
public sealed class Array<[MustBeVariant] T> :
IList<T>,
IReadOnlyList<T>,
ICollection<T>,

View File

@ -0,0 +1,11 @@
using System;
namespace Godot
{
/// <summary>
/// Attribute that restricts generic type parameters to be only types
/// that can be marshaled from/to a <see cref="Variant"/>.
/// </summary>
[AttributeUsage(AttributeTargets.GenericParameter)]
public class MustBeVariantAttribute : Attribute { }
}

View File

@ -352,7 +352,7 @@ namespace Godot.Collections
/// </summary>
/// <typeparam name="TKey">The type of the dictionary's keys.</typeparam>
/// <typeparam name="TValue">The type of the dictionary's values.</typeparam>
public class Dictionary<TKey, TValue> :
public class Dictionary<[MustBeVariant] TKey, [MustBeVariant] TValue> :
IDictionary<TKey, TValue>,
IReadOnlyDictionary<TKey, TValue>
{

View File

@ -56,6 +56,7 @@
<Compile Include="Core\Attributes\ExportCategoryAttribute.cs" />
<Compile Include="Core\Attributes\ExportGroupAttribute.cs" />
<Compile Include="Core\Attributes\ExportSubgroupAttribute.cs" />
<Compile Include="Core\Attributes\MustBeVariantAttribute.cs" />
<Compile Include="Core\Attributes\RPCAttribute.cs" />
<Compile Include="Core\Attributes\ScriptPathAttribute.cs" />
<Compile Include="Core\Attributes\SignalAttribute.cs" />