public enum InstrumentationType {

    /**
     * Before:
     * <pre>
     *     public void test(&#064;NotNull String s) {
     *         // body
     *     }
     * </pre>
     * After:
     * <pre>
     *     public void test(&#064;NotNull String s) {
     *         if (s == null) {
     *             throw new NullPointerException("[problem details]");
     *         }
     *         // body
     *     }
     * </pre>
     */
    METHOD_PARAMETER("parameter"),

    /**
     * Before:
     * <pre>
     *     &#064;NotNull
     *     public String compute() {
     *         return doCompute();
     *     }
     * </pre>
     * After:
     * <pre>
     *     &#064;NotNull
     *     public String compute() {
     *         String tmpVar = doCompute();
     *         if (tmpVar == null) {
     *             throw new NullPointerException("[problem details]");
     *         }
     *         return tmpVar;
     *     }
     * </pre>
     */
    METHOD_RETURN("return");

    private static Map<String, InstrumentationType> BY_SHORT_NAME = new HashMap<>();
    static {
        for (InstrumentationType type : values()) {
            BY_SHORT_NAME.put(type.getShortName(), type);
        }
    }

    @NotNull private final String shortName;

    InstrumentationType(@NotNull String shortName) {
        this.shortName = shortName;
    }

    @Nullable
    public static InstrumentationType byShortName(@NotNull String shortName) {
        return BY_SHORT_NAME.get(shortName);
    }

