Skip to content
This repository was archived by the owner on Mar 16, 2021. It is now read-only.

Commit d761dbe

Browse files
authored
Merge pull request #158 from jreehuis/feature/lint_checks
Feature: Lint Checks
2 parents d694574 + 139f3f8 commit d761dbe

File tree

13 files changed

+1610
-2
lines changed

13 files changed

+1610
-2
lines changed

build.gradle

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ buildscript {
77
google()
88
}
99
dependencies {
10-
classpath "com.android.tools.build:gradle:3.1.3"
10+
classpath "com.android.tools.build:gradle:3.1.3" // if you update this, also update the lintVersion below
1111
classpath "com.vanniktech:gradle-android-junit-jacoco-plugin:0.10.0"
1212
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
1313
}
@@ -45,4 +45,9 @@ ext {
4545
assertjVersion = '2.8.0'
4646
supportTestVersion = '1.0.1'
4747
espressoVersion = '3.0.1'
48+
49+
// According to https://github.com/googlesamples/android-custom-lint-rules/tree/master/android-studio-3
50+
// the lint version should match to the used Android Gradle Plugin by the formula "AGP Version X.Y.Z + 23.0.0"
51+
// E.g. "AGP Version 3.1.3 + 23.0.0 = Lint Version 26.1.3"
52+
lintVersion = '26.1.3'
4853
}

settings.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ include(
66
":thirtyinch-rx2",
77
":thirtyinch-test",
88
":thirtyinch-kotlin",
9+
":thirtyinch-lint",
910
":sample",
1011
":plugin-test"
11-
)
12+
)

thirtyinch-lint/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.idea
2+
build/
3+
.gradle
4+
gradle
5+
gradlew
6+
gradlew.bat
7+
.idea/workspace.xml

