Skip to content

Commit 3ed87d6

Browse files
[Analyzer] Fix DataLoader generation issue (#8999)
1 parent 277dfb5 commit 3ed87d6

6 files changed

+1031
-51
lines changed

src/HotChocolate/Core/src/Types.Analyzers/Generators/DefaultLocalTypeLookup.cs

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ namespace HotChocolate.Types.Analyzers.Generators;
88

99
public sealed class DefaultLocalTypeLookup(ImmutableArray<SyntaxInfo> syntaxInfos) : ILocalTypeLookup
1010
{
11+
#if NET9_0_OR_GREATER
12+
private readonly Lock _lock = new();
13+
#else
14+
private readonly object _lock = new();
15+
#endif
1116
private Dictionary<string, List<string>>? _typeNameLookup;
1217

1318
public bool TryGetTypeName(
@@ -29,66 +34,83 @@ public bool TryGetTypeName(
2934
return true;
3035
}
3136

32-
foreach (var namespaceString in GetContainingNamespaces(resolverMethod))
37+
foreach (var namespaceString in GetPotentialNamespaces(resolverMethod))
3338
{
34-
if (typeNames.Contains($"global::{namespaceString}.{type.Name}"))
39+
var candidateName = $"global::{namespaceString}.{type.Name}";
40+
if (typeNames.Contains(candidateName))
3541
{
36-
typeDisplayName = typeNames[0];
42+
typeDisplayName = candidateName;
3743
return true;
3844
}
3945
}
4046

41-
typeDisplayName = type.Name;
47+
typeDisplayName = typeNames[0];
4248
return true;
4349
}
4450

4551
private Dictionary<string, List<string>> GetTypeNameLookup()
4652
{
4753
if (_typeNameLookup is null)
4854
{
49-
_typeNameLookup = [];
50-
foreach (var syntaxInfo in syntaxInfos)
55+
lock (_lock)
5156
{
52-
if (syntaxInfo is not DataLoaderInfo dataLoaderInfo)
57+
if (_typeNameLookup is null)
5358
{
54-
continue;
55-
}
59+
var typeNameLookup = new Dictionary<string, List<string>>();
5660

57-
if (!_typeNameLookup.TryGetValue(dataLoaderInfo.Name, out var typeNames))
58-
{
59-
typeNames = [];
60-
_typeNameLookup[dataLoaderInfo.Name] = typeNames;
61-
}
61+
foreach (var syntaxInfo in syntaxInfos)
62+
{
63+
if (syntaxInfo is not DataLoaderInfo dataLoaderInfo)
64+
{
65+
continue;
66+
}
6267

63-
typeNames.Add("global::" + dataLoaderInfo.FullName);
68+
if (!typeNameLookup.TryGetValue(dataLoaderInfo.Name, out var typeNames))
69+
{
70+
typeNames = [];
71+
typeNameLookup[dataLoaderInfo.Name] = typeNames;
72+
}
6473

65-
if (!_typeNameLookup.TryGetValue(dataLoaderInfo.InterfaceName, out typeNames))
66-
{
67-
typeNames = [];
68-
_typeNameLookup[dataLoaderInfo.InterfaceName] = typeNames;
69-
}
74+
typeNames.Add("global::" + dataLoaderInfo.FullName);
75+
76+
if (!typeNameLookup.TryGetValue(dataLoaderInfo.InterfaceName, out typeNames))
77+
{
78+
typeNames = [];
79+
typeNameLookup[dataLoaderInfo.InterfaceName] = typeNames;
80+
}
7081

71-
typeNames.Add("global::" + dataLoaderInfo.InterfaceFullName);
82+
typeNames.Add("global::" + dataLoaderInfo.InterfaceFullName);
83+
}
84+
85+
_typeNameLookup = typeNameLookup;
86+
}
7287
}
7388
}
7489

7590
return _typeNameLookup;
7691
}
7792

78-
private static IEnumerable<string> GetContainingNamespaces(IMethodSymbol methodSymbol)
93+
private static IEnumerable<string> GetPotentialNamespaces(IMethodSymbol methodSymbol)
7994
{
80-
var namespaces = new HashSet<string>();
81-
var syntaxTree = methodSymbol.DeclaringSyntaxReferences.FirstOrDefault()?.SyntaxTree;
95+
var root = methodSymbol.DeclaringSyntaxReferences.FirstOrDefault()?.SyntaxTree.GetRoot();
8296

83-
if (syntaxTree != null)
97+
if (root is not CompilationUnitSyntax compilationUnit)
8498
{
85-
var root = syntaxTree.GetRoot();
86-
var namespaceDeclarations = root.DescendantNodes().OfType<NamespaceDeclarationSyntax>();
99+
return [];
100+
}
101+
102+
var namespaces = new HashSet<string>();
87103

88-
foreach (var namespaceDeclaration in namespaceDeclarations)
104+
foreach (var member in compilationUnit.Members)
105+
{
106+
if (member is NamespaceDeclarationSyntax namespaceDeclaration)
89107
{
90108
namespaces.Add(namespaceDeclaration.Name.ToString());
91109
}
110+
else if (member is FileScopedNamespaceDeclarationSyntax fileScopedNamespace)
111+
{
112+
namespaces.Add(fileScopedNamespace.Name.ToString());
113+
}
92114
}
93115

94116
return namespaces;

src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -769,29 +769,6 @@ public static bool IsService(
769769
}
770770
}
771771

772-
// DataLoaders are always services
773-
if (parameter.Type is INamedTypeSymbol namedTypeSymbol)
774-
{
775-
foreach (var @interface in namedTypeSymbol.AllInterfaces)
776-
{
777-
if (@interface.ToDisplayString().StartsWith(WellKnownTypes.DataLoader))
778-
{
779-
return true;
780-
}
781-
}
782-
783-
// DbContext types are always services
784-
var current = namedTypeSymbol.BaseType;
785-
while (current is not null)
786-
{
787-
if (current.ToDisplayString() == WellKnownTypes.DbContext)
788-
{
789-
return true;
790-
}
791-
current = current.BaseType;
792-
}
793-
}
794-
795772
return false;
796773
}
797774

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

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,4 +318,173 @@ public static int GetTest([ID] [ID] int test)
318318
internal class Test;
319319
""").MatchMarkdownAsync();
320320
}
321+
322+
[Fact]
323+
public async Task Resolver_With_Generated_DataLoader_Parameter_MatchesSnapshot()
324+
{
325+
await TestHelper.GetGeneratedSourceSnapshot(
326+
[
327+
"""
328+
using System.Collections.Generic;
329+
using System.Threading;
330+
using System.Threading.Tasks;
331+
using GreenDonut;
332+
333+
namespace TestNamespace;
334+
335+
internal static class DataLoaders
336+
{
337+
[DataLoader]
338+
public static Task<IReadOnlyDictionary<int, Entity>> GetEntityByIdAsync(
339+
IReadOnlyList<int> entityIds,
340+
CancellationToken cancellationToken)
341+
=> default!;
342+
}
343+
344+
public class Entity
345+
{
346+
public int Id { get; set; }
347+
public string Name { get; set; } = default!;
348+
}
349+
""",
350+
"""
351+
using System.Threading;
352+
using System.Threading.Tasks;
353+
using HotChocolate.Types;
354+
355+
namespace TestNamespace;
356+
357+
[QueryType]
358+
internal static partial class Query
359+
{
360+
public static async Task<Entity?> GetEntityAsync(
361+
int id,
362+
IEntityByIdDataLoader dataLoader,
363+
CancellationToken cancellationToken)
364+
=> await dataLoader.LoadAsync(id, cancellationToken);
365+
}
366+
"""
367+
]).MatchMarkdownAsync();
368+
}
369+
370+
[Fact]
371+
public async Task Resolver_With_Generated_DataLoader_From_Different_Namespace_MatchesSnapshot()
372+
{
373+
await TestHelper.GetGeneratedSourceSnapshot(
374+
[
375+
"""
376+
using System.Collections.Generic;
377+
using System.Threading;
378+
using System.Threading.Tasks;
379+
using GreenDonut;
380+
381+
namespace TestNamespace.DataAccess;
382+
383+
internal static class DataLoaders
384+
{
385+
[DataLoader]
386+
public static Task<IReadOnlyDictionary<int, Entity>> GetEntityByIdAsync(
387+
IReadOnlyList<int> entityIds,
388+
CancellationToken cancellationToken)
389+
=> default!;
390+
}
391+
392+
public class Entity
393+
{
394+
public int Id { get; set; }
395+
public string Name { get; set; } = default!;
396+
}
397+
""",
398+
"""
399+
using System.Threading;
400+
using System.Threading.Tasks;
401+
using HotChocolate.Types;
402+
using TestNamespace.DataAccess;
403+
404+
namespace TestNamespace;
405+
406+
[QueryType]
407+
internal static partial class Query
408+
{
409+
public static async Task<Entity?> GetEntityAsync(
410+
int id,
411+
IEntityByIdDataLoader dataLoader,
412+
CancellationToken cancellationToken)
413+
=> await dataLoader.LoadAsync(id, cancellationToken);
414+
}
415+
"""
416+
]).MatchMarkdownAsync();
417+
}
418+
419+
[Fact]
420+
public async Task Resolver_With_Multiple_DataLoaders_Same_Name_Different_Namespaces_MatchesSnapshot()
421+
{
422+
await TestHelper.GetGeneratedSourceSnapshot(
423+
[
424+
"""
425+
using System.Collections.Generic;
426+
using System.Threading;
427+
using System.Threading.Tasks;
428+
using GreenDonut;
429+
430+
namespace TestNamespace.DataAccess.Entities;
431+
432+
internal static class EntityDataLoaders
433+
{
434+
[DataLoader]
435+
public static Task<IReadOnlyDictionary<int, Entity>> GetEntityByIdAsync(
436+
IReadOnlyList<int> entityIds,
437+
CancellationToken cancellationToken)
438+
=> default!;
439+
}
440+
441+
public class Entity
442+
{
443+
public int Id { get; set; }
444+
public string Name { get; set; } = default!;
445+
}
446+
""",
447+
"""
448+
using System.Collections.Generic;
449+
using System.Threading;
450+
using System.Threading.Tasks;
451+
using GreenDonut;
452+
453+
namespace TestNamespace.DataAccess.Products;
454+
455+
internal static class ProductDataLoaders
456+
{
457+
[DataLoader]
458+
public static Task<IReadOnlyDictionary<int, Product>> GetEntityByIdAsync(
459+
IReadOnlyList<int> entityIds,
460+
CancellationToken cancellationToken)
461+
=> default!;
462+
}
463+
464+
public class Product
465+
{
466+
public int Id { get; set; }
467+
public string Name { get; set; } = default!;
468+
}
469+
""",
470+
"""
471+
using System.Threading;
472+
using System.Threading.Tasks;
473+
using HotChocolate.Types;
474+
using TestNamespace.DataAccess.Entities;
475+
476+
namespace TestNamespace;
477+
478+
[QueryType]
479+
internal static partial class Query
480+
{
481+
public static async Task<Entity?> GetEntityAsync(
482+
int id,
483+
IEntityByIdDataLoader dataLoader,
484+
CancellationToken cancellationToken)
485+
=> await dataLoader.LoadAsync(id, cancellationToken);
486+
}
487+
"""
488+
]).MatchMarkdownAsync();
489+
}
321490
}

0 commit comments

Comments
 (0)