Skip to content

Commit 4be2a1a

Browse files
committed
fix: sanitize Kotlin identifiers for underscore fields
1 parent bfaef13 commit 4be2a1a

File tree

12 files changed

+213
-65
lines changed

12 files changed

+213
-65
lines changed

graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/Kotlin2CodeGenTest.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,48 @@ class Kotlin2CodeGenTest {
118118
assertThat(updateExpected).isFalse()
119119
}
120120

121+
@Test
122+
fun generateKotlin2TypesWithUnderscoreField() {
123+
val schema =
124+
"""
125+
interface MyInterface {
126+
_: ID
127+
}
128+
129+
type MyInterfaceImpl implements MyInterface {
130+
_: ID
131+
}
132+
133+
type Query {
134+
impl: MyInterfaceImpl
135+
}
136+
""".trimIndent()
137+
138+
val packageName = "com.netflix.graphql.dgs.codegen.kotlin2.underscore"
139+
140+
val codeGenResult =
141+
CodeGen(
142+
CodeGenConfig(
143+
schemas = setOf(schema),
144+
packageName = packageName,
145+
language = Language.KOTLIN,
146+
generateKotlinNullableClasses = true,
147+
generateKotlinClosureProjections = true,
148+
generateClientApi = true,
149+
),
150+
).generate()
151+
152+
val interfaceSpec = codeGenResult.kotlinInterfaces.first { it.name == "MyInterface" }
153+
val interfaceType = interfaceSpec.members.filterIsInstance<com.squareup.kotlinpoet.TypeSpec>().first { it.name == "MyInterface" }
154+
assertThat(interfaceType.propertySpecs.map { it.name }).contains("__")
155+
156+
val dataSpec = codeGenResult.kotlinDataTypes.first { it.name == "MyInterfaceImpl" }
157+
val dataType = dataSpec.members.filterIsInstance<com.squareup.kotlinpoet.TypeSpec>().first { it.name == "MyInterfaceImpl" }
158+
assertThat(dataType.propertySpecs.map { it.name }).contains("__")
159+
160+
assertCompilesKotlin(codeGenResult)
161+
}
162+
121163
companion object {
122164
@Suppress("unused")
123165
@JvmStatic

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,10 @@ class KotlinConstantsGenerator(
213213
constantsType: TypeSpec.Builder,
214214
name: String,
215215
) {
216+
val kotlinName = sanitizeKotlinIdentifier(name)
216217
constantsType.addProperty(
217218
PropertySpec
218-
.builder(name.capitalized(), String::class)
219+
.builder(kotlinName.capitalized(), String::class)
219220
.addModifiers(KModifier.CONST)
220221
.initializer(""""$name"""")
221222
.build(),

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

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -68,25 +68,27 @@ class KotlinDataTypeGenerator(
6868
.filter(ReservedKeywordFilter.filterInvalidNames)
6969
.map {
7070
Field(
71-
it.name,
72-
typeUtils.findReturnType(it.type),
73-
typeUtils.isNullable(it.type),
74-
null,
75-
it.description,
76-
it.directives,
71+
graphQLName = it.name,
72+
kotlinName = sanitizeKotlinIdentifier(it.name),
73+
type = typeUtils.findReturnType(it.type),
74+
nullable = typeUtils.isNullable(it.type),
75+
default = null,
76+
description = it.description,
77+
directives = it.directives,
7778
)
7879
} +
7980
extensions
8081
.flatMap { it.fieldDefinitions }
8182
.filterSkipped()
8283
.map {
8384
Field(
84-
it.name,
85-
typeUtils.findReturnType(it.type),
86-
typeUtils.isNullable(it.type),
87-
null,
88-
it.description,
89-
it.directives,
85+
graphQLName = it.name,
86+
kotlinName = sanitizeKotlinIdentifier(it.name),
87+
type = typeUtils.findReturnType(it.type),
88+
nullable = typeUtils.isNullable(it.type),
89+
default = null,
90+
description = it.description,
91+
directives = it.directives,
9092
)
9193
}
9294
val interfaces = definition.implements + extensions.flatMap { it.implements }
@@ -128,7 +130,8 @@ class KotlinInputTypeGenerator(
128130
)
129131
}
130132
Field(
131-
name = it.name,
133+
graphQLName = it.name,
134+
kotlinName = sanitizeKotlinIdentifier(it.name),
132135
type = typeUtils.findReturnType(it.type),
133136
nullable = it.type !is NonNullType,
134137
default = defaultValue,
@@ -138,7 +141,8 @@ class KotlinInputTypeGenerator(
138141
}.plus(
139142
extensions.flatMap { it.inputValueDefinitions }.map {
140143
Field(
141-
name = it.name,
144+
graphQLName = it.name,
145+
kotlinName = sanitizeKotlinIdentifier(it.name),
142146
type = typeUtils.findReturnType(it.type),
143147
nullable = it.type !is NonNullType,
144148
default = null,
@@ -153,7 +157,8 @@ class KotlinInputTypeGenerator(
153157
}
154158

155159
internal data class Field(
156-
val name: String,
160+
val graphQLName: String,
161+
val kotlinName: String,
157162
val type: KtTypeName,
158163
val nullable: Boolean,
159164
val default: CodeBlock? = null,
@@ -209,8 +214,8 @@ abstract class AbstractKotlinDataTypeGenerator(
209214

210215
val parameterSpec =
211216
ParameterSpec
212-
.builder(field.name, returnType)
213-
.addAnnotation(jsonPropertyAnnotation(field.name))
217+
.builder(field.kotlinName, returnType)
218+
.addAnnotation(jsonPropertyAnnotation(field.graphQLName))
214219

215220
if (field.directives.isNotEmpty()) {
216221
parameterSpec.addAnnotations(applyDirectivesKotlin(field.directives, config))
@@ -234,12 +239,12 @@ abstract class AbstractKotlinDataTypeGenerator(
234239

235240
fields.forEach { field ->
236241
val returnType = if (field.nullable) field.type.copy(nullable = true) else field.type
237-
val propertySpecBuilder = PropertySpec.builder(field.name, returnType)
242+
val propertySpecBuilder = PropertySpec.builder(field.kotlinName, returnType)
238243

239244
if (field.description != null) {
240245
propertySpecBuilder.addKdoc("%L", field.description.sanitizeKdoc())
241246
}
242-
propertySpecBuilder.initializer(field.name)
247+
propertySpecBuilder.initializer(field.kotlinName)
243248

244249
val interfaceNames =
245250
interfaces
@@ -256,7 +261,7 @@ abstract class AbstractKotlinDataTypeGenerator(
256261
.map { it.name }
257262
.toSet()
258263

259-
if (field.name in interfaceFields) {
264+
if (field.graphQLName in interfaceFields) {
260265
// Properties are the syntactical element that will allow us to override things, they are the spec on
261266
// which we should add the override modifier.
262267
propertySpecBuilder.addModifiers(KModifier.OVERRIDE)

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

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,14 @@ class KotlinEntitiesRepresentationTypeGenerator(
6565
}
6666
var fieldsCodeGenAccumulator = CodeGenResult.EMPTY
6767
// generate representations of entity types that have @key, including the __typename field, and the key fields
68-
val typeName = Field("__typename", STRING, false, CodeBlock.of("%S", definitionName))
68+
val typeName =
69+
Field(
70+
graphQLName = "__typename",
71+
kotlinName = sanitizeKotlinIdentifier("__typename"),
72+
type = STRING,
73+
nullable = false,
74+
default = CodeBlock.of("%S", definitionName),
75+
)
6976
val fieldDefinitions =
7077
fields
7178
.filter { keyFields.containsKey(it.name) }
@@ -104,19 +111,26 @@ class KotlinEntitiesRepresentationTypeGenerator(
104111
}
105112
if (fieldType is ParameterizedTypeName && fieldType.rawType.simpleName == "List") {
106113
Field(
107-
it.name,
108-
LIST.parameterizedBy(ClassName(getPackageName(), fieldTypeRepresentationName)),
109-
typeUtils.isNullable(it.type),
114+
graphQLName = it.name,
115+
kotlinName = sanitizeKotlinIdentifier(it.name),
116+
type = LIST.parameterizedBy(ClassName(getPackageName(), fieldTypeRepresentationName)),
117+
nullable = typeUtils.isNullable(it.type),
110118
)
111119
} else {
112120
Field(
113-
it.name,
114-
ClassName(getPackageName(), fieldTypeRepresentationName),
115-
typeUtils.isNullable(it.type),
121+
graphQLName = it.name,
122+
kotlinName = sanitizeKotlinIdentifier(it.name),
123+
type = ClassName(getPackageName(), fieldTypeRepresentationName),
124+
nullable = typeUtils.isNullable(it.type),
116125
)
117126
}
118127
} else {
119-
Field(it.name, typeUtils.findReturnType(it.type), typeUtils.isNullable(it.type))
128+
Field(
129+
graphQLName = it.name,
130+
kotlinName = sanitizeKotlinIdentifier(it.name),
131+
type = typeUtils.findReturnType(it.type),
132+
nullable = typeUtils.isNullable(it.type),
133+
)
120134
}
121135
}
122136
// Generate base type representation...

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,14 @@ class KotlinInterfaceTypeGenerator(
6464

6565
val mergedFieldDefinitions = definition.fieldDefinitions + extensions.flatMap { it.fieldDefinitions }
6666

67-
mergedFieldDefinitions.filterSkipped().forEach { field ->
67+
mergedFieldDefinitions
68+
.filterSkipped()
69+
.filter(ReservedKeywordFilter.filterInvalidNames)
70+
.forEach { field ->
6871
val returnType = typeUtils.findReturnType(field.type)
6972
val nullableType = if (typeUtils.isNullable(field.type)) returnType.copy(nullable = true) else returnType
70-
val propertySpec = PropertySpec.builder(field.name, nullableType)
73+
val kotlinName = sanitizeKotlinIdentifier(field.name)
74+
val propertySpec = PropertySpec.builder(kotlinName, nullableType)
7175
if (field.description != null) {
7276
propertySpec.addKdoc("%L", field.description.sanitizeKdoc())
7377
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ import graphql.language.StringValue
4646
import graphql.language.Value
4747
import java.lang.IllegalArgumentException
4848

49+
private val kotlinReservedKeywordSanitizer = KotlinReservedKeywordSanitizer()
50+
51+
fun sanitizeKotlinIdentifier(name: String): String = kotlinReservedKeywordSanitizer.sanitize(name)
52+
4953
/**
5054
* Generate a [JsonTypeInfo] annotation, which allows for Jackson
5155
* polymorphic type handling when deserializing from JSON.

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ package com.netflix.graphql.dgs.codegen.generators.kotlin
2020

2121
import graphql.language.NamedNode
2222

23+
private val sanitizer = KotlinReservedKeywordSanitizer()
24+
2325
object ReservedKeywordFilter {
24-
val filterInvalidNames: (NamedNode<*>) -> Boolean = { it.name != "_" }
26+
val filterInvalidNames: (NamedNode<*>) -> Boolean = { sanitizer.sanitize(it.name).isNotBlank() }
2527
}

graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2ClientTypes.kt

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ fun generateKotlin2ClientTypes(
8282
.filter(ReservedKeywordFilter.filterInvalidNames)
8383
.map { field ->
8484

85+
val kotlinName = sanitizeKotlinIdentifier(field.name)
86+
8587
val isScalar = typeLookup.isScalar(field.type)
8688
val hasArgs = field.inputValueDefinitions.isNotEmpty()
8789

@@ -90,7 +92,7 @@ fun generateKotlin2ClientTypes(
9092
isScalar && !hasArgs -> {
9193
PropertySpec
9294
.builder(
93-
name = field.name,
95+
name = kotlinName,
9496
type = typeName,
9597
).getter(
9698
FunSpec
@@ -104,13 +106,16 @@ fun generateKotlin2ClientTypes(
104106
// scalars with args are functions to take the args with no projection
105107
isScalar && hasArgs -> {
106108
FunSpec
107-
.builder(field.name)
109+
.builder(kotlinName)
108110
.addInputArgs(config, typeLookup, typeName, field.inputValueDefinitions)
109111
.returns(typeName)
110112
.addCode(
111113
field.inputValueDefinitions.let { iv ->
112114
val builder = CodeBlock.builder().add("field(%S", field.name)
113-
iv.forEach { d -> builder.add(", %S to %N", d.name, d.name) }
115+
iv.forEach { d ->
116+
val argName = sanitizeKotlinIdentifier(d.name)
117+
builder.add(", %S to %N", d.name, argName)
118+
}
114119
builder.add(")\nreturn this").build()
115120
},
116121
).build()
@@ -123,7 +128,7 @@ fun generateKotlin2ClientTypes(
123128
val (projectionType, projection) = projectionType(config.packageNameClient, projectionTypeName)
124129

125130
FunSpec
126-
.builder(field.name)
131+
.builder(kotlinName)
127132
.addInputArgs(config, typeLookup, typeName, field.inputValueDefinitions)
128133
.addParameter(
129134
ParameterSpec
@@ -142,7 +147,10 @@ fun generateKotlin2ClientTypes(
142147
field.name,
143148
projectionType,
144149
)
145-
iv.forEach { d -> builder.add(", %S to %N", d.name, d.name) }
150+
iv.forEach { d ->
151+
val argName = sanitizeKotlinIdentifier(d.name)
152+
builder.add(", %S to %N", d.name, argName)
153+
}
146154
builder.add(")\nreturn this").build()
147155
},
148156
).build()
@@ -288,8 +296,9 @@ private fun FunSpec.Builder.addInputArgs(
288296
.addParameters(
289297
inputValueDefinitions.map {
290298
val returnType = typeLookup.findReturnType(config.packageNameTypes, it.type)
299+
val kotlinName = sanitizeKotlinIdentifier(it.name)
291300
ParameterSpec
292-
.builder(it.name, returnType)
301+
.builder(kotlinName, returnType)
293302
.apply {
294303
if (returnType.isNullable) {
295304
defaultValue("default<%T, %T>(%S)", typeName, returnType, it.name)

0 commit comments

Comments
 (0)