• 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.psi
18 
19 import com.android.SdkConstants
20 import com.android.tools.lint.UastEnvironment
21 import com.android.tools.lint.computeMetadata
22 import com.android.tools.lint.detector.api.Project
23 import com.android.tools.metalava.model.ClassResolver
24 import com.android.tools.metalava.model.Codebase
25 import com.android.tools.metalava.model.PackageFilter
26 import com.android.tools.metalava.model.source.DEFAULT_JAVA_LANGUAGE_LEVEL
27 import com.android.tools.metalava.model.source.SourceParser
28 import com.android.tools.metalava.model.source.SourceSet
29 import com.intellij.pom.java.LanguageLevel
30 import java.io.File
31 import org.jetbrains.kotlin.config.ApiVersion
32 import org.jetbrains.kotlin.config.JVMConfigurationKeys
33 import org.jetbrains.kotlin.config.LanguageVersion
34 import org.jetbrains.kotlin.config.LanguageVersionSettings
35 import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl
36 
37 internal val defaultJavaLanguageLevel = LanguageLevel.parse(DEFAULT_JAVA_LANGUAGE_LEVEL)!!
38 
39 internal val defaultKotlinLanguageLevel = LanguageVersionSettingsImpl.DEFAULT
40 
41 fun kotlinLanguageVersionSettings(value: String?): LanguageVersionSettings {
42     val languageLevel =
43         LanguageVersion.fromVersionString(value)
44             ?: throw IllegalStateException(
45                 "$value is not a valid or supported Kotlin language level"
46             )
47     val apiVersion = ApiVersion.createByLanguageVersion(languageLevel)
48     return LanguageVersionSettingsImpl(languageLevel, apiVersion)
49 }
50 
51 /**
52  * Parses a set of sources into a [PsiBasedCodebase].
53  *
54  * The codebases will use a project environment initialized according to the properties passed to
55  * the constructor and the paths passed to [parseSources].
56  */
57 internal class PsiSourceParser(
58     private val psiEnvironmentManager: PsiEnvironmentManager,
59     private val codebaseConfig: Codebase.Config,
60     private val javaLanguageLevel: LanguageLevel,
61     private val kotlinLanguageLevel: LanguageVersionSettings,
62     private val useK2Uast: Boolean,
63     private val allowReadingComments: Boolean,
64     private val jdkHome: File?,
65 ) : SourceParser {
66 
67     private val reporter = codebaseConfig.reporter
68 
getClassResolvernull69     override fun getClassResolver(classPath: List<File>): ClassResolver {
70         val uastEnvironment = loadUastFromJars(classPath)
71         return PsiBasedClassResolver(
72             uastEnvironment,
73             codebaseConfig,
74             allowReadingComments,
75         )
76     }
77 
78     /**
79      * Returns a codebase initialized from the given Java or Kotlin source files, with the given
80      * description.
81      *
82      * All supplied [File] objects will be mapped to [File.getAbsoluteFile].
83      */
parseSourcesnull84     override fun parseSources(
85         sourceSet: SourceSet,
86         description: String,
87         classPath: List<File>,
88         apiPackages: PackageFilter?,
89         projectDescription: File?,
90     ): Codebase {
91         return parseAbsoluteSources(
92             sourceSet.absoluteCopy().extractRoots(reporter),
93             description,
94             classPath.map { it.absoluteFile },
95             apiPackages,
96             projectDescription,
97         )
98     }
99 
100     /** Returns a codebase initialized from the given set of absolute files. */
parseAbsoluteSourcesnull101     private fun parseAbsoluteSources(
102         sourceSet: SourceSet,
103         description: String,
104         classpath: List<File>,
105         apiPackages: PackageFilter?,
106         projectDescription: File?,
107     ): PsiBasedCodebase {
108         val config = UastEnvironment.Configuration.create(useFirUast = useK2Uast)
109         config.javaLanguageLevel = javaLanguageLevel
110 
111         val rootDir = sourceSet.sourcePath.firstOrNull() ?: File("").canonicalFile
112 
113         when {
114             projectDescription != null -> {
115                 configureUastEnvironmentFromProjectDescription(config, projectDescription)
116             }
117             else -> {
118                 configureUastEnvironment(config, sourceSet.sourcePath, classpath, rootDir)
119             }
120         }
121         // K1 UAST: loading of JDK (via compiler config, i.e., only for FE1.0), when using JDK9+
122         jdkHome?.let {
123             if (isJdkModular(it)) {
124                 config.kotlinCompilerConfig.put(JVMConfigurationKeys.JDK_HOME, it)
125                 config.kotlinCompilerConfig.put(JVMConfigurationKeys.NO_JDK, false)
126             }
127         }
128 
129         val environment = psiEnvironmentManager.createEnvironment(config)
130         val kotlinFiles = sourceSet.sources.filter { it.path.endsWith(SdkConstants.DOT_KT) }
131         environment.analyzeFiles(kotlinFiles)
132 
133         val assembler =
134             PsiCodebaseAssembler(environment) {
135                 PsiBasedCodebase(
136                     location = rootDir,
137                     description = description,
138                     config = codebaseConfig,
139                     allowReadingComments = allowReadingComments,
140                     assembler = it,
141                     isMultiplatform = environment.isKMP,
142                 )
143             }
144 
145         assembler.initializeFromSources(sourceSet, apiPackages)
146         return assembler.codebase
147     }
148 
isJdkModularnull149     private fun isJdkModular(homePath: File): Boolean {
150         return File(homePath, "jmods").isDirectory
151     }
152 
loadFromJarnull153     override fun loadFromJar(apiJar: File, classPath: List<File>): Codebase {
154         val jars = buildList {
155             add(apiJar)
156             addAll(classPath)
157         }
158         val environment = loadUastFromJars(jars)
159         val assembler =
160             PsiCodebaseAssembler(environment) { assembler ->
161                 PsiBasedCodebase(
162                     location = apiJar,
163                     description = "Codebase loaded from $apiJar",
164                     config = codebaseConfig,
165                     allowReadingComments = allowReadingComments,
166                     assembler = assembler,
167                     isMultiplatform = environment.isKMP,
168                 )
169             }
170         val codebase = assembler.codebase
171         assembler.initializeFromJar(apiJar)
172         return codebase
173     }
174 
175     /** Initializes a UAST environment using the [apiJars] as classpath roots. */
loadUastFromJarsnull176     private fun loadUastFromJars(apiJars: List<File>): UastEnvironment {
177         val config = UastEnvironment.Configuration.create(useFirUast = useK2Uast)
178         // Use the empty dir otherwise this will end up scanning the current working directory.
179         configureUastEnvironment(config, listOf(psiEnvironmentManager.emptyDir), apiJars)
180 
181         val environment = psiEnvironmentManager.createEnvironment(config)
182         environment.analyzeFiles(emptyList()) // Initializes PSI machinery.
183         return environment
184     }
185 
configureUastEnvironmentnull186     private fun configureUastEnvironment(
187         config: UastEnvironment.Configuration,
188         sourceRoots: List<File>,
189         classpath: List<File>,
190         rootDir: File = sourceRoots.firstOrNull() ?: File("").canonicalFile
191     ) {
192         val lintClient = MetalavaCliClient()
193         // From ...lint.detector.api.Project, `dir` is, e.g., /tmp/foo/dev/src/project1,
194         // and `referenceDir` is /tmp/foo/. However, in many use cases, they are just same.
195         // `referenceDir` is used to adjust `lib` dir accordingly if needed,
196         // but we set `classpath` anyway below.
197         val lintProject =
198             Project.create(lintClient, /* dir = */ rootDir, /* referenceDir = */ rootDir)
199         lintProject.kotlinLanguageLevel = kotlinLanguageLevel
200         lintProject.javaSourceFolders.addAll(sourceRoots)
201         lintProject.javaLibraries.addAll(classpath)
202         config.addModules(
203             listOf(
204                 UastEnvironment.Module(
205                     lintProject,
206                     // K2 UAST: building KtSdkModule for JDK
207                     jdkHome,
208                     includeTests = false,
209                     includeTestFixtureSources = false,
210                     isUnitTest = false
211                 )
212             ),
213         )
214     }
215 
216     /**
217      * Configures the environment based on an XML description of Lint's project model.
218      *
219      * Alas, no proper documentation is available. Please refer to examples at upstream Lint:
220      * 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
221      *
222      * An ideal project structure would look like:
223      * ```
224      * <project>
225      *     <root dir="frameworks/support/compose/ui/ui"/>
226      *     <module name="commonMain" android="false">
227      *         <src file="src/commonMain/.../file1.kt" /> <!-- and so on -->
228      *         <klib file="lib/if/any.klib" />
229      *         <classpath jar="/path/to/kotlin/coroutinesCore.jar" />
230      *         ...
231      *     </module>
232      *     <module name="jvmMain" android="false">
233      *         <dep module="commonMain" kind="dependsOn" />
234      *         <src file="src/jvmMain/.../file1.kt" /> <!-- and so on -->
235      *         ...
236      *     </module>
237      *     <module name="androidMain" android="true">
238      *         <dep module="jvmMain" kind="dependsOn" />
239      *         <src file="src/androidMain/.../file1.kt" /> <!-- and so on -->
240      *         ...
241      *     </module>
242      *     ...
243      * </project>
244      * ```
245      *
246      * That is, there are common modules where `expect` declarations and common business logic
247      * reside, along with binary dependencies of several formats, including klib and jar.
248      *
249      * Then, platform-specific modules "depend" on common modules, and have their own source set and
250      * binary dependencies.
251      */
configureUastEnvironmentFromProjectDescriptionnull252     private fun configureUastEnvironmentFromProjectDescription(
253         config: UastEnvironment.Configuration,
254         projectDescription: File,
255     ) {
256         val lintClient = MetalavaCliClient()
257         // This will parse the description of Lint's project model and populate the module structure
258         // inside the given Lint client. We will use it to set up the project structure that
259         // [UastEnvironment] requires, which in turn uses that to set up Kotlin compiler frontend.
260         // The overall flow looks like:
261         //   project.xml -> Lint Project model -> UastEnvironment Module -> Kotlin compiler FE / AA
262         // There are a couple of limitations that force use fall into this long steps:
263         //  * Lint Project creation is not exposed at all. Only project.xml parsing is available.
264         //  * UastEnvironment Module simply reuses existing Lint Project model.
265         computeMetadata(lintClient, projectDescription)
266         config.addModules(
267             lintClient.knownProjects.mapNotNull { lintProject ->
268                 // TODO(b/383457595): For the given root dir,
269                 //   Lint creates a bogus, uninitialized [Project]
270                 if (
271                     // The default project name, if not given, is directory name
272                     // not something we provided, like `androidMain`.
273                     lintProject.name == lintProject.dir.name &&
274                         // source folder might be still the root dir
275                         // but libraries would be empty / not computed.
276                         (lintProject.javaSourceFolders.isEmpty() ||
277                             lintProject.javaLibraries.isEmpty())
278                 ) {
279                     return@mapNotNull null
280                 }
281                 lintProject.kotlinLanguageLevel = kotlinLanguageLevel
282                 UastEnvironment.Module(
283                     lintProject,
284                     // K2 UAST: building KtSdkModule for JDK
285                     jdkHome,
286                     includeTests = false,
287                     includeTestFixtureSources = false,
288                     isUnitTest = false
289                 )
290             }
291         )
292     }
293 
294     companion object {
295         private const val AAR = "aar"
296         private const val JAR = "jar"
297         private const val KLIB = "klib"
298         private val SUPPORTED_CLASSPATH_EXT = listOf(AAR, JAR, KLIB)
299     }
300 }
301