Repository: RBusarow/ModuleCheck (kotlin)

Issue:
Resource declarations can't be parsed if there's a dot in the name
example:
```xml
<resources>
  <style name="AppTheme.ClearActionBar" parent="Theme.AppCompat.Light.DarkActionBar"/>
</resources>
```
This yields the error:
```
Execution failed for task ':moduleCheckAuto'.
> The name `R.style.AppTheme.ClearActionBar` must follow the format `R.<prefix>.<identifier>`, such as `R.string.app_name`.
```

The actual generated resource is `R.style.AppTheme_ClearActionBar`.  There are no capitalization changes.

I just need to replace any `'.'` in the name attribute with `'_'`.

Pull request:
fixes #511

Patch:
diff --git a/modulecheck-parsing/source/src/main/kotlin/modulecheck/parsing/source/AndroidResourceDeclaredName.kt b/modulecheck-parsing/source/src/main/kotlin/modulecheck/parsing/source/AndroidResourceDeclaredName.kt
index f3a0e80001..f44aba2fde 100644
--- a/modulecheck-parsing/source/src/main/kotlin/modulecheck/parsing/source/AndroidResourceDeclaredName.kt
+++ b/modulecheck-parsing/source/src/main/kotlin/modulecheck/parsing/source/AndroidResourceDeclaredName.kt
@@ -204,31 +204,31 @@ sealed class UnqualifiedAndroidResourceDeclaredName(
         dir.startsWith("menu") -> Menu(name)
         dir.startsWith("mipmap") -> Mipmap(name)
         dir.startsWith("raw") -> Raw(name)
-        dir.startsWith("xml") -> Style(name)
         else -> null
       }
     }
 
     @Suppress("ComplexMethod")
     fun fromValuePair(type: String, name: String): UnqualifiedAndroidResourceDeclaredName? {
+      val fixedName = name.replace('.', '_')
       return when (type) {
-        "anim" -> Anim(name)
-        "animator" -> Animator(name)
-        "array" -> Arrays(name)
-        "bool" -> Bool(name)
-        "color" -> Color(name)
-        "dimen" -> Dimen(name)
-        "drawable" -> Drawable(name)
-        "font" -> Font(name)
-        "id" -> ID(name)
-        "integer" -> AndroidInteger(name)
-        "integer-array" -> Arrays(name)
-        "layout" -> Layout(name)
-        "menu" -> Menu(name)
-        "mipmap" -> Mipmap(name)
-        "raw" -> Raw(name)
-        "string" -> AndroidString(name)
-        "style" -> Style(name)
+        "anim" -> Anim(fixedName)
+        "animator" -> Animator(fixedName)
+        "array" -> Arrays(fixedName)
+        "bool" -> Bool(fixedName)
+        "color" -> Color(fixedName)
+        "dimen" -> Dimen(fixedName)
+        "drawable" -> Drawable(fixedName)
+        "font" -> Font(fixedName)
+        "id" -> ID(fixedName)
+        "integer" -> AndroidInteger(fixedName)
+        "integer-array" -> Arrays(fixedName)
+        "layout" -> Layout(fixedName)
+        "menu" -> Menu(fixedName)
+        "mipmap" -> Mipmap(fixedName)
+        "raw" -> Raw(fixedName)
+        "string" -> AndroidString(fixedName)
+        "style" -> Style(fixedName)
         else -> null
       }
     }


Test patch:
diff --git a/modulecheck-core/src/test/kotlin/modulecheck/core/UnusedDependenciesTest.kt b/modulecheck-core/src/test/kotlin/modulecheck/core/UnusedDependenciesTest.kt
index 9f7b86e56a..f44d3b9414 100644
--- a/modulecheck-core/src/test/kotlin/modulecheck/core/UnusedDependenciesTest.kt
+++ b/modulecheck-core/src/test/kotlin/modulecheck/core/UnusedDependenciesTest.kt
@@ -1813,6 +1813,134 @@ class UnusedDependenciesTest : RunnerTest() {
     logger.parsedReport() shouldBe listOf()
   }
 
