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