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