1 /*
<lambda>null2 * Copyright (C) 2023 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.turbine
18
19 import com.android.tools.metalava.model.AnnotationItem
20 import com.android.tools.metalava.model.ApiVariantSelectors
21 import com.android.tools.metalava.model.ClassItem
22 import com.android.tools.metalava.model.ClassOrigin
23 import com.android.tools.metalava.model.Item
24 import com.android.tools.metalava.model.ItemDocumentation.Companion.toItemDocumentationFactory
25 import com.android.tools.metalava.model.ItemLanguage
26 import com.android.tools.metalava.model.JAVA_PACKAGE_INFO
27 import com.android.tools.metalava.model.PackageFilter
28 import com.android.tools.metalava.model.TypeParameterScope
29 import com.android.tools.metalava.model.VisibilityLevel
30 import com.android.tools.metalava.model.createImmutableModifiers
31 import com.android.tools.metalava.model.item.DefaultCodebaseAssembler
32 import com.android.tools.metalava.model.item.DefaultCodebaseFactory
33 import com.android.tools.metalava.model.item.DefaultItemFactory
34 import com.android.tools.metalava.model.item.DefaultPackageItem
35 import com.android.tools.metalava.model.item.MutablePackageDoc
36 import com.android.tools.metalava.model.item.PackageDocs
37 import com.android.tools.metalava.model.source.SourceSet
38 import com.android.tools.metalava.model.source.utils.gatherPackageJavadoc
39 import com.android.tools.metalava.reporter.FileLocation
40 import com.google.common.collect.ImmutableList
41 import com.google.common.collect.ImmutableMap
42 import com.google.turbine.binder.Binder
43 import com.google.turbine.binder.Binder.BindingResult
44 import com.google.turbine.binder.ClassPathBinder
45 import com.google.turbine.binder.Processing.ProcessorInfo
46 import com.google.turbine.binder.bound.SourceTypeBoundClass
47 import com.google.turbine.binder.bound.TypeBoundClass
48 import com.google.turbine.binder.env.CompoundEnv
49 import com.google.turbine.binder.env.SimpleEnv
50 import com.google.turbine.binder.lookup.LookupKey
51 import com.google.turbine.binder.lookup.TopLevelIndex
52 import com.google.turbine.binder.sym.ClassSymbol
53 import com.google.turbine.diag.SourceFile
54 import com.google.turbine.diag.TurbineLog
55 import com.google.turbine.model.TurbineFlag
56 import com.google.turbine.parse.Parser
57 import com.google.turbine.processing.ModelFactory
58 import com.google.turbine.processing.TurbineElements
59 import com.google.turbine.processing.TurbineTypes
60 import com.google.turbine.tree.Tree.CompUnit
61 import com.google.turbine.tree.Tree.Ident
62 import com.google.turbine.type.AnnoInfo
63 import java.io.File
64 import java.util.Optional
65 import javax.lang.model.SourceVersion
66 import javax.lang.model.element.TypeElement
67
68 /**
69 * This initializer acts as an adapter between codebase and the output from Turbine parser.
70 *
71 * This is used for populating all the classes,packages and other items from the data present in the
72 * parsed Tree
73 */
74 internal class TurbineCodebaseInitialiser(
75 codebaseFactory: DefaultCodebaseFactory,
76 private val classpath: List<File>,
77 override val allowReadingComments: Boolean,
78 ) : DefaultCodebaseAssembler(), TurbineGlobalContext {
79
80 override val codebase = codebaseFactory(this)
81
82 /** The output from Turbine Binder */
83 private lateinit var bindingResult: BindingResult
84
85 /**
86 * Map between ClassSymbols and TurbineClass for classes present on the source path or the class
87 * path
88 */
89 private lateinit var envClassMap: CompoundEnv<ClassSymbol, TypeBoundClass>
90
91 private lateinit var index: TopLevelIndex
92
93 /** Caches [TurbineSourceFile] instances. */
94 override lateinit var sourceFileCache: TurbineSourceFileCache
95
96 /** Factory for creating [AnnotationItem]s from [AnnoInfo]s. */
97 override lateinit var annotationFactory: TurbineAnnotationFactory
98
99 /** Global [TurbineTypeItemFactory] from which all other instances are created. */
100 private lateinit var globalTypeItemFactory: TurbineTypeItemFactory
101
102 /** Creates [Item] instances for [codebase]. */
103 override val itemFactory =
104 DefaultItemFactory(
105 codebase = codebase,
106 // Turbine can only process java files.
107 defaultItemLanguage = ItemLanguage.JAVA,
108 // Source files need to track which parts belong to which API surface variants, so they
109 // need to create an ApiVariantSelectors instance that can be used to track that.
110 defaultVariantSelectorsFactory = ApiVariantSelectors.MUTABLE_FACTORY,
111 )
112
113 /**
114 * Data Type: TurbineElements (An implementation of javax.lang.model.util.Elements)
115 *
116 * Usage: Enables lookup of TypeElement objects by name.
117 */
118 private lateinit var turbineElements: TurbineElements
119
120 /**
121 * Populates [codebase] from the [sourceSet].
122 *
123 * Then creates the packages, classes and their members, as well as sets up various class
124 * hierarchies using the binder's output
125 */
126 fun initialize(
127 sourceSet: SourceSet,
128 apiPackages: PackageFilter?,
129 ) {
130 // Get the units from the source files provided on the command line.
131 val commandLineSources = sourceSet.sources
132 val sourceFiles = getSourceFiles(commandLineSources.asSequence())
133 val units = sourceFiles.map { Parser.parse(it) }
134
135 // Get the sequence of all files that can be found on the source path which are not
136 // explicitly listed on the command line.
137 val scannedFiles = scanSourcePath(sourceSet.sourcePath, commandLineSources.toSet())
138 val sourcePathFiles = getSourceFiles(scannedFiles)
139
140 // Get the set of qualified class names provided on the command line. If a `.java` file
141 // contains multiple java classes then it just used the main class name.
142 val commandLineClasses = units.mapNotNull { unit -> unit.mainClassQualifiedName }.toSet()
143
144 // Get the units for the extra source files found on the source path.
145 val extraUnits =
146 sourcePathFiles
147 .map { Parser.parse(it) }
148 // Ignore any files that contain duplicates of a class that was specified on the
149 // command line. This is needed when merging annotations from other java files as
150 // there may be duplicate definitions of the class on the source path.
151 .filter { unit -> unit.mainClassQualifiedName !in commandLineClasses }
152
153 // Combine all the units together.
154 val allUnits = ImmutableList.builder<CompUnit>().addAll(units).addAll(extraUnits).build()
155
156 // Bind the units
157 try {
158 val procInfo =
159 ProcessorInfo.create(
160 ImmutableList.of(),
161 null,
162 ImmutableMap.of(),
163 SourceVersion.latest()
164 )
165
166 // Any non-fatal error (like unresolved symbols) will be captured in this log and will
167 // be ignored.
168 val log = TurbineLog()
169
170 bindingResult =
171 Binder.bind(
172 log,
173 allUnits,
174 ClassPathBinder.bindClasspath(classpath.map { it.toPath() }),
175 procInfo,
176 ClassPathBinder.bindClasspath(listOf()),
177 Optional.empty()
178 )!!
179 index = bindingResult.tli()
180 } catch (e: Throwable) {
181 throw e
182 }
183 // Get the SourceTypeBoundClass for all units that have been bound together.
184 val allSourceClassMap = bindingResult.units()
185
186 // Maps class symbols to their source-based definitions
187 val sourceEnv = SimpleEnv(allSourceClassMap)
188
189 // Maps class symbols to their classpath-based definitions
190 val classPathEnv = bindingResult.classPathEnv()
191
192 // Provides a unified view of both source and classpath classes. Although, the `sourceEnv`
193 // is appended to the `CompoundEnv` that contains the `classPathEnv`, it is actually
194 // queried first. So, this will search for a class on the source path first and then on the
195 // class path.
196 envClassMap = CompoundEnv.of<ClassSymbol, TypeBoundClass>(classPathEnv).append(sourceEnv)
197
198 // used to create language model elements for code analysis
199 val factory = ModelFactory(envClassMap, ClassLoader.getSystemClassLoader(), index)
200 // provides type-related operations within the Turbine compiler context
201 val turbineTypes = TurbineTypes(factory)
202 // provides access to code elements (packages, types, members) for analysis.
203 turbineElements = TurbineElements(factory, turbineTypes)
204
205 // Create a cache from SourceFile to the TurbineSourceFile wrapper. The latter needs the
206 // CompUnit associated with the SourceFile so pass in all the CompUnits so it can find it.
207 sourceFileCache = TurbineSourceFileCache(codebase, allUnits)
208
209 // Create a factory for creating annotations from AnnoInfo.
210 annotationFactory = TurbineAnnotationFactory(codebase, sourceFileCache)
211
212 // Create the global TurbineTypeItemFactory.
213 globalTypeItemFactory =
214 TurbineTypeItemFactory(this, annotationFactory, TypeParameterScope.empty)
215
216 // Find the package-info.java units.
217 val packageInfoUnits = allUnits.filter { it.isPackageInfo() }
218
219 // Split the map from ClassSymbol to SourceTypeBoundClass into separate package-info and
220 // normal classes.
221 val (packageInfoClasses, allSourceClasses) =
222 separatePackageInfoClassesFromRealClasses(allSourceClassMap)
223
224 val packageInfoList =
225 combinePackageInfoClassesAndUnits(packageInfoClasses, packageInfoUnits)
226
227 // Scan the files looking for package.html and overview.html files and combine that with
228 // information from package-info.java units to create a comprehensive set of package
229 // documentation just in case they are needed during package creation.
230 val packageDocs =
231 gatherPackageJavadoc(
232 codebase.reporter,
233 sourceSet,
234 packageNameFilter = { true },
235 packageInfoList
236 ) { (unit, packageName, sourceTypeBoundClass) ->
237 val source = unit.source().source()
238 val file = File(unit.source().path())
239 val fileLocation = FileLocation.forFile(file)
240 val comment = getHeaderComments(source).toItemDocumentationFactory()
241
242 val annotations =
243 annotationFactory.createAnnotations(sourceTypeBoundClass.annotations())
244
245 val modifiers = createImmutableModifiers(VisibilityLevel.PUBLIC, annotations)
246 MutablePackageDoc(packageName, fileLocation, modifiers, comment)
247 }
248
249 // Get the map from ClassSymbol to SourceTypeBoundClass for only those classes provided on
250 // the command line as only those classes can contribute directly to the API.
251 val commandLineSourceClasses =
252 topLevelAccessibleCommandLineClasses(allSourceClasses, commandLineSources)
253
254 createAllPackages(packageDocs)
255 createAllCommandLineClasses(commandLineSourceClasses, apiPackages)
256 }
257
258 /**
259 * Compute the set of accessible, top level classes that were specified on the command line.
260 *
261 * @param allSourceClasses all the [SourceTypeBoundClass]s found during binding, includes those
262 * from the source path as well as those whose containing file was provided on the command
263 * line.
264 * @param commandLineSources the list of source [File]s provided on the command line.
265 */
266 private fun topLevelAccessibleCommandLineClasses(
267 allSourceClasses: Map<ClassSymbol, SourceTypeBoundClass>,
268 commandLineSources: List<File>
269 ): Map<ClassSymbol, SourceTypeBoundClass> {
270 // The set of paths supplied on the command line.
271 val commandLinePaths = commandLineSources.map { it.path }.toSet()
272
273 // Get the map from ClassSymbol to SourceTypeBoundClass for only the accessible, top level
274 // classes provided on the command line as only those classes (and their nested classes) can
275 // contribute directly to the API.
276 return allSourceClasses.filter { (_, sourceTypeBoundClass) ->
277 // Ignore nested classes, they will be created as part of the construction of their
278 // containing class.
279 if (sourceTypeBoundClass.owner() != null) return@filter false
280
281 // Ignore inaccessible classes.
282 if (!sourceTypeBoundClass.isAccessible) return@filter false
283
284 // Ignore classes whose paths were not specified on the command line.
285 val path = sourceTypeBoundClass.source().path()
286 path in commandLinePaths
287 }
288 }
289
290 /**
291 * Get the qualified class name of the main class in a unit.
292 *
293 * If a `.java` file contains multiple java classes then the main class is the first one which
294 * is assumed to be the public class.
295 */
296 private val CompUnit.mainClassQualifiedName: String?
297 get() {
298 val pkgName = getPackageName(this)
299 return decls().firstOrNull()?.let { decl -> "$pkgName.${decl.name()}" }
300 }
301
302 private fun scanSourcePath(sourcePath: List<File>, existingSources: Set<File>): Sequence<File> {
303 val visited = mutableSetOf<String>()
304 return sourcePath
305 .asSequence()
306 .flatMap { sourceRoot ->
307 sourceRoot
308 .walkTopDown()
309 // The following prevents repeatedly re-entering the same directory if there is
310 // a cycle in the files, e.g. a symlink from a subdirectory back up to an
311 // ancestor directory.
312 .onEnter { dir ->
313 // Use the canonical path as each file in a cycle can be represented by an
314 // infinite number of paths and using them would make the visited check
315 // useless.
316 val canonical = dir.canonicalPath
317 return@onEnter if (canonical in visited) false
318 else {
319 visited += canonical
320 true
321 }
322 }
323 }
324 .filter { it !in existingSources }
325 }
326
327 /**
328 * Find the TypeBoundClass for the `ClassSymbol` in the source path and if it could not find it
329 * then look in the class path. It is guaranteed to be found in one of those places as otherwise
330 * there would be no `ClassSymbol`.
331 */
332 override fun typeBoundClassForSymbol(classSymbol: ClassSymbol) = envClassMap.get(classSymbol)!!
333
334 /**
335 * Separate `package-info.java` synthetic classes from real classes.
336 *
337 * Turbine treats a `package-info.java` file as if it created a class called `package-info`.
338 * This method separates the [sourceClassMap] into two, one for the synthetic `package-info`
339 * classes and one for real classes.
340 *
341 * @param sourceClassMap the map from [ClassSymbol] to [SourceTypeBoundClass] for all classes,
342 * real or synthetic.
343 */
344 private fun separatePackageInfoClassesFromRealClasses(
345 sourceClassMap: Map<ClassSymbol, SourceTypeBoundClass>,
346 ): Pair<Map<ClassSymbol, SourceTypeBoundClass>, Map<ClassSymbol, SourceTypeBoundClass>> {
347 val packageInfoClasses = mutableMapOf<ClassSymbol, SourceTypeBoundClass>()
348 val sourceClasses = mutableMapOf<ClassSymbol, SourceTypeBoundClass>()
349 for ((symbol, typeBoundClass) in sourceClassMap) {
350 if (symbol.simpleName() == "package-info") {
351 packageInfoClasses[symbol] = typeBoundClass
352 } else {
353 sourceClasses[symbol] = typeBoundClass
354 }
355 }
356 return Pair(packageInfoClasses, sourceClasses)
357 }
358
359 /**
360 * Encapsulates information needed to create a [DefaultPackageItem] in [gatherPackageJavadoc].
361 */
362 data class PackageInfoClass(
363 val unit: CompUnit,
364 val packageName: String,
365 val sourceTypeBoundClass: SourceTypeBoundClass,
366 )
367
368 /** Combine `package-info.java` synthetic classes and units */
369 private fun combinePackageInfoClassesAndUnits(
370 sourceClassMap: Map<ClassSymbol, SourceTypeBoundClass>,
371 packageInfoUnits: List<CompUnit>
372 ): List<PackageInfoClass> {
373 // Create a mapping between the package name and the unit.
374 val packageInfoMap = packageInfoUnits.associateBy { getPackageName(it) }
375
376 return sourceClassMap.entries.map { (symbol, typeBoundClass) ->
377 val packageName = symbol.packageName().replace('/', '.')
378 PackageInfoClass(
379 unit = packageInfoMap[packageName]!!,
380 packageName = packageName,
381 sourceTypeBoundClass = typeBoundClass,
382 )
383 }
384 }
385
386 /** Check if this is for a `package-info.java` file or not. */
387 private fun CompUnit.isPackageInfo() =
388 source().path().let { it == JAVA_PACKAGE_INFO || it.endsWith("/" + JAVA_PACKAGE_INFO) }
389
390 private fun createAllPackages(packageDocs: PackageDocs) {
391 // Create packages for all the documentation packages and make sure there is a root package.
392 codebase.packageTracker.createInitialPackages(packageDocs)
393 }
394
395 private fun createAllCommandLineClasses(
396 sourceClassMap: Map<ClassSymbol, SourceTypeBoundClass>,
397 apiPackages: PackageFilter?,
398 ) {
399 // Iterate over all the classes in the sources.
400 for ((classSymbol, sourceBoundClass) in sourceClassMap) {
401 // If a package filter is supplied then ignore any classes that do not match it.
402 if (apiPackages != null) {
403 val packageName = classSymbol.dotSeparatedPackageName
404 if (!apiPackages.matches(packageName)) continue
405 }
406
407 val classItem =
408 createTopLevelClassAndContents(
409 classSymbol = classSymbol,
410 typeBoundClass = sourceBoundClass,
411 origin = ClassOrigin.COMMAND_LINE,
412 )
413 codebase.addTopLevelClassFromSource(classItem)
414 }
415 }
416
417 val ClassSymbol.isTopClass
418 get() = !binaryName().contains('$')
419
420 /**
421 * Create top level classes, their nested classes and all the other members.
422 *
423 * All the classes are registered by name and so can be found by
424 * [createClassFromUnderlyingModel].
425 */
426 private fun createTopLevelClassAndContents(
427 classSymbol: ClassSymbol,
428 typeBoundClass: TypeBoundClass,
429 origin: ClassOrigin,
430 ): ClassItem {
431 if (!classSymbol.isTopClass) error("$classSymbol is not a top level class")
432 val classBuilder =
433 TurbineClassBuilder(
434 globalContext = this,
435 classSymbol = classSymbol,
436 typeBoundClass = typeBoundClass,
437 )
438 return classBuilder.createClass(
439 containingClassItem = null,
440 enclosingClassTypeItemFactory = globalTypeItemFactory,
441 origin = origin,
442 )
443 }
444
445 /** Tries to create a class from a Turbine class with [qualifiedName]. */
446 override fun createClassFromUnderlyingModel(qualifiedName: String): ClassItem? {
447 // This will get the symbol for the top class even if the class name is for a nested
448 // class.
449 val topClassSym = getClassSymbol(qualifiedName)
450
451 // Create the top level class, if needed, along with any nested classes and register
452 // them all by name.
453 topClassSym?.let {
454 // It is possible that the top level class has already been created but just did not
455 // contain the requested nested class so check to make sure it exists before
456 // creating it.
457 val topClassName = topClassSym.qualifiedName
458 codebase.findClass(topClassName)
459 ?: let {
460 // Get the origin of the class.
461 val typeBoundClass = typeBoundClassForSymbol(topClassSym)
462 val origin =
463 when (typeBoundClass) {
464 is SourceTypeBoundClass -> ClassOrigin.SOURCE_PATH
465 else -> ClassOrigin.CLASS_PATH
466 }
467
468 // Create and register the top level class and its nested classes.
469 createTopLevelClassAndContents(
470 classSymbol = topClassSym,
471 typeBoundClass = typeBoundClass,
472 origin = origin,
473 )
474
475 // Now try and find the actual class that was requested by name. If it exists it
476 // should have been created in the previous call.
477 return codebase.findClass(qualifiedName)
478 }
479 }
480
481 // Could not be found.
482 return null
483 }
484
485 override fun createFieldResolver(
486 classSymbol: ClassSymbol,
487 sourceTypeBoundClass: SourceTypeBoundClass
488 ) =
489 TurbineFieldResolver(
490 classSymbol,
491 classSymbol,
492 sourceTypeBoundClass.memberImports(),
493 sourceTypeBoundClass.scope(),
494 envClassMap,
495 )
496
497 /**
498 * Get the ClassSymbol corresponding to a qualified name. Since the Turbine's lookup method
499 * returns only top-level classes, this method will return the ClassSymbol of outermost class
500 * for nested classes.
501 */
502 private fun getClassSymbol(name: String): ClassSymbol? {
503 val result = index.scope().lookup(createLookupKey(name))
504 return result?.let { it.sym() as ClassSymbol }
505 }
506
507 /** Creates a LookupKey from a given name */
508 private fun createLookupKey(name: String): LookupKey {
509 val idents = name.split(".").mapIndexed { idx, it -> Ident(idx, it) }
510 return LookupKey(ImmutableList.copyOf(idents))
511 }
512
513 internal fun getTypeElement(name: String): TypeElement? = turbineElements.getTypeElement(name)
514 }
515
516 /** Create a [SourceFile] for every `.java` file in [sources]. */
getSourceFilesnull517 private fun getSourceFiles(sources: Sequence<File>): List<SourceFile> {
518 return sources
519 .filter { it.isFile && it.extension == "java" } // Ensure only Java files are included
520 .map { SourceFile(it.path, it.readText()) }
521 .toList()
522 }
523
524 private const val ACC_PUBLIC_OR_PROTECTED = TurbineFlag.ACC_PUBLIC or TurbineFlag.ACC_PROTECTED
525
526 /** Check whether the [TypeBoundClass] is accessible. */
527 private val TypeBoundClass.isAccessible: Boolean
528 get() {
529 val flags = access()
530 return flags and ACC_PUBLIC_OR_PROTECTED != 0
531 }
532