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