From 74f2209b700acc4885456002d0b3dd1fd6ce6568 Mon Sep 17 00:00:00 2001 From: Vincent TE Date: Wed, 11 Jun 2025 15:06:54 +0200 Subject: [PATCH 01/10] feat: Add custom rules module --- KtlintCustomRules/.gitignore | 1 + KtlintCustomRules/build.gradle.kts | 11 +++++ KtlintCustomRules/proguard-rules.pro | 21 ++++++++++ .../customrules/CustomRuleSetProvider.kt | 31 ++++++++++++++ .../core/ktlint/customrules/NoVarRule.kt | 41 +++++++++++++++++++ ...int.cli.ruleset.core.api.RuleSetProviderV3 | 1 + .../ktlint/customrules/ExampleUnitTest.kt | 17 ++++++++ 7 files changed, 123 insertions(+) create mode 100644 KtlintCustomRules/.gitignore create mode 100644 KtlintCustomRules/build.gradle.kts create mode 100644 KtlintCustomRules/proguard-rules.pro create mode 100644 KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/CustomRuleSetProvider.kt create mode 100644 KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/NoVarRule.kt create mode 100644 KtlintCustomRules/src/main/resources/META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3 create mode 100644 KtlintCustomRules/src/test/java/com/infomaniak/core/ktlint/customrules/ExampleUnitTest.kt diff --git a/KtlintCustomRules/.gitignore b/KtlintCustomRules/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/KtlintCustomRules/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/KtlintCustomRules/build.gradle.kts b/KtlintCustomRules/build.gradle.kts new file mode 100644 index 000000000..2ef49b25a --- /dev/null +++ b/KtlintCustomRules/build.gradle.kts @@ -0,0 +1,11 @@ + +plugins { + id("kotlin") +} + +dependencies { + compileOnly("com.pinterest.ktlint:ktlint-cli-ruleset-core:1.6.0") + compileOnly("com.pinterest.ktlint:ktlint-rule-engine-core:1.6.0") + + testImplementation("junit:junit:4.13.2") +} diff --git a/KtlintCustomRules/proguard-rules.pro b/KtlintCustomRules/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /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 \ No newline at end of file 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..c7fbe43cb --- /dev/null +++ b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/CustomRuleSetProvider.kt @@ -0,0 +1,31 @@ +/* + * 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 { NoVarRule() }, + ) +} diff --git a/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/NoVarRule.kt b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/NoVarRule.kt new file mode 100644 index 000000000..522165756 --- /dev/null +++ b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/NoVarRule.kt @@ -0,0 +1,41 @@ +/* + * 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.rule.engine.core.api.AutocorrectDecision +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.RuleAutocorrectApproveHandler +import com.pinterest.ktlint.rule.engine.core.api.RuleId +import org.jetbrains.kotlin.com.intellij.lang.ASTNode + +class NoVarRule : + Rule( + ruleId = RuleId("${CUSTOM_RULE_SET_ID}:no-var"), + about = About(), + ), + RuleAutocorrectApproveHandler { + override fun beforeVisitChildNodes( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision, + ) { + if (node.elementType == ElementType.VAR_KEYWORD) { + emit(node.startOffset, "Unexpected var, use val instead", false) + } + } +} 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/KtlintCustomRules/src/test/java/com/infomaniak/core/ktlint/customrules/ExampleUnitTest.kt b/KtlintCustomRules/src/test/java/com/infomaniak/core/ktlint/customrules/ExampleUnitTest.kt new file mode 100644 index 000000000..e604411a3 --- /dev/null +++ b/KtlintCustomRules/src/test/java/com/infomaniak/core/ktlint/customrules/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.infomaniak.core.ktlint.customrules + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file From 7c2a56bb6af5a36f3d70160a41bf37337b4e92cd Mon Sep 17 00:00:00 2001 From: Vincent TE Date: Thu, 12 Jun 2025 14:59:55 +0200 Subject: [PATCH 02/10] feat: Add custom rule for trailing coma in classes and method --- .../customrules/CustomRuleSetProvider.kt | 2 +- .../core/ktlint/customrules/NoVarRule.kt | 41 ----- .../TrailingCommaInMultilineParametersRule.kt | 142 ++++++++++++++++++ 3 files changed, 143 insertions(+), 42 deletions(-) delete mode 100644 KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/NoVarRule.kt create mode 100644 KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/TrailingCommaInMultilineParametersRule.kt 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 index c7fbe43cb..43f0d8754 100644 --- a/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/CustomRuleSetProvider.kt +++ b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/CustomRuleSetProvider.kt @@ -26,6 +26,6 @@ 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 { NoVarRule() }, + RuleProvider { TrailingCommaInMultilineParametersRule() }, ) } diff --git a/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/NoVarRule.kt b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/NoVarRule.kt deleted file mode 100644 index 522165756..000000000 --- a/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/NoVarRule.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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.rule.engine.core.api.AutocorrectDecision -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.RuleAutocorrectApproveHandler -import com.pinterest.ktlint.rule.engine.core.api.RuleId -import org.jetbrains.kotlin.com.intellij.lang.ASTNode - -class NoVarRule : - Rule( - ruleId = RuleId("${CUSTOM_RULE_SET_ID}:no-var"), - about = About(), - ), - RuleAutocorrectApproveHandler { - override fun beforeVisitChildNodes( - node: ASTNode, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision, - ) { - if (node.elementType == ElementType.VAR_KEYWORD) { - emit(node.startOffset, "Unexpected var, use val instead", false) - } - } -} 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..1cfbec389 --- /dev/null +++ b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/TrailingCommaInMultilineParametersRule.kt @@ -0,0 +1,142 @@ +/* + * 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.Rule +import com.pinterest.ktlint.rule.engine.core.api.RuleId +import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithNewline +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.psi.KtFunction +import org.jetbrains.kotlin.psi.psiUtil.children + +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, + ) { + val isValueParameterList = node.elementType == ElementType.VALUE_PARAMETER_LIST + if (isValueParameterList && (node.psi.parent is KtFunction || node.psi.parent.parent is KtFunction)) { + checkTrailingComma(node, emit) + } + } + + private fun checkTrailingComma( + parameterListNode: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + val parameters = parameterListNode.children().filter { it.elementType == ElementType.VALUE_PARAMETER }.toList() + val leftParenthesis = parameterListNode.findChildByType(ElementType.LPAR) ?: return + val rightParenthesis = parameterListNode.findChildByType(ElementType.RPAR) ?: return + + if (parameters.size == 1) { + val paramNode = parameters.first() + val textBetweenLparAndParam = getTextBetween(leftParenthesis.treeNext, paramNode.treePrev) + val textBetweenParamAndRpar = getTextBetween(paramNode.treeNext, rightParenthesis.treePrev) + + if (!(textBetweenLparAndParam.contains('\n') || textBetweenParamAndRpar.contains('\n'))) { + return + } + } else if (parameters.isEmpty()) { + return + } + + var isMultiline = false + var previousParamEndNode: ASTNode = leftParenthesis + + for (paramIndex in parameters.indices) { + val currentParam = parameters[paramIndex] + var nodeToInspect: ASTNode? = previousParamEndNode.treeNext + + var foundNewlineBeforeCurrentParam = false + while (nodeToInspect != null && nodeToInspect != currentParam) { + if (nodeToInspect.isWhiteSpaceWithNewline()) { + foundNewlineBeforeCurrentParam = true + break + } + nodeToInspect = nodeToInspect.treeNext + } + + if (foundNewlineBeforeCurrentParam) { + isMultiline = true + break + } + previousParamEndNode = currentParam + } + + if (!isMultiline && parameters.isNotEmpty()) { + val lastParam = parameters.last() + var nodeToInspect: ASTNode? = lastParam.treeNext + while (nodeToInspect != null && nodeToInspect != rightParenthesis) { + if (nodeToInspect.isWhiteSpaceWithNewline()) { + isMultiline = true + break + } + nodeToInspect = nodeToInspect.treeNext + } + } + + if (isMultiline) { + val lastParameter = parameters.last() + val nextSignificantTokenAfterLastParam = lastParameter.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 (nextSignificantTokenAfterLastParam?.elementType != ElementType.COMMA) { + val insertOffset = lastParameter.startOffset + lastParameter.textLength + emit( + insertOffset, + "Multiline parameter list should have a trailing comma", + true, + ) + } + } + } + + private 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() + } +} From 9fece4776ea778101d8766d81db917e52792ee04 Mon Sep 17 00:00:00 2001 From: Vincent TE Date: Fri, 13 Jun 2025 09:07:01 +0200 Subject: [PATCH 03/10] feat: Adding rule to check trailing comma for multiline arguments --- .../customrules/CustomRuleSetProvider.kt | 1 + .../TrailingCommaInMultilineArgumentsRule.kt | 47 +++++++ .../TrailingCommaInMultilineParametersRule.kt | 113 ++-------------- .../core/ktlint/customrules/Utils.kt | 126 ++++++++++++++++++ 4 files changed, 186 insertions(+), 101 deletions(-) create mode 100644 KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/TrailingCommaInMultilineArgumentsRule.kt create mode 100644 KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/Utils.kt 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 index 43f0d8754..b834056cf 100644 --- a/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/CustomRuleSetProvider.kt +++ b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/CustomRuleSetProvider.kt @@ -27,5 +27,6 @@ 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 index 1cfbec389..5e13e1678 100644 --- a/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/TrailingCommaInMultilineParametersRule.kt +++ b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/TrailingCommaInMultilineParametersRule.kt @@ -17,13 +17,12 @@ */ 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 com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithNewline import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.psi.KtFunction -import org.jetbrains.kotlin.psi.psiUtil.children class TrailingCommaInMultilineParametersRule : Rule( @@ -36,107 +35,19 @@ class TrailingCommaInMultilineParametersRule : autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, ) { - val isValueParameterList = node.elementType == ElementType.VALUE_PARAMETER_LIST - if (isValueParameterList && (node.psi.parent is KtFunction || node.psi.parent.parent is KtFunction)) { - checkTrailingComma(node, emit) - } - } - - private fun checkTrailingComma( - parameterListNode: ASTNode, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, - ) { - val parameters = parameterListNode.children().filter { it.elementType == ElementType.VALUE_PARAMETER }.toList() - val leftParenthesis = parameterListNode.findChildByType(ElementType.LPAR) ?: return - val rightParenthesis = parameterListNode.findChildByType(ElementType.RPAR) ?: return - - if (parameters.size == 1) { - val paramNode = parameters.first() - val textBetweenLparAndParam = getTextBetween(leftParenthesis.treeNext, paramNode.treePrev) - val textBetweenParamAndRpar = getTextBetween(paramNode.treeNext, rightParenthesis.treePrev) - - if (!(textBetweenLparAndParam.contains('\n') || textBetweenParamAndRpar.contains('\n'))) { - return - } - } else if (parameters.isEmpty()) { - return - } - - var isMultiline = false - var previousParamEndNode: ASTNode = leftParenthesis - - for (paramIndex in parameters.indices) { - val currentParam = parameters[paramIndex] - var nodeToInspect: ASTNode? = previousParamEndNode.treeNext - - var foundNewlineBeforeCurrentParam = false - while (nodeToInspect != null && nodeToInspect != currentParam) { - if (nodeToInspect.isWhiteSpaceWithNewline()) { - foundNewlineBeforeCurrentParam = true - break - } - nodeToInspect = nodeToInspect.treeNext - } - - if (foundNewlineBeforeCurrentParam) { - isMultiline = true - break - } - previousParamEndNode = currentParam - } - - if (!isMultiline && parameters.isNotEmpty()) { - val lastParam = parameters.last() - var nodeToInspect: ASTNode? = lastParam.treeNext - while (nodeToInspect != null && nodeToInspect != rightParenthesis) { - if (nodeToInspect.isWhiteSpaceWithNewline()) { - isMultiline = true - break - } - nodeToInspect = nodeToInspect.treeNext - } - } - - if (isMultiline) { - val lastParameter = parameters.last() - val nextSignificantTokenAfterLastParam = lastParameter.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 (nextSignificantTokenAfterLastParam?.elementType != ElementType.COMMA) { - val insertOffset = lastParameter.startOffset + lastParameter.textLength - emit( - insertOffset, - "Multiline parameter list should have a trailing comma", - true, - ) - } + if (isValueParameterList(node)) { + checkTrailingComma( + node, + autoCorrect, + emit, + ElementType.VALUE_PARAMETER, + "Multiline parameter list should have a trailing comma", + ) } } - private 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() + 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..9c9e92b42 --- /dev/null +++ b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/Utils.kt @@ -0,0 +1,126 @@ +/* + * 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.isWhiteSpaceWithNewline +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType +import org.jetbrains.kotlin.psi.psiUtil.children + +object Utils { + + fun checkTrailingComma( + listNode: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + elementType: IElementType, + errorMessage: String, + ) { + val elements = listNode.children().filter { it.elementType == elementType }.toList() + val leftParenthesis = listNode.findChildByType(ElementType.LPAR) ?: return + val rightParenthesis = listNode.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 != ElementType.COMMA) { + val insertOffset = lastElement.startOffset + lastElement.textLength + emit( + insertOffset, + errorMessage, + autoCorrect, + ) + } + } + } + + 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() + } +} From 5ade0a12f371ebae63a28a755cf2e117a30c7b24 Mon Sep 17 00:00:00 2001 From: Vincent TE Date: Fri, 27 Jun 2025 09:01:56 +0200 Subject: [PATCH 04/10] fix: Add auto correct --- .../main/java/com/infomaniak/core/ktlint/customrules/Utils.kt | 4 ++++ 1 file changed, 4 insertions(+) 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 index 9c9e92b42..28b08480c 100644 --- a/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/Utils.kt +++ b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/Utils.kt @@ -107,6 +107,10 @@ object Utils { errorMessage, autoCorrect, ) + + if (autoCorrect) { + (rightParenthesis as TreeElement).rawInsertBeforeMe(LeafPsiElement(COMMA, ",")) + } } } } From 5830324927c106176792c2a782666f8ecd041424 Mon Sep 17 00:00:00 2001 From: Vincent TE Date: Fri, 27 Jun 2025 09:02:09 +0200 Subject: [PATCH 05/10] chore: Rename variables --- .../core/ktlint/customrules/Utils.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) 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 index 28b08480c..6f6be7d7a 100644 --- a/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/Utils.kt +++ b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/Utils.kt @@ -18,30 +18,33 @@ 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( - listNode: ASTNode, + node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, elementType: IElementType, errorMessage: String, ) { - val elements = listNode.children().filter { it.elementType == elementType }.toList() - val leftParenthesis = listNode.findChildByType(ElementType.LPAR) ?: return - val rightParenthesis = listNode.findChildByType(ElementType.RPAR) ?: return + 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 textBetweenLParAndElement = getTextBetween(leftParenthesis.treeNext, element.treePrev) val textBetweenArgAndRElement = getTextBetween(element.treeNext, rightParenthesis.treePrev) - if (!(textBetweenLparAndElement.contains('\n') || textBetweenArgAndRElement.contains('\n'))) { + if (!(textBetweenLParAndElement.contains('\n') || textBetweenArgAndRElement.contains('\n'))) { return } } else if (elements.isEmpty()) { @@ -100,8 +103,9 @@ object Utils { current } - if (nextSignificantTokenAfterLastElement?.elementType != ElementType.COMMA) { + if (nextSignificantTokenAfterLastElement?.elementType != COMMA) { val insertOffset = lastElement.startOffset + lastElement.textLength + emit( insertOffset, errorMessage, From 78859ef5b0d52e8951b08e9e039e4c087000e400 Mon Sep 17 00:00:00 2001 From: Vincent TE Date: Tue, 22 Jul 2025 17:07:39 +0200 Subject: [PATCH 06/10] chore: Use versions catalog # Conflicts: # gradle/core.versions.toml --- KtlintCustomRules/build.gradle.kts | 6 +++--- gradle/core.versions.toml | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/KtlintCustomRules/build.gradle.kts b/KtlintCustomRules/build.gradle.kts index 2ef49b25a..00eeb6a5c 100644 --- a/KtlintCustomRules/build.gradle.kts +++ b/KtlintCustomRules/build.gradle.kts @@ -4,8 +4,8 @@ plugins { } dependencies { - compileOnly("com.pinterest.ktlint:ktlint-cli-ruleset-core:1.6.0") - compileOnly("com.pinterest.ktlint:ktlint-rule-engine-core:1.6.0") + compileOnly(core.ktlint.cliRuleset) + compileOnly(core.ktlint.ruleEngineCore) - testImplementation("junit:junit:4.13.2") + testImplementation(core.androidx.junit) } diff --git a/gradle/core.versions.toml b/gradle/core.versions.toml index 65fabeb36..69cdd44a7 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,10 @@ 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"} +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } +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" } From b383c5fefa62522668b8809f1511c52a58d7a586 Mon Sep 17 00:00:00 2001 From: Vincent TE Date: Tue, 22 Jul 2025 17:07:53 +0200 Subject: [PATCH 07/10] chore: Delete unused .gitignore --- KtlintCustomRules/.gitignore | 1 - 1 file changed, 1 deletion(-) delete mode 100644 KtlintCustomRules/.gitignore diff --git a/KtlintCustomRules/.gitignore b/KtlintCustomRules/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/KtlintCustomRules/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file From 4df09566cdc75df8f5e112b2f933437b661093b1 Mon Sep 17 00:00:00 2001 From: Vincent TE Date: Tue, 22 Jul 2025 17:08:01 +0200 Subject: [PATCH 08/10] chore: Add EOL --- KtlintCustomRules/proguard-rules.pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KtlintCustomRules/proguard-rules.pro b/KtlintCustomRules/proguard-rules.pro index 481bb4348..f1b424510 100644 --- a/KtlintCustomRules/proguard-rules.pro +++ b/KtlintCustomRules/proguard-rules.pro @@ -18,4 +18,4 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile From 68fef188b257fc576b1e884bcaef0b084abcc6dc Mon Sep 17 00:00:00 2001 From: Vincent TE Date: Tue, 12 Aug 2025 14:58:22 +0200 Subject: [PATCH 09/10] chore: Fix core.version.toml --- gradle/core.versions.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/gradle/core.versions.toml b/gradle/core.versions.toml index 69cdd44a7..a7e346603 100644 --- a/gradle/core.versions.toml +++ b/gradle/core.versions.toml @@ -71,8 +71,6 @@ kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = 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"} -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } -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" } From 080e95decc51a614837b47dca027e358c1ea4a14 Mon Sep 17 00:00:00 2001 From: Vincent TE Date: Wed, 13 Aug 2025 07:23:05 +0200 Subject: [PATCH 10/10] chore: Remove unused test --- KtlintCustomRules/build.gradle.kts | 2 -- .../core/ktlint/customrules/ExampleUnitTest.kt | 17 ----------------- 2 files changed, 19 deletions(-) delete mode 100644 KtlintCustomRules/src/test/java/com/infomaniak/core/ktlint/customrules/ExampleUnitTest.kt diff --git a/KtlintCustomRules/build.gradle.kts b/KtlintCustomRules/build.gradle.kts index 00eeb6a5c..d0954e2be 100644 --- a/KtlintCustomRules/build.gradle.kts +++ b/KtlintCustomRules/build.gradle.kts @@ -6,6 +6,4 @@ plugins { dependencies { compileOnly(core.ktlint.cliRuleset) compileOnly(core.ktlint.ruleEngineCore) - - testImplementation(core.androidx.junit) } diff --git a/KtlintCustomRules/src/test/java/com/infomaniak/core/ktlint/customrules/ExampleUnitTest.kt b/KtlintCustomRules/src/test/java/com/infomaniak/core/ktlint/customrules/ExampleUnitTest.kt deleted file mode 100644 index e604411a3..000000000 --- a/KtlintCustomRules/src/test/java/com/infomaniak/core/ktlint/customrules/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.infomaniak.core.ktlint.customrules - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file