Merge pull request #64731 from raulsntos/dotnet6-variant-generics-analyzer
C#: Add `MustBeVariant` attribute and analyzer
This commit is contained in:
commit
6ffbec9e49
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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 { }
|
||||
}
|
|
@ -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>
|
||||
{
|
||||
|
|
|
@ -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" />
|
||||
|
|
Loading…
Reference in New Issue