CompilerConfiguration.java

package io.github.jonasrutishauser.errorprone.maven.plugin;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;
import javax.inject.Named;

import org.apache.commons.lang3.JavaVersion;
import org.apache.commons.lang3.SystemUtils;
import org.apache.maven.SessionScoped;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.PluginParameterExpressionEvaluator;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Named
@SessionScoped
class CompilerConfiguration {

    private static final Logger LOGGER = LoggerFactory.getLogger(CompilerConfiguration.class);

    private static final List<String> JVM_ARGS_STRONG_ENCAPSULATION = List.of(
            "-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
            "-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
            "-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
            "-J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED",
            "-J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
            "-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
            "-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
            "-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
            "-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
            "-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED");

    private static final List<String> COMPILER_ARGS = List.of( //
            "-XDcompilePolicy=simple", //
            "--should-stop=ifError=FLOW", //
            "-XDaddTypeAnnotationsToSymbol=true");

    private final Map<String, List<MojoExecution>> compilerExecutions = new HashMap<>();
    private final MavenSession session;

    @Inject
    CompilerConfiguration(MavenSession session) {
        this.session = session;
    }

    public void setCompilerExecutions(MavenProject project, List<MojoExecution> executions) {
        compilerExecutions.put(project.getId(), executions);
    }

    public void clearCompilerExecutions(MavenProject project) {
        compilerExecutions.remove(project.getId());
    }

    void configure(MavenProject project, String propertyName) {
        for (MojoExecution compilerExecution : compilerExecutions.getOrDefault(project.getId(), List.of())) {
            configureCompilerPlugin(project, compilerExecution, propertyName);
        }
    }

    String getParameterValue(MojoExecution mojoExecution, Xpp3Dom value) {
        PluginParameterExpressionEvaluator evaluator = new PluginParameterExpressionEvaluator(session, mojoExecution);
        return getParameterValue(evaluator, value);
    }

    private String getParameterValue(PluginParameterExpressionEvaluator evaluator, Xpp3Dom value) {
        if (value == null) {
            return null;
        }
        try {
            Object evaluated = evaluator.evaluate(value.getValue());
            return evaluated == null ? value.getAttribute("default-value") : evaluated.toString();
        } catch (ExpressionEvaluationException e) {
            return null;
        }
    }

    private void configureCompilerPlugin(MavenProject project, MojoExecution compilerExecution, String propertyName) {
        LOGGER.debug("Configuring compiler plugin for execution {}", compilerExecution.getExecutionId());
        PluginParameterExpressionEvaluator evaluator = new PluginParameterExpressionEvaluator(session, compilerExecution);

        addAnnotationProcessorPaths(project.getDependencies(), compilerExecution.getConfiguration(), evaluator);
        if (project.getDependencyManagement() != null) {
            addAnnotationProcessorPaths(project.getDependencyManagement().getDependencies(),
                    compilerExecution.getConfiguration(), evaluator);
        }

        Xpp3Dom fork = setForkIfNeeded(compilerExecution.getConfiguration(), evaluator);

        Xpp3Dom compilerArgs = createOrGetCompilerArgs(compilerExecution.getConfiguration());
        addPluginArgument(propertyName, compilerArgs, evaluator);
        addCompilerArguments(compilerArgs, evaluator);
        if (isTrue(fork, evaluator)) {
            addJvmStrongEncapsulationArguments(compilerArgs, evaluator);
        }
    }

    private void addAnnotationProcessorPaths(List<Dependency> dependencies, Xpp3Dom configuration, PluginParameterExpressionEvaluator evaluator) {
        for (Dependency dependency : dependencies) {
            if ("errorprone".equals(dependency.getType())) {
                addAnnotationProcessorPath(configuration, dependency, evaluator);
            }
        }
    }

    private void addJvmStrongEncapsulationArguments(Xpp3Dom compilerArgs, PluginParameterExpressionEvaluator evaluator) {
        for (String jvmArg : JVM_ARGS_STRONG_ENCAPSULATION) {
            if (!hasCompilerArg(compilerArgs.getChildren(), jvmArg, evaluator)) {
                LOGGER.debug("Adding compiler argument \"{}\"", jvmArg);
                Xpp3Dom compilerArg = new Xpp3Dom("arg");
                compilerArg.setValue(jvmArg);
                compilerArgs.addChild(compilerArg);
            }
        }
    }

    private void addCompilerArguments(Xpp3Dom compilerArgs, PluginParameterExpressionEvaluator evaluator) {
        for (String arg : COMPILER_ARGS) {
            if (!hasCompilerArg(compilerArgs.getChildren(), arg, evaluator)) {
                LOGGER.debug("Adding compiler argument \"{}\"", arg);
                Xpp3Dom compilerArg = new Xpp3Dom("arg");
                compilerArg.setValue(arg);
                compilerArgs.addChild(compilerArg);
            }
        }
    }

    private void addPluginArgument(String propertyName, Xpp3Dom compilerArgs, PluginParameterExpressionEvaluator evaluator) {
        if (!hasCompilerArg(compilerArgs.getChildren(), "-Xplugin:ErrorProne", evaluator)) {
            LOGGER.debug("Adding compiler argument \"${{}}\"", propertyName);
            Xpp3Dom compilerArg = new Xpp3Dom("arg");
            compilerArg.setValue("${" + propertyName + "}");
            compilerArgs.addChild(compilerArg);
        }
    }

