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 }