import de.undercouch.gradle.tasks.download.Download

plugins {
    // https://plugins.gradle.org/plugin/com.github.johnrengelman.shadow (v5 requires Gradle 5)
    id 'com.github.johnrengelman.shadow' version '5.2.0'
    // https://plugins.gradle.org/plugin/de.undercouch.download
    id "de.undercouch.download" version "4.0.2"
    id 'java'
    // https://github.com/tbroyer/gradle-errorprone-plugin
    id "net.ltgt.errorprone" version "1.1.1"
    // https://plugins.gradle.org/plugin/org.ajoberstar.grgit
    id 'org.ajoberstar.grgit' version '4.0.0' apply false
}
apply plugin: "de.undercouch.download"

import org.ajoberstar.grgit.Grgit

repositories {
    jcenter()
    mavenCentral()
}

ext {
    release = false

    // On a Java 8 JVM, use error-prone javac and source/target 8.
    // On a Java 9+ JVM, use the host javac, default source/target, and required module flags.
    isJava8 = JavaVersion.current() == JavaVersion.VERSION_1_8

    errorproneJavacVersion = '9+181-r4173-1'

    parentDir = file("${rootDir}/../").absolutePath

    annotationTools = "${parentDir}/annotation-tools"
    afu = "${annotationTools}/annotation-file-utilities"
    afuJar = "${afu}/annotation-file-utilities-all-but-javac.jar"

    stubparser = "${parentDir}/stubparser"
    stubparserJar = "${stubparser}/javaparser-core/target/stubparser.jar"

    jtregHome = "${parentDir}/jtreg"
    formatScriptsHome = "${project(':checker').projectDir}/bin-devel/.run-google-java-format"
    plumeScriptsHome = "${project(':checker').projectDir}/bin-devel/.plume-scripts"

    javadocMemberLevel = JavadocMemberLevel.PROTECTED

    // The local git repository, typically in the .git directory, but not for worktrees.
    // This value is always overwritten, but Gradle needs the variable to be initialized.
    localRepo = ".git"
}

task setLocalRepo(type:Exec) {
    commandLine 'git', 'worktree', 'list'
    standardOutput = new ByteArrayOutputStream()
    doLast {
       String worktreeList = standardOutput.toString()
       localRepo = worktreeList.substring(0, worktreeList.indexOf(" ")) + "/.git"
    }
}

task installGitHooks(type: Copy, dependsOn: 'setLocalRepo') {
    description 'Copies git hooks to .git directory'
    from files("checker/bin-devel/git.post-merge", "checker/bin-devel/git.pre-commit")
    rename('git\\.(.*)', '$1')
    into localRepo + "/hooks"
}