thirtyinch-lint/build.gradle

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
plugins {
2+
id "org.jetbrains.kotlin.jvm"
3+
id "jacoco"
4+
}
5+
6+
repositories {
7+
jcenter()
8+
}
9+
10+
configurations {
11+
lintChecks
12+
}
13+
14+
dependencies {
15+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
16+
17+
compileOnly "com.android.tools.lint:lint-api:$lintVersion"
18+
compileOnly "com.android.tools.lint:lint-checks:$lintVersion"
19+
20+
testImplementation 'junit:junit:4.12'
21+
testImplementation "org.assertj:assertj-core:$assertjVersion"
22+
23+
testImplementation "com.android.tools.lint:lint:$lintVersion"
24+
testImplementation "com.android.tools.lint:lint-tests:$lintVersion"
25+
26+
lintChecks files(jar)
27+
}
28+
29+
jar {
30+
manifest {
31+
attributes("Manifest-Version": 1.0)
32+
attributes("Lint-Registry": "net.grandcentrix.thirtyinch.lint.TiLintRegistry")
33+
// The TI checks are build with the new 3.0 APIs (including UAST) so we should also register the v2 lint registry.
34+
attributes("Lint-Registry-v2": "net.grandcentrix.thirtyinch.lint.TiLintRegistry")
35+
}
36+
}
37+
38+
compileKotlin {
39+
kotlinOptions {
40+
jvmTarget = "1.8"
41+
}
42+
}
43+
44+
compileTestKotlin {
45+
kotlinOptions {
46+
jvmTarget = "1.8"
47+
}
48+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package net.grandcentrix.thirtyinch.lint
2+
3+
import com.android.tools.lint.detector.api.Category
4+
import com.android.tools.lint.detector.api.Detector
5+
import com.android.tools.lint.detector.api.Implementation
6+
import com.android.tools.lint.detector.api.Issue
7+
import com.android.tools.lint.detector.api.Scope
8+
import com.android.tools.lint.detector.api.Severity
9+
10+
private val CATEGORY_TI = Category.create("ThirtyInch", 90)
11+
12+
sealed class TiIssue(
13+
val id: String,
14+
val briefDescription: String,
15+
val category: Category,
16+
val priority: Int,
17+
val severity: Severity
18+
) {
19+
20+
object MissingView : TiIssue(
21+
id = "MissingTiViewImplementation",
22+
briefDescription = "TiView Implementation missing in class",
23+
category = CATEGORY_TI,
24+
priority = 8,
25+
severity = Severity.ERROR
26+
)
27+
28+
fun asLintIssue(detectorCls: Class<out Detector>, description: String = briefDescription): Issue =
29+
Issue.create(
30+
id,
31+
briefDescription,
32+
description,
33+
category,
34+
priority,
35+
severity,
36+
Implementation(
37+
detectorCls,
38+
Scope.JAVA_FILE_SCOPE
39+
)
40+
)
41+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package net.grandcentrix.thirtyinch.lint
2+
3+
import com.android.tools.lint.client.api.IssueRegistry
4+
import com.android.tools.lint.detector.api.Issue
5+
import net.grandcentrix.thirtyinch.lint.detector.MissingViewInCompositeDetector
6+
import net.grandcentrix.thirtyinch.lint.detector.MissingViewInThirtyInchDetector
7+
8+
class TiLintRegistry : IssueRegistry() {
9+
override val issues: List<Issue>
10+
get() = listOf(
11+
MissingViewInThirtyInchDetector.ISSUE.apply {
12+
setEnabledByDefault(true)
13+
},
14+
MissingViewInCompositeDetector.ISSUE.apply {
15+
setEnabledByDefault(true)
16+
}
17+
)
18+
19+
override val api: Int = com.android.tools.lint.detector.api.CURRENT_API
20+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package net.grandcentrix.thirtyinch.lint.detector
2+
3+
import com.android.tools.lint.detector.api.Detector
4+
import com.android.tools.lint.detector.api.Issue
5+
import com.android.tools.lint.detector.api.JavaContext
6+
import com.android.tools.lint.detector.api.TextFormat
7+
import com.intellij.psi.PsiType
8+
import com.intellij.psi.util.PsiUtil
9+
import org.jetbrains.uast.UClass
10+
import org.jetbrains.uast.getUastContext
11+
12+
// Base class for Lint checks centered around the notion of "TiView not implemented"
13+
abstract class BaseMissingViewDetector : Detector(), Detector.UastScanner {
14+
15+
/**
16+
* The Issue that the detector is connected to, reported on illegal state detection
17+
*/
18+
abstract val issue: Issue
19+
20+
/**
21+
* The list of super-classes to detect.
22+
* We're forcing sub-classed Detectors to implement this by means of redeclaration
23+
*/
24+
abstract override fun applicableSuperClasses(): List<String>
25+
26+
/**
27+
* Whether or not to allow the absence of an "implements TiView" clause on the given declaration.
28+
* The View interface is given as well to allow for further introspection into the setup of the class at hand.
29+
* When false is returned here, Lint will report the Issue connected to this Detector on the given declaration.
30+
*/
31+
abstract fun allowMissingViewInterface(context: JavaContext, declaration: UClass, viewInterface: PsiType): Boolean
32+
33+
/**
34+
* Tries to extract the PsiType of the TiView sub-class that is relevant for the given declaration.
35+
* The relevant super-class (from applicableSuperClasses()) & its resolved variant are given as well.
36+
*/
37+
abstract fun findViewInterface(context: JavaContext, declaration: UClass): PsiType?
38+
39+
final override fun visitClass(context: JavaContext, declaration: UClass) {
40+
if (!context.isEnabled(issue)) {
41+
return
42+
}
43+
// Don't trigger on abstract classes
44+
if (PsiUtil.isAbstractClass(declaration.psi)) {
45+
return
46+
}
47+
// Extract the MVP View type from the declaration
48+
findViewInterface(context, declaration)?.let { viewInterface ->
49+
// Check if the class implements that interface as well
50+
if (!tryFindViewImplementation(context, declaration, viewInterface)) {
51+
// Interface not implemented; check if alternate condition applies
52+
if (!allowMissingViewInterface(context, declaration, viewInterface)) {
53+
// Invalid state: Report issue for this class
54+
declaration.nameIdentifier?.run {
55+
context.report(
56+
issue,
57+
context.getLocation(this.originalElement),
58+
issue.getBriefDescription(TextFormat.TEXT))
59+
}
60+
}
61+
}
62+
}
63+
}
64+
65+
private fun tryFindViewImplementation(context: JavaContext, declaration: UClass,
66+
viewInterface: PsiType): Boolean {
67+
for (implementedType in declaration.implementsListTypes) {
68+
if (implementedType == viewInterface) {
69+
return true
70+
}
71+
implementedType.resolve()?.let { resolvedType ->
72+
val uastContext = declaration.getUastContext()
73+
return tryFindViewImplementation(context, uastContext.getClass(resolvedType), viewInterface)
74+
}
75+
}
76+
return false
77+
}
78+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package net.grandcentrix.thirtyinch.lint.detector
2+
3+
import com.android.tools.lint.detector.api.Issue
4+
import com.android.tools.lint.detector.api.JavaContext
5+
import com.intellij.psi.PsiJavaCodeReferenceElement
6+
import com.intellij.psi.PsiType
7+
import net.grandcentrix.thirtyinch.lint.TiIssue.MissingView
8+
import org.jetbrains.uast.UBlockExpression
9+
import org.jetbrains.uast.UCallExpression
10+
import org.jetbrains.uast.UClass
11+
import org.jetbrains.uast.UExpression
12+
import org.jetbrains.uast.getUastContext
13+
14+
private const val ADD_PLUGIN_METHOD = "addPlugin"
15+
private const val TI_ACTIVITY_PLUGIN_NAME = "TiActivityPlugin"
16+
private const val TI_FRAGMENT_PLUGIN_NAME = "TiFragmentPlugin"
17+
private val CA_CLASS_NAMES = listOf(
18+
"com.pascalwelsch.compositeandroid.activity.CompositeActivity",
19+
"com.pascalwelsch.compositeandroid.fragment.CompositeFragment"
20+
)
21+
22+
class MissingViewInCompositeDetector : BaseMissingViewDetector() {
23+
companion object {
24+
val ISSUE = MissingView.asLintIssue(
25+
MissingViewInCompositeDetector::class.java,
26+
"When using ThirtyInch, a class extending CompositeActivity or CompositeFragment " +
27+
"has to implement the TiView interface associated with it in its signature, " +
28+
"if it applies the respective plugin as well."
29+
)
30+
}
31+
32+
override fun applicableSuperClasses() = CA_CLASS_NAMES
33+
34+
override val issue: Issue = MissingViewInThirtyInchDetector.ISSUE
35+
36+
override fun findViewInterface(context: JavaContext, declaration: UClass): PsiType? {
37+
// Expect TiPlugin to be applied in the extended CA class
38+
// Found default constructor
39+
val defaultConstructor = declaration.constructors.firstOrNull { it.typeParameters.isEmpty() }
40+
41+
defaultConstructor?.let {
42+
val uastContext = declaration.getUastContext()
43+
val body = uastContext.getMethodBody(defaultConstructor)
44+
return tryFindViewFromCompositeConstructor(context, declaration, body)
45+
}
46+
return null
47+
}
48+
49+
private fun tryFindViewFromCompositeConstructor(context: JavaContext, declaration: UClass,
50+
expression: UExpression?): PsiType? {
51+
if (expression == null) {
52+
return null
53+
}
54+
when (expression) {
55+
is UBlockExpression -> {
56+
// Unwrap block statements; the first resolvable result is returned
57+
expression.expressions
58+
.mapNotNull { tryFindViewFromCompositeConstructor(context, declaration, it) }
59+
.forEach { return it }
60+
}
61+
is UCallExpression -> {
62+
// Inspect call sites
63+
if (ADD_PLUGIN_METHOD == expression.methodName && expression.valueArgumentCount == 1) {
64+
// Expect a plugin to be used as the only argument to this method
65+
val argument = expression.valueArguments[0]
66+
if (argument is UCallExpression) {
67+
val argReference = argument.classReference ?: return null
68+
val resolvedName = argReference.resolvedName
69+
if (TI_ACTIVITY_PLUGIN_NAME == resolvedName || TI_FRAGMENT_PLUGIN_NAME == resolvedName) {
70+
// Matching names. Finally, find the type parameters passed to the plugin
71+
val psiReference = argReference.psi as PsiJavaCodeReferenceElement? ?: return null
72+
val parameterTypes = psiReference.typeParameters
73+
if (parameterTypes.size != 2) {
74+
return null
75+
}
76+
return parameterTypes[1]
77+
}
78+
}
79+
}
80+
}
81+
}
82+
return null
83+
}
84+
85+
override fun allowMissingViewInterface(context: JavaContext, declaration: UClass, viewInterface: PsiType) = false
86+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package net.grandcentrix.thirtyinch.lint.detector
2+
3+
import com.android.tools.lint.detector.api.Issue
4+
import com.android.tools.lint.detector.api.JavaContext
5+
import com.intellij.psi.PsiClassType
6+
import com.intellij.psi.PsiType
7+
import net.grandcentrix.thirtyinch.lint.TiIssue.MissingView
8+
import org.jetbrains.kotlin.utils.addToStdlib.firstNotNullResult
9+
import org.jetbrains.uast.UClass
10+
11+
private const val TI_VIEW_FQ = "net.grandcentrix.thirtyinch.TiView"
12+
private const val PROVIDE_VIEW_METHOD = "provideView"
13+
private val TI_CLASS_NAMES = listOf(
14+
"net.grandcentrix.thirtyinch.TiActivity",
15+
"net.grandcentrix.thirtyinch.TiFragment",
16+
"net.grandcentrix.thirtyinch.TiDialogFragment"
17+
)
18+
19+
class MissingViewInThirtyInchDetector : BaseMissingViewDetector() {
20+
companion object {
21+
val ISSUE = MissingView.asLintIssue(
22+
MissingViewInThirtyInchDetector::class.java,
23+
"When using ThirtyInch, a class extending TiActivity or TiFragment " +
24+
"has to implement the TiView interface associated with it in its signature, " +
25+
"or implement `provideView()` instead to override this default behaviour."
26+
)
27+
}
28+
29+
override fun applicableSuperClasses() = TI_CLASS_NAMES
30+
31+
override val issue: Issue = ISSUE
32+
33+
override fun findViewInterface(context: JavaContext, declaration: UClass): PsiType? {
34+
return declaration.extendsListTypes
35+
.firstNotNullResult { extendedType -> tryFindViewInterface(extendedType) }
36+
}
37+
38+
private fun tryFindViewInterface(extendedType: PsiClassType): PsiType? {
39+
val resolvedType = extendedType.resolveGenerics().element ?: return null
40+
41+
val parameters = extendedType.parameters
42+
val parameterTypes = resolvedType.typeParameters
43+
44+
check(parameters.size == parameterTypes.size) { "Got different Array Sizes" }
45+
46+
return parameters
47+
.mapIndexed { i, psiType -> Pair(psiType, parameterTypes[i]) }
48+
.firstNotNullResult { (type, typeParameter) ->
49+
typeParameter.extendsListTypes
50+
.map { it.resolveGenerics().element }
51+
.filter { TI_VIEW_FQ == it?.qualifiedName }
52+
.map { type }
53+
.firstOrNull()
54+
?: (type as? PsiClassType)?.let { tryFindViewInterface(it) }
55+
}
56+
}
57+
58+
override fun allowMissingViewInterface(context: JavaContext, declaration: UClass,
59+
viewInterface: PsiType): Boolean {
60+
// Interface not implemented; check if provideView() is overridden instead
61+
return declaration.findMethodsByName(PROVIDE_VIEW_METHOD, true)
62+
.any { viewInterface == it.returnType }
63+
}
64+
}

0 commit comments

Comments
 (0)