diff --git a/src/main/kotlin/zone/clanker/gradle/srcx/analysis/SourceFileMetadata.kt b/src/main/kotlin/zone/clanker/gradle/srcx/analysis/SourceFileMetadata.kt index c35c374..29acab6 100644 --- a/src/main/kotlin/zone/clanker/gradle/srcx/analysis/SourceFileMetadata.kt +++ b/src/main/kotlin/zone/clanker/gradle/srcx/analysis/SourceFileMetadata.kt @@ -98,9 +98,9 @@ fun parseSourceFile(file: File, psiManager: PsiManager): SourceFileMetadata? { * Prefer the overload that accepts a [PsiManager] for batch parsing. */ fun parseSourceFile(file: File): SourceFileMetadata? { - if (!file.isFile) return null - if (file.extension != "kt" && file.extension != "java") return null - return PsiEnvironment().use { env -> parseSourceFile(file, env.psiManager) } + if (!file.isFile || file.extension !in setOf("kt", "java")) return null + val env = PsiEnvironment.shared() ?: return null + return synchronized(env) { parseSourceFile(file, env.psiManager) } } /** Resolved type-level info from the first class/object in a Kotlin file. */ @@ -303,8 +303,8 @@ fun scanSources(srcDirs: List): List { } if (files.isEmpty()) return emptyList() - return PsiEnvironment().use { env -> - val psiManager = env.psiManager - files.mapNotNull { file -> parseSourceFile(file, psiManager) } + val env = PsiEnvironment.shared() ?: return emptyList() + return synchronized(env) { + files.mapNotNull { file -> parseSourceFile(file, env.psiManager) } } } diff --git a/src/main/kotlin/zone/clanker/gradle/srcx/parse/PsiEnvironment.kt b/src/main/kotlin/zone/clanker/gradle/srcx/parse/PsiEnvironment.kt index aa16b59..8c142d6 100644 --- a/src/main/kotlin/zone/clanker/gradle/srcx/parse/PsiEnvironment.kt +++ b/src/main/kotlin/zone/clanker/gradle/srcx/parse/PsiEnvironment.kt @@ -41,4 +41,44 @@ class PsiEnvironment : AutoCloseable { override fun close() { Disposer.dispose(disposable) } + + companion object { + private val logger = + org.gradle.api.logging.Logging + .getLogger(PsiEnvironment::class.java) + + @Volatile + private var shared: PsiEnvironment? = null + private val lock = Any() + + /** + * Get or create a shared PsiEnvironment instance. + * + * The IntelliJ platform's ApplicationManager is not thread-safe, + * so we create exactly one instance and reuse it across all + * analysis calls. The lock ensures only one thread initializes. + */ + fun shared(): PsiEnvironment? { + shared?.let { return it } + synchronized(lock) { + shared?.let { return it } + return runCatching { PsiEnvironment() } + .onSuccess { shared = it } + .onFailure { e -> + logger.error( + "srcx: PSI environment initialization failed: ${e.message}. " + + "Hub classes and entry points will not be available.", + ) + }.getOrNull() + } + } + + /** Release the shared instance. Call once when all analysis is done. */ + fun closeShared() { + synchronized(lock) { + shared?.close() + shared = null + } + } + } } diff --git a/src/main/kotlin/zone/clanker/gradle/srcx/task/ContextTask.kt b/src/main/kotlin/zone/clanker/gradle/srcx/task/ContextTask.kt index 2eef6c9..dddf216 100644 --- a/src/main/kotlin/zone/clanker/gradle/srcx/task/ContextTask.kt +++ b/src/main/kotlin/zone/clanker/gradle/srcx/task/ContextTask.kt @@ -157,6 +157,8 @@ abstract class ContextTask : DefaultTask() { writeSplitFiles(dir, summaryList, includedBuildSummaries, buildEdges, aggregatedSummary) ReportWriter.writeGitignore(root, outDir) + zone.clanker.gradle.srcx.parse.PsiEnvironment + .closeShared() logger.lifecycle("srcx: context written to $outDir/context.md") }