package com.google.errorprone.intellij;
import com.google.errorprone.matchers.Matcher;
import com.intellij.compiler.CompilerConfiguration;
import com.intellij.compiler.CompilerConfigurationImpl;
import com.intellij.compiler.OutputParser;
import com.intellij.compiler.impl.CompilerUtil;
import com.intellij.compiler.impl.javaCompiler.ExternalCompiler;
import com.intellij.compiler.impl.javaCompiler.ModuleChunk;
import com.intellij.compiler.impl.javaCompiler.javac.JavacConfigurable;
import com.intellij.compiler.impl.javaCompiler.javac.JavacConfiguration;
import com.intellij.compiler.impl.javaCompiler.javac.JavacOutputParser;
import com.intellij.compiler.impl.javaCompiler.javac.JavacSettingsBuilder;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.compiler.CompileContext;
import com.intellij.openapi.compiler.CompileScope;
import com.intellij.openapi.compiler.CompilerBundle;
import com.intellij.openapi.compiler.CompilerPaths;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.*;
import com.intellij.openapi.projectRoots.impl.MockJdkWrapper;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.util.ArrayUtil;
import com.intellij.util.PathUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jps.model.java.compiler.AnnotationProcessingConfiguration;
import org.jetbrains.jps.model.java.compiler.JpsJavaCompilerOptions;
import java.io.File;
import java.io.IOException;
import java.util.*;
public class ErrorProneIdeaCompiler extends ExternalCompiler {
private final Project myProject;
private final List<File> myTempFiles = new ArrayList<File>();
@NonNls
private boolean myAnnotationProcessorMode = false;
public ErrorProneIdeaCompiler(Project project) {
myProject = project;
}
public boolean checkCompiler(final CompileScope scope) {
final Module[] modules = scope.getAffectedModules();
final Set<Sdk> checkedJdks = new HashSet<Sdk>();
for (final Module module : modules) {
final Sdk jdk = ModuleRootManager.getInstance(module).getSdk();
if (jdk == null || checkedJdks.contains(jdk)) {
continue;
}
checkedJdks.add(jdk);
final SdkTypeId sdkType = jdk.getSdkType();
if (!(sdkType instanceof JavaSdkType)) {
continue;
}
final VirtualFile homeDirectory = jdk.getHomeDirectory();
if (homeDirectory == null) {
//noinspection DialogTitleCapitalization
Messages.showMessageDialog(
myProject,
ErrorProneIdeaBundle.jdkHomeNotFoundMessage(jdk),
ErrorProneIdeaBundle.compilerName(),
Messages.getErrorIcon()
);
return false;
}
final String vmExecutablePath = ((JavaSdkType)sdkType).getVMExecutablePath(jdk);
if (vmExecutablePath == null) {
Messages.showMessageDialog(
myProject,
ErrorProneIdeaBundle.message("error-prone.error.vm.executable.missing", jdk.getName()),
ErrorProneIdeaBundle.compilerName(),
Messages.getErrorIcon()
);
return false;
}
final String toolsJarPath = ((JavaSdkType)sdkType).getToolsPath(jdk);
if (toolsJarPath == null) {
Messages.showMessageDialog(
myProject,
ErrorProneIdeaBundle.message("error-prone.error.tools.jar.missing", jdk.getName()),
ErrorProneIdeaBundle.compilerName(),
Messages.getErrorIcon()
);
return false;
}
final String versionString = jdk.getVersionString();
if (versionString == null) {
Messages.showMessageDialog(
myProject,
ErrorProneIdeaBundle.message("error-prone.error.unknown.jdk.version", jdk.getName()),
ErrorProneIdeaBundle.compilerName(),
Messages.getErrorIcon()
);
return false;
}
if (CompilerUtil.isOfVersion(versionString, "1.0")) {
Messages.showMessageDialog(
myProject,
ErrorProneIdeaBundle.message("error-prone.error.1_0_compilation.not.supported"),
ErrorProneIdeaBundle.compilerName(),
Messages.getErrorIcon()
);
return false;
}
}
return true;
}
@NotNull
@NonNls
public String getId() { // used for externalization
return "Javac (with error-prone)";
}
@NotNull
public String getPresentableName() {
return ErrorProneIdeaBundle.compilerName();
}
@NotNull
public Configurable createConfigurable() {
return new JavacConfigurable(JavacConfiguration.getOptions(myProject, JavacConfiguration.class));
}
public OutputParser createErrorParser(@NotNull final String outputDir, Process process) {
return new JavacOutputParser(myProject);
}
public OutputParser createOutputParser(@NotNull final String outputDir) {
return null;
}
private static class MyException extends RuntimeException {
private MyException(Throwable cause) {
super(cause);
}
}
@NotNull
public String[] createStartupCommand(final ModuleChunk chunk, final CompileContext context, final String outputPath)
throws IOException, IllegalArgumentException {
try {
return ApplicationManager.getApplication().runReadAction(new Computable<String[]>() {
public String[] compute() {
try {
final List<String> commandLine = new ArrayList<String>();
createStartupCommand(chunk, commandLine, outputPath, JavacConfiguration.getOptions(myProject, JavacConfiguration.class), context.isAnnotationProcessorsEnabled());
System.out.println("Called as \n " + StringUtil.join(commandLine, " "));
return ArrayUtil.toStringArray(commandLine);
}
catch (IOException e) {
throw new MyException(e);
}
}
});
}
catch (MyException e) {
Throwable cause = e.getCause();
if (cause instanceof IOException) {
throw (IOException)cause;
}
throw e;
}
}
private void createStartupCommand(final ModuleChunk chunk, @NonNls final List<String> commandLine, final String outputPath,
JpsJavaCompilerOptions javacOptions, final boolean annotationProcessorsEnabled) throws IOException {
final Sdk jdk = getJdkForStartupCommand(chunk);
final String versionString = jdk.getVersionString();
JavaSdkVersion version = JavaSdk.getInstance().getVersion(jdk);
if (versionString == null || version == null || !(jdk.getSdkType() instanceof JavaSdkType)) {
throw new IllegalArgumentException(CompilerBundle.message("javac.error.unknown.jdk.version", jdk.getName()));
}
JavaSdkType sdkType = (JavaSdkType)jdk.getSdkType();
final String toolsJarPath = sdkType.getToolsPath(jdk);
if (toolsJarPath == null) {
throw new IllegalArgumentException(CompilerBundle.message("javac.error.tools.jar.missing", jdk.getName()));
}
final String vmExePath = sdkType.getVMExecutablePath(jdk);
commandLine.add(vmExePath);
commandLine.add("-Xmx" + javacOptions.MAXIMUM_HEAP_SIZE + "m");
final List<String> additionalOptions =
addAdditionalSettings(commandLine, javacOptions, myAnnotationProcessorMode, version, chunk, annotationProcessorsEnabled);
CompilerUtil.addLocaleOptions(commandLine, false);
commandLine.add("-classpath");
commandLine.add(sdkType.getToolsPath(jdk) + File.pathSeparator + PathUtil.getJarPathForClass(Matcher.class));
commandLine.add("com.google.errorprone.ErrorProneCompiler");
addCommandLineOptions(chunk, commandLine, outputPath, jdk, myAnnotationProcessorMode);
commandLine.addAll(additionalOptions);
final List<VirtualFile> files = chunk.getFilesToCompile();
for (final VirtualFile file : files) {
commandLine.add(file.getPath());
}
}
public static List<String> addAdditionalSettings(List<String> commandLine, JpsJavaCompilerOptions javacOptions, boolean isAnnotationProcessing,
JavaSdkVersion version, ModuleChunk chunk, boolean annotationProcessorsEnabled) {
final List<String> additionalOptions = new ArrayList<String>();
StringTokenizer tokenizer = new StringTokenizer(new JavacSettingsBuilder(javacOptions).getOptionsString(chunk), " ");
if (isAnnotationProcessing) {
final AnnotationProcessingConfiguration config = CompilerConfiguration.getInstance(chunk.getProject()).getAnnotationProcessingConfiguration(chunk.getModules()[0]);
additionalOptions.add("-Xprefer:source");
additionalOptions.add("-implicit:none");
additionalOptions.add("-proc:only");
if (!config.isObtainProcessorsFromClasspath()) {
final String processorPath = config.getProcessorPath();
additionalOptions.add("-processorpath");
additionalOptions.add(FileUtil.toSystemDependentName(processorPath));
}
final Set<String> processors = config.getProcessors();
if (!processors.isEmpty()) {
additionalOptions.add("-processor");
additionalOptions.add(StringUtil.join(processors, ","));
}
for (Map.Entry<String, String> entry : config.getProcessorOptions().entrySet()) {
additionalOptions.add("-A" + entry.getKey() + "=" +entry.getValue());
}
}
else {
if (annotationProcessorsEnabled) {
// Unless explicitly specified by user, disable annotation processing by default for 'java compilation' mode
// This is needed to suppress unwanted side-effects from auto-discovered processors from compilation classpath
additionalOptions.add("-proc:none");
}
}
while (tokenizer.hasMoreTokens()) {
@NonNls String token = tokenizer.nextToken();
if (version == JavaSdkVersion.JDK_1_0 && "-deprecation".equals(token)) {
continue; // not supported for this version
}
if (!version.isAtLeast(JavaSdkVersion.JDK_1_5) && "-Xlint".equals(token)) {
continue; // not supported in these versions
}
if (isAnnotationProcessing) {
if (token.startsWith("-proc:")) {
continue;
}
if (token.startsWith("-implicit:")) {
continue;
}
}
else { // compiling java
if (annotationProcessorsEnabled) {
// in this mode we have -proc:none already added above, so user's settings should be ignored
if (token.startsWith("-proc:")) {
continue;
}
}
}
if (token.startsWith("-J-")) {
commandLine.add(token.substring("-J".length()));
}
else {
additionalOptions.add(token);
}
}
return additionalOptions;
}
public static void addCommandLineOptions(@NotNull ModuleChunk chunk,
@NonNls List<String> commandLine,
@NotNull String outputPath,
@NotNull Sdk jdk,
boolean isAnnotationProcessingMode) throws IOException {
LanguageLevel languageLevel = chunk.getLanguageLevel();
CompilerUtil.addSourceCommandLineSwitch(jdk, languageLevel, commandLine);
commandLine.add("-verbose");
final String bootCp = chunk.getCompilationBootClasspath();
final String classPath = chunk.getCompilationClasspath();
commandLine.add("-bootclasspath");
addClassPathValue(commandLine, bootCp);
commandLine.add("-classpath");
addClassPathValue(commandLine, classPath);
if (isAnnotationProcessingMode) {
commandLine.add("-s");
commandLine.add(outputPath.replace('/', File.separatorChar));
final String moduleOutputPath = CompilerPaths.getModuleOutputPath(chunk.getModules()[0], false);
if (moduleOutputPath != null) {
commandLine.add("-d");
commandLine.add(moduleOutputPath.replace('/', File.separatorChar));
}
}
else {
commandLine.add("-d");
commandLine.add(outputPath.replace('/', File.separatorChar));
}
}
private static void addClassPathValue(@NotNull List<String> commandLine,
@NotNull String cpString) throws IOException {
commandLine.add(cpString);
}
private Sdk getJdkForStartupCommand(final ModuleChunk chunk) {
final Sdk jdk = chunk.getJdk();
if (ApplicationManager.getApplication().isUnitTestMode() && JavacConfiguration.getOptions(myProject, JavacConfiguration.class).isTestsUseExternalCompiler()) {
final String jdkHomePath = CompilerConfigurationImpl.getTestsExternalCompilerHome();
if (jdkHomePath == null) {
throw new IllegalArgumentException("[TEST-MODE] Cannot determine home directory for JDK to use javac from");
}
// when running under Mock JDK use VM executable from the JDK on which the tests run
return new MockJdkWrapper(jdkHomePath, jdk);
}
return jdk;
}
public void compileFinished() {
FileUtil.asyncDelete(myTempFiles);
myTempFiles.clear();
}
}