Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ class ClientApiGenerator internal constructor(
private val rootType = TypeVariableName.get("ROOT")
private val typeUtils = TypeUtils(getDatatypesPackageName(), config, schemaIndex)
private val javaReservedKeywordSanitizer = JavaReservedKeywordSanitizer()
private val javaDocEscaper = JavaDocEscaper()

private fun projectionType(
name: String,
Expand Down Expand Up @@ -188,15 +189,15 @@ class ClientApiGenerator internal constructor(
.superclass(ClassName.get(GraphQLQuery::class.java))

if (it.description != null) {
javaType.addJavadoc("\$L", it.description.content.escapeJavaDocComparisons())
javaType.addJavadoc("\$L", javaDocEscaper.escape(it.description.content))
}

val deprecatedClassDirective = getDeprecateDirective(it)
if (deprecatedClassDirective != null) {
javaType.addAnnotation(Deprecated::class.java)
val deprecationReason = getDeprecatedReason(deprecatedClassDirective)
if (deprecationReason != null) {
javaType.addJavadoc("@deprecated \$L", deprecationReason.escapeJavaDocComparisons())
javaType.addJavadoc("@deprecated \$L", javaDocEscaper.escape(deprecationReason))
}
}

Expand Down Expand Up @@ -447,13 +448,13 @@ class ClientApiGenerator internal constructor(
val javaDoc = CodeBlock.builder()

if (inputValue.description != null) {
javaDoc.add("\$L", inputValue.description.content.escapeJavaDocComparisons())
javaDoc.add("\$L", javaDocEscaper.escape(inputValue.description.content))
}
if (deprecationReason != null) {
if (!javaDoc.isEmpty) {
javaDoc.add("\n\n")
}
javaDoc.add("@deprecated \$L", deprecationReason.escapeJavaDocComparisons())
javaDoc.add("@deprecated \$L", javaDocEscaper.escape(deprecationReason))
}

if (!javaDoc.isEmpty) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ abstract class BaseDataTypeGenerator internal constructor(
internal val document = schemaIndex.document
internal val typeUtils = TypeUtils(packageName, config, schemaIndex)
private val javaReservedKeywordSanitizer = JavaReservedKeywordSanitizer()
private val javaDocEscaper = JavaDocEscaper()

internal fun generate(
name: String,
Expand Down Expand Up @@ -459,7 +460,7 @@ abstract class BaseDataTypeGenerator internal constructor(
}

if (description != null) {
javaType.addJavadoc("\$L", description.content.escapeJavaDocComparisons())
javaType.addJavadoc("\$L", javaDocEscaper.escape(description.content))
}

if (directives.isNotEmpty()) {
Expand All @@ -468,7 +469,7 @@ abstract class BaseDataTypeGenerator internal constructor(
javaType.addAnnotations(annotations[SiteTarget.DEFAULT.name])
}
if (!comments.isNullOrBlank()) {
javaType.addJavadoc("\$L", comments.escapeJavaDocComparisons())
javaType.addJavadoc("\$L", javaDocEscaper.escape(comments))
}
}

Expand Down Expand Up @@ -721,13 +722,13 @@ abstract class BaseDataTypeGenerator internal constructor(
}

if (fieldDefinition.description != null) {
fieldBuilder.addJavadoc("\$L", fieldDefinition.description.content.escapeJavaDocComparisons())
fieldBuilder.addJavadoc("\$L", javaDocEscaper.escape(fieldDefinition.description.content))
}

if (fieldDefinition.directives.isNotEmpty()) {
val (annotations, comments) = applyDirectivesJava(fieldDefinition.directives, config)
if (!comments.isNullOrBlank()) {
fieldBuilder.addJavadoc("\$L", comments.escapeJavaDocComparisons())
fieldBuilder.addJavadoc("\$L", javaDocEscaper.escape(comments))
}
for ((key, value) in annotations) {
when (SiteTarget.valueOf(key)) {
Expand Down Expand Up @@ -777,7 +778,7 @@ abstract class BaseDataTypeGenerator internal constructor(
}

if (fieldDefinition.description != null) {
getterMethodBuilder.addJavadoc("\$L", fieldDefinition.description.content.escapeJavaDocComparisons())
getterMethodBuilder.addJavadoc("\$L", javaDocEscaper.escape(fieldDefinition.description.content))
}

val setterName =
Expand Down Expand Up @@ -932,7 +933,7 @@ abstract class BaseDataTypeGenerator internal constructor(
.returns(builderClassName)

if (fieldDefinition.description != null) {
methodBuilder.addJavadoc("\$L", fieldDefinition.description.content.escapeJavaDocComparisons())
methodBuilder.addJavadoc("\$L", javaDocEscaper.escape(fieldDefinition.description.content))
}

if (fieldDefinition.trackFieldSet) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class EnumTypeGenerator(
private val config: CodeGenConfig,
) {
private val javaReservedKeywordSanitizer = JavaReservedKeywordSanitizer()
private val javaDocEscaper = JavaDocEscaper()

companion object {
private val logger: Logger = LoggerFactory.getLogger(EnumTypeGenerator::class.java)
Expand All @@ -56,20 +57,20 @@ class EnumTypeGenerator(
.addOptionalGeneratedAnnotation(config)

if (definition.description != null) {
javaType.addJavadoc("\$L", definition.description.content.escapeJavaDocComparisons())
javaType.addJavadoc("\$L", javaDocEscaper.escape(definition.description.content))
}

val mergedEnumDefinitions = definition.enumValueDefinitions + extensions.flatMap { it.enumValueDefinitions }

mergedEnumDefinitions.forEach {
val typeSpec = TypeSpec.anonymousClassBuilder("")
if (it.description != null && it.description.content.isNotBlank()) {
typeSpec.addJavadoc("\$L", it.description.content.escapeJavaDocComparisons())
typeSpec.addJavadoc("\$L", javaDocEscaper.escape(it.description.content))
}
if (it.directives.isNotEmpty()) {
val (annotations, comments) = applyDirectivesJava(it.directives, config)
if (!comments.isNullOrBlank()) {
typeSpec.addJavadoc("\$L", comments.escapeJavaDocComparisons())
typeSpec.addJavadoc("\$L", javaDocEscaper.escape(comments))
}
for (entry in annotations) {
when (SiteTarget.valueOf(entry.key)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class InterfaceGenerator internal constructor(

private val document = schemaIndex.document
private val javaReservedKeywordSanitizer = JavaReservedKeywordSanitizer()
private val javaDocEscaper = JavaDocEscaper()
private val packageName = config.packageNameTypes
private val typeUtils = TypeUtils(packageName, config, schemaIndex)
private val useInterfaceType = config.generateInterfaces
Expand All @@ -70,7 +71,7 @@ class InterfaceGenerator internal constructor(
}

if (definition.description != null) {
javaType.addJavadoc("\$L", definition.description.content.escapeJavaDocComparisons())
javaType.addJavadoc("\$L", javaDocEscaper.escape(definition.description.content))
}

definition.implements
Expand Down Expand Up @@ -175,7 +176,7 @@ class InterfaceGenerator internal constructor(
).addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
.returns(returnType)
if (fieldDefinition.description != null) {
getterBuilder.addJavadoc("\$L", fieldDefinition.description.content.escapeJavaDocComparisons())
getterBuilder.addJavadoc("\$L", javaDocEscaper.escape(fieldDefinition.description.content))
}

// Add @Nullable annotation for nullable return types when JSpecify is enabled
Expand Down Expand Up @@ -209,7 +210,7 @@ class InterfaceGenerator internal constructor(
.addParameter(parameterBuilder.build())

if (fieldDefinition.description != null) {
setterBuilder.addJavadoc("\$L", fieldDefinition.description.content.escapeJavaDocComparisons())
setterBuilder.addJavadoc("\$L", javaDocEscaper.escape(fieldDefinition.description.content))
}
javaType.addMethod(setterBuilder.build())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,25 +321,41 @@ private fun typeClassBestGuess(name: String): TypeName =
else -> ClassName.bestGuess(name)
}

/** Reuse descriptions across emission sites without retaining schemas beyond their generator. */
internal class JavaDocEscaper {
private val escapedDescriptions = mutableMapOf<String, String>()

fun escape(description: String): String = escapedDescriptions.getOrPut(description) { description.escapeJavaDocComparisons() }
}

/** Preserve Javadoc markup while escaping prose that would break doclint or end the Java comment. */
internal fun String.escapeJavaDocComparisons(): String =
buildString {
var offset = 0
while (offset < this@escapeJavaDocComparisons.length) {
val end = this@escapeJavaDocComparisons.javaDocMarkupEnd(offset)
if (end > offset) {
append(this@escapeJavaDocComparisons.substring(offset, end).escapeInlineJavaDocSourceEscapes())
offset = end
} else {
val character = this@escapeJavaDocComparisons[offset++]
when {
character == '<' -> append("&lt;")
character == '&' && bareJavaDocAmpersand.matchesAt(this@escapeJavaDocComparisons, offset - 1) -> append("&amp;")
else -> append(character)
internal fun String.escapeJavaDocComparisons(): String {
if (none { it == '<' || it == '&' || it == '\\' } && !contains("*/")) return this

val escaped =
buildString(length) {
var offset = 0
while (offset < this@escapeJavaDocComparisons.length) {
val end =
when (this@escapeJavaDocComparisons[offset]) {
'<', '{' -> this@escapeJavaDocComparisons.javaDocMarkupEnd(offset)
else -> -1
}
if (end > offset) {
append(this@escapeJavaDocComparisons.substring(offset, end).escapeInlineJavaDocSourceEscapes())
offset = end
} else {
val character = this@escapeJavaDocComparisons[offset++]
when {
character == '<' -> append("&lt;")
character == '&' && bareJavaDocAmpersand.matchesAt(this@escapeJavaDocComparisons, offset - 1) -> append("&amp;")
else -> append(character)
}
}
}
}
}.replace("*/", "*&#47;").replace(javaDocUnicodeEscape) { "${it.groupValues[1]}&#92;" }
}.replace("*/", "*&#47;")
return if ('\\' in escaped) escaped.replace(javaDocUnicodeEscape) { "${it.groupValues[1]}&#92;" } else escaped
}

private val bareJavaDocAmpersand = Regex("&(?!([a-zA-Z][a-zA-Z0-9]*|#[0-9]+|#[xX][0-9a-fA-F]+);)")

Expand All @@ -349,7 +365,7 @@ private val javaDocUnicodeEscape = Regex("""(?<!\\)((?:\\\\)*)\\(?=u)""")
private fun String.escapeInlineJavaDocSourceEscapes(): String {
val code = startsWith("{@code")
if (!code && !startsWith("{@literal")) return this
if (!contains("*/") && !javaDocUnicodeEscape.containsMatchIn(this)) return this
if (!contains("*/") && ('\\' !in this || !javaDocUnicodeEscape.containsMatchIn(this))) return this

// Entities inside inline literals render verbatim, so use equivalent HTML when Java source needs escaping.
val content =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ class JavaDocRegressionTest {
@JvmStatic
fun descriptionExamples(): List<Arguments> =
listOf(
"Plain text with ${'$'}value and a/b" to "Plain text with ${'$'}value and a/b",
"Use **bold** or *emphasis*" to "Use **bold** or *emphasis*",
"{@code plain text} {@literal {value}}" to "{@code plain text} {@literal {value}}",
"created_at:<now - created_at:<=2024" to "created_at:&lt;now - created_at:&lt;=2024",
"created_at:<now and created_at:>2024" to "created_at:&lt;now and created_at:>2024",
"x <y then z > 3" to "x &lt;y then z > 3",
Expand Down
Loading