Repository: RBusarow/ModuleCheck (kotlin)

Issue:
Constant properties defined in a .java file do not count toward that module's declarations
Given:

```java
package com.example;

public class Constants {

  public static final int MY_VALUE = 250;
}
```

There should be a declaration of `com.example.Constants.MyValue` for that module, and importing that name should make the module count as "used".

Pull request:
fixes #207

Patch:
diff --git a/modulecheck-parsing/java/src/main/kotlin/modulecheck/parsing/java/JavaFile.kt b/modulecheck-parsing/java/src/main/kotlin/modulecheck/parsing/java/JavaFile.kt
index 9bba10173a..a90b076735 100644
--- a/modulecheck-parsing/java/src/main/kotlin/modulecheck/parsing/java/JavaFile.kt
+++ b/modulecheck-parsing/java/src/main/kotlin/modulecheck/parsing/java/JavaFile.kt
@@ -19,11 +19,17 @@ import com.github.javaparser.StaticJavaParser
 import com.github.javaparser.ast.ImportDeclaration
 import com.github.javaparser.ast.Node
 import com.github.javaparser.ast.NodeList
+import com.github.javaparser.ast.body.EnumConstantDeclaration
+import com.github.javaparser.ast.body.FieldDeclaration
 import com.github.javaparser.ast.body.TypeDeclaration
+import com.github.javaparser.ast.expr.SimpleName
 import com.github.javaparser.ast.type.ClassOrInterfaceType
+import com.github.javaparser.resolution.Resolvable
+import modulecheck.parsing.DeclarationName
 import modulecheck.parsing.JvmFile
 import modulecheck.parsing.asDeclaractionName
 import java.io.File