    @NotNull
    public String getShortName() {
        return shortName;
    }
} public class TrautePluginSettings {

    public static final String DEFAULT_EXCEPTION_TO_THROW = NullPointerException.class.getSimpleName();

    private final Set<String>                           notNullAnnotations          = new HashSet<>();
    private final Set<String>                           nullableAnnotations         = new HashSet<>();
    private final Set<InstrumentationType>              instrumentationsToApply     = new HashSet<>();
    private final Map<InstrumentationType, String>      exceptionsToThrow           = new HashMap<>();
    private final Map<InstrumentationType, String>      exceptionTextPatterns       = new HashMap<>();
    private final Map<InstrumentationType, Set<String>> notNullByDefaultAnnotations = new HashMap<>();

    @Nullable private final File logFile;

    private final boolean verboseMode;

    public TrautePluginSettings(@NotNull Set<String> notNullAnnotations,
                                @NotNull Set<String> nullableAnnotations,
                                @NotNull Set<InstrumentationType> instrumentationsToApply,
                                @NotNull Map<InstrumentationType, String> exceptionsToThrow,
                                @NotNull Map<InstrumentationType, String> exceptionTextPatterns,
                                @NotNull Map<InstrumentationType, Set<String>> notNullByDefaultAnnotations,
                                @Nullable File logFile,
                                boolean verboseMode)
    {
        this.logFile = logFile;
        this.notNullAnnotations.addAll(notNullAnnotations);
        this.nullableAnnotations.addAll(nullableAnnotations);
        this.instrumentationsToApply.addAll(instrumentationsToApply);
        this.exceptionsToThrow.putAll(exceptionsToThrow);
        this.exceptionTextPatterns.putAll(exceptionTextPatterns);
        this.notNullByDefaultAnnotations.putAll(notNullByDefaultAnnotations);
        this.verboseMode = verboseMode;
    }

    @NotNull
    public Set<String> getNotNullAnnotations() {
        return notNullAnnotations;
    }

    @NotNull
    public Set<String> getNullableAnnotations() {
        return nullableAnnotations;
    }

    public boolean isEnabled(@NotNull InstrumentationType type) {
        return instrumentationsToApply.contains(type);
    }

    @NotNull
    public Set<InstrumentationType> getInstrumentationsToApply() {
        return instrumentationsToApply;
    }

    @NotNull
    public String getExceptionToThrow(@NotNull InstrumentationType type) {
        return exceptionsToThrow.getOrDefault(type, DEFAULT_EXCEPTION_TO_THROW);
    }

    @NotNull
    public Map<InstrumentationType, String> getExceptionsToThrow() {
        return exceptionsToThrow;
    }

    @Nullable
    public String getExceptionTextPattern(@NotNull InstrumentationType type) {
        return exceptionTextPatterns.get(type);
    }

    @NotNull
    public Map<InstrumentationType, String> getExceptionTextPatterns() {
        return exceptionTextPatterns;
    }

    @NotNull
    public Set<String> getNotNullByDefaultAnnotations(@NotNull InstrumentationType type) {
        return notNullByDefaultAnnotations.get(type);
    }

    @NotNull
    public Map<InstrumentationType, Set<String>> getNotNullByDefaultAnnotations() {
        return notNullByDefaultAnnotations;
    }

    @NotNull
    public Optional<File> getLogFile() {
        return Optional.ofNullable(logFile);
    }

    public boolean isVerboseMode() {
        return verboseMode;
    }
} public interface TestCompiler {

    /**
     * Produces a {@link CompilationResult} from the given {@link TestSource}
     *
     * @param testSources    test sources to compile
     * @param settings       plugin settings to use
     * @return               compilation result
     */
    @NotNull
    CompilationResult compile(@NotNull Collection<TestSource> testSources, @NotNull TrautePluginSettings settings);

    /**
     * Behaves like {@link #compile(Collection, TrautePluginSettings)} but additionally checks
     * resulting {@link CompilationResult} against the given {@link Expectation}
     *
     * @param testSources    test sources to compile
     * @param settings       plugin settings to use
     * @param expectation    a compilation result's expectation
     * @return               compilation result for the given test sources
     */
    @NotNull
    default CompilationResult compile(@NotNull Collection<TestSource> testSources,
                                      @NotNull TrautePluginSettings settings,
                                      @NotNull Expectation<CompilationResult> expectation)
    {
        CompilationResult result = compile(testSources, settings);
        expectation.match(result);
        return result;
    }

    /**
     * @param result    notifies that given compilation result is not needed anymore and all resources associated
     *                  with it can be released
     */
    void release(@NotNull CompilationResult result);
} public interface ClassFile {

    @NotNull String getName();

    @NotNull byte[] getBinaries();
} public interface CompilationResult extends Result<Collection<TestSource>> {

    /**
     * @return      compiled classes. We use a {@link Supplier} here because there is a possible case that
     *              compilation is failed and the test wants to check its {@code stderr}. We return a collection
     *              of {@link ClassFile} because a single source file might result in multiple binaries after
     *              compilation, e.g. when an inner/anonymous classes are involved
     */
    @NotNull
    Supplier<Collection<ClassFile>> getCompiledClassesSupplier();

    /**
     * @return      compiler's output generated during processing {@link #getInput() target binaries}
     */
    @NotNull
    String getCompilationOutput();
} public interface TestSource {

    /**
     * @return  complete test source
     */
    @NotNull
    String getSourceText();

    /**
     * @return  qualified class name for the given test source
     */
    @NotNull
    String getQualifiedClassName();
} public class ClassFileImpl implements ClassFile {

    @NotNull private final String name;
    @NotNull private final byte[] binaries;

    public ClassFileImpl(@NotNull String name, @NotNull byte[] binaries) {
        this.name = name;
        this.binaries = binaries;
    }

    @NotNull
    public String getName() {
        return name;
    }

    @NotNull
    public byte[] getBinaries() {
        return binaries;
    }

    @Override
    public String toString() {
        return getName();
    }
} public class CompilationResultImpl implements CompilationResult {

    private final Map<String, String>    additionalInfo = new HashMap<>();
    private final Collection<TestSource> input          = new ArrayList<>();

    @NotNull private final Supplier<Collection<ClassFile>> compiledClassesSupplier;
    @NotNull private final String compilationOutput;


    public CompilationResultImpl(@NotNull Supplier<Collection<ClassFile>> compiledClassesSupplier,
                                 @NotNull String compilationOutput,
                                 @NotNull Collection<TestSource> input)
    {
        this(compiledClassesSupplier, compilationOutput, input, emptyMap());
    }

    public CompilationResultImpl(@NotNull Supplier<Collection<ClassFile>> compiledClassesSupplier,
                                 @NotNull String compilationOutput,
                                 @NotNull Collection<TestSource> input,
                                 @NotNull Map<String, String> additionalInfo)
    {
        this.compiledClassesSupplier = compiledClassesSupplier;
        this.compilationOutput = compilationOutput;
        this.input.addAll(input);
        this.additionalInfo.putAll(additionalInfo);
    }

    @Override
    @NotNull
    public Supplier<Collection<ClassFile>> getCompiledClassesSupplier() {
        return compiledClassesSupplier;
    }

    @Override
    @NotNull
    public String getCompilationOutput() {
        return compilationOutput;
    }

    @NotNull
    @Override
    public Collection<TestSource> getInput() {
        return input;
    }

    @Override
    public String toString() {
        StringBuilder buffer = new StringBuilder();
        buffer.append("Test sources:\n\n").append(TestUtil.getSources(getInput())).append("\n\n");
        buffer.append("Compile output:\n").append(getCompilationOutput());

        for (Map.Entry<String, String> entry : additionalInfo.entrySet()) {
            buffer.append("\n\n").append(entry.getKey()).append(":\n").append(entry.getValue());
        }
        return buffer.toString();
    }
} @SuppressWarnings("UnusedReturnValue")
public class TrautePluginSettingsBuilder {

