From 81221da11978b6c9ba46f19b669580ef7fe97efe Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Wed, 1 Aug 2018 13:39:46 +0200 Subject: [PATCH 01/26] initial setup of the thirty inch-lint module --- build.gradle | 1 + settings.gradle | 3 ++- thirtyinch-lint/.gitignore | 7 ++++++ thirtyinch-lint/README.md | 1 + thirtyinch-lint/build.gradle | 42 ++++++++++++++++++++++++++++++++++++ 5 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 thirtyinch-lint/.gitignore create mode 100644 thirtyinch-lint/README.md create mode 100644 thirtyinch-lint/build.gradle diff --git a/build.gradle b/build.gradle index 433071f4..fdd85aa1 100644 --- a/build.gradle +++ b/build.gradle @@ -45,4 +45,5 @@ ext { assertjVersion = '2.8.0' supportTestVersion = '1.0.1' espressoVersion = '3.0.1' + lintVersion = '26.1.2' } 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/README.md b/thirtyinch-lint/README.md new file mode 100644 index 00000000..4c49205f --- /dev/null +++ b/thirtyinch-lint/README.md @@ -0,0 +1 @@ +# ThirtyInch - Lint diff --git a/thirtyinch-lint/build.gradle b/thirtyinch-lint/build.gradle new file mode 100644 index 00000000..1c0ac0f6 --- /dev/null +++ b/thirtyinch-lint/build.gradle @@ -0,0 +1,42 @@ +plugins { + id "org.jetbrains.kotlin.jvm" + id "jacoco" +} + +repositories { + jcenter() +} + +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 "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + 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" +} + +jar { + manifest { + } +} + +sourceCompatibility = 1.6 + +defaultTasks 'assemble' +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} + +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} From b6bfdadcff80779b031a6eceab30da4821651fe7 Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Wed, 1 Aug 2018 14:11:21 +0200 Subject: [PATCH 02/26] first Implementation for Java inspired by #111 --- thirtyinch-lint/build.gradle | 1 + .../grandcentrix/thirtyinch/lint/TiIssue.kt | 40 ++ .../thirtyinch/lint/TiLintRegistry.kt | 18 + .../lint/detector/BaseMissingViewDetector.kt | 100 +++++ .../MissingViewInCompositeDetector.kt | 89 +++++ .../MissingViewInThirtyInchDetector.kt | 57 +++ .../MissingViewInCompositeDetectorTest.kt | 371 ++++++++++++++++++ .../MissingViewInThirtyInchDetectorTest.kt | 286 ++++++++++++++ .../thirtyinch/lint/TiLintRegistryTest.kt | 18 + 9 files changed, 980 insertions(+) create mode 100644 thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiIssue.kt create mode 100644 thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiLintRegistry.kt create mode 100644 thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/detector/BaseMissingViewDetector.kt create mode 100644 thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/detector/MissingViewInCompositeDetector.kt create mode 100644 thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/detector/MissingViewInThirtyInchDetector.kt create mode 100644 thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInCompositeDetectorTest.kt create mode 100644 thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInThirtyInchDetectorTest.kt create mode 100644 thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/TiLintRegistryTest.kt diff --git a/thirtyinch-lint/build.gradle b/thirtyinch-lint/build.gradle index 1c0ac0f6..fb94d6af 100644 --- a/thirtyinch-lint/build.gradle +++ b/thirtyinch-lint/build.gradle @@ -23,6 +23,7 @@ dependencies { jar { manifest { + attributes("Lint-Registry": "net.grandcentrix.thirtyinch.lint.TiLintRegistry") } } 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..af6c4f6c --- /dev/null +++ b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiIssue.kt @@ -0,0 +1,40 @@ +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 +import java.util.EnumSet + +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.CORRECTNESS, + priority = 8, + severity = Severity.ERROR + ) + + fun asLintIssue(detectorCls: Class, description: String = briefDescription): Issue = + Issue.create( + id, + briefDescription, + description, + category, + priority, + severity, + Implementation( + detectorCls, + EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) + ) + ) +} \ 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..0ce377cc --- /dev/null +++ b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiLintRegistry.kt @@ -0,0 +1,18 @@ +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) + } + ) +} 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..d91f4ba7 --- /dev/null +++ b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/detector/BaseMissingViewDetector.kt @@ -0,0 +1,100 @@ +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.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 + */ + abstract override 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 + + 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 + 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.navigationElement), + 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/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..cfb56c70 --- /dev/null +++ b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/detector/MissingViewInCompositeDetector.kt @@ -0,0 +1,89 @@ +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.PsiClass +import com.intellij.psi.PsiClassType +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; get() = MissingViewInThirtyInchDetector.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.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..0fe39fff --- /dev/null +++ b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/detector/MissingViewInThirtyInchDetector.kt @@ -0,0 +1,57 @@ +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.PsiClass +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiType +import net.grandcentrix.thirtyinch.lint.TiIssue.MissingView +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" +) + +class MissingViewInThirtyInchDetector : BaseMissingViewDetector() { + companion object { + val ISSUE = MissingView.asLintIssue( + 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 } + } +} \ 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..d975f2fd --- /dev/null +++ b/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInThirtyInchDetectorTest.kt @@ -0,0 +1,286 @@ +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 testActivity_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 testActivity_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 testActivity_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 testActivity_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 testActivity_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) + } + /* + * -------------------------------------------------------------------------------- + * 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) + } +} \ 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 From 616ad0b4dcae765933829f4eddd3962a7c93c7b5 Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Mon, 27 Aug 2018 11:24:47 +0200 Subject: [PATCH 03/26] configures gradle to include the TI lint checks in the TI lib --- thirtyinch-lint/build.gradle | 7 +++++++ thirtyinch/build.gradle | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/thirtyinch-lint/build.gradle b/thirtyinch-lint/build.gradle index fb94d6af..54fe3ff6 100644 --- a/thirtyinch-lint/build.gradle +++ b/thirtyinch-lint/build.gradle @@ -7,6 +7,10 @@ repositories { jcenter() } +configurations { + lintChecks +} + dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" @@ -19,10 +23,13 @@ dependencies { 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") } } 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) +} From 770dfd621bf3feb49261369e8db46bef04491b85 Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Mon, 27 Aug 2018 11:25:27 +0200 Subject: [PATCH 04/26] fixes not executed lint checks in AS --- .../src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiIssue.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index af6c4f6c..db99a490 100644 --- a/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiIssue.kt +++ b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiIssue.kt @@ -34,7 +34,7 @@ sealed class TiIssue( severity, Implementation( detectorCls, - EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) + Scope.JAVA_FILE_SCOPE ) ) } \ No newline at end of file From 040e11713310267b0729e19f41f2607004406e5f Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Mon, 27 Aug 2018 11:25:47 +0200 Subject: [PATCH 05/26] create own TI lint category --- .../main/kotlin/net/grandcentrix/thirtyinch/lint/TiIssue.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index db99a490..071f7164 100644 --- a/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiIssue.kt +++ b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiIssue.kt @@ -6,7 +6,8 @@ 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 -import java.util.EnumSet + +private val CATEGORY_TI = Category.create("ThirtyInch", 5) sealed class TiIssue( val id: String, @@ -19,7 +20,7 @@ sealed class TiIssue( object MissingView : TiIssue( id = "MissingTiViewImplementation", briefDescription = "TiView Implementation missing in class", - category = Category.CORRECTNESS, + category = CATEGORY_TI, priority = 8, severity = Severity.ERROR ) From aebadbe8fa875d31ff89a8b857830ecbc018178f Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Mon, 27 Aug 2018 13:42:39 +0200 Subject: [PATCH 06/26] improves the highlighted editor element for the TiViewMissing lint issue --- .../lint/detector/BaseMissingViewDetector.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 index d91f4ba7..b7bc22c4 100644 --- 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 @@ -59,10 +59,12 @@ abstract class BaseMissingViewDetector : Detector(), Detector.UastScanner { // 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.navigationElement), - issue.getBriefDescription(TextFormat.TEXT)) + declaration.nameIdentifier?.run { + context.report( + issue, + context.getLocation(this.originalElement), + issue.getBriefDescription(TextFormat.TEXT)) + } } } } From 0166387a2af4a580f4b2782a7c469eda7168a0b0 Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Mon, 27 Aug 2018 14:14:32 +0200 Subject: [PATCH 07/26] some comment cleanup --- .../lint/detector/BaseMissingViewDetector.kt | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) 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 index b7bc22c4..d95c42ff 100644 --- 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 @@ -15,8 +15,7 @@ import org.jetbrains.uast.getUastContext abstract class BaseMissingViewDetector : Detector(), Detector.UastScanner { /** - * The Issue that the detector is connected to, - * reported on illegal state detection + * The Issue that the detector is connected to, reported on illegal state detection */ abstract val issue: Issue @@ -27,20 +26,16 @@ abstract class BaseMissingViewDetector : Detector(), Detector.UastScanner { abstract override 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. + * 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. + * 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 From d88c24cc1536b033d5371b1745d7e3cf0b40992b Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Mon, 27 Aug 2018 14:58:24 +0200 Subject: [PATCH 08/26] adds comments about the correct lintVersion --- build.gradle | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index fdd85aa1..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,5 +45,9 @@ ext { assertjVersion = '2.8.0' supportTestVersion = '1.0.1' espressoVersion = '3.0.1' - lintVersion = '26.1.2' + + // 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' } From 2b8e3f08109983ef4da41470272d1a3442850c4b Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Mon, 27 Aug 2018 14:58:46 +0200 Subject: [PATCH 09/26] also register the Lint-Registry-v2 --- thirtyinch-lint/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/thirtyinch-lint/build.gradle b/thirtyinch-lint/build.gradle index 54fe3ff6..a40ddd9e 100644 --- a/thirtyinch-lint/build.gradle +++ b/thirtyinch-lint/build.gradle @@ -31,6 +31,8 @@ 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") } } From 6fe43a9d6809d84106ac9db1077d8291c6237ae7 Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Tue, 28 Aug 2018 10:35:02 +0200 Subject: [PATCH 10/26] removes the empty README.md --- thirtyinch-lint/README.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 thirtyinch-lint/README.md diff --git a/thirtyinch-lint/README.md b/thirtyinch-lint/README.md deleted file mode 100644 index 4c49205f..00000000 --- a/thirtyinch-lint/README.md +++ /dev/null @@ -1 +0,0 @@ -# ThirtyInch - Lint From 3d43e9ba147ae63ea14725a4e2afcd15d79f3ac8 Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Tue, 28 Aug 2018 10:42:24 +0200 Subject: [PATCH 11/26] removes obsolete kotlin stdlib inclusion --- thirtyinch-lint/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/thirtyinch-lint/build.gradle b/thirtyinch-lint/build.gradle index a40ddd9e..532c6ac7 100644 --- a/thirtyinch-lint/build.gradle +++ b/thirtyinch-lint/build.gradle @@ -17,7 +17,6 @@ dependencies { compileOnly "com.android.tools.lint:lint-api:$lintVersion" compileOnly "com.android.tools.lint:lint-checks:$lintVersion" - testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" testImplementation 'junit:junit:4.12' testImplementation "org.assertj:assertj-core:$assertjVersion" From 9b34699e9bed0efcd0de7f8613e1654d9954ce00 Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Tue, 28 Aug 2018 10:59:03 +0200 Subject: [PATCH 12/26] removes obsolete defaultTasks --- thirtyinch-lint/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/thirtyinch-lint/build.gradle b/thirtyinch-lint/build.gradle index 532c6ac7..50375795 100644 --- a/thirtyinch-lint/build.gradle +++ b/thirtyinch-lint/build.gradle @@ -37,7 +37,6 @@ jar { sourceCompatibility = 1.6 -defaultTasks 'assemble' compileKotlin { kotlinOptions { jvmTarget = "1.8" From 7b5ac3eecee3676fcb6e61ea8535436ce7410d2c Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Tue, 28 Aug 2018 11:03:23 +0200 Subject: [PATCH 13/26] removes obsolete sourceCompatibility --- thirtyinch-lint/build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/thirtyinch-lint/build.gradle b/thirtyinch-lint/build.gradle index 50375795..b1cf717d 100644 --- a/thirtyinch-lint/build.gradle +++ b/thirtyinch-lint/build.gradle @@ -35,8 +35,6 @@ jar { } } -sourceCompatibility = 1.6 - compileKotlin { kotlinOptions { jvmTarget = "1.8" From da0b9164e8d8911aa5bd331ddb73b7b45f0b9e45 Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Tue, 28 Aug 2018 11:05:13 +0200 Subject: [PATCH 14/26] improves the issue val overriding syntax --- .../thirtyinch/lint/detector/MissingViewInCompositeDetector.kt | 2 +- .../thirtyinch/lint/detector/MissingViewInThirtyInchDetector.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index cfb56c70..bb75f47c 100644 --- 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 @@ -33,7 +33,7 @@ class MissingViewInCompositeDetector : BaseMissingViewDetector() { override fun applicableSuperClasses() = CA_CLASS_NAMES - override val issue: Issue; get() = MissingViewInThirtyInchDetector.ISSUE + override val issue: Issue = MissingViewInThirtyInchDetector.ISSUE override fun tryFindViewInterface(context: JavaContext, declaration: UClass, extendedType: PsiClassType, resolvedType: PsiClass): PsiType? { 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 index 0fe39fff..edbec04a 100644 --- 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 @@ -27,7 +27,7 @@ class MissingViewInThirtyInchDetector : BaseMissingViewDetector() { override fun applicableSuperClasses() = TI_CLASS_NAMES - override val issue: Issue; get() = ISSUE + override val issue: Issue = ISSUE override fun tryFindViewInterface(context: JavaContext, declaration: UClass, extendedType: PsiClassType, resolvedType: PsiClass): PsiType? { From fa159cb7236f01342836102721ab8ac8a88da398 Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Tue, 28 Aug 2018 11:07:20 +0200 Subject: [PATCH 15/26] corrects comment --- .../thirtyinch/lint/detector/MissingViewInThirtyInchDetector.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index edbec04a..7e6febd6 100644 --- 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 @@ -19,7 +19,7 @@ class MissingViewInThirtyInchDetector : BaseMissingViewDetector() { companion object { val ISSUE = MissingView.asLintIssue( MissingViewInThirtyInchDetector::class.java, - "When using ThirtyInch, a class extending TiActivity, TiFragment or CompositeActivity " + + "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." ) From dfafaa05543d19f70afe567f9bae8e15836ab835 Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Thu, 13 Sep 2018 11:04:46 +0200 Subject: [PATCH 16/26] adds Kolin Tests --- .../MissingViewInThirtyInchDetectorTest.kt | 121 +++++++++++++++++- 1 file changed, 116 insertions(+), 5 deletions(-) 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 index d975f2fd..7be1e1cb 100644 --- a/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInThirtyInchDetectorTest.kt +++ b/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInThirtyInchDetectorTest.kt @@ -63,7 +63,7 @@ class MissingViewInThirtyInchDetectorTest : LintDetectorTest() { * -------------------------------------------------------------------------------- */ - fun testActivity_dontTriggerOnAbstractClass() { + fun testJava_Activity_dontTriggerOnAbstractClass() { val activity = java( "package foo;\n" + "import net.grandcentrix.thirtyinch.*;\n" + @@ -83,7 +83,27 @@ class MissingViewInThirtyInchDetectorTest : LintDetectorTest() { ).isEqualTo(NO_WARNINGS) } - fun testActivity_andViewIsImplementedCorrectly_noWarnings() { + 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" + @@ -103,7 +123,27 @@ class MissingViewInThirtyInchDetectorTest : LintDetectorTest() { ).isEqualTo(NO_WARNINGS) } - fun testActivity_doesntImplementInterface_hasWarning() { + 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" + @@ -123,7 +163,27 @@ class MissingViewInThirtyInchDetectorTest : LintDetectorTest() { ).containsOnlyOnce(TiIssue.MissingView.id) } - fun testActivity_doesntImplementInterface_butOverridesProvideView_noWarnings() { + 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" + @@ -146,7 +206,30 @@ class MissingViewInThirtyInchDetectorTest : LintDetectorTest() { ).isEqualTo(NO_WARNINGS) } - fun testActivity_throughTransitiveBaseClass_hasWarning() { + 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" + @@ -173,6 +256,34 @@ class MissingViewInThirtyInchDetectorTest : LintDetectorTest() { ) ).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) + } /* * -------------------------------------------------------------------------------- * TiFragment From a0ce1739095b795a03f29f3974529470ff258d04 Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Thu, 13 Sep 2018 13:23:53 +0200 Subject: [PATCH 17/26] adds a more complex inheritance scenario as test --- .../MissingViewInThirtyInchDetectorTest.kt | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) 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 index 7be1e1cb..25c713d1 100644 --- a/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInThirtyInchDetectorTest.kt +++ b/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInThirtyInchDetectorTest.kt @@ -284,6 +284,178 @@ class MissingViewInThirtyInchDetectorTest : LintDetectorTest() { ) ).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 {\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 + ) + ).containsOnlyOnce(TiIssue.MissingView.id) + } + + 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() {\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 + ) + ).containsOnlyOnce(TiIssue.MissingView.id) + } /* * -------------------------------------------------------------------------------- * TiFragment From d339edf0f0ddb5d245bba10dbb6c124c14e92056 Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Thu, 13 Sep 2018 13:25:59 +0200 Subject: [PATCH 18/26] fixes false positive when having a base presenter class --- .../thirtyinch/lint/detector/BaseMissingViewDetector.kt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) 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 index d95c42ff..258382af 100644 --- 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 @@ -68,14 +68,7 @@ abstract class BaseMissingViewDetector : Detector(), Detector.UastScanner { 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 tryFindViewInterface(context, declaration, extendedType, resolvedType) } } return null From fd26e6000d5379f91956ebfd5a73fc0af9e991c6 Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Thu, 13 Sep 2018 13:26:15 +0200 Subject: [PATCH 19/26] adds missing lint version specification --- .../kotlin/net/grandcentrix/thirtyinch/lint/TiLintRegistry.kt | 2 ++ 1 file changed, 2 insertions(+) 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 index 0ce377cc..64159cc1 100644 --- a/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiLintRegistry.kt +++ b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiLintRegistry.kt @@ -15,4 +15,6 @@ class TiLintRegistry : IssueRegistry() { setEnabledByDefault(true) } ) + + override val api: Int = com.android.tools.lint.detector.api.CURRENT_API } From 38892fa67f72c3c5bf63720df9f9b2a71a8bfdce Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Fri, 14 Sep 2018 16:08:34 +0200 Subject: [PATCH 20/26] fixes wrong test conditions --- .../lint/MissingViewInThirtyInchDetectorTest.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 index 25c713d1..04f504fc 100644 --- a/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInThirtyInchDetectorTest.kt +++ b/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInThirtyInchDetectorTest.kt @@ -389,7 +389,7 @@ class MissingViewInThirtyInchDetectorTest : LintDetectorTest() { val activity = java( "package foo;\n" + "import net.grandcentrix.thirtyinch.*;\n" + - "public class MyActivity extends BaseActivity {\n" + + "public class MyActivity extends BaseActivity implements MyView {\n" + "}" ) @@ -411,7 +411,7 @@ class MissingViewInThirtyInchDetectorTest : LintDetectorTest() { baseActivity, activity ) - ).containsOnlyOnce(TiIssue.MissingView.id) + ).isEqualTo(NO_WARNINGS) } fun testKotlin_Activity_throughTransitiveBaseClass_withBasePresenter_noWarning() { @@ -432,7 +432,7 @@ class MissingViewInThirtyInchDetectorTest : LintDetectorTest() { val activity = kotlin( "package foo;\n" + "import net.grandcentrix.thirtyinch.*;\n" + - "class MyActivity : BaseActivity() {\n" + + "class MyActivity : BaseActivity(), MyView {\n" + "}" ) @@ -454,8 +454,9 @@ class MissingViewInThirtyInchDetectorTest : LintDetectorTest() { baseActivity, activity ) - ).containsOnlyOnce(TiIssue.MissingView.id) + ).isEqualTo(NO_WARNINGS) } + /* * -------------------------------------------------------------------------------- * TiFragment From d64196880ffb52ca8d4dc0ce81bc341766c1ce6a Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Fri, 14 Sep 2018 16:09:03 +0200 Subject: [PATCH 21/26] also implements the basePresenter tests for the fragment --- .../MissingViewInThirtyInchDetectorTest.kt | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) 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 index 04f504fc..e01377a8 100644 --- a/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInThirtyInchDetectorTest.kt +++ b/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInThirtyInchDetectorTest.kt @@ -567,4 +567,176 @@ class MissingViewInThirtyInchDetectorTest : LintDetectorTest() { ) ).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) + } } \ No newline at end of file From 620fecaab8931607ebef7a3a393be60ee01c965d Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Fri, 14 Sep 2018 16:09:36 +0200 Subject: [PATCH 22/26] Improves the TI Category priority to align with Category.CORRECTNESS --- .../src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiIssue.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 071f7164..7905707f 100644 --- a/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiIssue.kt +++ b/thirtyinch-lint/src/main/kotlin/net/grandcentrix/thirtyinch/lint/TiIssue.kt @@ -7,7 +7,7 @@ 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", 5) +private val CATEGORY_TI = Category.create("ThirtyInch", 90) sealed class TiIssue( val id: String, From ce5da87fd08975250440fbc2e01d3052669a762d Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Mon, 17 Sep 2018 11:01:06 +0200 Subject: [PATCH 23/26] makes the findViewInterface method signature more abstract --- .../lint/detector/BaseMissingViewDetector.kt | 26 +++++-------------- .../MissingViewInCompositeDetector.kt | 5 +--- .../MissingViewInThirtyInchDetector.kt | 12 +++++++-- 3 files changed, 18 insertions(+), 25 deletions(-) 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 index 258382af..041e16f3 100644 --- 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 @@ -4,8 +4,6 @@ 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 @@ -25,13 +23,6 @@ abstract class BaseMissingViewDetector : Detector(), Detector.UastScanner { */ abstract override 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. @@ -39,6 +30,12 @@ abstract class BaseMissingViewDetector : Detector(), Detector.UastScanner { */ 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 @@ -48,7 +45,7 @@ abstract class BaseMissingViewDetector : Detector(), Detector.UastScanner { return } // Extract the MVP View type from the declaration - tryFindViewInterface(context, declaration)?.let { viewInterface -> + 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 @@ -65,15 +62,6 @@ abstract class BaseMissingViewDetector : Detector(), Detector.UastScanner { } } - private fun tryFindViewInterface(context: JavaContext, declaration: UClass): PsiType? { - for (extendedType in declaration.extendsListTypes) { - extendedType.resolveGenerics().element?.let { resolvedType -> - return tryFindViewInterface(context, declaration, extendedType, resolvedType) - } - } - return null - } - private fun tryFindViewImplementation(context: JavaContext, declaration: UClass, viewInterface: PsiType): Boolean { for (implementedType in declaration.implementsListTypes) { 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 index bb75f47c..18cb53c4 100644 --- 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 @@ -2,8 +2,6 @@ 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.PsiClass -import com.intellij.psi.PsiClassType import com.intellij.psi.PsiJavaCodeReferenceElement import com.intellij.psi.PsiType import net.grandcentrix.thirtyinch.lint.TiIssue.MissingView @@ -35,8 +33,7 @@ class MissingViewInCompositeDetector : BaseMissingViewDetector() { override val issue: Issue = MissingViewInThirtyInchDetector.ISSUE - override fun tryFindViewInterface(context: JavaContext, declaration: UClass, extendedType: PsiClassType, - resolvedType: PsiClass): PsiType? { + 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() } 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 index 7e6febd6..c9967da0 100644 --- 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 @@ -29,8 +29,16 @@ class MissingViewInThirtyInchDetector : BaseMissingViewDetector() { override val issue: Issue = ISSUE - override fun tryFindViewInterface(context: JavaContext, declaration: UClass, extendedType: PsiClassType, - resolvedType: PsiClass): PsiType? { + override fun findViewInterface(context: JavaContext, declaration: UClass): PsiType? { + for (extendedType in declaration.extendsListTypes) { + extendedType.resolveGenerics().element?.let { resolvedType -> + return tryFindViewInterface(extendedType, resolvedType) + } + } + return null + } + + private fun tryFindViewInterface(extendedType: PsiClassType, resolvedType: PsiClass): PsiType? { // Expect

