1 /* 2 * 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.tools.lint.UastEnvironment 20 import com.android.tools.metalava.model.Codebase 21 import com.android.tools.metalava.model.ModelOptions 22 import com.android.tools.metalava.model.source.EnvironmentManager 23 import com.android.tools.metalava.model.source.SourceParser 24 import com.intellij.core.CoreApplicationEnvironment 25 import com.intellij.openapi.diagnostic.DefaultLogger 26 import com.intellij.openapi.util.Disposer 27 import com.intellij.pom.java.LanguageLevel 28 import com.intellij.psi.javadoc.CustomJavadocTagProvider 29 import com.intellij.psi.javadoc.JavadocTagInfo 30 import java.io.File 31 import kotlin.io.path.createTempDirectory 32 import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment 33 import org.jetbrains.kotlin.config.CommonConfigurationKeys 34 35 /** Manages the [UastEnvironment] objects created when processing sources. */ 36 class PsiEnvironmentManager( 37 private val disableStderrDumping: Boolean = false, 38 private val forTesting: Boolean = false, 39 ) : EnvironmentManager { 40 41 /** 42 * True if this is responsible for creating and this owning the application environment. 43 * 44 * This is needed to allow a [PsiEnvironmentManager] to be created while using another 45 * [PsiEnvironmentManager], e.g. in tests that require two [Codebase]s. 46 */ 47 private val ownApplicationEnvironment: Boolean = 48 KotlinCoreEnvironment.applicationEnvironment == null 49 50 /** 51 * An empty directory, used when it is necessary to create an environment without any source. 52 * Simply providing an empty list of source roots will cause it to use the current working 53 * directory. 54 */ <lambda>null55 internal val emptyDir by lazy { 56 val path = createTempDirectory() 57 val file = path.toFile() 58 file.deleteOnExit() 59 file 60 } 61 62 /** 63 * Determines whether the manager has been closed. Used to prevent creating new environments 64 * after the manager has closed. 65 */ 66 private var closed = false 67 68 /** The list of available environments. */ 69 private val uastEnvironments = mutableListOf<UastEnvironment>() 70 71 init { 72 if (forTesting) { 73 System.setProperty("java.awt.headless", "true") 74 Disposer.setDebugMode(true) 75 } 76 } 77 78 /** 79 * Create a [UastEnvironment] with the supplied configuration. 80 * 81 * @throws IllegalStateException if this manager has been closed. 82 */ createEnvironmentnull83 internal fun createEnvironment(config: UastEnvironment.Configuration): UastEnvironment { 84 if (closed) { 85 throw IllegalStateException("PsiEnvironmentManager is closed") 86 } 87 ensurePsiFileCapacity() 88 89 // Note: the Kotlin module name affects the naming of certain synthetic methods. 90 config.kotlinCompilerConfig.put( 91 CommonConfigurationKeys.MODULE_NAME, 92 METALAVA_SYNTHETIC_SUFFIX 93 ) 94 95 val environment = UastEnvironment.create(config) 96 uastEnvironments.add(environment) 97 98 if (disableStderrDumping) { 99 DefaultLogger.disableStderrDumping(environment.ideaProject) 100 } 101 102 // Missing service needed in metalava but not in lint: javadoc handling 103 environment.ideaProject.registerService( 104 com.intellij.psi.javadoc.JavadocManager::class.java, 105 com.intellij.psi.impl.source.javadoc.JavadocManagerImpl::class.java 106 ) 107 CoreApplicationEnvironment.registerExtensionPoint( 108 environment.ideaProject.extensionArea, 109 JavadocTagInfo.EP_NAME, 110 JavadocTagInfo::class.java 111 ) 112 CoreApplicationEnvironment.registerApplicationExtensionPoint( 113 CustomJavadocTagProvider.EP_NAME, 114 CustomJavadocTagProvider::class.java 115 ) 116 117 return environment 118 } 119 ensurePsiFileCapacitynull120 private fun ensurePsiFileCapacity() { 121 val fileSize = System.getProperty("idea.max.intellisense.filesize") 122 if (fileSize == null) { 123 // Ensure we can handle large compilation units like android.R 124 System.setProperty("idea.max.intellisense.filesize", "100000") 125 } 126 } 127 createSourceParsernull128 override fun createSourceParser( 129 codebaseConfig: Codebase.Config, 130 javaLanguageLevel: String, 131 kotlinLanguageLevel: String, 132 modelOptions: ModelOptions, 133 allowReadingComments: Boolean, 134 jdkHome: File?, 135 ): SourceParser { 136 return PsiSourceParser( 137 psiEnvironmentManager = this, 138 codebaseConfig = codebaseConfig, 139 javaLanguageLevel = javaLanguageLevelFromString(javaLanguageLevel), 140 kotlinLanguageLevel = kotlinLanguageVersionSettings(kotlinLanguageLevel), 141 useK2Uast = modelOptions[PsiModelOptions.useK2Uast], 142 allowReadingComments = allowReadingComments, 143 jdkHome = jdkHome, 144 ) 145 } 146 closenull147 override fun close() { 148 closed = true 149 150 // Codebase.dispose() is not consistently called, so we dispose the environments here too. 151 for (env in uastEnvironments) { 152 if (!env.ideaProject.isDisposed) { 153 env.dispose() 154 } 155 } 156 uastEnvironments.clear() 157 158 // Only dispose of the application environment if this object was responsible for creating. 159 // If it was not then there is no point in checking to make sure that [Disposer] is empty 160 // because it will include items that have not yet been disposed of by the 161 // [PsiEnvironmentManager] which does own the application environment. 162 if (ownApplicationEnvironment) { 163 UastEnvironment.disposeApplicationEnvironment() 164 if (forTesting) { 165 Disposer.assertIsEmpty(true) 166 } 167 } 168 } 169 170 companion object { javaLanguageLevelFromStringnull171 fun javaLanguageLevelFromString(value: String): LanguageLevel { 172 val level = LanguageLevel.parse(value) 173 when { 174 level == null -> 175 throw IllegalStateException( 176 "$value is not a valid or supported Java language level" 177 ) 178 level.isLessThan(LanguageLevel.JDK_1_7) -> 179 throw IllegalStateException("$value must be at least 1.7") 180 else -> return level 181 } 182 } 183 } 184 } 185 186 private const val METALAVA_SYNTHETIC_SUFFIX = "metalava_module" 187