Browse Source

Add MustBeVariant attribute and analyzer

- MustBeVariant attribute can be used to enforce that generic types must
be a marshable from/to Variant.
- Also renames all diagnostic ids to be valid unicode identifiers.
Raul Santos 3 years ago
parent
commit
6468f9b37c

+ 102 - 9
modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs

@@ -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));
+        }
     }
 }

+ 3 - 0
modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs

@@ -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;
 

+ 1 - 0
modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs

@@ -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";
     }

+ 2 - 2
modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs

@@ -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);
                 }
 

+ 100 - 0
modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs

@@ -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);
+        }
+    }
+}

+ 1 - 1
modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs

@@ -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)
                 {

+ 1 - 1
modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs

@@ -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)
                 {

+ 1 - 1
modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs

@@ -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)
                 {

+ 1 - 1
modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs

@@ -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)
                 {

+ 1 - 1
modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs

@@ -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)
                 {

+ 1 - 1
modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs

@@ -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>,

+ 11 - 0
modules/mono/glue/GodotSharp/GodotSharp/Core/Attributes/MustBeVariantAttribute.cs

@@ -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 { }
+}

+ 1 - 1
modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs

@@ -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>
     {

+ 1 - 0
modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj

@@ -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" />