diff --git a/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/Kotlin2CodeGenTest.kt b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/Kotlin2CodeGenTest.kt index ed265da5..e5221c6f 100644 --- a/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/Kotlin2CodeGenTest.kt +++ b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/Kotlin2CodeGenTest.kt @@ -118,6 +118,48 @@ class Kotlin2CodeGenTest { assertThat(updateExpected).isFalse() } + @Test + fun generateKotlin2TypesWithUnderscoreField() { + val schema = + """ + interface MyInterface { + _: ID + } + + type MyInterfaceImpl implements MyInterface { + _: ID + } + + type Query { + impl: MyInterfaceImpl + } + """.trimIndent() + + val packageName = "com.netflix.graphql.dgs.codegen.kotlin2.underscore" + + val codeGenResult = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = packageName, + language = Language.KOTLIN, + generateKotlinNullableClasses = true, + generateKotlinClosureProjections = true, + generateClientApi = true, + ), + ).generate() + + val interfaceSpec = codeGenResult.kotlinInterfaces.first { it.name == "MyInterface" } + val interfaceType = interfaceSpec.members.filterIsInstance().first { it.name == "MyInterface" } + assertThat(interfaceType.propertySpecs.map { it.name }).contains("underscoreField_") + + val dataSpec = codeGenResult.kotlinDataTypes.first { it.name == "MyInterfaceImpl" } + val dataType = dataSpec.members.filterIsInstance().first { it.name == "MyInterfaceImpl" } + assertThat(dataType.propertySpecs.map { it.name }).contains("underscoreField_") + + assertCompilesKotlin(codeGenResult) + } + companion object { @Suppress("unused") @JvmStatic diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinConstantsGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinConstantsGenerator.kt index f1eb4814..1351dd18 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinConstantsGenerator.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinConstantsGenerator.kt @@ -213,9 +213,10 @@ class KotlinConstantsGenerator( constantsType: TypeSpec.Builder, name: String, ) { + val kotlinName = sanitizeKotlinIdentifier(name) constantsType.addProperty( PropertySpec - .builder(name.capitalized(), String::class) + .builder(kotlinName.capitalized(), String::class) .addModifiers(KModifier.CONST) .initializer(""""$name"""") .build(), diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataTypeGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataTypeGenerator.kt index 8f241a9a..d121c70b 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataTypeGenerator.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataTypeGenerator.kt @@ -68,12 +68,13 @@ class KotlinDataTypeGenerator( .filter(ReservedKeywordFilter.filterInvalidNames) .map { Field( - it.name, - typeUtils.findReturnType(it.type), - typeUtils.isNullable(it.type), - null, - it.description, - it.directives, + graphQLName = it.name, + kotlinName = sanitizeKotlinIdentifier(it.name), + type = typeUtils.findReturnType(it.type), + nullable = typeUtils.isNullable(it.type), + default = null, + description = it.description, + directives = it.directives, ) } + extensions @@ -81,12 +82,13 @@ class KotlinDataTypeGenerator( .filterSkipped() .map { Field( - it.name, - typeUtils.findReturnType(it.type), - typeUtils.isNullable(it.type), - null, - it.description, - it.directives, + graphQLName = it.name, + kotlinName = sanitizeKotlinIdentifier(it.name), + type = typeUtils.findReturnType(it.type), + nullable = typeUtils.isNullable(it.type), + default = null, + description = it.description, + directives = it.directives, ) } val interfaces = definition.implements + extensions.flatMap { it.implements } @@ -128,7 +130,8 @@ class KotlinInputTypeGenerator( ) } Field( - name = it.name, + graphQLName = it.name, + kotlinName = sanitizeKotlinIdentifier(it.name), type = typeUtils.findReturnType(it.type), nullable = it.type !is NonNullType, default = defaultValue, @@ -138,7 +141,8 @@ class KotlinInputTypeGenerator( }.plus( extensions.flatMap { it.inputValueDefinitions }.map { Field( - name = it.name, + graphQLName = it.name, + kotlinName = sanitizeKotlinIdentifier(it.name), type = typeUtils.findReturnType(it.type), nullable = it.type !is NonNullType, default = null, @@ -153,7 +157,8 @@ class KotlinInputTypeGenerator( } internal data class Field( - val name: String, + val graphQLName: String, + val kotlinName: String, val type: KtTypeName, val nullable: Boolean, val default: CodeBlock? = null, @@ -209,8 +214,8 @@ abstract class AbstractKotlinDataTypeGenerator( val parameterSpec = ParameterSpec - .builder(field.name, returnType) - .addAnnotation(jsonPropertyAnnotation(field.name)) + .builder(field.kotlinName, returnType) + .addAnnotation(jsonPropertyAnnotation(field.graphQLName)) if (field.directives.isNotEmpty()) { parameterSpec.addAnnotations(applyDirectivesKotlin(field.directives, config)) @@ -234,12 +239,12 @@ abstract class AbstractKotlinDataTypeGenerator( fields.forEach { field -> val returnType = if (field.nullable) field.type.copy(nullable = true) else field.type - val propertySpecBuilder = PropertySpec.builder(field.name, returnType) + val propertySpecBuilder = PropertySpec.builder(field.kotlinName, returnType) if (field.description != null) { propertySpecBuilder.addKdoc("%L", field.description.sanitizeKdoc()) } - propertySpecBuilder.initializer(field.name) + propertySpecBuilder.initializer(field.kotlinName) val interfaceNames = interfaces @@ -256,7 +261,7 @@ abstract class AbstractKotlinDataTypeGenerator( .map { it.name } .toSet() - if (field.name in interfaceFields) { + if (field.graphQLName in interfaceFields) { // Properties are the syntactical element that will allow us to override things, they are the spec on // which we should add the override modifier. propertySpecBuilder.addModifiers(KModifier.OVERRIDE) diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinEntitiesRepresentationTypeGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinEntitiesRepresentationTypeGenerator.kt index 317c759a..73831df0 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinEntitiesRepresentationTypeGenerator.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinEntitiesRepresentationTypeGenerator.kt @@ -65,7 +65,14 @@ class KotlinEntitiesRepresentationTypeGenerator( } var fieldsCodeGenAccumulator = CodeGenResult.EMPTY // generate representations of entity types that have @key, including the __typename field, and the key fields - val typeName = Field("__typename", STRING, false, CodeBlock.of("%S", definitionName)) + val typeName = + Field( + graphQLName = "__typename", + kotlinName = sanitizeKotlinIdentifier("__typename"), + type = STRING, + nullable = false, + default = CodeBlock.of("%S", definitionName), + ) val fieldDefinitions = fields .filter { keyFields.containsKey(it.name) } @@ -104,19 +111,26 @@ class KotlinEntitiesRepresentationTypeGenerator( } if (fieldType is ParameterizedTypeName && fieldType.rawType.simpleName == "List") { Field( - it.name, - LIST.parameterizedBy(ClassName(getPackageName(), fieldTypeRepresentationName)), - typeUtils.isNullable(it.type), + graphQLName = it.name, + kotlinName = sanitizeKotlinIdentifier(it.name), + type = LIST.parameterizedBy(ClassName(getPackageName(), fieldTypeRepresentationName)), + nullable = typeUtils.isNullable(it.type), ) } else { Field( - it.name, - ClassName(getPackageName(), fieldTypeRepresentationName), - typeUtils.isNullable(it.type), + graphQLName = it.name, + kotlinName = sanitizeKotlinIdentifier(it.name), + type = ClassName(getPackageName(), fieldTypeRepresentationName), + nullable = typeUtils.isNullable(it.type), ) } } else { - Field(it.name, typeUtils.findReturnType(it.type), typeUtils.isNullable(it.type)) + Field( + graphQLName = it.name, + kotlinName = sanitizeKotlinIdentifier(it.name), + type = typeUtils.findReturnType(it.type), + nullable = typeUtils.isNullable(it.type), + ) } } // Generate base type representation... diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinInterfaceTypeGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinInterfaceTypeGenerator.kt index 35bfae72..004495cb 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinInterfaceTypeGenerator.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinInterfaceTypeGenerator.kt @@ -64,32 +64,36 @@ class KotlinInterfaceTypeGenerator( val mergedFieldDefinitions = definition.fieldDefinitions + extensions.flatMap { it.fieldDefinitions } - mergedFieldDefinitions.filterSkipped().forEach { field -> - val returnType = typeUtils.findReturnType(field.type) - val nullableType = if (typeUtils.isNullable(field.type)) returnType.copy(nullable = true) else returnType - val propertySpec = PropertySpec.builder(field.name, nullableType) - if (field.description != null) { - propertySpec.addKdoc("%L", field.description.sanitizeKdoc()) - } + mergedFieldDefinitions + .filterSkipped() + .filter(ReservedKeywordFilter.filterInvalidNames) + .forEach { field -> + val returnType = typeUtils.findReturnType(field.type) + val nullableType = if (typeUtils.isNullable(field.type)) returnType.copy(nullable = true) else returnType + val kotlinName = sanitizeKotlinIdentifier(field.name) + val propertySpec = PropertySpec.builder(kotlinName, nullableType) + if (field.description != null) { + propertySpec.addKdoc("%L", field.description.sanitizeKdoc()) + } - if (definition.implements.isNotEmpty()) { - val superInterfaceFields = - document - .getDefinitionsOfType(InterfaceTypeDefinition::class.java) - .filter { - superInterfacesNames(definition).contains(it.name) - }.asSequence() - .flatMap { it.fieldDefinitions } - .map { it.name } - .toSet() - - if (field.name in superInterfaceFields) { - propertySpec.addModifiers(KModifier.OVERRIDE) + if (definition.implements.isNotEmpty()) { + val superInterfaceFields = + document + .getDefinitionsOfType(InterfaceTypeDefinition::class.java) + .filter { + superInterfacesNames(definition).contains(it.name) + }.asSequence() + .flatMap { it.fieldDefinitions } + .map { it.name } + .toSet() + + if (field.name in superInterfaceFields) { + propertySpec.addModifiers(KModifier.OVERRIDE) + } } - } - interfaceBuilder.addProperty(propertySpec.build()) - } + interfaceBuilder.addProperty(propertySpec.build()) + } val implementations = document diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinPoetUtils.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinPoetUtils.kt index 82d0a586..7ea4aece 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinPoetUtils.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinPoetUtils.kt @@ -46,6 +46,14 @@ import graphql.language.StringValue import graphql.language.Value import java.lang.IllegalArgumentException +private val kotlinReservedKeywordSanitizer = KotlinReservedKeywordSanitizer() + +fun sanitizeKotlinIdentifier(name: String): String = + when { + name == "_" -> "underscoreField_" + else -> kotlinReservedKeywordSanitizer.sanitize(name) + } + /** * Generate a [JsonTypeInfo] annotation, which allows for Jackson * polymorphic type handling when deserializing from JSON. diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/ReservedKeywordFilter.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/ReservedKeywordFilter.kt index 61dc5f79..41f87096 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/ReservedKeywordFilter.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/ReservedKeywordFilter.kt @@ -21,5 +21,5 @@ package com.netflix.graphql.dgs.codegen.generators.kotlin import graphql.language.NamedNode object ReservedKeywordFilter { - val filterInvalidNames: (NamedNode<*>) -> Boolean = { it.name != "_" } + val filterInvalidNames: (NamedNode<*>) -> Boolean = { true } } diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2ClientTypes.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2ClientTypes.kt index 9a6aa0b4..99b63bb3 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2ClientTypes.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2ClientTypes.kt @@ -24,6 +24,7 @@ import com.netflix.graphql.dgs.codegen.GraphQLProjection import com.netflix.graphql.dgs.codegen.filterSkipped import com.netflix.graphql.dgs.codegen.generators.kotlin.ReservedKeywordFilter import com.netflix.graphql.dgs.codegen.generators.kotlin.addOptionalGeneratedAnnotation +import com.netflix.graphql.dgs.codegen.generators.kotlin.sanitizeKotlinIdentifier import com.netflix.graphql.dgs.codegen.generators.shared.SchemaExtensionsUtils import com.netflix.graphql.dgs.codegen.generators.shared.SchemaExtensionsUtils.collectAllFieldDefinitions import com.netflix.graphql.dgs.codegen.generators.shared.excludeSchemaTypeExtension @@ -82,6 +83,8 @@ fun generateKotlin2ClientTypes( .filter(ReservedKeywordFilter.filterInvalidNames) .map { field -> + val kotlinName = sanitizeKotlinIdentifier(field.name) + val isScalar = typeLookup.isScalar(field.type) val hasArgs = field.inputValueDefinitions.isNotEmpty() @@ -90,7 +93,7 @@ fun generateKotlin2ClientTypes( isScalar && !hasArgs -> { PropertySpec .builder( - name = field.name, + name = kotlinName, type = typeName, ).getter( FunSpec @@ -104,13 +107,16 @@ fun generateKotlin2ClientTypes( // scalars with args are functions to take the args with no projection isScalar && hasArgs -> { FunSpec - .builder(field.name) + .builder(kotlinName) .addInputArgs(config, typeLookup, typeName, field.inputValueDefinitions) .returns(typeName) .addCode( field.inputValueDefinitions.let { iv -> val builder = CodeBlock.builder().add("field(%S", field.name) - iv.forEach { d -> builder.add(", %S to %N", d.name, d.name) } + iv.forEach { d -> + val argName = sanitizeKotlinIdentifier(d.name) + builder.add(", %S to %N", d.name, argName) + } builder.add(")\nreturn this").build() }, ).build() @@ -123,7 +129,7 @@ fun generateKotlin2ClientTypes( val (projectionType, projection) = projectionType(config.packageNameClient, projectionTypeName) FunSpec - .builder(field.name) + .builder(kotlinName) .addInputArgs(config, typeLookup, typeName, field.inputValueDefinitions) .addParameter( ParameterSpec @@ -142,7 +148,10 @@ fun generateKotlin2ClientTypes( field.name, projectionType, ) - iv.forEach { d -> builder.add(", %S to %N", d.name, d.name) } + iv.forEach { d -> + val argName = sanitizeKotlinIdentifier(d.name) + builder.add(", %S to %N", d.name, argName) + } builder.add(")\nreturn this").build() }, ).build() @@ -288,8 +297,9 @@ private fun FunSpec.Builder.addInputArgs( .addParameters( inputValueDefinitions.map { val returnType = typeLookup.findReturnType(config.packageNameTypes, it.type) + val kotlinName = sanitizeKotlinIdentifier(it.name) ParameterSpec - .builder(it.name, returnType) + .builder(kotlinName, returnType) .apply { if (returnType.isNullable) { defaultValue("default<%T, %T>(%S)", typeName, returnType, it.name) diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2DataTypes.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2DataTypes.kt index 31a24e5d..c7adcf46 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2DataTypes.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2DataTypes.kt @@ -30,6 +30,7 @@ import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonIgnorePropertiesAnn import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonPropertyAnnotation import com.netflix.graphql.dgs.codegen.generators.kotlin.jvmNameAnnotation import com.netflix.graphql.dgs.codegen.generators.kotlin.sanitizeKdoc +import com.netflix.graphql.dgs.codegen.generators.kotlin.sanitizeKotlinIdentifier import com.netflix.graphql.dgs.codegen.generators.kotlin.suppressInapplicableJvmNameAnnotation import com.netflix.graphql.dgs.codegen.generators.kotlin.toKtTypeName import com.netflix.graphql.dgs.codegen.generators.shared.CodeGeneratorUtils.capitalized @@ -56,6 +57,13 @@ import java.io.Serializable internal val logger: Logger = LoggerFactory.getLogger("com.netflix.graphql.dgs.codegen.generators.kotlin2") +private data class KotlinFieldInfo( + val definition: FieldDefinition, + val kotlinName: String = sanitizeKotlinIdentifier(definition.name), +) { + val graphQLName: String = definition.name +} + fun generateKotlin2DataTypes( config: CodeGenConfig, document: Document, @@ -80,7 +88,7 @@ fun generateKotlin2DataTypes( val extensionTypes = findTypeExtensions(typeDefinition.name, document.definitions) // get all fields defined on the type itself or any extension types - val fields = + val fieldDefinitions = sequenceOf(typeDefinition) .plus(extensionTypes) .flatMap { it.fieldDefinitions } @@ -88,7 +96,9 @@ fun generateKotlin2DataTypes( .filter(ReservedKeywordFilter.filterInvalidNames) .toList() - fun type(field: FieldDefinition) = typeLookup.findReturnType(config.packageNameTypes, field.type) + val fields = fieldDefinitions.map(::KotlinFieldInfo) + + fun type(field: KotlinFieldInfo) = typeLookup.findReturnType(config.packageNameTypes, field.definition.type) // get a list of fields to override val overrideFields = typeLookup.overrideFields(implementedInterfaces) @@ -103,7 +113,7 @@ fun generateKotlin2DataTypes( fields.map { field -> PropertySpec .builder( - name = "${field.name}Default", + name = "${field.kotlinName}Default", type = LambdaTypeName.get(returnType = type(field)), ).addModifiers(KModifier.PRIVATE) .initializer( @@ -111,7 +121,7 @@ fun generateKotlin2DataTypes( addStatement( "\n{ throw %T(%S) }", IllegalStateException::class, - "Field `${field.name}` was not requested", + "Field `${field.graphQLName}` was not requested", ) }, ).build() @@ -131,11 +141,11 @@ fun generateKotlin2DataTypes( fields.map { field -> PropertySpec .builder( - name = field.name, + name = field.kotlinName, type = LambdaTypeName.get(returnType = type(field)), ).addModifiers(KModifier.PRIVATE) .mutable() - .initializer("${field.name}Default") + .initializer("${field.kotlinName}Default") .build() }, ) @@ -143,11 +153,11 @@ fun generateKotlin2DataTypes( .addFunctions( fields.map { field -> FunSpec - .builder("with${field.name.capitalized()}") - .addAnnotation(jsonPropertyAnnotation(field.name)) - .addParameter(field.name, type(field)) + .builder("with${field.kotlinName.capitalized()}") + .addAnnotation(jsonPropertyAnnotation(field.graphQLName)) + .addParameter(field.kotlinName, type(field)) .addControlFlow("return this.apply") { - addStatement("this.%N = { %N }", field.name, field.name) + addStatement("this.%N = { %N }", field.kotlinName, field.kotlinName) }.returns(builderClassName) .build() }, @@ -164,7 +174,7 @@ fun generateKotlin2DataTypes( "return %T(\n", ClassName(config.packageNameTypes, typeDefinition.name), ) - fs.forEach { f -> builder.add(" %N = %N,\n", f.name, f.name) } + fs.forEach { f -> builder.add(" %N = %N,\n", f.kotlinName, f.kotlinName) } builder.add(")").build() }, ).build(), @@ -209,9 +219,9 @@ fun generateKotlin2DataTypes( fields.map { field -> ParameterSpec .builder( - name = field.name, + name = field.kotlinName, type = LambdaTypeName.get(returnType = type(field)), - ).defaultValue("${field.name}Default") + ).defaultValue("${field.kotlinName}Default") .build() }, ).build(), @@ -221,10 +231,10 @@ fun generateKotlin2DataTypes( fields.map { field -> PropertySpec .builder( - name = "__${field.name}", + name = "__${field.kotlinName}", type = LambdaTypeName.get(returnType = type(field)), ).addModifiers(KModifier.PRIVATE) - .initializer("%N", field.name) + .initializer("%N", field.kotlinName) .build() }, ) @@ -233,22 +243,22 @@ fun generateKotlin2DataTypes( fields.map { field -> PropertySpec .builder( - name = field.name, + name = field.kotlinName, type = type(field), ).apply { - if (field.description != null) { - addKdoc("%L", field.description.sanitizeKdoc()) + if (field.definition.description != null) { + addKdoc("%L", field.definition.description.sanitizeKdoc()) } }.apply { - if (field.name in overrideFields) { + if (field.graphQLName in overrideFields) { addModifiers(KModifier.OVERRIDE) addAnnotation(suppressInapplicableJvmNameAnnotation()) } - }.addAnnotation(jvmNameAnnotation(field.name)) + }.addAnnotation(jvmNameAnnotation(field.kotlinName)) .getter( FunSpec .getterBuilder() - .addStatement("return __${field.name}.invoke()") + .addStatement("return __${field.kotlinName}.invoke()") .build(), ).build() }, diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2InputTypes.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2InputTypes.kt index 6182ea2a..8280c9c5 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2InputTypes.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2InputTypes.kt @@ -26,6 +26,7 @@ import com.netflix.graphql.dgs.codegen.generators.kotlin.KotlinTypeUtils import com.netflix.graphql.dgs.codegen.generators.kotlin.ReservedKeywordFilter import com.netflix.graphql.dgs.codegen.generators.kotlin.addOptionalGeneratedAnnotation import com.netflix.graphql.dgs.codegen.generators.kotlin.sanitizeKdoc +import com.netflix.graphql.dgs.codegen.generators.kotlin.sanitizeKotlinIdentifier import com.netflix.graphql.dgs.codegen.generators.shared.SchemaExtensionsUtils.findInputExtensions import com.netflix.graphql.dgs.codegen.generators.shared.excludeSchemaTypeExtension import com.netflix.graphql.dgs.codegen.generators.shared.generateKotlinCode @@ -92,9 +93,10 @@ fun generateKotlin2InputTypes( .addParameters( fields.map { field -> val type = type(field) + val kotlinName = sanitizeKotlinIdentifier(field.name) ParameterSpec .builder( - name = field.name, + name = kotlinName, type = type, ).addAnnotation(AnnotationSpec.builder(JsonProperty::class).addMember("%S", field.name).build()) .apply { @@ -119,11 +121,12 @@ fun generateKotlin2InputTypes( // add a backing property for each field .addProperties( fields.map { field -> + val kotlinName = sanitizeKotlinIdentifier(field.name) PropertySpec .builder( - name = field.name, + name = kotlinName, type = type(field), - ).initializer(field.name) + ).initializer(kotlinName) .build() }, ).addFunction( @@ -141,10 +144,11 @@ fun generateKotlin2InputTypes( fields.let { fs -> val builder = CodeBlock.builder().add("return listOf(") fs.forEachIndexed { i, f -> + val kotlinName = sanitizeKotlinIdentifier(f.name) builder.add( "%S to %N%L", f.name, - f.name, + kotlinName, if (i < fs.size.dec() ) { diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2Interfaces.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2Interfaces.kt index 8d320519..d4804916 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2Interfaces.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2Interfaces.kt @@ -20,11 +20,13 @@ package com.netflix.graphql.dgs.codegen.generators.kotlin2 import com.netflix.graphql.dgs.codegen.CodeGenConfig import com.netflix.graphql.dgs.codegen.filterSkipped +import com.netflix.graphql.dgs.codegen.generators.kotlin.ReservedKeywordFilter import com.netflix.graphql.dgs.codegen.generators.kotlin.addOptionalGeneratedAnnotation import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonSubTypesAnnotation import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonTypeInfoAnnotation import com.netflix.graphql.dgs.codegen.generators.kotlin.jvmNameAnnotation import com.netflix.graphql.dgs.codegen.generators.kotlin.sanitizeKdoc +import com.netflix.graphql.dgs.codegen.generators.kotlin.sanitizeKotlinIdentifier import com.netflix.graphql.dgs.codegen.generators.kotlin.suppressInapplicableJvmNameAnnotation import com.netflix.graphql.dgs.codegen.generators.shared.SchemaExtensionsUtils.findInterfaceExtensions import com.netflix.graphql.dgs.codegen.generators.shared.SchemaExtensionsUtils.findUnionExtensions @@ -80,6 +82,7 @@ fun generateKotlin2Interfaces( .plus(extensionTypes) .flatMap { it.fieldDefinitions } .filterSkipped() + .filter(ReservedKeywordFilter.filterInvalidNames) // get a list of fields to override val overrideFields = typeLookup.overrideFields(implementedInterfaces) @@ -110,9 +113,10 @@ fun generateKotlin2Interfaces( // add fields, overriding if needed .addProperties( fields.map { field -> + val kotlinName = sanitizeKotlinIdentifier(field.name) PropertySpec .builder( - name = field.name, + name = kotlinName, type = typeLookup.findReturnType(config.packageNameTypes, field.type), ).apply { if (field.description != null) { @@ -123,7 +127,7 @@ fun generateKotlin2Interfaces( addModifiers(KModifier.OVERRIDE) } }.addAnnotation(suppressInapplicableJvmNameAnnotation()) - .addAnnotation(jvmNameAnnotation(field.name)) + .addAnnotation(jvmNameAnnotation(kotlinName)) .build() }, ).build() diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt index 45bfceb9..273964c7 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt @@ -197,6 +197,55 @@ class KotlinCodeGenTest { assertCompilesKotlin(dataTypes) } + @Test + fun generateTypesWithUnderscoreField() { + val schema = + """ + interface MyInterface { + _: ID + } + + type MyInterfaceImpl implements MyInterface { + _: ID + } + + type Query { + impl: MyInterfaceImpl + } + """.trimIndent() + + val codeGenResult = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = BASE_PACKAGE_NAME, + language = Language.KOTLIN, + generateDataTypes = true, + ), + ).generate() + + val interfaceSpec = codeGenResult.kotlinInterfaces.first { it.name == "MyInterface" } + val interfaceType = interfaceSpec.members.filterIsInstance().first { it.name == "MyInterface" } + assertThat(interfaceType.propertySpecs.map { it.name }).contains("underscoreField_") + + val dataSpec = codeGenResult.kotlinDataTypes.first { it.name == "MyInterfaceImpl" } + val dataType = dataSpec.members.filterIsInstance().first { it.name == "MyInterfaceImpl" } + assertThat(dataType.propertySpecs.map { it.name }).contains("underscoreField_") + + val constructorParam = dataType.primaryConstructor?.parameters?.first { it.name == "underscoreField_" } + assertThat(constructorParam).isNotNull + + val jsonPropertyAnnotation = + constructorParam!!.annotations.firstOrNull { + it.typeName == ClassName("com.fasterxml.jackson.annotation", "JsonProperty") + } + assertThat(jsonPropertyAnnotation).isNotNull + assertThat(jsonPropertyAnnotation!!.members).isNotEmpty + assertThat(jsonPropertyAnnotation.members[0].toString()).isEqualTo("\"_\"") + + assertCompilesKotlin(codeGenResult) + } + @Test fun `non nullable primitive, but with kotlinAllFieldsOptional setting`() { val schema =