diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/compiler/GroovyCompiler.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/compiler/GroovyCompiler.java index 1346be937ad8..087684a4250f 100644 --- a/plugins/groovy/src/org/jetbrains/plugins/groovy/compiler/GroovyCompiler.java +++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/compiler/GroovyCompiler.java @@ -16,36 +16,44 @@ package org.jetbrains.plugins.groovy.compiler; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.compiler.CompileContext; import com.intellij.openapi.compiler.CompileScope; +import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.JavaModuleType; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.JavaSdkType; import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.roots.ContentIterator; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.roots.ui.configuration.ClasspathEditor; import com.intellij.openapi.roots.ui.configuration.ModulesConfigurator; import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.Computable; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.*; +import com.intellij.util.containers.FactoryMap; +import gnu.trove.THashMap; +import gnu.trove.THashSet; import org.jetbrains.annotations.NotNull; import org.jetbrains.plugins.groovy.GroovyBundle; import org.jetbrains.plugins.groovy.GroovyFileType; import org.jetbrains.plugins.groovy.GroovyFileTypeLoader; import org.jetbrains.plugins.groovy.config.GroovyConfigUtils; +import org.jetbrains.plugins.groovy.lang.psi.GroovyFile; +import org.jetbrains.plugins.groovy.lang.psi.api.types.GrCodeReferenceElement; import org.jetbrains.plugins.groovy.util.LibrariesUtil; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; /** * @author Dmitry.Krasilschikov */ public class GroovyCompiler extends GroovyCompilerBase { - private static final String GROOVY_COMPILER = "groovy compiler"; + private static final Logger LOG = Logger.getInstance("#org.jetbrains.plugins.groovy.compiler.GroovyCompiler"); public GroovyCompiler(Project project) { super(project); @@ -53,12 +61,145 @@ public class GroovyCompiler extends GroovyCompilerBase { @NotNull public String getDescription() { - return GROOVY_COMPILER; + return "groovy compiler"; } @Override - protected void compileFiles(CompileContext compileContext, Module module, List toCompile, OutputSink sink, boolean tests) { - runGroovycCompiler(compileContext, module, toCompile, false, getMainOutput(compileContext, module, tests), sink, tests); + protected void compileFiles(final CompileContext context, final Module module, List toCompile, OutputSink sink, boolean tests) { + final Set allToCompile = new LinkedHashSet(toCompile); + + // groovyc may fail if we don't also recompile files like B such that A depends on B and B depends on C, where A & C \in toCompile + // see http://jira.codehaus.org/browse/GROOVY-4024 + // this is important only if >1 files have been changed + if (toCompile.size() > 1 && !"false".equals(System.getProperty("compile.groovy.dependencies", "true"))) { + context.getProgressIndicator().checkCanceled(); + context.getProgressIndicator().setText("Enumerating Groovy classes..."); + + final Map moduleClasses = new THashMap(); + int groovyFiles = enumerateGroovyClasses(module, moduleClasses); + + if (toCompile.size() < groovyFiles) { + context.getProgressIndicator().checkCanceled(); + context.getProgressIndicator().setText("Processing Groovy dependencies..."); + + addIntermediateGroovyClasses(allToCompile, moduleClasses); + } + } + + context.getProgressIndicator().checkCanceled(); + context.getProgressIndicator().setText(GroovycOSProcessHandler.GROOVY_COMPILER_IN_OPERATION); + + runGroovycCompiler(context, module, new ArrayList(allToCompile), false, getMainOutput(context, module, tests), sink, tests); + } + + private void addIntermediateGroovyClasses(Set allToCompile, final Map moduleClasses) { + final Set initialClasses = new THashSet(); + for (VirtualFile file : allToCompile) { + initialClasses.addAll(getAllClasses(file)); + } + + final THashSet dirty = new THashSet(initialClasses); + + final THashSet visited = new THashSet(); + for (PsiClass aClass : initialClasses) { + if (visited.add(aClass)) { + goForIntermediateClasses(aClass, dirty, new FactoryMap>() { + @Override + protected Set create(final PsiClass key) { + return ApplicationManager.getApplication().runReadAction(new Computable>() { + public Set compute() { + return calcCodeReferenceDependencies(key, moduleClasses); + } + }); + } + }, visited); + } + } + for (PsiClass psiClass : dirty) { + allToCompile.add(moduleClasses.get(psiClass)); + } + } + + private int enumerateGroovyClasses(final Module module, final Map moduleClasses) { + final Set groovyFiles = new THashSet(); + ModuleRootManager.getInstance(module).getFileIndex().iterateContent(new ContentIterator() { + public boolean processFile(final VirtualFile vfile) { + if (!vfile.isDirectory() && GroovyFileType.GROOVY_FILE_TYPE.equals(vfile.getFileType())) { + final List classes = getAllClasses(vfile); + if (!classes.isEmpty()) { + groovyFiles.add(vfile); + for (PsiClass psiClass : classes) { + moduleClasses.put(psiClass, vfile); + } + } + + } + return true; + } + }); + return groovyFiles.size(); + } + + private List getAllClasses(final VirtualFile vfile) { + final ArrayList classes = new ArrayList(); + ApplicationManager.getApplication().runReadAction(new Runnable() { + public void run() { + final PsiFile file = PsiManager.getInstance(myProject).findFile(vfile); + if (file instanceof GroovyFile) { + addClassesFrom(((GroovyFile)file).getClasses(), classes); + } + } + + private void addClassesFrom(final PsiClass[] classes, List result) { + for (PsiClass aClass : classes) { + result.add(aClass); + addClassesFrom(aClass.getInnerClasses(), result); + } + } + + }); + return classes; + } + + + private static void goForIntermediateClasses(PsiClass from, Set dirty, FactoryMap> deps, Set visited) { + final Set set = deps.get(from); + for (PsiClass psiClass : set) { + if (visited.add(psiClass)) { + goForIntermediateClasses(psiClass, dirty, deps, visited); + } + if (dirty.contains(psiClass)) { + dirty.add(from); + } + } + } + + private Set calcCodeReferenceDependencies(PsiClass psiClass, final Map moduleClasses) { + final Set deps = new THashSet(); + psiClass.acceptChildren(new PsiElementVisitor() { + @Override + public void visitElement(PsiElement element) { + if (element instanceof PsiClass) { + return; + } + if (element instanceof GrCodeReferenceElement) { + GrCodeReferenceElement referenceElement = (GrCodeReferenceElement)element; + try { + final PsiElement target = referenceElement.resolve(); + if (target instanceof PsiClass && moduleClasses.containsKey(target)) { + deps.add((PsiClass)target); + } + } + catch (Exception e) { + LOG.error(e); + //prevent our PSI errors from failing the entire compilation + } + } + + element.acceptChildren(this); + } + }); + return deps; } public boolean validateConfiguration(CompileScope compileScope) { diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/compiler/GroovycOSProcessHandler.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/compiler/GroovycOSProcessHandler.java index 64ad41c847b1..b1d75ce67cef 100644 --- a/plugins/groovy/src/org/jetbrains/plugins/groovy/compiler/GroovycOSProcessHandler.java +++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/compiler/GroovycOSProcessHandler.java @@ -44,6 +44,7 @@ public class GroovycOSProcessHandler extends OSProcessHandler { private final CompileContext myContext; private static final Logger LOG = Logger.getInstance("org.jetbrains.plugins.groovy.compiler.GroovycOSProcessHandler"); + public static final String GROOVY_COMPILER_IN_OPERATION = "Groovy compiler in operation..."; public GroovycOSProcessHandler(CompileContext context, Process process, String s) { super(process, s); @@ -73,7 +74,7 @@ public class GroovycOSProcessHandler extends OSProcessHandler { } if (GroovycRunner.CLEAR_PRESENTABLE.equals(text)) { - myContext.getProgressIndicator().setText("Groovy compiler in operation..."); + myContext.getProgressIndicator().setText(GROOVY_COMPILER_IN_OPERATION); return; } diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/GroovyCompilerTest.groovy b/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/GroovyCompilerTest.groovy index fff8d04f1871..391947f56adc 100644 --- a/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/GroovyCompilerTest.groovy +++ b/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/GroovyCompilerTest.groovy @@ -287,5 +287,26 @@ class Foo { assertEmpty make() } + public void testRecompileDependentClass() throws Exception { + def cloud = myFixture.addFileToProject("Cloud.groovy", """ +class Cloud { + def accessFooProperty(Foo c) { + c.prop = 2 + } +} +""") + myFixture.addFileToProject "Foo.groovy", """ +class Foo { + def withGooParameter(Goo x) {} +}""" + def goo = myFixture.addFileToProject("Goo.groovy", "class Goo {}") + + assertEmpty make() + + touch(cloud.virtualFile) + touch(goo.virtualFile) + assertEmpty make() + } + }