allprojects {
    apply plugin: 'java'
    apply plugin: 'com.github.johnrengelman.shadow'
    apply plugin: "de.undercouch.download"
    apply plugin: 'net.ltgt.errorprone'

    group 'org.checkerframework'
    // Increment the minor version rather than just the patch level if:
    //   * any new checkers have been added,
    //   * the patch level is 9 (keep the patch level as a single digit), or
    //   * backward-incompatible changes have been made to APIs or elsewhere.
    version '3.0.1'

    repositories {
        mavenCentral()
    }

    configurations {
        javacJar

        // There's a bug in IntelliJ that adds the processor path to the classpath when building
        // a Gradle project.  Because ErrorProne uses old versions of some of our jars, IntelliJ uses
        // those jars instead of the source code. The next 3 lines exclude Checker Framework from the
        // from the processor path as a work around for this problem.
        annotationProcessor.exclude group:'org.checkerframework', module:'javacutil'
        annotationProcessor.exclude group:'org.checkerframework', module:'dataflow'
        annotationProcessor.exclude group:'org.checkerframework', module:'checker-qual'
    }

    dependencies {
        if (isJava8) {
            javacJar group: 'com.google.errorprone', name: 'javac', version: "$errorproneJavacVersion"
        }

        errorproneJavac("com.google.errorprone:javac:$errorproneJavacVersion")
    }

    // After all the tasks have been created, modify some of them.
    afterEvaluate {
        // Add the fat checker.jar to the classpath of every Javadoc task. This allows Javadoc in
        // any module to reference classes in any other module.
        // Also, build and use ManualTaglet as a taglet.
        tasks.withType(Javadoc) {
            def tagletVersion = isJava8 ? 'taglet' : 'tagletJdk11'

            dependsOn(':checker:shadowJar')
            dependsOn(":framework-test:${tagletVersion}Classes")
            doFirst {
                options.encoding = 'UTF-8'
                if (!name.startsWith('requireJavadoc') && !name.equals("javadocDoclintAll")) {
                    options.memberLevel = javadocMemberLevel
                }
                classpath += files(project(':checker').tasks.getByName('shadowJar').archivePath)
                if (isJava8) {
                    classpath += configurations.javacJar
                }
                options.taglets 'org.checkerframework.taglet.ManualTaglet'
                options.tagletPath(project(':framework-test').sourceSets."${tagletVersion}".output.classesDirs.getFiles() as File[])

                // We want to link to Java 9 documentation of the compiler classes since we use Java 9
                // versions of those classes and Java 8 for everything else.  Because the compiler classes are not
                // apart of the main documentation of Java 8, javadoc links to the Java 9 versions.
                options.links = ['https://docs.oracle.com/javase/8/docs/api/', 'https://docs.oracle.com/javase/9/docs/api/']
                // This file is looked for by Javadoc.
                file("${destinationDir}/resources/fonts/").mkdirs()
                ant.touch(file: "${destinationDir}/resources/fonts/dejavu.css")
                options.addStringOption('source', '8')
            }
        }

        /// TODO: One day we can enable this.  For now, CI jobs require Javadoc on changed lines.
        // // Turn Javadoc warnings into errors.
        // tasks.withType(Javadoc) {
        //     options.addStringOption('Xwerror', '-quiet')
        // }

        // Add standard javac options
        tasks.withType(JavaCompile) {
            dependsOn(':installGitHooks')
            sourceCompatibility = 8
            targetCompatibility = 8
            // Because the target is 8, all of the public compiler classes are accessible, so
            // --add-exports are not required, (nor are they allowed with target 8). See
            // https://openjdk.java.net/jeps/247 for details on compiling for older versions.

            // When sourceCompatibilty is changed to 11, then the following will be required.
            // options.compilerArgs += [
            // "--add-exports", "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
            // "--add-exports", "jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
            // "--add-exports", "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
            // "--add-exports", "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
            // "--add-exports", "jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
            // "--add-exports", "jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED",
            // "--add-exports", "jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
            // "--add-exports", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
            // "--add-exports", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
            // ]
            // This is equivalent to writing "exports jdk.compiler/... to ALL-UNNAMED" in the
            // module-info.java of jdk.compiler, so corresponding --add-opens are only required for
            // reflective access to private members.
            //
            // From https://openjdk.java.net/jeps/261, Section titled: "Breaking encapsulation"
            // "The effect of each instance [of --add-exports] is to add a qualified export of the
            // named package from the source module to the target module. This is, essentially, a
            // command-line form of an exports clause in a module declaration[...].
            // [...]
            // The --add-exports option enables access to the public types of a specified package.
            // It is sometimes necessary to go further and enable access to all non-public elements
            // via the setAccessible method of the core reflection API. The --add-opens option can
            // be used, at run time, to do this."

            options.failOnError = true
            options.deprecation = true
            options.compilerArgs += [
                    '-g',
                    '-Werror',
                    // To not get a warning about missing bootstrap classpath for Java 8 (once we use Java 9).
                    "-Xlint:-options",
                    "-Xlint",
            ]

            options.encoding = 'UTF-8'
            options.fork = true
            if (isJava8) {
                options.forkOptions.jvmArgs += ["-Xbootclasspath/p:${configurations.javacJar.asPath}"]
            }
            // Don't use error-prone by default
            options.errorprone.enabled = false
        }
    }
}

task cloneAndBuildDependencies(type: Exec) {
    description 'Clones (or updates) and builds all dependencies'
    executable 'checker/bin-devel/build.sh'
    args = ['downloadjdk']
}

task version() {
    description 'Print Checker Framework version'
    doLast {
        println version
    }
}

/**
 * Creates a task that runs the checker on the main source set of each subproject. The task is named
 * "check${shortName}", for example "checkPurity" or "checkNullness".
 * @param projectName name of the project
 * @param checker full qualified name of the checker to run
 * @param shortName shorter version of the checker to use to name the task.
 * @param args list of arguments to pass to the checker
 */
