Skip to content

Commit

Permalink
Populate the compiled types and reference info to the CompilationResu…
Browse files Browse the repository at this point in the history
…lt to support incremental build
  • Loading branch information
testforstephen authored and mickaelistria committed Jul 10, 2024
1 parent a9ccec4 commit 454ed58
Show file tree
Hide file tree
Showing 6 changed files with 459 additions and 51 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<String[]> javacQualifiedReferences = new TreeSet<>((a, b) -> Arrays.compare(a, b));
private Set<String> javacSimpleNameReferences = new TreeSet<>();
private Set<String> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<File, List<ICompilationUnit>> outputSourceMapping = Arrays.stream(sourceUnits)
Map<IContainer, List<ICompilationUnit>> outputSourceMapping = Arrays.stream(sourceUnits)
.filter(unit -> {
/**
* Exclude the generated sources from the original source path to
Expand All @@ -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<File, List<ICompilationUnit>> outputSourceSet : outputSourceMapping.entrySet()) {
var outputFile = outputSourceSet.getKey();
JavacUtils.configureJavacContext(javacContext, this.compilerConfig, javaProject, outputFile);
for (Entry<IContainer, List<ICompilationUnit>> 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;

Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -52,7 +51,7 @@ public record JavacConfig(
/**
* The mapping of source files to output directories.
*/
Map<File, File> sourceOutputMapping,
Map<IContainer, IContainer> 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.
Expand All @@ -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);
}
Expand Down
Loading

0 comments on commit 454ed58

Please sign in to comment.