diff options
author | Raul Santos <raulsntos@gmail.com> | 2022-08-15 05:57:52 +0200 |
---|---|---|
committer | Raul Santos <raulsntos@gmail.com> | 2022-08-25 01:47:40 +0200 |
commit | 6468f9b37c9605757593f9da2137da3ef756880e (patch) | |
tree | 0c4aec7e965d5f927c1b70bd2c7390ffab2df927 | |
parent | d0a2a4c98195eb8a43713286b5b865dfbed05163 (diff) |
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.
14 files changed, 227 insertions, 18 deletions
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs index 0f8b128da8..3dfa8000ba 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs +++ b/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)); + } } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs index bac4708165..d868678179 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs +++ b/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; diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs index e899440e10..1d8ddbabf2 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs +++ b/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"; } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs index ca84518c0c..831ac3bdeb 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs +++ b/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); } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs new file mode 100644 index 0000000000..7aaadb27be --- /dev/null +++ b/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); + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs index 1fdc04a262..5ac4f4a47e 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs +++ b/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) { diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs index b4db3fda62..0c9b17d69a 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs +++ b/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) { diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs index 3b8ba21107..85fadaa52e 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs +++ b/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) { diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs index 1b87c6e760..5a84122c4c 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs +++ b/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) { diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs index f1c7706f9c..161834a4be 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs +++ b/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) { diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs index 81991c6626..f2984bd1fb 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs +++ b/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>, diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Attributes/MustBeVariantAttribute.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Attributes/MustBeVariantAttribute.cs new file mode 100644 index 0000000000..23088378d1 --- /dev/null +++ b/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 { } +} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs index fa8c94ed18..95ad097192 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs +++ b/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> { diff --git a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj index f0d6748b73..aae7a5ebfa 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj +++ b/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" /> |