Add lintOptions#checkReleaseBuilds which runs lint on release builds

If lintOptions.checkReleaseBuilds is true (which it is by default) the
assemble task for non-debug targets will now run lint and check all
issues that have severity fatal.

It also tweaks the error message shown when the build aborts due to
a lint error. If you run a lint target, the message will look like this:

> Lint found errors in the project; aborting build.

  Fix the issues identified by lint, or add the following to your build
  script to proceed with errors:
  ...
  android {
      lintOptions {
          abortOnError false
      }
  }
  ...

If the build fails during release target assembly, it looks like this:

* What went wrong:
Execution failed for task ':lintVitalRelease'.
> Lint found fatal errors while assembling a release target.

  To proceed, either fix the issues identified by lint, or modify your
  build script as follows:
  ...
  android {
      lintOptions {
          checkReleaseBuilds false
          // Or, if you prefer, you can continue to check for errors
          // in release builds, but continue the build even when
          // errors are found:
          abortOnError false
      }
  }
  ...

Change-Id: I87debf8df0ed88f327b4df7119a9dfb057c34945
diff --git a/build-system/builder-model/src/main/java/com/android/builder/model/LintOptions.java b/build-system/builder-model/src/main/java/com/android/builder/model/LintOptions.java
index c2ae6de..ae3f632 100644
--- a/build-system/builder-model/src/main/java/com/android/builder/model/LintOptions.java
+++ b/build-system/builder-model/src/main/java/com/android/builder/model/LintOptions.java
@@ -34,6 +34,9 @@
  *          quiet true
  *          // if true, stop the gradle build if errors are found
  *          abortOnError false
+ *          // set to true to have all release builds run lint on issues with severity=fatal
+ *          // and abort the build (controlled by abortOnError above) if fatal issues are found
+ *          checkReleaseBuilds true
  *          // if true, only report errors
  *          ignoreWarnings true
  *          // if true, emit full/absolute paths to files with errors (true by default)
@@ -163,4 +166,9 @@
     @Nullable
     public File getXmlOutput();
 
+    /**
+     * Returns whether lint should check for fatal errors during release builds. Default is true.
+     * If issues with severity "fatal" are found, the release build is aborted.
+     */
+    public boolean isCheckReleaseBuilds();
 }
diff --git a/build-system/gradle/src/main/groovy/com/android/build/gradle/BasePlugin.groovy b/build-system/gradle/src/main/groovy/com/android/build/gradle/BasePlugin.groovy
index 6efd8e9..2ab63fb 100644
--- a/build-system/gradle/src/main/groovy/com/android/build/gradle/BasePlugin.groovy
+++ b/build-system/gradle/src/main/groovy/com/android/build/gradle/BasePlugin.groovy
@@ -174,6 +174,8 @@
     protected Task deviceCheck
     protected Task connectedCheck
     protected Task lintCompile
+    protected Task lintAll
+    protected Task lintVital
 
     protected BasePlugin(Instantiator instantiator, ToolingModelBuilderRegistry registry) {
         this.instantiator = instantiator
@@ -277,6 +279,14 @@
 
         doCreateAndroidTasks()
         createReportTasks()
+
+        if (lintVital != null) {
+            project.gradle.taskGraph.whenReady { taskGraph ->
+                if (taskGraph.hasTask(lintAll)) {
+                    lintVital.setEnabled(false)
+                }
+            }
+        }
     }
 
     void checkTasksAlreadyCreated() {
@@ -933,6 +943,7 @@
         lint.group = JavaBasePlugin.VERIFICATION_GROUP
         lint.setPlugin(this)
         project.tasks.check.dependsOn lint
+        lintAll = lint
 
         int count = variantDataList.size()
         for (int i = 0 ; i < count ; i++) {
@@ -959,6 +970,25 @@
         }
     }
 