def createCheckTypeTask(projectName, checker, shortName, args = []) {
    project("${projectName}").tasks.create(name: "check${shortName}", type: JavaCompile, dependsOn: ':checker:shadowJar') {
        description "Run the ${shortName} Checker on the main sources."
        group 'Verification'
        dependsOn ':checker:updateJdk'
        // Always run the task.
        outputs.upToDateWhen { false }
        source = project("${projectName}").sourceSets.main.java
        classpath = files(project("${projectName}").compileJava.classpath,project(':checker-qual').sourceSets.main.output)
        destinationDir = file("${buildDir}")

        options.annotationProcessorPath = files(project(':checker').tasks.shadowJar.archivePath)
        options.compilerArgs += [
                '-processor', "${checker}",
                '-proc:only',
                '-Xlint:-processing',
                ]
        options.compilerArgs += args

        if (isJava8) {
            options.compilerArgs += [
                "-Xbootclasspath/p:${rootDir}/checker/dist/jdk8.jar",
                "-source",
                "8",
                "-target",
                "8"
                ]
        } else {
            options.fork = true
            options.forkOptions.jvmArgs += [
                "--add-opens", "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
                ]
       }
    }
}

/**
 * Returns a list of all the Java files that should be formatted for the given project. These are:
 *
 * All java files in the main sourceSet.
 * All java files in the tests directory that compile.
 *
 * @param projectName name of the project to format
 * @return a list of all Java files that should be formatted for projectName
 */
List<String> getJavaFilesToFormat(projectName) {
    List<File> javaFiles = new ArrayList<>();
    project(':' + projectName).sourceSets.forEach { set ->
        javaFiles.addAll(set.java.files)
    }
    // Collect all java files in tests directory
    fileTree("${project(projectName).projectDir}/tests").visit { details ->
        if (!details.path.contains("nullness-javac-errors") && details.name.endsWith('java')) {
            javaFiles.add(details.file)
        }
    }
     // Collect all java files in jtreg directory
    fileTree("${project(projectName).projectDir}/jtreg").visit { details ->
        if (!details.path.contains("nullness-javac-errors") && details.name.endsWith('java')) {
            javaFiles.add(details.file)
        }
    }

    // Collect all java files in jtreg directory
    fileTree("${project(projectName).projectDir}/jtregJdk11").visit { details ->
        if (!details.path.contains("nullness-javac-errors") && details.name.endsWith('java')) {
            javaFiles.add(details.file)
        }
    }

    List<String> args = new ArrayList<>();
    for (File f : javaFiles) {
        args += f.absolutePath
    }
    return args
}

task htmlValidate(type: Exec, group: 'Format') {
    description 'Validate that HTML files are well-formed'
    executable 'html5validator'
    args = [
            "--ignore",
            "/api/",
            "/build/",
            "/docs/manual/manual.html",
            "/checker/jdk/nullness/src/java/lang/ref/package.html"
    ]
}


// `gradle allJavadoc` builds the Javadoc for all modules in `docs/api`.
//   This is what is published to checkerframework.org.
// `gradle javadoc` builds the Javadoc for each sub-project in <subproject>/build/docs/javadoc/ .
//   It's needed to create the Javadoc jars that we release in Maven Central.
// To make javadoc for only one subproject, run `./gradlew javadoc`
//   in the subproject or `./gradlew :checker:javadoc` at the top level.
task allJavadoc(type: Javadoc, group: "Documentation") {
    description = 'Generates a global API documentation for all the modules'
    dependsOn(':checker:shadowJar')
    destinationDir = file("${rootDir}/docs/api")
    source(project(':checker').sourceSets.main.allJava, project(':framework').sourceSets.main.allJava,
            project(':dataflow').sourceSets.main.allJava, project(':javacutil').sourceSets.main.allJava)

    classpath = files(subprojects.collect { it.sourceSets.main.compileClasspath })
    if (isJava8) {
        classpath += configurations.javacJar
    }
    doLast {
        copy {
            from 'docs/logo/Checkmark/CFCheckmark_favicon.png'
            rename('CFCheckmark_favicon.png', 'favicon-checkerframework.png')
            into "${rootDir}/docs/api"
        }
    }
}

// See documentation for allJavadoc task.
javadoc.dependsOn(allJavadoc)