+import kotlin.properties.Delegates
 
 class JavaFile(val file: File) : JvmFile() {
 
@@ -33,32 +39,47 @@ class JavaFile(val file: File) : JvmFile() {
     val packageFqName: String,
     val imports: NodeList<ImportDeclaration>,
     val classOrInterfaceTypes: Set<ClassOrInterfaceType>,
-    val typeDeclarations: Set<TypeDeclaration<*>>
+    val typeDeclarations: List<TypeDeclaration<*>>,
+    val fieldDeclarations: Set<DeclarationName>,
+    val enumDeclarations: Set<DeclarationName>
   )
 
   private val parsed by lazy {
     val unit = StaticJavaParser.parse(file)
 
     val classOrInterfaceTypes = mutableSetOf<ClassOrInterfaceType>()
-    val typeDeclarations = mutableSetOf<TypeDeclaration<*>>()
+    val typeDeclarations = mutableListOf<TypeDeclaration<*>>()
+    val fieldDeclarations = mutableSetOf<DeclarationName>()
+    val enumDeclarations = mutableSetOf<DeclarationName>()
 
-    val iterator = NodeIterator { node ->
+    val iterator = NodeVisitor { node ->
 
       when (node) {
         is ClassOrInterfaceType -> classOrInterfaceTypes.add(node)
         is TypeDeclaration<*> -> typeDeclarations.add(node)
+        is FieldDeclaration -> {
+
+          if (node.isStatic && node.isPublic) {
+            fieldDeclarations.add(DeclarationName(node.fqName(typeDeclarations)))
+          }
+        }
+        is EnumConstantDeclaration -> {
+          enumDeclarations.add(DeclarationName(node.fqName(typeDeclarations)))
+        }
       }
 
       true
     }
 
-    iterator.explore(unit)
+    iterator.visit(unit)
 
     ParsedFile(
       packageFqName = unit.packageDeclaration.get().nameAsString,
       imports = unit.imports,
       classOrInterfaceTypes = classOrInterfaceTypes,
-      typeDeclarations = typeDeclarations
+      typeDeclarations = typeDeclarations.distinct(),
+      fieldDeclarations = fieldDeclarations,
+      enumDeclarations = enumDeclarations
     )
   }
 
@@ -79,6 +100,8 @@ class JavaFile(val file: File) : JvmFile() {
     parsed.typeDeclarations
       .map { it.fullyQualifiedName.get().asDeclaractionName() }
       .toSet()
+      .plus(parsed.fieldDeclarations)
+      .plus(parsed.enumDeclarations)
   }
 
   override val wildcardImports: Set<String> by lazy {
@@ -103,16 +126,42 @@ class JavaFile(val file: File) : JvmFile() {
       }
       .toSet()
   }
+
+  private fun <T> T.fqName(typeDeclarations: List<TypeDeclaration<*>>): String
+    where T : Node, T : Resolvable<*> {
+    val simpleName = simpleName()
+
+    val parentTypeFqName = typeDeclarations
+      .last { isDescendantOf(it) }
+      .fullyQualifiedName.get()
+    return "$parentTypeFqName.$simpleName"
+  }
+
+  private fun <T> T.simpleName(): String
+    where T : Node, T : Resolvable<*> {
+    var name: String by Delegates.notNull()
+
+    NodeVisitor { node ->
+      if (node is SimpleName) {
+        name = node.asString()
+        false
+      } else {
+        true
+      }
+    }.visit(this)
+
+    return name
+  }
 }
 
-class NodeIterator(
+internal class NodeVisitor(
   private val predicate: (node: Node) -> Boolean
 ) {
 
-  fun explore(node: Node) {
+  fun visit(node: Node) {
     if (predicate(node)) {
       node.childNodes.forEach { child ->
-        explore(child)
+        visit(child)
       }
     }
   }


Test patch:
diff --git a/modulecheck-parsing/java/src/test/kotlin/modulecheck/parsing/java/JavaFileTest.kt b/modulecheck-parsing/java/src/test/kotlin/modulecheck/parsing/java/JavaFileTest.kt
new file mode 100644
index 0000000000..7601929b37
--- /dev/null
+++ b/modulecheck-parsing/java/src/test/kotlin/modulecheck/parsing/java/JavaFileTest.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2021 Rick Busarow
+ * 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 modulecheck.parsing.java
+
+import modulecheck.parsing.DeclarationName
+import modulecheck.testing.BaseTest
+import org.junit.jupiter.api.Test
+import java.io.File
+
+internal class JavaFileTest : BaseTest() {
+
+  @Test
+  fun `enum constants should count as declarations`() {
+
+    val javaFile = file(
+      """
+    package com.example;
+
+    public enum Color { RED, BLUE }
+      """.trimIndent()
+    )
+
+    javaFile.declarations shouldBe listOf(
+      DeclarationName("com.example.Color"),
+      DeclarationName("com.example.Color.RED"),
+      DeclarationName("com.example.Color.BLUE")
+    )
+  }
+
+  @Test
+  fun `nested enum constants should count as declarations`() {
+
+    val javaFile = file(
+      """
+    package com.example;
+
+    public class Constants {
+      public enum Color { RED, BLUE }
+    }
+      """.trimIndent()
+    )
+
+    javaFile.declarations shouldBe listOf(
+      DeclarationName("com.example.Constants"),
+      DeclarationName("com.example.Constants.Color"),
+      DeclarationName("com.example.Constants.Color.RED"),
+      DeclarationName("com.example.Constants.Color.BLUE")
+    )
+  }
+
+  @Test
+  fun `declared constants should count as declarations`() {
+
+    val javaFile = file(
+      """
+    package com.example;
+
+    public class Constants {
+
+      public static final int MY_VALUE = 250;
+    }
+      """.trimIndent()
+    )
+
+    javaFile.declarations shouldBe listOf(
+      DeclarationName("com.example.Constants"),
+      DeclarationName("com.example.Constants.MY_VALUE")
+    )
+  }
+
+  @Test
+  fun `declared nested constants should count as declarations`() {
+
+    val javaFile = file(
+      """
+    package com.example;
+
+    public class Constants {
+
+      public static class Values {
+
+        public static final int MY_VALUE = 250;
+      }
+    }
+      """.trimIndent()
+    )
+
+    javaFile.declarations shouldBe listOf(
+      DeclarationName("com.example.Constants"),
+      DeclarationName("com.example.Constants.Values"),
+      DeclarationName("com.example.Constants.Values.MY_VALUE")
+    )
+  }
+
+  fun file(content: String): JavaFile {
+    testProjectDir.mkdirs()
+
+    val javaFile = File(testProjectDir, "javaFile.java")
+      .also { it.writeText(content) }
+
+    return JavaFile(javaFile)
+  }
+}