+    private void createLintVitalTask(@NonNull ApkVariantData variantData) {
+        assert extension.lintOptions.checkReleaseBuilds
+        if (!variantData.variantConfiguration.buildType.debuggable) {
+            String variantName = variantData.variantConfiguration.fullName
+            def capitalizedVariantName = variantName.capitalize()
+            def taskName = "lintVital" + capitalizedVariantName
+            Lint lintReleaseCheck = project.tasks.create(taskName, Lint)
+            // TODO: Make this task depend on lintCompile too (resolve initialization order first)
+            lintReleaseCheck.dependsOn variantData.javaCompileTask
+            lintReleaseCheck.setPlugin(this)
+            lintReleaseCheck.setVariantName(variantName)
+            lintReleaseCheck.setFatalOnly(true)
+            lintReleaseCheck.description = "Runs lint on just the fatal issues in the " +
+                    capitalizedVariantName + " build"
+            variantData.assembleTask.dependsOn lintReleaseCheck
+            lintVital = lintReleaseCheck
+        }
+    }
+
     protected void createCheckTasks(boolean hasFlavors, boolean isLibraryTest) {
         List<AndroidReportTask> reportTasks = Lists.newArrayListWithExpectedSize(2)
 
@@ -1368,6 +1398,9 @@
         }
         assembleTask.dependsOn appTask
         variantData.assembleTask = assembleTask
+        if (extension.lintOptions.checkReleaseBuilds) {
+            createLintVitalTask(variantData)
+        }
 
         variantData.outputFile = { outputFileTask.outputFile }
 
diff --git a/build-system/gradle/src/main/groovy/com/android/build/gradle/internal/dsl/LintOptionsImpl.groovy b/build-system/gradle/src/main/groovy/com/android/build/gradle/internal/dsl/LintOptionsImpl.groovy
index 253cdd3..b04f801 100644
--- a/build-system/gradle/src/main/groovy/com/android/build/gradle/internal/dsl/LintOptionsImpl.groovy
+++ b/build-system/gradle/src/main/groovy/com/android/build/gradle/internal/dsl/LintOptionsImpl.groovy
@@ -60,6 +60,8 @@
     private boolean warningsAsErrors
     @Input
     private boolean showAll
+    @Input
+    private boolean checkReleaseBuilds = true;
     @InputFile
     private File lintConfig
     @Input
@@ -96,7 +98,8 @@
             boolean checkAllWarnings,
             boolean ignoreWarnings,
             boolean warningsAsErrors,
-            boolean showAll) {
+            boolean showAll,
+            boolean checkReleaseBuilds) {
         this.disable = disable
         this.enable = enable
         this.check = check
@@ -115,6 +118,7 @@
         this.ignoreWarnings = ignoreWarnings
         this.warningsAsErrors = warningsAsErrors
         this.showAll = showAll
+        this.checkReleaseBuilds = checkReleaseBuilds
     }
 
     @NonNull
@@ -137,7 +141,8 @@
                 source.isCheckAllWarnings(),
                 source.isIgnoreWarnings(),
                 source.isWarningsAsErrors(),
-                source.isShowAll()
+                source.isShowAll(),
+                source.isCheckReleaseBuilds()
         )
     }
 
@@ -296,6 +301,15 @@
         this.showAll = showAll
     }
 
+    @Override
+    public boolean isCheckReleaseBuilds() {
+        return checkReleaseBuilds;
+    }
+
+    public void setCheckReleaseBuilds(boolean checkReleaseBuilds) {
+        this.checkReleaseBuilds = checkReleaseBuilds
+    }
+
     /**
      * Returns the default configuration file to use as a fallback
      */
@@ -390,8 +404,8 @@
         flags.setShowEverything(showAll)
         flags.setDefaultConfiguration(lintConfig)
 
