Skip to content

Commit

Permalink
Recognize 'ij_kotlin_packages_to_use_import_on_demand' configuration …
Browse files Browse the repository at this point in the history
…to editorconfig to make 'no-wildcard-imports' more granularly configurable
  • Loading branch information
asmadsen committed Mar 6, 2022
1 parent cf96f51 commit 37d438d
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 9 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ 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.

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.


### Fixed

### Changed
- Print the rule id always in the PlainReporter ([#1121](https://github.com/pinterest/ktlint/issues/1121))


### Removed

## [0.44.0] - 2022-02-15
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PatternEntry> = 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<List<PatternEntry>> =
{ _, 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<List<PatternEntry>> =
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<UsesEditorConfigProperties.EditorConfigProperty<*>> = 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<PatternEntry> {
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.**"
)
}
}

0 comments on commit 37d438d

Please sign in to comment.