Skip to content

Commit ca14796

Browse files
authored
Merge pull request #1178 from json-api-dotnet/source-generator-tweaks
Source generator tweaks
2 parents eed2ab1 + 549586b commit ca14796

7 files changed

+389
-381
lines changed

JsonApiDotNetCore.sln.DotSettings

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ JsonApiDotNetCore.ArgumentGuard.NotNull($EXPR$, $NAME$);</s:String>
5454
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=EnforceUsingStatementBraces/@EntryIndexedValue">WARNING</s:String>
5555
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=EnforceWhileStatementBraces/@EntryIndexedValue">WARNING</s:String>
5656
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=EventNeverSubscribedTo_002ELocal/@EntryIndexedValue">WARNING</s:String>
57+
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=LambdaExpressionMustBeStatic/@EntryIndexedValue">WARNING</s:String>
5758
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=LocalizableElement/@EntryIndexedValue">DO_NOT_SHOW</s:String>
5859
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=LoopCanBePartlyConvertedToQuery/@EntryIndexedValue">HINT</s:String>
5960
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBeInternal/@EntryIndexedValue">SUGGESTION</s:String>
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
using System;
2-
using System.Collections.Generic;
31
using System.Collections.Immutable;
4-
using System.Linq;
52
using System.Text;
63
using Humanizer;
74
using Microsoft.CodeAnalysis;
@@ -11,166 +8,163 @@
118

129
#pragma warning disable RS2008 // Enable analyzer release tracking
1310

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
1520
{
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)
2434
{
25-
private const string Category = "JsonApiDotNetCore";
35+
context.RegisterForSyntaxNotifications(CreateSyntaxReceiver);
36+
}
2637

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;
3141

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+
}
3546

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");
3850

39-
public void Initialize(GeneratorInitializationContext context)
51+
if (resourceAttributeType == null || identifiableOpenInterface == null || loggerFactoryInterface == null)
4052
{
41-
context.RegisterForSyntaxNotifications(CreateSyntaxReceiver);
53+
return;
4254
}
4355

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)
4560
{
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);
4767

48-
if (receiver == null)
68+
if (resourceType == null)
4969
{
50-
return;
70+
continue;
5171
}
5272

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));
5675

57-
if (resourceAttributeType == null || identifiableOpenInterface == null || loggerFactoryInterface == null)
76+
if (resourceAttributeData == null)
5877
{
59-
return;
78+
continue;
6079
}
6180

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;
6483

65-
foreach (TypeDeclarationSyntax typeDeclarationSyntax in receiver.TypeDeclarations)
84+
if (endpointsArgument.Value != null && (JsonApiEndpointsCopy)endpointsArgument.Value == JsonApiEndpointsCopy.None)
6685
{
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+
}
10288

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;
10991

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);
11393

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));
11697

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;
119103
}
120-
}
121104

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;
126108

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);
134111

135-
return default;
112+
string fileName = GetUniqueFileName(controllerName, controllerNamesInUse);
113+
context.AddSource(fileName, sourceText);
136114
}
115+
}
137116

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)
139123
{
140-
if (!controllerNamespaceArgument.IsNull)
124+
if (predicate(element, context))
141125
{
142-
return (string)controllerNamespaceArgument.Value;
126+
return element;
143127
}
128+
}
144129

145-
if (resourceType.ContainingNamespace.IsGlobalNamespace)
146-
{
147-
return null;
148-
}
130+
return default;
131+
}
149132

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+
}
154139

155-
return $"{resourceType.ContainingNamespace.ContainingNamespace}.Controllers";
140+
if (resourceType.ContainingNamespace.IsGlobalNamespace)
141+
{
142+
return null;
156143
}
157144

158-
private static string GetUniqueFileName(string controllerName, IDictionary<string, int> controllerNamesInUse)
145+
if (resourceType.ContainingNamespace.ContainingNamespace.IsGlobalNamespace)
159146
{
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+
}
163149

164-
if (controllerNamesInUse.TryGetValue(controllerName, out int lastIndex))
165-
{
166-
lastIndex++;
167-
controllerNamesInUse[controllerName] = lastIndex;
150+
return $"{resourceType.ContainingNamespace.ContainingNamespace}.Controllers";
151+
}
168152

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.
171158

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";
174165
}
166+
167+
controllerNamesInUse[controllerName] = 1;
168+
return $"{controllerName}.g.cs";
175169
}
176170
}

src/JsonApiDotNetCore.SourceGenerators/JsonApiDotNetCore.SourceGenerators.csproj

+1-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
66
<IncludeBuildOutput>false</IncludeBuildOutput>
77
<NoWarn>$(NoWarn);NU5128</NoWarn>
8-
<Nullable>disable</Nullable>
9-
<ImplicitUsings>disable</ImplicitUsings>
8+
<LangVersion>latest</LangVersion>
109
<IsRoslynComponent>true</IsRoslynComponent>
1110
</PropertyGroup>
1211

Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
1-
using System;
1+
namespace JsonApiDotNetCore.SourceGenerators;
22

3-
namespace JsonApiDotNetCore.SourceGenerators
3+
// IMPORTANT: A copy of this type exists in the JsonApiDotNetCore project. Keep these in sync when making changes.
4+
[Flags]
5+
public enum JsonApiEndpointsCopy
46
{
5-
// IMPORTANT: A copy of this type exists in the JsonApiDotNetCore project. Keep these in sync when making changes.
6-
[Flags]
7-
public enum JsonApiEndpointsCopy
8-
{
9-
None = 0,
10-
GetCollection = 1,
11-
GetSingle = 1 << 1,
12-
GetSecondary = 1 << 2,
13-
GetRelationship = 1 << 3,
14-
Post = 1 << 4,
15-
PostRelationship = 1 << 5,
16-
Patch = 1 << 6,
17-
PatchRelationship = 1 << 7,
18-
Delete = 1 << 8,
19-
DeleteRelationship = 1 << 9,
7+
None = 0,
8+
GetCollection = 1,
9+
GetSingle = 1 << 1,
10+
GetSecondary = 1 << 2,
11+
GetRelationship = 1 << 3,
12+
Post = 1 << 4,
13+
PostRelationship = 1 << 5,
14+
Patch = 1 << 6,
15+
PatchRelationship = 1 << 7,
16+
Delete = 1 << 8,
17+
DeleteRelationship = 1 << 9,
2018

21-
Query = GetCollection | GetSingle | GetSecondary | GetRelationship,
22-
Command = Post | PostRelationship | Patch | PatchRelationship | Delete | DeleteRelationship,
19+
Query = GetCollection | GetSingle | GetSecondary | GetRelationship,
20+
Command = Post | PostRelationship | Patch | PatchRelationship | Delete | DeleteRelationship,
2321

24-
All = Query | Command
25-
}
22+
All = Query | Command
2623
}

0 commit comments

Comments
 (0)