diff --git a/README.md b/README.md index 630e4ff..fe491e2 100644 --- a/README.md +++ b/README.md @@ -99,9 +99,7 @@ Example projects (with intentional errors to see output): #### Compatibility -**IMPORTANT**: Plugin only works when `java-base` plugin (activated by any java-related plugin like `java-library`, `groovy`, `scala`, `org.jetbrains.kotlin.jvm`, etc.) is enabled, -otherwise nothing will be registered. -There is **no support for the Android plugin** (`java-base` plugin must be used to perform animalsniffer check). +Support for the Android plugin requires Android plugin version `7.4.0` or greater. For *kotlin multiplatform* plugin enable java support: diff --git a/examples/android-app/README.md b/examples/android-app/README.md new file mode 100644 index 0000000..4658ed2 --- /dev/null +++ b/examples/android-app/README.md @@ -0,0 +1,21 @@ +# Android library project with animalsniffer check + +Output: + +``` +> Task :android-app:animalsnifferDebug + +4 AnimalSniffer violations were found in 2 files. See the report at: file:////{PATH_FROM_ROOT}/gradle-animalsniffer-plugin/examples/android-app/build/reports/animalsniffer/debug.text + +[Undefined reference | java18-1.0] invalid.(Sample.java:9) + >> String String.repeat(int) + +[Undefined reference | android-api-level-21-5.0.1_r2] invalid.(Sample.java:9) + >> String String.repeat(int) + +[Undefined reference | android-api-level-21-5.0.1_r2] invalid.(Sample.java:15) + >> java.nio.file.Path java.io.File.toPath() + +[Undefined reference | android-api-level-21-5.0.1_r2] invalid.(SampleKotlin.kt:9) + >> java.nio.file.Path java.io.File.toPath() +``` \ No newline at end of file diff --git a/examples/android-app/build.gradle b/examples/android-app/build.gradle new file mode 100644 index 0000000..7acf440 --- /dev/null +++ b/examples/android-app/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'com.android.application' version '7.4.0' + id 'org.jetbrains.kotlin.android' version '1.9.23' + id 'ru.vyarus.animalsniffer' +} + +android { + compileSdk 33 + namespace 'com.example.namespace' + def javaVersion = JavaVersion.VERSION_1_8 + + compileOptions { + sourceCompatibility(javaVersion) + targetCompatibility(javaVersion) + } + + kotlinOptions { + jvmTarget = javaVersion.toString() + } +} + +animalsniffer { +// debug = true + // only show errors + ignoreFailures = true +} + +repositories { mavenCentral(); google() } +dependencies { + signature 'org.codehaus.mojo.signature:java18:1.0@signature' + signature 'net.sf.androidscents.signature:android-api-level-21:5.0.1_r2@signature' + + implementation 'org.slf4j:slf4j-api:1.7.25' +} \ No newline at end of file diff --git a/examples/android-app/src/main/AndroidManifest.xml b/examples/android-app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5c3d365 --- /dev/null +++ b/examples/android-app/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/examples/android-app/src/main/java/invalid/Sample.java b/examples/android-app/src/main/java/invalid/Sample.java new file mode 100644 index 0000000..53b2068 --- /dev/null +++ b/examples/android-app/src/main/java/invalid/Sample.java @@ -0,0 +1,17 @@ +package invalid; + +import java.io.File; + +public class Sample { + + public static void main(String[] args) { + // method added in 11 + "".repeat(5); + } + + public void someth() { + // not available in android + File file = new File(""); + file.toPath(); + } +} \ No newline at end of file diff --git a/examples/android-app/src/main/java/invalid/SampleKotlin.kt b/examples/android-app/src/main/java/invalid/SampleKotlin.kt new file mode 100644 index 0000000..e593537 --- /dev/null +++ b/examples/android-app/src/main/java/invalid/SampleKotlin.kt @@ -0,0 +1,11 @@ +package invalid + +import java.io.File + +class SampleKotlin { + fun someth() { + // not available in android + val file = File("") + file.toPath() + } +} \ No newline at end of file diff --git a/examples/android-lib/README.md b/examples/android-lib/README.md new file mode 100644 index 0000000..a7b479e --- /dev/null +++ b/examples/android-lib/README.md @@ -0,0 +1,21 @@ +# Android application project with animalsniffer check + +Output: + +``` +> Task :android-lib:animalsnifferDebug + +4 AnimalSniffer violations were found in 2 files. See the report at: file:////{PATH_FROM_ROOT}/gradle-animalsniffer-plugin/examples/android-lib/build/reports/animalsniffer/debug.text + +[Undefined reference | java18-1.0] invalid.(Sample.java:9) + >> String String.repeat(int) + +[Undefined reference | android-api-level-21-5.0.1_r2] invalid.(Sample.java:9) + >> String String.repeat(int) + +[Undefined reference | android-api-level-21-5.0.1_r2] invalid.(Sample.java:15) + >> java.nio.file.Path java.io.File.toPath() + +[Undefined reference | android-api-level-21-5.0.1_r2] invalid.(SampleKotlin.kt:9) + >> java.nio.file.Path java.io.File.toPath() +``` \ No newline at end of file diff --git a/examples/android-lib/build.gradle b/examples/android-lib/build.gradle new file mode 100644 index 0000000..292987c --- /dev/null +++ b/examples/android-lib/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'com.android.library' version '7.4.0' + id 'org.jetbrains.kotlin.android' version '1.9.23' + id 'ru.vyarus.animalsniffer' +} + +android { + compileSdk 33 + namespace 'com.example.namespace' + def javaVersion = JavaVersion.VERSION_1_8 + + compileOptions { + sourceCompatibility(javaVersion) + targetCompatibility(javaVersion) + } + + kotlinOptions { + jvmTarget = javaVersion.toString() + } +} + +animalsniffer { +// debug = true + // only show errors + ignoreFailures = true +} + +repositories { mavenCentral() } +dependencies { + signature 'org.codehaus.mojo.signature:java18:1.0@signature' + signature 'net.sf.androidscents.signature:android-api-level-21:5.0.1_r2@signature' + + implementation 'org.slf4j:slf4j-api:1.7.25' +} \ No newline at end of file diff --git a/examples/android-lib/src/main/AndroidManifest.xml b/examples/android-lib/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5c3d365 --- /dev/null +++ b/examples/android-lib/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/examples/android-lib/src/main/java/invalid/Sample.java b/examples/android-lib/src/main/java/invalid/Sample.java new file mode 100644 index 0000000..53b2068 --- /dev/null +++ b/examples/android-lib/src/main/java/invalid/Sample.java @@ -0,0 +1,17 @@ +package invalid; + +import java.io.File; + +public class Sample { + + public static void main(String[] args) { + // method added in 11 + "".repeat(5); + } + + public void someth() { + // not available in android + File file = new File(""); + file.toPath(); + } +} \ No newline at end of file diff --git a/examples/android-lib/src/main/java/invalid/SampleKotlin.kt b/examples/android-lib/src/main/java/invalid/SampleKotlin.kt new file mode 100644 index 0000000..e593537 --- /dev/null +++ b/examples/android-lib/src/main/java/invalid/SampleKotlin.kt @@ -0,0 +1,11 @@ +package invalid + +import java.io.File + +class SampleKotlin { + fun someth() { + // not available in android + val file = File("") + file.toPath() + } +} \ No newline at end of file diff --git a/examples/settings.gradle b/examples/settings.gradle index 1eb29c2..652334d 100644 --- a/examples/settings.gradle +++ b/examples/settings.gradle @@ -9,6 +9,7 @@ pluginManagement { repositories { mavenLocal() gradlePluginPortal() + google() maven { url 'https://jitpack.io' } } } @@ -16,6 +17,7 @@ pluginManagement { enableFeaturePreview "STABLE_CONFIGURATION_CACHE" include 'java', 'groovy', 'kotlin', 'scala', + 'android-lib', 'android-app', 'buildSignature:fromClasses', 'buildSignature:fromJars', 'buildSignature:fromSignatures', diff --git a/src/main/groovy/ru/vyarus/gradle/plugin/animalsniffer/AnimalSniffer.groovy b/src/main/groovy/ru/vyarus/gradle/plugin/animalsniffer/AnimalSniffer.groovy index 7f32b5f..649f5ce 100644 --- a/src/main/groovy/ru/vyarus/gradle/plugin/animalsniffer/AnimalSniffer.groovy +++ b/src/main/groovy/ru/vyarus/gradle/plugin/animalsniffer/AnimalSniffer.groovy @@ -66,7 +66,7 @@ class AnimalSniffer extends SourceTask implements VerificationTask, Reporting sourcesDirs + FileCollection sourcesDirs /** * Annotation class name to avoid check @@ -298,7 +298,7 @@ class AnimalSniffer extends SourceTask implements VerificationTask, Reporting collectSourceDirs() { Set res = [] as Set - res.addAll(getSourcesDirs()) + res.addAll(getSourcesDirs().getFiles()) // HACK to support kotlin multiplatform source path for jvm case (when withJava() active) // this MUST BE rewritten into separate support for multiplatform if (project.plugins.findPlugin('org.jetbrains.kotlin.multiplatform')) { diff --git a/src/main/groovy/ru/vyarus/gradle/plugin/animalsniffer/AnimalSnifferPlugin.groovy b/src/main/groovy/ru/vyarus/gradle/plugin/animalsniffer/AnimalSnifferPlugin.groovy index c376d26..6ccfa4b 100644 --- a/src/main/groovy/ru/vyarus/gradle/plugin/animalsniffer/AnimalSnifferPlugin.groovy +++ b/src/main/groovy/ru/vyarus/gradle/plugin/animalsniffer/AnimalSnifferPlugin.groovy @@ -3,16 +3,19 @@ package ru.vyarus.gradle.plugin.animalsniffer import groovy.transform.CompileStatic import groovy.transform.Memoized import groovy.transform.TypeCheckingMode +import kotlin.jvm.functions.Function1 import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.Dependency +import org.gradle.api.file.Directory import org.gradle.api.file.FileCollection import org.gradle.api.file.RegularFile import org.gradle.api.plugins.ExtraPropertiesExtension import org.gradle.api.plugins.JavaBasePlugin import org.gradle.api.plugins.ReportingBasePlugin +import org.gradle.api.provider.ListProperty import org.gradle.api.reporting.ReportingExtension import org.gradle.api.specs.NotSpec import org.gradle.api.tasks.SourceSet @@ -22,6 +25,7 @@ import org.gradle.util.GradleVersion import ru.vyarus.gradle.plugin.animalsniffer.info.SignatureInfoTask import ru.vyarus.gradle.plugin.animalsniffer.signature.AnimalSnifferSignatureExtension import ru.vyarus.gradle.plugin.animalsniffer.signature.BuildSignatureTask +import ru.vyarus.gradle.plugin.animalsniffer.util.AndroidClassesCollector import ru.vyarus.gradle.plugin.animalsniffer.util.ContainFilesSpec import ru.vyarus.gradle.plugin.animalsniffer.util.ExcludeFilePatternSpec @@ -56,17 +60,22 @@ class AnimalSnifferPlugin implements Plugin { @Override void apply(Project project) { - // activated only when java plugin is enabled - project.plugins.withType(JavaBasePlugin) { - this.project = project - project.plugins.apply(ReportingBasePlugin) + this.project = project + project.plugins.apply(ReportingBasePlugin) - checkGradleCompatibility() - registerShortcuts() - registerConfigurations() - registerExtensions() - registerCheckTasks() - registerBuildTasks() + checkGradleCompatibility() + registerShortcuts() + registerConfigurations() + registerExtensions() + registerBuildTasks() + project.plugins.withType(JavaBasePlugin) { + registerJavaCheckTasks() + } + project.plugins.withId("com.android.library") { + registerAndroidCheckTasks() + } + project.plugins.withId("com.android.application") { + registerAndroidCheckTasks() } } @@ -119,7 +128,7 @@ class AnimalSnifferPlugin implements Plugin { @SuppressWarnings(['Indentation', 'NestedBlockDepth']) @CompileStatic(TypeCheckingMode.SKIP) - private void registerCheckTasks() { + private void registerJavaCheckTasks() { // create tasks for each source set project.sourceSets.all { SourceSet sourceSet -> String sourceSetName = sourceSet.name @@ -137,7 +146,11 @@ class AnimalSnifferPlugin implements Plugin { }) } } - configureCheckTask(checkTask, sourceSet) + configureCheckTask(checkTask, + project.files(sourceSet.allJava.srcDirs), + sourceSet.getTaskName(ANIMALSNIFFER_CACHE, null), + sourceSet.classesTaskName, + sourceSet.compileClasspath) } // include required animalsniffer tasks in check lifecycle @@ -148,16 +161,70 @@ class AnimalSnifferPlugin implements Plugin { } } + @SuppressWarnings('GroovyAssignabilityCheck') + @CompileStatic(TypeCheckingMode.SKIP) + void registerAndroidCheckTasks() { + def androidComponents = project.androidComponents + androidComponents.onVariants(androidComponents.selector().all(), { variant -> + String sourceSetName = variant.name + String capitalizedSourceSetName = sourceSetName.capitalize() + String classesCollectorTaskName = sourceSetName + "AnimalSnifferClassesCollector" + TaskProvider classesCollector = createAndroidClassesCollector(classesCollectorTaskName, variant) + TaskProvider checkTask = project.tasks + . register(CHECK_SIGNATURE + capitalizedSourceSetName, + AnimalSniffer) { + description = "Run AnimalSniffer checks for ${sourceSetName} classes" + // task operates on classes instead of sources + source = classesCollector.flatMap { it.outputDirectory } + reports.all { report -> + report.required.convention(true) + report.outputLocation.convention(project.provider { + { -> + new File(extension.reportsDir, "${sourceSetName}.${report.name}") + } as RegularFile + }) + } + } + + configureCheckTask(checkTask, + project.files(variant.sources.java.all, variant.sources.kotlin.all), + ANIMALSNIFFER_CACHE + capitalizedSourceSetName, + classesCollectorTaskName, + variant.compileClasspath) + + }) + + } + + @CompileStatic(TypeCheckingMode.SKIP) + private TaskProvider createAndroidClassesCollector(String taskName, Object variant) { + TaskProvider collectClasses = project.tasks.register(taskName, AndroidClassesCollector) + def scopedArtifactsScopeType = Class.forName("com.android.build.api.variant.ScopedArtifacts\$Scope") + def scopedArtifactTypeClasses = Class.forName("com.android.build.api.artifact.ScopedArtifact\$CLASSES") + variant.artifacts.forScope(scopedArtifactsScopeType.PROJECT).use(collectClasses) + .toGet(scopedArtifactTypeClasses.INSTANCE, new Function1>() { + @Override + ListProperty invoke(AndroidClassesCollector task) { + return task.jarFiles + } + }, new Function1>() { + @Override + ListProperty invoke(AndroidClassesCollector task) { + return task.classesDirs + } + }) + return collectClasses + } + @SuppressWarnings(['Indentation', 'MethodSize', 'UnnecessaryGetter']) @CompileStatic(TypeCheckingMode.SKIP) - private void configureCheckTask(TaskProvider checkTask, SourceSet sourceSet) { + private void configureCheckTask(TaskProvider checkTask, FileCollection srcDirs, String signatureTaskName, String classesTaskName, FileCollection compileClasspath) { Configuration animalsnifferConfiguration = project.configurations[CHECK_SIGNATURE] // build special signature from provided signatures and all jars to be able to cache it // and perform much faster checks after the first run TaskProvider signatureTask = project.tasks - . register(sourceSet.getTaskName(ANIMALSNIFFER_CACHE, null), - BuildSignatureTask) { + . register(signatureTaskName, BuildSignatureTask) { // this special task can be skipped if animalsniffer check supposed to be skipped // note that task is still created because signatures could be registered dynamically onlyIf { !extension.signatures.empty && extension.cache.enabled } @@ -165,7 +232,7 @@ class AnimalSnifferPlugin implements Plugin { conventionMapping.with { animalsnifferClasspath = { animalsnifferConfiguration } signatures = { extension.signatures } - files = { excludeJars(getClasspathWithoutModules(sourceSet)) } + files = { excludeJars(getClasspathWithoutModules(compileClasspath)) } exclude = { extension.cache.exclude as Set } mergeSignatures = { extension.cache.mergeSignatures } // debug for cache tasks controlled by check debug @@ -173,19 +240,19 @@ class AnimalSnifferPlugin implements Plugin { } } checkTask.configure { - dependsOn(sourceSet.classesTaskName) + dependsOn(classesTaskName) // skip if no signatures configured or no sources to check onlyIf { !getAnimalsnifferSignatures().empty && getSource().size() > 0 } conventionMapping.with { classpath = { extension.cache.enabled ? - getModulesFromClasspath(sourceSet) : excludeJars(sourceSet.compileClasspath) + getModulesFromClasspath(compileClasspath) : excludeJars(compileClasspath) } animalsnifferSignatures = { extension.cache.enabled ? signatureTask.get().outputFiles : extension.signatures } animalsnifferClasspath = { animalsnifferConfiguration } - sourcesDirs = { sourceSet.allJava.srcDirs } + sourcesDirs = srcDirs ignoreFailures = { extension.ignoreFailures } annotation = { extension.annotation } ignoreClasses = { extension.ignore } @@ -246,12 +313,12 @@ class AnimalSnifferPlugin implements Plugin { */ @Memoized @CompileStatic(TypeCheckingMode.SKIP) - private FileCollection getClasspathWithoutModules(SourceSet sourceSet) { + private FileCollection getClasspathWithoutModules(FileCollection compileClasspath) { Set excludeJars = moduleJars if (excludeJars.empty) { - return sourceSet.compileClasspath + return compileClasspath } - sourceSet.compileClasspath.filter new NotSpec(new ContainFilesSpec(excludeJars)) + compileClasspath.filter new NotSpec(new ContainFilesSpec(excludeJars)) } /** @@ -263,12 +330,12 @@ class AnimalSnifferPlugin implements Plugin { */ @Memoized @CompileStatic(TypeCheckingMode.SKIP) - private FileCollection getModulesFromClasspath(SourceSet sourceSet) { + private FileCollection getModulesFromClasspath(FileCollection compileClasspath) { Set includeJars = moduleJars if (includeJars.empty) { return null } - sourceSet.compileClasspath.filter new ContainFilesSpec(includeJars) + compileClasspath.filter new ContainFilesSpec(includeJars) } /** diff --git a/src/main/groovy/ru/vyarus/gradle/plugin/animalsniffer/util/AndroidClassesCollector.groovy b/src/main/groovy/ru/vyarus/gradle/plugin/animalsniffer/util/AndroidClassesCollector.groovy new file mode 100644 index 0000000..9ce23e6 --- /dev/null +++ b/src/main/groovy/ru/vyarus/gradle/plugin/animalsniffer/util/AndroidClassesCollector.groovy @@ -0,0 +1,35 @@ +package ru.vyarus.gradle.plugin.animalsniffer.util + + +import org.gradle.api.DefaultTask +import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction + +abstract class AndroidClassesCollector extends DefaultTask { + + @InputFiles + abstract ListProperty getJarFiles() + + @InputFiles + abstract ListProperty getClassesDirs() + + @OutputDirectory + abstract DirectoryProperty getOutputDirectory() + + AndroidClassesCollector() { + getOutputDirectory().value(project.layout.buildDirectory.dir("intermediates/animal_sniffer/" + name)) + } + + @TaskAction + void execute() { + project.sync { + from(classesDirs) + into(outputDirectory) + } + } +}