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