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.psi
18 
19 import com.android.SdkConstants
20 import com.android.tools.lint.UastEnvironment
21 import com.android.tools.lint.annotations.Extractor
22 import com.android.tools.lint.computeMetadata
23 import com.android.tools.lint.detector.api.Project
24 import com.android.tools.metalava.model.AnnotationManager
25 import com.android.tools.metalava.model.ClassResolver
26 import com.android.tools.metalava.model.noOpAnnotationManager
27 import com.android.tools.metalava.model.source.DEFAULT_JAVA_LANGUAGE_LEVEL
28 import com.android.tools.metalava.model.source.SourceCodebase
29 import com.android.tools.metalava.model.source.SourceParser
30 import com.android.tools.metalava.model.source.SourceSet
31 import com.android.tools.metalava.model.source.utils.OVERVIEW_HTML
32 import com.android.tools.metalava.model.source.utils.PACKAGE_HTML
33 import com.android.tools.metalava.model.source.utils.findPackage
34 import com.android.tools.metalava.reporter.Reporter
35 import com.intellij.pom.java.LanguageLevel
36 import java.io.File
37 import org.jetbrains.kotlin.config.ApiVersion
38 import org.jetbrains.kotlin.config.JVMConfigurationKeys
39 import org.jetbrains.kotlin.config.LanguageVersion
40 import org.jetbrains.kotlin.config.LanguageVersionSettings
41 import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl
42 
43 internal val defaultJavaLanguageLevel = LanguageLevel.parse(DEFAULT_JAVA_LANGUAGE_LEVEL)!!
44 
45 internal val defaultKotlinLanguageLevel = LanguageVersionSettingsImpl.DEFAULT
46 
47 fun kotlinLanguageVersionSettings(value: String?): LanguageVersionSettings {
48     val languageLevel =
49         LanguageVersion.fromVersionString(value)
50             ?: throw IllegalStateException(
51                 "$value is not a valid or supported Kotlin language level"
52             )
53     val apiVersion = ApiVersion.createByLanguageVersion(languageLevel)
54     return LanguageVersionSettingsImpl(languageLevel, apiVersion)
55 }
56 
57 /**
58  * Parses a set of sources into a [PsiBasedCodebase].
59  *
60  * The codebases will use a project environment initialized according to the properties passed to
61  * the constructor and the paths passed to [parseSources].
62  */
63 internal class PsiSourceParser(
64     private val psiEnvironmentManager: PsiEnvironmentManager,
65     private val reporter: Reporter,
66     private val annotationManager: AnnotationManager = noOpAnnotationManager,
67     private val javaLanguageLevel: LanguageLevel = defaultJavaLanguageLevel,
68     private val kotlinLanguageLevel: LanguageVersionSettings = defaultKotlinLanguageLevel,
69     private val useK2Uast: Boolean = false,
70     private val allowReadingComments: Boolean,
71     private val jdkHome: File? = null,
72 ) : SourceParser {
73 
getClassResolvernull74     override fun getClassResolver(classPath: List<File>): ClassResolver {
75         val uastEnvironment = loadUastFromJars(classPath)
76         return PsiBasedClassResolver(
77             uastEnvironment,
78             annotationManager,
79             reporter,
80             allowReadingComments
81         )
82     }
83 
84     /**
85      * Returns a codebase initialized from the given Java or Kotlin source files, with the given
86      * description.
87      *
88      * All supplied [File] objects will be mapped to [File.getAbsoluteFile].
89      */
parseSourcesnull90     override fun parseSources(
91         sourceSet: SourceSet,
92         commonSourceSet: SourceSet,
93         description: String,
94         classPath: List<File>,
95     ): PsiBasedCodebase {
96         return parseAbsoluteSources(
97             sourceSet.absoluteCopy().extractRoots(reporter),
98             commonSourceSet.absoluteCopy().extractRoots(reporter),
99             description,
100             classPath.map { it.absoluteFile }
101         )
102     }
103 
104     /** Returns a codebase initialized from the given set of absolute files. */
parseAbsoluteSourcesnull105     private fun parseAbsoluteSources(
106         sourceSet: SourceSet,
107         commonSourceSet: SourceSet,
108         description: String,
109         classpath: List<File>,
110     ): PsiBasedCodebase {
111         val config = UastEnvironment.Configuration.create(useFirUast = useK2Uast)
112         config.javaLanguageLevel = javaLanguageLevel
113 
114         val rootDir = sourceSet.sourcePath.firstOrNull() ?: File("").canonicalFile
115 
116         if (commonSourceSet.sources.isNotEmpty()) {
117             configureUastEnvironmentForKMP(
118                 config,
119                 sourceSet.sources,
120                 commonSourceSet.sources,
121                 classpath,
122                 rootDir
123             )
124         } else {
125             configureUastEnvironment(config, sourceSet.sourcePath, classpath, rootDir)
126         }
127         // K1 UAST: loading of JDK (via compiler config, i.e., only for FE1.0), when using JDK9+
128         jdkHome?.let {
129             if (isJdkModular(it)) {
130                 config.kotlinCompilerConfig.put(JVMConfigurationKeys.JDK_HOME, it)
131                 config.kotlinCompilerConfig.put(JVMConfigurationKeys.NO_JDK, false)
132             }
133         }
134 
135         val environment = psiEnvironmentManager.createEnvironment(config)
136 
137         val kotlinFiles = sourceSet.sources.filter { it.path.endsWith(SdkConstants.DOT_KT) }
138         environment.analyzeFiles(kotlinFiles)
139 
140         val units = Extractor.createUnitsForFiles(environment.ideaProject, sourceSet.sources)
141         val packageDocs = gatherPackageJavadoc(sourceSet)
142 
143         val codebase =
144             PsiBasedCodebase(
145                 location = rootDir,
146                 description = description,
147                 annotationManager = annotationManager,
148                 reporter = reporter,
149                 allowReadingComments = allowReadingComments,
150             )
151         codebase.initializeFromSources(environment, units, packageDocs)
152         return codebase
153     }
154 
isJdkModularnull155     private fun isJdkModular(homePath: File): Boolean {
156         return File(homePath, "jmods").isDirectory
157     }
158 
loadFromJarnull159     override fun loadFromJar(apiJar: File): SourceCodebase {
160         val environment = loadUastFromJars(listOf(apiJar))
161         val codebase =
162             PsiBasedCodebase(
163                 location = apiJar,
164                 description = "Codebase loaded from $apiJar",
165                 annotationManager = annotationManager,
166                 reporter = reporter,
167                 allowReadingComments = allowReadingComments
168             )
169         codebase.initializeFromJar(environment, apiJar)
170         return codebase
171     }
172 
173     /** Initializes a UAST environment using the [apiJars] as classpath roots. */
loadUastFromJarsnull174     private fun loadUastFromJars(apiJars: List<File>): UastEnvironment {
175         val config = UastEnvironment.Configuration.create(useFirUast = useK2Uast)
176         // Use the empty dir otherwise this will end up scanning the current working directory.
177         configureUastEnvironment(config, listOf(psiEnvironmentManager.emptyDir), apiJars)
178 
179         val environment = psiEnvironmentManager.createEnvironment(config)
180         environment.analyzeFiles(emptyList()) // Initializes PSI machinery.
181         return environment
182     }
183 
configureUastEnvironmentnull184     private fun configureUastEnvironment(
185         config: UastEnvironment.Configuration,
186         sourceRoots: List<File>,
187         classpath: List<File>,
188         rootDir: File = sourceRoots.firstOrNull() ?: File("").canonicalFile
189     ) {
190         val lintClient = MetalavaCliClient()
191         // From ...lint.detector.api.Project, `dir` is, e.g., /tmp/foo/dev/src/project1,
192         // and `referenceDir` is /tmp/foo/. However, in many use cases, they are just same.
193         // `referenceDir` is used to adjust `lib` dir accordingly if needed,
194         // but we set `classpath` anyway below.
195         val lintProject =
196             Project.create(lintClient, /* dir = */ rootDir, /* referenceDir = */ rootDir)
197         lintProject.kotlinLanguageLevel = kotlinLanguageLevel
198         lintProject.javaSourceFolders.addAll(sourceRoots)
199         lintProject.javaLibraries.addAll(classpath)
200         config.addModules(
201             listOf(
202                 UastEnvironment.Module(
203                     lintProject,
204                     // K2 UAST: building KtSdkModule for JDK
205                     jdkHome,
206                     includeTests = false,
207                     includeTestFixtureSources = false,
208                     isUnitTest = false
209                 )
210             ),
211         )
212     }
213 
configureUastEnvironmentForKMPnull214     private fun configureUastEnvironmentForKMP(
215         config: UastEnvironment.Configuration,
216         sourceFiles: List<File>,
217         commonSourceFiles: List<File>,
218         classpath: List<File>,
219         rootDir: File,
220     ) {
221         // TODO(b/322111050): consider providing a nice DSL at Lint level
222         val projectXml = File.createTempFile("project", ".xml")
223         projectXml.deleteOnExit()
224 
225         fun describeSources(sources: List<File>) = buildString {
226             for (source in sources) {
227                 if (!source.isFile) continue
228                 appendLine("    <src file=\"${source.absolutePath}\" />")
229             }
230         }
231 
232         fun describeClasspath() = buildString {
233             for (dep in classpath) {
234                 // TODO: what other kinds of dependencies?
235                 if (dep.extension !in SUPPORTED_CLASSPATH_EXT) continue
236                 appendLine("    <classpath ${dep.extension}=\"${dep.absolutePath}\" />")
237             }
238         }
239 
240         // We're about to build the description of Lint's project model.
241         // Alas, no proper documentation is available. Please refer to examples at upstream Lint:
242         // https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:lint/libs/lint-tests/src/test/java/com/android/tools/lint/ProjectInitializerTest.kt
243         //
244         // An ideal project structure would look like:
245         //
246         // <project>
247         //   <root dir="frameworks/support/compose/ui/ui"/>
248         //   <module name="commonMain" android="false">
249         //     <src file="src/commonMain/.../file1.kt" /> <!-- and so on -->
250         //     <klib file="lib/if/any.klib" />
251         //     <classpath jar="/path/to/kotlin/coroutinesCore.jar" />
252         //     ...
253         //   </module>
254         //   <module name="jvmMain" android="false">
255         //     <dep module="commonMain" kind="dependsOn" />
256         //     <src file="src/jvmMain/.../file1.kt" /> <!-- and so on -->
257         //     ...
258         //   </module>
259         //   <module name="androidMain" android="true">
260         //     <dep module="jvmMain" kind="dependsOn" />
261         //     <src file="src/androidMain/.../file1.kt" /> <!-- and so on -->
262         //     ...
263         //   </module>
264         //   ...
265         // </project>
266         //
267         // That is, there are common modules where `expect` declarations and common business logic
268         // reside, along with binary dependencies of several formats, including klib and jar.
269         // Then, platform-specific modules "depend" on common modules, and have their own source set
270         // and binary dependencies.
271         //
272         // For now, with --common-source-path, common source files are isolated, but the project
273         // structure is not fully conveyed. Therefore, we will reuse the same binary dependencies
274         // for all modules (which only(?) cause performance degradation on binary resolution).
275         val description = buildString {
276             appendLine("""<?xml version="1.0" encoding="utf-8"?>""")
277             appendLine("<project>")
278             appendLine("  <root dir=\"${rootDir.absolutePath}\" />")
279             appendLine("  <module name=\"commonMain\" android=\"false\" >")
280             append(describeSources(commonSourceFiles))
281             append(describeClasspath())
282             appendLine("  </module>")
283             appendLine("  <module name=\"app\" >")
284             appendLine("    <dep module=\"commonMain\" kind=\"dependsOn\" />")
285             // NB: While K2 requires separate common / platform-specific source roots, K1 still
286             // needs to receive all source roots at once. Thus, existing usages (e.g., androidx)
287             // often pass all source files, according to compiler configuration.
288             // To make a correct module structure, we need to filter out common source files here.
289             // TODO: once fully switching to K2 and androidx usage is adjusted, we won't need this.
290             append(describeSources(sourceFiles - commonSourceFiles))
291             append(describeClasspath())
292             appendLine("  </module>")
293             appendLine("</project>")
294         }
295         projectXml.writeText(description)
296 
297         val lintClient = MetalavaCliClient()
298         // This will parse the description of Lint's project model and populate the module structure
299         // inside the given Lint client. We will use it to set up the project structure that
300         // [UastEnvironment] requires, which in turn uses that to set up Kotlin compiler frontend.
301         // The overall flow looks like:
302         //   project.xml -> Lint Project model -> UastEnvironment Module -> Kotlin compiler FE / AA
303         // There are a couple of limitations that force use fall into this long steps:
304         //  * Lint Project creation is not exposed at all. Only project.xml parsing is available.
305         //  * UastEnvironment Module simply reuses existing Lint Project model.
306         computeMetadata(lintClient, projectXml)
307         config.addModules(
308             lintClient.knownProjects.map { lintProject ->
309                 lintProject.kotlinLanguageLevel = kotlinLanguageLevel
310                 UastEnvironment.Module(
311                     lintProject,
312                     // K2 UAST: building KtSdkModule for JDK
313                     jdkHome,
314                     includeTests = false,
315                     includeTestFixtureSources = false,
316                     isUnitTest = false
317                 )
318             }
319         )
320     }
321 
322     companion object {
323         private const val AAR = "aar"
324         private const val JAR = "jar"
325         private const val KLIB = "klib"
326         private val SUPPORTED_CLASSPATH_EXT = listOf(AAR, JAR, KLIB)
327     }
328 }
329 
gatherPackageJavadocnull330 private fun gatherPackageJavadoc(sourceSet: SourceSet): PackageDocs {
331     val packageComments = HashMap<String, String>(100)
332     val overviewHtml = HashMap<String, String>(10)
333     val hiddenPackages = HashSet<String>(100)
334     val sortedSourceRoots = sourceSet.sourcePath.sortedBy { -it.name.length }
335     for (file in sourceSet.sources) {
336         var javadoc = false
337         val map =
338             when (file.name) {
339                 PACKAGE_HTML -> {
340                     javadoc = true
341                     packageComments
342                 }
343                 OVERVIEW_HTML -> {
344                     overviewHtml
345                 }
346                 else -> continue
347             }
348         var contents = file.readText(Charsets.UTF_8)
349         if (javadoc) {
350             contents = packageHtmlToJavadoc(contents)
351         }
352 
353         // Figure out the package: if there is a java file in the same directory, get the package
354         // name from the java file. Otherwise, guess from the directory path + source roots.
355         // NOTE: This causes metalava to read files other than the ones explicitly passed to it.
356         var pkg =
357             file.parentFile
358                 ?.listFiles()
359                 ?.filter { it.name.endsWith(SdkConstants.DOT_JAVA) }
360                 ?.asSequence()
361                 ?.mapNotNull { findPackage(it) }
362                 ?.firstOrNull()
363         if (pkg == null) {
364             // Strip the longest prefix source root.
365             val prefix = sortedSourceRoots.firstOrNull { file.startsWith(it) }?.path ?: ""
366             pkg = file.parentFile.path.substring(prefix.length).trim('/').replace("/", ".")
367         }
368         map[pkg] = contents
369         if (contents.contains("@hide")) {
370             hiddenPackages.add(pkg)
371         }
372     }
373 
374     return PackageDocs(packageComments, overviewHtml, hiddenPackages)
375 }
376