diff --git a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacClassFile.java b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacClassFile.java new file mode 100644 index 00000000000..79e094e77ff --- /dev/null +++ b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacClassFile.java @@ -0,0 +1,149 @@ +/******************************************************************************* +* Copyright (c) 2024 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License 2.0 +* which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Microsoft Corporation - initial API and implementation +*******************************************************************************/ + +package org.eclipse.jdt.internal.javac; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.Path; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.internal.compiler.ClassFile; +import org.eclipse.jdt.internal.compiler.util.SuffixConstants; + +public class JavacClassFile extends ClassFile { + private String fullName; + private IContainer outputDir; + private byte[] bytes = null; + private File proxyFile = null; + + public JavacClassFile(String qualifiedName, ClassFile enclosingClass, IContainer outputDir) { + this.fullName = qualifiedName; + this.isNestedType = enclosingClass != null; + this.enclosingClassFile = enclosingClass; + this.outputDir = outputDir; + } + + @Override + public char[][] getCompoundName() { + String[] names = this.fullName.split("\\."); + char[][] compoundNames = new char[names.length][]; + for (int i = 0; i < names.length; i++) { + compoundNames[i] = names[i].toCharArray(); + } + + return compoundNames; + } + + @Override + public char[] fileName() { + String compoundName = this.fullName.replace('.', '/'); + return compoundName.toCharArray(); + } + + @Override + public byte[] getBytes() { + if (this.bytes == null) { + File tempClassFile = this.getProxyTempClassFile(); + if (tempClassFile == null || !tempClassFile.exists()) { + this.bytes = new byte[0]; + } else { + try { + this.bytes = Files.readAllBytes(tempClassFile.toPath()); + } catch (IOException e) { + this.bytes = new byte[0]; + } + } + } + + return this.bytes; + } + + File getProxyTempClassFile() { + if (this.proxyFile == null) { + this.proxyFile = computeMappedTempClassFile(this.outputDir, this.fullName); + } + + return this.proxyFile; + } + + void deleteTempClassFile() { + File tempClassFile = this.getProxyTempClassFile(); + if (tempClassFile != null && tempClassFile.exists()) { + tempClassFile.delete(); + } + } + + void deleteExpectedClassFile() { + IFile targetClassFile = computeExpectedClassFile(this.outputDir, this.fullName); + if (targetClassFile != null) { + try { + targetClassFile.delete(true, null); + } catch (CoreException e) { + // ignore + } + } + } + + /** + * Returns the mapped temporary class file for the specified class symbol. + */ + public static File computeMappedTempClassFile(IContainer expectedOutputDir, String qualifiedClassName) { + if (expectedOutputDir != null) { + IPath baseOutputPath = getMappedTempOutput(expectedOutputDir); + String fileName = qualifiedClassName.replace('.', File.separatorChar); + IPath filePath = new Path(fileName); + return baseOutputPath.append(filePath.addFileExtension(SuffixConstants.EXTENSION_class)).toFile(); + } + + return null; + } + + /** + * Returns the expected class file for the specified class symbol. + */ + public static IFile computeExpectedClassFile(IContainer expectedOutputDir, String qualifiedClassName) { + if (expectedOutputDir != null) { + String fileName = qualifiedClassName.replace('.', File.separatorChar); + IPath filePath = new Path(fileName); + return expectedOutputDir.getFile(filePath.addFileExtension(SuffixConstants.EXTENSION_class)); + } + + return null; + } + + /** + * The upstream ImageBuilder expects the Javac Compiler to return the + * class file as bytes instead of writing it directly to the target + * output directory. To prevent conflicts with the ImageBuilder, we + * configure Javac to generate the class file in a temporary location. + * This method returns the mapped temporary output location for the + * specified output directory. + */ + public static IPath getMappedTempOutput(IContainer expectedOutput) { + IProject project = expectedOutput.getProject(); + if (project == null) { + return expectedOutput.getRawLocation(); + } + + IPath workingLocation = project.getWorkingLocation(JavaCore.PLUGIN_ID); + String tempOutputName = expectedOutput.getName() + "_" + Integer.toHexString(expectedOutput.hashCode()); + return workingLocation.append("javac/" + tempOutputName); + } +} diff --git a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacCompilationResult.java b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacCompilationResult.java new file mode 100644 index 00000000000..afdb12b8828 --- /dev/null +++ b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacCompilationResult.java @@ -0,0 +1,68 @@ +/******************************************************************************* +* Copyright (c) 2024 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License 2.0 +* which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Microsoft Corporation - initial API and implementation +*******************************************************************************/ + +package org.eclipse.jdt.internal.javac; + +import java.util.Arrays; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Stream; + +import org.eclipse.jdt.internal.compiler.CompilationResult; +import org.eclipse.jdt.internal.compiler.env.ICompilationUnit; + +public class JavacCompilationResult extends CompilationResult { + private Set javacQualifiedReferences = new TreeSet<>((a, b) -> Arrays.compare(a, b)); + private Set javacSimpleNameReferences = new TreeSet<>(); + private Set javacRootReferences = new TreeSet<>(); + private boolean isMigrated = false; + + public JavacCompilationResult(ICompilationUnit compilationUnit) { + this(compilationUnit, 0, 0, Integer.MAX_VALUE); + } + + public JavacCompilationResult(ICompilationUnit compilationUnit, int unitIndex, int totalUnitsKnown, + int maxProblemPerUnit) { + super(compilationUnit, unitIndex, totalUnitsKnown, maxProblemPerUnit); + } + + public boolean addQualifiedReference(String[] qualifiedReference) { + return this.javacQualifiedReferences.add(qualifiedReference); + } + + public boolean addSimpleNameReference(String simpleNameReference) { + return this.javacSimpleNameReferences.add(simpleNameReference); + } + + public boolean addRootReference(String rootReference) { + return this.javacRootReferences.add(rootReference); + } + + public void migrateReferenceInfo() { + if (isMigrated) { + return; + } + + this.simpleNameReferences = this.javacSimpleNameReferences.stream().map(String::toCharArray).toArray(char[][]::new); + this.rootReferences = this.javacRootReferences.stream().map(String::toCharArray).toArray(char[][]::new); + this.qualifiedReferences = this.javacQualifiedReferences.stream().map(qualifiedNames -> { + // convert String[] to char[][] + return Stream.of(qualifiedNames).map(String::toCharArray).toArray(char[][]::new); + }).toArray(char[][][]::new); + + this.javacSimpleNameReferences.clear(); + this.javacRootReferences.clear(); + this.javacQualifiedReferences.clear(); + this.isMigrated = true; + } +} diff --git a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacCompiler.java b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacCompiler.java index b358b9fb770..5b01970c98e 100644 --- a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacCompiler.java +++ b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacCompiler.java @@ -27,6 +27,7 @@ import javax.tools.JavaFileObject; import javax.tools.JavaFileObject.Kind; +import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IResource; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; @@ -77,11 +78,12 @@ public void compile(ICompilationUnit[] sourceUnits) { } } }); + IJavaProject javaProject = Stream.of(sourceUnits).filter(SourceFile.class::isInstance).map( SourceFile.class::cast).map(source -> source.resource).map(IResource::getProject).filter( JavaProject::hasJavaNature).map(JavaCore::create).findFirst().orElse(null); - Map> outputSourceMapping = Arrays.stream(sourceUnits) + Map> outputSourceMapping = Arrays.stream(sourceUnits) .filter(unit -> { /** * Exclude the generated sources from the original source path to @@ -103,13 +105,14 @@ public void compile(ICompilationUnit[] sourceUnits) { .collect(Collectors.groupingBy(this::computeOutputDirectory)); // Register listener to intercept intermediate results from Javac task. - JavacTaskListener resultListener = new JavacTaskListener(this.compilerConfig, outputSourceMapping); + JavacTaskListener javacListener = new JavacTaskListener(this.compilerConfig, outputSourceMapping); MultiTaskListener mtl = MultiTaskListener.instance(javacContext); - mtl.add(resultListener); + mtl.add(javacListener); - for (Entry> outputSourceSet : outputSourceMapping.entrySet()) { - var outputFile = outputSourceSet.getKey(); - JavacUtils.configureJavacContext(javacContext, this.compilerConfig, javaProject, outputFile); + for (Entry> outputSourceSet : outputSourceMapping.entrySet()) { + // Configure Javac to generate the class files in a mapped temporary location + var outputDir = JavacClassFile.getMappedTempOutput(outputSourceSet.getKey()).toFile(); + JavacUtils.configureJavacContext(javacContext, this.compilerConfig, javaProject, outputDir); JavaCompiler javac = new JavaCompiler(javacContext) { boolean isInGeneration = false; @@ -164,6 +167,13 @@ public int errorCount() { for (int i = 0; i < sourceUnits.length; i++) { ICompilationUnit in = sourceUnits[i]; CompilationResult result = new CompilationResult(in, i, sourceUnits.length, Integer.MAX_VALUE); + if (javacListener.getResults().containsKey(in)) { + result = javacListener.getResults().get(in); + ((JavacCompilationResult) result).migrateReferenceInfo(); + result.unitIndex = i; + result.totalUnitsKnown = sourceUnits.length; + } + if (javacProblems.containsKey(in)) { JavacProblem[] problems = javacProblems.get(in).toArray(new JavacProblem[0]); result.problems = problems; // JavaBuilder is responsible @@ -172,20 +182,39 @@ public int errorCount() { result.problemCount = problems.length; } this.requestor.acceptResult(result); + if (result.compiledTypes != null) { + for (Object type : result.compiledTypes.values()) { + if (type instanceof JavacClassFile classFile) { + // Delete the temporary class file generated by Javac + classFile.deleteTempClassFile(); + /** + * Javac does not generate class files for files with errors. + * However, we return 0 bytes to the CompilationResult to + * prevent NPE when the ImageBuilder writes failed class files. + * These 0-byte class files are empty and meaningless, which + * can confuse subsequent compilations since they are included + * in the classpath. Therefore, they should be deleted after + * compilation. + */ + if (classFile.getBytes().length == 0) { + classFile.deleteExpectedClassFile(); + } + } + } + } } } } - - private File computeOutputDirectory(ICompilationUnit unit) { + + private IContainer computeOutputDirectory(ICompilationUnit unit) { if (unit instanceof SourceFile sf) { - File sourceFile = sf.resource.getLocation().toFile(); - File sourceDirectory = sourceFile.getParentFile(); + IContainer sourceDirectory = sf.resource.getParent(); while (sourceDirectory != null) { - File mappedOutput = this.compilerConfig.sourceOutputMapping().get(sourceDirectory); + IContainer mappedOutput = this.compilerConfig.sourceOutputMapping().get(sourceDirectory); if (mappedOutput != null) { return mappedOutput; } - sourceDirectory = sourceDirectory.getParentFile(); + sourceDirectory = sourceDirectory.getParent(); } } return null; diff --git a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacConfig.java b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacConfig.java index e81987d4e15..a51a1e83f86 100644 --- a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacConfig.java +++ b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacConfig.java @@ -13,7 +13,6 @@ package org.eclipse.jdt.internal.javac; -import java.io.File; import java.net.URI; import java.util.List; import java.util.Map; @@ -52,7 +51,7 @@ public record JavacConfig( /** * The mapping of source files to output directories. */ - Map sourceOutputMapping, + Map sourceOutputMapping, /** * The compiler options used to control the compilation behavior. * See {@link org.eclipse.jdt.internal.compiler.impl.CompilerOptions} for a list of available options. @@ -71,7 +70,7 @@ static JavacConfig createFrom(CompilerConfiguration config) { config.modulepaths().stream().map(URI::getPath).collect(Collectors.toList()), config.annotationProcessorPaths().stream().map(URI::getPath).collect(Collectors.toList()), config.generatedSourcePaths().stream().map(IContainer::getRawLocation).filter(path -> path != null).map(IPath::toOSString).collect(Collectors.toList()), - config.sourceOutputMapping().entrySet().stream().collect(Collectors.toMap(e -> e.getKey().getRawLocation().toFile(), e -> e.getValue().getRawLocation().toFile())), + config.sourceOutputMapping(), config.compilerOptions(), config); } diff --git a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacTaskListener.java b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacTaskListener.java index 6455c4d9717..3f29df12f22 100644 --- a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacTaskListener.java +++ b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacTaskListener.java @@ -13,64 +13,219 @@ package org.eclipse.jdt.internal.javac; -import java.io.File; +import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.stream.Collectors; +import java.util.Objects; +import java.util.Set; import javax.lang.model.element.TypeElement; +import javax.tools.JavaFileObject; import org.eclipse.core.resources.IContainer; -import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IPath; -import org.eclipse.core.runtime.Path; +import org.eclipse.jdt.internal.compiler.ClassFile; import org.eclipse.jdt.internal.compiler.env.ICompilationUnit; -import org.eclipse.jdt.internal.compiler.util.SuffixConstants; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.MemberSelectTree; import com.sun.source.util.TaskEvent; import com.sun.source.util.TaskListener; +import com.sun.source.util.TreeScanner; +import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.ClassSymbol; +import com.sun.tools.javac.code.Symbol.PackageSymbol; +import com.sun.tools.javac.code.Symbol.TypeSymbol; +import com.sun.tools.javac.code.Symbol.VarSymbol; +import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.code.Type.ArrayType; +import com.sun.tools.javac.code.Type.MethodType; +import com.sun.tools.javac.code.Type.UnknownType; +import com.sun.tools.javac.tree.JCTree.JCClassDecl; +import com.sun.tools.javac.tree.JCTree.JCFieldAccess; +import com.sun.tools.javac.tree.JCTree.JCIdent; public class JavacTaskListener implements TaskListener { private Map sourceOutputMapping = new HashMap<>(); + private Map results = new HashMap<>(); + private static final Set PRIMITIVE_TYPES = new HashSet(Arrays.asList( + "byte", + "short", + "int", + "long", + "float", + "double", + "char", + "boolean" + )); - public JavacTaskListener(JavacConfig config, Map> outputSourceMapping) { - Map outputs = config.originalConfig().sourceOutputMapping().values().stream() - .distinct().filter(container -> container.getRawLocation() != null) - .collect(Collectors.toMap(container -> container.getRawLocation().toFile(), container -> container)); - for (Entry> entry : outputSourceMapping.entrySet()) { - if (outputs.containsKey(entry.getKey())) { - IContainer currentOutput = outputs.get(entry.getKey()); - entry.getValue().forEach(cu -> sourceOutputMapping.put(cu, currentOutput)); - } + public JavacTaskListener(JavacConfig config, Map> outputSourceMapping) { + for (Entry> entry : outputSourceMapping.entrySet()) { + IContainer currentOutput = entry.getKey(); + entry.getValue().forEach(cu -> sourceOutputMapping.put(cu, currentOutput)); } } @Override public void finished(TaskEvent e) { - if (e.getKind() == TaskEvent.Kind.GENERATE) { - if (e.getSourceFile() instanceof JavacFileObject sourceFile) { - ICompilationUnit originalUnit = sourceFile.getOriginalUnit(); - IContainer outputDir = this.sourceOutputMapping.get(originalUnit); - if (outputDir != null) { - TypeElement element = e.getTypeElement(); - if (element instanceof ClassSymbol clazz) { - String fileName = clazz.flatName().toString().replace('.', File.separatorChar); - IPath filePath = new Path(fileName); - IFile classFile = outputDir.getFile(filePath.addFileExtension(SuffixConstants.EXTENSION_class)); - try { - // refresh the class file to make sure it is visible in the Eclipse workspace - classFile.refreshLocal(IResource.DEPTH_ZERO, null); - } catch (CoreException e1) { - // TODO error handling + if (e.getKind() == TaskEvent.Kind.ANALYZE) { + final JavaFileObject file = e.getSourceFile(); + if (!(file instanceof JavacFileObject)) { + return; + } + + final ICompilationUnit cu = ((JavacFileObject) file).getOriginalUnit(); + final JavacCompilationResult result = this.results.computeIfAbsent(cu, (cu1) -> + new JavacCompilationResult(cu1)); + final Map visitedClasses = new HashMap(); + final Set hierarchyRecorded = new HashSet<>(); + final TypeElement currentTopLevelType = e.getTypeElement(); + TreeScanner scanner = new TreeScanner() { + @Override + public Object visitClass(ClassTree node, Object p) { + if (node instanceof JCClassDecl classDecl) { + /** + * If a Java file contains multiple top-level types, it will + * trigger multiple ANALYZE taskEvents for the same compilation + * unit. Each ANALYZE taskEvent corresponds to the completion + * of analysis for a single top-level type. Therefore, in the + * ANALYZE task event listener, we only visit the class and nested + * classes that belong to the currently analyzed top-level type. + */ + if (Objects.equals(currentTopLevelType, classDecl.sym) + || !(classDecl.sym.owner instanceof PackageSymbol)) { + String fullName = classDecl.sym.flatName().toString(); + String compoundName = fullName.replace('.', '/'); + Symbol enclosingClassSymbol = this.getEnclosingClass(classDecl.sym); + ClassFile enclosingClassFile = enclosingClassSymbol == null ? null : visitedClasses.get(enclosingClassSymbol); + IContainer expectedOutputDir = sourceOutputMapping.get(cu); + ClassFile currentClass = new JavacClassFile(fullName, enclosingClassFile, expectedOutputDir); + visitedClasses.put(classDecl.sym, currentClass); + result.record(compoundName.toCharArray(), currentClass); + recordTypeHierarchy(classDecl.sym); + } else { + return null; // Skip if it does not belong to the currently analyzed top-level type. } } + + return super.visitClass(node, p); } - } + + @Override + public Object visitIdentifier(IdentifierTree node, Object p) { + if (node instanceof JCIdent id + && id.sym instanceof TypeSymbol typeSymbol) { + String qualifiedName = typeSymbol.getQualifiedName().toString(); + recordQualifiedReference(qualifiedName, false); + } + return super.visitIdentifier(node, p); + } + + @Override + public Object visitMemberSelect(MemberSelectTree node, Object p) { + if (node instanceof JCFieldAccess field) { + if (field.sym != null && + !(field.type instanceof MethodType || field.type instanceof UnknownType)) { + recordQualifiedReference(node.toString(), false); + if (field.sym instanceof VarSymbol) { + TypeSymbol elementSymbol = field.type.tsym; + if (field.type instanceof ArrayType arrayType) { + elementSymbol = getElementType(arrayType); + } + if (elementSymbol instanceof ClassSymbol classSymbol) { + recordQualifiedReference(classSymbol.className(), true); + } + } + } + } + return super.visitMemberSelect(node, p); + } + + private Symbol getEnclosingClass(Symbol symbol) { + while (symbol != null) { + if (symbol.owner instanceof ClassSymbol) { + return symbol.owner; + } else if (symbol.owner instanceof PackageSymbol) { + return null; + } + + symbol = symbol.owner; + } + + return null; + } + + private TypeSymbol getElementType(ArrayType arrayType) { + if (arrayType.elemtype instanceof ArrayType subArrayType) { + return getElementType(subArrayType); + } + + return arrayType.elemtype.tsym; + } + + private void recordQualifiedReference(String qualifiedName, boolean recursive) { + if (PRIMITIVE_TYPES.contains(qualifiedName)) { + return; + } + + String[] nameParts = qualifiedName.split("\\."); + int length = nameParts.length; + if (length == 1) { + result.addRootReference(nameParts[0]); + result.addSimpleNameReference(nameParts[0]); + return; + } + + if (!recursive) { + result.addRootReference(nameParts[0]); + result.addSimpleNameReference(nameParts[length - 1]); + result.addQualifiedReference(nameParts); + } else { + result.addRootReference(nameParts[0]); + while (result.addQualifiedReference(Arrays.copyOfRange(nameParts, 0, length))) { + if (length == 2) { + result.addSimpleNameReference(nameParts[0]); + result.addSimpleNameReference(nameParts[1]); + return; + } + + length--; + result.addSimpleNameReference(nameParts[length]); + } + } + } + + private void recordTypeHierarchy(ClassSymbol classSymbol) { + if (hierarchyRecorded.contains(classSymbol)) { + return; + } + + hierarchyRecorded.add(classSymbol); + Type superClass = classSymbol.getSuperclass(); + if (superClass.tsym instanceof ClassSymbol superClassType) { + recordQualifiedReference(superClassType.className(), true); + recordTypeHierarchy(superClassType); + } + + for (Type superInterface : classSymbol.getInterfaces()) { + if (superInterface.tsym instanceof ClassSymbol superInterfaceType) { + recordQualifiedReference(superInterfaceType.className(), true); + recordTypeHierarchy(superInterfaceType); + } + } + } + }; + + final CompilationUnitTree unit = e.getCompilationUnit(); + scanner.scan(unit, null); } } + + public Map getResults() { + return this.results; + } } diff --git a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacUtils.java b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacUtils.java index 605022b16ae..e6463920650 100644 --- a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacUtils.java +++ b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacUtils.java @@ -168,14 +168,15 @@ private static void configurePaths(JavaProject javaProject, Context context, Jav } if (output != null) { - fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(output)); + fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(ensureDirExists(output))); } else if (compilerConfig != null && !compilerConfig.sourceOutputMapping().isEmpty()) { - fileManager.setLocation(StandardLocation.CLASS_OUTPUT, compilerConfig.sourceOutputMapping().values().stream().distinct().toList()); + fileManager.setLocation(StandardLocation.CLASS_OUTPUT, compilerConfig.sourceOutputMapping().values().stream().distinct() + .map(container -> ensureDirExists(JavacClassFile.getMappedTempOutput(container).toFile())).toList()); } else if (javaProject.getProject() != null) { IResource member = javaProject.getProject().getParent().findMember(javaProject.getOutputLocation()); if( member != null ) { File f = member.getLocation().toFile(); - fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(f)); + fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(ensureDirExists(f))); } } @@ -268,4 +269,11 @@ private static Collection classpathEntriesToFiles(JavaProject project, Pre } } + private static File ensureDirExists(File file) { + if (!file.exists()) { + file.mkdirs(); + } + + return file; + } }