    public static final Set<String> DEFAULT_NOT_NULL_ANNOTATIONS = new HashSet<>(asList(
            // Used by IntelliJ by default - https://www.jetbrains.com/help/idea/nullable-and-notnull-annotations.html
            "org.jetbrains.annotations.NotNull",

            // JSR-305 - status=dormant - https://jcp.org/en/jsr/detail?id=305
            "javax.annotation.Nonnull",

            // JavaEE - https://docs.oracle.com/javaee/7/api/javax/validation/constraints/NotNull.html
            "javax.validation.constraints.NotNull",

            // FindBugs - http://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/NonNull.html
            "edu.umd.cs.findbugs.annotations.NonNull",

            // Android - https://developer.android.com/reference/android/support/annotation/NonNull.html
            "android.support.annotation.NonNull",

            // Eclipse - http://help.eclipse.org/oxygen/index.jsp?topic=%2Forg.eclipse.jdt.doc.user%2Ftasks%2Ftask-using_null_annotations.htm
            "org.eclipse.jdt.annotation.NonNull",

            // Lombok - https://projectlombok.org/api/lombok/NonNull.html
            "lombok.NonNull",

            // Spring - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/lang/NonNull.html
            "org.springframework.lang.NonNull",

            // Checker - https://checkerframework.org/api/org/checkerframework/checker/nullness/qual/NonNull.html
            "org.checkerframework.checker.nullness.qual.NonNull",

            // Checker - https://checkerframework.org/api/org/checkerframework/checker/nullness/compatqual/NonNullDecl.html
            "org.checkerframework.checker.nullness.compatqual.NonNullDecl",

            // Checker - https://checkerframework.org/api/org/checkerframework/checker/nullness/compatqual/NonNullType.html
            "org.checkerframework.checker.nullness.compatqual.NonNullType"
    ));