-        if (report) {
-            if (textReport) {
+        if (report || flags.isFatalOnly()) {
+            if (textReport || flags.isFatalOnly()) {
                 File output = textOutput
                 if (output == null) {
                     output = new File(STDOUT)
@@ -420,8 +434,8 @@
             }
             if (xmlReport) {
                 File output = xmlOutput
-                if (output == null) {
-                    output = createOutputPath(project, variantName, DOT_XML)
+                if (output == null || flags.isFatalOnly()) {
+                    output = createOutputPath(project, variantName, DOT_XML, flags.isFatalOnly())
                 } else if (!output.isAbsolute()) {
                     output = project.file(output.getPath())
                 }
@@ -434,8 +448,8 @@
             }
             if (htmlReport) {
                 File output = htmlOutput
-                if (output == null) {
-                    output = createOutputPath(project, variantName, ".html")
+                if (output == null || flags.isFatalOnly()) {
+                    output = createOutputPath(project, variantName, ".html", flags.isFatalOnly())
                 } else if (!output.isAbsolute()) {
                     output = project.file(output.getPath())
                 }
@@ -488,13 +502,17 @@
     private static File createOutputPath(
             @NonNull Project project,
             @NonNull String variantName,
-            @NonNull String extension) {
+            @NonNull String extension,
+            boolean fatalOnly) {
         StringBuilder base = new StringBuilder()
         base.append("lint-results")
         if (variantName != null) {
             base.append("-")
             base.append(variantName)
         }
+        if (fatalOnly) {
+            base.append("-fatal")
+        }
         base.append(extension)
         return new File(project.buildDir, base.toString())
     }
diff --git a/build-system/gradle/src/main/groovy/com/android/build/gradle/internal/model/DefaultAndroidProject.java b/build-system/gradle/src/main/groovy/com/android/build/gradle/internal/model/DefaultAndroidProject.java
index b8f7ec2..4c61d48 100644
--- a/build-system/gradle/src/main/groovy/com/android/build/gradle/internal/model/DefaultAndroidProject.java
+++ b/build-system/gradle/src/main/groovy/com/android/build/gradle/internal/model/DefaultAndroidProject.java
@@ -18,7 +18,6 @@
 
 import com.android.annotations.NonNull;
 import com.android.build.gradle.internal.CompileOptions;
-import com.android.build.gradle.internal.dsl.LintOptionsImpl;
 import com.android.builder.model.AaptOptions;
 import com.android.builder.model.AndroidProject;
 import com.android.builder.model.ArtifactMetaData;
diff --git a/build-system/gradle/src/main/groovy/com/android/build/gradle/tasks/Lint.groovy b/build-system/gradle/src/main/groovy/com/android/build/gradle/tasks/Lint.groovy
index 60af0a4..2e67f97 100644
--- a/build-system/gradle/src/main/groovy/com/android/build/gradle/tasks/Lint.groovy
+++ b/build-system/gradle/src/main/groovy/com/android/build/gradle/tasks/Lint.groovy
@@ -37,11 +37,10 @@
 import org.gradle.api.Project
 import org.gradle.api.tasks.TaskAction
 
-import static com.android.SdkConstants.DOT_XML
-
 public class Lint extends DefaultTask {
     @NonNull private BasePlugin mPlugin
     @Nullable private String mVariantName
+    private boolean mFatalOnly
 
     public void setPlugin(@NonNull BasePlugin plugin) {
         mPlugin = plugin
@@ -51,6 +50,10 @@
         mVariantName = variantName
     }
 
+    public void setFatalOnly(boolean fatalOnly) {
+        mFatalOnly = fatalOnly
+    }
+
     @SuppressWarnings("GroovyUnusedDeclaration")
     @TaskAction
     public void lint() {
@@ -108,10 +111,44 @@
         }
 
         if (flags.isSetExitCode() && errorCount > 0) {
-            throw new GradleException("Lint found errors with abortOnError=true; aborting build.")
+            abort()
         }
     }
 
+    private void abort() {
+        def message;
+        if (mFatalOnly) {
+            message = "" +
+                    "Lint found fatal errors while assembling a release target.\n" +
+                    "\n" +
+                    "To proceed, either fix the issues identified by lint, or modify your build script as follows:\n" +
+                    "...\n" +
+                    "android {\n" +
+                    "    lintOptions {\n" +
+                    "        checkReleaseBuilds false\n" +
+                    "        // Or, if you prefer, you can continue to check for errors in release builds,\n" +
+                    "        // but continue the build even when errors are found:\n" +
+                    "        abortOnError false\n" +
+                    "    }\n" +
+                    "}\n" +
+                    "..."
+                    ""
+        } else {
+            message = "" +
+                    "Lint found errors in the project; aborting build.\n" +
+                    "\n" +
+                    "Fix the issues identified by lint, or add the following to your build script to proceed with errors:\n" +
+                    "...\n" +
+                    "android {\n" +
+                    "    lintOptions {\n" +
+                    "        abortOnError false\n" +
+                    "    }\n" +
+                    "}\n" +
+                    "..."
+        }
+        throw new GradleException(message);
+    }
+
     /**
      * Runs lint on a single specified variant
      */
@@ -128,7 +165,14 @@
         LintCliFlags flags = new LintCliFlags()
         LintGradleClient client = new LintGradleClient(registry, flags, mPlugin, modelProject,
                 variantName)
-        mPlugin.getExtension().lintOptions.syncTo(client, flags, variantName, project, report)
+        def options = mPlugin.getExtension().lintOptions
+        if (mFatalOnly) {
+            if (!options.isCheckReleaseBuilds()) {
+                return
+            }
+            flags.setFatalOnly(true)
+        }
+        options.syncTo(client, flags, variantName, project, report)
 
         List<Warning> warnings;
         try {
@@ -138,7 +182,7 @@
         }
 
         if (report && client.haveErrors() && flags.isSetExitCode()) {
-            throw new GradleException("Lint found errors with abortOnError=true; aborting build.")
+            abort()
         }
 
         return warnings;
diff --git a/lint/cli/src/main/java/com/android/tools/lint/LintCliClient.java b/lint/cli/src/main/java/com/android/tools/lint/LintCliClient.java
index ae004a2..e440ba3 100644
--- a/lint/cli/src/main/java/com/android/tools/lint/LintCliClient.java
+++ b/lint/cli/src/main/java/com/android/tools/lint/LintCliClient.java
@@ -156,7 +156,7 @@
 
     @Override
     public Configuration getConfiguration(@NonNull Project project) {
-        return new CliConfiguration(getConfiguration(), project);
+        return new CliConfiguration(getConfiguration(), project, mFlags.isFatalOnly());
     }
 
     /** File content cache */
@@ -374,12 +374,17 @@
      * flags supplied on the command line
      */
     class CliConfiguration extends DefaultConfiguration {
-        CliConfiguration(@NonNull Configuration parent, @NonNull Project project) {
+        private boolean mFatalOnly;
+
+        CliConfiguration(@NonNull Configuration parent, @NonNull Project project,
+                boolean fatalOnly) {
             super(LintCliClient.this, project, parent);
+            mFatalOnly = fatalOnly;
         }
 
-        CliConfiguration(File lintFile) {
+        CliConfiguration(File lintFile, boolean fatalOnly) {
             super(LintCliClient.this, null /*project*/, null /*parent*/, lintFile);
+            mFatalOnly = fatalOnly;
         }
 
         @NonNull
@@ -387,7 +392,11 @@
         public Severity getSeverity(@NonNull Issue issue) {
             Severity severity = computeSeverity(issue);
 
-            if (mFlags.isWarningsAsErrors() && severity != Severity.IGNORE) {
+            if (mFatalOnly && severity != Severity.FATAL) {
+                return Severity.IGNORE;
+            }
+
+            if (mFlags.isWarningsAsErrors() && severity.compareTo(Severity.ERROR) < 0) {
                 severity = Severity.ERROR;
             }
 
@@ -598,7 +607,7 @@
     }
 
     public Configuration createConfigurationFromFile(File file) {
-        return new CliConfiguration(file);
+        return new CliConfiguration(file, mFlags.isFatalOnly());
     }
 
     @SuppressWarnings("resource") // Eclipse doesn't know about Closeables.closeQuietly
diff --git a/lint/cli/src/main/java/com/android/tools/lint/LintCliFlags.java b/lint/cli/src/main/java/com/android/tools/lint/LintCliFlags.java
index caf74ce..6380e81 100644
--- a/lint/cli/src/main/java/com/android/tools/lint/LintCliFlags.java
+++ b/lint/cli/src/main/java/com/android/tools/lint/LintCliFlags.java
@@ -48,6 +48,7 @@
     private boolean mWarnAll;
     private boolean mNoWarnings;
     private boolean mAllErrors;
+    private boolean mFatalOnly;
     private List<File> mSources;
     private List<File> mClasses;
     private List<File> mLibraries;
@@ -337,4 +338,20 @@
     public void setResourcesOverride(@Nullable List<File> resources) {
         mResources = resources;
     }
+
+    /**
+     * Returns true if we should only check fatal issues
+     * @return true if we should only check fatal issues
+     */
+    public boolean isFatalOnly() {
+        return mFatalOnly;
+    }
+
+    /**
+     * Sets whether we should only check fatal issues
+     * @param fatalOnly if true, only check fatal issues
+     */
+    public void setFatalOnly(boolean fatalOnly) {
+        mFatalOnly = fatalOnly;
+    }
 }