From 01e065976d0c0f3ce5e075a2e9159e4b25e2e8cc Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 14 Feb 2024 16:11:22 +1100 Subject: [PATCH] Added operation generator --- .../src/Abstractions/EventMessageAttribute.cs | 5 +- .../Abstractions/MutationFieldAttribute.cs | 6 + .../src/Abstractions/QueryFieldAttribute.cs | 6 + .../SubscriptionFieldAttribute.cs | 6 + .../Core/src/Types.Analyzers/Errors.cs | 27 + .../Generators/DataLoaderGenerator.cs | 716 ------------------ .../Generators/DataLoaderSyntaxGenerator.cs | 301 ++++++++ .../Generators/ISyntaxGenerator.cs | 29 - .../ModuleSyntaxGenerator.cs | 6 +- .../OperationFieldSyntaxGenerator.cs | 108 +++ .../Generators/OperationGenerator.txt | 150 ---- .../Helpers/CodeWriterExtensions.cs | 2 - .../Types.Analyzers/Helpers/GeneratorUtils.cs | 44 ++ .../Inspectors/OperationInfo.cs | 58 ++ .../Inspectors/OperationInspector.cs | 43 +- .../Inspectors/OperationRegistrationInfo.cs | 50 ++ .../Types.Analyzers/TypeModuleGenerator.cs | 350 ++++++++- .../Types.Analyzers/WellKnownAttributes.cs | 4 +- .../src/Types.Analyzers/WellKnownFileNames.cs | 1 + .../AnnotationBasedSchemaTests.cs | 12 + .../Core/test/Types.Analyzers.Tests/Log.txt | 6 - .../Types.Analyzers.Tests/RootTypeTests.cs | 10 + .../Core/test/Types.Analyzers.Tests/ss.cs | 0 23 files changed, 1007 insertions(+), 933 deletions(-) create mode 100644 src/HotChocolate/Core/src/Abstractions/MutationFieldAttribute.cs create mode 100644 src/HotChocolate/Core/src/Abstractions/QueryFieldAttribute.cs create mode 100644 src/HotChocolate/Core/src/Abstractions/SubscriptionFieldAttribute.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Errors.cs delete mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderGenerator.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderSyntaxGenerator.cs delete mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Generators/ISyntaxGenerator.cs rename src/HotChocolate/Core/src/Types.Analyzers/{Helpers => Generators}/ModuleSyntaxGenerator.cs (91%) create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Generators/OperationFieldSyntaxGenerator.cs delete mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Generators/OperationGenerator.txt create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Helpers/GeneratorUtils.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInfo.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationRegistrationInfo.cs delete mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/Log.txt create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/RootTypeTests.cs delete mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/ss.cs diff --git a/src/HotChocolate/Core/src/Abstractions/EventMessageAttribute.cs b/src/HotChocolate/Core/src/Abstractions/EventMessageAttribute.cs index 572d433e18f..d15058702f4 100644 --- a/src/HotChocolate/Core/src/Abstractions/EventMessageAttribute.cs +++ b/src/HotChocolate/Core/src/Abstractions/EventMessageAttribute.cs @@ -3,7 +3,4 @@ namespace HotChocolate; [AttributeUsage(AttributeTargets.Parameter)] -public sealed class EventMessageAttribute - : Attribute -{ -} +public sealed class EventMessageAttribute : Attribute; \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Abstractions/MutationFieldAttribute.cs b/src/HotChocolate/Core/src/Abstractions/MutationFieldAttribute.cs new file mode 100644 index 00000000000..299e8c2f769 --- /dev/null +++ b/src/HotChocolate/Core/src/Abstractions/MutationFieldAttribute.cs @@ -0,0 +1,6 @@ +using System; + +namespace HotChocolate; + +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] +public sealed class MutationFieldAttribute : Attribute; \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Abstractions/QueryFieldAttribute.cs b/src/HotChocolate/Core/src/Abstractions/QueryFieldAttribute.cs new file mode 100644 index 00000000000..a49b8021eb2 --- /dev/null +++ b/src/HotChocolate/Core/src/Abstractions/QueryFieldAttribute.cs @@ -0,0 +1,6 @@ +using System; + +namespace HotChocolate; + +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] +public sealed class QueryFieldAttribute : Attribute; \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Abstractions/SubscriptionFieldAttribute.cs b/src/HotChocolate/Core/src/Abstractions/SubscriptionFieldAttribute.cs new file mode 100644 index 00000000000..048a86d6e29 --- /dev/null +++ b/src/HotChocolate/Core/src/Abstractions/SubscriptionFieldAttribute.cs @@ -0,0 +1,6 @@ +using System; + +namespace HotChocolate; + +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] +public sealed class SubscriptionFieldAttribute : Attribute; \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Errors.cs b/src/HotChocolate/Core/src/Types.Analyzers/Errors.cs new file mode 100644 index 00000000000..5ec5a06bd20 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Errors.cs @@ -0,0 +1,27 @@ +using HotChocolate.Types.Analyzers.Properties; +using Microsoft.CodeAnalysis; + +namespace HotChocolate.Types.Analyzers; + +public static class Errors +{ + public static readonly DiagnosticDescriptor KeyParameterMissing = + new( + id: "HC0074", + title: "Parameter Missing.", + messageFormat: + SourceGenResources.DataLoader_KeyParameterMissing, + category: "DataLoader", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor MethodAccessModifierInvalid = + new( + id: "HC0075", + title: "Access Modifier Invalid.", + messageFormat: + SourceGenResources.DataLoader_InvalidAccessModifier, + category: "DataLoader", + DiagnosticSeverity.Error, + isEnabledByDefault: true); +} \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderGenerator.cs deleted file mode 100644 index 1c1e22c2b5a..00000000000 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderGenerator.cs +++ /dev/null @@ -1,716 +0,0 @@ -using System.Collections.Immutable; -using System.Text; -using HotChocolate.Types.Analyzers.Helpers; -using HotChocolate.Types.Analyzers.Inspectors; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using static System.StringComparison; -using static HotChocolate.Types.Analyzers.Properties.SourceGenResources; -using static HotChocolate.Types.Analyzers.StringConstants; -using static HotChocolate.Types.Analyzers.WellKnownTypes; - -namespace HotChocolate.Types.Analyzers.Generators; - -public class DataLoaderGenerator : ISyntaxGenerator -{ - private static readonly DiagnosticDescriptor _keyParameterMissing = - new( - id: "HC0074", - title: "Parameter Missing.", - messageFormat: - DataLoader_KeyParameterMissing, - category: "DataLoader", - DiagnosticSeverity.Error, - isEnabledByDefault: true); - - private static readonly DiagnosticDescriptor _methodAccessModifierInvalid = - new( - id: "HC0075", - title: "Access Modifier Invalid.", - messageFormat: - DataLoader_InvalidAccessModifier, - category: "DataLoader", - DiagnosticSeverity.Error, - isEnabledByDefault: true); - - public void Initialize(IncrementalGeneratorPostInitializationContext context) { } - - public bool Consume(ISyntaxInfo syntaxInfo) - => syntaxInfo is DataLoaderInfo or ModuleInfo or DataLoaderDefaultsInfo; - - public void Generate( - SourceProductionContext context, - Compilation compilation, - ReadOnlySpan syntaxInfos) - { - var (module, defaults) = syntaxInfos.GetDataLoaderDefaults(compilation.AssemblyName); - - var dataLoaders = new List(); - var sourceText = StringBuilderPool.Get(); - - sourceText.AppendLine("// "); - sourceText.AppendLine("#nullable enable"); - sourceText.AppendLine("using System;"); - sourceText.AppendLine("using Microsoft.Extensions.DependencyInjection;"); - sourceText.AppendLine("using HotChocolate.Execution.Configuration;"); - - foreach (var syntaxInfo in syntaxInfos) - { - if (syntaxInfo is not DataLoaderInfo dataLoader) - { - continue; - } - - if (dataLoader.MethodSymbol.Parameters.Length == 0) - { - context.ReportDiagnostic( - Diagnostic.Create( - _keyParameterMissing, - Location.Create( - dataLoader.MethodSyntax.SyntaxTree, - dataLoader.MethodSyntax.ParameterList.Span))); - continue; - } - - if (dataLoader.MethodSymbol.DeclaredAccessibility is not Accessibility.Public - and not Accessibility.Internal and not Accessibility.ProtectedAndInternal) - { - context.ReportDiagnostic( - Diagnostic.Create( - _methodAccessModifierInvalid, - Location.Create( - dataLoader.MethodSyntax.SyntaxTree, - dataLoader.MethodSyntax.Modifiers.Span))); - continue; - } - - var keyArg = dataLoader.MethodSymbol.Parameters[0]; - var keyType = keyArg.Type; - var cancellationTokenIndex = -1; - var serviceMap = new Dictionary(); - - if (IsKeysArgument(keyType)) - { - keyType = ExtractKeyType(keyType); - } - - InspectDataLoaderParameters( - dataLoader, - ref cancellationTokenIndex, - serviceMap); - - DataLoaderKind kind; - - if (IsReturnTypeDictionary(dataLoader.MethodSymbol.ReturnType, keyType)) - { - kind = DataLoaderKind.Batch; - } - else if (IsReturnTypeLookup(dataLoader.MethodSymbol.ReturnType, keyType)) - { - kind = DataLoaderKind.Group; - } - else - { - keyType = keyArg.Type; - kind = DataLoaderKind.Cache; - } - - var valueType = ExtractValueType(dataLoader.MethodSymbol.ReturnType, kind); - - dataLoaders.Add(dataLoader); - - GenerateDataLoader( - dataLoader, - defaults, - kind, - keyType, - valueType, - dataLoader.MethodSymbol.Parameters.Length, - cancellationTokenIndex, - serviceMap); - } - - // if we find no valid DataLoader we will not create any file. - if (dataLoaders.Count > 0) - { - if (defaults.RegisterServices) - { - // write DI integration - sourceText.AppendLine(); - sourceText.AppendLine("namespace Microsoft.Extensions.DependencyInjection"); - sourceText.AppendLine("{"); - GenerateDataLoaderRegistrations(module, dataLoaders, sourceText); - sourceText.AppendLine("}"); - } - - context.AddSource( - WellKnownFileNames.DataLoaderFile, - SourceText.From(sourceText.ToString(), Encoding.UTF8)); - } - - StringBuilderPool.Return(sourceText); - } - - private static void GenerateDataLoader( - DataLoaderInfo dataLoader, - DataLoaderDefaultsInfo defaults, - DataLoaderKind kind, - ITypeSymbol keyType, - ITypeSymbol valueType, - int parameterCount, - int cancelIndex, - Dictionary services) - { - var isScoped = dataLoader.IsScoped ?? defaults.Scoped ?? false; - var isPublic = dataLoader.IsPublic ?? defaults.IsPublic ?? true; - var isInterfacePublic = dataLoader.IsInterfacePublic ?? defaults.IsInterfacePublic ?? true; - - var generator = new DataLoaderSyntaxGenerator(); - - generator.WriterHeader(); - generator.WriteBeginNamespace(dataLoader.Namespace); - - generator.WriteDataLoaderInterface(dataLoader.InterfaceName, isInterfacePublic, kind, keyType, valueType); - - generator.WriteBeginDataLoaderClass( - dataLoader.Name, - dataLoader.InterfaceName, - isPublic, - kind, - keyType, - valueType); - generator.WriteDataLoaderConstructor(dataLoader.Name, kind); - generator.WriteDataLoaderLoadMethod( - dataLoader.ContainingType, - dataLoader.MethodName, - isScoped, - kind, - keyType, - valueType, - services, - parameterCount, - cancelIndex); - generator.WriteEndDataLoaderClass(); - - generator.WriteEndNamespace(); - } - - private static void GenerateDataLoaderRegistrations( - ModuleInfo module, - List dataLoaders, - StringBuilder sourceText) - { - sourceText.Append(Indent) - .Append("public static partial class ") - .Append(module.ModuleName) - .AppendLine("RequestExecutorBuilderExtensions"); - - sourceText - .Append(Indent) - .AppendLine("{"); - - sourceText - .Append(Indent) - .Append(Indent) - .Append("static partial void RegisterGeneratedDataLoader(") - .AppendLine("IRequestExecutorBuilder builder)"); - - sourceText - .Append(Indent) - .Append(Indent) - .AppendLine("{"); - - foreach (var dataLoader in dataLoaders) - { - sourceText - .Append(Indent) - .Append(Indent) - .Append(Indent) - .Append("builder.AddDataLoader<") - .Append(dataLoader.InterfaceFullName) - .Append(", ") - .Append(dataLoader.FullName) - .AppendLine(">();"); - } - - sourceText - .Append(Indent) - .Append(Indent) - .AppendLine("}"); - - sourceText - .Append(Indent) - .AppendLine("}"); - } - - private static void InspectDataLoaderParameters( - DataLoaderInfo dataLoader, - ref int cancellationTokenIndex, - Dictionary serviceMap) - { - for (var i = 1; i < dataLoader.MethodSymbol.Parameters.Length; i++) - { - var argument = dataLoader.MethodSymbol.Parameters[i]; - var argumentType = argument.Type.ToFullyQualified(); - - if (IsCancellationToken(argumentType)) - { - if (cancellationTokenIndex != -1) - { - // report error - return; - } - - cancellationTokenIndex = i; - } - else - { - serviceMap[i] = argumentType; - } - } - } - - private static bool IsKeysArgument(ITypeSymbol type) - => type is INamedTypeSymbol { IsGenericType: true, TypeArguments.Length: 1, } nt && - ReadOnlyList.Equals(ToTypeNameNoGenerics(nt), Ordinal); - - private static ITypeSymbol ExtractKeyType(ITypeSymbol type) - { - if (type is INamedTypeSymbol { IsGenericType: true, TypeArguments.Length: 1, } namedType && - ReadOnlyList.Equals(ToTypeNameNoGenerics(namedType), Ordinal)) - { - return namedType.TypeArguments[0]; - } - - throw new InvalidOperationException(); - } - - private static bool IsCancellationToken(string typeName) - => string.Equals(typeName, WellKnownTypes.CancellationToken) || - string.Equals(typeName, GlobalCancellationToken); - - private static bool IsReturnTypeDictionary(ITypeSymbol returnType, ITypeSymbol keyType) - { - if (returnType is INamedTypeSymbol { TypeArguments.Length: 1, } namedType) - { - var resultType = namedType.TypeArguments[0]; - - if (IsReadOnlyDictionary(resultType) && - resultType is INamedTypeSymbol { TypeArguments.Length: 2, } dictionaryType && - dictionaryType.TypeArguments[0].Equals(keyType, SymbolEqualityComparer.Default)) - { - return true; - } - } - - return false; - } - - private static bool IsReturnTypeLookup(ITypeSymbol returnType, ITypeSymbol keyType) - { - if (returnType is INamedTypeSymbol { TypeArguments.Length: 1, } namedType) - { - var resultType = namedType.TypeArguments[0]; - - if (ToTypeNameNoGenerics(resultType).Equals(Lookup, Ordinal) && - resultType is INamedTypeSymbol { TypeArguments.Length: 2, } dictionaryType && - dictionaryType.TypeArguments[0].Equals(keyType, SymbolEqualityComparer.Default)) - { - return true; - } - } - return false; - } - - private static bool IsReadOnlyDictionary(ITypeSymbol type) - { - if (!ToTypeNameNoGenerics(type).Equals(ReadOnlyDictionary, Ordinal)) - { - foreach (var interfaceSymbol in type.Interfaces) - { - if (ToTypeNameNoGenerics(interfaceSymbol).Equals(ReadOnlyDictionary, Ordinal)) - { - return true; - } - } - - return false; - } - - return true; - } - - private static ITypeSymbol ExtractValueType(ITypeSymbol returnType, DataLoaderKind kind) - { - if (returnType is INamedTypeSymbol { TypeArguments.Length: 1, } namedType) - { - if (kind is DataLoaderKind.Batch or DataLoaderKind.Group && - namedType.TypeArguments[0] is INamedTypeSymbol { TypeArguments.Length: 2, } dict) - { - return dict.TypeArguments[1]; - } - - if (kind is DataLoaderKind.Cache) - { - return namedType.TypeArguments[0]; - } - } - - throw new InvalidOperationException(); - } - - private static string ToTypeNameNoGenerics(ITypeSymbol typeSymbol) - => $"{typeSymbol.ContainingNamespace}.{typeSymbol.Name}"; -} - -internal static class GeneratorUtils -{ - public static ModuleInfo GetModuleInfo( - this ImmutableArray syntaxInfos, - string? assemblyName, - out bool defaultModule) - { - foreach (var syntaxInfo in syntaxInfos) - { - if (syntaxInfo is ModuleInfo module) - { - defaultModule = false; - return module; - } - } - - defaultModule = true; - return new ModuleInfo(CreateModuleName(assemblyName), ModuleOptions.Default); - } - - public static (ModuleInfo, DataLoaderDefaultsInfo) GetDataLoaderDefaults( - this ReadOnlySpan syntaxInfos, - string? assemblyName) - { - ModuleInfo? moduleInfo = null; - DataLoaderDefaultsInfo? dataLoaderDefaultsInfo = null; - - foreach (var syntaxInfo in syntaxInfos) - { - if (moduleInfo is null && syntaxInfo is ModuleInfo mi) - { - moduleInfo = mi; - } - else if (dataLoaderDefaultsInfo is null && syntaxInfo is DataLoaderDefaultsInfo dldi) - { - dataLoaderDefaultsInfo = dldi; - } - - if (moduleInfo is not null && dataLoaderDefaultsInfo is not null) - { - return (moduleInfo, dataLoaderDefaultsInfo); - } - } - - moduleInfo ??= new ModuleInfo(CreateModuleName(assemblyName), ModuleOptions.Default); - dataLoaderDefaultsInfo ??= new DataLoaderDefaultsInfo(null, null, true, true); - - return (moduleInfo, dataLoaderDefaultsInfo); - } - - private static string CreateModuleName(string? assemblyName) - => assemblyName is null - ? "AssemblyTypes" - : assemblyName.Split('.').Last() + "Types"; -} - -public sealed class DataLoaderSyntaxGenerator -{ - private StringBuilder _sb; - private CodeWriter _writer; - private bool _disposed; - - public DataLoaderSyntaxGenerator() - { - _sb = StringBuilderPool.Get(); - _writer = new CodeWriter(_sb); - } - - public void WriterHeader() - { - _writer.WriteFileHeader(); - _writer.WriteIndentedLine("using Microsoft.Extensions.DependencyInjection;"); - } - - public void WriteBeginNamespace(string ns) - { - _writer.WriteIndentedLine("namespace {0}", ns); - _writer.WriteIndentedLine("{"); - _writer.IncreaseIndent(); - } - - public void WriteEndNamespace() - { - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("}"); - } - - public void WriteDataLoaderInterface( - string name, - bool isPublic, - DataLoaderKind kind, - ITypeSymbol key, - ITypeSymbol value) - { - _writer.WriteIndentedLine( - "{0} interface {1}", - isPublic - ? "public" - : "internal", - name); - _writer.IncreaseIndent(); - - if (kind is DataLoaderKind.Group) - { - _writer.WriteIndentedLine( - ": global::GreenDonut.IDataLoader<{0}, {1}[]>", - key.ToFullyQualified(), - value.ToFullyQualified()); - } - else - { - _writer.WriteIndentedLine( - ": global::GreenDonut.IDataLoader<{0}, {1}>", - key.ToFullyQualified(), - value.ToFullyQualified()); - } - - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("{"); - _writer.WriteIndentedLine("}"); - } - - public void WriteBeginDataLoaderClass( - string name, - string interfaceName, - bool isPublic, - DataLoaderKind kind, - ITypeSymbol key, - ITypeSymbol value) - { - _writer.WriteIndentedLine( - "{0} sealed class {1}", - isPublic - ? "public" - : "internal", - name); - _writer.IncreaseIndent(); - - switch (kind) - { - case DataLoaderKind.Batch: - _writer.WriteIndentedLine( - ": global::GreenDonut.BatchDataLoader<{0}, {1}[]>", - key.ToFullyQualified(), - value.ToFullyQualified()); - break; - - case DataLoaderKind.Group: - _writer.WriteIndentedLine( - ": global::GreenDonut.GroupedDataLoader<{0}, {1}>", - key.ToFullyQualified(), - value.ToFullyQualified()); - break; - - case DataLoaderKind.Cache: - _writer.WriteIndentedLine( - ": global::GreenDonut.CacheDataLoader<{0}, {1}>", - key.ToFullyQualified(), - value.ToFullyQualified()); - break; - } - - _writer.WriteIndentedLine(", {0}", interfaceName); - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("{"); - } - - public void WriteEndDataLoaderClass() - { - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("}"); - } - - public void WriteDataLoaderConstructor( - string name, - DataLoaderKind kind) - { - _writer.WriteIndentedLine("private readonly global::System.IServiceProvider _services;"); - _writer.WriteLine(); - - if (kind is DataLoaderKind.Batch or DataLoaderKind.Group) - { - _writer.WriteIndentedLine("public {0}(", name); - - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("global::System.IServiceProvider services,"); - _writer.WriteIndentedLine("global::GreenDonut.IBatchScheduler batchScheduler,"); - _writer.WriteIndentedLine("global::GreenDonut.DataLoaderOptions? options = null)"); - _writer.WriteIndentedLine(": base(batchScheduler, options)"); - } - } - else - { - _writer.WriteIndentedLine("public {0}(", name); - - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("global::System.IServiceProvider services,"); - _writer.WriteIndentedLine("global::GreenDonut.DataLoaderOptions? options = null)"); - _writer.WriteIndentedLine(": base(options)"); - } - } - - _writer.WriteIndentedLine("{"); - - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("_services = services ?? System.ArgumentNullException(nameof(services));"); - } - - _writer.WriteIndentedLine("}"); - _writer.WriteLine(); - } - - public void WriteDataLoaderLoadMethod( - string containingType, - string methodName, - bool isScoped, - DataLoaderKind kind, - ITypeSymbol key, - ITypeSymbol value, - Dictionary services, - int parameterCount, - int cancelIndex) - { - if (kind is DataLoaderKind.Batch) - { - _writer.WriteIndentedLine( - "protected override async global::{0}<{1}<{2}, {3}>> LoadBatchAsync(", - WellKnownTypes.Task, - ReadOnlyDictionary, - key.ToFullyQualified(), - value.ToFullyQualified()); - - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("{0}<{1}> keys,", ReadOnlyList, key.ToFullyQualified()); - _writer.WriteIndentedLine("global::{0} ct)", WellKnownTypes.CancellationToken); - } - } - else if (kind is DataLoaderKind.Group) - { - _writer.WriteIndentedLine( - "protected override async global::{0}<{1}<{2}, {3}>> LoadGroupedBatchAsync(", - WellKnownTypes.Task, - Lookup, - key.ToFullyQualified(), - value.ToFullyQualified()); - - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("{0}<{1}> keys,", ReadOnlyList, key.ToFullyQualified()); - _writer.WriteIndentedLine("global::{0} ct)", WellKnownTypes.CancellationToken); - } - } - else if (kind is DataLoaderKind.Cache) - { - _writer.WriteIndentedLine( - "protected override async global::{0}<{1}> LoadSingleAsync(", - WellKnownTypes.Task, - value.ToFullyQualified()); - - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("{0} keys,", ReadOnlyList, key.ToFullyQualified()); - _writer.WriteIndentedLine("global::{0} ct)", WellKnownTypes.CancellationToken); - } - } - - _writer.WriteIndentedLine("{"); - - using (_writer.IncreaseIndent()) - { - if (isScoped) - { - _writer.WriteIndentedLine("await using var scope = _services.CreateAsyncScope();"); - - foreach (var item in services.OrderBy(t => t.Key)) - { - _writer.WriteIndentedLine( - "var p{0} = scope.ServiceProvider.GetRequiredService<{1}>();", - item.Key, - item.Value); - } - } - else - { - foreach (var item in services.OrderBy(t => t.Key)) - { - _writer.WriteIndentedLine( - "var p{0} = _services.GetRequiredService<{1}>();", - item.Key, - item.Value); - } - } - - _writer.WriteIndented("return await {0}.{1}(", containingType, methodName); - - for (var i = 0; i < parameterCount; i++) - { - if (i > 0) - { - _writer.Write(", "); - } - - if (i == 0) - { - _writer.Write( - kind is DataLoaderKind.Cache - ? "key" - : "keys"); - } - else if (i == cancelIndex) - { - _writer.Write("ct"); - } - else - { - _writer.Write("p"); - _writer.Write(i); - } - } - _writer.WriteLine(").ConfigureAwait(false);"); - } - - _writer.WriteIndentedLine("}"); - _writer.WriteIndentedLine("}"); - _writer.WriteIndentedLine("}"); - _writer.WriteLine(); - } - - public override string ToString() - => _sb.ToString(); - - public SourceText ToSourceText() - => SourceText.From(ToString(), Encoding.UTF8); - - public void Dispose() - { - if (_disposed) - { - return; - } - - StringBuilderPool.Return(_sb); - _sb = default!; - _writer = default!; - _disposed = true; - } -} \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderSyntaxGenerator.cs new file mode 100644 index 00000000000..0e5b69474e9 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderSyntaxGenerator.cs @@ -0,0 +1,301 @@ +using System.Text; +using HotChocolate.Types.Analyzers.Helpers; +using HotChocolate.Types.Analyzers.Inspectors; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace HotChocolate.Types.Analyzers.Generators; + +public sealed class DataLoaderSyntaxGenerator : IDisposable +{ + private StringBuilder _sb; + private CodeWriter _writer; + private bool _disposed; + + public DataLoaderSyntaxGenerator() + { + _sb = StringBuilderPool.Get(); + _writer = new CodeWriter(_sb); + } + + public void WriterHeader() + { + _writer.WriteFileHeader(); + _writer.WriteIndentedLine("using Microsoft.Extensions.DependencyInjection;"); + _writer.WriteLine(); + } + + public void WriteBeginNamespace(string ns) + { + _writer.WriteIndentedLine("namespace {0}", ns); + _writer.WriteIndentedLine("{"); + _writer.IncreaseIndent(); + } + + public void WriteEndNamespace() + { + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("}"); + _writer.WriteLine(); + } + + public void WriteDataLoaderInterface( + string name, + bool isPublic, + DataLoaderKind kind, + ITypeSymbol key, + ITypeSymbol value) + { + _writer.WriteIndentedLine( + "{0} interface {1}", + isPublic + ? "public" + : "internal", + name); + _writer.IncreaseIndent(); + + _writer.WriteIndentedLine( + kind is DataLoaderKind.Group + ? ": global::GreenDonut.IDataLoader<{0}, {1}[]>" + : ": global::GreenDonut.IDataLoader<{0}, {1}>", + key.ToFullyQualified(), + value.ToFullyQualified()); + + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("{"); + _writer.WriteIndentedLine("}"); + _writer.WriteLine(); + } + + public void WriteBeginDataLoaderClass( + string name, + string interfaceName, + bool isPublic, + DataLoaderKind kind, + ITypeSymbol key, + ITypeSymbol value) + { + _writer.WriteIndentedLine( + "{0} sealed class {1}", + isPublic + ? "public" + : "internal", + name); + _writer.IncreaseIndent(); + + switch (kind) + { + case DataLoaderKind.Batch: + _writer.WriteIndentedLine( + ": global::GreenDonut.BatchDataLoader<{0}, {1}>", + key.ToFullyQualified(), + value.ToFullyQualified()); + break; + + case DataLoaderKind.Group: + _writer.WriteIndentedLine( + ": global::GreenDonut.GroupedDataLoader<{0}, {1}>", + key.ToFullyQualified(), + value.ToFullyQualified()); + break; + + case DataLoaderKind.Cache: + _writer.WriteIndentedLine( + ": global::GreenDonut.CacheDataLoader<{0}, {1}>", + key.ToFullyQualified(), + value.ToFullyQualified()); + break; + } + + _writer.WriteIndentedLine(", {0}", interfaceName); + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("{"); + _writer.IncreaseIndent(); + } + + public void WriteEndDataLoaderClass() + { + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("}"); + _writer.WriteLine(); + } + + public void WriteDataLoaderConstructor( + string name, + DataLoaderKind kind) + { + _writer.WriteIndentedLine("private readonly global::System.IServiceProvider _services;"); + _writer.WriteLine(); + + if (kind is DataLoaderKind.Batch or DataLoaderKind.Group) + { + _writer.WriteIndentedLine("public {0}(", name); + + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine("global::System.IServiceProvider services,"); + _writer.WriteIndentedLine("global::GreenDonut.IBatchScheduler batchScheduler,"); + _writer.WriteIndentedLine("global::GreenDonut.DataLoaderOptions? options = null)"); + _writer.WriteIndentedLine(": base(batchScheduler, options)"); + } + } + else + { + _writer.WriteIndentedLine("public {0}(", name); + + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine("global::System.IServiceProvider services,"); + _writer.WriteIndentedLine("global::GreenDonut.DataLoaderOptions? options = null)"); + _writer.WriteIndentedLine(": base(options)"); + } + } + + _writer.WriteIndentedLine("{"); + + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine("_services = services ??"); + + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine("throw new global::System.ArgumentNullException(nameof(services));"); + } + } + + _writer.WriteIndentedLine("}"); + } + + public void WriteDataLoaderLoadMethod( + string containingType, + string methodName, + bool isScoped, + DataLoaderKind kind, + ITypeSymbol key, + ITypeSymbol value, + Dictionary services, + int parameterCount, + int cancelIndex) + { + if (kind is DataLoaderKind.Batch) + { + _writer.WriteIndentedLine( + "protected override async global::{0}<{1}<{2}, {3}>> LoadBatchAsync(", + WellKnownTypes.Task, + WellKnownTypes.ReadOnlyDictionary, + key.ToFullyQualified(), + value.ToFullyQualified()); + + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine("{0}<{1}> keys,", WellKnownTypes.ReadOnlyList, key.ToFullyQualified()); + _writer.WriteIndentedLine("global::{0} ct)", WellKnownTypes.CancellationToken); + } + } + else if (kind is DataLoaderKind.Group) + { + _writer.WriteIndentedLine( + "protected override async global::{0}<{1}<{2}, {3}>> LoadGroupedBatchAsync(", + WellKnownTypes.Task, + WellKnownTypes.Lookup, + key.ToFullyQualified(), + value.ToFullyQualified()); + + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine("{0}<{1}> keys,", WellKnownTypes.ReadOnlyList, key.ToFullyQualified()); + _writer.WriteIndentedLine("global::{0} ct)", WellKnownTypes.CancellationToken); + } + } + else if (kind is DataLoaderKind.Cache) + { + _writer.WriteIndentedLine( + "protected override async global::{0}<{1}> LoadSingleAsync(", + WellKnownTypes.Task, + value.ToFullyQualified()); + + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine("{0} key,", key.ToFullyQualified()); + _writer.WriteIndentedLine("global::{0} ct)", WellKnownTypes.CancellationToken); + } + } + + _writer.WriteIndentedLine("{"); + + using (_writer.IncreaseIndent()) + { + if (isScoped) + { + _writer.WriteIndentedLine("await using var scope = _services.CreateAsyncScope();"); + + foreach (var item in services.OrderBy(t => t.Key)) + { + _writer.WriteIndentedLine( + "var p{0} = scope.ServiceProvider.GetRequiredService<{1}>();", + item.Key, + item.Value); + } + } + else + { + foreach (var item in services.OrderBy(t => t.Key)) + { + _writer.WriteIndentedLine( + "var p{0} = _services.GetRequiredService<{1}>();", + item.Key, + item.Value); + } + } + + _writer.WriteIndented("return await {0}.{1}(", containingType, methodName); + + for (var i = 0; i < parameterCount; i++) + { + if (i > 0) + { + _writer.Write(", "); + } + + if (i == 0) + { + _writer.Write( + kind is DataLoaderKind.Cache + ? "key" + : "keys"); + } + else if (i == cancelIndex) + { + _writer.Write("ct"); + } + else + { + _writer.Write("p"); + _writer.Write(i); + } + } + _writer.WriteLine(").ConfigureAwait(false);"); + } + + _writer.WriteIndentedLine("}"); + } + + public override string ToString() + => _sb.ToString(); + + public SourceText ToSourceText() + => SourceText.From(ToString(), Encoding.UTF8); + + public void Dispose() + { + if (_disposed) + { + return; + } + + StringBuilderPool.Return(_sb); + _sb = default!; + _writer = default!; + _disposed = true; + } +} \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ISyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ISyntaxGenerator.cs deleted file mode 100644 index 46fef62a445..00000000000 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ISyntaxGenerator.cs +++ /dev/null @@ -1,29 +0,0 @@ -using HotChocolate.Types.Analyzers.Inspectors; -using Microsoft.CodeAnalysis; - -namespace HotChocolate.Types.Analyzers.Generators; - -/// -/// A syntax generator produces C# code from the consumed syntax infos. -/// -public interface ISyntaxGenerator -{ - /// - /// Allows to create initial code like attributes. - /// - /// - void Initialize(IncrementalGeneratorPostInitializationContext context); - - /// - /// Specifies if the given will be consumed by this generator. - /// - bool Consume(ISyntaxInfo syntaxInfo); - - /// - /// Generates the C# source code for the consumed syntax infos. - /// - void Generate( - SourceProductionContext context, - Compilation compilation, - ReadOnlySpan consumed); -} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/ModuleSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleSyntaxGenerator.cs similarity index 91% rename from src/HotChocolate/Core/src/Types.Analyzers/Helpers/ModuleSyntaxGenerator.cs rename to src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleSyntaxGenerator.cs index 9ae7d928b61..62a8ed795f2 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/ModuleSyntaxGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleSyntaxGenerator.cs @@ -1,7 +1,8 @@ using System.Text; +using HotChocolate.Types.Analyzers.Helpers; using Microsoft.CodeAnalysis.Text; -namespace HotChocolate.Types.Analyzers.Helpers; +namespace HotChocolate.Types.Analyzers.Generators; public sealed class ModuleSyntaxGenerator : IDisposable { @@ -76,6 +77,9 @@ public void WriteRegisterTypeExtension(string typeName, bool staticType) public void WriteRegisterDataLoader(string typeName) => _writer.WriteIndentedLine("builder.AddDataLoader();", typeName); + + public void WriteRegisterDataLoader(string typeName, string interfaceTypeName) + => _writer.WriteIndentedLine("builder.AddDataLoader();", interfaceTypeName, typeName); public void WriteTryAddOperationType(OperationType type) { diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/OperationFieldSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/OperationFieldSyntaxGenerator.cs new file mode 100644 index 00000000000..13161077675 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/OperationFieldSyntaxGenerator.cs @@ -0,0 +1,108 @@ +using System.Text; +using HotChocolate.Types.Analyzers.Helpers; +using HotChocolate.Types.Analyzers.Inspectors; +using Microsoft.CodeAnalysis.Text; + +namespace HotChocolate.Types.Analyzers.Generators; + +public sealed class OperationFieldSyntaxGenerator: IDisposable +{ + private StringBuilder _sb; + private CodeWriter _writer; + private bool _disposed; + + public OperationFieldSyntaxGenerator() + { + _sb = StringBuilderPool.Get(); + _writer = new CodeWriter(_sb); + } + + public void WriterHeader() + { + _writer.WriteFileHeader(); + _writer.WriteLine(); + } + + public void WriteBeginNamespace(string ns) + { + _writer.WriteIndentedLine("namespace {0}", ns); + _writer.WriteIndentedLine("{"); + _writer.IncreaseIndent(); + } + + public void WriteEndNamespace() + { + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("}"); + _writer.WriteLine(); + } + + public void WriteBeginClass(string typeName) + { + _writer.WriteIndentedLine("public sealed class {0}", typeName); + + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine(": global::HotChocolate.Types.ObjectTypeExtension"); + } + _writer.WriteIndentedLine("{"); + _writer.IncreaseIndent(); + } + + public void WriteEndClass() + { + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("}"); + } + + public void WriteConfigureMethod(IEnumerable operations) + { + _writer.WriteIndentedLine("protected override void Configure("); + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine("global::HotChocolate.Types.IObjectTypeDescriptor descriptor)"); + } + _writer.WriteIndentedLine("{"); + _writer.IncreaseIndent(); + + var typeIndex = 0; + + foreach (var group in operations.GroupBy(t => t.TypeName)) + { + var typeName = $"type{++typeIndex}"; + _writer.WriteIndentedLine("Type {0} = typeof({1});", typeName, group.Key); + + foreach (var operation in group) + { + _writer.WriteIndentedLine( + "descriptor.Field({0}.GetMember(\"{1}\", System.Reflection.BindingFlags.Public)[0]);", + typeName, + operation.MethodName); + } + + _writer.WriteLine(); + } + + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("}"); + } + + public override string ToString() + => _sb.ToString(); + + public SourceText ToSourceText() + => SourceText.From(ToString(), Encoding.UTF8); + + public void Dispose() + { + if (_disposed) + { + return; + } + + StringBuilderPool.Return(_sb); + _sb = default!; + _writer = default!; + _disposed = true; + } +} \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/OperationGenerator.txt b/src/HotChocolate/Core/src/Types.Analyzers/Generators/OperationGenerator.txt deleted file mode 100644 index 4dcda3922a6..00000000000 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/OperationGenerator.txt +++ /dev/null @@ -1,150 +0,0 @@ -using System.Text; -using HotChocolate.Types.Analyzers.Helpers; -using HotChocolate.Types.Analyzers.Inspectors; -using Microsoft.CodeAnalysis; -using static HotChocolate.Types.Analyzers.StringConstants; - -namespace HotChocolate.Types.Analyzers.Generators; - -public class OperationGenerator : ISyntaxGenerator -{ - public void Initialize(IncrementalGeneratorPostInitializationContext context) { } - - public bool Consume(ISyntaxInfo syntaxInfo) - => syntaxInfo is DataLoaderInfo or ModuleInfo or DataLoaderDefaultsInfo; - - public void Generate( - SourceProductionContext context, - Compilation compilation, - ReadOnlySpan syntaxInfos) - { - var module = syntaxInfos.GetModuleInfo(compilation.AssemblyName, out var defaultModule); - - // if there is only the module info we do not need to generate a module. - if (!defaultModule && syntaxInfos.Length == 1) - { - return; - } - - var sourceText = StringBuilderPool.Get(); - sourceText.AppendLine("// "); - sourceText.AppendLine("#nullable enable"); - sourceText.AppendLine("using System;"); - sourceText.AppendLine("using HotChocolate.Execution.Configuration;"); - - sourceText.AppendLine(); - sourceText.AppendLine("namespace Microsoft.Extensions.DependencyInjection"); - sourceText.AppendLine("{"); - - sourceText.Append(Indent) - .Append("public static partial class ") - .Append(module.ModuleName) - .AppendLine("RequestExecutorBuilderExtensions"); - - sourceText.Append(Indent) - .AppendLine("{"); - - - - sourceText.Append(Indent) - .AppendLine("}"); - - sourceText.AppendLine("}"); - - throw new NotImplementedException(); - } - - private void Foo(ReadOnlySpan operations) - { - var sourceText = StringBuilderPool.Get(); - - foreach (var operation in operations) - { - /* - public class __Query_1234 : ObjectTypeExtension - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - Type type1 = typeof(Query); - descriptor.Field(type1.GetMember("GetBook", System.Reflection.BindingFlags.Public)[0]); - } - }*/ - } - } - // BindingFlags.Static | BindingFlags.Public -} - - - - -public static class QueryExtensionGenerator -{ - public static void Write( - CodeWriter writer, - string ns, - string typeName, - IReadOnlyCollection operations) - { - writer.WriteIndentedLine("namespace {0}", ns); - writer.WriteIndentedLine("{"); - - WriteType(writer, typeName, operations); - - writer.WriteIndentedLine("}"); - } - - private static void WriteType(CodeWriter writer, string typeName, IReadOnlyCollection operations) - { - writer.WriteIndentedLine("public sealed class {0} : ObjectTypeExtension", typeName); - writer.WriteIndentedLine("{"); - - using (writer.IncreaseIndent()) - { - WriteConfigure(writer, operations); - } - - writer.WriteIndentedLine("}"); - } - - private static void WriteConfigure(CodeWriter writer, IReadOnlyCollection operations) - { - writer.WriteIndentedLine( - "protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor)"); - writer.WriteIndentedLine("{"); - - using (writer.IncreaseIndent()) - { - var typeCount = 0; - foreach (var type in operations.GroupBy(t => t.TypeName)) - { - var typeVariableName = $"t_{++typeCount}"; - WriteType(writer, typeVariableName, type.Key); - - foreach (var operation in type) - { - WriteRootField(writer, typeVariableName, operation.MethodName); - } - } - } - - writer.WriteIndentedLine("}"); - } - - private static void WriteType(CodeWriter writer, string typeVariable, string typeName) - => writer.WriteIndentedLine("Type {0} = typeof({1});", typeVariable, typeName); - - private static void WriteRootField(CodeWriter writer, string typeVariable, string methodName) - => writer.WriteIndentedLine( - "descriptor.Field({0}.GetMember(\"{1}\", " + - "global::System.Reflection.BindingFlags.Static | " + - "global::System.Reflection.BindingFlags.Public | " + - "global::System.Reflection.BindingFlags.NonPublic)[0]);", - typeVariable, - methodName); -} - -public class OperationInfo -{ - public string TypeName { get; } = default!; - public string MethodName { get; } = default!; -} \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CodeWriterExtensions.cs b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CodeWriterExtensions.cs index 619956a4e34..43ddf5be758 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CodeWriterExtensions.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CodeWriterExtensions.cs @@ -19,8 +19,6 @@ public static void WriteGeneratedAttribute(this CodeWriter writer) "[global::System.CodeDom.Compiler.GeneratedCode(" + $"\"HotChocolate\", \"{version}\")]"); #endif - - } public static void WriteFileHeader(this CodeWriter writer) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/GeneratorUtils.cs b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/GeneratorUtils.cs new file mode 100644 index 00000000000..8c4ee3d95ed --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/GeneratorUtils.cs @@ -0,0 +1,44 @@ +using System.Collections.Immutable; +using HotChocolate.Types.Analyzers.Inspectors; + +namespace HotChocolate.Types.Analyzers.Helpers; + +internal static class GeneratorUtils +{ + public static ModuleInfo GetModuleInfo( + this ImmutableArray syntaxInfos, + string? assemblyName, + out bool defaultModule) + { + foreach (var syntaxInfo in syntaxInfos) + { + if (syntaxInfo is ModuleInfo module) + { + defaultModule = false; + return module; + } + } + + defaultModule = true; + return new ModuleInfo(CreateModuleName(assemblyName), ModuleOptions.Default); + } + + public static DataLoaderDefaultsInfo GetDataLoaderDefaults( + this ImmutableArray syntaxInfos) + { + foreach (var syntaxInfo in syntaxInfos) + { + if (syntaxInfo is DataLoaderDefaultsInfo defaults) + { + return defaults; + } + } + + return new DataLoaderDefaultsInfo(null, null, true, true); + } + + private static string CreateModuleName(string? assemblyName) + => assemblyName is null + ? "AssemblyTypes" + : assemblyName.Split('.').Last() + "Types"; +} \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInfo.cs new file mode 100644 index 00000000000..d2616268ea3 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInfo.cs @@ -0,0 +1,58 @@ +namespace HotChocolate.Types.Analyzers.Inspectors; + +public sealed class OperationInfo(OperationType type, string typeName, string methodName) : ISyntaxInfo +{ + public OperationType Type { get; } = type; + + public string TypeName { get; } = typeName; + + public string MethodName { get; } = methodName; + + public bool Equals(OperationInfo? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Type.Equals(other.Type) && + TypeName.Equals(other.TypeName, StringComparison.Ordinal) && + MethodName.Equals(other.MethodName, StringComparison.Ordinal); + } + + public bool Equals(ISyntaxInfo other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return other is OperationInfo info && Equals(info); + } + + public override bool Equals(object? obj) + => ReferenceEquals(this, obj) + || obj is DataLoaderInfo other && Equals(other); + + public override int GetHashCode() + { + unchecked + { + var hashCode = 5; + hashCode = (hashCode * 397) ^ Type.GetHashCode(); + hashCode = (hashCode * 397) ^ TypeName.GetHashCode(); + hashCode = (hashCode * 397) ^ MethodName.GetHashCode(); + return hashCode; + } + } +} \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInspector.cs index c0909ca4ee6..90ffeff82fd 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInspector.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInspector.cs @@ -26,18 +26,27 @@ public bool TryHandle( var attributeContainingTypeSymbol = attributeSymbol.ContainingType; var fullName = attributeContainingTypeSymbol.ToDisplayString(); + var operationType = ParseOperationType(fullName); + + if(operationType == OperationType.No) + { + continue; + } - if (!fullName.Equals(WellKnownAttributes.QueryAttribute, StringComparison.Ordinal) || - context.SemanticModel.GetDeclaredSymbol(methodSyntax) is not { } methodSymbol) + if (context.SemanticModel.GetDeclaredSymbol(methodSyntax) is not { } methodSymbol) + { + continue; + } + + if (!methodSymbol.IsStatic) { continue; } - syntaxInfo = new DataLoaderInfo( - attributeSyntax, - attributeSymbol, - methodSymbol, - methodSyntax); + syntaxInfo = new OperationInfo( + operationType, + methodSymbol.ContainingType.ToDisplayString(), + methodSymbol.Name); return true; } } @@ -46,4 +55,24 @@ public bool TryHandle( syntaxInfo = null; return false; } + + private OperationType ParseOperationType(string attributeName) + { + if (attributeName.Equals(WellKnownAttributes.QueryAttribute, StringComparison.Ordinal)) + { + return OperationType.Query; + } + + if (attributeName.Equals(WellKnownAttributes.MutationAttribute, StringComparison.Ordinal)) + { + return OperationType.Mutation; + } + + if (attributeName.Equals(WellKnownAttributes.SubscriptionAttribute, StringComparison.Ordinal)) + { + return OperationType.Subscription; + } + + return OperationType.No; + } } \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationRegistrationInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationRegistrationInfo.cs new file mode 100644 index 00000000000..4e970e5948e --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationRegistrationInfo.cs @@ -0,0 +1,50 @@ +namespace HotChocolate.Types.Analyzers.Inspectors; + +public sealed class OperationRegistrationInfo(string typeName) : ISyntaxInfo +{ + public string TypeName { get; } = typeName; + + public bool Equals(OperationRegistrationInfo? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return TypeName.Equals(other.TypeName, StringComparison.Ordinal); + } + + public bool Equals(ISyntaxInfo other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return other is OperationInfo info && Equals(info); + } + + public override bool Equals(object? obj) + => ReferenceEquals(this, obj) + || obj is DataLoaderInfo other && Equals(other); + + public override int GetHashCode() + { + unchecked + { + var hashCode = 5; + hashCode = (hashCode * 397) ^ TypeName.GetHashCode(); + return hashCode; + } + } +} \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Types.Analyzers/TypeModuleGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/TypeModuleGenerator.cs index ed697efb516..e8d633b6b49 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/TypeModuleGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/TypeModuleGenerator.cs @@ -4,7 +4,9 @@ using HotChocolate.Types.Analyzers.Inspectors; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using static System.StringComparison; using static HotChocolate.Types.Analyzers.WellKnownFileNames; +using static HotChocolate.Types.Analyzers.WellKnownTypes; using TypeInfo = HotChocolate.Types.Analyzers.Inspectors.TypeInfo; namespace HotChocolate.Types.Analyzers; @@ -19,12 +21,11 @@ public class TypeModuleGenerator : IIncrementalGenerator new ModuleInspector(), new DataLoaderInspector(), new DataLoaderDefaultsInspector(), + new OperationInspector(), ]; public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterPostInitializationOutput(c => PostInitialization(c)); - var modulesAndTypes = context.SyntaxProvider .CreateSyntaxProvider( @@ -40,11 +41,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) static (context, source) => Execute(context, source.Left, source.Right)); } - private static void PostInitialization(IncrementalGeneratorPostInitializationContext context) - { - - } - private static bool IsRelevant(SyntaxNode node) => IsTypeWithAttribute(node) || IsClassWithBaseClass(node) || @@ -87,8 +83,9 @@ private static void Execute( { return; } - + var module = syntaxInfos.GetModuleInfo(compilation.AssemblyName, out var defaultModule); + var dataLoaderDefaults = syntaxInfos.GetDataLoaderDefaults(); // if there is only the module info we do not need to generate a module. if (!defaultModule && syntaxInfos.Length == 1) @@ -96,21 +93,24 @@ private static void Execute( return; } - WriteConfiguration(context, syntaxInfos, module); + var syntaxInfoList = syntaxInfos.ToList(); + WriteOperationTypes(context, syntaxInfoList, module); + WriteDataLoader(context, syntaxInfoList, dataLoaderDefaults); + WriteConfiguration(context, syntaxInfoList, module); } - + private static void WriteConfiguration( SourceProductionContext context, - ImmutableArray syntaxInfos, + List syntaxInfos, ModuleInfo module) { using var generator = new ModuleSyntaxGenerator(module.ModuleName, "Microsoft.Extensions.DependencyInjection"); - + generator.WriterHeader(); generator.WriteBeginNamespace(); generator.WriteBeginClass(); generator.WriteBeginRegistrationMethod(); - + var operations = OperationType.No; foreach (var syntaxInfo in syntaxInfos) @@ -146,9 +146,28 @@ private static void WriteConfiguration( generator.WriteRegisterDataLoader(dataLoader.Name); } break; + + case DataLoaderInfo dataLoader: + if ((module.Options & ModuleOptions.RegisterDataLoader) == + ModuleOptions.RegisterDataLoader) + { + var typeName = $"{dataLoader.Namespace}.{dataLoader.Name}"; + var interfaceTypeName = $"{dataLoader.Namespace}.{dataLoader.InterfaceName}"; + + generator.WriteRegisterDataLoader(typeName, interfaceTypeName); + } + break; + + case OperationRegistrationInfo operation: + if ((module.Options & ModuleOptions.RegisterTypes) == + ModuleOptions.RegisterTypes) + { + generator.WriteRegisterTypeExtension(operation.TypeName, false); + } + break; } } - + if ((operations & OperationType.Query) == OperationType.Query) { generator.WriteTryAddOperationType(OperationType.Query); @@ -163,11 +182,308 @@ private static void WriteConfiguration( { generator.WriteTryAddOperationType(OperationType.Subscription); } - + generator.WriteEndRegistrationMethod(); generator.WriteEndClass(); generator.WriteEndNamespace(); - + context.AddSource(TypeModuleFile, generator.ToSourceText()); } -} + + private static void WriteDataLoader( + SourceProductionContext context, + List syntaxInfos, + DataLoaderDefaultsInfo defaults) + { + var dataLoaders = new List(); + + foreach (var syntaxInfo in syntaxInfos) + { + if (syntaxInfo is not DataLoaderInfo dataLoader) + { + continue; + } + + if (dataLoader.MethodSymbol.Parameters.Length == 0) + { + context.ReportDiagnostic( + Diagnostic.Create( + Errors.KeyParameterMissing, + Location.Create( + dataLoader.MethodSyntax.SyntaxTree, + dataLoader.MethodSyntax.ParameterList.Span))); + continue; + } + + if (dataLoader.MethodSymbol.DeclaredAccessibility is not Accessibility.Public + and not Accessibility.Internal and not Accessibility.ProtectedAndInternal) + { + context.ReportDiagnostic( + Diagnostic.Create( + Errors.MethodAccessModifierInvalid, + Location.Create( + dataLoader.MethodSyntax.SyntaxTree, + dataLoader.MethodSyntax.Modifiers.Span))); + continue; + } + + dataLoaders.Add(dataLoader); + } + + var generator = new DataLoaderSyntaxGenerator(); + generator.WriterHeader(); + + foreach (var group in dataLoaders.GroupBy(t => t.Namespace)) + { + generator.WriteBeginNamespace(group.Key); + + foreach (var dataLoader in group) + { + var keyArg = dataLoader.MethodSymbol.Parameters[0]; + var keyType = keyArg.Type; + var cancellationTokenIndex = -1; + var serviceMap = new Dictionary(); + + if (IsKeysArgument(keyType)) + { + keyType = ExtractKeyType(keyType); + } + + InspectDataLoaderParameters( + dataLoader, + ref cancellationTokenIndex, + serviceMap); + + DataLoaderKind kind; + + if (IsReturnTypeDictionary(dataLoader.MethodSymbol.ReturnType, keyType)) + { + kind = DataLoaderKind.Batch; + } + else if (IsReturnTypeLookup(dataLoader.MethodSymbol.ReturnType, keyType)) + { + kind = DataLoaderKind.Group; + } + else + { + keyType = keyArg.Type; + kind = DataLoaderKind.Cache; + } + + var valueType = ExtractValueType(dataLoader.MethodSymbol.ReturnType, kind); + + GenerateDataLoader( + generator, + dataLoader, + defaults, + kind, + keyType, + valueType, + dataLoader.MethodSymbol.Parameters.Length, + cancellationTokenIndex, + serviceMap); + } + + generator.WriteEndNamespace(); + } + + context.AddSource(DataLoaderFile, generator.ToSourceText()); + } + + private static void WriteOperationTypes( + SourceProductionContext context, + List syntaxInfos, + ModuleInfo module) + { + var operations = new List(); + + foreach (var syntaxInfo in syntaxInfos) + { + if (syntaxInfo is OperationInfo operation) + { + operations.Add(operation); + } + } + + if (operations.Count == 0) + { + return; + } + + var generator = new OperationFieldSyntaxGenerator(); + generator.WriterHeader(); + generator.WriteBeginNamespace("Microsoft.Extensions.DependencyInjection"); + + foreach (var group in operations.GroupBy(t => t.Type)) + { + var typeName = $"{module.ModuleName}{group.Key}Type"; + + generator.WriteBeginClass(typeName); + generator.WriteConfigureMethod(group); + generator.WriteEndClass(); + + syntaxInfos.Add(new OperationRegistrationInfo($"Microsoft.Extensions.DependencyInjection.{typeName}")); + } + + generator.WriteEndNamespace(); + + context.AddSource(RootTypesFile, generator.ToSourceText()); + } + + private static void GenerateDataLoader( + DataLoaderSyntaxGenerator generator, + DataLoaderInfo dataLoader, + DataLoaderDefaultsInfo defaults, + DataLoaderKind kind, + ITypeSymbol keyType, + ITypeSymbol valueType, + int parameterCount, + int cancelIndex, + Dictionary services) + { + var isScoped = dataLoader.IsScoped ?? defaults.Scoped ?? false; + var isPublic = dataLoader.IsPublic ?? defaults.IsPublic ?? true; + var isInterfacePublic = dataLoader.IsInterfacePublic ?? defaults.IsInterfacePublic ?? true; + + generator.WriteDataLoaderInterface(dataLoader.InterfaceName, isInterfacePublic, kind, keyType, valueType); + + generator.WriteBeginDataLoaderClass( + dataLoader.Name, + dataLoader.InterfaceName, + isPublic, + kind, + keyType, + valueType); + generator.WriteDataLoaderConstructor(dataLoader.Name, kind); + generator.WriteDataLoaderLoadMethod( + dataLoader.ContainingType, + dataLoader.MethodName, + isScoped, + kind, + keyType, + valueType, + services, + parameterCount, + cancelIndex); + generator.WriteEndDataLoaderClass(); + } + + private static void InspectDataLoaderParameters( + DataLoaderInfo dataLoader, + ref int cancellationTokenIndex, + Dictionary serviceMap) + { + for (var i = 1; i < dataLoader.MethodSymbol.Parameters.Length; i++) + { + var argument = dataLoader.MethodSymbol.Parameters[i]; + var argumentType = argument.Type.ToFullyQualified(); + + if (IsCancellationToken(argumentType)) + { + if (cancellationTokenIndex != -1) + { + // report error + return; + } + + cancellationTokenIndex = i; + } + else + { + serviceMap[i] = argumentType; + } + } + } + + private static bool IsKeysArgument(ITypeSymbol type) + => type is INamedTypeSymbol { IsGenericType: true, TypeArguments.Length: 1, } nt && + ReadOnlyList.Equals(ToTypeNameNoGenerics(nt), Ordinal); + + private static ITypeSymbol ExtractKeyType(ITypeSymbol type) + { + if (type is INamedTypeSymbol { IsGenericType: true, TypeArguments.Length: 1, } namedType && + ReadOnlyList.Equals(ToTypeNameNoGenerics(namedType), Ordinal)) + { + return namedType.TypeArguments[0]; + } + + throw new InvalidOperationException(); + } + + private static bool IsCancellationToken(string typeName) + => string.Equals(typeName, WellKnownTypes.CancellationToken) || + string.Equals(typeName, GlobalCancellationToken); + + private static bool IsReturnTypeDictionary(ITypeSymbol returnType, ITypeSymbol keyType) + { + if (returnType is INamedTypeSymbol { TypeArguments.Length: 1, } namedType) + { + var resultType = namedType.TypeArguments[0]; + + if (IsReadOnlyDictionary(resultType) && + resultType is INamedTypeSymbol { TypeArguments.Length: 2, } dictionaryType && + dictionaryType.TypeArguments[0].Equals(keyType, SymbolEqualityComparer.Default)) + { + return true; + } + } + + return false; + } + + private static bool IsReturnTypeLookup(ITypeSymbol returnType, ITypeSymbol keyType) + { + if (returnType is INamedTypeSymbol { TypeArguments.Length: 1, } namedType) + { + var resultType = namedType.TypeArguments[0]; + + if (ToTypeNameNoGenerics(resultType).Equals(Lookup, Ordinal) && + resultType is INamedTypeSymbol { TypeArguments.Length: 2, } dictionaryType && + dictionaryType.TypeArguments[0].Equals(keyType, SymbolEqualityComparer.Default)) + { + return true; + } + } + return false; + } + + private static bool IsReadOnlyDictionary(ITypeSymbol type) + { + if (!ToTypeNameNoGenerics(type).Equals(ReadOnlyDictionary, Ordinal)) + { + foreach (var interfaceSymbol in type.Interfaces) + { + if (ToTypeNameNoGenerics(interfaceSymbol).Equals(ReadOnlyDictionary, Ordinal)) + { + return true; + } + } + + return false; + } + + return true; + } + + private static ITypeSymbol ExtractValueType(ITypeSymbol returnType, DataLoaderKind kind) + { + if (returnType is INamedTypeSymbol { TypeArguments.Length: 1, } namedType) + { + if (kind is DataLoaderKind.Batch or DataLoaderKind.Group && + namedType.TypeArguments[0] is INamedTypeSymbol { TypeArguments.Length: 2, } dict) + { + return dict.TypeArguments[1]; + } + + if (kind is DataLoaderKind.Cache) + { + return namedType.TypeArguments[0]; + } + } + + throw new InvalidOperationException(); + } + + private static string ToTypeNameNoGenerics(ITypeSymbol typeSymbol) + => $"{typeSymbol.ContainingNamespace}.{typeSymbol.Name}"; +} \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs index 23ebe68efeb..33d8b89a118 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs @@ -12,7 +12,9 @@ public static class WellKnownAttributes public const string MutationTypeAttribute = "HotChocolate.Types.MutationTypeAttribute"; public const string SubscriptionTypeAttribute = "HotChocolate.Types.SubscriptionTypeAttribute"; public const string DataLoaderAttribute = "HotChocolate.DataLoaderAttribute"; - public const string QueryAttribute = "HotChocolate.QueryAttribute"; + public const string QueryAttribute = "HotChocolate.QueryFieldAttribute"; + public const string MutationAttribute = "HotChocolate.MutationFieldAttribute"; + public const string SubscriptionAttribute = "HotChocolate.SubscriptionFieldAttribute"; public static HashSet TypeAttributes { get; } = [ diff --git a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownFileNames.cs b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownFileNames.cs index d782696a6aa..38fd2bb9d12 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownFileNames.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownFileNames.cs @@ -5,4 +5,5 @@ public static class WellKnownFileNames public const string TypeModuleFile = "HotChocolateTypeModule.g.cs"; public const string DataLoaderFile = "HotChocolateDataLoader.g.cs"; public const string AttributesFile = "HotChocolateAttributes.g.cs"; + public const string RootTypesFile = "HotChocolateRootTypes.g.cs"; } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/AnnotationBasedSchemaTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/AnnotationBasedSchemaTests.cs index 0bd477599c2..eb26a042d10 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/AnnotationBasedSchemaTests.cs +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/AnnotationBasedSchemaTests.cs @@ -18,4 +18,16 @@ public async Task SchemaSnapshot() schema.MatchSnapshot(); } + + [Fact] + public async Task ExecuteRootField() + { + var result = + await new ServiceCollection() + .AddGraphQL() + .AddCustomModule() + .ExecuteRequestAsync("{ foo }"); + + result.MatchSnapshot(); + } } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Log.txt b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Log.txt deleted file mode 100644 index f36e1340770..00000000000 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Log.txt +++ /dev/null @@ -1,6 +0,0 @@ -scoped: global::HotChocolate.Types.GenericService> -non scoped: global::HotChocolate.Types.SomeService -scoped: global::HotChocolate.Types.SomeService -scoped: global::HotChocolate.Types.SomeService -scoped: global::HotChocolate.Types.SomeService -scoped: global::HotChocolate.Types.SomeService diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/RootTypeTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/RootTypeTests.cs new file mode 100644 index 00000000000..cdb0d452d00 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/RootTypeTests.cs @@ -0,0 +1,10 @@ +namespace HotChocolate.Types; + +public static class RootTypeTests +{ + [QueryField] + public static string Foo() => "foo"; + + [MutationField] + public static string Bar() => "bar"; +} \ No newline at end of file diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/ss.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/ss.cs deleted file mode 100644 index e69de29bb2d..00000000000