com.android.build.gradle.tasks.Lint Maven / Gradle / Ivy
Show all versions of gradle-core Show documentation
/*
* 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.tasks;
import static com.android.SdkConstants.VALUE_FALSE;
import static com.google.common.base.Preconditions.checkNotNull;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.gradle.internal.LintGradleClient;
import com.android.build.gradle.internal.TaskManager;
import com.android.build.gradle.internal.dsl.LintOptions;
import com.android.build.gradle.internal.scope.GlobalScope;
import com.android.build.gradle.internal.scope.TaskConfigAction;
import com.android.build.gradle.internal.scope.VariantScope;
import com.android.build.gradle.internal.tasks.BaseTask;
import com.android.builder.model.AndroidProject;
import com.android.builder.model.Variant;
import com.android.tools.lint.LintCliFlags;
import com.android.tools.lint.Reporter;
import com.android.tools.lint.Reporter.Stats;
import com.android.tools.lint.Warning;
import com.android.tools.lint.checks.BuiltinIssueRegistry;
import com.android.tools.lint.checks.GradleDetector;
import com.android.tools.lint.checks.UnusedResourceDetector;
import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.client.api.LintBaseline;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Severity;
import com.android.utils.Pair;
import com.android.utils.StringHelper;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.tasks.ParallelizableTask;
import org.gradle.api.tasks.TaskAction;
import org.gradle.tooling.provider.model.ToolingModelBuilder;
import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry;
@ParallelizableTask
public class Lint extends BaseTask {
/** Name of property used to enable {@link #MODEL_LIBRARIES} */
public static final String MODEL_LIBRARIES_PROPERTY = "lint.new-lib-model"; // for test access
/**
* Whether lint should attempt to do deep analysis of libraries. E.g. when
* building up the project graph, when it encounters an AndroidLibrary or JavaLibrary
* dependency, it should check if it's a local project, and if so recursively initialize
* the project with the local source paths etc of the library (in the past, this was not
* the case: it would naively just point to the library's resources and class files,
* which were the compiled outputs.
*
* The new behavior is clearly the correct behavior (see issue #194092), but since this
* is a risky fix, we're putting it behind a flag now and as soon as we get some real
* user testing, we should enable this by default and remove the old code.
*/
public static final boolean MODEL_LIBRARIES =
!VALUE_FALSE.equals(System.getProperty(MODEL_LIBRARIES_PROPERTY));
private static final Logger LOG = Logging.getLogger(Lint.class);
@Nullable private LintOptions lintOptions;
@Nullable private File sdkHome;
private boolean fatalOnly;
private ToolingModelBuilderRegistry toolingRegistry;
@Nullable private File reportsDir;
public void setLintOptions(@NonNull LintOptions lintOptions) {
this.lintOptions = lintOptions;
}
public void setSdkHome(@NonNull File sdkHome) {
this.sdkHome = sdkHome;
}
public void setToolingRegistry(ToolingModelBuilderRegistry toolingRegistry) {
this.toolingRegistry = toolingRegistry;
}
public void setReportsDir(@Nullable File reportDir) {
this.reportsDir = reportDir;
}
public void setFatalOnly(boolean fatalOnly) {
this.fatalOnly = fatalOnly;
}
@TaskAction
public void lint() throws IOException {
AndroidProject modelProject = createAndroidProject(getProject());
if (getVariantName() != null && !getVariantName().isEmpty()) {
for (Variant variant : modelProject.getVariants()) {
if (variant.getName().equals(getVariantName())) {
lintSingleVariant(modelProject, variant);
}
}
} else {
lintAllVariants(modelProject);
}
}
/**
* Runs lint individually on all the variants, and then compares the results
* across variants and reports these
*/
public void lintAllVariants(@NonNull AndroidProject modelProject) throws IOException {
// In the Gradle integration we iterate over each variant, and
// attribute unused resources to each variant, so don't make
// each variant run go and inspect the inactive variant sources
UnusedResourceDetector.sIncludeInactiveReferences = false;
Map> warningMap = Maps.newHashMap();
List baselines = Lists.newArrayList();
for (Variant variant : modelProject.getVariants()) {
Pair,LintBaseline> pair = runLint(modelProject, variant, false);
List warnings = pair.getFirst();
warningMap.put(variant, warnings);
LintBaseline baseline = pair.getSecond();
if (baseline != null) {
baselines.add(baseline);
}
}
// Compute error matrix
boolean quiet = false;
if (lintOptions != null) {
quiet = lintOptions.isQuiet();
}
for (Map.Entry> entry : warningMap.entrySet()) {
Variant variant = entry.getKey();
List warnings = entry.getValue();
if (!fatalOnly && !quiet) {
LOG.warn("Ran lint on variant {}: {} issues found",
variant.getName(), warnings.size());
}
}
List mergedWarnings = LintGradleClient.merge(warningMap, modelProject);
int errorCount = 0;
int warningCount = 0;
for (Warning warning : mergedWarnings) {
if (warning.severity == Severity.ERROR || warning.severity == Severity.FATAL) {
errorCount++;
} else if (warning.severity == Severity.WARNING) {
warningCount++;
}
}
// We pick the first variant to generate the full report and don't generate if we don't
// have any variants.
if (!modelProject.getVariants().isEmpty()) {
Set allVariants = Sets.newTreeSet(
(v1, v2) -> v1.getName().compareTo(v2.getName()));
allVariants.addAll(modelProject.getVariants());
Variant variant = allVariants.iterator().next();
IssueRegistry registry = new BuiltinIssueRegistry();
LintCliFlags flags = new LintCliFlags();
LintGradleClient client = new LintGradleClient(
registry, flags, getProject(), modelProject,
sdkHome, variant, getBuildTools());
syncOptions(lintOptions, client, flags, null, getProject(), reportsDir,
true, fatalOnly);
// Compute baseline counts. This is tricky because an error could appear in
// multiple variants, and in that case it should only be counted as filtered
// from the baseline once, but if there are errors that appear only in individual
// variants, then they shouldn't count as one. To correctly account for this we
// need to ask the baselines themselves to merge their results. Right now they
// only contain the remaining (fixed) issues; to address this we'd need to move
// found issues to a different map such that at the end we can successively
// merge the baseline instances together to a final one which has the full set
// of filtered and remaining counts.
int baselineErrorCount = 0;
int baselineWarningCount = 0;
int fixedCount = 0;
if (!baselines.isEmpty()) {
// Figure out the actual overlap; later I could stash these into temporary
// objects to compare
// For now just combine them in a dumb way
for (LintBaseline baseline : baselines) {
baselineErrorCount = Math.max(baselineErrorCount,
baseline.getFoundErrorCount());
baselineWarningCount = Math.max(baselineWarningCount,
baseline.getFoundWarningCount());
fixedCount = Math.max(fixedCount, baseline.getFixedCount());
}
}
Stats stats = new Stats(errorCount, warningCount, baselineErrorCount,
baselineWarningCount, fixedCount);
for (Reporter reporter : flags.getReporters()) {
reporter.write(stats, mergedWarnings);
}
File baselineFile = flags.getBaselineFile();
if (baselineFile != null && !baselineFile.exists()) {
File dir = baselineFile.getParentFile();
boolean ok = true;
if (!dir.isDirectory()) {
ok = dir.mkdirs();
}
if (!ok) {
System.err.println("Couldn't create baseline folder " + dir);
} else {
Reporter reporter = Reporter.createXmlReporter(client, baselineFile, true);
reporter.write(stats, mergedWarnings);
System.err.println("Created baseline file " + baselineFile);
System.err.println("(Also breaking build in case this was not intentional.)");
String message = ""
+ "Created baseline file " + baselineFile + "\n"
+ "\n"
+ "Also breaking the build in case this was not intentional. If you\n"
+ "deliberately created the baseline file, re-run the build and this\n"
+ "time it should succeed without warnings.\n"
+ "\n"
+ "If not, investigate the baseline path in the lintOptions config\n"
+ "or verify that the baseline file has been checked into version\n"
+ "control.\n";
throw new GradleException(message);
}
}
if (baselineErrorCount > 0 || baselineWarningCount > 0) {
System.out.println(String.format("%1$s were filtered out because "
+ "they were listed in the baseline file, %2$s\n",
LintUtils.describeCounts(baselineErrorCount, baselineWarningCount, false),
baselineFile));
}
if (fixedCount > 0) {
System.out.println(String.format("%1$d errors/warnings were listed in the "
+ "baseline file (%2$s) but not found in the project; perhaps they have "
+ "been fixed?\n", fixedCount, baselineFile));
}
if (flags.isSetExitCode() && errorCount > 0) {
abort();
}
}
}
private void abort() {
String message;
if (fatalOnly) {
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
*/
public void lintSingleVariant(@NonNull AndroidProject modelProject, @NonNull Variant variant) {
runLint(modelProject, variant, true);
}
/** Runs lint on the given variant and returns the set of warnings */
private Pair,LintBaseline> runLint(
/*
* Note that as soon as we disable {@link #MODEL_LIBRARIES} this is
* unused and we can delete it and all the callers passing it recursively
*/
@NonNull AndroidProject modelProject,
@NonNull Variant variant,
boolean report) {
IssueRegistry registry = createIssueRegistry();
LintCliFlags flags = new LintCliFlags();
LintGradleClient client = new LintGradleClient(registry, flags, getProject(), modelProject,
sdkHome, variant, getBuildTools());
if (fatalOnly) {
if (lintOptions != null && !lintOptions.isCheckReleaseBuilds()) {
return Pair.of(Collections.emptyList(), null);
}
flags.setFatalOnly(true);
}
if (lintOptions != null) {
syncOptions(lintOptions, client, flags, variant, getProject(), reportsDir, report,
fatalOnly);
}
if (!report || fatalOnly) {
flags.setQuiet(true);
}
flags.setWriteBaselineIfMissing(report);
Pair,LintBaseline> warnings;
try {
warnings = client.run(registry);
} catch (IOException e) {
throw new GradleException("Invalid arguments.", e);
}
if (report && client.haveErrors() && flags.isSetExitCode()) {
abort();
}
return warnings;
}
private static void syncOptions(
@NonNull LintOptions options,
@NonNull LintGradleClient client,
@NonNull LintCliFlags flags,
@Nullable Variant variant,
@NonNull Project project,
@Nullable File reportsDir,
boolean report,
boolean fatalOnly) {
options.syncTo(client, flags, variant != null ? variant.getName() : null, project,
reportsDir, report);
boolean displayEmpty = !(fatalOnly || flags.isQuiet());
for (Reporter reporter : flags.getReporters()) {
reporter.setDisplayEmpty(displayEmpty);
}
}
private AndroidProject createAndroidProject(@NonNull Project gradleProject) {
String modelName = AndroidProject.class.getName();
ToolingModelBuilder modelBuilder = toolingRegistry.getBuilder(modelName);
assert modelBuilder != null;
return (AndroidProject) modelBuilder.buildAll(modelName, gradleProject);
}
private static BuiltinIssueRegistry createIssueRegistry() {
return new LintGradleIssueRegistry();
}
// Issue registry when Lint is run inside Gradle: we replace the Gradle
// detector with a local implementation which directly references Groovy
// for parsing. In Studio on the other hand, the implementation is replaced
// by a PSI-based check. (This is necessary for now since we don't have a
// tool-agnostic API for the Groovy AST and we don't want to add a 6.3MB dependency
// on Groovy itself quite yet.
public static class LintGradleIssueRegistry extends BuiltinIssueRegistry {
private boolean mInitialized;
public LintGradleIssueRegistry() {
}
@NonNull
@Override
public List getIssues() {
List issues = super.getIssues();
if (!mInitialized) {
mInitialized = true;
for (Issue issue : issues) {
if (issue.getImplementation().getDetectorClass() == GradleDetector.class) {
issue.setImplementation(GroovyGradleDetector.IMPLEMENTATION);
}
}
}
return issues;
}
}
public static class ConfigAction implements TaskConfigAction {
private final VariantScope scope;
public ConfigAction(@NonNull VariantScope scope) {
this.scope = scope;
}
@Override
@NonNull
public String getName() {
return scope.getTaskName("lint");
}
@Override
@NonNull
public Class getType() {
return Lint.class;
}
@Override
public void execute(@NonNull Lint lint) {
GlobalScope globalScope = scope.getGlobalScope();
lint.setLintOptions(globalScope.getExtension().getLintOptions());
File sdkFolder = globalScope.getSdkHandler().getSdkFolder();
if (sdkFolder != null) {
lint.setSdkHome(sdkFolder);
}
lint.setAndroidBuilder(globalScope.getAndroidBuilder());
lint.setVariantName(scope.getVariantConfiguration().getFullName());
lint.setToolingRegistry(globalScope.getToolingRegistry());
lint.setReportsDir(globalScope.getReportsDir());
lint.setDescription("Runs lint on the " + StringHelper
.capitalize(scope.getVariantConfiguration().getFullName()) + " build.");
lint.setGroup(JavaBasePlugin.VERIFICATION_GROUP);
}
}
public static class VitalConfigAction implements TaskConfigAction {
private final VariantScope scope;
public VitalConfigAction(@NonNull VariantScope scope) {
this.scope = scope;
}
@NonNull
@Override
public String getName() {
return scope.getTaskName("lintVital");
}
@NonNull
@Override
public Class getType() {
return Lint.class;
}
@Override
public void execute(@NonNull Lint task) {
String variantName = scope.getVariantData().getVariantConfiguration().getFullName();
GlobalScope globalScope = scope.getGlobalScope();
task.setAndroidBuilder(globalScope.getAndroidBuilder());
// TODO: Make this task depend on lintCompile too (resolve initialization order first)
task.setLintOptions(globalScope.getExtension().getLintOptions());
task.setSdkHome(checkNotNull(
globalScope.getSdkHandler().getSdkFolder(), "SDK not set up."));
task.setVariantName(variantName);
task.setToolingRegistry(globalScope.getToolingRegistry());
task.setReportsDir(globalScope.getReportsDir());
task.setFatalOnly(true);
task.setDescription(
"Runs lint on just the fatal issues in the " + variantName + " build.");
}
}
public static class GlobalConfigAction implements TaskConfigAction {
private final GlobalScope globalScope;
public GlobalConfigAction(GlobalScope globalScope) {
this.globalScope = globalScope;
}
@NonNull
@Override
public String getName() {
return TaskManager.LINT;
}
@NonNull
@Override
public Class getType() {
return Lint.class;
}
@Override
public void execute(@NonNull Lint lintTask) {
lintTask.setDescription("Runs lint on all variants.");
lintTask.setVariantName("");
lintTask.setGroup(JavaBasePlugin.VERIFICATION_GROUP);
lintTask.setLintOptions(globalScope.getExtension().getLintOptions());
File sdkFolder = globalScope.getSdkHandler().getSdkFolder();
if (sdkFolder != null) {
lintTask.setSdkHome(sdkFolder);
}
lintTask.setToolingRegistry(globalScope.getToolingRegistry());
lintTask.setReportsDir(globalScope.getReportsDir());
lintTask.setAndroidBuilder(globalScope.getAndroidBuilder());
}
}
}