    private Xpp3Dom createOrGetCompilerArgs(Xpp3Dom configuration) {
        Xpp3Dom compilerArgs = configuration.getChild("compilerArgs");
        if (compilerArgs == null) {
            compilerArgs = new Xpp3Dom("compilerArgs");
            configuration.addChild(compilerArgs);
        }
        return compilerArgs;
    }

    private Xpp3Dom setForkIfNeeded(Xpp3Dom configuration, PluginParameterExpressionEvaluator evaluator) {
        Xpp3Dom fork = configuration.getChild("fork");
        if (StrongEncapsulationHelperJava.CURRENT_JVM_NEEDS_FORKING && !isTrue(fork, evaluator)) {
            if (fork == null) {
                fork = new Xpp3Dom("fork");
                configuration.addChild(fork);
            }
            fork.setValue("true");
            LOGGER.debug("Set fork to true");
        }
        return fork;
    }

    private void addAnnotationProcessorPath(Xpp3Dom configuration, Dependency dependency, PluginParameterExpressionEvaluator evaluator) {
        Xpp3Dom annotationProcessorPaths = configuration.getChild("annotationProcessorPaths");
        if (annotationProcessorPaths == null) {
            annotationProcessorPaths = new Xpp3Dom("annotationProcessorPaths");
            configuration.addChild(annotationProcessorPaths);
        }
        if (!hasDependency(annotationProcessorPaths.getChildren(), dependency, evaluator)) {
            LOGGER.debug("Adding annotation processor path for dependency {}:{}", dependency.getGroupId(),
                    dependency.getArtifactId());
            Xpp3Dom path = new Xpp3Dom("path");
            annotationProcessorPaths.addChild(path);
            Xpp3Dom groupId = new Xpp3Dom("groupId");
            groupId.setValue(dependency.getGroupId());
            path.addChild(groupId);
            Xpp3Dom artifactId = new Xpp3Dom("artifactId");
            artifactId.setValue(dependency.getArtifactId());
            path.addChild(artifactId);
            if (dependency.getVersion() != null) {
                Xpp3Dom version = new Xpp3Dom("version");
                version.setValue(dependency.getVersion());
                path.addChild(version);
            }
        }
    }

    private boolean isTrue(Xpp3Dom child, PluginParameterExpressionEvaluator evaluator) {
        return Boolean.parseBoolean(getParameterValue(evaluator, child));
    }

    private boolean hasDependency(Xpp3Dom[] paths, Dependency dependency, PluginParameterExpressionEvaluator evaluator) {
        for (Xpp3Dom path : paths) {
            String groupId = getParameterValue(evaluator, path.getChild("groupId"));
            String artifactId = getParameterValue(evaluator, path.getChild("artifactId"));
            if (dependency.getGroupId().equals(groupId) && dependency.getArtifactId().equals(artifactId)) {
                return true;
            }
        }
        return false;
    }

    private boolean hasCompilerArg(Xpp3Dom[] compilerArgs, String arg, PluginParameterExpressionEvaluator evaluator) {
        for (Xpp3Dom compilerArg : compilerArgs) {
            String value = getParameterValue(evaluator, compilerArg);
            if (value != null && value.startsWith(arg)) {
                return true;
            }
        }
        return false;
    }

    private static class StrongEncapsulationHelperJava {
        static final boolean CURRENT_JVM_NEEDS_FORKING = currentJvmNeedsForking();

        private static boolean currentJvmNeedsForking() {
            if (SystemUtils.isJavaVersionAtMost(JavaVersion.JAVA_15)) {
                return false;
            }
            try {
                Module unnamedModule = StrongEncapsulationHelperJava.class.getClassLoader().getUnnamedModule();
                for (String className : new String[] { //
                        "com.sun.tools.javac.api.BasicJavacTask", //
                        "com.sun.tools.javac.api.JavacTrees", //
                        "com.sun.tools.javac.file.JavacFileManager", //
                        "com.sun.tools.javac.main.JavaCompiler", //
                        "com.sun.tools.javac.model.JavacElements", //
                        "com.sun.tools.javac.parser.JavacParser", //
                        "com.sun.tools.javac.processing.JavacProcessingEnvironment", //
                        "com.sun.tools.javac.tree.JCTree", //
                        "com.sun.tools.javac.util.JCDiagnostic", //
                }) {
                    Class<?> clazz = Class.forName(className);
                    if (!clazz.getModule().isExported(clazz.getPackageName(), unnamedModule)) {
                        return true;
                    }
                }
                for (String className : new String[] { //
                        "com.sun.tools.javac.code.Symbol", //
                        "com.sun.tools.javac.comp.Enter", //
                }) {
                    Class<?> clazz = Class.forName(className);
                    if (!clazz.getModule().isOpen(clazz.getPackageName(), unnamedModule)) {
                        return true;
                    }
                }
            } catch (ClassNotFoundException ignored) {
                return true;
            }
            return false;
        }
    }
}