diff --git a/build.gradle b/build.gradle index 433071f4..0ebe39d3 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { google() } dependencies { - classpath "com.android.tools.build:gradle:3.1.3" + classpath "com.android.tools.build:gradle:3.1.3" // if you update this, also update the lintVersion below classpath "com.vanniktech:gradle-android-junit-jacoco-plugin:0.10.0" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } @@ -45,4 +45,9 @@ ext { assertjVersion = '2.8.0' supportTestVersion = '1.0.1' espressoVersion = '3.0.1' + + // According to https://github.com/googlesamples/android-custom-lint-rules/tree/master/android-studio-3 + // the lint version should match to the used Android Gradle Plugin by the formula "AGP Version X.Y.Z + 23.0.0" + // E.g. "AGP Version 3.1.3 + 23.0.0 = Lint Version 26.1.3" + lintVersion = '26.1.3' } diff --git a/settings.gradle b/settings.gradle index e64567a5..1618fbcb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,6 +6,7 @@ include( ":thirtyinch-rx2", ":thirtyinch-test", ":thirtyinch-kotlin", + ":thirtyinch-lint", ":sample", ":plugin-test" -) +) \ No newline at end of file diff --git a/thirtyinch-lint/.gitignore b/thirtyinch-lint/.gitignore new file mode 100644 index 00000000..2d9f8bf1 --- /dev/null +++ b/thirtyinch-lint/.gitignore @@ -0,0 +1,7 @@ +.idea +build/ +.gradle +gradle +gradlew +gradlew.bat +.idea/workspace.xml diff --git a/thirtyinch-lint/build.gradle b/thirtyinch-lint/build.gradle new file mode 100644 index 00000000..b1cf717d --- /dev/null +++ b/thirtyinch-lint/build.gradle @@ -0,0 +1,48 @@ +plugins { + id "org.jetbrains.kotlin.jvm" + id "jacoco" +} + +repositories { + jcenter() +} + +configurations { + lintChecks +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + + compileOnly "com.android.tools.lint:lint-api:$lintVersion" + compileOnly "com.android.tools.lint:lint-checks:$lintVersion" + + testImplementation 'junit:junit:4.12' + testImplementation "org.assertj:assertj-core:$assertjVersion" + + testImplementation "com.android.tools.lint:lint:$lintVersion" + testImplementation "com.android.tools.lint:lint-tests:$lintVersion" + + lintChecks files(jar) +} + +jar { + manifest { + attributes("Manifest-Version": 1.0) + attributes("Lint-Registry": "net.grandcentrix.thirtyinch.lint.TiLintRegistry") + // The TI checks are build with the new 3.0 APIs (including UAST) so we should also register the v2 lint registry. + attributes("Lint-Registry-v2": "net.grandcentrix.thirtyinch.lint.TiLintRegistry") + } +} + +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} + +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} diff --git a/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiIssue.kt b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiIssue.kt new file mode 100644 index 00000000..7905707f --- /dev/null +++ b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiIssue.kt @@ -0,0 +1,41 @@ +package net.grandcentrix.thirtyinch.lint + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity + +private val CATEGORY_TI = Category.create("ThirtyInch", 90) + +sealed class TiIssue( + val id: String, + val briefDescription: String, + val category: Category, + val priority: Int, + val severity: Severity +) { + + object MissingView : TiIssue( + id = "MissingTiViewImplementation", + briefDescription = "TiView Implementation missing in class", + category = CATEGORY_TI, + priority = 8, + severity = Severity.ERROR + ) + + fun asLintIssue(detectorCls: Class, description: String = briefDescription): Issue = + Issue.create( + id, + briefDescription, + description, + category, + priority, + severity, + Implementation( + detectorCls, + Scope.JAVA_FILE_SCOPE + ) + ) +} \ No newline at end of file diff --git a/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiLintRegistry.kt b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiLintRegistry.kt new file mode 100644 index 00000000..64159cc1 --- /dev/null +++ b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiLintRegistry.kt @@ -0,0 +1,20 @@ +package net.grandcentrix.thirtyinch.lint + +import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.detector.api.Issue +import net.grandcentrix.thirtyinch.lint.detector.MissingViewInCompositeDetector +import net.grandcentrix.thirtyinch.lint.detector.MissingViewInThirtyInchDetector + +class TiLintRegistry : IssueRegistry() { + override val issues: List + get() = listOf( + MissingViewInThirtyInchDetector.ISSUE.apply { + setEnabledByDefault(true) + }, + MissingViewInCompositeDetector.ISSUE.apply { + setEnabledByDefault(true) + } + ) + + override val api: Int = com.android.tools.lint.detector.api.CURRENT_API +} diff --git a/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/detector/BaseMissingViewDetector.kt b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/detector/BaseMissingViewDetector.kt new file mode 100644 index 00000000..041e16f3 --- /dev/null +++ b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/detector/BaseMissingViewDetector.kt @@ -0,0 +1,78 @@ +package net.grandcentrix.thirtyinch.lint.detector + +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.TextFormat +import com.intellij.psi.PsiType +import com.intellij.psi.util.PsiUtil +import org.jetbrains.uast.UClass +import org.jetbrains.uast.getUastContext + +// Base class for Lint checks centered around the notion of "TiView not implemented" +abstract class BaseMissingViewDetector : Detector(), Detector.UastScanner { + + /** + * The Issue that the detector is connected to, reported on illegal state detection + */ + abstract val issue: Issue + + /** + * The list of super-classes to detect. + * We're forcing sub-classed Detectors to implement this by means of redeclaration + */ + abstract override fun applicableSuperClasses(): List + + /** + * Whether or not to allow the absence of an "implements TiView" clause on the given declaration. + * The View interface is given as well to allow for further introspection into the setup of the class at hand. + * When false is returned here, Lint will report the Issue connected to this Detector on the given declaration. + */ + abstract fun allowMissingViewInterface(context: JavaContext, declaration: UClass, viewInterface: PsiType): Boolean + + /** + * Tries to extract the PsiType of the TiView sub-class that is relevant for the given declaration. + * The relevant super-class (from applicableSuperClasses()) & its resolved variant are given as well. + */ + abstract fun findViewInterface(context: JavaContext, declaration: UClass): PsiType? + + final override fun visitClass(context: JavaContext, declaration: UClass) { + if (!context.isEnabled(issue)) { + return + } + // Don't trigger on abstract classes + if (PsiUtil.isAbstractClass(declaration.psi)) { + return + } + // Extract the MVP View type from the declaration + findViewInterface(context, declaration)?.let { viewInterface -> + // Check if the class implements that interface as well + if (!tryFindViewImplementation(context, declaration, viewInterface)) { + // Interface not implemented; check if alternate condition applies + if (!allowMissingViewInterface(context, declaration, viewInterface)) { + // Invalid state: Report issue for this class + declaration.nameIdentifier?.run { + context.report( + issue, + context.getLocation(this.originalElement), + issue.getBriefDescription(TextFormat.TEXT)) + } + } + } + } + } + + private fun tryFindViewImplementation(context: JavaContext, declaration: UClass, + viewInterface: PsiType): Boolean { + for (implementedType in declaration.implementsListTypes) { + if (implementedType == viewInterface) { + return true + } + implementedType.resolve()?.let { resolvedType -> + val uastContext = declaration.getUastContext() + return tryFindViewImplementation(context, uastContext.getClass(resolvedType), viewInterface) + } + } + return false + } +} \ No newline at end of file diff --git a/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/detector/MissingViewInCompositeDetector.kt b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/detector/MissingViewInCompositeDetector.kt new file mode 100644 index 00000000..18cb53c4 --- /dev/null +++ b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/detector/MissingViewInCompositeDetector.kt @@ -0,0 +1,86 @@ +package net.grandcentrix.thirtyinch.lint.detector + +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.intellij.psi.PsiJavaCodeReferenceElement +import com.intellij.psi.PsiType +import net.grandcentrix.thirtyinch.lint.TiIssue.MissingView +import org.jetbrains.uast.UBlockExpression +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UClass +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.getUastContext + +private const val ADD_PLUGIN_METHOD = "addPlugin" +private const val TI_ACTIVITY_PLUGIN_NAME = "TiActivityPlugin" +private const val TI_FRAGMENT_PLUGIN_NAME = "TiFragmentPlugin" +private val CA_CLASS_NAMES = listOf( + "com.pascalwelsch.compositeandroid.activity.CompositeActivity", + "com.pascalwelsch.compositeandroid.fragment.CompositeFragment" +) + +class MissingViewInCompositeDetector : BaseMissingViewDetector() { + companion object { + val ISSUE = MissingView.asLintIssue( + MissingViewInCompositeDetector::class.java, + "When using ThirtyInch, a class extending CompositeActivity or CompositeFragment " + + "has to implement the TiView interface associated with it in its signature, " + + "if it applies the respective plugin as well." + ) + } + + override fun applicableSuperClasses() = CA_CLASS_NAMES + + override val issue: Issue = MissingViewInThirtyInchDetector.ISSUE + + override fun findViewInterface(context: JavaContext, declaration: UClass): PsiType? { + // Expect TiPlugin to be applied in the extended CA class + // Found default constructor + val defaultConstructor = declaration.constructors.firstOrNull { it.typeParameters.isEmpty() } + + defaultConstructor?.let { + val uastContext = declaration.getUastContext() + val body = uastContext.getMethodBody(defaultConstructor) + return tryFindViewFromCompositeConstructor(context, declaration, body) + } + return null + } + + private fun tryFindViewFromCompositeConstructor(context: JavaContext, declaration: UClass, + expression: UExpression?): PsiType? { + if (expression == null) { + return null + } + when (expression) { + is UBlockExpression -> { + // Unwrap block statements; the first resolvable result is returned + expression.expressions + .mapNotNull { tryFindViewFromCompositeConstructor(context, declaration, it) } + .forEach { return it } + } + is UCallExpression -> { + // Inspect call sites + if (ADD_PLUGIN_METHOD == expression.methodName && expression.valueArgumentCount == 1) { + // Expect a plugin to be used as the only argument to this method + val argument = expression.valueArguments[0] + if (argument is UCallExpression) { + val argReference = argument.classReference ?: return null + val resolvedName = argReference.resolvedName + if (TI_ACTIVITY_PLUGIN_NAME == resolvedName || TI_FRAGMENT_PLUGIN_NAME == resolvedName) { + // Matching names. Finally, find the type parameters passed to the plugin + val psiReference = argReference.psi as PsiJavaCodeReferenceElement? ?: return null + val parameterTypes = psiReference.typeParameters + if (parameterTypes.size != 2) { + return null + } + return parameterTypes[1] + } + } + } + } + } + return null + } + + override fun allowMissingViewInterface(context: JavaContext, declaration: UClass, viewInterface: PsiType) = false +} \ No newline at end of file diff --git a/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/detector/MissingViewInThirtyInchDetector.kt b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/detector/MissingViewInThirtyInchDetector.kt new file mode 100644 index 00000000..5cc81387 --- /dev/null +++ b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/detector/MissingViewInThirtyInchDetector.kt @@ -0,0 +1,64 @@ +package net.grandcentrix.thirtyinch.lint.detector + +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiType +import net.grandcentrix.thirtyinch.lint.TiIssue.MissingView +import org.jetbrains.kotlin.utils.addToStdlib.firstNotNullResult +import org.jetbrains.uast.UClass + +private const val TI_VIEW_FQ = "net.grandcentrix.thirtyinch.TiView" +private const val PROVIDE_VIEW_METHOD = "provideView" +private val TI_CLASS_NAMES = listOf( + "net.grandcentrix.thirtyinch.TiActivity", + "net.grandcentrix.thirtyinch.TiFragment", + "net.grandcentrix.thirtyinch.TiDialogFragment" +) + +class MissingViewInThirtyInchDetector : BaseMissingViewDetector() { + companion object { + val ISSUE = MissingView.asLintIssue( + MissingViewInThirtyInchDetector::class.java, + "When using ThirtyInch, a class extending TiActivity or TiFragment " + + "has to implement the TiView interface associated with it in its signature, " + + "or implement `provideView()` instead to override this default behaviour." + ) + } + + override fun applicableSuperClasses() = TI_CLASS_NAMES + + override val issue: Issue = ISSUE + + override fun findViewInterface(context: JavaContext, declaration: UClass): PsiType? { + return declaration.extendsListTypes + .firstNotNullResult { extendedType -> tryFindViewInterface(extendedType) } + } + + private fun tryFindViewInterface(extendedType: PsiClassType): PsiType? { + val resolvedType = extendedType.resolveGenerics().element ?: return null + + val parameters = extendedType.parameters + val parameterTypes = resolvedType.typeParameters + + check(parameters.size == parameterTypes.size) { "Got different Array Sizes" } + + return parameters + .mapIndexed { i, psiType -> Pair(psiType, parameterTypes[i]) } + .firstNotNullResult { (type, typeParameter) -> + typeParameter.extendsListTypes + .map { it.resolveGenerics().element } + .filter { TI_VIEW_FQ == it?.qualifiedName } + .map { type } + .firstOrNull() + ?: (type as? PsiClassType)?.let { tryFindViewInterface(it) } + } + } + + override fun allowMissingViewInterface(context: JavaContext, declaration: UClass, + viewInterface: PsiType): Boolean { + // Interface not implemented; check if provideView() is overridden instead + return declaration.findMethodsByName(PROVIDE_VIEW_METHOD, true) + .any { viewInterface == it.returnType } + } +} \ No newline at end of file diff --git a/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInCompositeDetectorTest.kt b/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInCompositeDetectorTest.kt new file mode 100644 index 00000000..b7ac826b --- /dev/null +++ b/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInCompositeDetectorTest.kt @@ -0,0 +1,371 @@ +package net.grandcentrix.thirtyinch.lint + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import net.grandcentrix.thirtyinch.lint.detector.MissingViewInCompositeDetector +import org.assertj.core.api.Assertions.* + +private const val NO_WARNINGS = "No warnings." + +class MissingViewInCompositeDetectorTest : LintDetectorTest() { + + /* Stubbed-out source files */ + + private val tiPresenterStub = java( + "package net.grandcentrix.thirtyinch;\n" + + "public abstract class TiPresenter {\n" + + "}" + ) + + private val tiViewStub = java( + "package net.grandcentrix.thirtyinch;\n" + + "public interface TiView {\n" + + "}" + ) + + private val caBasePluginStub = java( + "package com.pascalwelsch.compositeandroid;\n" + + "public interface Plugin {\n" + + "}" + ) + + private val caActivityStub = java( + "package com.pascalwelsch.compositeandroid.activity;\n" + + "import net.grandcentrix.thirtyinch.plugin.*;\n" + + "import com.pascalwelsch.compositeandroid.*;\n" + + "public class CompositeActivity {\n" + + " public void addPlugin(Plugin plugin) {\n" + + " }\n" + + "}" + ) + + private val caActivityPluginStub = java( + "package net.grandcentrix.thirtyinch.plugin;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "import com.pascalwelsch.compositeandroid.*;\n" + + "public class TiActivityPlugin

, V extends TiView> implements Plugin {\n" + + " public TiActivityPlugin(Runnable action) {\n" + + " }\n" + + "}" + ) + + private val caFragmentStub = java( + "package com.pascalwelsch.compositeandroid.fragment;\n" + + "import net.grandcentrix.thirtyinch.plugin.*;\n" + + "import com.pascalwelsch.compositeandroid.*;\n" + + "public class CompositeFragment {\n" + + " public void addPlugin(Plugin plugin) {\n" + + " }\n" + + "}" + ) + + private val caFragmentPluginStub = java( + "package net.grandcentrix.thirtyinch.plugin;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "import com.pascalwelsch.compositeandroid.*;\n" + + "public class TiFragmentPlugin

, V extends TiView> implements Plugin {\n" + + " public TiFragmentPlugin(Runnable action) {\n" + + " }\n" + + "}" + ) + + private val view = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "interface MyView extends TiView {\n" + + "}" + ) + + private val presenter = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "final class MyPresenter extends TiPresenter {\n" + + "}" + ) + + /* Overrides */ + + override fun getDetector(): Detector = MissingViewInCompositeDetector() + + override fun getIssues(): MutableList = mutableListOf( + MissingViewInCompositeDetector.ISSUE) + + /* + * -------------------------------------------------------------------------------- + * CompositeActivity + * -------------------------------------------------------------------------------- + */ + + fun testActivity_dontTriggerOnAbstractClass() { + val activity = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.plugin.*;\n" + + "import com.pascalwelsch.compositeandroid.activity.*;\n" + + "public abstract class MyActivity extends CompositeActivity {\n" + + "}" + ) + + assertThat( + lintProject( + caActivityStub, + caBasePluginStub, + caActivityPluginStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + activity + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testActivity_andViewIsImplementedCorrectly_noWarnings() { + val activity = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.plugin.*;\n" + + "import com.pascalwelsch.compositeandroid.activity.*;\n" + + "public class MyActivity extends CompositeActivity implements MyView {\n" + + " public MyActivity() {\n" + + " addPlugin(new TiActivityPlugin(\n" + + " () -> new MyPresenter()));\n" + + " }\n" + + "}" + ) + + assertThat( + lintProject( + caActivityStub, + caBasePluginStub, + caActivityPluginStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + activity + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testActivity_doesntImplementInterface_hasWarning() { + val activity = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.plugin.*;\n" + + "import com.pascalwelsch.compositeandroid.activity.*;\n" + + "public class MyActivity extends CompositeActivity {\n" + + " public MyActivity() {\n" + + " addPlugin(new TiActivityPlugin(\n" + + " () -> new MyPresenter()));\n" + + " }\n" + + "}" + ) + + assertThat( + lintProject( + caActivityStub, + caBasePluginStub, + caActivityPluginStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + activity + ) + ).containsOnlyOnce(TiIssue.MissingView.id) + } + + fun testActivity_doesntImplementInterface_butDoesntHavePluginAppliedEither_noWarnings() { + val activity = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.plugin.*;\n" + + "import com.pascalwelsch.compositeandroid.activity.*;\n" + + "public class MyActivity extends CompositeActivity {\n" + + "}" + ) + + assertThat( + lintProject( + caActivityStub, + caBasePluginStub, + caActivityPluginStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + activity + ) + ).isEqualTo(NO_WARNINGS) + } + + /* + * -------------------------------------------------------------------------------- + * CompositeFragment + * -------------------------------------------------------------------------------- + */ + + fun testFragment_dontTriggerOnAbstractClass() { + val fragment = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.plugin.*;\n" + + "import com.pascalwelsch.compositeandroid.activity.*;\n" + + "public abstract class MyActivity extends CompositeActivity {\n" + + "}" + ) + + assertThat( + lintProject( + caActivityStub, + caBasePluginStub, + caActivityPluginStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + fragment + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testFragment_andViewIsImplementedCorrectly_noWarnings() { + val fragment = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.plugin.*;\n" + + "import com.pascalwelsch.compositeandroid.fragment.*;\n" + + "public class MyFragment extends CompositeFragment implements MyView {\n" + + " public MyFragment() {\n" + + " addPlugin(new TiFragmentPlugin(\n" + + " () -> new MyPresenter()));\n" + + " }\n" + + "}" + ) + + assertThat( + lintProject( + caFragmentStub, + caBasePluginStub, + caFragmentPluginStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + fragment + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testFragment_doesntImplementInterface_hasWarning_java7() { + val fragment = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.plugin.*;\n" + + "import com.pascalwelsch.compositeandroid.fragment.*;\n" + + "public class MyFragment extends CompositeFragment {\n" + + " public MyFragment() {\n" + + " addPlugin(new TiFragmentPlugin<>(\n" + + " new Runnable() {\n" + + " @Override\n" + + " public void run() {\n" + + " new MyPresenter();\n" + + " }\n" + + " }));" + + " }\n" + + "}" + ) + + assertThat( + lintProject( + caFragmentStub, + caBasePluginStub, + caFragmentPluginStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + fragment + ) + ).containsOnlyOnce(TiIssue.MissingView.id) + } + + fun testFragment_doesntImplementInterface_hasWarning_java8() { + val fragment = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.plugin.*;\n" + + "import com.pascalwelsch.compositeandroid.fragment.*;\n" + + "public class MyFragment extends CompositeFragment {\n" + + " public MyFragment() {\n" + + " addPlugin(new TiFragmentPlugin(\n" + + " () -> new MyPresenter()));\n" + + " }\n" + + "}" + ) + + assertThat( + lintProject( + caFragmentStub, + caBasePluginStub, + caFragmentPluginStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + fragment + ) + ).containsOnlyOnce(TiIssue.MissingView.id) + } + + fun testFragment_doesntImplementInterface_butDoesntHavePluginAppliedEither_noWarnings() { + val fragment = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.plugin.*;\n" + + "import com.pascalwelsch.compositeandroid.fragment.*;\n" + + "public class MyFragment extends CompositeFragment {\n" + + "}" + ) + + assertThat( + lintProject( + caFragmentStub, + caBasePluginStub, + caFragmentPluginStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + fragment + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testFragment_appliesUnrelatedPlugin_noWarnings() { + val otherPlugin = java( + "package foo;\n" + + "import com.pascalwelsch.compositeandroid.*;\n" + + "public class OtherPlugin implements Plugin {\n" + + "}" + ) + + val fragment = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.plugin.*;\n" + + "import com.pascalwelsch.compositeandroid.fragment.*;\n" + + "public class MyFragment extends CompositeFragment {\n" + + " public MyFragment() {\n" + + " addPlugin(new OtherPlugin());\n" + + " }\n" + + "}" + ) + + assertThat( + lintProject( + caFragmentStub, + caBasePluginStub, + caFragmentPluginStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + otherPlugin, + fragment + ) + ).isEqualTo(NO_WARNINGS) + } +} \ No newline at end of file diff --git a/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInThirtyInchDetectorTest.kt b/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInThirtyInchDetectorTest.kt new file mode 100644 index 00000000..8adb175e --- /dev/null +++ b/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInThirtyInchDetectorTest.kt @@ -0,0 +1,850 @@ +package net.grandcentrix.thirtyinch.lint + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import net.grandcentrix.thirtyinch.lint.detector.MissingViewInThirtyInchDetector +import org.assertj.core.api.Assertions.* + +private const val NO_WARNINGS = "No warnings." + +class MissingViewInThirtyInchDetectorTest : LintDetectorTest() { + + /* Stubbed-out source files */ + + private val tiActivityStub = java( + "package net.grandcentrix.thirtyinch;\n" + + "public abstract class TiActivity

, V extends TiView> {\n" + + "}" + ) + + private val tiFragmentStub = java( + "package net.grandcentrix.thirtyinch;\n" + + "public abstract class TiFragment

, V extends TiView> {\n" + + "}" + ) + + private val tiPresenterStub = java( + "package net.grandcentrix.thirtyinch;\n" + + "public abstract class TiPresenter {\n" + + "}" + ) + + private val tiViewStub = java( + "package net.grandcentrix.thirtyinch;\n" + + "public interface TiView {\n" + + "}" + ) + + private val view = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "interface MyView extends TiView {\n" + + "}" + ) + + private val presenter = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "final class MyPresenter extends TiPresenter {\n" + + "}" + ) + + /* Overrides */ + + override fun getDetector(): Detector = MissingViewInThirtyInchDetector() + + override fun getIssues(): MutableList = mutableListOf( + MissingViewInThirtyInchDetector.ISSUE) + + /* + * -------------------------------------------------------------------------------- + * TiActivity + * -------------------------------------------------------------------------------- + */ + + fun testJava_Activity_dontTriggerOnAbstractClass() { + val activity = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public abstract class MyActivity extends TiActivity {\n" + + "}" + ) + + assertThat( + lintProject( + tiActivityStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + activity + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testKotlin_Activity_dontTriggerOnAbstractClass() { + val activity = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "abstract class MyActivity : TiActivity() {\n" + + "}" + ) + + assertThat( + lintProject( + tiActivityStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + activity + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testJava_Activity_andViewIsImplementedCorrectly_noWarnings() { + val activity = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public class MyActivity extends TiActivity implements MyView {\n" + + "}" + ) + + assertThat( + lintProject( + tiActivityStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + activity + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testKotlin_Activity_andViewIsImplementedCorrectly_noWarnings() { + val activity = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "class MyActivity : TiActivity(), MyView {\n" + + "}" + ) + + assertThat( + lintProject( + tiActivityStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + activity + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testJava_Activity_doesntImplementInterface_hasWarning() { + val activity = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public class MyActivity extends TiActivity {\n" + + "}" + ) + + assertThat( + lintProject( + tiActivityStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + activity + ) + ).containsOnlyOnce(TiIssue.MissingView.id) + } + + fun testKotlin_Activity_doesntImplementInterface_hasWarning() { + val activity = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "class MyActivity : TiActivity() {\n" + + "}" + ) + + assertThat( + lintProject( + tiActivityStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + activity + ) + ).containsOnlyOnce(TiIssue.MissingView.id) + } + + fun testJava_Activity_doesntImplementInterface_butOverridesProvideView_noWarnings() { + val activity = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public class MyActivity extends TiActivity {\n" + + " public MyView provideView() {\n" + + " return null;\n" + + " }\n" + + "}" + ) + + assertThat( + lintProject( + tiActivityStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + activity + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testKotlin_Activity_doesntImplementInterface_butOverridesProvideView_noWarnings() { + val activity = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "class MyActivity : TiActivity() {\n" + + " fun provideView() : MyView {\n" + + " return null\n" + + " }\n" + + "}" + ) + + assertThat( + lintProject( + tiActivityStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + activity + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testJava_Activity_throughTransitiveBaseClass_hasWarning() { + val baseActivity = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public abstract class BaseActivity

, V extends TiView> extends TiActivity {\n" + + "}" + ) + + val activity = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public class MyActivity extends BaseActivity {\n" + + "}" + ) + + assertThat( + lintProject( + tiActivityStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + baseActivity, + activity + ) + ).containsOnlyOnce(TiIssue.MissingView.id) + } + + fun testKotlin_Activity_throughTransitiveBaseClass_hasWarning() { + val baseActivity = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "abstract class BaseActivity

, V : TiView> : TiActivity() {\n" + + "}" + ) + + val activity = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "class MyActivity : BaseActivity() {\n" + + "}" + ) + + assertThat( + lintProject( + tiActivityStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + baseActivity, + activity + ) + ).containsOnlyOnce(TiIssue.MissingView.id) + } + + fun testJava_Activity_throughTransitiveBaseClass_withBasePresenter_hasWarning() { + val basePresenter = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public abstract class BasePresenter extends TiPresenter {\n" + + "}" + ) + + val baseActivity = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public abstract class BaseActivity

, V extends TiView> extends TiActivity {\n" + + "}" + ) + + val activity = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public class MyActivity extends BaseActivity {\n" + + "}" + ) + + val customPresenter = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "final class MyPresenter extends BasePresenter {\n" + + "}" + ) + + assertThat( + lintProject( + tiActivityStub, + tiPresenterStub, + tiViewStub, + basePresenter, + customPresenter, + view, + baseActivity, + activity + ) + ).containsOnlyOnce(TiIssue.MissingView.id) + } + + fun testKotlin_Activity_throughTransitiveBaseClass_withBasePresenter_hasWarning() { + val basePresenter = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "abstract class BasePresenter : TiPresenter() {\n" + + "}" + ) + + val baseActivity = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "abstract class BaseActivity

, V : TiView> : TiActivity() {\n" + + "}" + ) + + val activity = kotlin( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "class MyActivity : BaseActivity() {\n" + + "}" + ) + + val customPresenter = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "class MyPresenter : BasePresenter() {\n" + + "}" + ) + + assertThat( + lintProject( + tiActivityStub, + tiPresenterStub, + tiViewStub, + basePresenter, + customPresenter, + view, + baseActivity, + activity + ) + ).containsOnlyOnce(TiIssue.MissingView.id) + } + + fun testJava_Activity_throughTransitiveBaseClass_withBasePresenter_noWarning() { + val basePresenter = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public abstract class BasePresenter extends TiPresenter {\n" + + "}" + ) + + val baseActivity = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public abstract class BaseActivity

, V extends TiView> extends TiActivity {\n" + + "}" + ) + + val activity = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public class MyActivity extends BaseActivity implements MyView {\n" + + "}" + ) + + val customPresenter = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "final class MyPresenter extends BasePresenter implements MyView {\n" + + "}" + ) + + assertThat( + lintProject( + tiActivityStub, + tiPresenterStub, + tiViewStub, + basePresenter, + customPresenter, + view, + baseActivity, + activity + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testKotlin_Activity_throughTransitiveBaseClass_withBasePresenter_noWarning() { + val basePresenter = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "abstract class BasePresenter : TiPresenter() {\n" + + "}" + ) + + val baseActivity = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "abstract class BaseActivity

, V : TiView> : TiActivity() {\n" + + "}" + ) + + val activity = kotlin( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "class MyActivity : BaseActivity(), MyView {\n" + + "}" + ) + + val customPresenter = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "class MyPresenter : BasePresenter(), MyView {\n" + + "}" + ) + + assertThat( + lintProject( + tiActivityStub, + tiPresenterStub, + tiViewStub, + basePresenter, + customPresenter, + view, + baseActivity, + activity + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testKotlin_Activity_throughBaseClass_noWarning() { + val baseActivity = kotlin( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public class BaseActivity : TiActivity, MyView>(), MyView {\n" + + "}" + ) + + val activity = kotlin( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "class MyActivity : BaseActivity {\n" + + "}" + ) + + assertThat( + lintProject( + tiActivityStub, + tiPresenterStub, + tiViewStub, + view, + baseActivity, + activity + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testKotlin_Activity_throughBaseClass_hasWarning() { + val baseActivity = kotlin( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public class BaseActivity : TiActivity, MyView>() {\n" + + "}" + ) + + val activity = kotlin( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "class MyActivity : BaseActivity {\n" + + "}" + ) + + assertThat( + lintProject( + tiActivityStub, + tiPresenterStub, + tiViewStub, + view, + baseActivity, + activity + ) + ).containsOnlyOnce(TiIssue.MissingView.id) + } + + /* + * -------------------------------------------------------------------------------- + * TiFragment + * -------------------------------------------------------------------------------- + */ + + fun testFragment_dontTriggerOnAbstractClass() { + val fragment = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public abstract class MyFragment extends TiFragment {\n" + + "}" + ) + + assertThat( + lintProject( + tiFragmentStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + fragment + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testFragment_andViewIsImplementedCorrectly_noWarnings() { + val fragment = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public class MyFragment extends TiFragment implements MyView {\n" + + "}" + ) + + assertThat(lintProject( + tiFragmentStub, tiPresenterStub, tiViewStub, + presenter, view, fragment)) + .isEqualTo(NO_WARNINGS) + } + + fun testFragment_doesntImplementInterface_hasWarning() { + val fragment = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public class MyFragment extends TiFragment {\n" + + "}" + ) + + assertThat( + lintProject( + tiFragmentStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + fragment + ) + ).containsOnlyOnce(TiIssue.MissingView.id) + } + + fun testFragment_doesntImplementInterface_butOverridesProvideView_noWarnings() { + val fragment = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public class MyFragment extends TiFragment {\n" + + " public MyView provideView() {\n" + + " return null;\n" + + " }\n" + + "}" + ) + + assertThat( + lintProject( + tiFragmentStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + fragment + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testFragment_throughTransitiveBaseClass_hasWarning() { + val baseFragment = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public abstract class BaseFragment

, V extends TiView> extends TiFragment {\n" + + "}" + ) + + val fragment = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public class MyFragment extends BaseFragment {\n" + + "}" + ) + + assertThat( + lintProject( + tiFragmentStub, + tiPresenterStub, + tiViewStub, + presenter, + view, + baseFragment, + fragment + ) + ).containsOnlyOnce(TiIssue.MissingView.id) + } + + fun testJava_Fragment_throughTransitiveBaseClass_withBasePresenter_hasWarning() { + val basePresenter = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public abstract class BasePresenter extends TiPresenter {\n" + + "}" + ) + + val baseFragment = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public abstract class BaseFragment

, V extends TiView> extends TiFragment {\n" + + "}" + ) + + val fragment = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public class MyFragment extends BaseFragment {\n" + + "}" + ) + + val customPresenter = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "final class MyPresenter extends BasePresenter {\n" + + "}" + ) + + assertThat( + lintProject( + tiFragmentStub, + tiPresenterStub, + tiViewStub, + basePresenter, + customPresenter, + view, + baseFragment, + fragment + ) + ).containsOnlyOnce(TiIssue.MissingView.id) + } + + fun testKotlin_Fragment_throughTransitiveBaseClass_withBasePresenter_hasWarning() { + val basePresenter = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "abstract class BasePresenter : TiPresenter() {\n" + + "}" + ) + + val baseFragment = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "abstract class BaseFragment

, V : TiView> : TiFragment() {\n" + + "}" + ) + + val fragment = kotlin( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "class MyFragment : BaseFragment() {\n" + + "}" + ) + + val customPresenter = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "class MyPresenter : BasePresenter() {\n" + + "}" + ) + + assertThat( + lintProject( + tiFragmentStub, + tiPresenterStub, + tiViewStub, + basePresenter, + customPresenter, + view, + baseFragment, + fragment + ) + ).containsOnlyOnce(TiIssue.MissingView.id) + } + + fun testJava_Fragment_throughTransitiveBaseClass_withBasePresenter_noWarning() { + val basePresenter = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public abstract class BasePresenter extends TiPresenter {\n" + + "}" + ) + + val baseFragment = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public abstract class BaseFragment

, V extends TiView> extends TiFragment {\n" + + "}" + ) + + val fragment = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public class MyFragment extends BaseFragment implements MyView {\n" + + "}" + ) + + val customPresenter = java( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "final class MyPresenter extends BasePresenter implements MyView {\n" + + "}" + ) + + assertThat( + lintProject( + tiFragmentStub, + tiPresenterStub, + tiViewStub, + basePresenter, + customPresenter, + view, + baseFragment, + fragment + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testKotlin_Fragment_throughTransitiveBaseClass_withBasePresenter_noWarning() { + val basePresenter = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "abstract class BasePresenter : TiPresenter() {\n" + + "}" + ) + + val baseFragment = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "abstract class BaseFragment

, V : TiView> : TiFragment() {\n" + + "}" + ) + + val fragment = kotlin( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "class MyActivity : BaseFragment(), MyView {\n" + + "}" + ) + + val customPresenter = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "class MyPresenter : BasePresenter(), MyView {\n" + + "}" + ) + + assertThat( + lintProject( + tiFragmentStub, + tiPresenterStub, + tiViewStub, + basePresenter, + customPresenter, + view, + baseFragment, + fragment + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testKotlin_Fragment_throughBaseClass_noWarning() { + val baseFragment = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "class BaseFragment : TiFragment, MyView>(), MyView {\n" + + "}" + ) + + val fragment = kotlin( + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "class MyFragment : BaseFragment {\n" + + "}" + ) + + assertThat( + lintProject( + tiFragmentStub, + tiPresenterStub, + tiViewStub, + view, + baseFragment, + fragment + ) + ).isEqualTo(NO_WARNINGS) + } + + fun testKotlin_Fragment_throughBaseClass_hasWarning() { + val baseFragment = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "class BaseFragment : TiFragment, MyView>() {\n" + + "}" + ) + + val fragment = kotlin( + "package foo\n" + + "import net.grandcentrix.thirtyinch.*\n" + + "class MyFragment : BaseFragment {\n" + + "}" + ) + + assertThat( + lintProject( + tiFragmentStub, + tiPresenterStub, + tiViewStub, + view, + baseFragment, + fragment + ) + ).containsOnlyOnce(TiIssue.MissingView.id) + } +} \ No newline at end of file diff --git a/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/TiLintRegistryTest.kt b/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/TiLintRegistryTest.kt new file mode 100644 index 00000000..60fa4e5c --- /dev/null +++ b/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/TiLintRegistryTest.kt @@ -0,0 +1,18 @@ +package net.grandcentrix.thirtyinch.lint + +import net.grandcentrix.thirtyinch.lint.detector.MissingViewInCompositeDetector +import net.grandcentrix.thirtyinch.lint.detector.MissingViewInThirtyInchDetector +import org.assertj.core.api.Assertions.* +import org.junit.* + +class IssueRegistryTest { + + @Test + fun testIssueList() { + assertThat(TiLintRegistry().issues) + .containsExactly( + MissingViewInThirtyInchDetector.ISSUE, + MissingViewInCompositeDetector.ISSUE + ) + } +} \ No newline at end of file diff --git a/thirtyinch/build.gradle b/thirtyinch/build.gradle index cc6ed45f..0d6f4545 100644 --- a/thirtyinch/build.gradle +++ b/thirtyinch/build.gradle @@ -30,6 +30,10 @@ android { } } +configurations { + lintChecks +} + dependencies { compileOnly "com.android.support:appcompat-v7:$supportLibraryVersion" @@ -41,6 +45,8 @@ dependencies { androidTestImplementation "com.android.support.test:runner:$supportTestVersion" androidTestImplementation "org.mockito:mockito-core:$mockitoVersion" androidTestImplementation "org.assertj:assertj-core:$assertjVersion" + + lintChecks project(path: ":thirtyinch-lint", configuration: "lintChecks") } // For uploading to bintray @@ -49,3 +55,16 @@ configurePublish { artifactId = 'thirtyinch' desc = 'a Model View Presenter library for Android' } + +task copyLintJar(type: Copy) { + from(configurations.lintChecks) { + rename { String fileName -> 'lint.jar' } + } + into 'build/intermediates/lint/' +} + + +project.afterEvaluate { + def compileLintTask = project.tasks.find { it.name == 'compileLint' } + compileLintTask.dependsOn(copyLintJar) +}