diff --git a/CHANGELOG.md b/CHANGELOG.md index 76eba2c677..e95665b53b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## Unreleased ### Added +- Recognize `ij_kotlin_packages_to_use_import_on_demand` configuration in editorconfig to be able to allow certain wildcard imports. ### Fixed @@ -12,6 +13,8 @@ This project adheres to [Semantic Versioning](https://semver.org/). - Print the rule id always in the PlainReporter ([#1121](https://github.com/pinterest/ktlint/issues/1121)) +This is to make developing with libraries like ktor or kotlin-react more pleasant. These libraries rely heavily on extension functions, which in turn adds a lot of imports. + ### Removed ## [0.44.0] - 2022-02-15 diff --git a/README.md b/README.md index da754c0e69..e3f419da60 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,12 @@ ij_kotlin_imports_layout=android.**,|,^org.junit.**,kotlin.io.Closeable.*,|,*,^ # backticks to be longer than the maximum line length. (Since 0.41.0) [**/test/**.kt] ktlint_ignore_back_ticked_identifier=true + +# Comma-separated list of allowed wildcard imports that will override the no-wildcard-imports rule. +# This can be used for allowing wildcard imports from libraries like Ktor where extension functions are used in a way that creates a lot of imports. +# "**" applies to package and all subpackages +ij_kotlin_packages_to_use_import_on_demand=java.util.* # allow java.util.* as wildcard import +ij_kotlin_packages_to_use_import_on_demand=io.ktor.** # allow wildcard import from io.ktor.* and all subpackages ``` ### Overriding Editorconfig properties for specific directories diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoWildcardImportsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoWildcardImportsRule.kt index 0c40afa0ea..bcc0b92571 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoWildcardImportsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoWildcardImportsRule.kt @@ -1,23 +1,98 @@ package com.pinterest.ktlint.ruleset.standard +import com.pinterest.ktlint.core.KtLint import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.api.FeatureInAlphaState +import com.pinterest.ktlint.core.api.UsesEditorConfigProperties import com.pinterest.ktlint.core.ast.ElementType.IMPORT_DIRECTIVE +import com.pinterest.ktlint.core.ast.isRoot +import com.pinterest.ktlint.ruleset.standard.internal.importordering.PatternEntry +import org.ec4j.core.model.PropertyType import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.psi.KtImportDirective -class NoWildcardImportsRule : Rule("no-wildcard-imports") { +@OptIn(FeatureInAlphaState::class) +public class NoWildcardImportsRule : Rule("no-wildcard-imports"), UsesEditorConfigProperties { + private var allowedWildcardImports: List = emptyList() + + public companion object { + internal const val IDEA_PACKAGES_TO_USE_IMPORT_ON_DEMAND_PROPERTY_NAME = "ij_kotlin_packages_to_use_import_on_demand" + private const val PROPERTY_DESCRIPTION = "Defines allowed wildcard imports" + + /** + * Default IntelliJ IDEA style: Use wildcard imports for packages in "java.util", "kotlin.android.synthetic" and + * it's subpackages. + * + * https://github.com/JetBrains/kotlin/blob/ffdab473e28d0d872136b910eb2e0f4beea2e19c/idea/formatter/src/org/jetbrains/kotlin/idea/core/formatter/KotlinCodeStyleSettings.java#L81-L82 + */ + private val IDEA_PATTERN = parseAllowedWildcardImports("java.util.*,kotlinx.android.synthetic.**") + + private val editorConfigPropertyParser: (String, String?) -> PropertyType.PropertyValue> = + { _, value -> + when { + else -> try { + PropertyType.PropertyValue.valid( + value, + value?.let(::parseAllowedWildcardImports) ?: emptyList() + ) + } catch (e: IllegalArgumentException) { + PropertyType.PropertyValue.invalid( + value, + "Unexpected imports layout: $value" + ) + } + } + } + + public val ideaPackagesToUseImportOnDemandProperty: UsesEditorConfigProperties.EditorConfigProperty> = + UsesEditorConfigProperties.EditorConfigProperty( + type = PropertyType( + IDEA_PACKAGES_TO_USE_IMPORT_ON_DEMAND_PROPERTY_NAME, + PROPERTY_DESCRIPTION, + editorConfigPropertyParser + ), + defaultValue = IDEA_PATTERN, + propertyWriter = { it.joinToString(separator = ",") } + ) + } + + override val editorConfigProperties: List> = listOf( + ideaPackagesToUseImportOnDemandProperty + ) override fun visit( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { + if (node.isRoot()) { + val editorConfig = node.getUserData(KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY)!! + allowedWildcardImports = editorConfig.getEditorConfigValue(ideaPackagesToUseImportOnDemandProperty) + } if (node.elementType == IMPORT_DIRECTIVE) { val importDirective = node.psi as KtImportDirective - val path = importDirective.importPath - if (path != null && path.isAllUnder && !path.pathStr.startsWith("kotlinx.android.synthetic")) { + val path = importDirective.importPath ?: return + if (!path.isAllUnder) return + if (allowedWildcardImports.none { it.matches(path) }) { emit(node.startOffset, "Wildcard import", false) } } } } + +internal const val WILDCARD_CHAR = "*" + +internal fun parseAllowedWildcardImports(allowedWildcardImports: String): List { + val importsList = allowedWildcardImports.split(",").onEach { it.trim() } + + return importsList.map { + var import = it + var withSubpackages = false + if (import.endsWith(WILDCARD_CHAR + WILDCARD_CHAR)) { // java.** + import = import.substringBeforeLast(WILDCARD_CHAR) + withSubpackages = true + } + + PatternEntry(import, withSubpackages, false) + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/internal/importordering/PatternEntry.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/internal/importordering/PatternEntry.kt index 1a08bea294..17119c0466 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/internal/importordering/PatternEntry.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/internal/importordering/PatternEntry.kt @@ -22,12 +22,18 @@ public class PatternEntry( if (otherPackageName.startsWith(packageName)) { if (otherPackageName.length == packageName.length) return true if (withSubpackages) { - if (otherPackageName[packageName.length] == '.') return true + if (otherPackageName[packageName.length] == '.') { + return true + } } } return false } + internal fun matches(import: ImportPath): Boolean { + return matchesPackageName(import.pathStr.removeSuffix(".*")) + } + internal fun isBetterMatchForPackageThan(entry: PatternEntry?, import: ImportPath): Boolean { if (hasAlias != import.hasAlias() || !matchesPackageName(import.pathStr)) return false if (entry == null) return true diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/NoWildcardImportsRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/NoWildcardImportsRuleTest.kt index f873b81370..84928f0f4b 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/NoWildcardImportsRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/NoWildcardImportsRuleTest.kt @@ -1,29 +1,65 @@ package com.pinterest.ktlint.ruleset.standard import com.pinterest.ktlint.core.LintError +import com.pinterest.ktlint.core.api.FeatureInAlphaState +import com.pinterest.ktlint.test.EditorConfigOverride import com.pinterest.ktlint.test.lint import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test +@OptIn(FeatureInAlphaState::class) class NoWildcardImportsRuleTest { + private val rule = NoWildcardImportsRule() @Test fun testLint() { + val imports = + """ + import a.* + import a.b.c.* + import a.b + import kotlinx.android.synthetic.main.layout_name.* + import foo.bar.`**` + """.trimIndent() assertThat( - NoWildcardImportsRule().lint( + rule.lint(imports, IDEA_DEFAULT_ALLOWED_WILDCARD_IMPORTS) + ).isEqualTo( + listOf( + LintError(1, 1, "no-wildcard-imports", "Wildcard import"), + LintError(2, 1, "no-wildcard-imports", "Wildcard import"), + ) + ) + } + + @Test + fun testAllowedWildcardImports() { + assertThat( + rule.lint( """ import a.* import a.b.c.* import a.b - import kotlinx.android.synthetic.main.layout_name.* import foo.bar.`**` - """.trimIndent() + import react.* + import react.dom.* + import kotlinx.css.* + """.trimIndent(), + EditorConfigOverride.from( + NoWildcardImportsRule.ideaPackagesToUseImportOnDemandProperty to "react.*,react.dom.*" + ) ) ).isEqualTo( listOf( LintError(1, 1, "no-wildcard-imports", "Wildcard import"), - LintError(2, 1, "no-wildcard-imports", "Wildcard import") + LintError(2, 1, "no-wildcard-imports", "Wildcard import"), + LintError(7, 1, "no-wildcard-imports", "Wildcard import") ) ) } + + private companion object { + val IDEA_DEFAULT_ALLOWED_WILDCARD_IMPORTS = EditorConfigOverride.from( + NoWildcardImportsRule.ideaPackagesToUseImportOnDemandProperty to "java.util.*,kotlinx.android.synthetic.**" + ) + } }