/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.build.gradle.internal.dsl;
import static com.android.SdkConstants.DOT_XML;
import static com.android.builder.model.AndroidProject.FD_OUTPUTS;
import static com.android.tools.lint.detector.api.Severity.ERROR;
import static com.android.tools.lint.detector.api.Severity.FATAL;
import static com.android.tools.lint.detector.api.Severity.IGNORE;
import static com.android.tools.lint.detector.api.Severity.INFORMATIONAL;
import static com.android.tools.lint.detector.api.Severity.WARNING;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.HtmlReporter;
import com.android.tools.lint.LintCliClient;
import com.android.tools.lint.LintCliFlags;
import com.android.tools.lint.TextReporter;
import com.android.tools.lint.XmlReporter;
import com.android.tools.lint.checks.BuiltinIssueRegistry;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Severity;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.gradle.api.GradleException;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFile;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.Writer;
import java.util.Map;
import java.util.Set;
/**
* DSL object for configuring lint options.
*/
public class LintOptions implements com.android.builder.model.LintOptions, Serializable {
public static final String STDOUT = "stdout";
public static final String STDERR = "stderr";
private static final long serialVersionUID = 1L;
@NonNull
private Set<String> disable = Sets.newHashSet();
@NonNull
private Set<String> enable = Sets.newHashSet();
@Nullable
private Set<String> check = Sets.newHashSet();
private boolean abortOnError = true;
private boolean absolutePaths = true;
private boolean noLines;
private boolean quiet;
private boolean checkAllWarnings;
private boolean ignoreWarnings;
private boolean warningsAsErrors;
private boolean showAll;
private boolean checkReleaseBuilds = true;
private boolean explainIssues = true;
@Nullable
private File lintConfig;
private boolean textReport;
@Nullable
private File textOutput;
private boolean htmlReport = true;
@Nullable
private File htmlOutput;
private boolean xmlReport = true;
@Nullable
private File xmlOutput;
private Map<String,Severity> severities = Maps.newHashMap();
public LintOptions() {
}
public LintOptions(
@NonNull Set<String> disable,
@NonNull Set<String> enable,
@Nullable Set<String> check,
@Nullable File lintConfig,
boolean textReport,
@Nullable File textOutput,
boolean htmlReport,
@Nullable File htmlOutput,
boolean xmlReport,
@Nullable File xmlOutput,
boolean abortOnError,
boolean absolutePaths,
boolean noLines,
boolean quiet,
boolean checkAllWarnings,
boolean ignoreWarnings,
boolean warningsAsErrors,
boolean showAll,
boolean explainIssues,
boolean checkReleaseBuilds,
@Nullable Map<String,Integer> severityOverrides) {
this.disable = disable;
this.enable = enable;
this.check = check;
this.lintConfig = lintConfig;
this.textReport = textReport;
this.textOutput = textOutput;
this.htmlReport = htmlReport;
this.htmlOutput = htmlOutput;
this.xmlReport = xmlReport;
this.xmlOutput = xmlOutput;
this.abortOnError = abortOnError;
this.absolutePaths = absolutePaths;
this.noLines = noLines;
this.quiet = quiet;
this.checkAllWarnings = checkAllWarnings;
this.ignoreWarnings = ignoreWarnings;
this.warningsAsErrors = warningsAsErrors;
this.showAll = showAll;
this.explainIssues = explainIssues;
this.checkReleaseBuilds = checkReleaseBuilds;
if (severityOverrides != null) {
for (Map.Entry<String,Integer> entry : severityOverrides.entrySet()) {
severities.put(entry.getKey(), convert(entry.getValue()));
}
}
}
@NonNull
public static com.android.builder.model.LintOptions create(@NonNull com.android.builder.model.LintOptions source) {
return new LintOptions(
source.getDisable(),
source.getEnable(),
source.getCheck(),
source.getLintConfig(),
source.getTextReport(),
source.getTextOutput(),
source.getHtmlReport(),
source.getHtmlOutput(),
source.getXmlReport(),
source.getXmlOutput(),
source.isAbortOnError(),
source.isAbsolutePaths(),
source.isNoLines(),
source.isQuiet(),
source.isCheckAllWarnings(),
source.isIgnoreWarnings(),
source.isWarningsAsErrors(),
source.isShowAll(),
source.isExplainIssues(),
source.isCheckReleaseBuilds(),
source.getSeverityOverrides()
);
}
/**
* Returns the set of issue id's to suppress. Callers are allowed to modify this collection.
*/
@Override
@NonNull
@Input
public Set<String> getDisable() {
return disable;
}
/**
* Sets the set of issue id's to suppress. Callers are allowed to modify this collection.
* Note that these ids add to rather than replace the given set of ids.
*/
public void setDisable(@Nullable Set<String> ids) {
disable.addAll(ids);
}
/**
* Returns the set of issue id's to enable. Callers are allowed to modify this collection.
* To enable a given issue, add the issue ID to the returned set.
*/
@Override
@NonNull
@Input
public Set<String> getEnable() {
return enable;
}
/**
* Sets the set of issue id's to enable. Callers are allowed to modify this collection.
* Note that these ids add to rather than replace the given set of ids.
*/
public void setEnable(@Nullable Set<String> ids) {
enable.addAll(ids);
}
/**
* Returns the exact set of issues to check, or null to run the issues that are enabled
* by default plus any issues enabled via {@link #getEnable} and without issues disabled
* via {@link #getDisable}. If non-null, callers are allowed to modify this collection.
*/
@Override
@Nullable
@Optional
@Input
public Set<String> getCheck() {
return check;
}
/**
* Sets the <b>exact</b> set of issues to check.
* @param ids the set of issue id's to check
*/
public void setCheck(@NonNull Set<String> ids) {
check.addAll(ids);
}
/** Whether lint should set the exit code of the process if errors are found */
@Override
@Input
public boolean isAbortOnError() {
return abortOnError;
}
/** Sets whether lint should set the exit code of the process if errors are found */
public void setAbortOnError(boolean abortOnError) {
this.abortOnError = abortOnError;
}
/**
* Whether lint should display full paths in the error output. By default the paths
* are relative to the path lint was invoked from.
*/
@Override
@Input
public boolean isAbsolutePaths() {
return absolutePaths;
}
/**
* Sets whether lint should display full paths in the error output. By default the paths
* are relative to the path lint was invoked from.
*/
public void setAbsolutePaths(boolean absolutePaths) {
this.absolutePaths = absolutePaths;
}
/**
* Whether lint should include the source lines in the output where errors occurred
* (true by default)
*/
@Override
@Input
public boolean isNoLines() {
return this.noLines;
}
/**
* Sets whether lint should include the source lines in the output where errors occurred
* (true by default)
*/
public void setNoLines(boolean noLines) {
this.noLines = noLines;
}
/**
* Returns whether lint should be quiet (for example, not write informational messages
* such as paths to report files written)
*/
@Override
@Input
public boolean isQuiet() {
return quiet;
}
/**
* Sets whether lint should be quiet (for example, not write informational messages
* such as paths to report files written)
*/
public void setQuiet(boolean quiet) {
this.quiet = quiet;
}
/** Returns whether lint should check all warnings, including those off by default */
@Override
@Input
public boolean isCheckAllWarnings() {
return checkAllWarnings;
}
/** Sets whether lint should check all warnings, including those off by default */
public void setCheckAllWarnings(boolean warnAll) {
this.checkAllWarnings = warnAll;
}
/** Returns whether lint will only check for errors (ignoring warnings) */
@Override
@Input
public boolean isIgnoreWarnings() {
return ignoreWarnings;
}
/** Sets whether lint will only check for errors (ignoring warnings) */
public void setIgnoreWarnings(boolean noWarnings) {
this.ignoreWarnings = noWarnings;
}
/** Returns whether lint should treat all warnings as errors */
@Override
@Input
public boolean isWarningsAsErrors() {
return warningsAsErrors;
}
/** Sets whether lint should treat all warnings as errors */
public void setWarningsAsErrors(boolean allErrors) {
this.warningsAsErrors = allErrors;
}
/** Returns whether lint should include explanations for issue errors. (Note that
* HTML and XML reports intentionally do this unconditionally, ignoring this setting.) */
@Override
@Input
public boolean isExplainIssues() {
return explainIssues;
}
public void setExplainIssues(boolean explainIssues) {
this.explainIssues = explainIssues;
}
/**
* Returns whether lint should include all output (e.g. include all alternate
* locations, not truncating long messages, etc.)
*/
@Override
@Input
public boolean isShowAll() {
return showAll;
}
/**
* Sets whether lint should include all output (e.g. include all alternate
* locations, not truncating long messages, etc.)
*/
public void setShowAll(boolean showAll) {
this.showAll = showAll;
}
/**
* 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.
*/
@Override
@Input
public boolean isCheckReleaseBuilds() {
return checkReleaseBuilds;
}
public void setCheckReleaseBuilds(boolean checkReleaseBuilds) {
this.checkReleaseBuilds = checkReleaseBuilds;
}
/**
* Returns the default configuration file to use as a fallback
*/
@Override
@Optional @InputFile
public File getLintConfig() {
return lintConfig;
}
/** Whether we should write an text report. Default false. The location can be
* controlled by {@link #getTextOutput()}. */
@Override
@Input
public boolean getTextReport() {
return textReport;
}
public void setTextReport(boolean textReport) {
this.textReport = textReport;
}
public void setHtmlReport(boolean htmlReport) {
this.htmlReport = htmlReport;
}
public void setHtmlOutput(@NonNull File htmlOutput) {
this.htmlOutput = htmlOutput;
}
public void setXmlReport(boolean xmlReport) {
this.xmlReport = xmlReport;
}
public void setXmlOutput(@NonNull File xmlOutput) {
this.xmlOutput = xmlOutput;
}
/**
* The optional path to where a text report should be written. The special value
* "stdout" can be used to point to standard output.
*/
@Override
@Nullable
@Optional
@Input
public File getTextOutput() {
return textOutput;
}
/** Whether we should write an HTML report. Default true. The location can be
* controlled by {@link #getHtmlOutput()}. */
@Override
@Input
public boolean getHtmlReport() {
return htmlReport;
}
/** The optional path to where an HTML report should be written */
@Override
@Nullable
@Optional
@OutputFile
public File getHtmlOutput() {
return htmlOutput;
}
/** Whether we should write an XML report. Default true. The location can be
* controlled by {@link #getXmlOutput()}. */
@Override
@Input
public boolean getXmlReport() {
return xmlReport;
}
/** The optional path to where an XML report should be written */
@Override
@Nullable
@Optional
@OutputFile
public File getXmlOutput() {
return xmlOutput;
}
/**
* Sets the default config file to use as a fallback. This corresponds to a {@code lint.xml}
* file with severities etc to use when a project does not have more specific information.
*/
public void setLintConfig(@NonNull File lintConfig) {
this.lintConfig = lintConfig;
}
public void syncTo(
@NonNull LintCliClient client,
@NonNull LintCliFlags flags,
@Nullable String variantName,
@Nullable org.gradle.api.Project project,
boolean report) {
if (disable != null) {
flags.getSuppressedIds().addAll(disable);
}
if (enable != null) {
flags.getEnabledIds().addAll(enable);
}
if (check != null && !check.isEmpty()) {
flags.setExactCheckedIds(check);
}
flags.setSetExitCode(this.abortOnError);
flags.setFullPath(absolutePaths);
flags.setShowSourceLines(!noLines);
flags.setQuiet(quiet);
flags.setCheckAllWarnings(checkAllWarnings);
flags.setIgnoreWarnings(ignoreWarnings);
flags.setWarningsAsErrors(warningsAsErrors);
flags.setShowEverything(showAll);
flags.setDefaultConfiguration(lintConfig);
flags.setSeverityOverrides(severities);
flags.setExplainIssues(explainIssues);
if (report || flags.isFatalOnly() && this.abortOnError) {
if (textReport || flags.isFatalOnly()) {
File output = textOutput;
if (output == null) {
output = new File(flags.isFatalOnly() ? STDERR: STDOUT);
} else if (!output.isAbsolute() && !isStdOut(output) && !isStdErr(output)) {
output = project.file(output.getPath());
}
output = validateOutputFile(output);
Writer writer;
File file = null;
boolean closeWriter;
if (isStdOut(output)) {
writer = new PrintWriter(System.out, true);
closeWriter = false;
} else if (isStdErr(output)) {
writer = new PrintWriter(System.err, true);
closeWriter = false;
} else {
file = output;
try {
writer = new BufferedWriter(new FileWriter(output));
} catch (IOException e) {
throw new org.gradle.api.GradleException("Text invalid argument.", e);
}
closeWriter = true;
}
flags.getReporters().add(new TextReporter(client, flags, file, writer,
closeWriter));
}
if (htmlReport) {
File output = htmlOutput;
if (output == null || flags.isFatalOnly()) {
output = createOutputPath(project, variantName, ".html", flags.isFatalOnly());
} else if (!output.isAbsolute()) {
output = project.file(output.getPath());
}
output = validateOutputFile(output);
try {
flags.getReporters().add(new HtmlReporter(client, output));
} catch (IOException e) {
throw new GradleException("HTML invalid argument.", e);
}
}
if (xmlReport) {
File output = xmlOutput;
if (output == null || flags.isFatalOnly()) {
output = createOutputPath(project, variantName, DOT_XML, flags.isFatalOnly());
} else if (!output.isAbsolute()) {
output = project.file(output.getPath());
}
output = validateOutputFile(output);
try {
flags.getReporters().add(new XmlReporter(client, output));
} catch (IOException e) {
throw new org.gradle.api.GradleException("XML invalid argument.", e);
}
}
}
}
private static boolean isStdOut(@NonNull File output) {
return STDOUT.equals(output.getPath());
}
private static boolean isStdErr(@NonNull File output) {
return STDERR.equals(output.getPath());
}
@NonNull
private static File validateOutputFile(@NonNull File output) {
if (isStdOut(output) || isStdErr(output)) {
return output;
}
File parent = output.getParentFile();
if (!parent.exists()) {
parent.mkdirs();
}
output = output.getAbsoluteFile();
if (output.exists()) {
boolean delete = output.delete();
if (!delete) {
throw new org.gradle.api.GradleException("Could not delete old " + output);
}
}
if (output.getParentFile() != null && !output.getParentFile().canWrite()) {
throw new org.gradle.api.GradleException("Cannot write output file " + output);
}
return output;
}
private static File createOutputPath(
@NonNull org.gradle.api.Project project,
@NonNull String variantName,
@NonNull String extension,
boolean fatalOnly) {
StringBuilder base = new StringBuilder();
base.append(FD_OUTPUTS);
base.append(File.separator);
base.append("lint-results");
if (variantName != null) {
base.append("-");
base.append(variantName);
}
if (fatalOnly) {
base.append("-fatal");
}
base.append(extension);
return new File(project.getBuildDir(), base.toString());
}
/**
* An optional map of severity overrides. The map maps from issue id's to the corresponding
* severity to use, which must be "fatal", "error", "warning", or "ignore".
*
* @return a map of severity overrides, or null. The severities are one of the constants
* {@link #SEVERITY_FATAL}, {@link #SEVERITY_ERROR}, {@link #SEVERITY_WARNING},
* {@link #SEVERITY_INFORMATIONAL}, {@link #SEVERITY_IGNORE}
*/
@Override
@Nullable
public Map<String, Integer> getSeverityOverrides() {
if (severities == null || severities.isEmpty()) {
return null;
}
Map<String, Integer> map =
Maps.newHashMapWithExpectedSize(severities.size());
for (Map.Entry<String,Severity> entry : severities.entrySet()) {
map.put(entry.getKey(), convert(entry.getValue()));
}
return map;
}
// -- DSL Methods.
/**
* Adds the id to the set of issues to check.
*/
public void check(String id) {
check.add(id);
}
/**
* Adds the ids to the set of issues to check.
*/
public void check(String... ids) {
for (String id : ids) {
check(id);
}
}
/**
* Adds the id to the set of issues to enable.
*/
public void enable(String id) {
enable.add(id);
Issue issue = new BuiltinIssueRegistry().getIssue(id);
severities.put(id, issue != null ? issue.getDefaultSeverity() : WARNING);
}
/**
* Adds the ids to the set of issues to enable.
*/
public void enable(String... ids) {
for (String id : ids) {
enable(id);
}
}
/**
* Adds the id to the set of issues to enable.
*/
public void disable(String id) {
disable.add(id);
severities.put(id, IGNORE);
}
/**
* Adds the ids to the set of issues to enable.
*/
public void disable(String... ids) {
for (String id : ids) {
disable(id);
}
}
// For textOutput 'stdout' or 'stderr' (normally a file)
public void textOutput(String textOutput) {
this.textOutput = new File(textOutput);
}
// For textOutput file()
public void textOutput(File textOutput) {
this.textOutput = textOutput;
}
/**
* Adds a severity override for the given issues.
*/
public void fatal(String id) {
severities.put(id, FATAL);
}
/**
* Adds a severity override for the given issues.
*/
public void fatal(String... ids) {
for (String id : ids) {
fatal(id);
}
}
/**
* Adds a severity override for the given issues.
*/
public void error(String id) {
severities.put(id, ERROR);
}
/**
* Adds a severity override for the given issues.
*/
public void error(String... ids) {
for (String id : ids) {
error(id);
}
}
/**
* Adds a severity override for the given issues.
*/
public void warning(String id) {
severities.put(id, WARNING);
}
/**
* Adds a severity override for the given issues.
*/
public void warning(String... ids) {
for (String id : ids) {
warning(id);
}
}
/**
* Adds a severity override for the given issues.
*/
public void ignore(String id) {
severities.put(id, IGNORE);
}
/**
* Adds a severity override for the given issues.
*/
public void ignore(String... ids) {
for (String id : ids) {
ignore(id);
}
}
// Without these qualifiers, Groovy compilation will fail with "Apparent variable
// 'SEVERITY_FATAL' was found in a static scope but doesn't refer to a local variable,
// static field or class"
//@SuppressWarnings("UnnecessaryQualifiedReference")
private static int convert(Severity s) {
switch (s) {
case FATAL:
return com.android.builder.model.LintOptions.SEVERITY_FATAL;
case ERROR:
return com.android.builder.model.LintOptions.SEVERITY_ERROR;
case WARNING:
return com.android.builder.model.LintOptions.SEVERITY_WARNING;
case INFORMATIONAL:
return com.android.builder.model.LintOptions.SEVERITY_INFORMATIONAL;
case IGNORE:
default:
return com.android.builder.model.LintOptions.SEVERITY_IGNORE;
}
}
//@SuppressWarnings("UnnecessaryQualifiedReference")
private static Severity convert(int s) {
switch (s) {
case com.android.builder.model.LintOptions.SEVERITY_FATAL:
return FATAL;
case com.android.builder.model.LintOptions.SEVERITY_ERROR:
return ERROR;
case com.android.builder.model.LintOptions.SEVERITY_WARNING:
return WARNING;
case com.android.builder.model.LintOptions.SEVERITY_INFORMATIONAL:
return INFORMATIONAL;
case com.android.builder.model.LintOptions.SEVERITY_IGNORE:
default:
return IGNORE;
}
}
}