diff --git a/KtlintCustomRules/build.gradle.kts b/KtlintCustomRules/build.gradle.kts new file mode 100644 index 000000000..d0954e2be --- /dev/null +++ b/KtlintCustomRules/build.gradle.kts @@ -0,0 +1,9 @@ + +plugins { + id("kotlin") +} + +dependencies { + compileOnly(core.ktlint.cliRuleset) + compileOnly(core.ktlint.ruleEngineCore) +} diff --git a/KtlintCustomRules/proguard-rules.pro b/KtlintCustomRules/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/KtlintCustomRules/proguard-rules.pro @@ -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 diff --git a/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/CustomRuleSetProvider.kt b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/CustomRuleSetProvider.kt new file mode 100644 index 000000000..b834056cf --- /dev/null +++ b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/CustomRuleSetProvider.kt @@ -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 . + */ +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 = + setOf( + RuleProvider { TrailingCommaInMultilineParametersRule() }, + RuleProvider { TrailingCommaInMultilineArgumentsRule() }, + ) +} diff --git a/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/TrailingCommaInMultilineArgumentsRule.kt b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/TrailingCommaInMultilineArgumentsRule.kt new file mode 100644 index 000000000..d924e899b --- /dev/null +++ b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/TrailingCommaInMultilineArgumentsRule.kt @@ -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 . + */ +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" + ) + } + } +} diff --git a/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/TrailingCommaInMultilineParametersRule.kt b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/TrailingCommaInMultilineParametersRule.kt new file mode 100644 index 000000000..5e13e1678 --- /dev/null +++ b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/TrailingCommaInMultilineParametersRule.kt @@ -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 . + */ +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) + } +} diff --git a/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/Utils.kt b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/Utils.kt new file mode 100644 index 000000000..6f6be7d7a --- /dev/null +++ b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/Utils.kt @@ -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 . + */ +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() + } +} diff --git a/KtlintCustomRules/src/main/resources/META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3 b/KtlintCustomRules/src/main/resources/META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3 new file mode 100644 index 000000000..15a6710ef --- /dev/null +++ b/KtlintCustomRules/src/main/resources/META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3 @@ -0,0 +1 @@ +com.infomaniak.core.ktlint.customrules.CustomRuleSetProvider \ No newline at end of file diff --git a/gradle/core.versions.toml b/gradle/core.versions.toml index 65fabeb36..a7e346603 100644 --- a/gradle/core.versions.toml +++ b/gradle/core.versions.toml @@ -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" @@ -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" }