From e8a18c6ee8e4c93df093c00224fdd3b024ed5774 Mon Sep 17 00:00:00 2001 From: Tor Norbye Date: Mon, 16 Sep 2013 15:57:55 -0700 Subject: Update lint support First, this changeset creates lint-style projects for each Gradle model app and library (and sets reporting flags) such that for example errors in AAR libraries are no longer reported. Second, it creates a new lint task which rather than depend on individual lint tasks (for running lint on a given variant), runs lint through all the variants on its own and then merges the errors together such that it can report for each error whether it's specific to a set of variants and if so which ones. Change-Id: Ia2370d546515d4bcb104d1eec37a0394ac703704 --- .../com/android/build/gradle/BasePlugin.groovy | 110 ++---- .../build/gradle/internal/LintGradleClient.java | 119 +++++- .../build/gradle/internal/LintGradleProject.java | 411 +++++++++++++++++++-- .../build/gradle/internal/LintGradleRequest.java | 63 ++++ .../com/android/build/gradle/tasks/Lint.groovy | 273 +++++++------- 5 files changed, 742 insertions(+), 234 deletions(-) create mode 100644 gradle/src/main/groovy/com/android/build/gradle/internal/LintGradleRequest.java (limited to 'gradle') diff --git a/gradle/src/main/groovy/com/android/build/gradle/BasePlugin.groovy b/gradle/src/main/groovy/com/android/build/gradle/BasePlugin.groovy index 471b581..a01bf7b 100644 --- a/gradle/src/main/groovy/com/android/build/gradle/BasePlugin.groovy +++ b/gradle/src/main/groovy/com/android/build/gradle/BasePlugin.groovy @@ -15,6 +15,7 @@ */ package com.android.build.gradle + import com.android.annotations.NonNull import com.android.annotations.Nullable import com.android.build.gradle.api.AndroidSourceSet @@ -131,6 +132,7 @@ import static com.android.builder.BuilderConstants.FD_INSTRUMENT_TESTS import static com.android.builder.BuilderConstants.FD_REPORTS import static com.android.builder.BuilderConstants.INSTRUMENT_TEST import static java.io.File.separator + /** * Base class for all Android plugins */ @@ -216,9 +218,17 @@ public abstract class BasePlugin { mainPreBuild = project.tasks.create("preBuild") - lint = project.tasks.create("lint") + lint = project.tasks.create("lint", Lint) lint.description = "Runs lint on all variants." lint.group = JavaBasePlugin.VERIFICATION_GROUP + lint.setPlugin(this) + int count = variantDataList.size() + for (int i = 0 ; i < count ; i++) { + final BaseVariantData baseVariantData = variantDataList.get(i) + if (isLintVariant(baseVariantData)) { + lint.dependsOn baseVariantData.javaCompileTask + } + } project.tasks.check.dependsOn lint project.afterEvaluate { @@ -894,89 +904,35 @@ public abstract class BasePlugin { } } - protected void createLintTasks() { - File configFile = project.file("$project.projectDir/lint.xml") + /** Is the given variant relevant for lint? */ + private static boolean isLintVariant(@NonNull BaseVariantData baseVariantData) { + // Only create lint targets for variants like debug and release, not debugTest + VariantConfiguration config = baseVariantData.variantConfiguration + return config.getType() != VariantConfiguration.Type.TEST; + } - // for each variant setup a lint task + // Add tasks for running lint on individual variants. We've already added a + // lint task earlier which runs on all variants. + protected void createLintTasks() { int count = variantDataList.size() for (int i = 0 ; i < count ; i++) { final BaseVariantData baseVariantData = variantDataList.get(i) + if (!isLintVariant(baseVariantData)) { + continue; + } + String variantName = baseVariantData.variantConfiguration.fullName - Lint lintCheck = project.tasks.create("lint" + variantName, Lint) + def capitalizedVariantName = variantName.capitalize() + Task lintCheck = project.tasks.create("lint" + capitalizedVariantName, Lint) lintCheck.dependsOn baseVariantData.javaCompileTask, lintCompile - lint.dependsOn lintCheck + // Note that we don't do "lint.dependsOn lintCheck"; the "lint" target will + // on its own run through all variants (and compare results), it doesn't delegate + // to the individual tasks (since it needs to coordinate data collection and + // reporting) lintCheck.setPlugin(this) - - String outputName = "$project.buildDir/lint/" + variantName - VariantConfiguration config = baseVariantData.variantConfiguration - List depList = config.getAllLibraries() - List> javaSource = Lists.newArrayList() - List> resourceSource = Lists.newArrayList() - - // set the java and res source of this variant's flavors - Iterator flavorSources = config.flavorSourceProviders.iterator() - SourceProvider source - while (flavorSources.hasNext()) { - source = flavorSources.next() - javaSource.add(source.javaDirectories) - resourceSource.add(source.resDirectories) - } - - javaSource.add(config.defaultSourceSet.javaDirectories) - if (config.getType() != VariantConfiguration.Type.TEST) { - javaSource.add(config.buildTypeSourceSet.javaDirectories) - } - SourceProvider sourceProvider = config.variantSourceProvider - if (sourceProvider != null) { - javaSource.add(sourceProvider.javaDirectories) - resourceSource.add(sourceProvider.resDirectories) - } - sourceProvider = config.multiFlavorSourceProvider - if (sourceProvider != null) { - javaSource.add(sourceProvider.javaDirectories) - resourceSource.add(sourceProvider.resDirectories) - } - - resourceSource.add(config.defaultSourceSet.resDirectories) - if (config.getType() != VariantConfiguration.Type.TEST) { - resourceSource.add(config.buildTypeSourceSet.resDirectories) - } - File genRes = baseVariantData.renderscriptCompileTask.getResOutputDir() - if (genRes != null) { - resourceSource.add( - Collections.singleton(genRes) - ) - } - - lintCheck.doFirst { - File compiledLintJar = project.file("$project.buildDir/lint/lint.jar"); - if (compiledLintJar.exists()) { - lintCheck.addCustomRule(compiledLintJar); - } - // set custom lint rules from library dependencies - for (LibraryDependency dep : depList) { - File depLintJar = dep.getLintJar() - if (depLintJar.exists()) { - lintCheck.addCustomRule(depLintJar) - } - } - - // set lint output options - lintCheck.setHtmlOutput(project.file(outputName + "Output.html")) - lintCheck.setXmlOutput(project.file(outputName + "Output.xml")) - lintCheck.setQuiet() - - // set lint enabled checks - if (configFile.exists()) { - lintCheck.setConfig(configFile) - } - - // set lint project options - lintCheck.setSources(javaSource) - lintCheck.setClasspath( - "$project.buildDir/classes/$baseVariantData.variantConfiguration.dirName") - lintCheck.setLintResources(resourceSource) - } + lintCheck.setVariantName(variantName) + lintCheck.description = "Runs lint on the " + capitalizedVariantName + " build" + lintCheck.group = JavaBasePlugin.VERIFICATION_GROUP } } diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/LintGradleClient.java b/gradle/src/main/groovy/com/android/build/gradle/internal/LintGradleClient.java index 6ebb37f..3bf7b8d 100644 --- a/gradle/src/main/groovy/com/android/build/gradle/internal/LintGradleClient.java +++ b/gradle/src/main/groovy/com/android/build/gradle/internal/LintGradleClient.java @@ -17,24 +17,45 @@ package com.android.build.gradle.internal; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.build.gradle.BasePlugin; +import com.android.builder.model.AndroidProject; +import com.android.builder.model.Variant; import com.android.tools.lint.LintCliClient; import com.android.tools.lint.LintCliFlags; +import com.android.tools.lint.Warning; +import com.android.tools.lint.client.api.IssueRegistry; +import com.android.tools.lint.client.api.LintRequest; +import com.android.tools.lint.detector.api.Issue; import com.android.tools.lint.detector.api.Project; import java.io.File; +import java.io.IOException; +import java.util.Collections; import java.util.List; +import java.util.Map; public class LintGradleClient extends LintCliClient { + private final AndroidProject mModelProject; + private final String mVariantName; + private final BasePlugin mPlugin; private List mCustomRules = Lists.newArrayList(); - private BasePlugin mPlugin; - public LintGradleClient(@NonNull LintCliFlags flags, @NonNull BasePlugin plugin) { + public LintGradleClient( + @NonNull IssueRegistry registry, + @NonNull LintCliFlags flags, + @NonNull BasePlugin plugin, + @NonNull AndroidProject modelProject, + @Nullable String variantName) { super(flags); mPlugin = plugin; + mModelProject = modelProject; + mVariantName = variantName; + mRegistry = registry; } @NonNull @@ -53,7 +74,9 @@ public class LintGradleClient extends LintCliClient { @Override protected Project createProject(@NonNull File dir, @NonNull File referenceDir) { - return new LintGradleProject(this, dir, referenceDir); + // Should not be called by lint since we supply an explicit set of projects + // to the LintRequest + throw new IllegalStateException(); } @Override @@ -64,4 +87,94 @@ public class LintGradleClient extends LintCliClient { } return super.getSdkHome(); } + + @Override + @NonNull + protected LintRequest createLintRequest(@NonNull List files) { + return new LintGradleRequest(this, mModelProject, mPlugin, mVariantName, files); + } + + /** Run lint with the given registry and return the resulting warnings */ + @NonNull + public List run(@NonNull IssueRegistry registry) throws IOException { + run(registry, Collections.emptyList()); + return mWarnings; + } + + /** + * Given a list of results from separate variants, merge them into a single + * list of warnings, and mark their + * @param warningMap a map from variant to corresponding warnings + * @param project the project model + * @return a merged list of issues + */ + @NonNull + public static List merge( + @NonNull Map> warningMap, + @NonNull AndroidProject project) { + // Easy merge? + if (warningMap.size() == 1) { + return warningMap.values().iterator().next(); + } + int maxCount = 0; + for (List warnings : warningMap.values()) { + int size = warnings.size(); + maxCount = Math.max(size, maxCount); + } + if (maxCount == 0) { + return Collections.emptyList(); + } + + int totalVariantCount = project.getVariants().size(); + + List merged = Lists.newArrayListWithExpectedSize(2 * maxCount); + + // Map fro issue to message to line number to file name to canonical warning + Map>>> map = + Maps.newHashMapWithExpectedSize(2 * maxCount); + + for (Map.Entry> entry : warningMap.entrySet()) { + Variant variant = entry.getKey(); + List warnings = entry.getValue(); + for (Warning warning : warnings) { + Map>> messageMap = map.get(warning.issue); + if (messageMap == null) { + messageMap = Maps.newHashMap(); + map.put(warning.issue, messageMap); + } + Map> lineMap = messageMap.get(warning.message); + if (lineMap == null) { + lineMap = Maps.newHashMap(); + messageMap.put(warning.message, lineMap); + } + Map fileMap = lineMap.get(warning.line); + if (fileMap == null) { + fileMap = Maps.newHashMap(); + lineMap.put(warning.line, fileMap); + } + String fileName = warning.file != null ? warning.file.getName() : ""; + Warning canonical = fileMap.get(fileName); + if (canonical == null) { + canonical = warning; + fileMap.put(fileName, canonical); + canonical.variants = Sets.newHashSet(); + canonical.gradleProject = project; + } + merged.add(canonical); + canonical.variants.add(variant); + } + } + + // Clear out variants on any nodes that don't define all + for (Warning warning : merged) { + if (warning.variants != null && warning.variants.size() == totalVariantCount) { + // If this error is present in all variants, just clear it out + warning.variants = null; + } + + } + + Collections.sort(merged); + return merged; + } } diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/LintGradleProject.java b/gradle/src/main/groovy/com/android/build/gradle/internal/LintGradleProject.java index 6724268..fb50f00 100644 --- a/gradle/src/main/groovy/com/android/build/gradle/internal/LintGradleProject.java +++ b/gradle/src/main/groovy/com/android/build/gradle/internal/LintGradleProject.java @@ -1,32 +1,86 @@ package com.android.build.gradle.internal; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + import com.android.annotations.NonNull; -import com.android.build.gradle.BasePlugin; -import com.android.build.gradle.LibraryPlugin; -import com.android.build.gradle.internal.model.ModelBuilder; +import com.android.annotations.Nullable; import com.android.builder.model.AndroidLibrary; import com.android.builder.model.AndroidProject; +import com.android.builder.model.BuildTypeContainer; +import com.android.builder.model.Dependencies; +import com.android.builder.model.ProductFlavor; +import com.android.builder.model.ProductFlavorContainer; +import com.android.builder.model.SourceProvider; import com.android.builder.model.Variant; -import com.android.tools.lint.client.api.LintClient; +import com.android.sdklib.AndroidTargetHash; +import com.android.sdklib.AndroidVersion; import com.android.tools.lint.detector.api.Project; +import com.android.utils.Pair; import java.io.File; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.List; +import java.util.Set; -public class LintGradleProject extends Project { - private AndroidProject mProject; +import static java.io.File.separatorChar; - LintGradleProject( - @NonNull LintClient client, +/** + * An implementation of Lint's {@link Project} class wrapping a Gradle model (project or + * library) + */ +public class LintGradleProject extends Project { + private LintGradleProject( + @NonNull LintGradleClient client, @NonNull File dir, @NonNull File referenceDir) { super(client, dir, referenceDir); - mGradleProject = true; mMergeManifests = true; - mDirectLibraries = Collections.emptyList(); + mDirectLibraries = Lists.newArrayList(); + } + + /** + * Creates a {@link com.android.build.gradle.internal.LintGradleProject} from + * the given {@link com.android.builder.model.AndroidProject} definition for + * a given {@link com.android.builder.model.Variant}, and returns it along with + * a set of lint custom rule jars applicable for the given model project. + * + * @param client the client + * @param project the model project + * @param variant the variant + * @param gradleProject the gradle project + * @return a pair of new project and list of custom rule jars + */ + @NonNull + public static Pair> create( + @NonNull LintGradleClient client, + @NonNull AndroidProject project, + @NonNull Variant variant, + @NonNull org.gradle.api.Project gradleProject) { + File dir = gradleProject.getRootDir(); + AppGradleProject lintProject = new AppGradleProject(client, dir, + dir, project, variant); + + List customRules = Lists.newArrayList(); + File appLintJar = new File(gradleProject.getBuildDir(), + "lint" + separatorChar + "lint.jar"); + if (appLintJar.exists()) { + customRules.add(appLintJar); + } + + Set libraries = Sets.newHashSet(); + Dependencies dependencies = variant.getMainArtifact().getDependencies(); + for (AndroidLibrary library : dependencies.getLibraries()) { + lintProject.addDirectLibrary(createLibrary(client, library, libraries, customRules)); + } + + return Pair.>of(lintProject, customRules); } + @Override protected void initialize() { // Deliberately not calling super; that code is for ADT compatibility @@ -37,33 +91,330 @@ public class LintGradleProject extends Project { return true; } - @Override - public boolean isLibrary() { - LintGradleClient client = (LintGradleClient) mClient; - BasePlugin plugin = client.getPlugin(); - return plugin instanceof LibraryPlugin; + void addDirectLibrary(@NonNull Project project) { + mDirectLibraries.add(project); } - @Override - public AndroidProject getGradleProjectModel() { - if (mProject == null) { - LintGradleClient client = (LintGradleClient) mClient; - BasePlugin plugin = client.getPlugin(); - String modelName = AndroidProject.class.getName(); - ModelBuilder builder = new ModelBuilder(); - mProject = (AndroidProject) builder.buildAll(modelName, plugin.getProject()); + @NonNull + private static LibraryProject createLibrary(@NonNull LintGradleClient client, + @NonNull AndroidLibrary library, + @NonNull Set seen, List customRules) { + seen.add(library); + File dir = library.getFolder(); + LibraryProject project = new LibraryProject(client, dir, dir, library); + + File ruleJar = library.getLintJar(); + if (ruleJar.exists()) { + customRules.add(ruleJar); + } + + for (AndroidLibrary dependent : library.getLibraryDependencies()) { + if (!seen.contains(dependent)) { + project.addDirectLibrary(createLibrary(client, dependent, seen, customRules)); + } } - return mProject; + return project; } - @Override - public AndroidLibrary getGradleLibraryModel() { - return null; + private static class AppGradleProject extends LintGradleProject { + private AndroidProject mProject; + private Variant mVariant; + private List mProviders; + + private AppGradleProject( + @NonNull LintGradleClient client, + @NonNull File dir, + @NonNull File referenceDir, + @NonNull AndroidProject project, + @NonNull Variant variant) { + super(client, dir, referenceDir); + + mProject = project; + mVariant = variant; + } + + @Override + public boolean isLibrary() { + return mProject.isLibrary(); + } + + @Override + public AndroidProject getGradleProjectModel() { + return mProject; + } + + @Override + public Variant getCurrentVariant() { + return mVariant; + } + + private List getSourceProviders() { + if (mProviders == null) { + List providers = Lists.newArrayList(); + + SourceProvider defaultProvider = mProject.getDefaultConfig().getSourceProvider(); + if (defaultProvider != null) { + providers.add(defaultProvider); + } + + SourceProvider sourceProvider = mVariant.getMainArtifact().getVariantSourceProvider(); + if (sourceProvider != null) { + providers.add(sourceProvider); + } + + sourceProvider = mVariant.getMainArtifact().getMultiFlavorSourceProvider(); + if (sourceProvider != null) { + providers.add(sourceProvider); + } + + for (String flavorName : mVariant.getProductFlavors()) { + for (ProductFlavorContainer flavor : mProject.getProductFlavors()) { + if (flavorName.equals(flavor.getProductFlavor().getName())) { + SourceProvider provider = flavor.getSourceProvider(); + if (provider != null) { + providers.add(provider); + } + break; + } + } + } + + String buildTypeName = mVariant.getBuildType(); + for (BuildTypeContainer buildType : mProject.getBuildTypes()) { + if (buildTypeName.equals(buildType.getBuildType().getName())) { + SourceProvider provider = buildType.getSourceProvider(); + if (provider != null) { + providers.add(provider); + } + break; + } + } + mProviders = providers; + } + + return mProviders; + } + + @NonNull + @Override + public List getManifestFiles() { + if (mManifestFiles == null) { + mManifestFiles = Lists.newArrayList(); + for (SourceProvider provider : getSourceProviders()) { + File manifestFile = provider.getManifestFile(); + if (manifestFile.exists()) { // model returns path whether or not it exists + mManifestFiles.add(manifestFile); + } + } + } + + return mManifestFiles; + } + + @Override + public List getProguardFiles() { + if (mProguardFiles == null) { + ProductFlavor flavor = mProject.getDefaultConfig().getProductFlavor(); + mProguardFiles = Lists.newArrayList(); + mProguardFiles.addAll(flavor.getProguardFiles()); + // TODO: This is currently broken: + //mProguardFiles.addAll(flavor.getConsumerProguardFiles()); + // It will throw + //org.gradle.tooling.model.UnsupportedMethodException: Unsupported method: BaseConfig.getConsumerProguardFiles(). + // The version of Gradle you connect to does not support that method. + // To resolve the problem you can change/upgrade the target version of Gradle you connect to. + // Alternatively, you can ignore this exception and read other information from the model. + //at org.gradle.tooling.model.internal.Exceptions.unsupportedMethod(Exceptions.java:33) + //at org.gradle.tooling.internal.adapter.ProtocolToModelAdapter$InvocationHandlerImpl.invoke(ProtocolToModelAdapter.java:239) + //at com.sun.proxy.$Proxy129.getConsumerProguardFiles(Unknown Source) + } + + return mProguardFiles; + } + + @NonNull + @Override + public List getResourceFolders() { + if (mResourceFolders == null) { + mResourceFolders = Lists.newArrayList(); + for (SourceProvider provider : getSourceProviders()) { + Collection resDirs = provider.getResDirectories(); + for (File res : resDirs) { + if (res.exists()) { // model returns path whether or not it exists + mResourceFolders.add(res); + } + } + } + } + + return mResourceFolders; + } + + @NonNull + @Override + public List getJavaSourceFolders() { + if (mJavaSourceFolders == null) { + mJavaSourceFolders = Lists.newArrayList(); + for (SourceProvider provider : getSourceProviders()) { + Collection resDirs = provider.getJavaDirectories(); + for (File res : resDirs) { + if (res.exists()) { // model returns path whether or not it exists + mJavaSourceFolders.add(res); + } + } + } + } + + return mJavaSourceFolders; + } + + @NonNull + @Override + public List getJavaClassFolders() { + if (mJavaClassFolders == null) { + mJavaClassFolders = new ArrayList(1); + File outputClassFolder = mVariant.getMainArtifact().getClassesFolder(); + if (outputClassFolder != null && outputClassFolder.exists()) { + mJavaClassFolders.add(outputClassFolder); + } + } + + return mJavaClassFolders; + } + + @Override + public List getJavaLibraries() { + if (mJavaLibraries == null) { + Collection jars = mVariant.getMainArtifact().getDependencies().getJars(); + mJavaLibraries = Lists.newArrayListWithExpectedSize(jars.size()); + for (File jar : jars) { + if (jar.exists()) { + mJavaLibraries.add(jar); + } + } + } + return mJavaLibraries; + } + + @Nullable + @Override + public String getPackage() { + // TODO: Merge with manifest + return mProject.getDefaultConfig().getProductFlavor().getPackageName(); + } + + @Override + public int getMinSdk() { + // TODO: Merge with manifest + return mProject.getDefaultConfig().getProductFlavor().getMinSdkVersion(); + } + + @Override + public int getTargetSdk() { + // TODO: Merge with manifest + return mProject.getDefaultConfig().getProductFlavor().getTargetSdkVersion(); + } + + @Override + public int getBuildSdk() { + String compileTarget = mProject.getCompileTarget(); + if (compileTarget != null) { + AndroidVersion version = AndroidTargetHash.getPlatformVersion(compileTarget); + if (version != null) { + return version.getApiLevel(); + } + } + + return super.getBuildSdk(); + } } - @Override - public Variant getCurrentVariant() { - return null; + private static class LibraryProject extends LintGradleProject { + private AndroidLibrary mLibrary; + + private LibraryProject( + @NonNull LintGradleClient client, + @NonNull File dir, + @NonNull File referenceDir, + @NonNull AndroidLibrary library) { + super(client, dir, referenceDir); + mLibrary = library; + + // TODO: Make sure we don't use this project for any source library projects! + mReportIssues = false; + } + + @Override + public boolean isLibrary() { + return true; + } + + @Override + public AndroidLibrary getGradleLibraryModel() { + return mLibrary; + } + + @Override + public Variant getCurrentVariant() { + return null; + } + + @NonNull + @Override + public List getManifestFiles() { + if (mManifestFiles == null) { + File manifest = mLibrary.getManifest(); + if (manifest.exists()) { + mManifestFiles = Collections.singletonList(manifest); + } else { + mManifestFiles = Collections.emptyList(); + } + } + + return mManifestFiles; + } + + @NonNull + @Override + public List getResourceFolders() { + if (mResourceFolders == null) { + File folder = mLibrary.getResFolder(); + if (folder.exists()) { + mResourceFolders = Collections.singletonList(folder); + } else { + mResourceFolders = Collections.emptyList(); + } + } + + return mResourceFolders; + } + + @NonNull + @Override + public List getJavaSourceFolders() { + return Collections.emptyList(); + } + + @NonNull + @Override + public List getJavaClassFolders() { + return Collections.emptyList(); + } + + @NonNull + @Override + public List getJavaLibraries() { + if (mJavaLibraries == null) { + File jarFile = mLibrary.getJarFile(); + if (jarFile.exists()) { + mJavaLibraries = Collections.singletonList(jarFile); + } else { + mJavaLibraries = Collections.emptyList(); + } + } + + return mJavaLibraries; + } } } diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/LintGradleRequest.java b/gradle/src/main/groovy/com/android/build/gradle/internal/LintGradleRequest.java new file mode 100644 index 0000000..b5674f9 --- /dev/null +++ b/gradle/src/main/groovy/com/android/build/gradle/internal/LintGradleRequest.java @@ -0,0 +1,63 @@ +package com.android.build.gradle.internal; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.build.gradle.BasePlugin; +import com.android.builder.model.AndroidProject; +import com.android.builder.model.Variant; +import com.android.tools.lint.client.api.LintRequest; +import com.android.tools.lint.detector.api.Project; +import com.android.utils.Pair; + +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +class LintGradleRequest extends LintRequest { + @NonNull private final LintGradleClient mLintClient; + @NonNull private final BasePlugin mPlugin; + @NonNull private final String mVariantName; + @NonNull private final AndroidProject mModelProject; + + public LintGradleRequest( + @NonNull LintGradleClient client, + @NonNull AndroidProject modelProject, + @NonNull BasePlugin plugin, + @Nullable String variantName, + @NonNull List files) { + super(client, files); + mLintClient = client; + mModelProject = modelProject; + mPlugin = plugin; + mVariantName = variantName; + } + + @Nullable + @Override + public Collection getProjects() { + if (mProjects == null) { + Variant variant = findVariant(mModelProject, mVariantName); + assert variant != null : mVariantName; + Pair> result = LintGradleProject.create( + mLintClient, mModelProject, variant, mPlugin.getProject()); + mProjects = Collections.singletonList(result.getFirst()); + mLintClient.setCustomRules(result.getSecond()); + } + + return mProjects; + } + + private static Variant findVariant(@NonNull AndroidProject project, + @NonNull String variantName) { + if (variantName != null) { + for (Variant variant : project.getVariants()) { + if (variantName.equals(variant.getName())) { + return variant; + } + } + } + + return null; + } +} diff --git a/gradle/src/main/groovy/com/android/build/gradle/tasks/Lint.groovy b/gradle/src/main/groovy/com/android/build/gradle/tasks/Lint.groovy index 17d62cf..088ee72 100644 --- a/gradle/src/main/groovy/com/android/build/gradle/tasks/Lint.groovy +++ b/gradle/src/main/groovy/com/android/build/gradle/tasks/Lint.groovy @@ -20,37 +20,39 @@ import com.android.annotations.NonNull import com.android.annotations.Nullable import com.android.build.gradle.BasePlugin import com.android.build.gradle.internal.LintGradleClient +import com.android.build.gradle.internal.model.ModelBuilder +import com.android.builder.model.AndroidProject +import com.android.builder.model.Variant import com.android.tools.lint.HtmlReporter import com.android.tools.lint.LintCliFlags import com.android.tools.lint.Reporter +import com.android.tools.lint.Warning import com.android.tools.lint.XmlReporter import com.android.tools.lint.checks.BuiltinIssueRegistry import com.android.tools.lint.client.api.IssueRegistry -import com.android.tools.lint.detector.api.LintUtils +import com.android.tools.lint.detector.api.Severity +import com.google.common.collect.Maps import org.gradle.api.DefaultTask import org.gradle.api.GradleException +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 List mCustomRules @Nullable private File mConfigFile @Nullable private File mHtmlOutput @Nullable private File mXmlOutput - @Nullable private List> mSourceSets - @Nullable private String mClassPath - @Nullable private List> mResourceSets - private boolean mQuiet + @Nullable private String mVariantName + private boolean mQuiet = true public void setPlugin(@NonNull BasePlugin plugin) { mPlugin = plugin } - public void addCustomRule(@NonNull File jar) { - if (mCustomRules == null) { - mCustomRules = new ArrayList() - } - mCustomRules.add(jar) + public void setVariantName(@NonNull String variantName) { + mVariantName = variantName } public void setQuiet() { @@ -66,95 +68,84 @@ public class Lint extends DefaultTask { } public void setXmlOutput(@NonNull File xmlOutput) { - mXmlOutput = xmlOutput; - } - - /** - * Adds all files in sourceSets as a source file for lint. - * - * @param sourceSets files to be added to sources. - */ - public void setSources(@NonNull List> sourceSets) { - mSourceSets = sourceSets - } - - /** - * Adds all class files in directory specified by paths for lint. - * - * @param paths A set of paths to class files separated with path separators - */ - public void setClasspath(@NonNull String paths) { - mClassPath = paths - } - - /** - * Adds all files in resourceSets as a resource file for lint. - * - * @param resourceSets files to be added to resources. - */ - public void setLintResources(@NonNull List> resourceSets) { - mResourceSets = resourceSets + mXmlOutput = xmlOutput } @SuppressWarnings("GroovyUnusedDeclaration") @TaskAction public void lint() { - IssueRegistry registry = new BuiltinIssueRegistry() - LintCliFlags flags = new LintCliFlags() - LintGradleClient client = new LintGradleClient(flags, mPlugin) - - // Configure Reporters + def modelProject = createAndroidProject(mPlugin.getProject()) + if (mVariantName != null) { + lintSingleVariant(modelProject, mVariantName) + } else { + lintAllVariants(modelProject) + } + } - if (mHtmlOutput != null) { - mHtmlOutput = mHtmlOutput.getAbsoluteFile() - if (mHtmlOutput.exists()) { - boolean delete = mHtmlOutput.delete() - if (!delete) { - throw new GradleException("Could not delete old " + mHtmlOutput) - } - } - if (mHtmlOutput.getParentFile() != null && !mHtmlOutput.getParentFile().canWrite()) { - throw new GradleException("Cannot write HTML output file " + mHtmlOutput) - } + /** + * Runs lint individually on all the variants, and then compares the results + * across variants and reports these + */ + public void lintAllVariants(@NonNull AndroidProject modelProject) { + Map> warningMap = Maps.newHashMap() + for (Variant variant : modelProject.getVariants()) { try { - flags.getReporters().add(new HtmlReporter(client, mHtmlOutput)) + List warnings = runLint(modelProject, variant.getName()) + warningMap.put(variant, warnings) } catch (IOException e) { - throw new GradleException("HTML invalid argument.", e) + throw new GradleException("Invalid arguments.", e) } } - if (mXmlOutput != null) { - mXmlOutput = mXmlOutput.getAbsoluteFile() - if (mXmlOutput.exists()) { - boolean delete = mXmlOutput.delete(); - if (!delete) { - throw new GradleException("Could not delete old " + mXmlOutput) - } - } - if (mXmlOutput.getParentFile() != null && !mXmlOutput.getParentFile().canWrite()) { - throw new GradleException("Cannot write XML output file " + mXmlOutput) - } - try { - flags.getReporters().add(new XmlReporter(client, mXmlOutput)) - } catch (IOException e) { - throw new GradleException("XML invalid argument.", e) - } + // Compute error matrix + for (Map.Entry> entry : warningMap.entrySet()) { + def variant = entry.getKey() + def warnings = entry.getValue() + println "Ran lint on variant " + variant.getName() + ": " + warnings.size() + + " issues found" } - List reporters = flags.getReporters() - if (reporters.isEmpty()) { - throw new GradleException("No reporter specified.") + 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++ + } } - Map map = new HashMap(){{ - put("", "file://") - }} - for (Reporter reporter : reporters) { - reporter.setUrlMap(map) + IssueRegistry registry = new BuiltinIssueRegistry() + LintCliFlags flags = new LintCliFlags() + LintGradleClient client = new LintGradleClient(registry, flags, mPlugin, modelProject, + null) + configureReporters(client, flags, null) + for (Reporter reporter : flags.getReporters()) { + reporter.write(errorCount, warningCount, mergedWarnings) } + } - // Flags + /** + * Runs lint on a single specified variant + */ + public void lintSingleVariant(@NonNull AndroidProject modelProject, String variantName) { + runLint(modelProject, variantName) + } + + /** Runs lint on the given variant and returns the set of warnings */ + private List runLint( + @NonNull AndroidProject modelProject, + @NonNull String variantName) { + IssueRegistry registry = new BuiltinIssueRegistry() + LintCliFlags flags = new LintCliFlags() + LintGradleClient client = new LintGradleClient(registry, flags, mPlugin, modelProject, + variantName) + + // Configure Reporters + configureReporters(client, flags, variantName) + // Flags if (mQuiet) { flags.setQuiet(true) } @@ -162,59 +153,93 @@ public class Lint extends DefaultTask { flags.setDefaultConfiguration(client.createConfigurationFromFile(mConfigFile)) } - // Flags: sources, resources, classes + // Finally perform lint run + try { + return client.run(registry) + } catch (IOException e) { + throw new GradleException("Invalid arguments.", e) + } + } - for (Collection args : mSourceSets) { - for (File input : args) { - if (input.exists()) { - List sources = flags.getSourcesOverride() - if (sources == null) { - sources = new ArrayList() - flags.setSourcesOverride(sources) - } - sources.add(input) - } + private void configureReporters(@NonNull LintGradleClient client, @NonNull LintCliFlags flags, + @Nullable String variantName) { + StringBuilder base = new StringBuilder() + base.append("lint-results/") + if (variantName != null) { + base.append(variantName) + base.append("/") + } + base.append("lint-results") + if (variantName != null) { + base.append("-") + base.append(variantName) + } + File htmlOutput = mHtmlOutput + File xmlOutput = mXmlOutput + if (htmlOutput == null) { + htmlOutput = project.file(base.toString() + ".html") + File parent = htmlOutput.parentFile + if (!parent.exists()) { + parent.mkdirs() } } - for (String path : LintUtils.splitPath(mClassPath)) { - File input = new File(path); - if (!input.exists()) { - throw new GradleException("Class path entry " + input + " does not exist.") - } - List classes = flags.getClassesOverride(); - if (classes == null) { - classes = new ArrayList(); - flags.setClassesOverride(classes); - } - classes.add(input); - } - - for (Collection args : mResourceSets) { - for (File input : args) { - if (input.exists()) { - List resources = flags.getResourcesOverride() - if (resources == null) { - resources = new ArrayList() - flags.setResourcesOverride(resources) - } - resources.add(input) - } + if (xmlOutput == null) { + xmlOutput = project.file(base.toString() + DOT_XML) + File parent = xmlOutput.parentFile + if (!parent.exists()) { + parent.mkdirs() } } - // Client setup - - if (mCustomRules != null) { - client.setCustomRules(mCustomRules) + htmlOutput = htmlOutput.getAbsoluteFile() + if (htmlOutput.exists()) { + boolean delete = htmlOutput.delete() + if (!delete) { + throw new GradleException("Could not delete old " + htmlOutput) + } + } + if (htmlOutput.getParentFile() != null && !htmlOutput.getParentFile().canWrite()) { + throw new GradleException("Cannot write HTML output file " + htmlOutput) + } + try { + flags.getReporters().add(new HtmlReporter(client, htmlOutput)) + } catch (IOException e) { + throw new GradleException("HTML invalid argument.", e) } - // Finally perform lint run - + xmlOutput = xmlOutput.getAbsoluteFile() + if (xmlOutput.exists()) { + boolean delete = xmlOutput.delete() + if (!delete) { + throw new GradleException("Could not delete old " + xmlOutput) + } + } + if (xmlOutput.getParentFile() != null && !xmlOutput.getParentFile().canWrite()) { + throw new GradleException("Cannot write XML output file " + xmlOutput) + } try { - client.run(registry, Arrays.asList(project.projectDir)); + flags.getReporters().add(new XmlReporter(client, xmlOutput)) } catch (IOException e) { - throw new GradleException("Invalid arguments.", e) + throw new GradleException("XML invalid argument.", e) } + + List reporters = flags.getReporters() + if (reporters.isEmpty()) { + throw new GradleException("No reporter specified.") + } + + Map map = new HashMap() {{ + put("", "file://") + }} + for (Reporter reporter : reporters) { + reporter.setUrlMap(map) + } + } + + private static AndroidProject createAndroidProject(@NonNull Project gradleProject) { + String modelName = AndroidProject.class.getName() + ModelBuilder builder = new ModelBuilder() + return (AndroidProject) builder.buildAll(modelName, gradleProject) } } -- cgit v1.2.3