• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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