    /**
     * It's possible to specify that method parameters should be treated as {@code not-null} by default.
     * Current collection holds default annotations to use.
     */
    public static final Set<String> DEFAULT_PARAMETERS_NOT_NULL_BY_DEFAULT_ANNOTATIONS = new HashSet<>(asList(
            // Eclipse - https://help.eclipse.org/mars/index.jsp?topic=%2Forg.eclipse.jdt.doc.isv%2Freference%2Fapi%2Forg%2Feclipse%2Fjdt%2Fannotation%2FNonNullByDefault.html
            "org.eclipse.jdt.annotation.NonNullByDefault",

            // JSR-305 - https://static.javadoc.io/com.google.code.findbugs/jsr305/3.0.1/javax/annotation/ParametersAreNonnullByDefault.html
            "javax.annotation.ParametersAreNonnullByDefault",

            // Spring - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/lang/NonNullApi.html
            "org.springframework.lang.NonNullApi"
    ));

    public static final Set<String> DEFAULT_RETURN_NOT_NULL_BY_DEFAULT_ANNOTATIONS = new HashSet<>(
            // Spring - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/lang/NonNullApi.html
            singleton("org.springframework.lang.NonNullApi")
    );

    /**
     * This annotations work in conjunction with {@link #DEFAULT_PARAMETERS_NOT_NULL_BY_DEFAULT_ANNOTATIONS}
     * annotations, e.g. particular package/class/method might be marked by, say,
     * {@code javax.annotation.ParametersAreNonnullByDefault} and we want to specify that particular method parameter
     * might be {@code null}.
     */
    public static final Set<String> DEFAULT_NULLABLE_ANNOTATIONS = new HashSet<>(asList(
            // IntelliJ - https://www.jetbrains.com/help/idea/nullable-and-notnull-annotations.html#nullable
            "org.jetbrains.annotations.Nullable",

            // JSR-305 - status=dormant - https://jcp.org/en/jsr/detail?id=305
            "javax.annotation.Nullable",
            "javax.annotation.CheckForNull",

            // JavaEE - https://docs.oracle.com/javaee/7/api/javax/validation/constraints/Null.html
            "javax.validation.constraints.Null",

            // FindBugs - http://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/Nullable.html
            "edu.umd.cs.findbugs.annotations.Nullable",

            // FindBugs - http://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/CheckForNull.html
            "edu.umd.cs.findbugs.annotations.CheckForNull",

            // Android - https://developer.android.com/reference/android/support/annotation/Nullable.html
            "android.support.annotation.Nullable",

            // Eclipse - http://help.eclipse.org/oxygen/index.jsp?topic=%2Forg.eclipse.jdt.doc.user%2Ftasks%2Ftask-using_null_annotations.htm
            "org.eclipse.jdt.annotation.Nullable",

            // Spring - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/lang/Nullable.html
            "org.springframework.lang.Nullable",

            // Checker - https://checkerframework.org/api/org/checkerframework/checker/nullness/qual/Nullable.html
            "org.checkerframework.checker.nullness.qual.Nullable",

            // Checker - https://checkerframework.org/api/org/checkerframework/checker/nullness/compatqual/NullableDecl.html
            "org.checkerframework.checker.nullness.compatqual.NullableDecl",

            // Checker - https://checkerframework.org/api/org/checkerframework/checker/nullness/compatqual/NullableType.html
            "org.checkerframework.checker.nullness.compatqual.NullableType"
    ));

    public static final Set<InstrumentationType> DEFAULT_INSTRUMENTATIONS_TO_APPLY =
            EnumSet.allOf(InstrumentationType.class);

    public static final boolean DEFAULT_VERBOSE_MODE = false;

    private final Set<String>              notNullAnnotations      = new HashSet<>();
    private final Set<String>              nullableAnnotations     = new HashSet<>();
    private final Set<InstrumentationType> instrumentationsToApply = EnumSet.noneOf(InstrumentationType.class);

    private final Map<InstrumentationType, String>      exceptionsToThrow           = new HashMap<>();
    private final Map<InstrumentationType, String>      exceptionTextPatterns       = new HashMap<>();
    private final Map<InstrumentationType, Set<String>> notNullByDefaultAnnotations = new HashMap<>();

    @Nullable private File    logFile;
    @Nullable private Boolean verbose;

