Skip to content

Commit 01693c5

Browse files
DSL (#12)
* Create a `@Rule` annotation to create new detectors. It is applied to functions after much consideration. I thought about using it in classes but requiring two fields (the issue and the rule description) I thought requiring an interface was going to be too cumbersome. With a function, as long as you use the builders, it should be safe. * Create an annotation processor tool reading the `@Rule` annotation and creating a `Detector` class with that definition. It calls the annotated function directly from the new Detector class. We are using KotlinPoet to generate detector classes. * Create a type definition of the rules we need to describe with a `suchThat` function to apply a predicate and see if the detector should report and issue. * Migrate implemented rules to the new DSL. * Change packages so that we use `com.serchinastico.lin` as the base package name and then use `dsl`, `processor` and so on for its modules.
1 parent bf55da2 commit 01693c5

29 files changed

+723
-230
lines changed

annotations/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

annotations/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
apply plugin: 'java-library'
2+
apply plugin: 'kotlin'
3+
4+
dependencies {
5+
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.10"
6+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.serchinastico.lin.annotations
2+
3+
@Retention(AnnotationRetention.SOURCE)
4+
@Target(AnnotationTarget.FUNCTION)
5+
annotation class Rule

app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ android {
2222

2323
dependencies {
2424
implementation fileTree(dir: 'libs', include: ['*.jar'])
25-
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
25+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.10"
2626
implementation 'com.android.support:appcompat-v7:28.0.0'
2727
testImplementation 'junit:junit:4.12'
2828
androidTestImplementation 'com.android.support.test:runner:1.0.2'

build.gradle

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
buildscript {
2-
ext.kotlin_version = '1.3.10'
32
repositories {
43
google()
54
jcenter()
65
}
76
dependencies {
87
classpath 'com.android.tools.build:gradle:3.2.1'
9-
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
8+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.10"
109
}
1110
}
1211

dsl/build.gradle

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
repositories {
2+
maven { url "https://dl.bintray.com/juanchosaravia/autodsl" }
3+
}
4+
15
apply plugin: 'java-library'
26
apply plugin: 'kotlin'
7+
apply plugin: 'kotlin-kapt'
38

49
dependencies {
510
compileOnly 'com.android.tools.lint:lint:26.2.1'
@@ -12,7 +17,4 @@ dependencies {
1217
testCompile 'com.android.tools.lint:lint:26.2.1'
1318
testCompile 'com.android.tools.lint:lint-tests:26.2.1'
1419
testCompile 'com.android.tools:testutils:26.2.1'
15-
}
16-
17-
sourceCompatibility = JavaVersion.VERSION_1_7
18-
targetCompatibility = JavaVersion.VERSION_1_7
20+
}

rules/src/main/java/com/serchinastico/rules/Extensions.kt renamed to dsl/src/main/kotlin/com/serchinastico/lin/dsl/Extensions.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.serchinastico.rules
1+
package com.serchinastico.lin.dsl
22

33
import com.android.tools.lint.detector.api.Issue
44
import com.android.tools.lint.detector.api.JavaContext

rules/src/main/java/com/serchinastico/rules/IssueBuilder.kt renamed to dsl/src/main/kotlin/com/serchinastico/lin/dsl/IssueBuilder.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.serchinastico.rules
1+
package com.serchinastico.lin.dsl
22

33
import com.android.tools.lint.detector.api.*
44
import java.util.*
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package com.serchinastico.lin.dsl
2+
3+
import com.android.tools.lint.detector.api.*
4+
import org.jetbrains.uast.*
5+
import java.util.*
6+
import kotlin.reflect.KClass
7+
8+
data class LinRule(val issueBuilder: IssueBuilder) {
9+
10+
private var lNode: LNode<*>? = null
11+
12+
val applicableTypes: List<Class<out UElement>>
13+
get() = lNode?.applicableType?.let { listOf(it) } ?: emptyList()
14+
15+
fun matches(node: UElement): Boolean = lNode?.match(node) ?: false
16+
17+
fun file(block: LNode.LFile.() -> LNode.LFile): LinRule {
18+
lNode = LNode.LFile().block()
19+
return this
20+
}
21+
22+
fun import(block: LNode.LImport.() -> LNode.LImport): LinRule {
23+
lNode = LNode.LImport().block()
24+
return this
25+
}
26+
27+
fun type(block: LNode.LType.() -> LNode.LType): LinRule {
28+
lNode = LNode.LType().block()
29+
return this
30+
}
31+
32+
fun switch(block: LNode.LSwitchExpression.() -> LNode.LSwitchExpression): LinRule {
33+
lNode = LNode.LSwitchExpression().block()
34+
return this
35+
}
36+
37+
fun call(block: LNode.LCallExpression.() -> LNode.LCallExpression): LinRule {
38+
lNode = LNode.LCallExpression().block()
39+
return this
40+
}
41+
42+
fun field(block: LNode.LField.() -> LNode.LField): LinRule {
43+
lNode = LNode.LField().block()
44+
return this
45+
}
46+
}
47+
48+
sealed class LNode<T : UElement> {
49+
50+
abstract val applicableType: Class<out T>
51+
internal val children: MutableList<LNode<*>> = mutableListOf()
52+
private var suchThatPredicate: ((T) -> Boolean)? = null
53+
54+
fun match(element: UElement): Boolean {
55+
val predicate = suchThatPredicate ?: { true }
56+
return predicate.invoke(applicableType.cast(element)) && children.all { child ->
57+
child.lookForChildren(element).any { child.match(it) }
58+
}
59+
}
60+
61+
fun <S : LNode<*>> suchThat(predicate: (T) -> Boolean): S {
62+
suchThatPredicate = predicate
63+
return this as S
64+
}
65+
66+
open fun lookForChildren(element: UElement): List<T> = emptyList()
67+
68+
class LFile : LNode<UFile>() {
69+
70+
fun import(block: LImport.() -> LImport): LFile {
71+
children.add(LImport().block())
72+
return this
73+
}
74+
75+
fun type(block: LType.() -> LType): LFile {
76+
children.add(LType().block())
77+
return this
78+
}
79+
80+
override val applicableType: Class<out UFile> = UFile::class.java
81+
}
82+
83+
class LImport : LNode<UImportStatement>() {
84+
override val applicableType: Class<out UImportStatement> = UImportStatement::class.java
85+
86+
override fun lookForChildren(element: UElement): List<UImportStatement> = when (element) {
87+
is UFile -> element.imports
88+
else -> super.lookForChildren(element)
89+
}
90+
}
91+
92+
class LType : LNode<UClass>() {
93+
override val applicableType: Class<out UClass> = UClass::class.java
94+
95+
override fun lookForChildren(element: UElement): List<UClass> = when (element) {
96+
is UFile -> element.classes
97+
else -> super.lookForChildren(element)
98+
}
99+
100+
fun switch(block: LSwitchExpression.() -> LSwitchExpression): LType {
101+
children.add(LSwitchExpression().block())
102+
return this
103+
}
104+
105+
fun calls(block: LCallExpression.() -> LCallExpression): LType {
106+
children.add(LCallExpression().block())
107+
return this
108+
}
109+
110+
fun field(block: LField.() -> LField): LType {
111+
children.add(LField().block())
112+
return this
113+
}
114+
}
115+
116+
class LSwitchExpression : LNode<USwitchExpression>() {
117+
override val applicableType: Class<out USwitchExpression> = USwitchExpression::class.java
118+
}
119+
120+
class LCallExpression : LNode<UCallExpression>() {
121+
override val applicableType: Class<out UCallExpression> = UCallExpression::class.java
122+
}
123+
124+
class LField : LNode<UField>() {
125+
override val applicableType: Class<out UField> = UField::class.java
126+
}
127+
}
128+
129+
fun rule(issueBuilder: IssueBuilder, block: LinRule.() -> LinRule): LinRule = LinRule(issueBuilder).block()
130+
131+
fun main(args: Array<String>) {
132+
val detectorScope = Scope.JAVA_FILE_SCOPE
133+
134+
rule(
135+
issue(
136+
detectorScope,
137+
"Framework classes to get or store data should never be called from Activities, Fragments or any other" +
138+
" Android related view.",
139+
"Your Android classes should not be responsible for retrieving or storing information, that should be " +
140+
"responsibility of another classes.",
141+
Category.INTEROPERABILITY
142+
)
143+
) {
144+
file {
145+
import { suchThat { it.isFrameworkLibraryImport } }
146+
type { suchThat { node -> node.uastSuperTypes.any { it.isAndroidFrameworkType } } }
147+
}
148+
}
149+
}
150+
151+
fun issue(
152+
scope: EnumSet<Scope>,
153+
description: String,
154+
explanation: String,
155+
category: Category
156+
): IssueBuilder {
157+
return IssueBuilder(scope, description, explanation, category)
158+
}
159+
160+
data class IssueBuilder(
161+
val scope: EnumSet<Scope>,
162+
val description: String,
163+
val explanation: String,
164+
val category: Category,
165+
var priority: Int = 5,
166+
var severity: Severity = Severity.ERROR
167+
) {
168+
fun <T : Detector> build(detectorClass: KClass<T>): Issue =
169+
Issue.create(
170+
detectorClass.simpleName ?: "RuleWithNoId",
171+
description,
172+
explanation,
173+
category,
174+
priority,
175+
severity,
176+
Implementation(detectorClass.java, scope)
177+
)
178+
}

processor/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

0 commit comments

Comments
 (0)