Skip to content

Commit 4012f6e

Browse files
authored
Fixed issue when inferring array type. (#8927)
1 parent 5dc88e7 commit 4012f6e

File tree

3 files changed

+221
-3
lines changed

3 files changed

+221
-3
lines changed

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

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Concurrent;
22
using System.Diagnostics;
3+
using System.Diagnostics.CodeAnalysis;
34
using System.Security.Cryptography;
45
using HotChocolate.Types.Analyzers.Models;
56
using Microsoft.CodeAnalysis;
@@ -118,8 +119,31 @@ private static (string TypeStructure, string TypeDefinition, bool IsSimpleType)
118119

119120
if (underlyingType is INamedTypeSymbol namedType && IsListType(namedType))
120121
{
121-
var elementType = namedType.TypeArguments[0];
122-
var (typeStructure, typeDefinition, _) = CreateTypeKey(elementType);
122+
var listElementType = namedType.TypeArguments[0];
123+
var (typeStructure, typeDefinition, _) = CreateTypeKey(listElementType);
124+
125+
if (isNullable)
126+
{
127+
typeStructure = string.Format(
128+
"new global::{0}({1})",
129+
WellKnownTypes.ListTypeNode,
130+
typeStructure);
131+
}
132+
else
133+
{
134+
typeStructure = string.Format(
135+
"new global::{0}(new global::{1}({2}))",
136+
WellKnownTypes.NonNullTypeNode,
137+
WellKnownTypes.ListTypeNode,
138+
typeStructure);
139+
}
140+
141+
return (typeStructure, typeDefinition, false);
142+
}
143+
144+
if (IsArrayType(unwrappedType, out var arrayElementType))
145+
{
146+
var (typeStructure, typeDefinition, _) = CreateTypeKey(arrayElementType);
123147

124148
if (isNullable)
125149
{
@@ -206,6 +230,18 @@ private static bool ImplementsFieldResultInterface(INamedTypeSymbol namedType, I
206230
return false;
207231
}
208232

233+
private static bool IsArrayType(ITypeSymbol namedType, [NotNullWhen(true)] out ITypeSymbol? elementType)
234+
{
235+
if (namedType is IArrayTypeSymbol arrayType)
236+
{
237+
elementType = arrayType.ElementType;
238+
return true;
239+
}
240+
241+
elementType = null;
242+
return false;
243+
}
244+
209245
private static bool IsListType(INamedTypeSymbol namedType)
210246
{
211247
if (!namedType.IsGenericType)

src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/Product.cs

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
using System.Reflection;
2+
using HotChocolate.Language;
3+
using HotChocolate.Types.Descriptors;
4+
15
namespace HotChocolate.Types;
26

37
[InterfaceType]
@@ -18,8 +22,128 @@ public class Book : Product
1822
public required string Title { get; set; }
1923
}
2024

25+
public class Television : Product;
26+
27+
[ObjectType<Television>]
28+
public static partial class TelevisionType
29+
{
30+
public static string ArgumentWithExplicitType(
31+
[GraphQLType<NonNullType<VersionType>>]
32+
long arg)
33+
=> throw new Exception();
34+
35+
public static string NullableArgumentWithExplicitType(
36+
[GraphQLType<VersionType>] long? arg)
37+
=> throw new Exception();
38+
}
39+
2140
[QueryType]
2241
public static partial class Query
2342
{
24-
public static Product GetProduct() => new Book { Id = "1", Title = "GraphQL in Action" };
43+
public static Product GetProduct()
44+
=> new Book { Id = "1", Title = "GraphQL in Action" };
45+
46+
[UsePaging]
47+
[RewriteAfterToVersion]
48+
public static IQueryable<Product> GetProducts([GraphQLType<NonNullType<VersionType>>] long after)
49+
=> throw new Exception();
50+
51+
[RewriteArgToVersion]
52+
public static string ArgumentWithExplicitType([GraphQLType<NonNullType<VersionType>>] long arg)
53+
=> throw new Exception();
54+
55+
[RewriteArgToVersion]
56+
public static string NullableArgumentWithExplicitType([GraphQLType<VersionType>] long? arg)
57+
=> throw new Exception();
58+
59+
public static string NullableArrayArgumentRef(string[]? items)
60+
=> throw new Exception();
61+
62+
public static string ArrayArgumentRef(string[] items)
63+
=> throw new Exception();
64+
65+
public static string ArrayNullableElementArgumentRef(string?[] items)
66+
=> throw new Exception();
67+
68+
public static string NullableArrayNullableElementArgumentRef(string?[]? items)
69+
=> throw new Exception();
70+
71+
public static string NullableListArgumentRef(List<string>? items)
72+
=> throw new Exception();
73+
74+
public static string ListArgumentRef(List<string> items)
75+
=> throw new Exception();
76+
77+
public static string ListNullableElementArgumentRef(List<string?> items)
78+
=> throw new Exception();
79+
80+
public static string NullableListNullableElementArgumentRef(List<string?>? items)
81+
=> throw new Exception();
82+
}
83+
84+
public class VersionType : ScalarType<long, StringValueNode>
85+
{
86+
public VersionType() : base("Version", BindingBehavior.Explicit)
87+
{
88+
}
89+
90+
public override IValueNode ParseResult(object? resultValue)
91+
=> throw new NotImplementedException();
92+
93+
protected override long ParseLiteral(StringValueNode valueSyntax)
94+
=> throw new NotImplementedException();
95+
96+
protected override StringValueNode ParseValue(long runtimeValue)
97+
=> throw new NotImplementedException();
98+
}
99+
100+
public class Version2Type : ScalarType<long, StringValueNode>
101+
{
102+
public Version2Type() : base("Version2", BindingBehavior.Explicit)
103+
{
104+
}
105+
106+
public override IValueNode ParseResult(object? resultValue)
107+
=> throw new NotImplementedException();
108+
109+
protected override long ParseLiteral(StringValueNode valueSyntax)
110+
=> throw new NotImplementedException();
111+
112+
protected override StringValueNode ParseValue(long runtimeValue)
113+
=> throw new NotImplementedException();
114+
}
115+
116+
public sealed class RewriteArgToVersionAttribute
117+
: ObjectFieldDescriptorAttribute
118+
{
119+
protected override void OnConfigure(
120+
IDescriptorContext context,
121+
IObjectFieldDescriptor descriptor,
122+
MemberInfo? member)
123+
{
124+
descriptor
125+
.ExtendWith(static extension =>
126+
{
127+
var argument = extension.Configuration.Arguments.First(arg => arg.Name == "arg");
128+
argument.Type = extension.Context.TypeInspector.GetTypeRef(typeof(Version2Type), TypeContext.Input);
129+
});
130+
}
131+
}
132+
133+
public sealed class RewriteAfterToVersionAttribute
134+
: ObjectFieldDescriptorAttribute
135+
{
136+
protected override void OnConfigure(
137+
IDescriptorContext context,
138+
IObjectFieldDescriptor descriptor,
139+
MemberInfo? member)
140+
{
141+
descriptor
142+
.Extend()
143+
.OnBeforeCreate(static (context, field) =>
144+
{
145+
var argument = field.Arguments.First(arg => arg.Name == "after");
146+
argument.Type = context.TypeInspector.GetTypeRef(typeof(Version2Type), TypeContext.Input);
147+
});
148+
}
25149
}

src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/__snapshots__/InterfaceTests.Schema_Snapshot.snap

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,64 @@ type Book implements Product {
1313
kind: String!
1414
}
1515

16+
"Information about pagination in a connection."
17+
type PageInfo {
18+
"Indicates whether more edges exist following the set defined by the clients arguments."
19+
hasNextPage: Boolean!
20+
"Indicates whether more edges exist prior the set defined by the clients arguments."
21+
hasPreviousPage: Boolean!
22+
"When paginating backwards, the cursor to continue."
23+
startCursor: String
24+
"When paginating forwards, the cursor to continue."
25+
endCursor: String
26+
}
27+
28+
"A connection to a list of items."
29+
type ProductsConnection {
30+
"Information to aid in pagination."
31+
pageInfo: PageInfo!
32+
"A list of edges."
33+
edges: [ProductsEdge!]
34+
"A flattened list of the nodes."
35+
nodes: [Product!]
36+
}
37+
38+
"An edge in a connection."
39+
type ProductsEdge {
40+
"A cursor for use in pagination."
41+
cursor: String!
42+
"The item at the end of the edge."
43+
node: Product!
44+
}
45+
1646
type Query {
1747
product: Product!
48+
products("Returns the elements in the list that come after the specified cursor." after: Version2 "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): ProductsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10")
49+
argumentWithExplicitType(arg: Version2): String!
50+
nullableArgumentWithExplicitType(arg: Version2): String!
51+
nullableArrayArgumentRef(items: [String!]): String!
52+
arrayArgumentRef(items: [String!]!): String!
53+
arrayNullableElementArgumentRef(items: [String]!): String!
54+
nullableArrayNullableElementArgumentRef(items: [String]): String!
55+
nullableListArgumentRef(items: [String!]): String!
56+
listArgumentRef(items: [String!]!): String!
57+
listNullableElementArgumentRef(items: [String]!): String!
58+
nullableListNullableElementArgumentRef(items: [String]): String!
59+
}
60+
61+
type Television implements Product {
62+
argumentWithExplicitType(arg: Version!): String!
63+
nullableArgumentWithExplicitType(arg: Version): String!
64+
id: String!
65+
kind: String!
1866
}
67+
68+
"The purpose of the `cost` directive is to define a `weight` for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response."
69+
directive @cost("The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." weight: String!) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM | INPUT_FIELD_DEFINITION
70+
71+
"The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information."
72+
directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query." slicingArgumentDefaultValue: Int "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis that it should expect that exactly one of the defined slicing arguments is present in a query. If that is not the case (i.e., if none or multiple slicing arguments are present), the static analysis may throw an error." requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION
73+
74+
scalar Version
75+
76+
scalar Version2

0 commit comments

Comments
 (0)