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 }