Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

kotlinpoet will fail if it returns an expression that is too long #1252

Closed
zsqw123 opened this issue May 18, 2022 · 11 comments · Fixed by #1256
Closed

kotlinpoet will fail if it returns an expression that is too long #1252

zsqw123 opened this issue May 18, 2022 · 11 comments · Fixed by #1256

Comments

@zsqw123
Copy link
Contributor

zsqw123 commented May 18, 2022

When the generated return someExpression() expression is too long (over 100), it will cause kotlinpoet to automatically insert a newline and the IDE check will pop up a [RETURN_TYPE_MISMATCH] error and the compilation will fail.

And when I tried to change generate columnLimit, I found it very difficult to change it because CodeWriter is an internal class.

image

image

My SymbolProcessor and FileSpec generation code is as follows, which simply generates a long return expression:

@AutoService(SymbolProcessorProvider::class)
class SimpleKspProcessor : SymbolProcessorProvider {
    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
        return object : SymbolProcessor {
            override fun process(resolver: Resolver): List<KSAnnotated> {
                val allSeq = resolver.getSymbolsWithAnnotation(Anno::class.java.canonicalName)
                val annotated = allSeq.firstOrNull() ?: return emptyList()
                annotated.accept(object : KSVisitorVoid() {
                    override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: Unit) {
                        val fileSpec = longReturn(function.packageName.asString())
                        runCatching {
                            val file = environment.codeGenerator.createNewFile(Dependencies.ALL_FILES, fileSpec.packageName, fileSpec.name)
                            OutputStreamWriter(file, StandardCharsets.UTF_8).use(fileSpec::writeTo)
                        }
                    }
                }, Unit)
                return listOf(annotated)
            }
        }
    }
}

fun longReturn(packageName: String): FileSpec {
    val myLongClass = "Qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm"
    val simpleClass = TypeSpec.classBuilder("KspGen")
    simpleClass.addFunction(
        FunSpec.builder("ccc")
            .addModifiers(KModifier.PUBLIC)
            .returns(ClassName.bestGuess("org.example.$myLongClass"))
            .addStatement("val a = 888")
            .addStatement("return $myLongClass()")
            .build()
    )
    return FileSpec.builder(packageName, "KspGen")
        .addType(simpleClass.build())
        .build()
}

versions:

  • kotlin: 1.6.20
  • kotlinpoet: 1.11.0
  • ksp: 1.6.20-1.0.5

My test code is here: zsqw123/KotlinPoetIssue

@zsqw123
Copy link
Contributor Author

zsqw123 commented May 18, 2022

My current solution is by giving a temporary variable and returning it, but I think this is supposed to be a bug inside kotlinpoet:

fun ccc(): LoooooooooooooooooooooooooooooongClass {
    val a = LoooooooooooooooooooooooooooooongClass()
    return a
}

@Egorand
Copy link
Collaborator

Egorand commented May 18, 2022

KotlinPoet is designed to wrap any space in the statements you pass to it to achieve better formatting, hence the space inside "return $myLongClass()" will get replaced with a newline character if the line is too long. The trick here is to use the non-breaking space symbol, ·, to denote spaces that should never break (as that would lead to broken code). You can find more info in Spaces wrap by default!. So, to fix your issue, you'd simply replace:

.addStatement("return $myLongClass()")

with:

.addStatement("return·$myLongClass()")

Let us know if that worked so I could close this issue. Thanks!

@zsqw123
Copy link
Contributor Author

zsqw123 commented May 18, 2022

Thanks! But I think the framework should avoid such incorrect formatting internally, and I released my library without even being aware of this problem when I didn't test such long expressions, which is risky for the users of my library

@JakeWharton
Copy link
Member

The only way to avoid it would be to disable automatic wrapping entirely. Kotlin is whitespace sensitive in far too many areas and often it is not statically known whether wrapping is safe.

@zsqw123
Copy link
Contributor Author

zsqw123 commented May 18, 2022

In kotlin expressions, I think it is actually a rare case that an expression like this cannot be followed by a newline. My thought was whether this problem could be avoided by a special treatment of return.

However, if this is difficult or not convenient, then I will remember to use return· in the future.

@Egorand
Copy link
Collaborator

Egorand commented May 18, 2022

Yeah, we have at least one use case where we special-case return, so that should be doable.

We made the decision to wrap spaces by default right before 1.0 (0.x versions did not wrap by default and had a special "wrapping space" modifier), in hindsight this might be more harm than good.

@zsqw123
Copy link
Contributor Author

zsqw123 commented May 28, 2022

I have three thoughts here:

  1. Wrap the return expression in parentheses, like this:
    image
  2. Create temporary local variables, although this may affect the logic of the code and may still cause problems if the nesting is very deep because the last statement is too long
  3. Disable return expression auto format by default

@Egorand
Copy link
Collaborator

Egorand commented May 28, 2022

Option 3 is what I'd prefer - let's special-case statements that start with return and replace return with return·. We have a somewhat similar bit of logic to turn a function with body starting with return into an expression function:

private fun CodeBlock.asExpressionBody(): CodeBlock? {
val codeBlock = this.trim()
val asReturnExpressionBody = codeBlock.withoutPrefix(RETURN_EXPRESSION_BODY_PREFIX_SPACE)
?: codeBlock.withoutPrefix(RETURN_EXPRESSION_BODY_PREFIX_NBSP)
if (asReturnExpressionBody != null) {
return asReturnExpressionBody
}
if (codeBlock.withoutPrefix(THROW_EXPRESSION_BODY_PREFIX_SPACE) != null ||
codeBlock.withoutPrefix(THROW_EXPRESSION_BODY_PREFIX_NBSP) != null
) {
return codeBlock
}
return null
}

Would you be interested in sending us a PR?

@zsqw123
Copy link
Contributor Author

zsqw123 commented May 29, 2022

Another problem I found is that when I have a super long string with spaces in it, it also triggers the line break logic, which is not wanted:
image

code:

FunSpec.builder("ccc")
       .addModifiers(KModifier.PUBLIC)
       .returns(ClassName.bestGuess("org.example.$myLongClass"))
       .addStatement("val a = \"Looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong String\"")
       .addStatement("return $myLongClass()")
       .build()

https://github.com/zsqw123/KotlinPoetIssue/blob/master/ksp/src/main/java/org/example/SimpleKspProcessor.kt

Although this example may be extreme and few people write such long strings, when the code is nested deeper, the return will be indented very far back and a string newline exception may occur

@zsqw123
Copy link
Contributor Author

zsqw123 commented May 29, 2022

When I was fixing the problem above with the return expression being too long, I tried to use a long string as a test case and found that the newline appeared to not work as expected.

image

And as I continued to test, I found that string line breaks can be problematic even for non-return expressions.

@JakeWharton
Copy link
Member

Use %S for string literals.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants