Skip to content

Commit

Permalink
fix: support reserved kotlin keywords (#83)
Browse files Browse the repository at this point in the history
* unit test

* implement the fix

* implement the fix

* rename unit test
  • Loading branch information
danadajian authored Jun 12, 2024
1 parent 086877e commit d173cef
Show file tree
Hide file tree
Showing 10 changed files with 189 additions and 17 deletions.
9 changes: 7 additions & 2 deletions src/definitions/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { indentMultiline } from "@graphql-codegen/visitor-plugin-common";
import { buildAnnotations } from "../annotations/build-annotations";
import { shouldExcludeTypeDefinition } from "../config/should-exclude-type-definition";
import { CodegenConfigWithDefaults } from "../config/build-config-with-defaults";
import { sanitizeName } from "../utils/sanitize-name";

export function buildEnumTypeDefinition(
node: EnumTypeDefinitionNode,
Expand All @@ -25,7 +26,7 @@ export function buildEnumTypeDefinition(
return "";
}

const enumName = node.name.value;
const enumName = sanitizeName(node.name.value);
const enumValues =
node.values?.map((valueNode) => {
return buildEnumValueDefinition(valueNode, config);
Expand All @@ -52,5 +53,9 @@ function buildEnumValueDefinition(
config,
definitionNode: node,
});
return `${annotations}${config.convert?.(node)}`;
if (!config.convert) {
throw new Error("Convert function was somehow not found in the config.");
}
const fieldName = sanitizeName(config.convert(node));
return `${annotations}${fieldName}`;
}
9 changes: 5 additions & 4 deletions src/definitions/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { indent } from "@graphql-codegen/visitor-plugin-common";
import { buildAnnotations } from "../annotations/build-annotations";
import { findTypeInResolverInterfacesConfig } from "../config/find-type-in-resolver-interfaces-config";
import { shouldGenerateFunctionsInClass } from "./object";
import { sanitizeName } from "../utils/sanitize-name";

export function buildObjectFieldDefinition({
node,
Expand Down Expand Up @@ -209,7 +210,7 @@ function buildFunctionDefinition(
typeInResolverInterfacesConfig,
config,
);
return `${modifier} ${fieldNode.name.value}${fieldArguments}`;
return `${modifier} ${sanitizeName(fieldNode.name.value)}${fieldArguments}`;
}

function buildConstructorFunctionDefinition(
Expand All @@ -229,7 +230,7 @@ function buildConstructorFunctionDefinition(
typeInResolverInterfacesConfig,
);
const fieldArguments = "";
return `${modifier} ${fieldNode.name.value}${fieldArguments}`;
return `${modifier} ${sanitizeName(fieldNode.name.value)}${fieldArguments}`;
}

function buildFieldModifier(
Expand Down Expand Up @@ -284,7 +285,7 @@ function buildFieldArguments(
const nullableSuffix = isOverrideFunction ? "?" : "? = null";
const existingFieldArguments = fieldNode.arguments?.map((arg) => {
const argMetadata = buildTypeMetadata(arg.type, schema, config);
return `${arg.name.value}: ${argMetadata.typeName}${arg.type.kind === Kind.NON_NULL_TYPE ? "" : nullableSuffix}`;
return `${sanitizeName(arg.name.value)}: ${argMetadata.typeName}${arg.type.kind === Kind.NON_NULL_TYPE ? "" : nullableSuffix}`;
});
const dataFetchingEnvironmentArgument =
"dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment";
Expand Down Expand Up @@ -323,7 +324,7 @@ function getDefaultImplementation(
(fieldNode) => !fieldNode.arguments?.length,
);
return !typeInResolverInterfacesConfig && atLeastOneFieldHasNoArguments
? fieldNode.name.value
? sanitizeName(fieldNode.name.value)
: notImplementedError;
}

Expand Down
11 changes: 6 additions & 5 deletions src/definitions/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { buildAnnotations } from "../annotations/build-annotations";
import { indent } from "@graphql-codegen/visitor-plugin-common";
import { CodegenConfigWithDefaults } from "../config/build-config-with-defaults";
import { inputTypeHasMatchingOutputType } from "../utils/input-type-has-matching-output-type";
import { sanitizeName } from "../utils/sanitize-name";

export function buildInputObjectDefinition(
node: InputObjectTypeDefinitionNode,
Expand All @@ -34,16 +35,16 @@ export function buildInputObjectDefinition(
}

const classMembers = (node.fields ?? [])
.map((arg) => {
const typeToUse = buildTypeMetadata(arg.type, schema, config);
.map((field) => {
const typeToUse = buildTypeMetadata(field.type, schema, config);
const initial = typeToUse.isNullable ? " = null" : "";

const annotations = buildAnnotations({
config,
definitionNode: arg,
definitionNode: field,
});
return `${annotations}${indent(
`val ${arg.name.value}: ${typeToUse.typeName}${
`val ${sanitizeName(field.name.value)}: ${typeToUse.typeName}${
typeToUse.isNullable ? "?" : ""
}${initial}`,
2,
Expand All @@ -58,7 +59,7 @@ export function buildInputObjectDefinition(

const inputRestrictionAnnotation =
"@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.INPUT_OBJECT])\n";
return `${annotations}${inputRestrictionAnnotation}data class ${node.name.value}(
return `${annotations}${inputRestrictionAnnotation}data class ${sanitizeName(node.name.value)}(
${classMembers}
)`;
}
3 changes: 2 additions & 1 deletion src/definitions/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { shouldExcludeTypeDefinition } from "../config/should-exclude-type-defin
import { buildInterfaceFieldDefinition } from "./field";
import { CodegenConfigWithDefaults } from "../config/build-config-with-defaults";
import { getDependentInterfaceNames } from "../utils/dependent-type-utils";
import { sanitizeName } from "../utils/sanitize-name";

export function buildInterfaceDefinition(
node: InterfaceTypeDefinitionNode,
Expand Down Expand Up @@ -46,7 +47,7 @@ export function buildInterfaceDefinition(
const interfacesToInherit = getDependentInterfaceNames(node);
const interfaceInheritance = `${interfacesToInherit.length ? ` : ${interfacesToInherit.join(", ")}` : ""}`;

return `${annotations}interface ${node.name.value}${interfaceInheritance} {
return `${annotations}interface ${sanitizeName(node.name.value)}${interfaceInheritance} {
${classMembers}
}`;
}
8 changes: 6 additions & 2 deletions src/definitions/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
import { CodegenConfigWithDefaults } from "../config/build-config-with-defaults";
import { inputTypeHasMatchingOutputType } from "../utils/input-type-has-matching-output-type";
import { findTypeInResolverInterfacesConfig } from "../config/find-type-in-resolver-interfaces-config";
import { sanitizeName } from "../utils/sanitize-name";

export function buildObjectTypeDefinition(
node: ObjectTypeDefinitionNode,
Expand All @@ -44,14 +45,17 @@ export function buildObjectTypeDefinition(
config,
definitionNode: node,
});
const name = node.name.value;
const name = sanitizeName(node.name.value);
const dependentInterfaces = getDependentInterfaceNames(node);
const dependentUnions = getDependentUnionsForType(schema, node);
const interfacesToInherit =
config.unionGeneration === "MARKER_INTERFACE"
? dependentInterfaces.concat(dependentUnions)
: dependentInterfaces;
const interfaceInheritance = `${interfacesToInherit.length ? ` : ${interfacesToInherit.join(", ")}` : ""}`;
const sanitizedInterfaceNames = interfacesToInherit.map((_interface) =>
sanitizeName(_interface),
);
const interfaceInheritance = `${interfacesToInherit.length ? ` : ${sanitizedInterfaceNames.join(", ")}` : ""}`;

const potentialMatchingInputType = schema.getType(`${name}Input`);
const typeWillBeConsolidated =
Expand Down
9 changes: 6 additions & 3 deletions src/definitions/union.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
buildAnnotations,
trimDescription,
} from "../annotations/build-annotations";
import { sanitizeName } from "../utils/sanitize-name";

export function buildUnionTypeDefinition(
node: UnionTypeDefinitionNode,
Expand All @@ -31,15 +32,17 @@ export function buildUnionTypeDefinition(
definitionNode: node,
});
if (config.unionGeneration === "MARKER_INTERFACE") {
return `${annotations}interface ${node.name.value}`;
return `${annotations}interface ${sanitizeName(node.name.value)}`;
}

const possibleTypes =
node.types?.map((type) => `${type.name.value}::class`).join(", ") || "";
node.types
?.map((type) => `${sanitizeName(type.name.value)}::class`)
.join(", ") || "";
return `${annotations}@GraphQLUnion(
name = "${node.name.value}",
possibleTypes = [${possibleTypes}],
description = "${trimDescription(node.description?.value)}"
)
annotation class ${node.name.value}`;
annotation class ${sanitizeName(node.name.value)}`;
}
53 changes: 53 additions & 0 deletions src/utils/sanitize-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
Copyright 2024 Expedia, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/**
* Sanitizes a name of a field or type if it is a reserved keyword in Kotlin.
*/
export function sanitizeName(name: string) {
return RESERVED_KEYWORDS.includes(name) ? `\`${name}\`` : name;
}

/**
* https://kotlinlang.org/docs/keyword-reference.html#hard-keywords
*/
const RESERVED_KEYWORDS = [
"as",
"break",
"class",
"continue",
"do",
"else",
"false",
"for",
"fun",
"if",
"in",
"interface",
"is",
"null",
"object",
"package",
"return",
"super",
"this",
"throw",
"true",
"try",
"typealias",
"typeof",
"val",
"var",
"when",
"while",
] as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { GraphQLKotlinCodegenConfig } from "../../../src/plugin";

export default {
namingConvention: "keep",
} satisfies GraphQLKotlinCodegenConfig;
58 changes: 58 additions & 0 deletions test/unit/should_handle_reserved_kotlin_keywords/expected.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.kotlin.generated

import com.expediagroup.graphql.generator.annotations.*

@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
data class TypeWithReservedKotlinKeywords(
val `as`: String? = null,
val `break`: String? = null,
val `is`: String? = null
)

@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
open class TypeWithReservedKotlinKeywordsAndFieldArgs(
val `typeof`: String? = null,
private val `throw`: String? = null
) {
open fun `throw`(`else`: String? = null, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = `throw`
}

@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
data class `true`(
val field: String? = null
)

@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.INPUT_OBJECT])
data class InputWithReservedKotlinKeywords(
val `continue`: String? = null,
val `class`: String? = null,
val `do`: String? = null
)

enum class EnumWithReservedKotlinKeywords {
`fun`,
`package`,
`val`;

companion object {
fun findByName(name: String, ignoreCase: Boolean = false): EnumWithReservedKotlinKeywords? = values().find { it.name.equals(name, ignoreCase = ignoreCase) }
}
}

interface InterfaceWithReservedKotlinKeywords {
val `null`: String?
val `return`: String?
val `object`: String?
}

@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
data class TypeForUnion1(
val field: String? = null
) : `this`

@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
data class TypeForUnion2(
val field: String? = null
) : `this`

interface `this`
41 changes: 41 additions & 0 deletions test/unit/should_handle_reserved_kotlin_keywords/schema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
type TypeWithReservedKotlinKeywords {
as: String
break: String
is: String
}

type TypeWithReservedKotlinKeywordsAndFieldArgs {
typeof: String
throw(else: String): String
}

type true {
field: String
}

input InputWithReservedKotlinKeywords {
continue: String
class: String
do: String
}

enum EnumWithReservedKotlinKeywords {
fun
package
val
}

interface InterfaceWithReservedKotlinKeywords {
null: String
return: String
object: String
}

type TypeForUnion1 {
field: String
}
type TypeForUnion2 {
field: String
}

union this = TypeForUnion1 | TypeForUnion2

0 comments on commit d173cef

Please sign in to comment.