configurations {
    requireJavadoc
}
dependencies {
    // https://mvnrepository.com/artifact/org.plumelib/require-javadoc
    requireJavadoc group: 'org.plumelib', name: 'require-javadoc', version: '0.1.2'
}


/**
 * Creates a task named taskName that runs javadoc.
 *
 * @param taskName the name of the task to create
 * @param taskDescription description of the task
 * @param memberLevel the JavadocMemberLevel to use
 * @param requireJavadoc whether or not to run the RequireJavadoc doclet. If false -Xdoclint:all is
 * passed. Defaults to true.
 * @return the new task
 */
def createJavadocTask(taskName, taskDescription, memberLevel, requireJavadoc = true) {
    tasks.create(name: taskName, type: Javadoc) {
        description = taskDescription
        destinationDir = file("${rootDir}/docs/tmpapi")
        destinationDir.mkdirs()
        subprojects.forEach {
            if(!it.name.startsWith("checker-qual")) {
                source += it.sourceSets.main.allJava
            }
        }

        classpath = files(subprojects.collect { it.sourceSets.main.compileClasspath })

        destinationDir.deleteDir()
        options.memberLevel = memberLevel
        if (requireJavadoc) {
            options.docletpath = configurations.requireJavadoc.asList()
            options.doclet = "org.plumelib.javadoc.RequireJavadoc"
        }
        options.addBooleanOption('Xdoclint:all', !requireJavadoc)
        options.addStringOption('Xmaxwarns', '99999')

        // options.addStringOption('skip', 'ClassNotToCheck|OtherClass')
    }
}
// With JDK 11 this task produces the following error:
// javadoc: error - invalid flag: -d
createJavadocTask('requireJavadoc', 'Ensures that protected and public Java elements have Javadoc documentation.', JavadocMemberLevel.PROTECTED)
// With JDK 11 this task produces the following error:
// javadoc: error - invalid flag: -d
createJavadocTask('requireJavadocPrivate', 'Ensures that private, protected, and public Java elements have Javadoc documentation.', JavadocMemberLevel.PRIVATE)
createJavadocTask('javadocDoclintAll', 'Runs javadoc with -Xdoclint:all option.', JavadocMemberLevel.PRIVATE, false)

task downloadJtreg(type: Download) {
    description "Downloads and unpacks jtreg."
    onlyIf { !(new File("${jtregHome}/lib/jtreg.jar").exists()) }
    // TODO: ci.adoptopenjdk.net is currently down, use a local copy. (4/4/2019)
    // src 'https://ci.adoptopenjdk.net/view/Dependencies/job/jtreg/lastSuccessfulBuild/artifact/jtreg-4.2.0-tip.tar.gz'
    src 'https://checkerframework.org/jtreg-4.2.0-tip.tar.gz'
    overwrite true
    dest new File(buildDir, 'jtreg-4.2.0-tip.tar.gz')
    doLast {
        copy {
            from tarTree(dest)
            into "${jtregHome}/.."
        }
        exec {
            commandLine('chmod',  '+x', "${jtregHome}/bin/jtdiff", "${jtregHome}/bin/jtreg")
        }
    }
}

// See alternate implementation getCodeFormatScriptsInGradle below.
task getCodeFormatScripts() {
    description 'Obtain or update the run-google-java-format scripts'
    if (file(formatScriptsHome).exists()) {
        exec {
            workingDir formatScriptsHome
            executable 'git'
            args = ['pull', '-q']
            ignoreExitValue = true
        }
    } else {
        exec {
            workingDir "${formatScriptsHome}/../"
            executable 'git'
            args = ['clone', '-q', 'https://github.com/plume-lib/run-google-java-format.git', '.run-google-java-format']
        }
    }
}

// This implementation is preferable to the above because it does work in Gradle rather than in bash.
// However, it fails in the presence of worktrees: https://github.com/ajoberstar/grgit/issues/97
task getCodeFormatScriptsInGradle {
  description "Obtain the run-google-java-format scripts"
  doLast {
    if (! new File(formatScriptsHome).exists()) {
      def rgjfGit = Grgit.clone(dir: formatScriptsHome, uri: 'https://github.com/plume-lib/run-google-java-format.git')
    } else {
      def rgjfGit = Grgit.open(dir: formatScriptsHome)
      rgjfGit.pull()
    }
  }
}

