From 74c4bf95d5a3d6e8175d45ec4ee2fce094bba16b Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Wed, 18 Jun 2025 16:37:52 +0200 Subject: [PATCH] [jOOQ/jOOQ#18473] Slash in name results in invalid identifier being generated using KotlinGenerator This includes: - [jOOQ/jOOQ#18641] Bad file names generated in KotlinGenerator and ScalaGenerator when using special characters - [jOOQ/jOOQ#18643] KotlinGenerator produces bad code when table / column identifier disambiguation clashes with special character escaping - [jOOQ/jOOQ#18654] Compilation error in Kotlin generated code when table name contains special characters and paths are generated --- .../java/org/jooq/codegen/GenerationUtil.java | 33 +++++++++++++------ .../codegen/GeneratorStrategyWrapper.java | 25 +++++++++++--- .../java/org/jooq/codegen/JavaGenerator.java | 18 ++++------ 3 files changed, 49 insertions(+), 27 deletions(-) diff --git a/jOOQ-codegen/src/main/java/org/jooq/codegen/GenerationUtil.java b/jOOQ-codegen/src/main/java/org/jooq/codegen/GenerationUtil.java index 58b4d65112..123dfb5fea 100644 --- a/jOOQ-codegen/src/main/java/org/jooq/codegen/GenerationUtil.java +++ b/jOOQ-codegen/src/main/java/org/jooq/codegen/GenerationUtil.java @@ -395,7 +395,13 @@ class GenerationUtil { // case '}': case '<': case '>': + + // [#18641] OS forbidden characters: https://stackoverflow.com/q/1976007/521799 + case '*': + case '?': + case '|': return false; + default: return true; } @@ -475,19 +481,26 @@ class GenerationUtil { else if (language == KOTLIN && !isKotlinIdentifierPart(c)) sb.append(escape(c)); - // TODO: Should we do this for Scala as well? - else if (language == KOTLIN && !Character.isJavaIdentifierPart(c)) - return "`" + literal + "`"; - else if (language == KOTLIN && i == 0 && !Character.isJavaIdentifierStart(c)) - return "`" + literal + "`"; - - // [#10867] The $ character is not allowed in Kotlin unquoted identifiers - else if (language == KOTLIN && c == '$') - return "`" + literal + "`"; else sb.append(c); } + // TODO: Should we do this for Scala as well? + if (language == KOTLIN) { + for (int i = 0; i < sb.length(); i++) { + char c = sb.charAt(i); + + if (!Character.isJavaIdentifierPart(c)) + return "`" + sb + "`"; + else if (i == 0 && !Character.isJavaIdentifierStart(c)) + return "`" + sb + "`"; + + // [#10867] The $ character is not allowed in Kotlin unquoted identifiers + else if (c == '$') + return "`" + sb + "`"; + } + } + return sb.toString(); } @@ -499,7 +512,7 @@ class GenerationUtil { return convertToIdentifier(literal, Language.JAVA); } - private static String escape(char c) { + static String escape(char c) { if (c == ' ' || c == '-' || c == '.') return "_"; else diff --git a/jOOQ-codegen/src/main/java/org/jooq/codegen/GeneratorStrategyWrapper.java b/jOOQ-codegen/src/main/java/org/jooq/codegen/GeneratorStrategyWrapper.java index 4e3e7792be..15e868ea7c 100644 --- a/jOOQ-codegen/src/main/java/org/jooq/codegen/GeneratorStrategyWrapper.java +++ b/jOOQ-codegen/src/main/java/org/jooq/codegen/GeneratorStrategyWrapper.java @@ -51,6 +51,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.regex.Matcher; import org.jooq.Record; import org.jooq.impl.AbstractRoutine; @@ -118,18 +119,18 @@ class GeneratorStrategyWrapper extends AbstractDelegatingGeneratorStrategy { TypedElementDefinition e = (TypedElementDefinition) definition; if (identifier.equals(getJavaIdentifier(e.getContainer()))) - return identifier + "_"; + return append(identifier, "_"); // [#2781] Disambiguate collisions with the leading package name if (identifier.equals(getJavaPackageName(e.getContainer()).replaceAll("\\..*", ""))) - return identifier + "_"; + return append(identifier, "_"); } else if (definition instanceof TableDefinition) { SchemaDefinition schema = definition.getSchema(); if (identifier.equals(getJavaIdentifier(schema))) - return identifier + "_"; + return append(identifier, "_"); } // [#5557] Once more, this causes issues... @@ -137,13 +138,27 @@ class GeneratorStrategyWrapper extends AbstractDelegatingGeneratorStrategy { CatalogDefinition catalog = definition.getCatalog(); if (identifier.equals(getJavaIdentifier(catalog))) - return identifier + "_"; + return append(identifier, "_"); } identifier = overload(definition, Mode.DEFAULT, identifier); return identifier; } + static final String append(String identifier, String suffix) { + if (identifier.startsWith("`") && identifier.endsWith("`")) + return identifier.replaceFirst("^`(.*)`$", "`$1" + Matcher.quoteReplacement(suffix) + "`"); + else + return identifier + suffix; + } + + static final String prepend(String prefix, String identifier) { + if (identifier.startsWith("`") && identifier.endsWith("`")) + return identifier.replaceFirst("^`(.*)`$", "`" + Matcher.quoteReplacement(prefix) + "$1`"); + else + return prefix + identifier; + } + @Override public String getJavaSetterName(Definition definition, Mode mode) { return fixMethodName(definition, mode, delegate.getJavaSetterName(definition, mode)); @@ -171,7 +186,7 @@ class GeneratorStrategyWrapper extends AbstractDelegatingGeneratorStrategy { */ private String overload(Definition definition, Mode mode, String identifier) { if (!StringUtils.isBlank(definition.getOverload())) - identifier += getOverloadSuffix(definition, mode, definition.getOverload()); + identifier = append(identifier, getOverloadSuffix(definition, mode, definition.getOverload())); return identifier; } diff --git a/jOOQ-codegen/src/main/java/org/jooq/codegen/JavaGenerator.java b/jOOQ-codegen/src/main/java/org/jooq/codegen/JavaGenerator.java index b21e7c47ae..b29a8b1086 100644 --- a/jOOQ-codegen/src/main/java/org/jooq/codegen/JavaGenerator.java +++ b/jOOQ-codegen/src/main/java/org/jooq/codegen/JavaGenerator.java @@ -7996,9 +7996,6 @@ public class JavaGenerator extends AbstractGenerator { ? out.ref(getStrategy().getFullJavaIdentifier(foreignKey)) : out.ref(getStrategy().getFullJavaIdentifier(foreignKey), 2); - // [#13008] Prevent conflicts with the below leading underscore - final String unquotedKeyMethodName = keyMethodName.replace("`", ""); - keyMethodNames.put(keyMethodName, foreignKey); if (scala || kotlin) {} @@ -8022,8 +8019,8 @@ public class JavaGenerator extends AbstractGenerator { out.println("%s%sval %s: %s by lazy { %s(this, %s, null) }", visibility(), keyMethodOverride ? "override " : "", keyMethodName, referencedTableClassName, referencedTableClassName, keyFullId); } else { - out.println("%s%sfun %s(): %s = _%s", visibility(), keyMethodOverride ? "override " : "", keyMethodName, referencedTableClassName, unquotedKeyMethodName); - out.println("private val _%s: %s by lazy { %s(this, %s, null) }", unquotedKeyMethodName, referencedTableClassName, referencedTableClassName, keyFullId); + out.println("%s%sfun %s(): %s = %s", visibility(), keyMethodOverride ? "override " : "", keyMethodName, referencedTableClassName, GeneratorStrategyWrapper.prepend("_", keyMethodName)); + out.println("private val %s: %s by lazy { %s(this, %s, null) }", GeneratorStrategyWrapper.prepend("_", keyMethodName), referencedTableClassName, referencedTableClassName, keyFullId); } } else { @@ -8070,15 +8067,12 @@ public class JavaGenerator extends AbstractGenerator { + (generateImplicitJoinPathTableSubtypes() ? ("." + getStrategy().getJavaClassName(foreignKey.getReferencingTable(), Mode.PATH)) : "") ); - // [#13008] Prevent conflicts with the below leading underscore - final String unquotedKeyMethodName = keyMethodName.replace("`", ""); - if (scala) {} else { out.println(); if (kotlin) - out.println("private lateinit var _%s: %s", unquotedKeyMethodName, referencingTableClassName); + out.println("private lateinit var %s: %s", GeneratorStrategyWrapper.prepend("_", keyMethodName), referencingTableClassName); else out.println("private transient %s _%s;", referencingTableClassName, keyMethodName); } @@ -8093,10 +8087,10 @@ public class JavaGenerator extends AbstractGenerator { } else if (kotlin) { out.println("%s%sfun %s(): %s {", visibility(), keyMethodOverride ? "override " : "", keyMethodName, referencingTableClassName); - out.println("if (!this::_%s.isInitialized)", unquotedKeyMethodName); - out.println("_%s = %s(this, null, %s.inverseKey)", unquotedKeyMethodName, referencingTableClassName, keyFullId); + out.println("if (!this::%s.isInitialized)", GeneratorStrategyWrapper.prepend("_", keyMethodName)); + out.println("%s = %s(this, null, %s.inverseKey)", GeneratorStrategyWrapper.prepend("_", keyMethodName), referencingTableClassName, keyFullId); out.println(); - out.println("return _%s;", unquotedKeyMethodName); + out.println("return %s;", GeneratorStrategyWrapper.prepend("_", keyMethodName)); out.println("}"); if (generateImplicitJoinPathsAsKotlinProperties()) {