    @NotNull
    public static TrautePluginSettingsBuilder settingsBuilder() {
        return new TrautePluginSettingsBuilder();
    }

    @NotNull
    public TrautePluginSettingsBuilder withNotNullAnnotations(@NotNull String... notNullAnnotations) {
        this.notNullAnnotations.addAll(Arrays.asList(notNullAnnotations));
        return this;
    }

    @NotNull
    public TrautePluginSettingsBuilder withNullableAnnotations(@NotNull String... nullableAnnotations) {
        this.nullableAnnotations.addAll(Arrays.asList(nullableAnnotations));
        return this;
    }

    @NotNull
    public TrautePluginSettingsBuilder withInstrumentationToApply(@NotNull InstrumentationType type) {
        instrumentationsToApply.add(type);
        return this;
    }

    @NotNull
    public TrautePluginSettingsBuilder withExceptionToThrow(@NotNull InstrumentationType type,
                                                            @NotNull String exceptionToThrow)
    {
        exceptionsToThrow.put(type, exceptionToThrow);
        return this;
    }

    @NotNull
    public TrautePluginSettingsBuilder withExceptionTextPattern(@NotNull InstrumentationType type,
                                                                @NotNull String pattern)
    {
        exceptionTextPatterns.put(type, pattern);
        return this;
    }

    @NotNull
    public TrautePluginSettingsBuilder withNotNullByDefaultAnnotations(@NotNull InstrumentationType type,
                                                                       @NotNull Collection<String> annotations)
    {
        notNullByDefaultAnnotations.put(type, new HashSet<>(annotations));
        return this;
    }

    @NotNull
    public TrautePluginSettingsBuilder withLogFile(@NotNull File file) {
        logFile = file;
        return this;
    }

    @NotNull
    public TrautePluginSettingsBuilder withVerboseMode(boolean verbose) {
        this.verbose = verbose;
        return this;
    }