signature in the extended Ti class val parameters = extendedType.parameters val parameterTypes = resolvedType.typeParameters From ea76784d5b408bfb5309ec4fb58557542551e422 Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Mon, 17 Sep 2018 12:37:59 +0200 Subject: [PATCH 24/26] implements more robust view type determination --- .../MissingViewInThirtyInchDetector.kt | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) 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 index c9967da0..fcc83e54 100644 --- 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 @@ -2,10 +2,10 @@ 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.PsiClass 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" @@ -30,30 +30,28 @@ class MissingViewInThirtyInchDetector : BaseMissingViewDetector() { override val issue: Issue = ISSUE override fun findViewInterface(context: JavaContext, declaration: UClass): PsiType? { - for (extendedType in declaration.extendsListTypes) { - extendedType.resolveGenerics().element?.let { resolvedType -> - return tryFindViewInterface(extendedType, resolvedType) - } - } - return null + return declaration.extendsListTypes + .firstNotNullResult { extendedType -> tryFindViewInterface(extendedType) } } - private fun tryFindViewInterface(extendedType: PsiClassType, resolvedType: PsiClass): PsiType? { - // Expect

signature in the extended Ti class + private fun tryFindViewInterface(extendedType: PsiClassType): PsiType? { + val resolvedType = extendedType.resolveGenerics().element ?: return null + 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() + 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, From bf9f5c290cea75c26b4b3045db87e9470bcaf567 Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Mon, 17 Sep 2018 12:38:16 +0200 Subject: [PATCH 25/26] adds more tests for the better view type implementation --- .../MissingViewInThirtyInchDetectorTest.kt | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) 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 index e01377a8..8adb175e 100644 --- a/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInThirtyInchDetectorTest.kt +++ b/thirtyinch-lint/src/test/kotlin/net/grandcentrix/thirtyinch/lint/MissingViewInThirtyInchDetectorTest.kt @@ -457,6 +457,60 @@ class MissingViewInThirtyInchDetectorTest : LintDetectorTest() { ).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 @@ -739,4 +793,58 @@ class MissingViewInThirtyInchDetectorTest : LintDetectorTest() { ) ).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 From 139f3f810fe93cef77a5da7eb032b1a9ea023d83 Mon Sep 17 00:00:00 2001 From: Jan Reehuis Date: Mon, 17 Sep 2018 12:39:07 +0200 Subject: [PATCH 26/26] enables the ViewNotImplementedCheck for the TiDialogFragment --- .../lint/detector/MissingViewInThirtyInchDetector.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index fcc83e54..5cc81387 100644 --- 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 @@ -12,7 +12,8 @@ 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.TiFragment", + "net.grandcentrix.thirtyinch.TiDialogFragment" ) class MissingViewInThirtyInchDetector : BaseMissingViewDetector() {