• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
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.android.tools.metalava.model.psi
18 
19 import com.android.tools.metalava.model.ClassItem
20 import com.android.tools.metalava.model.CompilationUnit
21 import com.android.tools.metalava.model.Item
22 import com.android.tools.metalava.model.MemberItem
23 import com.android.tools.metalava.model.PackageItem
24 import com.android.tools.metalava.model.visitors.ItemVisitor
25 import com.google.common.collect.ArrayListMultimap
26 import com.google.common.collect.Multimap
27 import com.intellij.psi.PsiClass
28 import com.intellij.psi.PsiClassOwner
29 import com.intellij.psi.PsiComment
30 import com.intellij.psi.PsiElement
31 import com.intellij.psi.PsiField
32 import com.intellij.psi.PsiFile
33 import com.intellij.psi.PsiJavaFile
34 import com.intellij.psi.PsiMethod
35 import com.intellij.psi.PsiPackage
36 import com.intellij.psi.PsiWhiteSpace
37 import org.jetbrains.kotlin.kdoc.psi.api.KDoc
38 import org.jetbrains.kotlin.psi.KtFile
39 import org.jetbrains.kotlin.psi.psiUtil.startOffset
40 import org.jetbrains.uast.UFile
41 import java.util.function.Predicate
42 
43 /** Whether we should limit import statements to symbols found in class docs  */
44 private const val ONLY_IMPORT_CLASSES_REFERENCED_IN_DOCS = true
45 
46 class PsiCompilationUnit(
47     val codebase: PsiBasedCodebase,
48     uFile: UFile?,
49     containingFile: PsiFile
50 ) : CompilationUnit(containingFile, uFile) {
getHeaderCommentsnull51     override fun getHeaderComments(): String? {
52         if (uFile != null) {
53             var comment: String? = null
54             for (uComment in uFile.allCommentsInFile) {
55                 val text = uComment.text
56                 comment = if (comment != null) {
57                     comment + "\n" + text
58                 } else {
59                     text
60                 }
61             }
62             return comment
63         }
64 
65         // https://youtrack.jetbrains.com/issue/KT-22135
66         if (file is PsiJavaFile) {
67             val pkg = file.packageStatement ?: return null
68             return file.text.substring(0, pkg.startOffset)
69         } else if (file is KtFile) {
70             var curr: PsiElement? = file.firstChild
71             var comment: String? = null
72             while (curr != null) {
73                 if (curr is PsiComment || curr is KDoc) {
74                     val text = curr.text
75                     comment = if (comment != null) {
76                         comment + "\n" + text
77                     } else {
78                         text
79                     }
80                 } else if (curr !is PsiWhiteSpace) {
81                     break
82                 }
83                 curr = curr.nextSibling
84             }
85             return comment
86         }
87 
88         return super.getHeaderComments()
89     }
90 
getImportStatementsnull91     override fun getImportStatements(predicate: Predicate<Item>): Collection<Item> {
92         val imports = mutableListOf<Item>()
93 
94         if (file is PsiJavaFile) {
95             val importList = file.importList
96             if (importList != null) {
97                 for (importStatement in importList.importStatements) {
98                     val resolved = importStatement.resolve() ?: continue
99                     if (resolved is PsiClass) {
100                         val classItem = codebase.findClass(resolved) ?: continue
101                         if (predicate.test(classItem)) {
102                             imports.add(classItem)
103                         }
104                     } else if (resolved is PsiPackage) {
105                         val pkgItem = codebase.findPackage(resolved.qualifiedName) ?: continue
106                         if (predicate.test(pkgItem) &&
107                             // Also make sure it isn't an empty package (after applying the filter)
108                             // since in that case we'd have an invalid import
109                             pkgItem.topLevelClasses().any { it.emit && predicate.test(it) }
110                         ) {
111                             imports.add(pkgItem)
112                         }
113                     } else if (resolved is PsiMethod) {
114                         codebase.findClass(resolved.containingClass ?: continue) ?: continue
115                         val methodItem = codebase.findMethod(resolved)
116                         if (predicate.test(methodItem)) {
117                             imports.add(methodItem)
118                         }
119                     } else if (resolved is PsiField) {
120                         val classItem = codebase.findClass(resolved.containingClass ?: continue) ?: continue
121                         val fieldItem = classItem.findField(
122                             resolved.name,
123                             includeSuperClasses = true,
124                             includeInterfaces = false
125                         ) ?: continue
126                         if (predicate.test(fieldItem)) {
127                             imports.add(fieldItem)
128                         }
129                     }
130                 }
131             }
132         } else if (file is KtFile) {
133             for (importDirective in file.importDirectives) {
134                 val resolved = importDirective.reference?.resolve() ?: continue
135                 if (resolved is PsiClass) {
136                     val classItem = codebase.findClass(resolved) ?: continue
137                     if (predicate.test(classItem)) {
138                         imports.add(classItem)
139                     }
140                 }
141             }
142         }
143 
144         // Next only keep those that are present in any docs; those are the only ones
145         // we need to import
146         if (imports.isNotEmpty()) {
147             val map: Multimap<String, Item> = ArrayListMultimap.create()
148             for (item in imports) {
149                 if (item is ClassItem) {
150                     map.put(item.simpleName(), item)
151                 } else if (item is MemberItem) {
152                     map.put(item.name(), item)
153                 }
154             }
155 
156             // Compute set of import statements that are actually referenced
157             // from the documentation (we do inexact matching here; we don't
158             // need to have an exact set of imports since it's okay to have
159             // some extras). This isn't a big problem since our code style
160             // forbids/discourages wildcards, so it shows up in fewer places,
161             // but we need to handle it when it does -- such as in ojluni.
162 
163             @Suppress("ConstantConditionIf")
164             return if (ONLY_IMPORT_CLASSES_REFERENCED_IN_DOCS) {
165                 val result = mutableListOf<Item>()
166 
167                 // We keep the wildcard imports since we don't know which ones of those are relevant
168                 imports.filterIsInstance<PackageItem>().forEach { result.add(it) }
169 
170                 for (cls in classes(predicate)) {
171                     cls.accept(object : ItemVisitor() {
172                         override fun visitItem(item: Item) {
173                             // Do not let documentation on hidden items affect the imports.
174                             if (!predicate.test(item)) {
175                                 return
176                             }
177                             val doc = item.documentation
178                             if (doc.isNotBlank()) {
179                                 var found: MutableList<String>? = null
180                                 for (name in map.keys()) {
181                                     if (docContainsWord(doc, name)) {
182                                         if (found == null) {
183                                             found = mutableListOf()
184                                         }
185                                         found.add(name)
186                                     }
187                                 }
188                                 found?.let {
189                                     for (name in found) {
190                                         val all = map.get(name) ?: continue
191                                         for (referenced in all) {
192                                             if (!result.contains(referenced)) {
193                                                 result.add(referenced)
194                                             }
195                                         }
196                                         map.removeAll(name)
197                                     }
198                                 }
199                             }
200                         }
201                     })
202                 }
203                 result
204             } else {
205                 imports
206             }
207         }
208 
209         return emptyList()
210     }
211 
classesnull212     private fun classes(predicate: Predicate<Item>): List<ClassItem> {
213         val topLevel = mutableListOf<ClassItem>()
214         if (file is PsiClassOwner) {
215             for (psiClass in file.classes) {
216                 val classItem = codebase.findClass(psiClass) ?: continue
217                 if (predicate.test(classItem)) {
218                     topLevel.add(classItem)
219                 }
220             }
221         }
222 
223         return topLevel
224     }
225 
226     companion object {
227         // Cache pattern compilation across source files
228         private val regexMap = HashMap<String, Regex>()
229 
docContainsWordnull230         private fun docContainsWord(doc: String, word: String): Boolean {
231             if (!doc.contains(word)) {
232                 return false
233             }
234 
235             val regex = regexMap[word] ?: run {
236                 val new = Regex("""\b$word\b""")
237                 regexMap[word] = new
238                 new
239             }
240             return regex.find(doc) != null
241         }
242     }
243 }