    @NotNull
    public TrautePluginSettings build() {
        Set<String> notNullAnnotations = new HashSet<>(this.notNullAnnotations);
        if (notNullAnnotations.isEmpty()) {
            notNullAnnotations.addAll(DEFAULT_NOT_NULL_ANNOTATIONS);
        }

        Set<String> nullableAnnotations = new HashSet<>(this.nullableAnnotations);
        if (nullableAnnotations.isEmpty()) {
            nullableAnnotations.addAll(DEFAULT_NULLABLE_ANNOTATIONS);
        }

        Map<InstrumentationType, Set<String>> notNullByDefaultAnnotations
                = new HashMap<>(this.notNullByDefaultAnnotations);
        if (!notNullByDefaultAnnotations.containsKey(InstrumentationType.METHOD_PARAMETER)) {
            notNullByDefaultAnnotations.put(InstrumentationType.METHOD_PARAMETER,
                                            DEFAULT_PARAMETERS_NOT_NULL_BY_DEFAULT_ANNOTATIONS);
        }
        if (!notNullByDefaultAnnotations.containsKey(InstrumentationType.METHOD_RETURN)) {
            notNullByDefaultAnnotations.put(InstrumentationType.METHOD_RETURN,
                                            DEFAULT_RETURN_NOT_NULL_BY_DEFAULT_ANNOTATIONS);
        }

        Set<InstrumentationType> instrumentationsToApply = EnumSet.copyOf(this.instrumentationsToApply);
        if (instrumentationsToApply.isEmpty()) {
            instrumentationsToApply.addAll(DEFAULT_INSTRUMENTATIONS_TO_APPLY);
        }

        Boolean verbose = this.verbose;
        if (verbose == null) {
            verbose = DEFAULT_VERBOSE_MODE;
        }
        return new TrautePluginSettings(notNullAnnotations,
                                        nullableAnnotations,
                                        instrumentationsToApply,
                                        exceptionsToThrow,
                                        exceptionTextPatterns,
                                        notNullByDefaultAnnotations,
                                        logFile,
                                        verbose);
    }
} public class TrauteConstants {

    public static final String PLUGIN_NAME = "Traute";

    public static final Set<String> PRIMITIVE_TYPES = Collections.unmodifiableSet(new HashSet<>(asList(
            "boolean", "byte", "short", "char", "int", "long", "float", "double"
    )));

    public static final Set<String> METHOD_RETURN_TYPES_TO_SKIP;

    /**
     * <p>
     *     Compiler's option name to use for specifying custom {@code @NotNull} annotations to use
     *     ({@value #SEPARATOR}-separated).
     * </p>
     * <p>
     *     This is not mandatory setting as default annotations are used otherwise. Only given annotations are
     *     checked if this argument is specified.
     * </p>
     * <p>
     *     Example: consider a situation when given parameter's value is
     *     {@code 'org.eclipse.jdt.annotation.NonNull:android.support.annotation.NonNull'} (eclipse and android
     *     annotations, defined as
     *     {@code -Atraute.annotations.not.null=org.eclipse.jdt.annotation.NonNull:android.support.annotation.NonNull}
     *     in the {@code javac} command line). That means that a method which parameter is marked by, say
     *     {@code org.jetbrains.annotations.NotNull} won't trigger {@code null}-check generation by the plugin.
     * </p>
     */
    public static final String OPTION_ANNOTATIONS_NOT_NULL = "traute.annotations.not.null";

    /**
     * <p>
     *     Compiler's option name to use for specifying custom {@code @Nullable} annotations to use
     *     ({@value #SEPARATOR}-separated).
     * </p>
     * <p>
     *     This is not mandatory setting as default annotations are used otherwise. Only given annotations are
     *     checked if this argument is specified.
     * </p>
     * <p>
     *     Example: consider a situation when given parameter's value is
     *     {@code 'org.eclipse.jdt.annotation.Nullable:android.support.annotation.Nullable'} (eclipse and android
     *     annotations, defined as
     *     {@code -Atraute.annotations.nullable=org.eclipse.jdt.annotation.Nullable:android.support.annotation.Nullable}
     *     in the {@code javac} command line). That means that a method which parameter is marked by, say
     *     {@code org.jetbrains.annotations.Nullable} won't be considered as {@code nullable} by the plugin.
     * </p>
     */
    public static final String OPTION_ANNOTATIONS_NULLABLE = "traute.annotations.nullable";

    /**
     * <p>
     *     Prefix for compiler's option prefix for specifying a set of {@code not null by default} annotations
     *     (separated by {@value #SEPARATOR}). Resulting option is constructed from the current prefix
     *     and {@link InstrumentationType#getShortName()}.
     * </p>
     * <p>
     *     E.g. {@code -Atraute.not.null.by.default.parameter=javax.annotation.ParametersAreNonnullByDefault}
     *     instructs the plugin to consider all method parameters not marked as {@link #OPTION_ANNOTATIONS_NULLABLE}
     *     to be treated as {@code not-null} for a package where {@code package-info.java} is marked by the
     *     {@code ParametersAreNonnullByDefault} annotation.
     * </p>
     */
    public static final String OPTION_PREFIX_ANNOTATIONS_NOT_NULL_BY_DEFAULT = "traute.annotations.not.null.by.default.";

    /**
     * <p>
     *     Compiler's option name to use for specifying if the plugin should produce verbose output
     *     for its processing
     * </p>
     * <p>
     *     Verbose output is produced if this option's value is {@code 'true'}, e.g.
     *     {@code '-Atraute.log.verbose=true'} in the {@code javac} command line. Any other value (or now value)
     *     mean that no verbose output should be provided.
     * </p>
     */
    public static final String OPTION_LOG_VERBOSE = "traute.log.verbose";

    /**
     * <p>Compiler's option name for specifying a path to a file to store plugin's logs.</p>
     * <p>The logs are printed to the standard compiler's output by default.</p>
     */
    public static final String OPTION_LOG_FILE = "traute.log.file";

    /**
     * Compiler's option name to use for specifying instrumentation types to use
     *
     * @see InstrumentationType
     * @see InstrumentationType#getShortName()
     */
    public static final String OPTION_INSTRUMENTATIONS_TO_USE = "traute.instrumentations";

    /**
     * <p>
     *     Prefix for compiler's option prefix for specifying an exception to throw on failed
     *     {@code null}-check. Resulting option is constructed from the current prefix and
     *     {@link InstrumentationType#getShortName()}.
     * </p>
     * <p>
     *     E.g. {@code -Atraute.exception.parameter=IllegalArgumentException} instructs the plugin
     *     to generated a parameter check where an {@link IllegalArgumentException} is thrown in case
     *     of failure.
     * </p>
     */
    public static final String OPTION_PREFIX_EXCEPTION_TO_THROW = "traute.exception.";

    /**
     * <p>
     *     Prefix for compiler's option prefix for specifying a text to use in an exception thrown from
     *     a failed {@code null}-check. Resulting option is constructed from the current prefix and
     *     {@link InstrumentationType#getShortName()}.
     * </p>
     * <p>
     *     E.g. {@code "-Atraute.failure.text.parameter=${PARAMETER_NAME} must not be null"} instructs the plugin
     *     to generated a check for a parameter named, say, {@code 'arg'} with exception text
     *     {@code "arg must not be null"}.
     * </p>
     */
    public static final String OPTION_PREFIX_EXCEPTION_TEXT = "traute.failure.text.";

    /**
     * This text is replaced by the actual parameter name in the
     * {@link InstrumentationType#METHOD_PARAMETER parametere check}.
     */
    public static final String VARIABLE_PARAMETER_NAME = "PARAMETER_NAME";

    /**
     * This function capitalizes the variable inside it, e.g. {@code ${capitalize(PARAMETER_NAME)}} for
     * parameter {@code 'arg'} produced {@code 'Arg'}.
     */
    public static final String FUNCTION_CAPITALIZE = "capitalize";

    /**
     * Separator to use for composite properties, e.g. when a user want to specify more than one
     * {@code NonNull} annotation.
     */
    public static final String SEPARATOR = ":";

    public static final String PACKAGE_INFO = "package-info";

    static {
        Set<String> set = new HashSet<>(PRIMITIVE_TYPES);
        set.add("void");
        set.add("Void");
        METHOD_RETURN_TYPES_TO_SKIP = set;
    }

    private TrauteConstants() {
    }
}
import org.jetbrains.annotations.NotNull;
import tech.harmonysoft.oss.traute.common.instrumentation.InstrumentationType;
import tech.harmonysoft.oss.traute.common.settings.TrautePluginSettings;
import tech.harmonysoft.oss.traute.test.api.engine.TestCompiler;
import tech.harmonysoft.oss.traute.test.api.model.ClassFile;
import tech.harmonysoft.oss.traute.test.api.model.CompilationResult;
import tech.harmonysoft.oss.traute.test.api.model.TestSource;
import tech.harmonysoft.oss.traute.test.impl.model.ClassFileImpl;
import tech.harmonysoft.oss.traute.test.impl.model.CompilationResultImpl;
import java.io.*;
import java.nio.file.Files;
import java.util.*;
import static java.util.stream.Collectors.joining;
import static tech.harmonysoft.oss.traute.common.instrumentation.InstrumentationType.METHOD_PARAMETER;
import static tech.harmonysoft.oss.traute.common.instrumentation.InstrumentationType.METHOD_RETURN;
import static tech.harmonysoft.oss.traute.common.settings.TrautePluginSettingsBuilder.*;
import static tech.harmonysoft.oss.traute.common.util.TrauteConstants.*;
package tech.harmonysoft.oss.traute.test.impl.engine;




