Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

File tree

7 files changed

+389
-381
lines changed

7 files changed

+389
-381
lines changed

‎JsonApiDotNetCore.sln.DotSettings‎

Lines changed: 1 addition & 0 deletions
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>
Lines changed: 115 additions & 121 deletions
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-
publicvoidInitialize(GeneratorInitializationContextcontext)
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-
varcontrollerNamesInUse=newDictionary<string,int>(StringComparer.OrdinalIgnoreCase);
63-
varwriter=newSourceCodeWriter(context,MissingIndentInTableError);
81+
TypedConstantendpointsArgument=
82+
resourceAttributeData.NamedArguments.FirstOrDefault(static pair =>pair.Key=="GenerateControllerEndpoints").Value;
6483

65-
foreach(TypeDeclarationSyntaxtypeDeclarationSyntaxinreceiver.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-
privatestaticstringGetUniqueFileName(stringcontrollerName,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‎

Lines changed: 1 addition & 2 deletions
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

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
1-
usingSystem;
1+
namespaceJsonApiDotNetCore.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)

AltStyle によって変換されたページ (->オリジナル) /