diff --git a/build.gradle b/build.gradle index f1c7d4f9..05433ab5 100644 --- a/build.gradle +++ b/build.gradle @@ -28,10 +28,14 @@ ext { COMPILE_SDK_VERSION = 25 BUILD_TOOLS_VERSION = '25.0.2' + kotlinVersion = '1.1.3-2' + supportLibraryVersion = '25.2.0' bintrayVersion = '0.3.4' junitVersion = '4.12' mockitoVersion = '1.10.19' assertjVersion = '2.5.0' + + lintVersion = '25.4.0-alpha7' } diff --git a/lint/.gitignore b/lint/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/lint/.gitignore @@ -0,0 +1 @@ +/build diff --git a/lint/build.gradle b/lint/build.gradle new file mode 100644 index 00000000..b2e81530 --- /dev/null +++ b/lint/build.gradle @@ -0,0 +1,40 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + } +} + +apply plugin: 'kotlin' +apply plugin: 'jacoco' + +targetCompatibility = JavaVersion.VERSION_1_7 +sourceCompatibility = JavaVersion.VERSION_1_7 + +configurations { + lintChecks +} + +jar { + manifest { + attributes('Lint-Registry': 'net.grandcentrix.thirtyinch.IssueRegistry') + } +} + +repositories { + maven { url "https://dl.bintray.com/android/android-tools" } +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + compile "com.android.tools.lint:lint-api:$lintVersion" + compile "com.android.tools.lint:lint-checks:$lintVersion" + + testCompile "com.android.tools.lint:lint:$lintVersion" + testCompile "com.android.tools.lint:lint-tests:$lintVersion" + testCompile "org.assertj:assertj-core:$assertjVersion" + + lintChecks files(jar) +} diff --git a/lint/src/main/kotlin/net/grandcentrix/thirtyinch/BaseMissingViewDetector.kt b/lint/src/main/kotlin/net/grandcentrix/thirtyinch/BaseMissingViewDetector.kt new file mode 100644 index 00000000..ca8a99c5 --- /dev/null +++ b/lint/src/main/kotlin/net/grandcentrix/thirtyinch/BaseMissingViewDetector.kt @@ -0,0 +1,104 @@ +package net.grandcentrix.thirtyinch + +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.PsiClass +import com.intellij.psi.PsiClassType +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 + */ + override abstract fun applicableSuperClasses(): List + + /** + * 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 tryFindViewInterface(context: JavaContext, declaration: UClass, extendedType: PsiClassType, resolvedType: PsiClass): PsiType? + + /** + * 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 + + override final 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 + tryFindViewInterface(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 + context.report( + issue, + context.getLocation(declaration.nameIdentifier), + issue.getBriefDescription(TextFormat.TEXT)) + } + } + } + } + + private fun tryFindViewInterface(context: JavaContext, declaration: UClass): PsiType? { + for (extendedType in declaration.extendsListTypes) { + extendedType.resolveGenerics().element?.let { resolvedType -> + val qualifiedName = resolvedType.qualifiedName + if (applicableSuperClasses().contains(qualifiedName)) { + // This detector is interested in this class; delegate to it + return tryFindViewInterface(context, declaration, extendedType, resolvedType) + } + + // Crawl up the type hierarchy to catch declarations in super classes + val uastContext = declaration.getUastContext() + return tryFindViewInterface(context, uastContext.getClass(resolvedType)) + } + } + + return null + } + + 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/lint/src/main/kotlin/net/grandcentrix/thirtyinch/IssueRegistry.kt b/lint/src/main/kotlin/net/grandcentrix/thirtyinch/IssueRegistry.kt new file mode 100644 index 00000000..e9bbb214 --- /dev/null +++ b/lint/src/main/kotlin/net/grandcentrix/thirtyinch/IssueRegistry.kt @@ -0,0 +1,9 @@ +package net.grandcentrix.thirtyinch + +class IssueRegistry : com.android.tools.lint.client.api.IssueRegistry() { + + override fun getIssues() = listOf( + MissingViewInThirtyInchDetector.ISSUE, + MissingViewInCompositeDetector.ISSUE + ) +} diff --git a/lint/src/main/kotlin/net/grandcentrix/thirtyinch/Issues.kt b/lint/src/main/kotlin/net/grandcentrix/thirtyinch/Issues.kt new file mode 100644 index 00000000..d68118d3 --- /dev/null +++ b/lint/src/main/kotlin/net/grandcentrix/thirtyinch/Issues.kt @@ -0,0 +1,31 @@ +package net.grandcentrix.thirtyinch + +import com.android.tools.lint.detector.api.* +import java.util.* + +enum class Issues( + val id: String, + val briefDescription: String, + val category: Category, + val priority: Int, + val severity: Severity) { + + MISSING_VIEW( + id = "MissingTiViewImplementation", + briefDescription = "TiView Implementation missing in class", + category = Category.CORRECTNESS, + priority = 8, + severity = Severity.ERROR); + + fun create(detectorCls: Class, description: String = briefDescription): Issue = + Issue.create( + id, + briefDescription, + description, + category, + priority, + severity, + Implementation( + detectorCls, + EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES))) +} diff --git a/lint/src/main/kotlin/net/grandcentrix/thirtyinch/MissingViewInCompositeDetector.kt b/lint/src/main/kotlin/net/grandcentrix/thirtyinch/MissingViewInCompositeDetector.kt new file mode 100644 index 00000000..1cb17b63 --- /dev/null +++ b/lint/src/main/kotlin/net/grandcentrix/thirtyinch/MissingViewInCompositeDetector.kt @@ -0,0 +1,98 @@ +package net.grandcentrix.thirtyinch + +import com.android.annotations.VisibleForTesting +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiJavaCodeReferenceElement +import com.intellij.psi.PsiType +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 val ADD_PLUGIN_METHOD = "addPlugin" +private val TI_ACTIVITY_PLUGIN_NAME = "TiActivityPlugin" +private 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 { + @VisibleForTesting + val ISSUE: Issue = Issues.MISSING_VIEW.create( + 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 + get() = ISSUE + + override fun tryFindViewInterface(context: JavaContext, declaration: UClass, extendedType: PsiClassType, resolvedType: PsiClass): PsiType? { + // Expect TiPlugin to be applied in the extended CA class + // Found default constructor + val defaultConstructor = declaration.constructors + .filter { it.typeParameters.isEmpty() } + .firstOrNull() + + 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 +} diff --git a/lint/src/main/kotlin/net/grandcentrix/thirtyinch/MissingViewInThirtyInchDetector.kt b/lint/src/main/kotlin/net/grandcentrix/thirtyinch/MissingViewInThirtyInchDetector.kt new file mode 100644 index 00000000..9bb23bb0 --- /dev/null +++ b/lint/src/main/kotlin/net/grandcentrix/thirtyinch/MissingViewInThirtyInchDetector.kt @@ -0,0 +1,56 @@ +package net.grandcentrix.thirtyinch + +import com.android.annotations.VisibleForTesting +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiType +import org.jetbrains.uast.UClass + +private val TI_VIEW_FQ = "net.grandcentrix.thirtyinch.TiView" +private val PROVIDE_VIEW_METHOD = "provideView" +private val TI_CLASS_NAMES = listOf( + "net.grandcentrix.thirtyinch.TiActivity", + "net.grandcentrix.thirtyinch.TiFragment") + +class MissingViewInThirtyInchDetector : BaseMissingViewDetector() { + + companion object { + @VisibleForTesting + val ISSUE: Issue = Issues.MISSING_VIEW.create( + MissingViewInThirtyInchDetector::class.java, + "When using ThirtyInch, a class extending TiActivity, TiFragment or CompositeActivity " + + "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 + get() = ISSUE + + override fun tryFindViewInterface(context: JavaContext, declaration: UClass, extendedType: PsiClassType, resolvedType: PsiClass): PsiType? { + // Expect

signature in the extended Ti class + val parameters = extendedType.parameters + val parameterTypes = resolvedType.typeParameters + if (parameters.size != 2 || parameterTypes.size != 2) { + return null + } + + // Check that the second type parameter is actually a TiView + val parameterType = parameterTypes[1] + val parameter = parameters[1] + return parameterType.extendsListTypes + .map { it.resolveGenerics().element } + .filter { TI_VIEW_FQ == it?.qualifiedName } + .map { parameter } + .firstOrNull() + } + + 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 } + } +} diff --git a/lint/src/test/java/net/grandcentrix/thirtyinch/MissingViewInCompositeDetectorTest.java b/lint/src/test/java/net/grandcentrix/thirtyinch/MissingViewInCompositeDetectorTest.java new file mode 100644 index 00000000..959c05dd --- /dev/null +++ b/lint/src/test/java/net/grandcentrix/thirtyinch/MissingViewInCompositeDetectorTest.java @@ -0,0 +1,279 @@ +package net.grandcentrix.thirtyinch; + +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 java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MissingViewInCompositeDetectorTest extends LintDetectorTest { + + private static final String NO_WARNINGS = "No warnings."; + + /* Stubbed-out source files */ + + private final TestFile tiPresenterStub = java("" + + "package net.grandcentrix.thirtyinch;\n" + + "public abstract class TiPresenter {\n" + + "}"); + + private final TestFile tiViewStub = java("" + + "package net.grandcentrix.thirtyinch;\n" + + "public interface TiView {\n" + + "}"); + + private final TestFile caBasePluginStub = java("" + + "package com.pascalwelsch.compositeandroid;\n" + + "public interface Plugin {\n" + + "}"); + + private final TestFile 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 final TestFile 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 final TestFile 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 final TestFile 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 final TestFile view = java("" + + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "interface MyView extends TiView {\n" + + "}"); + + private final TestFile presenter = java("" + + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "final class MyPresenter extends TiPresenter {\n" + + "}"); + + /* + * -------------------------------------------------------------------------------- + * CompositeActivity + * -------------------------------------------------------------------------------- + */ + + public void testActivity_dontTriggerOnAbstractClass() throws Exception { + TestFile 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); + } + + public void testActivity_andViewIsImplementedCorrectly_noWarnings() throws Exception { + TestFile 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); + } + + public void testActivity_doesntImplementInterface_hasWarning() throws Exception { + TestFile 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(Issues.MISSING_VIEW.getId()); + } + + public void testActivity_doesntImplementInterface_butDoesntHavePluginAppliedEither_noWarnings() throws Exception { + TestFile 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 + * -------------------------------------------------------------------------------- + */ + + public void testFragment_dontTriggerOnAbstractClass() throws Exception { + TestFile 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); + } + + public void testFragment_andViewIsImplementedCorrectly_noWarnings() throws Exception { + TestFile 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); + } + + @SuppressWarnings("Convert2Lambda") + public void testFragment_doesntImplementInterface_hasWarning_java7() throws Exception { + TestFile 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(Issues.MISSING_VIEW.getId()); + } + + public void testFragment_doesntImplementInterface_hasWarning_java8() throws Exception { + TestFile 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(Issues.MISSING_VIEW.getId()); + } + + public void testFragment_doesntImplementInterface_butDoesntHavePluginAppliedEither_noWarnings() throws Exception { + TestFile 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); + } + + public void testFragment_appliesUnrelatedPlugin_noWarnings() throws Exception { + TestFile otherPlugin = java("" + + "package foo;\n" + + "import com.pascalwelsch.compositeandroid.*;\n" + + "public class OtherPlugin implements Plugin {\n" + + "}"); + + TestFile 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); + } + + /* Overrides */ + + @Override + protected Detector getDetector() { + return new MissingViewInCompositeDetector(); + } + + @Override + protected List getIssues() { + return Collections.singletonList(MissingViewInCompositeDetector.Companion.getISSUE()); + } +} diff --git a/lint/src/test/java/net/grandcentrix/thirtyinch/MissingViewInThirtyInchDetectorTest.java b/lint/src/test/java/net/grandcentrix/thirtyinch/MissingViewInThirtyInchDetectorTest.java new file mode 100644 index 00000000..8da8caae --- /dev/null +++ b/lint/src/test/java/net/grandcentrix/thirtyinch/MissingViewInThirtyInchDetectorTest.java @@ -0,0 +1,220 @@ +package net.grandcentrix.thirtyinch; + +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 java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MissingViewInThirtyInchDetectorTest extends LintDetectorTest { + + private static final String NO_WARNINGS = "No warnings."; + + /* Stubbed-out source files */ + + private final TestFile tiActivityStub = java("" + + "package net.grandcentrix.thirtyinch;\n" + + "public abstract class TiActivity

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

, V extends TiView> {\n" + + "}"); + + private final TestFile tiPresenterStub = java("" + + "package net.grandcentrix.thirtyinch;\n" + + "public abstract class TiPresenter {\n" + + "}"); + + private final TestFile tiViewStub = java("" + + "package net.grandcentrix.thirtyinch;\n" + + "public interface TiView {\n" + + "}"); + + private final TestFile view = java("" + + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "interface MyView extends TiView {\n" + + "}"); + + private final TestFile presenter = java("" + + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "final class MyPresenter extends TiPresenter {\n" + + "}"); + + /* + * -------------------------------------------------------------------------------- + * TiActivity + * -------------------------------------------------------------------------------- + */ + + public void testActivity_dontTriggerOnAbstractClass() throws Exception { + TestFile 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); + } + + public void testActivity_andViewIsImplementedCorrectly_noWarnings() throws Exception { + TestFile 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); + } + + public void testActivity_doesntImplementInterface_hasWarning() throws Exception { + TestFile 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(Issues.MISSING_VIEW.getId()); + } + + public void testActivity_doesntImplementInterface_butOverridesProvideView_noWarnings() throws Exception { + TestFile 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); + } + + public void testActivity_throughTransitiveBaseClass_hasWarning() throws Exception { + TestFile baseActivity = java("" + + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public abstract class BaseActivity

, V extends TiView> extends TiActivity {\n" + + "}"); + + TestFile 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(Issues.MISSING_VIEW.getId()); + } + /* + * -------------------------------------------------------------------------------- + * TiFragment + * -------------------------------------------------------------------------------- + */ + + public void testFragment_dontTriggerOnAbstractClass() throws Exception { + TestFile 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); + } + + public void testFragment_andViewIsImplementedCorrectly_noWarnings() throws Exception { + TestFile 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); + } + + public void testFragment_doesntImplementInterface_hasWarning() throws Exception { + TestFile 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(Issues.MISSING_VIEW.getId()); + } + + public void testFragment_doesntImplementInterface_butOverridesProvideView_noWarnings() throws Exception { + TestFile 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); + } + + public void testFragment_throughTransitiveBaseClass_hasWarning() throws Exception { + TestFile baseFragment = java("" + + "package foo;\n" + + "import net.grandcentrix.thirtyinch.*;\n" + + "public abstract class BaseFragment

, V extends TiView> extends TiFragment {\n" + + "}"); + + TestFile 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(Issues.MISSING_VIEW.getId()); + } + + /* Overrides */ + + @Override + protected Detector getDetector() { + return new MissingViewInThirtyInchDetector(); + } + + @Override + protected List getIssues() { + return Collections.singletonList(MissingViewInThirtyInchDetector.Companion.getISSUE()); + } +} diff --git a/lint/src/test/kotlin/net/grandcentrix/thirtyinch/IssueRegistryTest.kt b/lint/src/test/kotlin/net/grandcentrix/thirtyinch/IssueRegistryTest.kt new file mode 100644 index 00000000..76909e6d --- /dev/null +++ b/lint/src/test/kotlin/net/grandcentrix/thirtyinch/IssueRegistryTest.kt @@ -0,0 +1,15 @@ +package net.grandcentrix.thirtyinch + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class IssueRegistryTest { + + @Test fun testIssueList() { + assertThat(IssueRegistry().issues) + .containsExactly( + MissingViewInThirtyInchDetector.ISSUE, + MissingViewInCompositeDetector.ISSUE + ) + } +} diff --git a/settings.gradle b/settings.gradle index f0bb3c73..8cdabc03 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ include ':thirtyinch', ':plugin', ':sample', ':rx', ':test', ':plugin-test', ':rx2', - ':logginginterceptor' + ':logginginterceptor', ':lint' diff --git a/thirtyinch/build.gradle b/thirtyinch/build.gradle index 290cae18..f37ef6b7 100644 --- a/thirtyinch/build.gradle +++ b/thirtyinch/build.gradle @@ -41,9 +41,15 @@ android { } } +configurations { + lintChecks +} + dependencies { provided "com.android.support:appcompat-v7:$supportLibraryVersion" + lintChecks project(path: ':lint', configuration: 'lintChecks') + testCompile "junit:junit:$junitVersion" testCompile "org.mockito:mockito-core:$mockitoVersion" testCompile "org.assertj:assertj-core:$assertjVersion" @@ -52,6 +58,18 @@ dependencies { androidTestCompile "org.mockito:mockito-core:$mockitoVersion" } +task copyLintJar(type: Copy) { + from(configurations.lintChecks) { + rename { 'lint.jar' } + } + into 'build/intermediates/lint/' +} + +project.afterEvaluate { + def compileLintTask = project.tasks.find { it.name == 'compileLint' } + compileLintTask.dependsOn(copyLintJar) +} + publish { userOrg = 'passsy' groupId = 'net.grandcentrix.thirtyinch'