public abstract class AbstractExternalSystemTestCompiler implements TestCompiler {

    private final Map<CompilationResult, File> projectDirs = new WeakHashMap<>();

    @Override
    @NotNull
    public CompilationResult compile(@NotNull Collection<TestSource> testSources,
                                     @NotNull TrautePluginSettings settings)
    {
        try {
            return doCompile(testSources, settings);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @NotNull
    private CompilationResult doCompile(@NotNull Collection<TestSource> testSources,
                                        @NotNull TrautePluginSettings settings)
            throws Exception
    {
        File projectRootDir = createRootDir();
        File externalSystemConfig = createExternalSystemConfig(projectRootDir, settings);
        File sourceRoot = createSourceRootDir(projectRootDir, getRelativeSrcPath());
        for (TestSource testSource : testSources) {
            File sourceFile = createFile(testSource.getQualifiedClassName(), sourceRoot);
            write(sourceFile, testSource.getSourceText());
        }

        String output = compile(projectRootDir);
        CompilationResultImpl result = new CompilationResultImpl(
                () -> findBinaries(projectRootDir, getRelativeBinariesPath()),
                output,
                testSources,
                Collections.singletonMap(externalSystemConfig.getName(), new String(read(externalSystemConfig)))
        );
        projectDirs.put(result, projectRootDir);
        return result;
    }

    @NotNull
    protected abstract File createExternalSystemConfig(@NotNull File projectRootDir,
                                                       @NotNull TrautePluginSettings settings)
            throws IOException;

    @NotNull
    protected abstract String getRelativeSrcPath();

    @NotNull
    protected abstract String compile(@NotNull File projectRootDir) throws Exception;

    @NotNull
    protected abstract String getRelativeBinariesPath();

    @Override
    public void release(@NotNull CompilationResult result) {
        File rootDir = projectDirs.get(result);
        if (rootDir.isDirectory()) {
            delete(rootDir);
        }
    }

    @NotNull
    private static File createRootDir() throws IOException {
        File result = Files.createTempDirectory("maven-traute").toFile();
        if (!result.isDirectory()) {
            throw new IllegalStateException("Can't create a root directory for a test project at "
                                            + result.getAbsolutePath());
        }
        return result;
    }

    @NotNull
    private static File createSourceRootDir(@NotNull File projectRootDir, @NotNull String relativeSrcPath) {
        File result = new File(projectRootDir, relativeSrcPath);
        boolean created = result.mkdirs();
        if (!created) {
            throw new IllegalStateException("Can't create a source root directory for a test project at "
                                            + result.getAbsolutePath());
        }
        return result;
    }

    @NotNull
    private static File createFile(@NotNull String qualifiedClassName, @NotNull File sourceRootDir) {
        int i = qualifiedClassName.lastIndexOf('.');
        if (i <= 0) {
            return new File(sourceRootDir, qualifiedClassName + ".java");
        } else {
            File dir = new File(sourceRootDir, qualifiedClassName.substring(0, i)
                                                                 .replace('.', '/'));
            if (!dir.exists()) {
                boolean created = dir.mkdirs();
                if (!created) {
                    throw new IllegalStateException(String.format("Can't create a directory for the source class %s at %s",
                                                                  qualifiedClassName, dir.getAbsolutePath()));
                }
            }
            return new File(dir, qualifiedClassName.substring(i + 1) + ".java");
        }
    }

    protected static void write(@NotNull File file, @NotNull String content) throws IOException {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
            writer.write(content);
        }
    }

    @NotNull
    public static byte[] read(@NotNull File file) throws IOException {
        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int read;
        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file))) {
            while ((read = in.read(buffer)) >= 0) {
                bOut.write(buffer, 0, read);
            }
        }
        return bOut.toByteArray();
    }

    @NotNull
    private static Collection<ClassFile> findBinaries(@NotNull File projectRoot, @NotNull String relativeBinariesPath) {