Skip to content

Commit d173cef

Browse files
authored
fix: support reserved kotlin keywords (#83)
* unit test * implement the fix * implement the fix * rename unit test
1 parent 086877e commit d173cef

File tree

10 files changed

+189
-17
lines changed

10 files changed

+189
-17
lines changed

src/definitions/enum.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { indentMultiline } from "@graphql-codegen/visitor-plugin-common";
1616
import { buildAnnotations } from "../annotations/build-annotations";
1717
import { shouldExcludeTypeDefinition } from "../config/should-exclude-type-definition";
1818
import { CodegenConfigWithDefaults } from "../config/build-config-with-defaults";
19+
import { sanitizeName } from "../utils/sanitize-name";
1920

2021
export function buildEnumTypeDefinition(
2122
node: EnumTypeDefinitionNode,
@@ -25,7 +26,7 @@ export function buildEnumTypeDefinition(
2526
return "";
2627
}
2728

28-
const enumName = node.name.value;
29+
const enumName = sanitizeName(node.name.value);
2930
const enumValues =
3031
node.values?.map((valueNode) => {
3132
return buildEnumValueDefinition(valueNode, config);
@@ -52,5 +53,9 @@ function buildEnumValueDefinition(
5253
config,
5354
definitionNode: node,
5455
});
55-
return `${annotations}${config.convert?.(node)}`;
56+
if (!config.convert) {
57+
throw new Error("Convert function was somehow not found in the config.");
58+
}
59+
const fieldName = sanitizeName(config.convert(node));
60+
return `${annotations}${fieldName}`;
5661
}

src/definitions/field.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { indent } from "@graphql-codegen/visitor-plugin-common";
2525
import { buildAnnotations } from "../annotations/build-annotations";
2626
import { findTypeInResolverInterfacesConfig } from "../config/find-type-in-resolver-interfaces-config";
2727
import { shouldGenerateFunctionsInClass } from "./object";
28+
import { sanitizeName } from "../utils/sanitize-name";
2829

2930
export function buildObjectFieldDefinition({
3031
node,
@@ -209,7 +210,7 @@ function buildFunctionDefinition(
209210
typeInResolverInterfacesConfig,
210211
config,
211212
);
212-
return `${modifier} ${fieldNode.name.value}${fieldArguments}`;
213+
return `${modifier} ${sanitizeName(fieldNode.name.value)}${fieldArguments}`;
213214
}
214215

215216
function buildConstructorFunctionDefinition(
@@ -229,7 +230,7 @@ function buildConstructorFunctionDefinition(
229230
typeInResolverInterfacesConfig,
230231
);
231232
const fieldArguments = "";
232-
return `${modifier} ${fieldNode.name.value}${fieldArguments}`;
233+
return `${modifier} ${sanitizeName(fieldNode.name.value)}${fieldArguments}`;
233234
}
234235

235236
function buildFieldModifier(
@@ -284,7 +285,7 @@ function buildFieldArguments(
284285
const nullableSuffix = isOverrideFunction ? "?" : "? = null";
285286
const existingFieldArguments = fieldNode.arguments?.map((arg) => {
286287
const argMetadata = buildTypeMetadata(arg.type, schema, config);
287-
return `${arg.name.value}: ${argMetadata.typeName}${arg.type.kind === Kind.NON_NULL_TYPE ? "" : nullableSuffix}`;
288+
return `${sanitizeName(arg.name.value)}: ${argMetadata.typeName}${arg.type.kind === Kind.NON_NULL_TYPE ? "" : nullableSuffix}`;
288289
});
289290
const dataFetchingEnvironmentArgument =
290291
"dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment";
@@ -323,7 +324,7 @@ function getDefaultImplementation(
323324
(fieldNode) => !fieldNode.arguments?.length,
324325
);
325326
return !typeInResolverInterfacesConfig && atLeastOneFieldHasNoArguments
326-
? fieldNode.name.value
327+
? sanitizeName(fieldNode.name.value)
327328
: notImplementedError;
328329
}
329330

src/definitions/input.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { buildAnnotations } from "../annotations/build-annotations";
1818
import { indent } from "@graphql-codegen/visitor-plugin-common";
1919
import { CodegenConfigWithDefaults } from "../config/build-config-with-defaults";
2020
import { inputTypeHasMatchingOutputType } from "../utils/input-type-has-matching-output-type";
21+
import { sanitizeName } from "../utils/sanitize-name";
2122

2223
export function buildInputObjectDefinition(
2324
node: InputObjectTypeDefinitionNode,
@@ -34,16 +35,16 @@ export function buildInputObjectDefinition(
3435
}
3536

3637
const classMembers = (node.fields ?? [])
37-
.map((arg) => {
38-
const typeToUse = buildTypeMetadata(arg.type, schema, config);
38+
.map((field) => {
39+
const typeToUse = buildTypeMetadata(field.type, schema, config);
3940
const initial = typeToUse.isNullable ? " = null" : "";
4041

4142
const annotations = buildAnnotations({
4243
config,
43-
definitionNode: arg,
44+
definitionNode: field,
4445
});
4546
return `${annotations}${indent(
46-
`val ${arg.name.value}: ${typeToUse.typeName}${
47+
`val ${sanitizeName(field.name.value)}: ${typeToUse.typeName}${
4748
typeToUse.isNullable ? "?" : ""
4849
}${initial}`,
4950
2,
@@ -58,7 +59,7 @@ export function buildInputObjectDefinition(
5859

5960
const inputRestrictionAnnotation =
6061
"@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.INPUT_OBJECT])\n";
61-
return `${annotations}${inputRestrictionAnnotation}data class ${node.name.value}(
62+
return `${annotations}${inputRestrictionAnnotation}data class ${sanitizeName(node.name.value)}(
6263
${classMembers}
6364
)`;
6465
}

src/definitions/interface.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { shouldExcludeTypeDefinition } from "../config/should-exclude-type-defin
1717
import { buildInterfaceFieldDefinition } from "./field";
1818
import { CodegenConfigWithDefaults } from "../config/build-config-with-defaults";
1919
import { getDependentInterfaceNames } from "../utils/dependent-type-utils";
20+
import { sanitizeName } from "../utils/sanitize-name";
2021

2122
export function buildInterfaceDefinition(
2223
node: InterfaceTypeDefinitionNode,
@@ -46,7 +47,7 @@ export function buildInterfaceDefinition(
4647
const interfacesToInherit = getDependentInterfaceNames(node);
4748
const interfaceInheritance = `${interfacesToInherit.length ? ` : ${interfacesToInherit.join(", ")}` : ""}`;
4849

49-
return `${annotations}interface ${node.name.value}${interfaceInheritance} {
50+
return `${annotations}interface ${sanitizeName(node.name.value)}${interfaceInheritance} {
5051
${classMembers}
5152
}`;
5253
}

src/definitions/object.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
import { CodegenConfigWithDefaults } from "../config/build-config-with-defaults";
3131
import { inputTypeHasMatchingOutputType } from "../utils/input-type-has-matching-output-type";
3232
import { findTypeInResolverInterfacesConfig } from "../config/find-type-in-resolver-interfaces-config";
33+
import { sanitizeName } from "../utils/sanitize-name";
3334

3435
export function buildObjectTypeDefinition(
3536
node: ObjectTypeDefinitionNode,
@@ -44,14 +45,17 @@ export function buildObjectTypeDefinition(
4445
config,
4546
definitionNode: node,
4647
});
47-
const name = node.name.value;
48+
const name = sanitizeName(node.name.value);
4849
const dependentInterfaces = getDependentInterfaceNames(node);
4950
const dependentUnions = getDependentUnionsForType(schema, node);
5051
const interfacesToInherit =
5152
config.unionGeneration === "MARKER_INTERFACE"
5253
? dependentInterfaces.concat(dependentUnions)
5354
: dependentInterfaces;
54-
const interfaceInheritance = `${interfacesToInherit.length ? ` : ${interfacesToInherit.join(", ")}` : ""}`;
55+
const sanitizedInterfaceNames = interfacesToInherit.map((_interface) =>
56+
sanitizeName(_interface),
57+
);
58+
const interfaceInheritance = `${interfacesToInherit.length ? ` : ${sanitizedInterfaceNames.join(", ")}` : ""}`;
5559

5660
const potentialMatchingInputType = schema.getType(`${name}Input`);
5761
const typeWillBeConsolidated =

src/definitions/union.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
buildAnnotations,
1919
trimDescription,
2020
} from "../annotations/build-annotations";
21+
import { sanitizeName } from "../utils/sanitize-name";
2122

2223
export function buildUnionTypeDefinition(
2324
node: UnionTypeDefinitionNode,
@@ -31,15 +32,17 @@ export function buildUnionTypeDefinition(
3132
definitionNode: node,
3233
});
3334
if (config.unionGeneration === "MARKER_INTERFACE") {
34-
return `${annotations}interface ${node.name.value}`;
35+
return `${annotations}interface ${sanitizeName(node.name.value)}`;
3536
}
3637

3738
const possibleTypes =
38-
node.types?.map((type) => `${type.name.value}::class`).join(", ") || "";
39+
node.types
40+
?.map((type) => `${sanitizeName(type.name.value)}::class`)
41+
.join(", ") || "";
3942
return `${annotations}@GraphQLUnion(
4043
name = "${node.name.value}",
4144
possibleTypes = [${possibleTypes}],
4245
description = "${trimDescription(node.description?.value)}"
4346
)
44-
annotation class ${node.name.value}`;
47+
annotation class ${sanitizeName(node.name.value)}`;
4548
}

src/utils/sanitize-name.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
Copyright 2024 Expedia, Inc.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
https://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
/**
15+
* Sanitizes a name of a field or type if it is a reserved keyword in Kotlin.
16+
*/
17+
export function sanitizeName(name: string) {
18+
return RESERVED_KEYWORDS.includes(name) ? `\`${name}\`` : name;
19+
}
20+
21+
/**
22+
* https://kotlinlang.org/docs/keyword-reference.html#hard-keywords
23+
*/
24+
const RESERVED_KEYWORDS = [
25+
"as",
26+
"break",
27+
"class",
28+
"continue",
29+
"do",
30+
"else",
31+
"false",
32+
"for",
33+
"fun",
34+
"if",
35+
"in",
36+
"interface",
37+
"is",
38+
"null",
39+
"object",
40+
"package",
41+
"return",
42+
"super",
43+
"this",
44+
"throw",
45+
"true",
46+
"try",
47+
"typealias",
48+
"typeof",
49+
"val",
50+
"var",
51+
"when",
52+
"while",
53+
] as const;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { GraphQLKotlinCodegenConfig } from "../../../src/plugin";
2+
3+
export default {
4+
namingConvention: "keep",
5+
} satisfies GraphQLKotlinCodegenConfig;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.kotlin.generated
2+
3+
import com.expediagroup.graphql.generator.annotations.*
4+
5+
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
6+
data class TypeWithReservedKotlinKeywords(
7+
val `as`: String? = null,
8+
val `break`: String? = null,
9+
val `is`: String? = null
10+
)
11+
12+
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
13+
open class TypeWithReservedKotlinKeywordsAndFieldArgs(
14+
val `typeof`: String? = null,
15+
private val `throw`: String? = null
16+
) {
17+
open fun `throw`(`else`: String? = null, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = `throw`
18+
}
19+
20+
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
21+
data class `true`(
22+
val field: String? = null
23+
)
24+
25+
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.INPUT_OBJECT])
26+
data class InputWithReservedKotlinKeywords(
27+
val `continue`: String? = null,
28+
val `class`: String? = null,
29+
val `do`: String? = null
30+
)
31+
32+
enum class EnumWithReservedKotlinKeywords {
33+
`fun`,
34+
`package`,
35+
`val`;
36+
37+
companion object {
38+
fun findByName(name: String, ignoreCase: Boolean = false): EnumWithReservedKotlinKeywords? = values().find { it.name.equals(name, ignoreCase = ignoreCase) }
39+
}
40+
}
41+
42+
interface InterfaceWithReservedKotlinKeywords {
43+
val `null`: String?
44+
val `return`: String?
45+
val `object`: String?
46+
}
47+
48+
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
49+
data class TypeForUnion1(
50+
val field: String? = null
51+
) : `this`
52+
53+
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
54+
data class TypeForUnion2(
55+
val field: String? = null
56+
) : `this`
57+
58+
interface `this`
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
type TypeWithReservedKotlinKeywords {
2+
as: String
3+
break: String
4+
is: String
5+
}
6+
7+
type TypeWithReservedKotlinKeywordsAndFieldArgs {
8+
typeof: String
9+
throw(else: String): String
10+
}
11+
12+
type true {
13+
field: String
14+
}
15+
16+
input InputWithReservedKotlinKeywords {
17+
continue: String
18+
class: String
19+
do: String
20+
}
21+
22+
enum EnumWithReservedKotlinKeywords {
23+
fun
24+
package
25+
val
26+
}
27+
28+
interface InterfaceWithReservedKotlinKeywords {
29+
null: String
30+
return: String
31+
object: String
32+
}
33+
34+
type TypeForUnion1 {
35+
field: String
36+
}
37+
type TypeForUnion2 {
38+
field: String
39+
}
40+
41+
union this = TypeForUnion1 | TypeForUnion2

0 commit comments

Comments
 (0)