• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2025 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 package com.android.platform.test.ravenwood.ravenhelper.sourcemap
17 
18 /*
19  * This file contains classes used to parse Java source files to build "source map" which
20  * basically tells you what classes/methods/fields are declared in what line of what file.
21  */
22 
23 import com.android.hoststubgen.GeneralUserErrorException
24 import com.android.hoststubgen.log
25 import com.android.tools.lint.UastEnvironment
26 import com.intellij.openapi.editor.Document
27 import com.intellij.openapi.vfs.StandardFileSystems
28 import com.intellij.psi.PsiClass
29 import com.intellij.psi.PsiClassOwner
30 import com.intellij.psi.PsiElement
31 import com.intellij.psi.PsiFile
32 import com.intellij.psi.PsiManager
33 import com.intellij.psi.PsiMethod
34 import com.intellij.psi.PsiNameIdentifierOwner
35 import com.intellij.psi.SyntheticElement
36 import java.io.File
37 
38 
39 /**
40  * Represents the location of an item. (class, field or method)
41  */
42 data class Location (
43     /** Full path filename. */
44     val file: String,
45 
46     /** 1-based line number */
47     val line: Int,
48 
49     /** Indent of the line */
50     val indent: Int,
51 ) {
52 
53     fun getIndent(): String {
54         return " ".repeat(indent)
55     }
56 
57     fun dump() {
58         log.i("Location: $file:$line (indent: $indent)")
59     }
60 }
61 
62 /**
63  * Represents the type of item.
64  */
65 enum class ItemType {
66     Class,
67     Field,
68     Method,
69 }
70 
71 /** Holds a field's location. */
72 data class FieldInfo (
73     val name: String,
74     val location: Location,
75 ) {
dumpnull76     fun dump() {
77         log.i("Field: $name")
78         log.withIndent {
79             location.dump()
80         }
81     }
82 }
83 
84 /** Holds a method's location. */
85 data class MethodInfo (
86     val name: String,
87     /** "Simplified" description. */
88     val simpleDesc: String,
89     val location: Location,
90 ) {
dumpnull91     fun dump() {
92         log.i("Method: $name$simpleDesc")
93         log.withIndent {
94             location.dump()
95         }
96     }
97 }
98 
99 /** Holds a class's location and members. */
100 data class ClassInfo (
101     val fullName: String,
102     val location: Location,
103     val fields: MutableMap<String, FieldInfo> = mutableMapOf(),
104     val methods: MutableMap<String, MutableList<MethodInfo>> = mutableMapOf(),
105 ) {
addnull106     fun add(fi: FieldInfo) {
107         fields.put(fi.name, fi)
108     }
109 
addnull110     fun add(mi: MethodInfo) {
111         val list = methods.get(mi.name)
112         if (list != null) {
113             list.add(mi)
114         } else {
115             methods.put(mi.name, mutableListOf(mi))
116         }
117     }
118 
dumpnull119     fun dump() {
120         log.i("Class: $fullName")
121         log.withIndent {
122             location.dump()
123 
124             // Sort and print fields and methods.
125             methods.toSortedMap().forEach { entry ->
126                 entry.value.sortedBy { method -> method.simpleDesc }.forEach {
127                     it.dump()
128                 }
129             }
130         }
131     }
132 
133     /** Find a field by name */
findFieldnull134     fun findField(fieldName: String): FieldInfo? {
135         return fields[fieldName]
136     }
137 
138     /**
139      * Find a field by name and descriptor.
140      *
141      * If [descriptor] is "*", then all methods with the name will be returned.
142      */
findMethodsnull143     fun findMethods(methodName: String, methodDesc: String): List<MethodInfo>? {
144         val list = methods[methodName] ?: return null
145 
146         // Wildcard method policy.
147         if (methodDesc == "*") {
148             return list
149         }
150 
151         val simpleDesc = simplifyMethodDesc(methodDesc)
152         list.forEach { mi ->
153             if (simpleDesc == mi.simpleDesc) {
154                 return listOf(mi)
155             }
156         }
157         log.w("Method $fullName.$methodName found, but none match description '$methodDesc'")
158         return null
159     }
160 }
161 
162 /**
163  * Stores all classes
164  */
165 data class AllClassInfo (
166     val classes: MutableMap<String, ClassInfo> = mutableMapOf(),
167 ) {
addnull168     fun add(ci: ClassInfo) {
169         classes.put(ci.fullName, ci)
170     }
171 
dumpnull172     fun dump() {
173         classes.toSortedMap { a, b -> a.compareTo(b) }.forEach {
174             it.value.dump()
175         }
176     }
177 
findClassnull178     fun findClass(name: String): ClassInfo? {
179         return classes.get(name)
180     }
181 }
182 
typeToSimpleDescnull183 fun typeToSimpleDesc(origType: String): String {
184     var type = origType
185 
186     // Detect arrays.
187     var arrayPrefix = ""
188     while (type.endsWith("[]")) {
189         arrayPrefix += "["
190         type = type.substring(0, type.length - 2)
191     }
192 
193     // Delete generic parameters. (delete everything after '<')
194     type.indexOf('<').let { pos ->
195         if (pos >= 0) {
196             type = type.substring(0, pos)
197         }
198     }
199 
200     // Handle builtins.
201     val builtinType = when (type) {
202         "byte" -> "B"
203         "short" -> "S"
204         "int" -> "I"
205         "long" -> "J"
206         "float" -> "F"
207         "double" -> "D"
208         "boolean" -> "Z"
209         "char" -> "C"
210         "void" -> "V"
211         else -> null
212     }
213 
214     builtinType?.let {
215         return arrayPrefix + builtinType
216     }
217 
218     return arrayPrefix + "L" + type + ";"
219 }
220 
221 /**
222  * Get a "simple" description of a method.
223  *
224  * "Simple" descriptions are similar to "real" ones, except:
225  * - No return type.
226  * - No package names in type names.
227  */
getSimpleDescnull228 fun getSimpleDesc(method: PsiMethod): String {
229     val sb = StringBuilder()
230 
231     sb.append("(")
232 
233     val params = method.parameterList
234     for (i in 0..<params.parametersCount) {
235         val param = params.getParameter(i)
236 
237         val type = param?.type?.presentableText
238 
239         if (type == null) {
240             throw RuntimeException(
241                 "Unable to decode parameter list from method from ${params.parent}")
242         }
243 
244         sb.append(typeToSimpleDesc(type))
245     }
246 
247     sb.append(")")
248 
249     return sb.toString()
250 }
251 
252 private val reTypeFinder = "L.*/".toRegex()
253 
simplifyMethodDescnull254 private fun simplifyMethodDesc(origMethodDesc: String): String {
255     // We don't need the return type, so remove everything after the ')'.
256     val pos = origMethodDesc.indexOf(')')
257     var desc = if (pos < 0) { origMethodDesc } else { origMethodDesc.substring(0, pos + 1) }
258 
259     // Then we remove the package names from all the class names.
260     // i.e. convert "Ljava/lang/String" to "LString".
261 
262     return desc.replace(reTypeFinder, "L")
263 }
264 
265 /**
266  * Class that reads and parses java source files using PSI and populate [AllClassInfo].
267  */
268 class SourceLoader(
269     val environment: UastEnvironment,
270 ) {
271     private val fileSystem = StandardFileSystems.local()
272     private val manager = PsiManager.getInstance(environment.ideaProject)
273 
274     /** Classes that were parsed */
275     private var numParsedClasses = 0
276 
277     /**
278      * Main entry point.
279      */
loadnull280     fun load(filesOrDirectories: List<String>, classes: AllClassInfo) {
281         val psiFiles = mutableListOf<PsiFile>()
282         log.i("Loading source files...")
283         log.iTime("Discovering source files") {
284             load(filesOrDirectories.map { File(it) }, psiFiles)
285         }
286 
287         log.i("${psiFiles.size} file(s) found.")
288 
289         if (psiFiles.size == 0) {
290             throw GeneralUserErrorException("No source files found.")
291         }
292 
293         log.iTime("Parsing source files") {
294             log.withIndent {
295                 for (file in psiFiles.asSequence().distinct()) {
296                     val classesInFile = (file as? PsiClassOwner)?.classes?.toList()
297                     classesInFile?.forEach { clazz ->
298                         loadClass(clazz)?.let { classes.add(it) }
299 
300                         clazz.innerClasses.forEach { inner ->
301                             loadClass(inner)?.let { classes.add(it) }
302                         }
303                     }
304                 }
305             }
306         }
307         log.i("$numParsedClasses class(es) found.")
308     }
309 
loadnull310     private fun load(filesOrDirectories: List<File>, result: MutableList<PsiFile>) {
311         filesOrDirectories.forEach {
312             load(it, result)
313         }
314     }
315 
loadnull316     private fun load(file: File, result: MutableList<PsiFile>) {
317         if (file.isDirectory) {
318             file.listFiles()?.forEach { child ->
319                 load(child, result)
320             }
321             return
322         }
323 
324         // It's a file
325         when (file.extension) {
326             "java" -> {
327                 // Load it.
328             }
329             "kt" -> {
330                 log.w("Kotlin not supported, not loading ${file.path}")
331                 return
332             }
333             else -> return // Silently skip
334         }
335         fileSystem.findFileByPath(file.path)?.let { virtualFile ->
336             manager.findFile(virtualFile)?.let { psiFile ->
337                 result.add(psiFile)
338             }
339         }
340     }
341 
loadClassnull342     private fun loadClass(clazz: PsiClass): ClassInfo? {
343         if (clazz is SyntheticElement) {
344             return null
345         }
346         log.forVerbose {
347             log.v("Class found: ${clazz.qualifiedName}")
348         }
349         numParsedClasses++
350 
351         log.withIndent {
352             val ci = ClassInfo(
353                 clazz.qualifiedName!!,
354                 getLocation(clazz) ?: return null,
355             )
356 
357             // Load fields.
358             clazz.fields.filter { it !is SyntheticElement }.forEach {
359                 val name = it.name
360                 log.forDebug { log.d("Field found: $name") }
361                 val loc = getLocation(it) ?: return@forEach
362                 ci.add(FieldInfo(name, loc))
363             }
364 
365             // Load methods.
366             clazz.methods.filter { it !is SyntheticElement }.forEach {
367                 val name = resolveMethodName(it)
368                 val simpleDesc = getSimpleDesc(it)
369                 log.forDebug { log.d("Method found: $name$simpleDesc") }
370                 val loc = getLocation(it) ?: return@forEach
371                 ci.add(MethodInfo(name, simpleDesc, loc))
372             }
373             return ci
374         }
375     }
376 
resolveMethodNamenull377     private fun resolveMethodName(method: PsiMethod): String {
378         val clazz = method.containingClass!!
379         if (clazz.name == method.name) {
380             return "<init>" // It's a constructor.
381         }
382         return method.name
383     }
384 
getLocationnull385     private fun getLocation(elem: PsiElement): Location? {
386         val lineAndIndent = getLineNumberAndIndent(elem)
387         if (lineAndIndent == null) {
388             log.w("Unable to determine location of $elem")
389             return null
390         }
391         return Location(
392             elem.containingFile.originalFile.virtualFile.path,
393             lineAndIndent.first,
394             lineAndIndent.second,
395         )
396     }
397 
getLineNumberAndIndentnull398     private fun getLineNumberAndIndent(element: PsiElement): Pair<Int, Int>? {
399         val psiFile: PsiFile = element.containingFile ?: return null
400         val document: Document = psiFile.viewProvider.document ?: return null
401 
402         // Actual elements such as PsiClass, PsiMethod and PsiField contains the leading
403         // javadoc, etc, so use the "identifier"'s element, if available.
404         // For synthesized elements, this may return null.
405         val targetRange = (
406                 (element as PsiNameIdentifierOwner).nameIdentifier?.textRange ?: element.textRange
407                 ) ?: return null
408         val lineNumber = document.getLineNumber(targetRange.startOffset)
409         val lineStartOffset = document.getLineStartOffset(lineNumber)
410 
411         val lineLeadingText = document.getText(
412             com.intellij.openapi.util.TextRange(lineStartOffset, targetRange.startOffset))
413 
414         val indent = lineLeadingText.takeWhile { it.isWhitespace() }.length
415 
416         // Line numbers are 0-based, add 1 for human-readable format
417         return Pair(lineNumber + 1, indent)
418     }
419 }