diff --git a/src/HotChocolate/Core/src/Abstractions/DataLoaderAttribute.cs b/src/HotChocolate/Core/src/Abstractions/DataLoaderAttribute.cs index 5a304277ded..502c11c3107 100644 --- a/src/HotChocolate/Core/src/Abstractions/DataLoaderAttribute.cs +++ b/src/HotChocolate/Core/src/Abstractions/DataLoaderAttribute.cs @@ -7,17 +7,12 @@ namespace HotChocolate; /// types source generator to generate necessary code around this method. /// [AttributeUsage(AttributeTargets.Method)] -public sealed class DataLoaderAttribute : Attribute +public sealed class DataLoaderAttribute(string? name = null) : Attribute { - public DataLoaderAttribute(string? name = null) - { - Name = name; - } - /// /// Gets the name override for the DataLoader or null. /// - public string? Name { get; } + public string? Name { get; } = name; /// /// Specifies how services by default are handled. 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 a46e7b03b96..00000000000 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderGenerator.cs +++ /dev/null @@ -1,650 +0,0 @@ -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, - sourceText); - } - - // 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, - StringBuilder sourceText) - { - var isScoped = dataLoader.IsScoped ?? defaults.Scoped ?? false; - var isPublic = dataLoader.IsPublic ?? defaults.IsPublic ?? true; - var isInterfacePublic = dataLoader.IsInterfacePublic ?? defaults.IsInterfacePublic ?? true; - - sourceText.AppendLine(); - sourceText.Append("namespace "); - sourceText.AppendLine(dataLoader.Namespace); - sourceText.AppendLine("{"); - - // first we generate a DataLoader interface ... - var interfaceName = dataLoader.InterfaceName; - - if (isInterfacePublic) - { - sourceText.Append(" public interface "); - } - else - { - sourceText.Append(" internal interface "); - } - - sourceText.Append(interfaceName); - - if (kind is DataLoaderKind.Batch or DataLoaderKind.Cache) - { - sourceText.Append(" : global::GreenDonut.IDataLoader<"); - sourceText.Append(keyType.ToFullyQualified()); - sourceText.Append(", "); - sourceText.Append(valueType.ToFullyQualified()); - sourceText.Append(">"); - } - else if (kind is DataLoaderKind.Group) - { - sourceText.Append(" : global::GreenDonut.IDataLoader<"); - sourceText.Append(keyType.ToFullyQualified()); - sourceText.Append(", "); - sourceText.Append(valueType.ToFullyQualified()); - sourceText.Append("[]>"); - } - - sourceText.AppendLine(" { }"); - sourceText.AppendLine(); - - // ... then the actual DataLoader implementation. - if (isPublic) - { - sourceText.Append(" public sealed class "); - } - else - { - sourceText.Append(" internal sealed class "); - } - - sourceText.Append(dataLoader.Name); - - if (kind is DataLoaderKind.Batch) - { - sourceText.Append(" : global::GreenDonut.BatchDataLoader<"); - sourceText.Append(keyType.ToFullyQualified()); - sourceText.Append(", "); - sourceText.Append(valueType.ToFullyQualified()); - sourceText.Append(">"); - } - else if (kind is DataLoaderKind.Group) - { - sourceText.Append(" : global::GreenDonut.GroupedDataLoader<"); - sourceText.Append(keyType.ToFullyQualified()); - sourceText.Append(", "); - sourceText.Append(valueType.ToFullyQualified()); - sourceText.Append(">"); - } - else if (kind is DataLoaderKind.Cache) - { - sourceText.Append(" : global::GreenDonut.CacheDataLoader<"); - sourceText.Append(keyType.ToFullyQualified()); - sourceText.Append(", "); - sourceText.Append(valueType.ToFullyQualified()); - sourceText.Append(">"); - } - - sourceText.Append(", "); - sourceText.AppendLine(interfaceName); - sourceText.AppendLine(" {"); - - sourceText.AppendLine( - " private readonly global::System.IServiceProvider _services;"); - sourceText.AppendLine(); - - if (kind is DataLoaderKind.Batch or DataLoaderKind.Group) - { - sourceText - .Append(Indent) - .Append(Indent) - .Append("public ") - .Append(dataLoader.Name) - .AppendLine("("); - sourceText - .Append(Indent) - .Append(Indent) - .Append(Indent) - .AppendLine("global::System.IServiceProvider services,"); - sourceText - .Append(Indent) - .Append(Indent) - .Append(Indent) - .AppendLine("global::GreenDonut.IBatchScheduler batchScheduler,"); - sourceText - .Append(Indent) - .Append(Indent) - .Append(Indent) - .AppendLine("global::GreenDonut.DataLoaderOptions? options = null)"); - - sourceText - .Append(Indent) - .Append(Indent) - .Append(Indent) - .AppendLine(": base(batchScheduler, options)"); - } - else - { - sourceText - .Append(Indent) - .Append(Indent) - .Append("public ") - .Append(dataLoader.Name) - .AppendLine("("); - - sourceText - .Append(Indent) - .Append(Indent) - .Append(Indent) - .AppendLine("global::System.IServiceProvider services,"); - - sourceText - .Append(Indent) - .Append(Indent) - .Append(Indent) - .AppendLine("global::GreenDonut.DataLoaderOptions? options = null)"); - - sourceText - .Append(Indent) - .Append(Indent) - .Append(Indent) - .AppendLine(": base(options)"); - } - - sourceText.AppendLine(" {"); - sourceText.AppendLine(" _services = services ??"); - sourceText.Append(" throw new global::") - .AppendLine("System.ArgumentNullException(nameof(services));"); - sourceText.AppendLine(" }"); - sourceText.AppendLine(); - - if (kind is DataLoaderKind.Batch) - { - sourceText.Append($" protected override async global::{WellKnownTypes.Task}<"); - sourceText.Append($"{ReadOnlyDictionary}<"); - sourceText.Append(keyType.ToFullyQualified()); - sourceText.Append(", "); - sourceText.Append(valueType.ToFullyQualified()); - sourceText.Append(">> "); - sourceText.AppendLine("LoadBatchAsync("); - sourceText.Append($" {ReadOnlyList}<"); - sourceText.Append(keyType.ToFullyQualified()).AppendLine("> keys,"); - sourceText.AppendLine($" global::{WellKnownTypes.CancellationToken} ct)"); - } - else if (kind is DataLoaderKind.Group) - { - sourceText.Append($" protected override async global::{WellKnownTypes.Task}<"); - sourceText.Append($"{Lookup}<"); - sourceText.Append(keyType.ToFullyQualified()); - sourceText.Append(", "); - sourceText.Append(valueType.ToFullyQualified()); - sourceText.Append(">> "); - sourceText.AppendLine("LoadGroupedBatchAsync("); - sourceText.Append($" {ReadOnlyList}<"); - sourceText.Append(keyType.ToFullyQualified()).AppendLine("> keys,"); - sourceText.AppendLine($" global::{WellKnownTypes.CancellationToken} ct)"); - } - else if (kind is DataLoaderKind.Cache) - { - sourceText.Append($" protected override async global::{WellKnownTypes.Task}<"); - sourceText.Append(valueType.ToFullyQualified()); - sourceText.Append("> "); - sourceText.AppendLine("LoadSingleAsync("); - sourceText - .Append(" ") - .Append(keyType.ToFullyQualified()) - .AppendLine(" key,"); - sourceText.AppendLine($" {WellKnownTypes.CancellationToken} ct)"); - } - - sourceText.AppendLine(" {"); - - if (isScoped) - { - sourceText - .Append(Indent) - .Append(Indent) - .Append(Indent) - .AppendLine("await using var scope = _services.CreateAsyncScope();"); - - foreach (var item in services.OrderBy(t => t.Key)) - { - sourceText.Append(" var p").Append(item.Key).Append(" = "); - sourceText.Append("scope.ServiceProvider.GetRequiredService<"); - sourceText.Append(item.Value); - sourceText.AppendLine(">();"); - } - } - else - { - foreach (var item in services.OrderBy(t => t.Key)) - { - sourceText - .Append(" var p") - .Append(item.Key) - .Append(" = _services.GetRequiredService<"); - sourceText.Append(item.Value); - sourceText.AppendLine(">();"); - } - } - - sourceText.Append(" return await "); - sourceText.Append(dataLoader.ContainingType); - sourceText.Append("."); - sourceText.Append(dataLoader.MethodName); - sourceText.Append("("); - - for (var i = 0; i < parameterCount; i++) - { - if (i > 0) - { - sourceText.Append(", "); - } - - if (i == 0) - { - if (kind is DataLoaderKind.Batch or DataLoaderKind.Group) - { - sourceText.Append("keys"); - } - else - { - sourceText.Append("key"); - } - } - else if (i == cancelIndex) - { - sourceText.Append("ct"); - } - else - { - sourceText.Append("p"); - sourceText.Append(i); - } - } - sourceText.AppendLine(").ConfigureAwait(false);"); - - sourceText.AppendLine(" }"); - sourceText.AppendLine(" }"); - sourceText.AppendLine("}"); - } - - 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 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 ReadOnlySpan 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"; -} \ 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/Generators/ModuleGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleGenerator.cs deleted file mode 100644 index facf45e114d..00000000000 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleGenerator.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System.Text; -using HotChocolate.Types.Analyzers.Helpers; -using HotChocolate.Types.Analyzers.Inspectors; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using static HotChocolate.Types.Analyzers.StringConstants; -using static HotChocolate.Types.Analyzers.WellKnownFileNames; -using TypeInfo = HotChocolate.Types.Analyzers.Inspectors.TypeInfo; - -namespace HotChocolate.Types.Analyzers.Generators; - -public class ModuleGenerator : ISyntaxGenerator -{ - public void Initialize(IncrementalGeneratorPostInitializationContext context) - { - } - - public bool Consume(ISyntaxInfo syntaxInfo) - => syntaxInfo is TypeInfo or TypeExtensionInfo or - RegisterDataLoaderInfo or ModuleInfo or DataLoaderInfo; - - 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) - .Append(Indent) - .Append("public static IRequestExecutorBuilder Add") - .Append(module.ModuleName) - .AppendLine("(this IRequestExecutorBuilder builder)"); - - sourceText.Append(Indent).Append(Indent).AppendLine("{"); - - sourceText - .Append(Indent) - .Append(Indent) - .Append(Indent) - .AppendLine("RegisterGeneratedDataLoader(builder);"); - sourceText.AppendLine(); - - var operations = OperationType.No; - - foreach (var syntaxInfo in syntaxInfos) - { - switch (syntaxInfo) - { - case TypeInfo type: - if ((module.Options & ModuleOptions.RegisterTypes) == - ModuleOptions.RegisterTypes) - { - sourceText.Append(Indent) - .Append(Indent) - .Append(Indent) - .Append("builder.AddType();"); - } - break; - - case TypeExtensionInfo extension: - if ((module.Options & ModuleOptions.RegisterTypes) == - ModuleOptions.RegisterTypes) - { - if (extension.IsStatic) - { - sourceText.Append(Indent) - .Append(Indent) - .Append(Indent) - .Append("builder.AddTypeExtension(typeof(global::") - .Append(extension.Name) - .AppendLine("));"); - } - else - { - sourceText.Append(Indent) - .Append(Indent) - .Append(Indent) - .Append("builder.AddTypeExtension();"); - } - - if (extension.Type is not OperationType.No && - (operations & extension.Type) != extension.Type) - { - operations |= extension.Type; - } - } - break; - - case RegisterDataLoaderInfo dataLoader: - if ((module.Options & ModuleOptions.RegisterDataLoader) == - ModuleOptions.RegisterDataLoader) - { - sourceText.Append(Indent) - .Append(Indent) - .Append(Indent) - .Append("builder.AddDataLoader();"); - } - break; - } - } - - if ((operations & OperationType.Query) == OperationType.Query) - { - WriteTryAddOperationType(sourceText, OperationType.Query); - } - - if ((operations & OperationType.Mutation) == OperationType.Mutation) - { - WriteTryAddOperationType(sourceText, OperationType.Mutation); - } - - if ((operations & OperationType.Subscription) == OperationType.Subscription) - { - WriteTryAddOperationType(sourceText, OperationType.Subscription); - } - - sourceText.Append(Indent).Append(Indent).Append(Indent).AppendLine("return builder;"); - sourceText.Append(Indent).Append(Indent).AppendLine("}"); - - sourceText.AppendLine(); - - sourceText - .Append(Indent) - .Append(Indent) - .Append("static partial void RegisterGeneratedDataLoader(") - .AppendLine("IRequestExecutorBuilder builder);"); - - sourceText.Append(Indent).AppendLine("}"); - - sourceText.AppendLine("}"); - - context.AddSource(TypeModuleFile, SourceText.From(sourceText.ToString(), Encoding.UTF8)); - StringBuilderPool.Return(sourceText); - } - - private static void WriteTryAddOperationType(StringBuilder sourceText, OperationType type) - => sourceText.Append(Indent) - .Append(Indent) - .Append(Indent) - .AppendLine("builder.ConfigureSchema(") - .Append(Indent) - .Append(Indent) - .Append(Indent) - .Append(Indent) - .AppendLine("b => b.TryAddRootType(") - .Append(Indent) - .Append(Indent) - .Append(Indent) - .Append(Indent) - .Append(Indent) - .AppendLine("() => new global::HotChocolate.Types.ObjectType(") - .Append(Indent) - .Append(Indent) - .Append(Indent) - .Append(Indent) - .Append(Indent) - .Append(Indent) - .Append("d => d.Name(global::HotChocolate.Types.OperationTypeNames.") - .Append(type) - .AppendLine(")),") - .Append(Indent) - .Append(Indent) - .Append(Indent) - .Append(Indent) - .Append(Indent) - .Append("HotChocolate.Language.OperationType.") - .Append(type) - .AppendLine("));"); -} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleSyntaxGenerator.cs new file mode 100644 index 00000000000..62a8ed795f2 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleSyntaxGenerator.cs @@ -0,0 +1,124 @@ +using System.Text; +using HotChocolate.Types.Analyzers.Helpers; +using Microsoft.CodeAnalysis.Text; + +namespace HotChocolate.Types.Analyzers.Generators; + +public sealed class ModuleSyntaxGenerator : IDisposable +{ + private readonly string _moduleName; + private readonly string _ns; + private StringBuilder _sb; + private CodeWriter _writer; + private bool _disposed; + + public ModuleSyntaxGenerator(string moduleName, string ns) + { + _moduleName = moduleName; + _ns = ns; + _sb = StringBuilderPool.Get(); + _writer = new CodeWriter(_sb); + } + + public void WriterHeader() + => _writer.WriteFileHeader(); + + public void WriteBeginNamespace() + { + _writer.WriteIndentedLine("namespace {0}", _ns); + _writer.WriteIndentedLine("{"); + _writer.IncreaseIndent(); + } + + public void WriteEndNamespace() + { + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("}"); + } + + public void WriteBeginClass() + { + _writer.WriteIndentedLine("public static partial class {0}RequestExecutorBuilderExtensions", _moduleName); + _writer.WriteIndentedLine("{"); + _writer.IncreaseIndent(); + } + + public void WriteEndClass() + { + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("}"); + } + + public void WriteBeginRegistrationMethod() + { + _writer.WriteIndentedLine( + "public static IRequestExecutorBuilder Add{0}(this IRequestExecutorBuilder builder)", + _moduleName); + _writer.WriteIndentedLine("{"); + _writer.IncreaseIndent(); + } + + public void WriteEndRegistrationMethod() + { + _writer.WriteIndentedLine("return builder;"); + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("}"); + } + + public void WriteRegisterType(string typeName) + => _writer.WriteIndentedLine("builder.AddType();", typeName); + + public void WriteRegisterTypeExtension(string typeName, bool staticType) + => _writer.WriteIndentedLine( + staticType + ? "builder.AddTypeExtension(typeof(global::{0}));" + : "builder.AddTypeExtension();", + typeName); + + 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) + { + _writer.WriteIndentedLine("builder.ConfigureSchema("); + + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine("b => b.TryAddRootType("); + + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine("() => new global::HotChocolate.Types.ObjectType("); + + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine("d => d.Name(global::HotChocolate.Types.OperationTypeNames.{0})),", type); + } + + _writer.WriteIndentedLine("HotChocolate.Language.OperationType.{0}));", type); + } + } + } + + 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/OperationFieldSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/OperationFieldSyntaxGenerator.cs new file mode 100644 index 00000000000..dc7f9c57a74 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/OperationFieldSyntaxGenerator.cs @@ -0,0 +1,133 @@ +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 _first = true; + 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) + { + if (!_first) + { + _writer.WriteLine(); + } + _first = false; + + _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(OperationType type, IEnumerable operations) + { + _writer.WriteIndentedLine("protected override void Configure("); + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine("global::HotChocolate.Types.IObjectTypeDescriptor descriptor)"); + } + _writer.WriteIndentedLine("{"); + _writer.IncreaseIndent(); + + _writer.WriteIndentedLine("var bindingFlags = System.Reflection.BindingFlags.Public |"); + + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine("System.Reflection.BindingFlags.NonPublic |"); + _writer.WriteIndentedLine("System.Reflection.BindingFlags.Static;"); + } + + _writer.WriteIndentedLine("descriptor.Name({0});", GetOperationConstant(type)); + + var typeIndex = 0; + foreach (var group in operations.GroupBy(t => t.TypeName)) + { + _writer.WriteLine(); + + var typeName = $"type{++typeIndex}"; + _writer.WriteIndentedLine("var {0} = typeof({1});", typeName, group.Key); + + foreach (var operation in group) + { + _writer.WriteIndentedLine( + "descriptor.Field({0}.GetMember(\"{1}\", bindingFlags)[0]);", + typeName, + operation.MethodName); + } + } + + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("}"); + } + + private static string GetOperationConstant(OperationType type) + => type switch + { + OperationType.Query => "global::HotChocolate.Types.OperationTypeNames.Query", + OperationType.Mutation => "global::HotChocolate.Types.OperationTypeNames.Mutation", + OperationType.Subscription => "global::HotChocolate.Types.OperationTypeNames.Subscription", + _ => throw new InvalidOperationException() + }; + + 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/Helpers/CodeWriter.cs b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CodeWriter.cs new file mode 100644 index 00000000000..3bef6695746 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CodeWriter.cs @@ -0,0 +1,156 @@ +using System.Text; + +namespace HotChocolate.Types.Analyzers.Helpers; + +public class CodeWriter : TextWriter +{ + private readonly TextWriter _writer; + private readonly bool _disposeWriter; + private bool _disposed; + private int _indent; + + public CodeWriter(TextWriter writer) + { + _writer = writer; + _disposeWriter = false; + } + + public CodeWriter(StringBuilder text) + { + _writer = new StringWriter(text); + _disposeWriter = true; + } + + public override Encoding Encoding { get; } = Encoding.UTF8; + + public static string Indent { get; } = new(' ', 4); + + public override void Write(char value) => + _writer.Write(value); + + public void WriteStringValue(string value) + { + Write('"'); + Write(value); + Write('"'); + } + + public void WriteIndent() + { + if (_indent > 0) + { + var spaces = _indent * 4; + for (var i = 0; i < spaces; i++) + { + Write(' '); + } + } + } + + public string GetIndentString() + { + if (_indent > 0) + { + return new string(' ', _indent * 4); + } + return string.Empty; + } + + public void WriteIndentedLine(string format, params object?[] args) + { + WriteIndent(); + + if (args.Length == 0) + { + Write(format); + } + else + { + Write(format, args); + } + + WriteLine(); + } + + public void WriteIndented(string format, params object?[] args) + { + WriteIndent(); + + if (args.Length == 0) + { + Write(format); + } + else + { + Write(format, args); + } + } + + public void WriteSpace() => Write(' '); + + public IDisposable IncreaseIndent() + { + _indent++; + return new Block(DecreaseIndent); + } + + public void DecreaseIndent() + { + if (_indent > 0) + { + _indent--; + } + } + + public IDisposable WriteBraces() + { + WriteLeftBrace(); + WriteLine(); + +#pragma warning disable CA2000 + var indent = IncreaseIndent(); +#pragma warning restore CA2000 + + return new Block(() => + { + WriteLine(); + indent.Dispose(); + WriteIndent(); + WriteRightBrace(); + }); + } + + public void WriteLeftBrace() => Write('{'); + + public void WriteRightBrace() => Write('}'); + + public override void Flush() + { + base.Flush(); + _writer.Flush(); + } + + protected override void Dispose(bool disposing) + { + if (!_disposed && _disposeWriter) + { + if (disposing) + { + _writer.Dispose(); + } + _disposed = true; + } + } + + private sealed class Block : IDisposable + { + private readonly Action _decrease; + + public Block(Action close) + { + _decrease = close; + } + + public void Dispose() => _decrease(); + } +} \ 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 new file mode 100644 index 00000000000..43ddf5be758 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CodeWriterExtensions.cs @@ -0,0 +1,45 @@ +namespace HotChocolate.Types.Analyzers.Helpers; + +public static class CodeWriterExtensions +{ + public static void WriteGeneratedAttribute(this CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + +#if DEBUG + writer.WriteIndentedLine( + "[global::System.CodeDom.Compiler.GeneratedCode(" + + "\"HotChocolate\", \"11.0.0\")]"); +#else + var version = typeof(CodeWriter).Assembly.GetName().Version!.ToString(); + writer.WriteIndentedLine( + "[global::System.CodeDom.Compiler.GeneratedCode(" + + $"\"HotChocolate\", \"{version}\")]"); +#endif + } + + public static void WriteFileHeader(this CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteIndentedLine("// "); + writer.WriteLine(); + writer.WriteIndentedLine("#nullable enable"); + writer.WriteLine(); + writer.WriteIndentedLine("using System;"); + writer.WriteIndentedLine("using HotChocolate.Execution.Configuration;"); + } + + public static CodeWriter WriteComment(this CodeWriter writer, string comment) + { + writer.Write("// "); + writer.WriteLine(comment); + return writer; + } +} \ No newline at end of file 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/DataLoaderInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderInspector.cs index 40fcead47a4..4e99fd5733d 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderInspector.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderInspector.cs @@ -45,4 +45,4 @@ public bool TryHandle( syntaxInfo = null; return false; } -} +} \ 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 new file mode 100644 index 00000000000..90ffeff82fd --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInspector.cs @@ -0,0 +1,78 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace HotChocolate.Types.Analyzers.Inspectors; + +public sealed class OperationInspector : ISyntaxInspector +{ + public bool TryHandle( + GeneratorSyntaxContext context, + [NotNullWhen(true)] out ISyntaxInfo? syntaxInfo) + { + if (context.Node is MethodDeclarationSyntax { AttributeLists.Count: > 0, } methodSyntax) + { + foreach (var attributeListSyntax in methodSyntax.AttributeLists) + { + foreach (var attributeSyntax in attributeListSyntax.Attributes) + { + var symbol = context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol; + + if (symbol is not IMethodSymbol attributeSymbol) + { + continue; + } + + var attributeContainingTypeSymbol = attributeSymbol.ContainingType; + var fullName = attributeContainingTypeSymbol.ToDisplayString(); + var operationType = ParseOperationType(fullName); + + if(operationType == OperationType.No) + { + continue; + } + + if (context.SemanticModel.GetDeclaredSymbol(methodSyntax) is not { } methodSymbol) + { + continue; + } + + if (!methodSymbol.IsStatic) + { + continue; + } + + syntaxInfo = new OperationInfo( + operationType, + methodSymbol.ContainingType.ToDisplayString(), + methodSymbol.Name); + return true; + } + } + } + + 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..2f7671c4c76 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationRegistrationInfo.cs @@ -0,0 +1,52 @@ +namespace HotChocolate.Types.Analyzers.Inspectors; + +public sealed class OperationRegistrationInfo(OperationType type, string typeName) : ISyntaxInfo +{ + public OperationType Type { get; } = type; + + 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 d0e92549d73..821bafedb7e 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/TypeModuleGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/TypeModuleGenerator.cs @@ -1,9 +1,13 @@ -using System.Buffers; -using System.Collections.Immutable; +using System.Collections.Immutable; using HotChocolate.Types.Analyzers.Generators; +using HotChocolate.Types.Analyzers.Helpers; 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; @@ -17,18 +21,11 @@ public class TypeModuleGenerator : IIncrementalGenerator new ModuleInspector(), new DataLoaderInspector(), new DataLoaderDefaultsInspector(), - ]; - - private static readonly ISyntaxGenerator[] _generators = - [ - new ModuleGenerator(), - new DataLoaderGenerator(), + new OperationInspector(), ]; public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterPostInitializationOutput(c => PostInitialization(c)); - var modulesAndTypes = context.SyntaxProvider .CreateSyntaxProvider( @@ -44,14 +41,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) static (context, source) => Execute(context, source.Left, source.Right)); } - private static void PostInitialization(IncrementalGeneratorPostInitializationContext context) - { - foreach (var syntaxGenerator in _generators) - { - syntaxGenerator.Initialize(context); - } - } - private static bool IsRelevant(SyntaxNode node) => IsTypeWithAttribute(node) || IsClassWithBaseClass(node) || @@ -95,37 +84,414 @@ private static void Execute( return; } - var buffer = ArrayPool.Shared.Rent(syntaxInfos.Length * 2); + 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) + { + return; + } + + var syntaxInfoList = syntaxInfos.ToList(); + WriteOperationTypes(context, syntaxInfoList, module); + WriteDataLoader(context, syntaxInfoList, dataLoaderDefaults); + WriteConfiguration(context, syntaxInfoList, module); + } + + private static void WriteConfiguration( + SourceProductionContext context, + 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) + { + switch (syntaxInfo) + { + case TypeInfo type: + if ((module.Options & ModuleOptions.RegisterTypes) == + ModuleOptions.RegisterTypes) + { + generator.WriteRegisterType(type.Name); + } + break; + + case TypeExtensionInfo extension: + if ((module.Options & ModuleOptions.RegisterTypes) == + ModuleOptions.RegisterTypes) + { + generator.WriteRegisterTypeExtension(extension.Name, extension.IsStatic); + + if (extension.Type is not OperationType.No && + (operations & extension.Type) != extension.Type) + { + operations |= extension.Type; + } + } + break; + + case RegisterDataLoaderInfo dataLoader: + if ((module.Options & ModuleOptions.RegisterDataLoader) == + ModuleOptions.RegisterDataLoader) + { + 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); + + if (operation.Type is not OperationType.No && + (operations & operation.Type) != operation.Type) + { + operations |= operation.Type; + } + } + break; + } + } - // prepare context - for (var i = syntaxInfos.Length - 1; i >= 0; i--) + if ((operations & OperationType.Query) == OperationType.Query) { - buffer[i] = syntaxInfos[i]; + generator.WriteTryAddOperationType(OperationType.Query); } - var nodes = buffer.AsSpan().Slice(0, syntaxInfos.Length); - var batch = buffer.AsSpan().Slice(syntaxInfos.Length, syntaxInfos.Length); + if ((operations & OperationType.Mutation) == OperationType.Mutation) + { + generator.WriteTryAddOperationType(OperationType.Mutation); + } - foreach (var generator in _generators) + if ((operations & OperationType.Subscription) == OperationType.Subscription) { - var next = 0; + generator.WriteTryAddOperationType(OperationType.Subscription); + } + + generator.WriteEndRegistrationMethod(); + generator.WriteEndClass(); + generator.WriteEndNamespace(); - // gather infos for current generator - foreach (var node in nodes) + 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) { - if (generator.Consume(node)) + var keyArg = dataLoader.MethodSymbol.Parameters[0]; + var keyType = keyArg.Type; + var cancellationTokenIndex = -1; + var serviceMap = new Dictionary(); + + if (IsKeysArgument(keyType)) { - batch[next++] = node; + 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(); - // generate - if (next > 0) + foreach (var syntaxInfo in syntaxInfos) + { + if (syntaxInfo is OperationInfo operation) { - generator.Generate(context, compilation, batch.Slice(0, next)); + operations.Add(operation); } } - ArrayPool.Shared.Return(buffer); + 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.Key, group); + generator.WriteEndClass(); + + syntaxInfos.Add(new OperationRegistrationInfo( + group.Key, + $"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 bf72578fe9c..33d8b89a118 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs @@ -12,6 +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.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/__snapshots__/SchemaTests.ExecuteRootField.snap b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.ExecuteRootField.snap new file mode 100644 index 00000000000..9ba5fdcb2d5 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.ExecuteRootField.snap @@ -0,0 +1,5 @@ +{ + "data": { + "foo": "foo" + } +} \ No newline at end of file diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.SchemaSnapshot.graphql b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.SchemaSnapshot.graphql index 7de6b9fc193..ee37cdb2295 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.SchemaSnapshot.graphql +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.SchemaSnapshot.graphql @@ -9,6 +9,7 @@ interface Entity { } type Mutation { + bar: String! doSomething: String! } @@ -19,6 +20,7 @@ type Person implements Entity { } type Query { + foo: String! person: Entity enum: CustomEnum book: SomeBook! 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