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 }