Sort modifiers according to Kotlin conventions - #641
Conversation
|
Thanks for the pull request! For transparency, we'd appreciate if you can note whether / how much you've used the help of AI coding tools for creating this PR. Thanks! |
|
I used codex extensively to help implement this change. I personally reviewed and tested all changes before submitting |
hick209
left a comment
There was a problem hiding this comment.
LGTM, but now it's up to Kotlin Foundations on whether they want to take this in or not.
There were also plenty of changes recently, so you will need a rebase.
e379d46 to
d8741f2
Compare
zarechenskiy
left a comment
There was a problem hiding this comment.
I haven't really looked into the code, only at the expect/actual results. For the code part, @AbdullinAM will review it
| setOf("override"), | ||
| setOf("lateinit"), | ||
| setOf("tailrec"), | ||
| setOf("vararg"), |
There was a problem hiding this comment.
vararg is a parameter modifier. It seems we don't need it, do we?
Or, if we also want to tackle all modifiers, then it makes sense to include noinline and other parameter modifiers as well
There was a problem hiding this comment.
Yes, I think current implementation requires having all modifiers. Currently implementation fails with a parsing error on this example:
inline fun f(@Magic // c
noinline b: () -> Unit) {}
Modifier sorter does not consider noinline as modifier and removes the line break after comment, transforming code into invalid
inline fun f(@Magic // c noinline b: () -> Unit) {}
| @Test | ||
| fun `modifier sorting preserves line separators and context receivers`() { | ||
| val code = "context(Something)\r\noverride public fun f() {}\r\n" | ||
| val expected = "context(Something)\r\npublic override fun f() {}\r\n" |
There was a problem hiding this comment.
Context receivers were replaced with context parameters, so let's use them in tests
| } | ||
|
|
||
| @Test | ||
| fun `modifier sorting preserves line separators and context receivers`() { |
There was a problem hiding this comment.
Speaking of context parameters: I believe we should put context to the first place among the modifiers. So while this test makes sense, we also have to check that something like:
internal context(other: Int)
fun something(s: String) {}
is formatted into:
context(other: Int)
internal fun something(s: String) {}
otherwise, there might be some confusion whether some modifier is applied to context or to a function (it always applies to a function)
| import org.jetbrains.kotlin.psi.psiUtil.endOffset | ||
| import org.jetbrains.kotlin.psi.psiUtil.startOffset | ||
|
|
||
| /** Sorts declaration modifiers according to the Kotlin coding conventions. */ |
There was a problem hiding this comment.
This comment doesn't add much value, let's remove it
| @Magic public final class Interleaved | ||
|
|
||
| public @Magic(1, "argument") final class InterleavedWithArgs | ||
| @Magic(1, "argument") public final class InterleavedWithArgs |
There was a problem hiding this comment.
Let's also check that we don't touch annotations in case of use-site targets, for instance:
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Anno
class F {
final internal val @receiver:Anno Int.prop get() = 4
}
Should be formatted as internal final val @receiver:Anno Int.prop get() = 4
There was a problem hiding this comment.
Thanks for the implementation! There are some technical issues, nothing too major though. But I would suggest you fix @zarechenskiy's comments first
P.s. If at any point you need help or want to pass over the PR --- I can take over and finalise it myself.
| import org.junit.jupiter.api.Assertions.assertEquals | ||
| import org.junit.jupiter.api.Test | ||
|
|
||
| class ModifierSorterTest { |
There was a problem hiding this comment.
I think we should stick to a new file-based tests format, see FormatTest for an example.
| setOf("override"), | ||
| setOf("lateinit"), | ||
| setOf("tailrec"), | ||
| setOf("vararg"), |
There was a problem hiding this comment.
Yes, I think current implementation requires having all modifiers. Currently implementation fails with a parsing error on this example:
inline fun f(@Magic // c
noinline b: () -> Unit) {}
Modifier sorter does not consider noinline as modifier and removes the line break after comment, transforming code into invalid
inline fun f(@Magic // c noinline b: () -> Unit) {}
| val followingComments: MutableList<PsiComment> = mutableListOf(), | ||
| ) | ||
|
|
||
| private sealed interface Part { |
There was a problem hiding this comment.
It feels like this abstraction is not necessary. Both subclasses are only created in one place in code and no distinction is made between them after:
val parts = mutableListOf<Part>() // line 93
parts.add(...) // lines 98, 107
val sorted = parts.map { it.text }.joinWithSpaces() // line 112
Lets remove this abstraction. Or add documentation explaining why its necessary
| flushSortableSegment() | ||
|
|
||
| val sorted = parts.map { it.text }.joinWithSpaces() | ||
| return sorted.takeIf { it != list.text } |
There was a problem hiding this comment.
One potential problem of this implementation is that we don't preserve the line breaks between modifiers. So we will trigger replacement/parsing even if the original code just contained line breaks like
public
override
fun outsideSelection() {}
There was a problem hiding this comment.
I would suggest modifying the code to detect if the modifiers were actually sorted
| } | ||
|
|
||
| if (leadingComments.isNotEmpty()) { | ||
| units.lastOrNull()?.followingComments?.addAll(leadingComments) |
There was a problem hiding this comment.
To be honest I was not able to come up with a test case that covers this condition (except for test cases with currently unsupported modifiers). If we handle all modifiers properly --- would we even need that?
| } | ||
| val sortedCode = result.toString() | ||
| return if (innermostReplacements.size == replacements.size) sortedCode | ||
| else sort(Parser.parse(sortedCode)) |
There was a problem hiding this comment.
I suspect that this sorter can be implemented in one pass (i.e. requiring only one re-parsing). I'm not 100% sure if we need it though, because it can complicate the code. So we can merge this version and improve it later
| return sortedUnits.map { it.render() }.joinWithSpaces() | ||
| } | ||
|
|
||
| private fun PsiElement.isAnnotation(): Boolean = this is KtAnnotation || this is KtAnnotationEntry |
There was a problem hiding this comment.
We can give annotations rank -1, so we don't have to always handle them separately
| } | ||
|
|
||
| val sortedUnits = | ||
| units.filter { it.isAnnotation } + units.filterNot { it.isAnnotation }.sortedBy { it.rank } |
There was a problem hiding this comment.
Having a rank for annotations will simplify this code
| } | ||
| } | ||
|
|
||
| private fun Iterable<String>.joinWithSpaces(): String = buildString { |
There was a problem hiding this comment.
There are several cases in code where you use list.map { ... }.joinWithSpaces(). I would suggest you modify this fun to take a lambda that transforms the list element into string and then you can write:
list.joinWithSpaces { ... }
This will be similar to joinToString from stdlib
Sorts declaration modifiers according to the order defined by the Kotlin coding conventions.
For example:
is formatted as:
Annotations and comments keep their relative association. Unsupported modifiers such as
noinline,crossinline,in,out, andreifiedact as ordering barriers.The cleanup applies to both full and partial formatting.
Tests:
Fixes #293