+  @Test
+  fun `style resource with dot-qualified name used in kotlin source should not be unused`() {
+
+    settings.deleteUnused = false
+
+    val lib1 = androidLibrary(":lib1", "com.modulecheck.lib1") {
+
+      addResourceFile(
+        "values/strings.xml",
+        """
+        <resources>
+          <style name="AppTheme.ClearActionBar" parent="Theme.AppCompat.Light.DarkActionBar"/>
+        </resources>
+        """
+      )
+    }
+
+    val lib2 = androidLibrary(":lib2", "com.modulecheck.lib2") {
+      addDependency(ConfigurationName.api, lib1)
+
+      buildFile {
+        """
+        plugins {
+          id("com.android.library")
+          kotlin("android")
+        }
+
+        dependencies {
+          api(project(path = ":lib1"))
+        }
+        """
+      }
+
+      addKotlinSource(
+        """
+        package com.modulecheck.lib2
+
+        val style = R.style.AppTheme_ClearActionBar
+        """
+      )
+    }
+
+    run().isSuccess shouldBe true
+
+    lib2.buildFile shouldHaveText """
+        plugins {
+          id("com.android.library")
+          kotlin("android")
+        }
+
+        dependencies {
+          api(project(path = ":lib1"))
+        }
+    """
+
+    logger.parsedReport() shouldBe listOf()
+  }
+
+  @Test
+  fun `style resource with dot-qualified name used in manifest should not be unused`() {
+
+    settings.deleteUnused = false
+
+    val lib1 = androidLibrary(":lib1", "com.modulecheck.lib1") {
+
+      addResourceFile(
+        "values/strings.xml",
+        """
+        <resources>
+          <style name="AppTheme.ClearActionBar" parent="Theme.AppCompat.Light.DarkActionBar"/>
+        </resources>
+        """
+      )
+    }
+
+    val lib2 = androidLibrary(":lib2", "com.modulecheck.lib2") {
+      addDependency(ConfigurationName.api, lib1)
+
+      buildFile {
+        """
+        plugins {
+          id("com.android.library")
+          kotlin("android")
+        }
+
+        dependencies {
+          api(project(path = ":lib1"))
+        }
+        """
+      }
+
+      addManifest(
+        """
+          <manifest
+            xmlns:android="https://schemas.android.com/apk/res/android"
+            package="com.example.app"
+            >
+
+            <application
+              android:name=".App"
+              android:allowBackup="true"
+              android:icon="@mipmap/ic_launcher"
+              android:label="@string/app_name"
+              android:roundIcon="@mipmap/ic_launcher_round"
+              android:supportsRtl="true"
+              android:theme="@style/AppTheme.ClearActionBar"
+              />
+          </manifest>
+        """
+      )
+    }
+
+    run().isSuccess shouldBe true
+
+    lib2.buildFile shouldHaveText """
+        plugins {
+          id("com.android.library")
+          kotlin("android")
+        }
+
+        dependencies {
+          api(project(path = ":lib1"))
+        }
+    """
+
+    logger.parsedReport() shouldBe listOf()
+  }
+
   @Test
   fun `declaration used via class reference wtih wildcard import should not be unused`() {
 
diff --git a/modulecheck-parsing/android/src/test/kotlin/modulecheck/parsing/android/AndroidResourceParserTest.kt b/modulecheck-parsing/android/src/test/kotlin/modulecheck/parsing/android/AndroidResourceParserTest.kt
index df797ea6cb..17214f09ab 100644
--- a/modulecheck-parsing/android/src/test/kotlin/modulecheck/parsing/android/AndroidResourceParserTest.kt
+++ b/modulecheck-parsing/android/src/test/kotlin/modulecheck/parsing/android/AndroidResourceParserTest.kt
@@ -16,6 +16,7 @@
 package modulecheck.parsing.android
 
 import modulecheck.parsing.source.UnqualifiedAndroidResourceDeclaredName.AndroidString
+import modulecheck.parsing.source.UnqualifiedAndroidResourceDeclaredName.Style
 import modulecheck.testing.BaseTest
 import modulecheck.testing.createSafely
 import modulecheck.utils.child
@@ -78,4 +79,24 @@ internal class AndroidResourceParserTest : BaseTest() {
       AndroidString("app_name")
     )
   }
+
+  @Test
+  fun `a node with dots in its name should be parsed using underscores`() {
+    val text = """
+      <?xml version="1.0" encoding="utf-8"?>
+      <resources>
+        <style name="AppTheme.ClearActionBar" parent="Theme.AppCompat.Light.DarkActionBar"/>
+      </resources>
+    """.trimIndent()
+
+    val xml = testProjectDir
+      .child("styles.xml")
+      .createSafely(text)
+
+    val declarations = AndroidResourceParser().parseFile(xml)
+
+    declarations shouldBe setOf(
+      Style("AppTheme_ClearActionBar")
+    )
+  }
 }