aboutsummaryrefslogtreecommitdiff
path: root/detekt/src/main/kotlin/imports/CustomImportOrdering.kt
blob: 9e0302153d0a8855f19d872be1f76802c27f6632 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
package at.hannibal2.skyhanni.detektrules.imports

import at.hannibal2.skyhanni.detektrules.PreprocessingPattern
import at.hannibal2.skyhanni.detektrules.PreprocessingPattern.Companion.containsPreprocessingPattern
import io.gitlab.arturbosch.detekt.api.CodeSmell
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Debt
import io.gitlab.arturbosch.detekt.api.Entity
import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.Severity
import org.jetbrains.kotlin.psi.KtImportDirective
import org.jetbrains.kotlin.psi.KtImportList

class CustomImportOrdering(config: Config) : Rule(config) {
    override val issue = Issue(
        "CustomImportOrdering",
        Severity.Style,
        "Enforces correct import ordering, taking into account preprocessed imports.",
        Debt.FIVE_MINS,
    )

    companion object {
        private val importOrder = ImportSorter()

        private val packageImportOrdering = listOf("java.", "javax.", "kotlin.")

        private class ImportSorter : Comparator<KtImportDirective> {
            override fun compare(
                import1: KtImportDirective,
                import2: KtImportDirective,
            ): Int {
                val importPath1 = import1.importPath!!.pathStr
                val importPath2 = import2.importPath!!.pathStr

                val isTypeAlias1 = import1.aliasName != null
                val isTypeAlias2 = import2.aliasName != null

                val index1 = packageImportOrdering.indexOfFirst { importPath1.startsWith(it) }
                val index2 = packageImportOrdering.indexOfFirst { importPath2.startsWith(it) }

                return when {
                    isTypeAlias1 && isTypeAlias2 -> importPath1.compareTo(importPath2)
                    isTypeAlias1 && !isTypeAlias2 -> 1
                    !isTypeAlias1 && isTypeAlias2 -> -1
                    index1 == -1 && index2 == -1 -> importPath1.compareTo(importPath2)
                    index1 == -1 -> -1
                    index2 == -1 -> 1
                    else -> index1.compareTo(index2)
                }
            }
        }
    }

    private fun isImportsCorrectlyOrdered(imports: List<KtImportDirective>, rawText: List<String>): Boolean {
        if (rawText.any { it.isBlank() }) {
            return false
        }

        var inPreprocess = false
        val linesToIgnore = mutableListOf<String>()

        for (line in rawText) {
            if (line.contains(PreprocessingPattern.IF.asComment)) {
                inPreprocess = true
                continue
            }
            if (line.contains(PreprocessingPattern.ENDIF.asComment)) {
                inPreprocess = false
                continue
            }
            if (line.contains(PreprocessingPattern.DOLLAR_DOLLAR.asComment)) {
                continue
            }
            if (inPreprocess) {
                linesToIgnore.add(line)
            }
        }

        val originalImports = rawText.filter { !it.containsPreprocessingPattern() && !linesToIgnore.contains(it) }
        val formattedOriginal = originalImports.joinToString("\n") { it }

        val expectedImports = imports.sortedWith(importOrder).map { "import ${it.importPath}" }
        val formattedExpected = expectedImports.filter { !linesToIgnore.contains(it) }.joinToString("\n")

        return formattedOriginal == formattedExpected
    }

    override fun visitImportList(importList: KtImportList) {

        val testEntity = Entity.from(importList)

        val rawText = importList.text.trim()
        if (rawText.isBlank()) {
            return
        }

        val importsCorrect = isImportsCorrectlyOrdered(importList.imports, rawText.lines())

        if (!importsCorrect) {
            report(
                CodeSmell(
                    issue,
                    testEntity,
                    "Imports must be ordered in lexicographic order without any empty lines in-between " +
                        "with \"java\", \"javax\", \"kotlin\" and aliases in the end. This should then be followed by " +
                        "pre-processed imports.",
                ),
            )
        }

        super.visitImportList(importList)
    }
}