Skip to content

Sort modifiers according to Kotlin conventions - #641

Open
user1223644 wants to merge 1 commit into
Kotlin:mainfrom
user1223644:Codex/issue-293-modifier-order
Open

Sort modifiers according to Kotlin conventions#641
user1223644 wants to merge 1 commit into
Kotlin:mainfrom
user1223644:Codex/issue-293-modifier-order

Conversation

@user1223644

Copy link
Copy Markdown

Sorts declaration modifiers according to the order defined by the Kotlin coding conventions.

For example:

final @Magic public class Foo

suspend override protected fun foo() {}

is formatted as:

@Magic public final class Foo

protected override suspend fun foo() {}

Annotations and comments keep their relative association. Unsupported modifiers such as noinline, crossinline, in, out, and reified act as ordering barriers.

The cleanup applies to both full and partial formatting.

Tests:

./gradlew ktfmtFormat check --no-daemon

Fixes #293

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jul 14, 2026
@user1223644
user1223644 marked this pull request as ready for review July 14, 2026 16:30
@cgrushko

Copy link
Copy Markdown
Contributor

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!

@user1223644

Copy link
Copy Markdown
Author

I used codex extensively to help implement this change. I personally reviewed and tested all changes before submitting

@hick209 hick209 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@user1223644
user1223644 force-pushed the Codex/issue-293-modifier-order branch from e379d46 to d8741f2 Compare August 23, 2026 20:31

@zarechenskiy zarechenskiy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context receivers were replaced with context parameters, so let's use them in tests

}

@Test
fun `modifier sorting preserves line separators and context receivers`() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. */

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@AbdullinAM AbdullinAM left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest modifying the code to detect if the modifiers were actually sorted

}

if (leadingComments.isNotEmpty()) {
units.lastOrNull()?.followingComments?.addAll(leadingComments)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a rank for annotations will simplify this code

}
}

private fun Iterable<String>.joinWithSpaces(): String = buildString {

@AbdullinAM AbdullinAM Aug 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

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

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sort modifiers based on Kotlin conventions

5 participants