xref: /aosp_15_r20/external/ktfmt/core/src/main/java/com/facebook/ktfmt/format/RedundantImportDetector.kt (revision 5be3f65c8cf0e6db0a7e312df5006e8e93cdf9ec)
1 /*
<lambda>null2  * Copyright (c) Meta Platforms, Inc. and affiliates.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.facebook.ktfmt.format
18 
19 import org.jetbrains.kotlin.com.intellij.psi.PsiElement
20 import org.jetbrains.kotlin.kdoc.psi.impl.KDocImpl
21 import org.jetbrains.kotlin.kdoc.psi.impl.KDocLink
22 import org.jetbrains.kotlin.kdoc.psi.impl.KDocName
23 import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection
24 import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag
25 import org.jetbrains.kotlin.name.FqName
26 import org.jetbrains.kotlin.psi.KtImportDirective
27 import org.jetbrains.kotlin.psi.KtImportList
28 import org.jetbrains.kotlin.psi.KtPackageDirective
29 import org.jetbrains.kotlin.psi.KtReferenceExpression
30 import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType
31 
32 internal class RedundantImportDetector(val enabled: Boolean) {
33   companion object {
34     private val OPERATORS =
35         setOf(
36             // Unary prefix operators
37             "unaryPlus",
38             "unaryMinus",
39             "not",
40             // Increments and decrements
41             "inc",
42             "dec",
43             // Arithmetic operators
44             "plus",
45             "minus",
46             "times",
47             "div",
48             "rem",
49             "mod", // deprecated
50             "rangeTo",
51             // 'In' operator
52             "contains",
53             // Indexed access operator
54             "get",
55             "set",
56             // Invoke operator
57             "invoke",
58             // Augmented assignments
59             "plusAssign",
60             "minusAssign",
61             "timesAssign",
62             "divAssign",
63             "remAssign",
64             "modAssign", // deprecated
65             // Equality and inequality operators
66             "equals",
67             // Comparison operators
68             "compareTo",
69             // Iterator operators
70             "iterator",
71             "next",
72             "hasNext",
73             // Bitwise operators
74             "and",
75             "or",
76             // Property delegation operators
77             "getValue",
78             "setValue",
79             "provideDelegate",
80             // assign operator - Gradle compiler plugin
81             // https://blog.gradle.org/simpler-kotlin-dsl-property-assignment
82             "assign",
83         )
84 
85     private val COMPONENT_OPERATOR_REGEX = Regex("component\\d+")
86 
87     private val KDOC_TAG_SKIP_FIRST_REFERENCE_REGEX = Regex("^@(param|property) (.+)")
88   }
89 
90   private var thisPackage: FqName? = null
91 
92   private val usedReferences = OPERATORS.toMutableSet()
93 
94   private lateinit var importCleanUpCandidates: Set<KtImportDirective>
95 
96   private var isPackageElement = false
97   private var isImportElement = false
98 
99   fun takePackageDirective(directive: KtPackageDirective, superBlock: () -> Unit) {
100     if (!enabled) {
101       return superBlock.invoke()
102     }
103 
104     thisPackage = directive.fqName
105 
106     isPackageElement = true
107     superBlock.invoke()
108     isPackageElement = false
109   }
110 
111   fun takeImportList(importList: KtImportList, superBlock: () -> Unit) {
112     if (!enabled) {
113       return superBlock.invoke()
114     }
115 
116     importCleanUpCandidates =
117         importList.imports
118             .filter { import ->
119               val identifier = import.identifier ?: return@filter false
120               import.isValidImport &&
121                   identifier !in OPERATORS &&
122                   !COMPONENT_OPERATOR_REGEX.matches(identifier)
123             }
124             .toSet()
125 
126     isImportElement = true
127     superBlock.invoke()
128     isImportElement = false
129   }
130 
131   fun takeKdoc(kdoc: KDocImpl) {
132     kdoc.getChildrenOfType<KDocSection>().forEach { kdocSection ->
133       val tagLinks =
134           kdocSection.getChildrenOfType<KDocTag>().flatMap { tag ->
135             val tagLinks = tag.getChildrenOfType<KDocLink>().toList()
136             when {
137               KDOC_TAG_SKIP_FIRST_REFERENCE_REGEX.matches(tag.text) -> tagLinks.drop(1)
138               else -> tagLinks
139             }
140           }
141 
142       val links = kdocSection.getChildrenOfType<KDocLink>() + tagLinks
143 
144       val references =
145           links.flatMap { link ->
146             link.getChildrenOfType<KDocName>().mapNotNull {
147               it.getQualifiedName().firstOrNull()?.trim('[', ']')
148             }
149           }
150 
151       usedReferences += references
152     }
153   }
154 
155   fun takeReferenceExpression(expression: KtReferenceExpression) {
156     if (!enabled) return
157 
158     if (!isPackageElement && !isImportElement && expression.children.isEmpty()) {
159       usedReferences += expression.text.trim('`')
160     }
161   }
162 
163   fun getRedundantImportElements(): List<PsiElement> {
164     if (!enabled) return emptyList()
165 
166     val identifierCounts =
167         importCleanUpCandidates.groupBy { it.identifier }.mapValues { it.value.size }
168 
169     return importCleanUpCandidates.filter {
170       val isUsed = it.identifier in usedReferences
171       val isFromThisPackage = it.importedFqName?.parent() == thisPackage
172       val hasAlias = it.alias != null
173       val isOverload = requireNotNull(identifierCounts[it.identifier]) > 1
174       // Remove if...
175       !isUsed || (isFromThisPackage && !hasAlias && !isOverload)
176     }
177   }
178 
179   /** The imported short name, possibly an alias name, if any. */
180   private inline val KtImportDirective.identifier: String?
181     get() = importPath?.importedName?.identifier?.trim('`')
182 }
183