Skip to content

Commit 58d2bd0

Browse files
authored
Add analyzer "Lookup Must Not Return List Type" (#9553)
1 parent dccde34 commit 58d2bd0

12 files changed

Lines changed: 1575 additions & 1 deletion

src/HotChocolate/Core/src/Types.Analyzers/Errors.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,4 +265,13 @@ public static class Errors
265265
category: "TypeSystem",
266266
DiagnosticSeverity.Warning,
267267
isEnabledByDefault: true);
268+
269+
public static readonly DiagnosticDescriptor LookupReturnsListType =
270+
new(
271+
id: "HC0114",
272+
title: "Lookup Must Not Return List Type",
273+
messageFormat: "A method or property with the [Lookup] attribute must not return a list type",
274+
category: "TypeSystem",
275+
DiagnosticSeverity.Error,
276+
isEnabledByDefault: true);
268277
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System.Collections.Immutable;
2+
using HotChocolate.Types.Analyzers.Helpers;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
using Microsoft.CodeAnalysis.CSharp.Syntax;
6+
using Microsoft.CodeAnalysis.Diagnostics;
7+
8+
namespace HotChocolate.Types.Analyzers;
9+
10+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
11+
public sealed class LookupReturnsListTypeAnalyzer : DiagnosticAnalyzer
12+
{
13+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
14+
[Errors.LookupReturnsListType];
15+
16+
public override void Initialize(AnalysisContext context)
17+
{
18+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
19+
context.EnableConcurrentExecution();
20+
context.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration);
21+
context.RegisterSyntaxNodeAction(AnalyzePropertyDeclaration, SyntaxKind.PropertyDeclaration);
22+
}
23+
24+
private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
25+
{
26+
var methodDeclaration = (MethodDeclarationSyntax)context.Node;
27+
28+
if (!HasLookupAttribute(context, methodDeclaration.AttributeLists))
29+
{
30+
return;
31+
}
32+
33+
var methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodDeclaration);
34+
if (methodSymbol is null)
35+
{
36+
return;
37+
}
38+
39+
var returnType = context.Compilation.IsTaskOrValueTask(methodSymbol.ReturnType, out var innerType)
40+
? innerType
41+
: methodSymbol.ReturnType;
42+
43+
if (!IsListType(returnType))
44+
{
45+
return;
46+
}
47+
48+
var diagnostic = Diagnostic.Create(
49+
Errors.LookupReturnsListType,
50+
methodDeclaration.ReturnType.GetLocation());
51+
52+
context.ReportDiagnostic(diagnostic);
53+
}
54+
55+
private static void AnalyzePropertyDeclaration(SyntaxNodeAnalysisContext context)
56+
{
57+
var propertyDeclaration = (PropertyDeclarationSyntax)context.Node;
58+
59+
if (!HasLookupAttribute(context, propertyDeclaration.AttributeLists))
60+
{
61+
return;
62+
}
63+
64+
var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclaration);
65+
if (propertySymbol is null)
66+
{
67+
return;
68+
}
69+
70+
var propertyType = context.Compilation.IsTaskOrValueTask(propertySymbol.Type, out var innerType)
71+
? innerType
72+
: propertySymbol.Type;
73+
74+
if (!IsListType(propertyType))
75+
{
76+
return;
77+
}
78+
79+
var diagnostic = Diagnostic.Create(
80+
Errors.LookupReturnsListType,
81+
propertyDeclaration.Type.GetLocation());
82+
83+
context.ReportDiagnostic(diagnostic);
84+
}
85+
86+
private static bool IsListType(ITypeSymbol typeSymbol)
87+
=> typeSymbol is IArrayTypeSymbol || typeSymbol.IsListType(out _);
88+
89+
private static bool HasLookupAttribute(
90+
SyntaxNodeAnalysisContext context,
91+
SyntaxList<AttributeListSyntax> attributeLists)
92+
{
93+
var semanticModel = context.SemanticModel;
94+
95+
foreach (var attributeList in attributeLists)
96+
{
97+
foreach (var attribute in attributeList.Attributes)
98+
{
99+
var symbolInfo = semanticModel.GetSymbolInfo(attribute);
100+
if (symbolInfo.Symbol is not IMethodSymbol attributeSymbol)
101+
{
102+
continue;
103+
}
104+
105+
var attributeType = attributeSymbol.ContainingType;
106+
if (attributeType.ToDisplayString() == WellKnownAttributes.LookupAttribute)
107+
{
108+
return true;
109+
}
110+
}
111+
}
112+
113+
return false;
114+
}
115+
}

