Last active
May 3, 2025 08:33
-
-
Save KovshefulCoder/34cba5e880641f0a334a059e158f654e to your computer and use it in GitHub Desktop.
Custom Spotless step that allow to split Modifier's functions chain calls into lines (like ij_kotlin_method_call_chain_wrap = split_into_lines in .editorconfig)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Placeholder to fix gist name |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Features: | |
- Split single line Modifier functions chain calls into lines (like ij_kotlin_method_call_chain_wrap = split_into_lines in .editorconfig) | |
- Do not affect single call of function, like 'Modifier.padding(4.dp),' | |
- Do not affect Modifier extension function declarations | |
- Add IDE formatting disabling comments around formatted line to disable further collisions | |
- Move only the functions of Modifier to new lines, keeping the instance on which they were called in place: | |
'modifier = Modifier.fillMaxWidth().padding(4.dp)' formatted to | |
'modifier = Modifier | |
.fillMaxWidth() | |
.padding(4.dp)' | |
NOT | |
'modifier = | |
Modifier.fillMaxWidth() | |
.padding(4.dp)' | |
Known issues: | |
- Unproperly handles lines, that end with opening parenthesis of Modifier's function call like this - | |
'modifier = Modifier.fillMaxWidth().padding('. Logs warning about this issue with affected line |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
spotless_verision = "6.25.0" # Pump to 7+ version after fix https://github.com/diffplug/spotless/issues/2387 | |
[libraries] | |
gradleplugin-spotless = { module = "com.diffplug.spotless:com.diffplug.spotless.gradle.plugin", version.ref = "spotless_verision" } | |
[plugins] | |
spotless = { id = "com.diffplug.spotless", version.ref = "spotless_verision" } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.slf4j.LoggerFactory | |
plugins.applyIfNeeded(libs.plugins.spotless.get().pluginId) | |
// slf4j - logging facade used by Spotless Gradle plugin | |
private val log = LoggerFactory.getLogger("spotless") | |
// Warning | |
// This plugin depends on version 6.25.0 of the 'com.diffplug.spotless' plugin. | |
// It is incompatible with version 7.0.0 and above due to configuration cache issue - | |
// https://github.com/diffplug/spotless/issues/2387 | |
// Known issues - unproperly handles lines, that end with opening parenthesis of Modifier's function call like this - | |
// modifier = Modifier.fillMaxWidth().padding( | |
// Logs warning about this issue with affected line | |
spotlessConfig { | |
kotlin { | |
target("**/*.kt") | |
// Exclude all Kotlin files located anywhere inside a build directory or its subdirectories | |
targetExclude("**/build/**/*.kt") | |
custom("ComposeModifierChainSplitIntoLines") { text -> | |
// Skip the file if it contains no Compose functions or no Modifier usages. | |
// A Modifier is considered used when one of its functions is called for it, | |
// for example Modifier.padding() | |
if (!(text.contains("@Composable") && modifierPatterns.any { text.contains(it) })) { | |
return@custom text | |
} | |
text.lines().joinToString("\n") { line -> | |
// Find modifier chains in a line that need formatting, then split them into separate lines | |
val modifierPattern = modifierPatterns.firstOrNull { pattern -> | |
line.contains(pattern) && isLineContainsModifierChainForFormatting( | |
line = line, | |
modifierPattern = pattern.removeSuffix("."), | |
) | |
} ?: run { | |
// Skip the line if it has no modifiers or no modifier chains that need formatting | |
return@joinToString line | |
} | |
// Compute the indentation level for new lines when splitting modifier chains. | |
val lineLeadingWhitespacesLength = line.takeWhile { it.isWhitespace() }.length | |
val chainLeadingWhitespacesLength = lineLeadingWhitespacesLength + indentSize | |
// Use the pattern without a leading dot, since it is only used to detect chain calls | |
val formatterLine = line | |
.addIDEFormattingDisable(lineLeadingWhitespacesLength) | |
.formatModifier( | |
modifierPattern = modifierPattern.removeSuffix("."), | |
indent = chainLeadingWhitespacesLength, | |
isLineHasModifierAssignment = modifierAssignmentRegexMap.values.any { regex -> | |
regex.containsMatchIn(line) | |
} | |
) | |
if (line.endsWith('(')) { | |
log.warn( | |
"Spotless WARNING: Line below may be incorrect:\n$formatterLine\n" + | |
"Please, verify it manually." | |
) | |
} | |
formatterLine | |
} | |
} | |
} | |
} | |
// Indentation size (4 spaces) as specified in .editorconfig | |
private val indentSize: Int = 4 | |
// Formatter enable/disable tags as specified in .editorconfig | |
private val editorConfigFormatterOffTagComment = "// @formatter:off" | |
private val editorConfigFormatterOnTagComment = "// @formatter:on" | |
private val modifierPatterns = setOf("Modifier.", "modifier.") | |
private fun indent(count: Int) = " ".repeat(count) | |
/** | |
* Adds formatting-disabling comments to the beginning and end of the given ;ine. | |
* | |
* This prevents the IDE formatter, based on .editorconfig, from reformatting | |
* code that has already been modified by this Spotless custom step. | |
*/ | |
private fun String.addIDEFormattingDisable(indentCount: Int): String { | |
return indent(count = indentCount) + editorConfigFormatterOffTagComment + "\n" + | |
this + | |
"\n" + indent(count = indentCount) + editorConfigFormatterOnTagComment | |
} | |
/** | |
* Formats the given line to split [androidx.compose.ui.Modifier]'s functions chain calls | |
* into separate lines to improve readability. | |
* | |
* @param modifierPattern The pattern to match (e.g. "Modifier" or "modifier"). | |
* @param indent The number of spaces to indent the formatted Modifier methods calls. | |
* @param isLineHasModifierAssignment Indicates if the line contains an assignment related to Modifier, | |
* [modifierAssignmentRegexMap] | |
* | |
* @return The formatted string. | |
*/ | |
private fun String.formatModifier( | |
modifierPattern: String, | |
indent: Int, | |
isLineHasModifierAssignment: Boolean = true, | |
): String { | |
// If Modifier's assignment is present in line, move chain calls after it to the next line, | |
// otherwise chain calls starts right after [modifierPattern], so move it to the next line | |
val line = if (isLineHasModifierAssignment) { | |
modifierAssignmentRegexMap[modifierPattern]?.replace(this) { match -> | |
"${match.value}\n${indent(count = indent)}" | |
} ?: this | |
} else { | |
replace( | |
oldValue = modifierPattern, | |
newValue = "$modifierPattern\n${indent(count = indent)}", | |
) | |
} | |
// Put each call in a Modifier chain on its own line | |
return line.replace( | |
oldValue = ").", | |
newValue = ")\n${indent(count = indent)}.", | |
) | |
} | |
/** | |
* Checks if the given [line] contains a valid [androidx.compose.ui.Modifier] chain for formatting. | |
* | |
* Line should have at least two chained calls of Modifier' functions in single line (not split by \n) | |
* and should not contain an extension function declaration. | |
* | |
* Using [modifierExtensionRegexMap], check for extension function declaration, may be redundant, | |
* but left it just for sure. | |
* | |
* @param line The line to check. | |
* @param modifierPattern The pattern to match (e.g. "Modifier" or "modifier"). | |
* */ | |
private fun isLineContainsModifierChainForFormatting( | |
line: String, | |
modifierPattern: String, | |
): Boolean { | |
return modifierChainRegexMap[modifierPattern]?.containsMatchIn(line) == true && | |
modifierExtensionRegexMap[modifierPattern]?.containsMatchIn(line) == false | |
} | |
/** | |
* Map of modifier patterns to their corresponding assignment regexes. | |
* For example: | |
* - "Modifier" maps to regex for "modifier = Modifier" | |
* - "modifier" maps to regex for "modifier = modifier" | |
*/ | |
private val modifierAssignmentRegexMap = mapOf( | |
"Modifier" to Regex("""\bmodifier\s*=\s*Modifier\b"""), | |
"modifier" to Regex("""\bmodifier\s*=\s*modifier\b""") | |
) | |
/** | |
* A map of [modifierPatterns] without dots with regex patterns matching chains of | |
* [androidx.compose.ui.Modifier] calls, where at least two calls are chained together in single | |
* line (not split by \n). | |
* | |
* Used to identify lines with Modifier' chain calls that should be formatted. | |
* */ | |
private val modifierChainRegexMap: Map<String, Regex> = modifierPatterns | |
.map { it.removeSuffix(".") } | |
.associateWith { pattern -> | |
Regex("""\b${Regex.escape(pattern)}(?:\.\w+\([^)]*\)){2,}""") | |
} | |
/** | |
* A map of [modifierPatterns] without dots with regex patterns match Modifier extension function | |
* declarations. | |
* | |
* Used to identify lines that should be excluded from modifier formatting. | |
* */ | |
private val modifierExtensionRegexMap: Map<String, Regex> = modifierPatterns | |
.map { it.removeSuffix(".") } | |
.associateWith { pattern -> | |
Regex("""^\s*fun\s+${Regex.escape(pattern)}\.\w+\(""") | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.diffplug.gradle.spotless.SpotlessExtension | |
import org.gradle.api.Project | |
import org.gradle.kotlin.dsl.findByType | |
internal val Project.spotlessExtension: SpotlessExtension | |
get() = checkNotNull(extensions.findByType(SpotlessExtension::class)) | |
internal fun Project.spotlessConfig(block: SpotlessExtension.() -> Unit) = block(spotlessExtension) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment