From c2ff557fc28a532439d3c1fccea5c02f2f7c8897 Mon Sep 17 00:00:00 2001 From: Nikolay Chashnikov Date: Mon, 4 Dec 2023 17:10:41 +0100 Subject: [PATCH] [groovy] properly reset classloader caches after stub generation is finished (IDEA-322782) If class loaders are used to resolve references to classes in groovyc, it may happen that class file for groovy file is compiled from generated stub after groovy stub generation finished. To avoid "Unable to load class" error, we need to clear caches of the class loader which was created before stub generation started. Also, we need to disable the usage of classpath.index files which are updated only when the module chunk is fully compiled. Second compilation is still needed in such cases, but given that it's performed automatically, it doesn't affect users much. GitOrigin-RevId: 53b265c8966df06900e6b05558ce01ea8dfa77e9 --- .../incremental/groovy/InProcessGroovyc.java | 10 ++++- .../groovy/compiler/GroovycTestBase.groovy | 39 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/plugins/groovy/jps-plugin/src/org/jetbrains/jps/incremental/groovy/InProcessGroovyc.java b/plugins/groovy/jps-plugin/src/org/jetbrains/jps/incremental/groovy/InProcessGroovyc.java index d9f5e2054d8c..7765e95b926b 100644 --- a/plugins/groovy/jps-plugin/src/org/jetbrains/jps/incremental/groovy/InProcessGroovyc.java +++ b/plugins/groovy/jps-plugin/src/org/jetbrains/jps/incremental/groovy/InProcessGroovyc.java @@ -108,7 +108,7 @@ final class InProcessGroovyc implements GroovycFlavor { //noinspection unchecked Queue toGroovyc = (Queue)msg; loader.resetCache(); - return createContinuation(future, toGroovyc, parser); + return createContinuation(future, toGroovyc, parser, loader); } else if (msg != null) { throw new AssertionError("Unknown message: " + msg); @@ -119,11 +119,13 @@ final class InProcessGroovyc implements GroovycFlavor { @NotNull private static GroovycContinuation createContinuation(Future future, @NotNull Queue mailbox, - GroovycOutputParser parser) { + GroovycOutputParser parser, + @NotNull JointCompilationClassLoader loader) { return new GroovycContinuation() { @NotNull @Override public GroovyCompilerResult continueCompilation() throws Exception { + loader.resetCache(); parser.onContinuation(); mailbox.offer(GroovyRtConstants.JAVAC_COMPLETED); future.get(); @@ -206,6 +208,10 @@ final class InProcessGroovyc implements GroovycFlavor { return UrlClassLoader.build(). files(toPaths(compilationClassPath)) .parent(parent) + /* obsolete classpath.index files are deleted only after compilation of the module chunk finishes, so they may not include *.class + files produced by javac during compilation of this chunk; + therefore, persistent index should be disabled for Groovy class loader */ + .usePersistentClasspathIndexForLocalClassDirectories(false) .useCache(ourLoaderCachePool, file -> { String filePath = FileUtil.toCanonicalPath(file.toString()); for (String output : myOutputs) { diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/compiler/GroovycTestBase.groovy b/plugins/groovy/test/org/jetbrains/plugins/groovy/compiler/GroovycTestBase.groovy index e5051bb7bdef..4b1463b6dbbe 100644 --- a/plugins/groovy/test/org/jetbrains/plugins/groovy/compiler/GroovycTestBase.groovy +++ b/plugins/groovy/test/org/jetbrains/plugins/groovy/compiler/GroovycTestBase.groovy @@ -1,11 +1,14 @@ // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package org.jetbrains.plugins.groovy.compiler +import com.intellij.compiler.CompilerConfiguration import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ReadAction import com.intellij.openapi.module.Module import com.intellij.openapi.util.io.FileUtil import groovy.transform.CompileStatic +import org.jetbrains.groovy.compiler.rt.GroovyRtConstants +import org.jetbrains.jps.incremental.groovy.JpsGroovycRunner import org.jetbrains.plugins.groovy.TestLibrary import static com.intellij.testFramework.EdtTestUtil.runInEdtAndWait @@ -56,6 +59,42 @@ class Bar {}''' assert msg.message.contains('org.apache.commons.logging.Log') } + void "test circular dependency with in-process class loading resolving"() { + def groovyFile = myFixture.addFileToProject('mix/GroovyClass.groovy', ''' +package mix +@groovy.transform.CompileStatic +class GroovyClass { + JavaClass javaClass + String bar() { + return javaClass.foo() + } +} +''') + myFixture.addFileToProject('mix/JavaClass.java', ''' +package mix; +public class JavaClass { + GroovyClass groovyClass; + public String foo() { + return "foo"; + } +} +''') + CompilerConfiguration.getInstance(project).buildProcessVMOptions += + " -D$JpsGroovycRunner.GROOVYC_IN_PROCESS=true -D$GroovyRtConstants.GROOVYC_ASM_RESOLVING_ONLY=false" + assertEmpty(make()) + + touch(groovyFile.virtualFile) + + def messages = make() + + /* since only groovy file is changed, its class file is deleted, but javac isn't called (JavaBuilder.compile returns early), so + GroovyClass.class file from the generated stub isn't produced, and the classloader failed to load JavaClass during compilation of + GroovyClass. After chunk rebuild is requested, javac is called so it compiles the stub and groovyc finishes successfully. + */ + assert messages.collect { it.message } == chunkRebuildMessage("Groovy compiler") + } + + protected List chunkRebuildMessage(String builder) { return ['Builder "' + builder + '" requested rebuild of module chunk "mainModule"'] }