• 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 java.util.function.Predicate
41 
42 /** Whether we should limit import statements to symbols found in class docs  */
43 private const val ONLY_IMPORT_CLASSES_REFERENCED_IN_DOCS = true
44 
45 class PsiCompilationUnit(val codebase: PsiBasedCodebase, containingFile: PsiFile) : CompilationUnit(containingFile) {
getHeaderCommentsnull46     override fun getHeaderComments(): String? {
47         // https://youtrack.jetbrains.com/issue/KT-22135
48         if (file is PsiJavaFile) {
49             val pkg = file.packageStatement ?: return null
50             return file.text.substring(0, pkg.startOffset)
51         } else if (file is KtFile) {
52             var curr: PsiElement? = file.firstChild
53             var comment: String? = null
54             while (curr != null) {
55                 if (curr is PsiComment || curr is KDoc) {
56                     val text = curr.text
57                     comment = if (comment != null) {
58                         comment + "\n" + text
59                     } else {
60                         text
61                     }
62                 } else if (curr !is PsiWhiteSpace) {
63                     break
64                 }
65                 curr = curr.nextSibling
66             }
67             return comment
68         }
69 
70         return super.getHeaderComments()
71     }
72 
getImportStatementsnull73     override fun getImportStatements(predicate: Predicate<Item>): Collection<Item> {
74         val imports = mutableListOf<Item>()
75 
76         if (file is PsiJavaFile) {
77             val importList = file.importList
78             if (importList != null) {
79                 for (importStatement in importList.importStatements) {
80                     val resolved = importStatement.resolve() ?: continue
81                     if (resolved is PsiClass) {
82                         val classItem = codebase.findClass(resolved) ?: continue
83                         if (predicate.test(classItem)) {
84                             imports.add(classItem)
85                         }
86                     } else if (resolved is PsiPackage) {
87                         val pkgItem = codebase.findPackage(resolved.qualifiedName) ?: continue
88                         if (predicate.test(pkgItem) &&
89                             // Also make sure it isn't an empty package (after applying the filter)
90                             // since in that case we'd have an invalid import
91                             pkgItem.topLevelClasses().any { it.emit && predicate.test(it) }
92                         ) {
93                             imports.add(pkgItem)
94                         }
95                     } else if (resolved is PsiMethod) {
96                         codebase.findClass(resolved.containingClass ?: continue) ?: continue
97                         val methodItem = codebase.findMethod(resolved)
98                         if (predicate.test(methodItem)) {
99                             imports.add(methodItem)
100                         }
101                     } else if (resolved is PsiField) {
102                         val classItem = codebase.findClass(resolved.containingClass ?: continue) ?: continue
103                         val fieldItem = classItem.findField(resolved.name, true, false) ?: continue
104                         if (predicate.test(fieldItem)) {
105                             imports.add(fieldItem)
106                         }
107                     }
108                 }
109             }
110         } else if (file is KtFile) {
111             for (importDirective in file.importDirectives) {
112                 val resolved = importDirective.reference?.resolve() ?: continue
113                 if (resolved is PsiClass) {
114                     val classItem = codebase.findClass(resolved) ?: continue
115                     if (predicate.test(classItem)) {
116                         imports.add(classItem)
117                     }
118                 }
119             }
120         }
121 
122         // Next only keep those that are present in any docs; those are the only ones
123         // we need to import
124         if (!imports.isEmpty()) {
125             val map: Multimap<String, Item> = ArrayListMultimap.create()
126             for (item in imports) {
127                 if (item is ClassItem) {
128                     map.put(item.simpleName(), item)
129                 } else if (item is MemberItem) {
130                     map.put(item.name(), item)
131                 }
132             }
133 
134             // Compute set of import statements that are actually referenced
135             // from the documentation (we do inexact matching here; we don't
136             // need to have an exact set of imports since it's okay to have
137             // some extras). This isn't a big problem since our codestyle
138             // forbids/discourages wildcards, so it shows up in fewer places,
139             // but we need to handle it when it does -- such as in ojluni.
140 
141             @Suppress("ConstantConditionIf")
142             return if (ONLY_IMPORT_CLASSES_REFERENCED_IN_DOCS) {
143                 val result = mutableListOf<Item>()
144 
145                 // We keep the wildcard imports since we don't know which ones of those are relevant
146                 imports.filter { it is PackageItem }.forEach { result.add(it) }
147 
148                 for (cls in classes(predicate)) {
149                     cls.accept(object : ItemVisitor() {
150                         override fun visitItem(item: Item) {
151                             val doc = item.documentation
152                             if (doc.isNotBlank()) {
153                                 var found: MutableList<String>? = null
154                                 for (name in map.keys()) {
155                                     if (docContainsWord(doc, name)) {
156                                         if (found == null) {
157                                             found = mutableListOf()
158                                         }
159                                         found.add(name)
160                                     }
161                                 }
162                                 found?.let {
163                                     for (name in found) {
164                                         val all = map.get(name) ?: continue
165                                         for (referenced in all) {
166                                             if (!result.contains(referenced)) {
167                                                 result.add(referenced)
168                                             }
169                                         }
170                                         map.removeAll(name)
171                                     }
172                                 }
173                             }
174                         }
175                     })
176                 }
177                 result
178             } else {
179                 imports
180             }
181         }
182 
183         return emptyList()
184     }
185 
classesnull186     private fun classes(predicate: Predicate<Item>): List<ClassItem> {
187         val topLevel = mutableListOf<ClassItem>()
188         if (file is PsiClassOwner) {
189             for (psiClass in file.classes) {
190                 val classItem = codebase.findClass(psiClass) ?: continue
191                 if (predicate.test(classItem)) {
192                     topLevel.add(classItem)
193                 }
194             }
195         }
196 
197         return topLevel
198     }
199 
200     companion object {
201         // Cache pattern compilation across source files
202         private val regexMap = HashMap<String, Regex>()
203 
docContainsWordnull204         private fun docContainsWord(doc: String, word: String): Boolean {
205             if (!doc.contains(word)) {
206                 return false
207             }
208 
209             val regex = regexMap[word] ?: run {
210                 val new = Regex("""\b$word\b""")
211                 regexMap[word] = new
212                 new
213             }
214             return regex.find(doc) != null
215         }
216     }
217 }