Skip to content

Commit f19b2ec

Browse files
committed
fix: Sanitize Java keywords in generated client
Prior to this change, the generated code would have the right name for the parameter but wrong name in the creation of InputArgument. ```java InputArgument packageArg = new InputArgument("package", package, false, null); ``` Now we have a test for generated code being compilable, and we handle this case correctly.
1 parent a98e108 commit f19b2ec

File tree

2 files changed

+112
-21
lines changed

2 files changed

+112
-21
lines changed

graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/ClientApiGenerator.kt

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -214,21 +214,20 @@ class ClientApiGenerator(
214214

215215
it.inputValueDefinitions.forEach { inputValue ->
216216
val findReturnType = TypeUtils(getDatatypesPackageName(), config, document).findReturnType(inputValue.type)
217+
val sanitizedInputName = javaReservedKeywordSanitizer.sanitize(inputValue.name)
217218

218219
val deprecatedDirective = getDeprecateDirective(inputValue)
219220
val deprecationReason = deprecatedDirective?.let { it1 -> getDeprecatedReason(it1) }
220221

221222
val methodBuilder =
222223
MethodSpec
223-
.methodBuilder(javaReservedKeywordSanitizer.sanitize(inputValue.name))
224-
.addParameter(findReturnType, javaReservedKeywordSanitizer.sanitize(inputValue.name))
224+
.methodBuilder(sanitizedInputName)
225+
.addParameter(findReturnType, sanitizedInputName)
225226
.returns(ClassName.get("", "Builder"))
226227
.addModifiers(Modifier.PUBLIC)
227228
.addCode(
228229
"""
229-
|this.${javaReservedKeywordSanitizer.sanitize(
230-
inputValue.name,
231-
)} = ${javaReservedKeywordSanitizer.sanitize(inputValue.name)};
230+
|this.$sanitizedInputName = $sanitizedInputName;
232231
|this.fieldsSet.add("${inputValue.name}");
233232
|return this;
234233
""".trimMargin(),
@@ -237,19 +236,19 @@ class ClientApiGenerator(
237236
addDeprecationWarnings(deprecatedDirective, methodBuilder, inputValue, deprecationReason)
238237
builderClass
239238
.addMethod(methodBuilder.build())
240-
.addField(findReturnType, javaReservedKeywordSanitizer.sanitize(inputValue.name), Modifier.PRIVATE)
239+
.addField(findReturnType, sanitizedInputName, Modifier.PRIVATE)
241240

242241
val inputValueType = inputValue.type
243242
val typeForVariableDefinition = getVariableDefinitionType(inputValueType)
244243
val referenceMethodBuilder =
245244
MethodSpec
246-
.methodBuilder(javaReservedKeywordSanitizer.sanitize(inputValue.name) + "Reference")
245+
.methodBuilder(sanitizedInputName + "Reference")
247246
.addParameter(stringType, "variableRef")
248247
.returns(ClassName.get("", "Builder"))
249248
.addModifiers(Modifier.PUBLIC)
250249
.addCode(
251250
"""
252-
|this.variableReferences.put("${javaReservedKeywordSanitizer.sanitize(inputValue.name)}", variableRef);
251+
|this.variableReferences.put("$sanitizedInputName", variableRef);
253252
|this.variableDefinitions.add(graphql.language.VariableDefinition.newVariableDefinition(variableRef, $typeForVariableDefinition).build());
254253
|this.fieldsSet.add("${inputValue.name}");
255254
|return this;
@@ -259,21 +258,21 @@ class ClientApiGenerator(
259258
addDeprecationWarnings(deprecatedDirective, referenceMethodBuilder, inputValue, deprecationReason)
260259
builderClass.addMethod(referenceMethodBuilder.build())
261260

262-
constructorBuilder.addParameter(findReturnType, javaReservedKeywordSanitizer.sanitize(inputValue.name))
263-
legacyConstructorBuilder.addParameter(findReturnType, javaReservedKeywordSanitizer.sanitize(inputValue.name))
261+
constructorBuilder.addParameter(findReturnType, sanitizedInputName)
262+
legacyConstructorBuilder.addParameter(findReturnType, sanitizedInputName)
264263

265264
if (findReturnType.isPrimitive) {
266265
val code =
267266
"""
268-
|getInput().put("${inputValue.name}", ${javaReservedKeywordSanitizer.sanitize(inputValue.name)});
267+
|getInput().put("${inputValue.name}", $sanitizedInputName);
269268
""".trimMargin()
270269
constructorBuilder.addCode(code)
271270
legacyConstructorBuilder.addCode(code)
272271
} else {
273272
val code =
274273
"""
275-
|if (${javaReservedKeywordSanitizer.sanitize(inputValue.name)} != null || fieldsSet.contains("${inputValue.name}")) {
276-
| getInput().put("${inputValue.name}", ${javaReservedKeywordSanitizer.sanitize(inputValue.name)});
274+
|if ($sanitizedInputName != null || fieldsSet.contains("${inputValue.name}")) {
275+
| getInput().put("${inputValue.name}", $sanitizedInputName);
277276
|}
278277
""".trimMargin()
279278
constructorBuilder.addCode(code)
@@ -551,9 +550,10 @@ class ClientApiGenerator(
551550
|getInputArguments().computeIfAbsent("${fieldDefinition.name}", k -> new ${'$'}T<>());
552551
|${
553552
fieldDefinition.inputValueDefinitions.joinToString("\n") { input ->
553+
val sanitizedName = javaReservedKeywordSanitizer.sanitize(input.name)
554554
"""
555-
|InputArgument ${input.name}Arg = new InputArgument("${input.name}", ${input.name}, false, null);
556-
|getInputArguments().get("${fieldDefinition.name}").add(${input.name}Arg);
555+
|InputArgument ${sanitizedName}Arg = new InputArgument("${input.name}", $sanitizedName, false, null);
556+
|getInputArguments().get("${fieldDefinition.name}").add(${sanitizedName}Arg);
557557
""".trimMargin()
558558
}
559559
}
@@ -590,11 +590,12 @@ class ClientApiGenerator(
590590
|getInputArguments().computeIfAbsent("${fieldDefinition.name}", k -> new ${'$'}T<>());
591591
|${
592592
fieldDefinition.inputValueDefinitions.joinToString("\n") { input ->
593+
val sanitizedName = javaReservedKeywordSanitizer.sanitize(input.name)
593594
"""
594-
|InputArgument ${input.name}Arg = new InputArgument("${input.name}", ${input.name}Reference, true, ${getVariableDefinitionType(
595+
|InputArgument ${sanitizedName}Arg = new InputArgument("${input.name}", ${sanitizedName}Reference, true, ${getVariableDefinitionType(
595596
input.type,
596597
)});
597-
|getInputArguments().get("${fieldDefinition.name}").add(${input.name}Arg);
598+
|getInputArguments().get("${fieldDefinition.name}").add(${sanitizedName}Arg);
598599
""".trimMargin()
599600
}
600601
}
@@ -604,7 +605,8 @@ class ClientApiGenerator(
604605
).addModifiers(Modifier.PUBLIC)
605606

606607
fieldDefinition.inputValueDefinitions.forEach { input ->
607-
methodBuilder.addParameter(ParameterSpec.builder(ClassName.get(String::class.java), "${input.name}Reference").build())
608+
val sanitizedName = javaReservedKeywordSanitizer.sanitize(input.name)
609+
methodBuilder.addParameter(ParameterSpec.builder(ClassName.get(String::class.java), "${sanitizedName}Reference").build())
608610
}
609611
return javaType.addMethod(methodBuilder.build())
610612
}
@@ -934,9 +936,10 @@ class ClientApiGenerator(
934936
|getInputArguments().computeIfAbsent("${it.name}", k -> new ${'$'}T<>());
935937
|${
936938
it.inputValueDefinitions.joinToString("\n") { input ->
939+
val sanitizedName = javaReservedKeywordSanitizer.sanitize(input.name)
937940
"""
938-
|InputArgument ${input.name}Arg = new InputArgument("${input.name}", ${input.name}, false, null);
939-
|getInputArguments().get("${it.name}").add(${input.name}Arg);
941+
|InputArgument ${sanitizedName}Arg = new InputArgument("${input.name}", $sanitizedName, false, null);
942+
|getInputArguments().get("${it.name}").add(${sanitizedName}Arg);
940943
""".trimMargin()
941944
}}
942945
|return this;
@@ -945,8 +948,9 @@ class ClientApiGenerator(
945948
).addModifiers(Modifier.PUBLIC)
946949

947950
it.inputValueDefinitions.forEach { input ->
951+
val sanitizedName = javaReservedKeywordSanitizer.sanitize(input.name)
948952
methodWithInputArgumentsBuilder.addParameter(
949-
ParameterSpec.builder(typeUtils.findReturnType(input.type), input.name).build(),
953+
ParameterSpec.builder(typeUtils.findReturnType(input.type), sanitizedName).build(),
950954
)
951955
}
952956

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
*
3+
* Copyright 2025 Netflix, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
package com.netflix.graphql.dgs.codegen.clientapi
19+
20+
import com.netflix.graphql.dgs.codegen.*
21+
import org.assertj.core.api.Assertions.assertThat
22+
import org.intellij.lang.annotations.Language
23+
import org.junit.jupiter.api.Test
24+
25+
class ClientApiGenReservedKeywordTest {
26+
@Test
27+
fun `Field named package should be sanitized in generated client API`() {
28+
@Language("GraphQL")
29+
val schema =
30+
"""
31+
type Query {
32+
something: Something
33+
}
34+
35+
type Something {
36+
package: String
37+
}
38+
""".trimIndent()
39+
40+
val codeGenResult =
41+
CodeGen(
42+
CodeGenConfig(
43+
schemas = setOf(schema),
44+
packageName = "com.netflix.graphql.dgs.codegen.tests.generated",
45+
generateClientApi = true,
46+
),
47+
).generate()
48+
49+
assertThat(codeGenResult.javaDataTypes).hasSize(1)
50+
assertThat(codeGenResult.clientProjections).hasSize(1)
51+
52+
// Check that the field name is properly sanitized in the generated data type
53+
val dataType = codeGenResult.javaDataTypes[0].typeSpec()
54+
val fieldNames = dataType.fieldSpecs().map { it.name() }
55+
assertThat(fieldNames).contains("_package")
56+
assertThat(fieldNames).doesNotContain("package")
57+
}
58+
59+
@Test
60+
fun `Query field with argument named package should generate valid code`() {
61+
@Language("GraphQL")
62+
val schema =
63+
"""
64+
type Query {
65+
something(package: String): String
66+
}
67+
""".trimIndent()
68+
69+
val codeGenResult =
70+
CodeGen(
71+
CodeGenConfig(
72+
schemas = setOf(schema),
73+
packageName = "com.netflix.graphql.dgs.codegen.tests.generated",
74+
generateClientApi = true,
75+
),
76+
).generate()
77+
78+
// Verify the generated code compiles without errors
79+
assertCompilesJava(
80+
codeGenResult.clientProjections +
81+
codeGenResult.javaQueryTypes +
82+
codeGenResult.javaEnumTypes +
83+
codeGenResult.javaDataTypes +
84+
codeGenResult.javaInterfaces,
85+
)
86+
}
87+
}

0 commit comments

Comments
 (0)