|
1 |
| -using System; |
2 |
| -using System.Collections.Generic; |
3 | 1 | using System.Collections.Immutable;
|
4 |
| -using System.Linq; |
5 | 2 | using System.Text;
|
6 | 3 | using Humanizer;
|
7 | 4 | using Microsoft.CodeAnalysis;
|
|
11 | 8 |
|
12 | 9 | #pragma warning disable RS2008 // Enable analyzer release tracking
|
13 | 10 |
|
14 |
| -namespace JsonApiDotNetCore.SourceGenerators |
| 11 | +namespace JsonApiDotNetCore.SourceGenerators; |
| 12 | +// To debug in Visual Studio (requires v17.2 or higher): |
| 13 | +// - Set JsonApiDotNetCore.SourceGenerators as startup project |
| 14 | +// - Add a breakpoint at the start of the Initialize or Execute method |
| 15 | +// - Optional: change targetProject in Properties\launchSettings.json |
| 16 | +// - Press F5 |
| 17 | + |
| 18 | +[Generator(LanguageNames.CSharp)] |
| 19 | +public sealed class ControllerSourceGenerator : ISourceGenerator |
15 | 20 | {
|
16 |
| - // To debug in Visual Studio (requires v17.2 or higher): |
17 |
| - // - Set JsonApiDotNetCore.SourceGenerators as startup project |
18 |
| - // - Add a breakpoint at the start of the Initialize or Execute method |
19 |
| - // - Optional: change targetProject in Properties\launchSettings.json |
20 |
| - // - Press F5 |
21 |
| - |
22 |
| - [Generator(LanguageNames.CSharp)] |
23 |
| - public sealed class ControllerSourceGenerator : ISourceGenerator |
| 21 | + private const string Category = "JsonApiDotNetCore"; |
| 22 | + |
| 23 | + private static readonly DiagnosticDescriptor MissingInterfaceWarning = new("JADNC001", "Resource type does not implement IIdentifiable<TId>", |
| 24 | + "Type '{0}' must implement IIdentifiable<TId> when using ResourceAttribute to auto-generate ASP.NET controllers", Category, DiagnosticSeverity.Warning, |
| 25 | + true); |
| 26 | + |
| 27 | + private static readonly DiagnosticDescriptor MissingIndentInTableError = new("JADNC900", "Internal error: Insufficient entries in IndentTable", |
| 28 | + "Internal error: Missing entry in IndentTable for depth {0}", Category, DiagnosticSeverity.Warning, true); |
| 29 | + |
| 30 | + // PERF: Heap-allocate the delegate only once, instead of per compilation. |
| 31 | + private static readonly SyntaxReceiverCreator CreateSyntaxReceiver = static () => new TypeWithAttributeSyntaxReceiver(); |
| 32 | + |
| 33 | + public void Initialize(GeneratorInitializationContext context) |
24 | 34 | {
|
25 |
| - private const string Category = "JsonApiDotNetCore"; |
| 35 | + context.RegisterForSyntaxNotifications(CreateSyntaxReceiver); |
| 36 | + } |
26 | 37 |
|
27 |
| - private static readonly DiagnosticDescriptor MissingInterfaceWarning = new DiagnosticDescriptor("JADNC001", |
28 |
| - "Resource type does not implement IIdentifiable<TId>", |
29 |
| - "Type '{0}' must implement IIdentifiable<TId> when using ResourceAttribute to auto-generate ASP.NET controllers", Category, |
30 |
| - DiagnosticSeverity.Warning, true); |
| 38 | + public void Execute(GeneratorExecutionContext context) |
| 39 | + { |
| 40 | + var receiver = (TypeWithAttributeSyntaxReceiver?)context.SyntaxReceiver; |
31 | 41 |
|
32 |
| - private static readonly DiagnosticDescriptor MissingIndentInTableError = new DiagnosticDescriptor("JADNC900", |
33 |
| - "Internal error: Insufficient entries in IndentTable", "Internal error: Missing entry in IndentTable for depth {0}", Category, |
34 |
| - DiagnosticSeverity.Warning, true); |
| 42 | + if (receiver == null) |
| 43 | + { |
| 44 | + return; |
| 45 | + } |
35 | 46 |
|
36 |
| - // PERF: Heap-allocate the delegate only once, instead of per compilation. |
37 |
| - private static readonly SyntaxReceiverCreator CreateSyntaxReceiver = () => new TypeWithAttributeSyntaxReceiver(); |
| 47 | + INamedTypeSymbol? resourceAttributeType = context.Compilation.GetTypeByMetadataName("JsonApiDotNetCore.Resources.Annotations.ResourceAttribute"); |
| 48 | + INamedTypeSymbol? identifiableOpenInterface = context.Compilation.GetTypeByMetadataName("JsonApiDotNetCore.Resources.IIdentifiable`1"); |
| 49 | + INamedTypeSymbol? loggerFactoryInterface = context.Compilation.GetTypeByMetadataName("Microsoft.Extensions.Logging.ILoggerFactory"); |
38 | 50 |
|
39 |
| - public void Initialize(GeneratorInitializationContext context) |
| 51 | + if (resourceAttributeType == null || identifiableOpenInterface == null || loggerFactoryInterface == null) |
40 | 52 | {
|
41 |
| - context.RegisterForSyntaxNotifications(CreateSyntaxReceiver); |
| 53 | + return; |
42 | 54 | }
|
43 | 55 |
|
44 |
| - public void Execute(GeneratorExecutionContext context) |
| 56 | + var controllerNamesInUse = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); |
| 57 | + var writer = new SourceCodeWriter(context, MissingIndentInTableError); |
| 58 | + |
| 59 | + foreach (TypeDeclarationSyntax? typeDeclarationSyntax in receiver.TypeDeclarations) |
45 | 60 | {
|
46 |
| - var receiver = (TypeWithAttributeSyntaxReceiver)context.SyntaxReceiver; |
| 61 | + // PERF: Note that our code runs on every keystroke in the IDE, which makes it critical to provide near-realtime performance. |
| 62 | + // This means keeping an eye on memory allocations and bailing out early when compilations are cancelled while the user is still typing. |
| 63 | + context.CancellationToken.ThrowIfCancellationRequested(); |
| 64 | + |
| 65 | + SemanticModel semanticModel = context.Compilation.GetSemanticModel(typeDeclarationSyntax.SyntaxTree); |
| 66 | + INamedTypeSymbol? resourceType = semanticModel.GetDeclaredSymbol(typeDeclarationSyntax, context.CancellationToken); |
47 | 67 |
|
48 |
| - if (receiver == null) |
| 68 | + if (resourceType == null) |
49 | 69 | {
|
50 |
| - return; |
| 70 | + continue; |
51 | 71 | }
|
52 | 72 |
|
53 |
| - INamedTypeSymbol resourceAttributeType = context.Compilation.GetTypeByMetadataName("JsonApiDotNetCore.Resources.Annotations.ResourceAttribute"); |
54 |
| - INamedTypeSymbol identifiableOpenInterface = context.Compilation.GetTypeByMetadataName("JsonApiDotNetCore.Resources.IIdentifiable`1"); |
55 |
| - INamedTypeSymbol loggerFactoryInterface = context.Compilation.GetTypeByMetadataName("Microsoft.Extensions.Logging.ILoggerFactory"); |
| 73 | + AttributeData? resourceAttributeData = FirstOrDefault(resourceType.GetAttributes(), resourceAttributeType, |
| 74 | + static (data, type) => SymbolEqualityComparer.Default.Equals(data.AttributeClass, type)); |
56 | 75 |
|
57 |
| - if (resourceAttributeType == null || identifiableOpenInterface == null || loggerFactoryInterface == null) |
| 76 | + if (resourceAttributeData == null) |
58 | 77 | {
|
59 |
| - return; |
| 78 | + continue; |
60 | 79 | }
|
61 | 80 |
|
62 |
| - var controllerNamesInUse = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); |
63 |
| - var writer = new SourceCodeWriter(context, MissingIndentInTableError); |
| 81 | + TypedConstant endpointsArgument = |
| 82 | + resourceAttributeData.NamedArguments.FirstOrDefault(static pair => pair.Key == "GenerateControllerEndpoints").Value; |
64 | 83 |
|
65 |
| - foreach (TypeDeclarationSyntax typeDeclarationSyntax in receiver.TypeDeclarations) |
| 84 | + if (endpointsArgument.Value != null && (JsonApiEndpointsCopy)endpointsArgument.Value == JsonApiEndpointsCopy.None) |
66 | 85 | {
|
67 |
| - // PERF: Note that our code runs on every keystroke in the IDE, which makes it critical to provide near-realtime performance. |
68 |
| - // This means keeping an eye on memory allocations and bailing out early when compilations are cancelled while the user is still typing. |
69 |
| - context.CancellationToken.ThrowIfCancellationRequested(); |
70 |
| - |
71 |
| - SemanticModel semanticModel = context.Compilation.GetSemanticModel(typeDeclarationSyntax.SyntaxTree); |
72 |
| - INamedTypeSymbol resourceType = semanticModel.GetDeclaredSymbol(typeDeclarationSyntax, context.CancellationToken); |
73 |
| - |
74 |
| - if (resourceType == null) |
75 |
| - { |
76 |
| - continue; |
77 |
| - } |
78 |
| - |
79 |
| - AttributeData resourceAttributeData = FirstOrDefault(resourceType.GetAttributes(), resourceAttributeType, |
80 |
| - (data, type) => SymbolEqualityComparer.Default.Equals(data.AttributeClass, type)); |
81 |
| - |
82 |
| - if (resourceAttributeData == null) |
83 |
| - { |
84 |
| - continue; |
85 |
| - } |
86 |
| - |
87 |
| - TypedConstant endpointsArgument = resourceAttributeData.NamedArguments.FirstOrDefault(pair => pair.Key == "GenerateControllerEndpoints").Value; |
88 |
| - |
89 |
| - if (endpointsArgument.Value != null && (JsonApiEndpointsCopy)endpointsArgument.Value == JsonApiEndpointsCopy.None) |
90 |
| - { |
91 |
| - continue; |
92 |
| - } |
93 |
| - |
94 |
| - TypedConstant controllerNamespaceArgument = |
95 |
| - resourceAttributeData.NamedArguments.FirstOrDefault(pair => pair.Key == "ControllerNamespace").Value; |
96 |
| - |
97 |
| - string controllerNamespace = GetControllerNamespace(controllerNamespaceArgument, resourceType); |
98 |
| - |
99 |
| - INamedTypeSymbol identifiableClosedInterface = FirstOrDefault(resourceType.AllInterfaces, identifiableOpenInterface, |
100 |
| - (@interface, openInterface) => @interface.IsGenericType && |
101 |
| - SymbolEqualityComparer.Default.Equals(@interface.ConstructedFrom, openInterface)); |
| 86 | + continue; |
| 87 | + } |
102 | 88 |
|
103 |
| - if (identifiableClosedInterface == null) |
104 |
| - { |
105 |
| - var diagnostic = Diagnostic.Create(MissingInterfaceWarning, typeDeclarationSyntax.GetLocation(), resourceType.Name); |
106 |
| - context.ReportDiagnostic(diagnostic); |
107 |
| - continue; |
108 |
| - } |
| 89 | + TypedConstant controllerNamespaceArgument = |
| 90 | + resourceAttributeData.NamedArguments.FirstOrDefault(static pair => pair.Key == "ControllerNamespace").Value; |
109 | 91 |
|
110 |
| - ITypeSymbol idType = identifiableClosedInterface.TypeArguments[0]; |
111 |
| - string controllerName = $"{resourceType.Name.Pluralize()}Controller"; |
112 |
| - JsonApiEndpointsCopy endpointsToGenerate = (JsonApiEndpointsCopy?)(int?)endpointsArgument.Value ?? JsonApiEndpointsCopy.All; |
| 92 | + string? controllerNamespace = GetControllerNamespace(controllerNamespaceArgument, resourceType); |
113 | 93 |
|
114 |
| - string sourceCode = writer.Write(resourceType, idType, endpointsToGenerate, controllerNamespace, controllerName, loggerFactoryInterface); |
115 |
| - SourceText sourceText = SourceText.From(sourceCode, Encoding.UTF8); |
| 94 | + INamedTypeSymbol? identifiableClosedInterface = FirstOrDefault(resourceType.AllInterfaces, identifiableOpenInterface, |
| 95 | + static (@interface, openInterface) => |
| 96 | + @interface.IsGenericType && SymbolEqualityComparer.Default.Equals(@interface.ConstructedFrom, openInterface)); |
116 | 97 |
|
117 |
| - string fileName = GetUniqueFileName(controllerName, controllerNamesInUse); |
118 |
| - context.AddSource(fileName, sourceText); |
| 98 | + if (identifiableClosedInterface == null) |
| 99 | + { |
| 100 | + var diagnostic = Diagnostic.Create(MissingInterfaceWarning, typeDeclarationSyntax.GetLocation(), resourceType.Name); |
| 101 | + context.ReportDiagnostic(diagnostic); |
| 102 | + continue; |
119 | 103 | }
|
120 |
| - } |
121 | 104 |
|
122 |
| - private static TElement FirstOrDefault<TElement, TContext>(ImmutableArray<TElement> source, TContext context, Func<TElement, TContext, bool> predicate) |
123 |
| - { |
124 |
| - // PERF: Using this method enables to avoid allocating a closure in the passed lambda expression. |
125 |
| - // See https://www.jetbrains.com/help/resharper/2021.2/Fixing_Issues_Found_by_DPA.html#closures-in-lambda-expressions. |
| 105 | + ITypeSymbol idType = identifiableClosedInterface.TypeArguments[0]; |
| 106 | + string controllerName = $"{resourceType.Name.Pluralize()}Controller"; |
| 107 | + JsonApiEndpointsCopy endpointsToGenerate = (JsonApiEndpointsCopy?)(int?)endpointsArgument.Value ?? JsonApiEndpointsCopy.All; |
126 | 108 |
|
127 |
| - foreach (TElement element in source) |
128 |
| - { |
129 |
| - if (predicate(element, context)) |
130 |
| - { |
131 |
| - return element; |
132 |
| - } |
133 |
| - } |
| 109 | + string sourceCode = writer.Write(resourceType, idType, endpointsToGenerate, controllerNamespace, controllerName, loggerFactoryInterface); |
| 110 | + SourceText sourceText = SourceText.From(sourceCode, Encoding.UTF8); |
134 | 111 |
|
135 |
| - return default; |
| 112 | + string fileName = GetUniqueFileName(controllerName, controllerNamesInUse); |
| 113 | + context.AddSource(fileName, sourceText); |
136 | 114 | }
|
| 115 | + } |
137 | 116 |
|
138 |
| - private static string GetControllerNamespace(TypedConstant controllerNamespaceArgument, INamedTypeSymbol resourceType) |
| 117 | + private static TElement? FirstOrDefault<TElement, TContext>(ImmutableArray<TElement> source, TContext context, Func<TElement, TContext, bool> predicate) |
| 118 | + { |
| 119 | + // PERF: Using this method enables to avoid allocating a closure in the passed lambda expression. |
| 120 | + // See https://www.jetbrains.com/help/resharper/2021.2/Fixing_Issues_Found_by_DPA.html#closures-in-lambda-expressions. |
| 121 | + |
| 122 | + foreach (TElement element in source) |
139 | 123 | {
|
140 |
| - if (!controllerNamespaceArgument.IsNull) |
| 124 | + if (predicate(element, context)) |
141 | 125 | {
|
142 |
| - return (string)controllerNamespaceArgument.Value; |
| 126 | + return element; |
143 | 127 | }
|
| 128 | + } |
144 | 129 |
|
145 |
| - if (resourceType.ContainingNamespace.IsGlobalNamespace) |
146 |
| - { |
147 |
| - return null; |
148 |
| - } |
| 130 | + return default; |
| 131 | + } |
149 | 132 |
|
150 |
| - if (resourceType.ContainingNamespace.ContainingNamespace.IsGlobalNamespace) |
151 |
| - { |
152 |
| - return "Controllers"; |
153 |
| - } |
| 133 | + private static string? GetControllerNamespace(TypedConstant controllerNamespaceArgument, INamedTypeSymbol resourceType) |
| 134 | + { |
| 135 | + if (!controllerNamespaceArgument.IsNull) |
| 136 | + { |
| 137 | + return (string?)controllerNamespaceArgument.Value; |
| 138 | + } |
154 | 139 |
|
155 |
| - return $"{resourceType.ContainingNamespace.ContainingNamespace}.Controllers"; |
| 140 | + if (resourceType.ContainingNamespace.IsGlobalNamespace) |
| 141 | + { |
| 142 | + return null; |
156 | 143 | }
|
157 | 144 |
|
158 |
| - private static string GetUniqueFileName(string controllerName, IDictionary<string, int> controllerNamesInUse) |
| 145 | + if (resourceType.ContainingNamespace.ContainingNamespace.IsGlobalNamespace) |
159 | 146 | {
|
160 |
| - // We emit unique file names to prevent a failure in the source generator, but also because our test suite |
161 |
| - // may contain two resources with the same class name in different namespaces. That works, as long as only |
162 |
| - // one of its controllers gets registered. |
| 147 | + return "Controllers"; |
| 148 | + } |
163 | 149 |
|
164 |
| - if (controllerNamesInUse.TryGetValue(controllerName, out int lastIndex)) |
165 |
| - { |
166 |
| - lastIndex++; |
167 |
| - controllerNamesInUse[controllerName] = lastIndex; |
| 150 | + return $"{resourceType.ContainingNamespace.ContainingNamespace}.Controllers"; |
| 151 | + } |
168 | 152 |
|
169 |
| - return $"{controllerName}{lastIndex}.g.cs"; |
170 |
| - } |
| 153 | + private static string GetUniqueFileName(string controllerName, IDictionary<string, int> controllerNamesInUse) |
| 154 | + { |
| 155 | + // We emit unique file names to prevent a failure in the source generator, but also because our test suite |
| 156 | + // may contain two resources with the same class name in different namespaces. That works, as long as only |
| 157 | + // one of its controllers gets registered. |
171 | 158 |
|
172 |
| - controllerNamesInUse[controllerName] = 1; |
173 |
| - return $"{controllerName}.g.cs"; |
| 159 | + if (controllerNamesInUse.TryGetValue(controllerName, out int lastIndex)) |
| 160 | + { |
| 161 | + lastIndex++; |
| 162 | + controllerNamesInUse[controllerName] = lastIndex; |
| 163 | + |
| 164 | + return $"{controllerName}{lastIndex}.g.cs"; |
174 | 165 | }
|
| 166 | + |
| 167 | + controllerNamesInUse[controllerName] = 1; |
| 168 | + return $"{controllerName}.g.cs"; |
175 | 169 | }
|
176 | 170 | }
|
0 commit comments