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