task getPlumeScripts() {
    description 'Obtain or update plume-scripts'
    if (file(plumeScriptsHome).exists()) {
        exec {
            workingDir plumeScriptsHome
            executable 'git'
            args = ['pull', '-q']
            ignoreExitValue = true
        }
    } else {
        exec {
            workingDir "${plumeScriptsHome}/../"
            executable 'git'
            args = ['clone', '-q', 'https://github.com/plume-lib/plume-scripts.git', '.plume-scripts']
        }
    }
}

task pythonIsInstalled(type: Exec) {
  description "Check that the python executable is installed."
  executable = "python"
  args "--version"
}

task tags {
    description 'Create Emacs TAGS table'
    doLast {
        exec {
            commandLine "etags", "-i", "checker/TAGS", "-i", "dataflow/TAGS", "-i", "framework/TAGS", "-i", "framework-test/TAGS", "-i", "javacutil/TAGS", "-i", "docs/manual/TAGS"
        }
        exec {
            commandLine "make", "-C", "docs/manual", "tags"
        }
    }
}

subprojects {
    configurations {
        errorprone
    }

    dependencies {
        // https://mvnrepository.com/artifact/com.google.errorprone/error_prone_core
        // If you update this:
        //  * Temporarily comment out "-Werror" elsewhere in this file
        //  * Repeatedly run `./gradlew clean runErrorProne` and fix all errors
        //  * Uncomment "-Werror"
        errorprone group: 'com.google.errorprone', name: 'error_prone_core', version: '2.3.3'
    }

    task checkFormat(type: Exec, dependsOn: [getCodeFormatScripts, pythonIsInstalled], group: 'Format') {
        description 'Check whether the source code is properly formatted'
        // jdk8 and checker-qual have no source, so skip
        onlyIf { !project.name.is('jdk8') && !project.name.startsWith('checker-qual') }
        executable 'python'

        doFirst {
            args += "${formatScriptsHome}/check-google-java-format.py"
            args += "--aosp" // 4 space indentation
            args += getJavaFilesToFormat(project.name)
        }
        ignoreExitValue = true
        doLast {
            if (execResult.exitValue != 0) {
                throw new RuntimeException('Found improper formatting, try running:  ./gradlew reformat"')
            }
        }
    }

    task reformat(type: Exec, dependsOn: [getCodeFormatScripts, pythonIsInstalled], group: 'Format') {
        description 'Format the Java source code'
        // jdk8 and checker-qual have no source, so skip
        onlyIf { !project.name.is('jdk8') && !project.name.startsWith('checker-qual') }
        executable 'python'
        doFirst {
            args += "${formatScriptsHome}/run-google-java-format.py"
            args += "--aosp" // 4 space indentation
            args += getJavaFilesToFormat(project.name)
        }
    }

    shadowJar {
        // Relocate packages that might conflict with user's classpath.
        doFirst {
            if (release) {
                // Only relocate JavaParser during a release:
                relocate 'com.github.javaparser', 'org.checkerframework.com.github.javaparser'
            }
        }
        // Don't relocate javac.jar:
        // relocate 'com.sun', 'org.checkeframework.com.sun'
        // relocate 'javax','org.checkerframework.javax'
        // relocate 'jdk', 'org.checkerframework.jdk'

        // These appear in annotation-file-utilities-all.jar:
        relocate 'org.apache', 'org.checkerframework.org.apache'
        relocate 'org.relaxng', 'org.checkerframework.org.relaxng'
        relocate 'org.plumelib', 'org.checkerframework.org.plumelib'
        relocate 'org.codehaus', 'org.checkerframework.org.codehaus'
        // relocate 'sun', 'org.checkerframework.sun'
        relocate 'org.objectweb.asm', 'org.checkerframework.org.objectweb.asm'
        relocate 'com.google', 'org.checkerframework.com.google'
        relocate 'plume', 'org.checkerframework.plume'
    }

    if (!project.name.startsWith('checker-qual') && !project.name.is('jdk8')) {
        task tags(type: Exec) {
            description 'Create Emacs TAGS table'
            commandLine "bash", "-c", "find . \\( -name jdk \\) -prune -o -name '*.java' -print | sort-directory-order | xargs ctags -e -f TAGS"
        }
    }

    // Things in this block reference definitions in the subproject that do not exist,
    // until the project is evaluated.
    afterEvaluate {
        // Create a sourcesJar task for each subproject
        tasks.create(name: 'sourcesJar', type: Jar) {
            description 'Creates sources jar.'
            archiveClassifier = 'source'
            archiveBaseName = jar.archiveBaseName
            from sourceSets.main.java
        }

        // Create a javadocJar task for each subproject
        tasks.create(name: 'javadocJar', type: Jar, dependsOn: 'javadoc') {
            description 'Creates javadoc jar.'
            archiveClassifier = 'javadoc'
            archiveBaseName = jar.archiveBaseName
            from tasks.javadoc.destinationDir
        }

        // Adds manifest to all Jar files
        tasks.withType(Jar) {
            includeEmptyDirs = false
            if (archiveFileName.get().startsWith("checker-qual")) {
                metaInf {
                    from './LICENSE.txt'
                }
            } else {
                metaInf {
                    from "${rootDir}/LICENSE.txt"
                }
            }
            manifest {
                attributes("Implementation-Version": "${version}")
                attributes("Implementation-URL": "https://checkerframework.org")
                if (! archiveFileName.get().endsWith("source.jar")) {
                    attributes('Automatic-Module-Name': "org.checkerframework." + project.name.replaceAll('-', '.'))
                }
                if (archiveFileName.get().startsWith("checker-qual")) {
                    attributes("Bundle-License": "MIT")
                } else {
                    attributes("Bundle-License": "(GPL-2.0-only WITH Classpath-exception-2.0)")
                }
            }
        }

        // Add tasks to run various checkers on all the main source sets.
        createCheckTypeTask(project.name, 'org.checkerframework.checker.nullness.NullnessChecker', 'Nullness')
        createCheckTypeTask(project.name, 'org.checkerframework.checker.nullness.NullnessChecker', 'WorkingNullness', ['-AskipUses=com.sun.*', '-AskipDefs=org.checkerframework.checker.*|org.checkerframework.common.*|org.checkerframework.framework.*|org.checkerframework.dataflow.cfg.CFGBuilder'])
        createCheckTypeTask(project.name, 'org.checkerframework.framework.util.PurityChecker', 'Purity')
        // TODO: fix or suppress all not.interned warnings and remove the suppression here.
        createCheckTypeTask(project.name, 'org.checkerframework.checker.interning.InterningChecker', 'Interning', ['-AsuppressWarnings=not.interned'])

        // Add jtregTests to framework and checker modules
        if (project.name.is('framework') || project.name.is('checker')) {
            tasks.create(name: 'jtregTests', dependsOn: ':downloadJtreg', group: 'Verification') {
                description 'Run the jtreg tests.'
                dependsOn('compileJava')
                dependsOn('compileTestJava')
                dependsOn(':checker:updateJdk')
                dependsOn('shadowJar')

                String jtregOutput = "${buildDir}/jtreg"
                String name = 'all'
                String tests = '.'
                doLast {
                    exec {
                        executable "${jtregHome}/bin/jtreg"
                        args = [
                                "-dir:${projectDir}/jtreg",
                                "-workDir:${jtregOutput}/${name}/work",
                                "-reportDir:${jtregOutput}/${name}/report",
                                "-verbose:error,fail",
                                "-javacoptions:-g",
                                "-keywords:!ignore",
                                "-samevm",
                                "-javacoptions:-classpath ${tasks.shadowJar.archiveFile.get()}:${sourceSets.test.output.asPath}",
                                // Required for checker/jtreg/nullness/PersistUtil.java and other tests
                                "-vmoptions:-classpath ${tasks.shadowJar.archiveFile.get()}:${sourceSets.test.output.asPath}",
                        ]
                        if (isJava8) {
                            // Use Error Prone javac and source/target 8
                            args += [
                                "-vmoptions:-Xbootclasspath/p:${configurations.javacJar.asPath}",
                                "-javacoptions:-Xbootclasspath/p:${configurations.javacJar.asPath}",
                                "-javacoptions:-source 8",
                                "-javacoptions:-target 8"
                                ]
                            if (project.name.is('checker')) {
                                args += [
                                        "-javacoptions:-Xbootclasspath/p:${projectDir}/dist/jdk8.jar",
                                ]
                            }
                        } else {
                            args += [
                                    // checker/jtreg/nullness/defaultsPersist/ReferenceInfoUtil.java
                                    // uses the jdk.jdeps module.
                                    "-javacoptions:--add-modules jdk.jdeps",
                                    "-javacoptions:--add-exports=jdk.jdeps/com.sun.tools.classfile=ALL-UNNAMED",
                                    "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
                            ]
                        }
                        if (project.name.is('framework')) {
                            // Do not check for the annotated JDK
                            args += [
                                    "-javacoptions:-Anocheckjdk"
                            ]
                        } else if (project.name.is('checker')) {
                            args += [
                                    "-javacoptions:-classpath ${sourceSets.testannotations.output.asPath}",
                            ]
                        }

                        // Location of jtreg tests
                        args += "${tests}"
                    }
                }
            }
        }

        // Create a task for each JUnit test class whose name is the same as the JUnit class name.
        sourceSets.test.allJava.filter { it.path.contains('src/test/java/tests') }.forEach { file ->
            String junitClassName = file.name.replaceAll(".java", "")
            tasks.create(name: "${junitClassName}", type: Test) {
                description "Run ${junitClassName} tests."
                include "**/${name}.class"
            }
        }

        // Configure JUnit tests
        tasks.withType(Test) {
            if (isJava8) {
                jvmArgs "-Xbootclasspath/p:${configurations.javacJar.asPath}"
            } else {
                // Without this, the test throw "java.lang.OutOfMemoryError: Java heap space"
                forkEvery(1)
                jvmArgs += [
                        "--illegal-access=warn",
                        "--add-opens", "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
                ]
            }

            if (project.name.is('checker')) {
                dependsOn('copyJarsToDist')
                systemProperties += [JDK_JAR: "${projectDir}/dist/jdk8.jar"]
            }

            if (project.hasProperty('emit.test.debug')) {
                systemProperties += ["emit.test.debug": 'true']
            }

            testLogging {
                showStandardStreams = true
                // Always run the tests
                outputs.upToDateWhen { false }

                // Show the found unexpected diagnostics and expected diagnostics not found.
                exceptionFormat "full"
                events "failed"
            }

            // After each test, print a summary.
            afterSuite { desc, result ->
                if (desc.getClassName() != null) {
                    long mils = result.getEndTime() - result.getStartTime()
                    double seconds = mils / 1000.0

                    println "Testsuite: ${desc.getClassName()}\n" +
                            "Tests run: ${result.testCount}, " +
                            "Failures: ${result.failedTestCount}, " +
                            "Skipped: ${result.skippedTestCount}, " +
                            "Time elapsed: ${seconds} sec\n"
                }

            }
        }

        // Create a runErrorProne task.
        tasks.create(name: 'runErrorProne', type: JavaCompile, group: 'Verification') {
            description 'Run the error-prone compiler on the main sources'

            source = sourceSets.main.java.asFileTree
            classpath = sourceSets.main.compileClasspath.asFileTree
            destinationDir = new File("${buildDir}", 'errorprone')

            // Error Prone must be available in the annotation processor path
            options.annotationProcessorPath = configurations.errorprone
            // Enable Error Prone
            options.errorprone.enabled = true
            options.errorprone.disableWarningsInGeneratedCode = true
            options.errorprone.errorproneArgs = [
                    // Many compiler classes are interned.
                    '-Xep:ReferenceEquality:OFF',
                    // These might be worth fixing.
                    '-Xep:DefaultCharset:OFF',
                    // Not useful to suggest Splitter; maybe clean up.
                    '-Xep:StringSplitter:OFF',
                    // Too broad, rejects seemingly-correct code.
                    '-Xep:EqualsGetClass:OFF',
                    // Not a real problem
                    '-Xep:MixedMutabilityReturnType:OFF',
                    // -Werror halts the build if Error Prone issues a warning, which ensures that
                    // the errors get fixed.  On the downside, Error Prone (or maybe the compiler?)
                    // stops as soon as it one warning, rather than outputting them all.
                    // https://github.com/google/error-prone/issues/436
                    '-Werror',
            ]
        }

        // Create a nonJunitTests task per project
        tasks.create(name: 'nonJunitTests', type: GradleBuild, group: 'Verification') {
            description 'Run all Checker Framework tests except for the Junit tests.'
            tasks = ['checkPurity', 'checkInterning', 'checkWorkingNullness']
            if (project.name.is('framework') || project.name.is('checker')) {
                tasks += ['checkCompilerMessages', 'jtregTests']
            }
            if (project.name.is('framework')) {
                tasks += ['wholeProgramInferenceTests', 'loaderTests']
            }

            if (project.name.is('checker')) {
                if (!isJava8) {
                    tasks += ['jtregJdk11Tests']
                }
                tasks += ['nullnessExtraTests', 'commandLineTests', 'tutorialTests']
            }
        }

        // Create an allTests task per project
        tasks.create(name: 'allTests', type: GradleBuild, group: 'Verification') {
            description 'Run all Checker Framework tests'
	    // The 'test' target is just the JUnit tests.
            tasks = ['nonJunitTests', 'test']
        }

        task javadocPrivate(dependsOn: javadoc) {
            doFirst {
                javadocMemberLevel = JavadocMemberLevel.PRIVATE
            }
            doLast {
                javadocMemberLevel = JavadocMemberLevel.PROTECTED
            }
        }
    }
}

