1 /*
2 * Copyright (C) 2024 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.reporter.BaselineKey
20 import com.android.tools.metalava.reporter.FileLocation
21 import com.android.tools.metalava.reporter.Issues
22 import com.android.tools.metalava.reporter.Reporter
23 import com.intellij.openapi.util.TextRange
24 import com.intellij.openapi.vfs.VfsUtilCore
25 import com.intellij.psi.PsiClass
26 import com.intellij.psi.PsiCompiledElement
27 import com.intellij.psi.PsiElement
28 import com.intellij.psi.PsiField
29 import com.intellij.psi.PsiFile
30 import com.intellij.psi.PsiMethod
31 import com.intellij.psi.PsiModifierListOwner
32 import com.intellij.psi.PsiNameIdentifierOwner
33 import com.intellij.psi.PsiPackage
34 import com.intellij.psi.PsiParameter
35 import com.intellij.psi.impl.light.LightElement
36 import java.nio.file.Path
37 import org.jetbrains.kotlin.fileClasses.javaFileFacadeFqName
38 import org.jetbrains.kotlin.psi.KtClass
39 import org.jetbrains.kotlin.psi.KtFunction
40 import org.jetbrains.kotlin.psi.KtModifierListOwner
41 import org.jetbrains.kotlin.psi.KtProperty
42 import org.jetbrains.kotlin.psi.psiUtil.containingClass
43 import org.jetbrains.kotlin.psi.psiUtil.parameterIndex
44 import org.jetbrains.uast.UClass
45 import org.jetbrains.uast.UElement
46 import org.jetbrains.uast.toUElement
47
48 /** A [FileLocation] that wraps [psiElement] and computes the [path] and [line] number on demand. */
49 class PsiFileLocation(private val psiElement: PsiElement) : FileLocation() {
50 /**
51 * Backing property for the [path] getter.
52 *
53 * This must only be accessed after calling [ensureInitialized].
54 */
55 private var _path: Path? = null
56
57 /**
58 * Backing property for the [line] getter.
59 *
60 * If this is [Int.MAX_VALUE] then this has not been initialized.
61 *
62 * This must only be accessed after calling [ensureInitialized].
63 */
64 private var _line: Int = Int.MIN_VALUE
65
66 override val path: Path?
67 get() {
68 ensureInitialized()
69 return _path
70 }
71
72 override val line: Int
73 get() {
74 ensureInitialized()
75 return _line
76 }
77
78 override val baselineKey: BaselineKey
79 get() = getBaselineKey(psiElement)
80
81 /**
82 * Make sure that this is initialized, if it is not then compute the [path] and [line] from the
83 * [psiElement].
84 */
ensureInitializednull85 private fun ensureInitialized() {
86 if (_line != Int.MIN_VALUE) {
87 return
88 }
89
90 // Record that this has been initialized. This will make sure that whatever happens this
91 // method does not get run multiple times on a single instance.
92 _line = 0
93
94 val psiFile = psiElement.containingFile ?: return
95 val virtualFile = psiFile.virtualFile ?: return
96
97 // Record the path.
98 _path =
99 try {
100 virtualFile.toNioPath().toAbsolutePath()
101 } catch (e: UnsupportedOperationException) {
102 return
103 }
104
105 // Unwrap UAST for accurate Kotlin line numbers (UAST synthesizes text offsets sometimes)
106 val sourceElement = (psiElement as? UElement)?.sourcePsi ?: psiElement
107
108 // Skip doc comments for classes, methods and fields by pointing at the line where the
109 // element's name is or falling back to the first line of its modifier list (which may
110 // include annotations) or lastly to the start of the element itself
111 val rangeElement =
112 (sourceElement as? PsiNameIdentifierOwner)?.nameIdentifier
113 ?: (sourceElement as? KtModifierListOwner)?.modifierList
114 ?: (sourceElement as? PsiModifierListOwner)?.modifierList ?: sourceElement
115
116 val range = getTextRange(rangeElement)
117
118 // Update the line number.
119 _line =
120 if (range == null) {
121 -1 // No source offsets, use invalid line number
122 } else {
123 getLineNumber(psiFile.text, range.startOffset) + 1
124 }
125 }
126
127 companion object {
128 /**
129 * Compute a [FileLocation] from a [PsiElement]
130 *
131 * @param element the optional element from which the path, line and [BaselineKey] will be
132 * computed.
133 */
fromPsiElementnull134 fun fromPsiElement(element: PsiElement?): FileLocation {
135 return element?.let { PsiFileLocation(it) } ?: FileLocation.UNKNOWN
136 }
137
getTextRangenull138 private fun getTextRange(element: PsiElement): TextRange? {
139 var range: TextRange? = null
140
141 if (element is UClass) {
142 range = element.sourcePsi?.textRange
143 } else if (element is PsiCompiledElement) {
144 if (element is LightElement) {
145 range = (element as PsiElement).textRange
146 }
147 if (range == null || TextRange.EMPTY_RANGE == range) {
148 return null
149 }
150 } else {
151 range = element.textRange
152 }
153
154 return range
155 }
156
157 /** Returns the 0-based line number of character position <offset> in <text> */
getLineNumbernull158 private fun getLineNumber(text: String, offset: Int): Int {
159 var line = 0
160 var curr = 0
161 val target = offset.coerceAtMost(text.length)
162 while (curr < target) {
163 if (text[curr++] == '\n') {
164 line++
165 }
166 }
167 return line
168 }
169
getBaselineKeynull170 internal fun getBaselineKey(element: PsiElement?): BaselineKey {
171 element ?: return BaselineKey.UNKNOWN
172 return when (element) {
173 is PsiFile -> {
174 val virtualFile = element.virtualFile
175 val file = VfsUtilCore.virtualToIoFile(virtualFile)
176 BaselineKey.forPath(file.toPath())
177 }
178 else -> {
179 val elementId = getElementId(element)
180 BaselineKey.forElementId(elementId)
181 }
182 }
183 }
184
getElementIdnull185 private fun getElementId(element: PsiElement): String {
186 return when (element) {
187 is PsiClass -> element.qualifiedName ?: element.name ?: "?"
188 is KtClass -> element.fqName?.asString() ?: element.name ?: "?"
189 is PsiMethod -> {
190 val containingClass = element.containingClass
191 val name = element.name
192 val parameterList =
193 "(" +
194 element.parameterList.parameters.joinToString {
195 it.type.canonicalText
196 } +
197 ")"
198 if (containingClass != null) {
199 getElementId(containingClass) + "#" + name + parameterList
200 } else {
201 name + parameterList
202 }
203 }
204 is PsiField -> {
205 val containingClass = element.containingClass
206 val name = element.name
207 if (containingClass != null) {
208 getElementId(containingClass) + "#" + name
209 } else {
210 name
211 }
212 }
213 is KtProperty -> {
214 val containingClass =
215 element.containingClass()?.let { getElementId(it) }
216 // If there is no containing class, find the file facade class because that
217 // will be the containing class in the Codebase.
218 ?: element.containingKtFile.javaFileFacadeFqName.asString()
219 val name = element.nameAsSafeName.asString()
220 "$containingClass#$name"
221 }
222 is PsiPackage -> element.qualifiedName
223 is PsiParameter -> {
224 val method = element.declarationScope.parent
225 if (method is PsiMethod) {
226 getElementId(method) + " parameter #" + element.parameterIndex()
227 } else {
228 "?"
229 }
230 }
231 is KtFunction -> {
232 // Try converting this to the Java API view (as a PsiMethod)
233 (element.toUElement()?.javaPsi as? PsiMethod)?.let { getElementId(it) }
234 ?: element.toString()
235 }
236 else -> element.toString()
237 }
238 }
239 }
240 }
241
Reporternull242 fun Reporter.report(id: Issues.Issue, element: PsiElement?, message: String): Boolean {
243 val location = PsiFileLocation.fromPsiElement(element)
244 return report(id, null, message, location)
245 }
246