Skip to content
Draft
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
9 changes: 9 additions & 0 deletions KtlintCustomRules/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

plugins {
id("kotlin")
}

dependencies {
compileOnly(core.ktlint.cliRuleset)
compileOnly(core.ktlint.ruleEngineCore)
}
21 changes: 21 additions & 0 deletions KtlintCustomRules/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Infomaniak Core - Android
* Copyright (C) 2025-2025 Infomaniak Network SA
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.core.ktlint.customrules

import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3
import com.pinterest.ktlint.rule.engine.core.api.RuleProvider
import com.pinterest.ktlint.rule.engine.core.api.RuleSetId

internal const val CUSTOM_RULE_SET_ID = "custom-rule-set-id"

class CustomRuleSetProvider : RuleSetProviderV3(RuleSetId(CUSTOM_RULE_SET_ID)) {
override fun getRuleProviders(): Set<RuleProvider> =
setOf(
RuleProvider { TrailingCommaInMultilineParametersRule() },
RuleProvider { TrailingCommaInMultilineArgumentsRule() },
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Infomaniak Core - Android
* Copyright (C) 2025 Infomaniak Network SA
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.core.ktlint.customrules

import com.infomaniak.core.ktlint.customrules.Utils.checkTrailingComma
import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import org.jetbrains.kotlin.com.intellij.lang.ASTNode

class TrailingCommaInMultilineArgumentsRule :
Rule(
ruleId = RuleId("${CUSTOM_RULE_SET_ID}:trailing-comma-multiline-arguments"),
about = About()
) {

override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
if (node.elementType == ElementType.VALUE_ARGUMENT_LIST) {
checkTrailingComma(
node,
autoCorrect,
emit,
ElementType.VALUE_ARGUMENT,
"Multiline argument list should have a trailing comma"
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Infomaniak Core - Android
* Copyright (C) 2025 Infomaniak Network SA
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.core.ktlint.customrules

import com.infomaniak.core.ktlint.customrules.Utils.checkTrailingComma
import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.psi.KtFunction

class TrailingCommaInMultilineParametersRule :
Rule(
ruleId = RuleId("${CUSTOM_RULE_SET_ID}:trailing-comma-multiline-parameters"),
about = About(),
) {

override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
if (isValueParameterList(node)) {
checkTrailingComma(
node,
autoCorrect,
emit,
ElementType.VALUE_PARAMETER,
"Multiline parameter list should have a trailing comma",
)
}
}

private fun isValueParameterList(node: ASTNode): Boolean {
val isValueParameterList = node.elementType == ElementType.VALUE_PARAMETER_LIST
return isValueParameterList && (node.psi.parent is KtFunction || node.psi.parent.parent is KtFunction)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Infomaniak Core - Android
* Copyright (C) 2025 Infomaniak Network SA
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.core.ktlint.customrules

import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.ElementType.COMMA
import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithNewline
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.TreeElement
import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType
import org.jetbrains.kotlin.psi.psiUtil.children

object Utils {

fun checkTrailingComma(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
elementType: IElementType,
errorMessage: String,
) {
val elements = node.children().filter { it.elementType == elementType }.toList()
val leftParenthesis = node.findChildByType(ElementType.LPAR) ?: return
val rightParenthesis = node.findChildByType(ElementType.RPAR) ?: return

if (elements.size == 1) {
val element = elements.first()
val textBetweenLParAndElement = getTextBetween(leftParenthesis.treeNext, element.treePrev)
val textBetweenArgAndRElement = getTextBetween(element.treeNext, rightParenthesis.treePrev)

if (!(textBetweenLParAndElement.contains('\n') || textBetweenArgAndRElement.contains('\n'))) {
return
}
} else if (elements.isEmpty()) {
return
}

var isMultiline = false
var previousElementEndNode: ASTNode = leftParenthesis

for (argIndex in elements.indices) {
val currentElement = elements[argIndex]
var nodeToInspect: ASTNode? = previousElementEndNode.treeNext
var foundNewlineBeforeCurrentElement = false

while (nodeToInspect != null && nodeToInspect != currentElement) {
if (nodeToInspect.isWhiteSpaceWithNewline()) {
foundNewlineBeforeCurrentElement = true
break
}
nodeToInspect = nodeToInspect.treeNext
}

if (foundNewlineBeforeCurrentElement) {
isMultiline = true
break
}
previousElementEndNode = currentElement
}

if (!isMultiline && elements.isNotEmpty()) {
val lastElement = elements.last()
var nodeToInspect: ASTNode? = lastElement.treeNext

while (nodeToInspect != null && nodeToInspect != rightParenthesis) {
if (nodeToInspect.isWhiteSpaceWithNewline()) {
isMultiline = true
break
}
nodeToInspect = nodeToInspect.treeNext
}
}

if (isMultiline) {
val lastElement = elements.last()
val nextSignificantTokenAfterLastElement = lastElement.treeNext?.let { node ->
var current: ASTNode? = node
while (current != null &&
(
current.elementType == ElementType.WHITE_SPACE ||
current.elementType == ElementType.EOL_COMMENT ||
current.elementType == ElementType.BLOCK_COMMENT
)
) {
current = current.treeNext
}
current
}

if (nextSignificantTokenAfterLastElement?.elementType != COMMA) {
val insertOffset = lastElement.startOffset + lastElement.textLength

emit(
insertOffset,
errorMessage,
autoCorrect,
)

if (autoCorrect) {
(rightParenthesis as TreeElement).rawInsertBeforeMe(LeafPsiElement(COMMA, ","))
}
}
}
}

fun getTextBetween(startNode: ASTNode?, endNode: ASTNode?): String {
if (startNode == null || endNode == null || startNode == endNode) return ""

return StringBuilder().apply {
var current: ASTNode? = startNode
while (current != null && current != endNode) {
append(current.text)
current = current.treeNext
if (current == endNode) break
}
}.toString()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.infomaniak.core.ktlint.customrules.CustomRuleSetProvider
3 changes: 3 additions & 0 deletions gradle/core.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ junitAndroidx = "1.3.0"
kotest = "5.9.1"
kotlinxCoroutines = "1.10.2"
kotlinxSerialization = "1.8.1" # Doesn't build when bumped to 1.9.0 (Waiting Kotlin 2.2.0)
ktlint="1.7.1"
ktor = "3.2.3"
lifecycleRuntimeKtx = "2.8.7" # Doesn't build when bumped to 2.9.0 (Waiting SDK 35)
material = "1.12.0"
Expand Down Expand Up @@ -69,6 +70,8 @@ integrity = { module = "com.google.android.play:integrity", version.ref = "integ
kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
ktlint-cliRuleset = { module = "com.pinterest.ktlint:ktlint-cli-ruleset-core", version.ref = "ktlint"}
ktlint-ruleEngineCore = { module = "com.pinterest.ktlint:ktlint-rule-engine-core", version.ref = "ktlint"}
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinxSerialization" }
ktor-client-content-negociation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
Expand Down