Field parser, run lint for test files

Bug: 32342709
Test: FieldParserTest.kt
Change-Id: I6e1e486eeb11bb4ae0844c3982401217c5198811
diff --git a/flatfoot-common/init.gradle b/flatfoot-common/init.gradle
index cbbc1b6..61f160b 100644
--- a/flatfoot-common/init.gradle
+++ b/flatfoot-common/init.gradle
@@ -55,6 +55,7 @@
     def kotlinCheckstyle = project.tasks.create(name : 'checkstyleKotlin', type: Checkstyle) {
         configFile rootProject.file('../flatfoot-common/kotlin-checkstyle.xml')
         source project.sourceSets.main.allJava
+        source project.sourceSets.test.allJava
         ignoreFailures false
         showViolations true
         include '**/*.kt'
@@ -64,14 +65,15 @@
     project.tasks.findByName("check").dependsOn(kotlinCheckstyle)
     // poor man's line length check
     def lineCheck = project.tasks.create(name : "lineLengthCheck") {
-        project.sourceSets.main.allJava.getSourceDirectories().each { sourceDir ->
-            fileTree(dir : sourceDir, include : "**/*.kt").each{ file ->
-                file.readLines().eachWithIndex { line, index ->
-                    if (line.size() > 100) {
-                        throw new Exception("line too long:\nfile: $file line:$index line: $line")
-                    }
-                }
-            }
+        (project.sourceSets.main.allJava.getSourceDirectories() +
+            project.sourceSets.test.allJava.getSourceDirectories()).each { sourceDir ->
+                  fileTree(dir : sourceDir, include : "**/*.kt").each{ file ->
+                      file.readLines().eachWithIndex { line, index ->
+                          if (line.size() > 100) {
+                              throw new Exception("line too long:\nfile: $file line:$index line: $line")
+                          }
+                      }
+                  }
         }
     }
     kotlinCheckstyle.dependsOn(lineCheck)
@@ -80,7 +82,7 @@
 def createAndroidCheckstyle(Project project) {
     def androidCheckstyle = project.tasks.create(name : 'checkstyleAndroid', type: Checkstyle) {
         configFile rootProject.file('../../../development/tools/checkstyle/android-style.xml')
-        source 'src/main/java'
+        source project.sourceSets.main.allJava
         ignoreFailures false
         showViolations true
         include '**/*.java'
diff --git a/room/common/src/main/java/com/android/support/room/PrimaryKey.java b/room/common/src/main/java/com/android/support/room/PrimaryKey.java
new file mode 100644
index 0000000..49798cd
--- /dev/null
+++ b/room/common/src/main/java/com/android/support/room/PrimaryKey.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.SOURCE)
+public @interface PrimaryKey {
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/preconditions/Checks.kt b/room/compiler/src/main/kotlin/com/android/support/room/preconditions/Checks.kt
index 532cd15..05d0394 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/preconditions/Checks.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/preconditions/Checks.kt
@@ -17,6 +17,10 @@
 package com.android.support.room.preconditions
 
 import com.android.support.room.errors.ElementBoundException
+import com.android.support.room.processor.ProcessorErrors
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeVariableName
 import javax.lang.model.element.Element
 
 /**
@@ -28,4 +32,13 @@
             throw ElementBoundException(element, String.format(errorMsg, args))
         }
     }
+
+    fun assertNotUnbound(typeName: TypeName, element: Element, errorMsg : String,
+                         vararg args : Any) {
+        // TODO support bounds cases like <T extends Foo> T bar()
+        Checks.check(typeName !is TypeVariableName, element, errorMsg, args)
+        if (typeName is ParameterizedTypeName) {
+            typeName.typeArguments.forEach { assertNotUnbound(it, element, errorMsg, args) }
+        }
+    }
 }
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/FieldParser.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/FieldParser.kt
new file mode 100644
index 0000000..d02f25a
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/FieldParser.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.processor
+
+import com.android.support.room.PrimaryKey
+import com.android.support.room.preconditions.Checks
+import com.android.support.room.vo.Field
+import com.google.auto.common.MoreElements
+import com.squareup.javapoet.TypeName
+import javax.annotation.processing.ProcessingEnvironment
+import javax.annotation.processing.RoundEnvironment
+import javax.lang.model.element.Element
+import javax.lang.model.type.DeclaredType
+
+class FieldParser(val roundEnv: RoundEnvironment,
+                  val processingEnvironment: ProcessingEnvironment) {
+    fun parse(containing : DeclaredType, element : Element) : Field {
+        val member = processingEnvironment.typeUtils.asMemberOf(containing, element)
+        val type = TypeName.get(member)
+        Checks.assertNotUnbound(type, element,
+                ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_ENTITY_FIELDS)
+        return Field(name = element.simpleName.toString(),
+                type = type,
+                primaryKey = MoreElements.isAnnotationPresent(element, PrimaryKey::class.java))
+    }
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt
index 1f53c99..c2fe0e4 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt
@@ -23,4 +23,5 @@
     val CANNOT_RESOLVE_RETURN_TYPE = "Cannot resolve return type for %s"
     val CANNOT_USE_UNBOUND_GENERICS_IN_QUERY_METHODS = "Cannot use unbound generics in query " +
             "methods. It must be bound to a type through base Dao class."
+    val CANNOT_USE_UNBOUND_GENERICS_IN_ENTITY_FIELDS = "Cannot use unbound fields in entities."
 }
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/QueryParser.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/QueryParser.kt
index 0b96331..838fa87 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/QueryParser.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/QueryParser.kt
@@ -48,20 +48,12 @@
 
         val returnTypeName = TypeName.get(executableType.returnType)
 
-        assertNoUnboundParameter(returnTypeName, executableElement)
+        Checks.assertNotUnbound(returnTypeName, executableElement,
+                ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_QUERY_METHODS)
         return QueryMethod(
                 query,
                 executableElement.simpleName.toString(),
                 returnTypeName,
                 executableElement.parameters.map { parameterParser.parse(containing, it) })
     }
-
-    fun assertNoUnboundParameter(typeName: TypeName, element: Element) {
-        // TODO support bounds cases like <T extends Foo> T bar()
-        Checks.check(typeName !is TypeVariableName, element,
-                ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_QUERY_METHODS)
-        if (typeName is ParameterizedTypeName) {
-            typeName.typeArguments.forEach { assertNoUnboundParameter(it, element) }
-        }
-    }
 }
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/Entity.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/Entity.kt
new file mode 100644
index 0000000..100d26b
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/Entity.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.vo
+
+import com.squareup.javapoet.TypeName
+
+data class Entity(val type: TypeName)
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/Field.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/Field.kt
new file mode 100644
index 0000000..17488ca
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/Field.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.vo
+
+import com.squareup.javapoet.TypeName
+
+data class Field(val name : String, val type: TypeName, val primaryKey : Boolean)
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/Parameter.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/Parameter.kt
index a71338d..f9c1a70 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/Parameter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/Parameter.kt
@@ -21,4 +21,4 @@
 /**
  * Holds the parameter for a {@link QueryMethod}.
  */
-class Parameter(val name: String, val type: TypeName)
+data class Parameter(val name: String, val type: TypeName)
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/QueryMethod.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/QueryMethod.kt
index 38ef6a1..952f026 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/QueryMethod.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/QueryMethod.kt
@@ -23,5 +23,5 @@
  * A class that holds information about a QueryMethod.
  * It is self sufficient and must have all generics etc resolved once created.
  */
-class QueryMethod(val query: ParsedQuery, val name: String, val returnType: TypeName,
+data class QueryMethod(val query: ParsedQuery, val name: String, val returnType: TypeName,
                   val parameters: List<Parameter>)
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/processor/FieldParserTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/processor/FieldParserTest.kt
new file mode 100644
index 0000000..07d6f26
--- /dev/null
+++ b/room/compiler/src/test/kotlin/com/android/support/room/processor/FieldParserTest.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.processor
+
+import com.android.support.room.Entity
+import com.android.support.room.testing.TestProcessor
+import com.android.support.room.vo.Field
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourceSubjectFactory
+import com.squareup.javapoet.ArrayTypeName
+import com.squareup.javapoet.TypeName
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import javax.lang.model.element.ElementKind
+
+@RunWith(JUnit4::class)
+class FieldParserTest {
+    companion object {
+        const val ENTITY_PREFIX = """
+                package foo.bar;
+                import com.android.support.room.*;
+                @Entity
+                abstract class MyEntity {
+                """
+        const val ENTITY_SUFFIX = "}"
+        val ALL_PRIMITIVES = arrayListOf(
+                TypeName.BOOLEAN,
+                TypeName.BYTE,
+                TypeName.SHORT,
+                TypeName.INT,
+                TypeName.LONG,
+                TypeName.CHAR,
+                TypeName.FLOAT,
+                TypeName.DOUBLE)
+    }
+
+    @Test
+    fun primitives() {
+        ALL_PRIMITIVES.forEach { primitive ->
+            singleEntity("$primitive x;") { field ->
+                assertThat(field.name, `is`("x"))
+                assertThat(field.type, `is`(primitive))
+                assertThat(field.primaryKey, `is`(false))
+            }.compilesWithoutError()
+        }
+    }
+
+    @Test
+    fun boxed() {
+        ALL_PRIMITIVES.forEach { primitive ->
+            singleEntity("${primitive.box()} y;") { field ->
+                assertThat(field.name, `is`("y"))
+                assertThat(field.type, `is`(primitive.box()))
+                assertThat(field.primaryKey, `is`(false))
+            }.compilesWithoutError()
+        }
+    }
+
+    @Test
+    fun primaryKey() {
+        singleEntity("""
+            @PrimaryKey
+            int x;
+            """) { field ->
+            assertThat(field, `is`(
+                    Field(name = "x", type = TypeName.INT, primaryKey = true)
+            ))
+        }.compilesWithoutError()
+    }
+
+    @Test
+    fun primitiveArray() {
+        ALL_PRIMITIVES.forEach { primitive ->
+            singleEntity("$primitive[] arr;") { field ->
+                assertThat(field, `is`(
+                        Field(name = "arr", type = ArrayTypeName.of(primitive), primaryKey = false)
+                ))
+            }.compilesWithoutError()
+        }
+    }
+
+    @Test
+    fun boxedArray() {
+        ALL_PRIMITIVES.forEach { primitive ->
+            singleEntity("${primitive.box()}[] arr;") { field ->
+                assertThat(field, `is`(
+                        Field(name = "arr", type = ArrayTypeName.of(primitive.box()),
+                                primaryKey = false)
+                ))
+            }.compilesWithoutError()
+        }
+    }
+
+    @Test
+    fun generic() {
+        singleEntity("""
+                static class BaseClass<T> {
+                    T item;
+                }
+                @Entity
+                static class Extending extends BaseClass<java.lang.Integer> {
+                }
+                """) { field ->
+            assertThat(field, `is`(Field(name = "item",
+                    type = TypeName.INT.box(),
+                    primaryKey = false)))
+        }.compilesWithoutError()
+    }
+
+    @Test
+    fun unboundGeneric() {
+        singleEntity("""
+                @Entity
+                static class BaseClass<T> {
+                    T item;
+                }
+                """) {}.failsToCompile()
+                .withErrorContaining(ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_ENTITY_FIELDS)
+    }
+
+    fun singleEntity(vararg input: String, handler: (Field) -> Unit): CompileTester {
+        return Truth.assertAbout(JavaSourceSubjectFactory.javaSource())
+                .that(JavaFileObjects.forSourceString("foo.bar.MyEntity",
+                        ENTITY_PREFIX + input.joinToString("\n") + ENTITY_SUFFIX
+                ))
+                .processedWith(TestProcessor.builder()
+                        .forAnnotations(com.android.support.room.Entity::class)
+                        .nextRunHandler { invocation ->
+                            val (owner, field) = invocation.roundEnv
+                                    .getElementsAnnotatedWith(Entity::class.java)
+                                    .map {
+                                        Pair(it, invocation.processingEnv.elementUtils
+                                                .getAllMembers(MoreElements.asType(it))
+                                                .firstOrNull { it.kind == ElementKind.FIELD })
+                                    }
+                                    .first { it.second != null }
+                            val parser = FieldParser(invocation.roundEnv, invocation.processingEnv)
+                            handler(parser.parse(
+                                    MoreTypes.asDeclared(owner.asType()), field!!))
+                            true
+                        }
+                        .build())
+    }
+}
\ No newline at end of file
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/processor/QueryParserTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/processor/QueryParserTest.kt
index 9a53034..23cfffa 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/processor/QueryParserTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/processor/QueryParserTest.kt
@@ -36,6 +36,7 @@
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
+@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
 @RunWith(JUnit4::class)
 class QueryParserTest {
     companion object {
@@ -132,26 +133,32 @@
                 static abstract class ExtendingModel extends BaseModel<Integer> {
                 }
                 """) { parsedQuery ->
-            assertThat(parsedQuery.parameters.first().type, `is`(ClassName.get(Integer::class.java) as TypeName))
+            assertThat(parsedQuery.parameters.first().type,
+                    `is`(ClassName.get(Integer::class.java) as TypeName))
         }.compilesWithoutError()
     }
 
 
-    fun singleQueryMethod(vararg methods: String,
+    fun singleQueryMethod(vararg input: String,
                           handler: (QueryMethod) -> Unit):
             CompileTester {
         return assertAbout(JavaSourceSubjectFactory.javaSource())
                 .that(JavaFileObjects.forSourceString("foo.bar.MyClass",
-                        DAO_PREFIX + methods.joinToString("\n") + DAO_SUFFIX
-
+                        DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
                 ))
                 .processedWith(TestProcessor.builder()
                         .forAnnotations(Query::class, Dao::class)
                         .nextRunHandler { invocation ->
-                            val (owner, methods) = invocation.roundEnv.getElementsAnnotatedWith(Dao::class.java)
+                            val (owner, methods) = invocation.roundEnv
+                                    .getElementsAnnotatedWith(Dao::class.java)
                                     .map {
-                                        Pair(it, invocation.processingEnv.elementUtils.getAllMembers(MoreElements.asType(it))
-                                                .filter { MoreElements.isAnnotationPresent(it, Query::class.java) }
+                                        Pair(it,
+                                                invocation.processingEnv.elementUtils
+                                                        .getAllMembers(MoreElements.asType(it))
+                                                        .filter {
+                                                            MoreElements.isAnnotationPresent(it,
+                                                                    Query::class.java)
+                                                        }
                                         )
                                     }.filter { it.second.isNotEmpty() }.first()
                             val parser = QueryParser(invocation.roundEnv, invocation.processingEnv)
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/testing/TestProcessor.kt b/room/compiler/src/test/kotlin/com/android/support/room/testing/TestProcessor.kt
index 7713685..6496e7a 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/testing/TestProcessor.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/testing/TestProcessor.kt
@@ -30,9 +30,11 @@
 class TestProcessor(val handlers: List<(TestInvocation) -> Boolean>,
                     val annotations: MutableSet<String>) : AbstractProcessor() {
     var count = 0
-    override fun process(annotations: MutableSet<out TypeElement>, roundEnv: RoundEnvironment): Boolean {
+    override fun process(annotations: MutableSet<out TypeElement>, roundEnv: RoundEnvironment)
+            : Boolean {
         try {
-            return handlers.getOrNull(count++)?.invoke(TestInvocation(processingEnv, annotations, roundEnv)) ?: true
+            return handlers.getOrNull(count++)?.invoke(
+                    TestInvocation(processingEnv, annotations, roundEnv)) ?: true
         } catch (elmBound: ElementBoundException) {
             processingEnv.messager.printMessage(ERROR, elmBound.msg, elmBound.element)
         }