src/HotChocolate/Core/src/Types.Analyzers/LookupReturnsNonNullableTypeAnalyzer.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
4040
? innerType
4141
: methodSymbol.ReturnType;
4242

43+
if (returnType is IArrayTypeSymbol || returnType.IsListType(out _))
44+
{
45+
return;
46+
}
47+
4348
if (returnType.IsNullableType())
4449
{
4550
return;
@@ -71,6 +76,11 @@ private static void AnalyzePropertyDeclaration(SyntaxNodeAnalysisContext context
7176
? innerType
7277
: propertySymbol.Type;
7378

79+
if (propertyType is IArrayTypeSymbol || propertyType.IsListType(out _))
80+
{
81+
return;
82+
}
83+
7484
if (propertyType.IsNullableType())
7585
{
7686
return;
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
namespace HotChocolate.Types;
2+
3+
public class LookupReturnsListTypeAnalyzerTests
4+
{
5+
[Fact]
6+
public async Task Method_ListReturn_RaisesError()
7+
{
8+
await TestHelper.GetGeneratedSourceSnapshot(
9+
["""
10+
#nullable enable
11+
using HotChocolate;
12+
using HotChocolate.Types;
13+
using HotChocolate.Types.Composite;
14+
using System.Collections.Generic;
15+
16+
namespace TestNamespace;
17+
18+
[QueryType]
19+
internal static partial class Query
20+
{
21+
[Lookup]
22+
public static List<User?> GetUsersById(int id) => default!;
23+
}
24+
25+
public class User
26+
{
27+
public int Id { get; set; }
28+
public string? Name { get; set; }
29+
}
30+
"""],
31+
enableAnalyzers: true).MatchMarkdownAsync();
32+
}
33+
34+
[Fact]
35+
public async Task Method_ArrayReturn_RaisesError()
36+
{
37+
await TestHelper.GetGeneratedSourceSnapshot(
38+
["""
39+
#nullable enable
40+
using HotChocolate;
41+
using HotChocolate.Types;
42+
using HotChocolate.Types.Composite;
43+
44+
namespace TestNamespace;
45+
46+
[QueryType]
47+
internal static partial class Query
48+
{
49+
[Lookup]
50+
public static User?[] GetUsersById(int id) => default!;
51+
}
52+
53+
public class User
54+
{
55+
public int Id { get; set; }
56+
public string? Name { get; set; }
57+
}
58+
"""],
59+
enableAnalyzers: true).MatchMarkdownAsync();
60+
}
61+
62+
[Fact]
63+
public async Task Method_IEnumerableReturn_RaisesError()
64+
{
65+
await TestHelper.GetGeneratedSourceSnapshot(
66+
["""
67+
#nullable enable
68+
using HotChocolate;
69+
using HotChocolate.Types;
70+
using HotChocolate.Types.Composite;
71+
using System.Collections.Generic;
72+
73+
namespace TestNamespace;
74+
75+
[QueryType]
76+
internal static partial class Query
77+
{
78+
[Lookup]
79+
public static IEnumerable<User?> GetUsersById(int id) => default!;
80+
}
81+
82+
public class User
83+
{
84+
public int Id { get; set; }
85+
public string? Name { get; set; }
86+
}
87+
"""],
88+
enableAnalyzers: true).MatchMarkdownAsync();
89+
}
90+
91+
[Fact]
92+
public async Task Method_TaskListReturn_RaisesError()
93+
{
94+
await TestHelper.GetGeneratedSourceSnapshot(
95+
["""
96+
#nullable enable
97+
using HotChocolate;
98+
using HotChocolate.Types;
99+
using HotChocolate.Types.Composite;
100+
using System.Collections.Generic;
101+
using System.Threading.Tasks;
102+
103+
namespace TestNamespace;
104+
105+
[QueryType]
106+
internal static partial class Query
107+
{
108+
[Lookup]
109+
public static Task<List<User?>> GetUsersByIdAsync(int id) => default!;
110+
}
111+
112+
public class User
113+
{
114+
public int Id { get; set; }
115+
public string? Name { get; set; }
116+
}
117+
"""],
118+
enableAnalyzers: true).MatchMarkdownAsync();
119+
}
120+
121+
[Fact]
122+
public async Task Property_ListReturn_RaisesError()
123+
{
124+
await TestHelper.GetGeneratedSourceSnapshot(
125+
["""
126+
#nullable enable
127+
using HotChocolate;
128+
using HotChocolate.Types;
129+
using HotChocolate.Types.Composite;
130+
using System.Collections.Generic;
131+
132+
namespace TestNamespace;
133+
134+
[QueryType]
135+
internal static partial class Query
136+
{
137+
[Lookup]
138+
public static List<User?> AllUsers => default!;
139+
}
140+
141+
public class User
142+
{
143+
public int Id { get; set; }
144+
public string? Name { get; set; }
145+
}
146+
"""],
147+
enableAnalyzers: true).MatchMarkdownAsync();
148+
}
149+
150+
[Fact]
151+
public async Task Method_SingleReturn_NoError()
152+
{
153+
await TestHelper.GetGeneratedSourceSnapshot(
154+
["""
155+
#nullable enable
156+
using HotChocolate;
157+
using HotChocolate.Types;
158+
using HotChocolate.Types.Composite;
159+
160+
namespace TestNamespace;
161+
162+
[QueryType]
163+
internal static partial class Query
164+
{
165+
[Lookup]
166+
public static User? GetUserById(int id) => default;
167+
}
168+
169+
public class User
170+
{
171+
public int Id { get; set; }
172+
public string? Name { get; set; }
173+
}
174+
"""],
175+
enableAnalyzers: true).MatchMarkdownAsync();
176+
}
177+
178+
[Fact]
179+
public async Task Method_NoLookupAttribute_NoError()
180+
{
181+
await TestHelper.GetGeneratedSourceSnapshot(
182+
["""
183+
#nullable enable
184+
using HotChocolate;
185+
using HotChocolate.Types;
186+
using HotChocolate.Types.Composite;
187+
using System.Collections.Generic;
188+
189+
namespace TestNamespace;
190+
191+
[QueryType]
192+
internal static partial class Query
193+
{
194+
public static List<User?> GetUsersById(int id) => default!;
195+
}
196+
197+
public class User
198+
{
199+
public int Id { get; set; }
200+
public string? Name { get; set; }
201+
}
202+
"""],
203+
enableAnalyzers: true).MatchMarkdownAsync();
204+
}
205+
}

src/HotChocolate/Core/test/Types.Analyzers.Tests/TestHelper.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,8 @@ private static Snapshot CreateSnapshot(CSharpCompilation compilation, GeneratorD
221221
new DataAttributeOrderAnalyzer(),
222222
new IdAttributeOnRecordParameterAnalyzer(),
223223
new WrongAuthorizationAttributeAnalyzer(),
224-
new LookupReturnsNonNullableTypeAnalyzer());
224+
new LookupReturnsNonNullableTypeAnalyzer(),
225+
new LookupReturnsListTypeAnalyzer());
225226

226227
var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers);
227228
var analyzerDiagnostics = compilationWithAnalyzers.GetAllDiagnosticsAsync().Result;

0 commit comments

Comments
 (0)