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