assemble.dependsOn(':checker:copyJarsToDist')

task checkBasicStyle(group: 'Format') {
    description 'Check basic style guidelines.  Not related to Checkstyle tool.'
    String[] ignoreDirectories = ['.git',
                                  '.gradle',
                                  '.idea',
                                  '.plume-scripts',
                                  'annotated',
                                  'api',
                                  'bib',
                                  'bootstrap',
                                  'build',
                                  'jdk',
                                  'maven-artifacts']

    String[] ignoreFilePatterns = [
            '*.aux',
            '*.bib',
            '*.class',
            '*.dvi',
            '*.expected',
            '*.gif',
            '*.jar',
            '*.jtr',
            '*.log',
            '*.out',
            '*.patch',
            '*.pdf',
            '*.png',
            '*.sty',
            '*.xcf',
            '*~',
            '#*#',
            'CFLogo.ai',
            'logfile.log.rec.index',
            'manual.html',
            'manual.html-e',
            'junit.*.properties']
    doLast {
        FileTree tree = fileTree(dir: projectDir)
        for (String dir : ignoreDirectories) {
            tree.exclude "**/${dir}/**"
        }
        for (String file : ignoreFilePatterns) {
            tree.exclude "**/${file}"
        }
        boolean failed = false
        tree.visit {
            if (!it.file.isDirectory()) {
                int isBlankLine
                it.file.eachLine { line ->
                    if (line.endsWith(' ')) {
                        println("Trailing whitespace: ${it.file.absolutePath}")
                        failed = true
                    }
                    if (!line.startsWith('\\') &&
                            (line.matches('^.* (else|finally|try)\\{}.*$')
                                    || line.matches('^.*}(catch|else|finally) .*$')
                                    || line.matches('^.* (catch|for|if|while)\\('))) {
                        // This runs on non-java files, too.
                        println("Missing space: ${it.file.absolutePath}")
                        failed = true
                    }
                    if (line.isEmpty()) {
                        isBlankLine++;
                    } else {
                        isBlankLine = 0;
                    }
                }

                if (isBlankLine > 1) {
                    println("Blank line at end of file: ${it.file.absolutePath}")
                    failed = true
                }

                RandomAccessFile file
                try {
                    file = new RandomAccessFile(it.file, 'r')
                    int end = file.length() - 1;
                    if (end > 0) {
                        file.seek(end)
                        byte last = file.readByte()
                        if (last != '\n') {
                            println("Missing newline at end of file: ${it.file.absolutePath}")
                            failed = true
                        }
                    }
                } finally {
                    if (file != null) {
                        file.close()
                    }
                }
            }
        }
        if (failed) {
            throw new GradleException("Files do not meet basic style guidelines.")
        }
    }
}

task releaseBuild(type: GradleBuild) {
    description 'Build everything required for a release'
    startParameter = new StartParameter()
    startParameter.setProjectProperties(release: true)
    // This downloads rather than builds the jdk8.jar because that's what is used in the CI tests.
    tasks = ['clean', 'assemble', 'sourcesJar', 'javadocJar', 'allJavadoc', 'framework:allJavadocJar', 'framework:allSourcesJar', 'checker:allJavadocJar', 'checker:allSourcesJar']
}

task releaseAndTest(type: GradleBuild, dependsOn: 'releaseBuild') {
    description 'Build everything required for a release and run allTests'
    startParameter = new StartParameter()
    startParameter.setProjectProperties(release: true)
    tasks = ['allTests']
}

// Don't create an empty checker-framework-VERSION.jar
jar.onlyIf {false}
