diff --git a/JsonApiDotNetCore.sln.DotSettings b/JsonApiDotNetCore.sln.DotSettings index 2a7eb28d9b..95c0c2b7b2 100644 --- a/JsonApiDotNetCore.sln.DotSettings +++ b/JsonApiDotNetCore.sln.DotSettings @@ -54,6 +54,7 @@ JsonApiDotNetCore.ArgumentGuard.NotNull($EXPR$, $NAME$); WARNING WARNING WARNING + WARNING DO_NOT_SHOW HINT SUGGESTION diff --git a/src/JsonApiDotNetCore.SourceGenerators/ControllerSourceGenerator.cs b/src/JsonApiDotNetCore.SourceGenerators/ControllerSourceGenerator.cs index 65800bba82..89a511b08e 100644 --- a/src/JsonApiDotNetCore.SourceGenerators/ControllerSourceGenerator.cs +++ b/src/JsonApiDotNetCore.SourceGenerators/ControllerSourceGenerator.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using System.Text; using Humanizer; using Microsoft.CodeAnalysis; @@ -11,166 +8,163 @@ #pragma warning disable RS2008 // Enable analyzer release tracking -namespace JsonApiDotNetCore.SourceGenerators +namespace JsonApiDotNetCore.SourceGenerators; +// To debug in Visual Studio (requires v17.2 or higher): +// - Set JsonApiDotNetCore.SourceGenerators as startup project +// - Add a breakpoint at the start of the Initialize or Execute method +// - Optional: change targetProject in Properties\launchSettings.json +// - Press F5 + +[Generator(LanguageNames.CSharp)] +public sealed class ControllerSourceGenerator : ISourceGenerator { - // To debug in Visual Studio (requires v17.2 or higher): - // - Set JsonApiDotNetCore.SourceGenerators as startup project - // - Add a breakpoint at the start of the Initialize or Execute method - // - Optional: change targetProject in Properties\launchSettings.json - // - Press F5 - - [Generator(LanguageNames.CSharp)] - public sealed class ControllerSourceGenerator : ISourceGenerator + private const string Category = "JsonApiDotNetCore"; + + private static readonly DiagnosticDescriptor MissingInterfaceWarning = new("JADNC001", "Resource type does not implement IIdentifiable", + "Type '{0}' must implement IIdentifiable when using ResourceAttribute to auto-generate ASP.NET controllers", Category, DiagnosticSeverity.Warning, + true); + + private static readonly DiagnosticDescriptor MissingIndentInTableError = new("JADNC900", "Internal error: Insufficient entries in IndentTable", + "Internal error: Missing entry in IndentTable for depth {0}", Category, DiagnosticSeverity.Warning, true); + + // PERF: Heap-allocate the delegate only once, instead of per compilation. + private static readonly SyntaxReceiverCreator CreateSyntaxReceiver = static () => new TypeWithAttributeSyntaxReceiver(); + + public void Initialize(GeneratorInitializationContext context) { - private const string Category = "JsonApiDotNetCore"; + context.RegisterForSyntaxNotifications(CreateSyntaxReceiver); + } - private static readonly DiagnosticDescriptor MissingInterfaceWarning = new DiagnosticDescriptor("JADNC001", - "Resource type does not implement IIdentifiable", - "Type '{0}' must implement IIdentifiable when using ResourceAttribute to auto-generate ASP.NET controllers", Category, - DiagnosticSeverity.Warning, true); + public void Execute(GeneratorExecutionContext context) + { + var receiver = (TypeWithAttributeSyntaxReceiver?)context.SyntaxReceiver; - private static readonly DiagnosticDescriptor MissingIndentInTableError = new DiagnosticDescriptor("JADNC900", - "Internal error: Insufficient entries in IndentTable", "Internal error: Missing entry in IndentTable for depth {0}", Category, - DiagnosticSeverity.Warning, true); + if (receiver == null) + { + return; + } - // PERF: Heap-allocate the delegate only once, instead of per compilation. - private static readonly SyntaxReceiverCreator CreateSyntaxReceiver = () => new TypeWithAttributeSyntaxReceiver(); + INamedTypeSymbol? resourceAttributeType = context.Compilation.GetTypeByMetadataName("JsonApiDotNetCore.Resources.Annotations.ResourceAttribute"); + INamedTypeSymbol? identifiableOpenInterface = context.Compilation.GetTypeByMetadataName("JsonApiDotNetCore.Resources.IIdentifiable`1"); + INamedTypeSymbol? loggerFactoryInterface = context.Compilation.GetTypeByMetadataName("Microsoft.Extensions.Logging.ILoggerFactory"); - public void Initialize(GeneratorInitializationContext context) + if (resourceAttributeType == null || identifiableOpenInterface == null || loggerFactoryInterface == null) { - context.RegisterForSyntaxNotifications(CreateSyntaxReceiver); + return; } - public void Execute(GeneratorExecutionContext context) + var controllerNamesInUse = new Dictionary(StringComparer.OrdinalIgnoreCase); + var writer = new SourceCodeWriter(context, MissingIndentInTableError); + + foreach (TypeDeclarationSyntax? typeDeclarationSyntax in receiver.TypeDeclarations) { - var receiver = (TypeWithAttributeSyntaxReceiver)context.SyntaxReceiver; + // PERF: Note that our code runs on every keystroke in the IDE, which makes it critical to provide near-realtime performance. + // This means keeping an eye on memory allocations and bailing out early when compilations are cancelled while the user is still typing. + context.CancellationToken.ThrowIfCancellationRequested(); + + SemanticModel semanticModel = context.Compilation.GetSemanticModel(typeDeclarationSyntax.SyntaxTree); + INamedTypeSymbol? resourceType = semanticModel.GetDeclaredSymbol(typeDeclarationSyntax, context.CancellationToken); - if (receiver == null) + if (resourceType == null) { - return; + continue; } - INamedTypeSymbol resourceAttributeType = context.Compilation.GetTypeByMetadataName("JsonApiDotNetCore.Resources.Annotations.ResourceAttribute"); - INamedTypeSymbol identifiableOpenInterface = context.Compilation.GetTypeByMetadataName("JsonApiDotNetCore.Resources.IIdentifiable`1"); - INamedTypeSymbol loggerFactoryInterface = context.Compilation.GetTypeByMetadataName("Microsoft.Extensions.Logging.ILoggerFactory"); + AttributeData? resourceAttributeData = FirstOrDefault(resourceType.GetAttributes(), resourceAttributeType, + static (data, type) => SymbolEqualityComparer.Default.Equals(data.AttributeClass, type)); - if (resourceAttributeType == null || identifiableOpenInterface == null || loggerFactoryInterface == null) + if (resourceAttributeData == null) { - return; + continue; } - var controllerNamesInUse = new Dictionary(StringComparer.OrdinalIgnoreCase); - var writer = new SourceCodeWriter(context, MissingIndentInTableError); + TypedConstant endpointsArgument = + resourceAttributeData.NamedArguments.FirstOrDefault(static pair => pair.Key == "GenerateControllerEndpoints").Value; - foreach (TypeDeclarationSyntax typeDeclarationSyntax in receiver.TypeDeclarations) + if (endpointsArgument.Value != null && (JsonApiEndpointsCopy)endpointsArgument.Value == JsonApiEndpointsCopy.None) { - // PERF: Note that our code runs on every keystroke in the IDE, which makes it critical to provide near-realtime performance. - // This means keeping an eye on memory allocations and bailing out early when compilations are cancelled while the user is still typing. - context.CancellationToken.ThrowIfCancellationRequested(); - - SemanticModel semanticModel = context.Compilation.GetSemanticModel(typeDeclarationSyntax.SyntaxTree); - INamedTypeSymbol resourceType = semanticModel.GetDeclaredSymbol(typeDeclarationSyntax, context.CancellationToken); - - if (resourceType == null) - { - continue; - } - - AttributeData resourceAttributeData = FirstOrDefault(resourceType.GetAttributes(), resourceAttributeType, - (data, type) => SymbolEqualityComparer.Default.Equals(data.AttributeClass, type)); - - if (resourceAttributeData == null) - { - continue; - } - - TypedConstant endpointsArgument = resourceAttributeData.NamedArguments.FirstOrDefault(pair => pair.Key == "GenerateControllerEndpoints").Value; - - if (endpointsArgument.Value != null && (JsonApiEndpointsCopy)endpointsArgument.Value == JsonApiEndpointsCopy.None) - { - continue; - } - - TypedConstant controllerNamespaceArgument = - resourceAttributeData.NamedArguments.FirstOrDefault(pair => pair.Key == "ControllerNamespace").Value; - - string controllerNamespace = GetControllerNamespace(controllerNamespaceArgument, resourceType); - - INamedTypeSymbol identifiableClosedInterface = FirstOrDefault(resourceType.AllInterfaces, identifiableOpenInterface, - (@interface, openInterface) => @interface.IsGenericType && - SymbolEqualityComparer.Default.Equals(@interface.ConstructedFrom, openInterface)); + continue; + } - if (identifiableClosedInterface == null) - { - var diagnostic = Diagnostic.Create(MissingInterfaceWarning, typeDeclarationSyntax.GetLocation(), resourceType.Name); - context.ReportDiagnostic(diagnostic); - continue; - } + TypedConstant controllerNamespaceArgument = + resourceAttributeData.NamedArguments.FirstOrDefault(static pair => pair.Key == "ControllerNamespace").Value; - ITypeSymbol idType = identifiableClosedInterface.TypeArguments[0]; - string controllerName = $"{resourceType.Name.Pluralize()}Controller"; - JsonApiEndpointsCopy endpointsToGenerate = (JsonApiEndpointsCopy?)(int?)endpointsArgument.Value ?? JsonApiEndpointsCopy.All; + string? controllerNamespace = GetControllerNamespace(controllerNamespaceArgument, resourceType); - string sourceCode = writer.Write(resourceType, idType, endpointsToGenerate, controllerNamespace, controllerName, loggerFactoryInterface); - SourceText sourceText = SourceText.From(sourceCode, Encoding.UTF8); + INamedTypeSymbol? identifiableClosedInterface = FirstOrDefault(resourceType.AllInterfaces, identifiableOpenInterface, + static (@interface, openInterface) => + @interface.IsGenericType && SymbolEqualityComparer.Default.Equals(@interface.ConstructedFrom, openInterface)); - string fileName = GetUniqueFileName(controllerName, controllerNamesInUse); - context.AddSource(fileName, sourceText); + if (identifiableClosedInterface == null) + { + var diagnostic = Diagnostic.Create(MissingInterfaceWarning, typeDeclarationSyntax.GetLocation(), resourceType.Name); + context.ReportDiagnostic(diagnostic); + continue; } - } - private static TElement FirstOrDefault(ImmutableArray source, TContext context, Func predicate) - { - // PERF: Using this method enables to avoid allocating a closure in the passed lambda expression. - // See https://www.jetbrains.com/help/resharper/2021.2/Fixing_Issues_Found_by_DPA.html#closures-in-lambda-expressions. + ITypeSymbol idType = identifiableClosedInterface.TypeArguments[0]; + string controllerName = $"{resourceType.Name.Pluralize()}Controller"; + JsonApiEndpointsCopy endpointsToGenerate = (JsonApiEndpointsCopy?)(int?)endpointsArgument.Value ?? JsonApiEndpointsCopy.All; - foreach (TElement element in source) - { - if (predicate(element, context)) - { - return element; - } - } + string sourceCode = writer.Write(resourceType, idType, endpointsToGenerate, controllerNamespace, controllerName, loggerFactoryInterface); + SourceText sourceText = SourceText.From(sourceCode, Encoding.UTF8); - return default; + string fileName = GetUniqueFileName(controllerName, controllerNamesInUse); + context.AddSource(fileName, sourceText); } + } - private static string GetControllerNamespace(TypedConstant controllerNamespaceArgument, INamedTypeSymbol resourceType) + private static TElement? FirstOrDefault(ImmutableArray source, TContext context, Func predicate) + { + // PERF: Using this method enables to avoid allocating a closure in the passed lambda expression. + // See https://www.jetbrains.com/help/resharper/2021.2/Fixing_Issues_Found_by_DPA.html#closures-in-lambda-expressions. + + foreach (TElement element in source) { - if (!controllerNamespaceArgument.IsNull) + if (predicate(element, context)) { - return (string)controllerNamespaceArgument.Value; + return element; } + } - if (resourceType.ContainingNamespace.IsGlobalNamespace) - { - return null; - } + return default; + } - if (resourceType.ContainingNamespace.ContainingNamespace.IsGlobalNamespace) - { - return "Controllers"; - } + private static string? GetControllerNamespace(TypedConstant controllerNamespaceArgument, INamedTypeSymbol resourceType) + { + if (!controllerNamespaceArgument.IsNull) + { + return (string?)controllerNamespaceArgument.Value; + } - return $"{resourceType.ContainingNamespace.ContainingNamespace}.Controllers"; + if (resourceType.ContainingNamespace.IsGlobalNamespace) + { + return null; } - private static string GetUniqueFileName(string controllerName, IDictionary controllerNamesInUse) + if (resourceType.ContainingNamespace.ContainingNamespace.IsGlobalNamespace) { - // We emit unique file names to prevent a failure in the source generator, but also because our test suite - // may contain two resources with the same class name in different namespaces. That works, as long as only - // one of its controllers gets registered. + return "Controllers"; + } - if (controllerNamesInUse.TryGetValue(controllerName, out int lastIndex)) - { - lastIndex++; - controllerNamesInUse[controllerName] = lastIndex; + return $"{resourceType.ContainingNamespace.ContainingNamespace}.Controllers"; + } - return $"{controllerName}{lastIndex}.g.cs"; - } + private static string GetUniqueFileName(string controllerName, IDictionary controllerNamesInUse) + { + // We emit unique file names to prevent a failure in the source generator, but also because our test suite + // may contain two resources with the same class name in different namespaces. That works, as long as only + // one of its controllers gets registered. - controllerNamesInUse[controllerName] = 1; - return $"{controllerName}.g.cs"; + if (controllerNamesInUse.TryGetValue(controllerName, out int lastIndex)) + { + lastIndex++; + controllerNamesInUse[controllerName] = lastIndex; + + return $"{controllerName}{lastIndex}.g.cs"; } + + controllerNamesInUse[controllerName] = 1; + return $"{controllerName}.g.cs"; } } diff --git a/src/JsonApiDotNetCore.SourceGenerators/JsonApiDotNetCore.SourceGenerators.csproj b/src/JsonApiDotNetCore.SourceGenerators/JsonApiDotNetCore.SourceGenerators.csproj index bcd8c06b0a..8bf3e90cf6 100644 --- a/src/JsonApiDotNetCore.SourceGenerators/JsonApiDotNetCore.SourceGenerators.csproj +++ b/src/JsonApiDotNetCore.SourceGenerators/JsonApiDotNetCore.SourceGenerators.csproj @@ -5,8 +5,7 @@ true false $(NoWarn);NU5128 - disable - disable + latest true diff --git a/src/JsonApiDotNetCore.SourceGenerators/JsonApiEndpointsCopy.cs b/src/JsonApiDotNetCore.SourceGenerators/JsonApiEndpointsCopy.cs index 14134adcfd..911be3f359 100644 --- a/src/JsonApiDotNetCore.SourceGenerators/JsonApiEndpointsCopy.cs +++ b/src/JsonApiDotNetCore.SourceGenerators/JsonApiEndpointsCopy.cs @@ -1,26 +1,23 @@ -using System; +namespace JsonApiDotNetCore.SourceGenerators; -namespace JsonApiDotNetCore.SourceGenerators +// IMPORTANT: A copy of this type exists in the JsonApiDotNetCore project. Keep these in sync when making changes. +[Flags] +public enum JsonApiEndpointsCopy { - // IMPORTANT: A copy of this type exists in the JsonApiDotNetCore project. Keep these in sync when making changes. - [Flags] - public enum JsonApiEndpointsCopy - { - None = 0, - GetCollection = 1, - GetSingle = 1 << 1, - GetSecondary = 1 << 2, - GetRelationship = 1 << 3, - Post = 1 << 4, - PostRelationship = 1 << 5, - Patch = 1 << 6, - PatchRelationship = 1 << 7, - Delete = 1 << 8, - DeleteRelationship = 1 << 9, + None = 0, + GetCollection = 1, + GetSingle = 1 << 1, + GetSecondary = 1 << 2, + GetRelationship = 1 << 3, + Post = 1 << 4, + PostRelationship = 1 << 5, + Patch = 1 << 6, + PatchRelationship = 1 << 7, + Delete = 1 << 8, + DeleteRelationship = 1 << 9, - Query = GetCollection | GetSingle | GetSecondary | GetRelationship, - Command = Post | PostRelationship | Patch | PatchRelationship | Delete | DeleteRelationship, + Query = GetCollection | GetSingle | GetSecondary | GetRelationship, + Command = Post | PostRelationship | Patch | PatchRelationship | Delete | DeleteRelationship, - All = Query | Command - } + All = Query | Command } diff --git a/src/JsonApiDotNetCore.SourceGenerators/SourceCodeWriter.cs b/src/JsonApiDotNetCore.SourceGenerators/SourceCodeWriter.cs index e03e3cbad2..13dc91a836 100644 --- a/src/JsonApiDotNetCore.SourceGenerators/SourceCodeWriter.cs +++ b/src/JsonApiDotNetCore.SourceGenerators/SourceCodeWriter.cs @@ -1,266 +1,271 @@ -using System.Collections.Generic; using System.Text; using Microsoft.CodeAnalysis; -namespace JsonApiDotNetCore.SourceGenerators +namespace JsonApiDotNetCore.SourceGenerators; + +/// +/// Writes the source code for an ASP.NET controller for a JSON:API resource. +/// +internal sealed class SourceCodeWriter { - /// - /// Writes the source code for an ASP.NET controller for a JSON:API resource. - /// - internal sealed class SourceCodeWriter - { - private const int SpacesPerIndent = 4; + private const int SpacesPerIndent = 4; - private static readonly IDictionary IndentTable = new Dictionary + private static readonly IDictionary IndentTable = new Dictionary + { + [0] = string.Empty, + [1] = new(' ', 1 * SpacesPerIndent), + [2] = new(' ', 2 * SpacesPerIndent), + [3] = new(' ', 3 * SpacesPerIndent) + }; + + private static readonly IDictionary AggregateEndpointToServiceNameMap = + new Dictionary { - [0] = string.Empty, - [1] = new string(' ', 1 * SpacesPerIndent), - [2] = new string(' ', 2 * SpacesPerIndent), - [3] = new string(' ', 3 * SpacesPerIndent) + [JsonApiEndpointsCopy.All] = ("IResourceService", "resourceService"), + [JsonApiEndpointsCopy.Query] = ("IResourceQueryService", "queryService"), + [JsonApiEndpointsCopy.Command] = ("IResourceCommandService", "commandService") }; - private static readonly IDictionary AggregateEndpointToServiceNameMap = - new Dictionary - { - [JsonApiEndpointsCopy.All] = ("IResourceService", "resourceService"), - [JsonApiEndpointsCopy.Query] = ("IResourceQueryService", "queryService"), - [JsonApiEndpointsCopy.Command] = ("IResourceCommandService", "commandService") - }; - - private static readonly IDictionary EndpointToServiceNameMap = - new Dictionary - { - [JsonApiEndpointsCopy.GetCollection] = ("IGetAllService", "getAll"), - [JsonApiEndpointsCopy.GetSingle] = ("IGetByIdService", "getById"), - [JsonApiEndpointsCopy.GetSecondary] = ("IGetSecondaryService", "getSecondary"), - [JsonApiEndpointsCopy.GetRelationship] = ("IGetRelationshipService", "getRelationship"), - [JsonApiEndpointsCopy.Post] = ("ICreateService", "create"), - [JsonApiEndpointsCopy.PostRelationship] = ("IAddToRelationshipService", "addToRelationship"), - [JsonApiEndpointsCopy.Patch] = ("IUpdateService", "update"), - [JsonApiEndpointsCopy.PatchRelationship] = ("ISetRelationshipService", "setRelationship"), - [JsonApiEndpointsCopy.Delete] = ("IDeleteService", "delete"), - [JsonApiEndpointsCopy.DeleteRelationship] = ("IRemoveFromRelationshipService", "removeFromRelationship") - }; - - private readonly GeneratorExecutionContext _context; - private readonly DiagnosticDescriptor _missingIndentInTableErrorDescriptor; - - private readonly StringBuilder _sourceBuilder = new StringBuilder(); - private int _depth; - - public SourceCodeWriter(GeneratorExecutionContext context, DiagnosticDescriptor missingIndentInTableErrorDescriptor) + private static readonly IDictionary EndpointToServiceNameMap = + new Dictionary { - _context = context; - _missingIndentInTableErrorDescriptor = missingIndentInTableErrorDescriptor; - } - - public string Write(INamedTypeSymbol resourceType, ITypeSymbol idType, JsonApiEndpointsCopy endpointsToGenerate, string controllerNamespace, - string controllerName, INamedTypeSymbol loggerFactoryInterface) - { - _sourceBuilder.Clear(); - _depth = 0; - - if (idType.IsReferenceType && idType.NullableAnnotation == NullableAnnotation.Annotated) - { - WriteNullableEnable(); - } + [JsonApiEndpointsCopy.GetCollection] = ("IGetAllService", "getAll"), + [JsonApiEndpointsCopy.GetSingle] = ("IGetByIdService", "getById"), + [JsonApiEndpointsCopy.GetSecondary] = ("IGetSecondaryService", "getSecondary"), + [JsonApiEndpointsCopy.GetRelationship] = ("IGetRelationshipService", "getRelationship"), + [JsonApiEndpointsCopy.Post] = ("ICreateService", "create"), + [JsonApiEndpointsCopy.PostRelationship] = ("IAddToRelationshipService", "addToRelationship"), + [JsonApiEndpointsCopy.Patch] = ("IUpdateService", "update"), + [JsonApiEndpointsCopy.PatchRelationship] = ("ISetRelationshipService", "setRelationship"), + [JsonApiEndpointsCopy.Delete] = ("IDeleteService", "delete"), + [JsonApiEndpointsCopy.DeleteRelationship] = ("IRemoveFromRelationshipService", "removeFromRelationship") + }; - WriteNamespaceImports(loggerFactoryInterface, resourceType); + private readonly GeneratorExecutionContext _context; + private readonly DiagnosticDescriptor _missingIndentInTableErrorDescriptor; - if (controllerNamespace != null) - { - WriteNamespaceDeclaration(controllerNamespace); - } + private readonly StringBuilder _sourceBuilder = new(); + private int _depth; - WriteOpenClassDeclaration(controllerName, endpointsToGenerate, resourceType, idType); - _depth++; + public SourceCodeWriter(GeneratorExecutionContext context, DiagnosticDescriptor missingIndentInTableErrorDescriptor) + { + _context = context; + _missingIndentInTableErrorDescriptor = missingIndentInTableErrorDescriptor; + } - WriteConstructor(controllerName, loggerFactoryInterface, endpointsToGenerate, resourceType, idType); + public string Write(INamedTypeSymbol resourceType, ITypeSymbol idType, JsonApiEndpointsCopy endpointsToGenerate, string? controllerNamespace, + string controllerName, INamedTypeSymbol loggerFactoryInterface) + { + _sourceBuilder.Clear(); + _depth = 0; - _depth--; - WriteCloseCurly(); + WriteAutoGeneratedComment(); - return _sourceBuilder.ToString(); + if (idType.IsReferenceType && idType.NullableAnnotation == NullableAnnotation.Annotated) + { + WriteNullableEnable(); } - private void WriteNullableEnable() + WriteNamespaceImports(loggerFactoryInterface, resourceType); + + if (controllerNamespace != null) { - _sourceBuilder.AppendLine("#nullable enable"); - _sourceBuilder.AppendLine(); + WriteNamespaceDeclaration(controllerNamespace); } - private void WriteNamespaceImports(INamedTypeSymbol loggerFactoryInterface, INamedTypeSymbol resourceType) - { - _sourceBuilder.AppendLine($@"using {loggerFactoryInterface.ContainingNamespace};"); + WriteOpenClassDeclaration(controllerName, endpointsToGenerate, resourceType, idType); + _depth++; - _sourceBuilder.AppendLine("using JsonApiDotNetCore.Configuration;"); - _sourceBuilder.AppendLine("using JsonApiDotNetCore.Controllers;"); - _sourceBuilder.AppendLine("using JsonApiDotNetCore.Services;"); + WriteConstructor(controllerName, loggerFactoryInterface, endpointsToGenerate, resourceType, idType); - if (!resourceType.ContainingNamespace.IsGlobalNamespace) - { - _sourceBuilder.AppendLine($"using {resourceType.ContainingNamespace};"); - } + _depth--; + WriteCloseCurly(); - _sourceBuilder.AppendLine(); - } + return _sourceBuilder.ToString(); + } - private void WriteNamespaceDeclaration(string controllerNamespace) - { - _sourceBuilder.AppendLine($"namespace {controllerNamespace};"); - _sourceBuilder.AppendLine(); - } + private void WriteAutoGeneratedComment() + { + _sourceBuilder.AppendLine("// "); + _sourceBuilder.AppendLine(); + } - private void WriteOpenClassDeclaration(string controllerName, JsonApiEndpointsCopy endpointsToGenerate, INamedTypeSymbol resourceType, - ITypeSymbol idType) - { - string baseClassName = GetControllerBaseClassName(endpointsToGenerate); + private void WriteNullableEnable() + { + _sourceBuilder.AppendLine("#nullable enable"); + _sourceBuilder.AppendLine(); + } - WriteIndent(); - _sourceBuilder.AppendLine($@"public sealed partial class {controllerName} : {baseClassName}<{resourceType.Name}, {idType}>"); + private void WriteNamespaceImports(INamedTypeSymbol loggerFactoryInterface, INamedTypeSymbol resourceType) + { + _sourceBuilder.AppendLine($@"using {loggerFactoryInterface.ContainingNamespace};"); - WriteOpenCurly(); - } + _sourceBuilder.AppendLine("using JsonApiDotNetCore.Configuration;"); + _sourceBuilder.AppendLine("using JsonApiDotNetCore.Controllers;"); + _sourceBuilder.AppendLine("using JsonApiDotNetCore.Services;"); - private static string GetControllerBaseClassName(JsonApiEndpointsCopy endpointsToGenerate) + if (!resourceType.ContainingNamespace.IsGlobalNamespace) { - switch (endpointsToGenerate) - { - case JsonApiEndpointsCopy.Query: - { - return "JsonApiQueryController"; - } - case JsonApiEndpointsCopy.Command: - { - return "JsonApiCommandController"; - } - default: - { - return "JsonApiController"; - } - } + _sourceBuilder.AppendLine($"using {resourceType.ContainingNamespace};"); } - private void WriteConstructor(string controllerName, INamedTypeSymbol loggerFactoryInterface, JsonApiEndpointsCopy endpointsToGenerate, - INamedTypeSymbol resourceType, ITypeSymbol idType) - { - string loggerName = loggerFactoryInterface.Name; + _sourceBuilder.AppendLine(); + } + + private void WriteNamespaceDeclaration(string controllerNamespace) + { + _sourceBuilder.AppendLine($"namespace {controllerNamespace};"); + _sourceBuilder.AppendLine(); + } + + private void WriteOpenClassDeclaration(string controllerName, JsonApiEndpointsCopy endpointsToGenerate, INamedTypeSymbol resourceType, ITypeSymbol idType) + { + string baseClassName = GetControllerBaseClassName(endpointsToGenerate); - WriteIndent(); - _sourceBuilder.AppendLine($"public {controllerName}(IJsonApiOptions options, IResourceGraph resourceGraph, {loggerName} loggerFactory,"); + WriteIndent(); + _sourceBuilder.AppendLine($@"public sealed partial class {controllerName} : {baseClassName}<{resourceType.Name}, {idType}>"); - _depth++; + WriteOpenCurly(); + } - if (AggregateEndpointToServiceNameMap.TryGetValue(endpointsToGenerate, out (string ServiceName, string ParameterName) value)) + private static string GetControllerBaseClassName(JsonApiEndpointsCopy endpointsToGenerate) + { + switch (endpointsToGenerate) + { + case JsonApiEndpointsCopy.Query: + { + return "JsonApiQueryController"; + } + case JsonApiEndpointsCopy.Command: { - WriteParameterListForShortConstructor(value.ServiceName, value.ParameterName, resourceType, idType); + return "JsonApiCommandController"; } - else + default: { - WriteParameterListForLongConstructor(endpointsToGenerate, resourceType, idType); + return "JsonApiController"; } + } + } - _depth--; + private void WriteConstructor(string controllerName, INamedTypeSymbol loggerFactoryInterface, JsonApiEndpointsCopy endpointsToGenerate, + INamedTypeSymbol resourceType, ITypeSymbol idType) + { + string loggerName = loggerFactoryInterface.Name; - WriteOpenCurly(); - WriteCloseCurly(); - } + WriteIndent(); + _sourceBuilder.AppendLine($"public {controllerName}(IJsonApiOptions options, IResourceGraph resourceGraph, {loggerName} loggerFactory,"); - private void WriteParameterListForShortConstructor(string serviceName, string parameterName, INamedTypeSymbol resourceType, ITypeSymbol idType) - { - WriteIndent(); - _sourceBuilder.AppendLine($"{serviceName}<{resourceType.Name}, {idType}> {parameterName})"); + _depth++; - WriteIndent(); - _sourceBuilder.AppendLine($": base(options, resourceGraph, loggerFactory, {parameterName})"); + if (AggregateEndpointToServiceNameMap.TryGetValue(endpointsToGenerate, out (string ServiceName, string ParameterName) value)) + { + WriteParameterListForShortConstructor(value.ServiceName, value.ParameterName, resourceType, idType); } - - private void WriteParameterListForLongConstructor(JsonApiEndpointsCopy endpointsToGenerate, INamedTypeSymbol resourceType, ITypeSymbol idType) + else { - bool isFirstEntry = true; + WriteParameterListForLongConstructor(endpointsToGenerate, resourceType, idType); + } + + _depth--; + + WriteOpenCurly(); + WriteCloseCurly(); + } + + private void WriteParameterListForShortConstructor(string serviceName, string parameterName, INamedTypeSymbol resourceType, ITypeSymbol idType) + { + WriteIndent(); + _sourceBuilder.AppendLine($"{serviceName}<{resourceType.Name}, {idType}> {parameterName})"); + + WriteIndent(); + _sourceBuilder.AppendLine($": base(options, resourceGraph, loggerFactory, {parameterName})"); + } + + private void WriteParameterListForLongConstructor(JsonApiEndpointsCopy endpointsToGenerate, INamedTypeSymbol resourceType, ITypeSymbol idType) + { + bool isFirstEntry = true; - foreach (KeyValuePair entry in EndpointToServiceNameMap) + foreach (KeyValuePair entry in EndpointToServiceNameMap) + { + if ((endpointsToGenerate & entry.Key) == entry.Key) { - if ((endpointsToGenerate & entry.Key) == entry.Key) + if (isFirstEntry) + { + isFirstEntry = false; + } + else { - if (isFirstEntry) - { - isFirstEntry = false; - } - else - { - _sourceBuilder.AppendLine(Tokens.Comma); - } - - WriteIndent(); - _sourceBuilder.Append($"{entry.Value.ServiceName}<{resourceType.Name}, {idType}> {entry.Value.ParameterName}"); + _sourceBuilder.AppendLine(Tokens.Comma); } + + WriteIndent(); + _sourceBuilder.Append($"{entry.Value.ServiceName}<{resourceType.Name}, {idType}> {entry.Value.ParameterName}"); } + } - _sourceBuilder.AppendLine(Tokens.CloseParen); + _sourceBuilder.AppendLine(Tokens.CloseParen); - WriteIndent(); - _sourceBuilder.AppendLine(": base(options, resourceGraph, loggerFactory,"); + WriteIndent(); + _sourceBuilder.AppendLine(": base(options, resourceGraph, loggerFactory,"); - isFirstEntry = true; - _depth++; + isFirstEntry = true; + _depth++; - foreach (KeyValuePair entry in EndpointToServiceNameMap) + foreach (KeyValuePair entry in EndpointToServiceNameMap) + { + if ((endpointsToGenerate & entry.Key) == entry.Key) { - if ((endpointsToGenerate & entry.Key) == entry.Key) + if (isFirstEntry) { - if (isFirstEntry) - { - isFirstEntry = false; - } - else - { - _sourceBuilder.AppendLine(Tokens.Comma); - } - - WriteIndent(); - _sourceBuilder.Append($"{entry.Value.ParameterName}: {entry.Value.ParameterName}"); + isFirstEntry = false; + } + else + { + _sourceBuilder.AppendLine(Tokens.Comma); } - } - _sourceBuilder.AppendLine(Tokens.CloseParen); - _depth--; + WriteIndent(); + _sourceBuilder.Append($"{entry.Value.ParameterName}: {entry.Value.ParameterName}"); + } } - private void WriteOpenCurly() - { - WriteIndent(); - _sourceBuilder.AppendLine(Tokens.OpenCurly); - } + _sourceBuilder.AppendLine(Tokens.CloseParen); + _depth--; + } - private void WriteCloseCurly() - { - WriteIndent(); - _sourceBuilder.AppendLine(Tokens.CloseCurly); - } + private void WriteOpenCurly() + { + WriteIndent(); + _sourceBuilder.AppendLine(Tokens.OpenCurly); + } - private void WriteIndent() - { - // PERF: Reuse pre-calculated indents instead of allocating a new string each time. - if (!IndentTable.TryGetValue(_depth, out string indent)) - { - var diagnostic = Diagnostic.Create(_missingIndentInTableErrorDescriptor, Location.None, _depth.ToString()); - _context.ReportDiagnostic(diagnostic); + private void WriteCloseCurly() + { + WriteIndent(); + _sourceBuilder.AppendLine(Tokens.CloseCurly); + } - indent = new string(' ', _depth * SpacesPerIndent); - } + private void WriteIndent() + { + // PERF: Reuse pre-calculated indents instead of allocating a new string each time. + if (!IndentTable.TryGetValue(_depth, out string? indent)) + { + var diagnostic = Diagnostic.Create(_missingIndentInTableErrorDescriptor, Location.None, _depth.ToString()); + _context.ReportDiagnostic(diagnostic); - _sourceBuilder.Append(indent); + indent = new string(' ', _depth * SpacesPerIndent); } + _sourceBuilder.Append(indent); + } + #pragma warning disable AV1008 // Class should not be static - private static class Tokens - { - public const string OpenCurly = "{"; - public const string CloseCurly = "}"; - public const string CloseParen = ")"; - public const string Comma = ","; - } -#pragma warning restore AV1008 // Class should not be static + private static class Tokens + { + public const string OpenCurly = "{"; + public const string CloseCurly = "}"; + public const string CloseParen = ")"; + public const string Comma = ","; } +#pragma warning restore AV1008 // Class should not be static } diff --git a/src/JsonApiDotNetCore.SourceGenerators/TypeWithAttributeSyntaxReceiver.cs b/src/JsonApiDotNetCore.SourceGenerators/TypeWithAttributeSyntaxReceiver.cs index 0fbc18a758..b23de19cc9 100644 --- a/src/JsonApiDotNetCore.SourceGenerators/TypeWithAttributeSyntaxReceiver.cs +++ b/src/JsonApiDotNetCore.SourceGenerators/TypeWithAttributeSyntaxReceiver.cs @@ -1,41 +1,39 @@ -using System.Collections.Generic; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace JsonApiDotNetCore.SourceGenerators +namespace JsonApiDotNetCore.SourceGenerators; + +/// +/// Collects type declarations in the project that have at least one attribute on them. Because this receiver operates at the syntax level, we cannot +/// check for the expected attribute. This must be done during semantic analysis, because source code may contain any of these: +/// { } +/// +/// [ResourceAttribute] +/// public class ExampleResource2 : Identifiable { } +/// +/// [AlternateNamespaceName.Annotations.Resource] +/// public class ExampleResource3 : Identifiable { } +/// +/// [AlternateTypeName] +/// public class ExampleResource4 : Identifiable { } +/// ]]> +/// +/// +internal sealed class TypeWithAttributeSyntaxReceiver : ISyntaxReceiver { - /// - /// Collects type declarations in the project that have at least one attribute on them. Because this receiver operates at the syntax level, we cannot - /// check for the expected attribute. This must be done during semantic analysis, because source code may contain any of these: - /// { } - /// - /// [ResourceAttribute] - /// public class ExampleResource2 : Identifiable { } - /// - /// [AlternateNamespaceName.Annotations.Resource] - /// public class ExampleResource3 : Identifiable { } - /// - /// [AlternateTypeName] - /// public class ExampleResource4 : Identifiable { } - /// ]]> - /// - /// - internal sealed class TypeWithAttributeSyntaxReceiver : ISyntaxReceiver - { - public readonly ISet TypeDeclarations = new HashSet(); + public readonly ISet TypeDeclarations = new HashSet(); - public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is TypeDeclarationSyntax typeDeclarationSyntax && typeDeclarationSyntax.AttributeLists.Any()) { - if (syntaxNode is TypeDeclarationSyntax typeDeclarationSyntax && typeDeclarationSyntax.AttributeLists.Any()) - { - TypeDeclarations.Add(typeDeclarationSyntax); - } + TypeDeclarations.Add(typeDeclarationSyntax); } } } diff --git a/test/SourceGeneratorTests/ControllerGenerationTests.cs b/test/SourceGeneratorTests/ControllerGenerationTests.cs index 614b4d316c..9f5f9f83d3 100644 --- a/test/SourceGeneratorTests/ControllerGenerationTests.cs +++ b/test/SourceGeneratorTests/ControllerGenerationTests.cs @@ -51,7 +51,9 @@ public sealed class Item : Identifiable GeneratorDriverRunResult runResult = driver.GetRunResult(); runResult.Should().NotHaveDiagnostics(); - runResult.Should().HaveProducedSourceCode(@"using Microsoft.Extensions.Logging; + runResult.Should().HaveProducedSourceCode(@"// + +using Microsoft.Extensions.Logging; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; @@ -111,7 +113,9 @@ public sealed class Item : Identifiable GeneratorDriverRunResult runResult = driver.GetRunResult(); runResult.Should().NotHaveDiagnostics(); - runResult.Should().HaveProducedSourceCode(@"using Microsoft.Extensions.Logging; + runResult.Should().HaveProducedSourceCode(@"// + +using Microsoft.Extensions.Logging; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; @@ -171,7 +175,9 @@ public sealed class Item : Identifiable GeneratorDriverRunResult runResult = driver.GetRunResult(); runResult.Should().NotHaveDiagnostics(); - runResult.Should().HaveProducedSourceCode(@"using Microsoft.Extensions.Logging; + runResult.Should().HaveProducedSourceCode(@"// + +using Microsoft.Extensions.Logging; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; @@ -234,7 +240,9 @@ public sealed class Item : Identifiable GeneratorDriverRunResult runResult = driver.GetRunResult(); runResult.Should().NotHaveDiagnostics(); - runResult.Should().HaveProducedSourceCode(@"using Microsoft.Extensions.Logging; + runResult.Should().HaveProducedSourceCode(@"// + +using Microsoft.Extensions.Logging; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; @@ -574,7 +582,9 @@ public sealed class Item : Identifiable GeneratorDriverRunResult runResult = driver.GetRunResult(); runResult.Should().NotHaveDiagnostics(); - runResult.Should().HaveProducedSourceCode(@"using Microsoft.Extensions.Logging; + runResult.Should().HaveProducedSourceCode(@"// + +using Microsoft.Extensions.Logging; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; @@ -633,7 +643,9 @@ public sealed class Item : Identifiable GeneratorDriverRunResult runResult = driver.GetRunResult(); runResult.Should().NotHaveDiagnostics(); - runResult.Should().HaveProducedSourceCode(@"using Microsoft.Extensions.Logging; + runResult.Should().HaveProducedSourceCode(@"// + +using Microsoft.Extensions.Logging; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; @@ -691,7 +703,9 @@ public sealed class Item : Identifiable GeneratorDriverRunResult runResult = driver.GetRunResult(); runResult.Should().NotHaveDiagnostics(); - runResult.Should().HaveProducedSourceCode(@"using Microsoft.Extensions.Logging; + runResult.Should().HaveProducedSourceCode(@"// + +using Microsoft.Extensions.Logging; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services;