Skip to content

Commit ec2ca00

Browse files
committed
fix(java): preserve and escape all-underscore discriminator values
CasingConfiguration.computeName() built Java identifiers via toCamelCase(), which uses a word-splitting regex that never matches underscores. An all-underscore input like "_" therefore collapsed to an empty string before the reserved-keyword check ever ran, so JavaPoet rejected the resulting empty method name for a union discriminant of "_". Short-circuit all-underscore inputs before word-splitting so they keep their literal underscores, and add "_" to JAVA_RESERVED_KEYWORDS so it escapes to "__" like any other reserved identifier, while the serialized discriminator value stays "_". Fixes #17431
1 parent f26fb8f commit ec2ca00

3 files changed

Lines changed: 56 additions & 0 deletions

File tree

generators/java/generator-utils/src/main/java/com/fern/java/utils/CasingConfiguration.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,18 @@ public final class CasingConfiguration {
3636

3737
private static final Pattern STARTS_WITH_NUMBER = Pattern.compile("^[0-9]");
3838

39+
// splitWords() (and therefore toCamelCase/toBasicSnakeCase/toSmartSnakeCase) never matches
40+
// underscores, so an all-underscore input collapses to "" before sanitizeName ever runs.
41+
// Short-circuit that case and keep the literal underscores instead of losing them.
42+
private static final Pattern ALL_UNDERSCORES = Pattern.compile("^_+$");
43+
3944
// Match lodash words() regex: [A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|[0-9]+
4045
private static final Pattern SPLIT_WORDS_PATTERN =
4146
Pattern.compile("[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|[0-9]+");
4247

4348
// Java reserved keywords for keyword sanitization
4449
private static final Set<String> JAVA_RESERVED_KEYWORDS = Set.of(
50+
"_", // reserved since Java 9 (unused lambda param), a compile error since Java 21 (JEP 456)
4551
"abstract",
4652
"assert",
4753
"boolean",
@@ -166,6 +172,11 @@ public NameParts computeName(String inputName) {
166172
private NameParts computeNameInternal(String inputName) {
167173
String name = preprocessName(inputName);
168174

175+
if (ALL_UNDERSCORES.matcher(name).matches()) {
176+
String safeName = sanitizeName(name);
177+
return new NameParts(inputName, name, safeName, name, safeName, name, safeName, name, safeName);
178+
}
179+
169180
String camelCaseName = toCamelCase(name);
170181
String pascalCaseName = upperFirst(camelCaseName);
171182
String snakeCaseName;

generators/java/generator-utils/src/test/java/com/fern/java/utils/CasingConfigurationTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,4 +957,38 @@ private static CasingConfiguration buildConfig(boolean smartCasing, String langu
957957
root.set("casingsConfig", casingsConfig);
958958
return CasingConfiguration.fromIrJson(root);
959959
}
960+
961+
// ===== keyword sanitization: all-underscore inputs =====
962+
963+
@Nested
964+
class UnderscoreTests {
965+
966+
@Test
967+
void computeName_singleUnderscore_isEscaped() {
968+
CasingConfiguration config = buildConfig(true, "java", null);
969+
CasingConfiguration.NameParts parts = config.computeName("_");
970+
assertThat(parts.camelUnsafe).isEqualTo("_");
971+
assertThat(parts.camelSafe).isEqualTo("__");
972+
}
973+
974+
@Test
975+
void computeName_doubleUnderscore_isAlreadyValid() {
976+
CasingConfiguration config = buildConfig(true, "java", null);
977+
CasingConfiguration.NameParts parts = config.computeName("__");
978+
assertThat(parts.camelUnsafe).isEqualTo("__");
979+
assertThat(parts.camelSafe).isEqualTo("__");
980+
}
981+
982+
@Test
983+
void computeName_tripleUnderscore_isAlreadyValid() {
984+
CasingConfiguration config = buildConfig(true, "java", null);
985+
assertThat(config.computeName("___").camelSafe).isEqualTo("___");
986+
}
987+
988+
@Test
989+
void computeName_ordinaryUnderscoreSeparatedName_unaffected() {
990+
CasingConfiguration config = buildConfig(true, "java", null);
991+
assertThat(config.computeName("user_id").camelSafe).isEqualTo("userId");
992+
}
993+
}
960994
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json
2+
3+
- summary: |
4+
Fix generation failure when a discriminated union's discriminator value is a
5+
single underscore (`_`). Previously the compressed-name casing logic (used to
6+
reconstruct Java identifiers from the IR) collapsed an all-underscore input to
7+
an empty string before it could reach the reserved-keyword check, so JavaPoet
8+
rejected the resulting empty method name. All-underscore inputs are now kept
9+
intact, and `_` is escaped to `__` (matching the existing convention used for
10+
other reserved words), while the serialized discriminator value is unchanged.
11+
type: fix

0 commit comments

Comments
 (0)