Repository: RBusarow/ModuleCheck (kotlin)

Issue:
`GroovyLangParser` has frequent `NullPointerException`s when parsing concurrently
This stacktrace shows an exception while parsing ~190 `build.gradle` (in Groovy) files for the `SortDependenciesRule`.

```
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':moduleCheckSortDependenciesAuto'.
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.lambda$executeIfValid$1(ExecuteActionsTaskExecuter.java:147)
        at org.gradle.internal.Try$Failure.ifSuccessfulOrElse(Try.java:282)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:145)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:133)
        at org.gradle.api.internal.tasks.execution.CleanupStaleOutputsExecuter.execute(CleanupStaleOutputsExecuter.java:77)
        at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:46)
        at org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter.execute(ResolveTaskExecutionModeExecuter.java:51)
        at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:57)
        at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:56)
        at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:199)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
        at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:74)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:333)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:320)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:313)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:299)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.lambda$run$0(DefaultPlanExecutor.java:143)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:227)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.executeNextNode(DefaultPlanExecutor.java:218)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:140)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
        at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
Caused by: java.lang.NullPointerException
        at groovyjarjarantlr4.v4.runtime.atn.ParserATNSimulator.adaptivePredict(ParserATNSimulator.java:367)
        at groovyjarjarantlr4.v4.runtime.atn.ParserATNSimulator.adaptivePredict(ParserATNSimulator.java:357)
        at org.apache.groovy.parser.antlr4.GroovyParser.firstArgumentListElement(GroovyParser.java:11998)
        at org.apache.groovy.parser.antlr4.GroovyParser.argumentList(GroovyParser.java:11843)
        at org.apache.groovy.parser.antlr4.GroovyParser.commandExpression(GroovyParser.java:9308)
        at org.apache.groovy.parser.antlr4.GroovyParser.statementExpression(GroovyParser.java:8067)
        at org.apache.groovy.parser.antlr4.GroovyParser.statement(GroovyParser.java:6915)
        at org.apache.groovy.parser.antlr4.GroovyParser.blockStatement(GroovyParser.java:5555)
        at org.apache.groovy.parser.antlr4.GroovyParser.blockStatements(GroovyParser.java:4866)
        at org.apache.groovy.parser.antlr4.GroovyParser.blockStatementsOpt(GroovyParser.java:4805)
        at org.apache.groovy.parser.antlr4.GroovyParser.closure(GroovyParser.java:4706)
        at org.apache.groovy.parser.antlr4.GroovyParser.closureOrLambdaExpression(GroovyParser.java:4752)
        at org.apache.groovy.parser.antlr4.GroovyParser.pathElement(GroovyParser.java:9717)
        at org.apache.groovy.parser.antlr4.GroovyParser.pathExpression(GroovyParser.java:9487)
        at org.apache.groovy.parser.antlr4.GroovyParser.postfixExpression(GroovyParser.java:8108)
        at org.apache.groovy.parser.antlr4.GroovyParser.expression(GroovyParser.java:8650)
        at org.apache.groovy.parser.antlr4.GroovyParser.commandExpression(GroovyParser.java:9299)
        at org.apache.groovy.parser.antlr4.GroovyParser.statementExpression(GroovyParser.java:8067)
        at org.apache.groovy.parser.antlr4.GroovyParser.statement(GroovyParser.java:6915)
        at org.apache.groovy.parser.antlr4.GroovyParser.scriptStatement(GroovyParser.java:514)
        at org.apache.groovy.parser.antlr4.GroovyParser.scriptStatements(GroovyParser.java:421)
        at org.apache.groovy.parser.antlr4.GroovyParser.compilationUnit(GroovyParser.java:357)
        at modulecheck.parsing.groovy.antlr.GroovyParsingScope.<init>(GroovyParsingScope.kt:36)
        at modulecheck.parsing.groovy.antlr.GroovyParsingScopeKt$parse$1.invokeSuspend(GroovyParsingScope.kt:52)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
        at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
        at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
        at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
        at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
        at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
        at modulecheck.parsing.groovy.antlr.GroovyParsingScopeKt.parse(GroovyParsingScope.kt:50)
        at modulecheck.parsing.groovy.antlr.GroovyDependencyBlockParser.parse(GroovyDependencyBlockParser.kt:37)
        at modulecheck.parsing.wiring.RealDependenciesBlocksProvider.get(RealDependenciesBlocksProvider.kt:40)
        at modulecheck.project.BuildFileParser.dependenciesBlocks(BuildFileParser.kt:53)
        at modulecheck.core.rule.SortDependenciesRule.check(SortDependenciesRule.kt:50)
        at modulecheck.core.rule.SingleRuleFindingFactory$evaluateRule$2$1$1.invokeSuspend(SingleRuleFindingFactory.kt:53)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.internal.LimitedDispatcher.run(LimitedDispatcher.kt:39)
        at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:95)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
```

Pull request description:
fixes #462

Patch:
diff --git a/modulecheck-parsing/groovy-antlr/src/main/kotlin/modulecheck/parsing/groovy/antlr/GroovyDependencyBlockParser.kt b/modulecheck-parsing/groovy-antlr/src/main/kotlin/modulecheck/parsing/groovy/antlr/GroovyDependencyBlockParser.kt
index e6c4406464..e151b5e0ed 100644
--- a/modulecheck-parsing/groovy-antlr/src/main/kotlin/modulecheck/parsing/groovy/antlr/GroovyDependencyBlockParser.kt
+++ b/modulecheck-parsing/groovy-antlr/src/main/kotlin/modulecheck/parsing/groovy/antlr/GroovyDependencyBlockParser.kt
@@ -29,11 +29,12 @@ import org.apache.groovy.parser.antlr4.GroovyParser.ScriptStatementContext
 import org.apache.groovy.parser.antlr4.GroovyParser.SepContext
 import org.apache.groovy.parser.antlr4.GroovyParser.StringLiteralContext
 import org.apache.groovy.parser.antlr4.GroovyParserBaseVisitor
+import java.io.File
 import javax.inject.Inject
 
 class GroovyDependencyBlockParser @Inject constructor() {
 
-  fun parse(file: String): List<GroovyDependenciesBlock> = parse(file) {
+  fun parse(file: File): List<GroovyDependenciesBlock> = parse(file) {
     val dependenciesBlocks = mutableListOf<GroovyDependenciesBlock>()
 
     val rawModuleNameVisitor = object : GroovyParserBaseVisitor<String?>() {
diff --git a/modulecheck-parsing/groovy-antlr/src/main/kotlin/modulecheck/parsing/groovy/antlr/GroovyParsingScope.kt b/modulecheck-parsing/groovy-antlr/src/main/kotlin/modulecheck/parsing/groovy/antlr/GroovyParsingScope.kt
index b2891e04e4..8f5d8befbe 100644
--- a/modulecheck-parsing/groovy-antlr/src/main/kotlin/modulecheck/parsing/groovy/antlr/GroovyParsingScope.kt
+++ b/modulecheck-parsing/groovy-antlr/src/main/kotlin/modulecheck/parsing/groovy/antlr/GroovyParsingScope.kt
@@ -37,10 +37,11 @@ internal data class GroovyParsingScope(val fullText: String) {
   }
 }
 
+private val groovyLangParserLock = Unit
+
 internal inline fun <T> parse(file: File, parsingAction: GroovyParsingScope.() -> T): T {
-  return GroovyParsingScope(file.readText()).parsingAction()
-}
 
-internal inline fun <T> parse(fullText: String, parsingAction: GroovyParsingScope.() -> T): T {
-  return GroovyParsingScope(fullText).parsingAction()
+  // GroovyLangParser has frequent NPE's when parsing concurrently.
+  // https://github.com/RBusarow/ModuleCheck/issues/462
+  return synchronized(groovyLangParserLock) { GroovyParsingScope(file.readText()).parsingAction() }
 }
diff --git a/modulecheck-parsing/groovy-antlr/src/main/kotlin/modulecheck/parsing/groovy/antlr/GroovyPluginsBlockParser.kt b/modulecheck-parsing/groovy-antlr/src/main/kotlin/modulecheck/parsing/groovy/antlr/GroovyPluginsBlockParser.kt
index 7f8979de9d..e566c5543b 100644
--- a/modulecheck-parsing/groovy-antlr/src/main/kotlin/modulecheck/parsing/groovy/antlr/GroovyPluginsBlockParser.kt
+++ b/modulecheck-parsing/groovy-antlr/src/main/kotlin/modulecheck/parsing/groovy/antlr/GroovyPluginsBlockParser.kt
@@ -18,11 +18,12 @@ package modulecheck.parsing.groovy.antlr
 import org.apache.groovy.parser.antlr4.GroovyParser.BlockStatementContext
 import org.apache.groovy.parser.antlr4.GroovyParser.ScriptStatementContext
 import org.apache.groovy.parser.antlr4.GroovyParserBaseVisitor
+import java.io.File
 import javax.inject.Inject
 
 class GroovyPluginsBlockParser @Inject constructor() {
 
-  fun parse(file: String): GroovyPluginsBlock? = parse(file) {
+  fun parse(file: File): GroovyPluginsBlock? = parse(file) {
 
     var block: GroovyPluginsBlock? = null
 
@@ -36,7 +37,7 @@ class GroovyPluginsBlockParser @Inject constructor() {
         if (statement?.start?.text == "plugins") {
           val blockBodyReg = """plugins\s*\{([\s\S]*)\}""".toRegex()
 
-          val blockBody = blockBodyReg.find(file)
+          val blockBody = blockBodyReg.find(file.readText())
             ?.groupValues
             ?.get(1)
             ?.removePrefix("\n")
diff --git a/modulecheck-parsing/wiring/src/main/kotlin/modulecheck/parsing/wiring/RealDependenciesBlocksProvider.kt b/modulecheck-parsing/wiring/src/main/kotlin/modulecheck/parsing/wiring/RealDependenciesBlocksProvider.kt
index b2f80fa73f..190e49ca72 100644
--- a/modulecheck-parsing/wiring/src/main/kotlin/modulecheck/parsing/wiring/RealDependenciesBlocksProvider.kt
+++ b/modulecheck-parsing/wiring/src/main/kotlin/modulecheck/parsing/wiring/RealDependenciesBlocksProvider.kt
@@ -37,7 +37,7 @@ class RealDependenciesBlocksProvider(
 
     return when {
       buildFile.isKotlinScriptFile() -> kotlinParser.parse(invokesConfigurationNames)
-      buildFile.extension == "gradle" -> groovyParser.parse(buildFile.readText())
+      buildFile.extension == "gradle" -> groovyParser.parse(buildFile)
       else -> throw IllegalArgumentException(
         "The file argument must be either a `*.gradle.kts` file or `*.gradle`.  " +
           "The supplied argument was `${buildFile.name}`"
diff --git a/modulecheck-parsing/wiring/src/main/kotlin/modulecheck/parsing/wiring/RealPluginsBlockProvider.kt b/modulecheck-parsing/wiring/src/main/kotlin/modulecheck/parsing/wiring/RealPluginsBlockProvider.kt
index 73f014d0b9..c6c948645b 100644
--- a/modulecheck-parsing/wiring/src/main/kotlin/modulecheck/parsing/wiring/RealPluginsBlockProvider.kt
+++ b/modulecheck-parsing/wiring/src/main/kotlin/modulecheck/parsing/wiring/RealPluginsBlockProvider.kt
@@ -36,7 +36,7 @@ class RealPluginsBlockProvider(
   override fun get(): PluginsBlock? {
     return when {
       buildFile.isKotlinFile(listOf("kts")) -> kotlinParser.parse(buildFile.asKtFile())
-      buildFile.extension == "gradle" -> groovyParser.parse(buildFile.readText())
+      buildFile.extension == "gradle" -> groovyParser.parse(buildFile)
       else -> throw IllegalArgumentException(
         "The file argument must be either a `*.gradle.kts` file or `*.gradle`.  " +
           "The supplied argument was `${buildFile.name}`"


Test patch:
diff --git a/modulecheck-internal-testing/src/main/kotlin/modulecheck/testing/asserts.kt b/modulecheck-internal-testing/src/main/kotlin/modulecheck/testing/asserts.kt
index 216c890bb6..a8919d9619 100644
--- a/modulecheck-internal-testing/src/main/kotlin/modulecheck/testing/asserts.kt
+++ b/modulecheck-internal-testing/src/main/kotlin/modulecheck/testing/asserts.kt
@@ -17,7 +17,9 @@ package modulecheck.testing
 
 import org.junit.jupiter.api.fail
 
-fun <T : Any> T?.requireNotNullOrFail(lazyMessage: () -> String): T {
+fun <T : Any> T?.requireNotNullOrFail(
+  lazyMessage: () -> String = { "The receiver cannot be null, but it was. ¯\\_(ツ)_/¯" }
+): T {
 
   if (this != null) return this
 
diff --git a/modulecheck-parsing/groovy-antlr/src/test/kotlin/modulecheck/parsing/groovy/antlr/GroovyDependencyBlockParserTest.kt b/modulecheck-parsing/groovy-antlr/src/test/kotlin/modulecheck/parsing/groovy/antlr/GroovyDependencyBlockParserTest.kt
index bccbd9c7d4..326ae1c65d 100644
--- a/modulecheck-parsing/groovy-antlr/src/test/kotlin/modulecheck/parsing/groovy/antlr/GroovyDependencyBlockParserTest.kt
+++ b/modulecheck-parsing/groovy-antlr/src/test/kotlin/modulecheck/parsing/groovy/antlr/GroovyDependencyBlockParserTest.kt
@@ -15,7 +15,6 @@
 
 package modulecheck.parsing.groovy.antlr
 
-import io.kotest.matchers.shouldBe
 import modulecheck.parsing.gradle.ConfigurationName
 import modulecheck.parsing.gradle.ExternalDependencyDeclaration
 import modulecheck.parsing.gradle.ModuleDependencyDeclaration
@@ -23,22 +22,24 @@ import modulecheck.parsing.gradle.ModuleRef
 import modulecheck.parsing.gradle.ModuleRef.StringRef
 import modulecheck.parsing.gradle.ModuleRef.TypeSafeRef
 import modulecheck.parsing.gradle.UnknownDependencyDeclaration
+import modulecheck.testing.BaseTest
+import modulecheck.testing.createSafely
+import modulecheck.utils.child
+import org.intellij.lang.annotations.Language
 import org.junit.jupiter.api.Test
 
-internal class GroovyDependencyBlockParserTest {
+internal class GroovyDependencyBlockParserTest : BaseTest() {
 
   @Test
-  fun `external declaration`() {
-    val block = GroovyDependencyBlockParser()
-      .parse(
-        """
-       dependencies {
-          api 'com.foo:bar:1.2.3.4'
-       }
-        """.trimIndent()
-      ).single()
-
-    block.settings shouldBe listOf(
+  fun `external declaration`() = parse(
+    """
+    dependencies {
+       api 'com.foo:bar:1.2.3.4'
+    }
+    """
+  ) {
+
+    settings shouldBe listOf(
       ExternalDependencyDeclaration(
         configName = ConfigurationName.api,
         declarationText = "api 'com.foo:bar:1.2.3.4'",
@@ -51,18 +52,16 @@ internal class GroovyDependencyBlockParserTest {
   }
 
   @Test
-  fun `declaration's original string should include trailing comment`() {
-    val block = GroovyDependencyBlockParser()
-      .parse(
-        """
-       dependencies {
-          api project(':core:jvm') // trailing comment
-          api project(':core:jvm')
-       }
-        """.trimIndent()
-      ).single()
-
-    block.getOrEmpty(ModuleRef.StringRef(":core:jvm"), ConfigurationName.api) shouldBe listOf(
+  fun `declaration's original string should include trailing comment`() = parse(
+    """
+    dependencies {
+       api project(':core:jvm') // trailing comment
+       api project(':core:jvm')
+    }
+    """
+  ) {
+
+    getOrEmpty(ModuleRef.StringRef(":core:jvm"), ConfigurationName.api) shouldBe listOf(
       ModuleDependencyDeclaration(
         moduleRef = StringRef(":core:jvm"),
         moduleAccess = "project(':core:jvm')",
@@ -81,20 +80,18 @@ internal class GroovyDependencyBlockParserTest {
   }
 
   @Test
-  fun `declaration with noinspection should include suppressed with argument`() {
-    val block = GroovyDependencyBlockParser()
-      .parse(
-        """
-       dependencies {
-         api project(':core:android')
-         //noinspection Unused, MustBeApi
-         api project(':core:jvm')
-         testImplementation project(':core:test')
-       }
-        """.trimIndent()
-      ).single()
-
-    block.settings shouldBe listOf(
+  fun `declaration with noinspection should include suppressed with argument`() = parse(
+    """
+    dependencies {
+      api project(':core:android')
+      //noinspection Unused, MustBeApi
+      api project(':core:jvm')
+      testImplementation project(':core:test')
+    }
+    """
+  ) {
+
+    settings shouldBe listOf(
       ModuleDependencyDeclaration(
         moduleRef = StringRef(":core:android"),
         moduleAccess = "project(':core:android')",
@@ -123,53 +120,50 @@ internal class GroovyDependencyBlockParserTest {
   }
 
   @Test
-  fun `dependency block with noinspection comment should suppress those warnings in all declarations`() {
-    val block = GroovyDependencyBlockParser()
-      .parse(
-        """
-       //noinspection Unused, MustBeApi
-       dependencies {
-         api project(':core:android')
-         //noinspection InheritedDependency
-         api project(':core:jvm')
-       }
-        """.trimIndent()
-      ).single()
-
-    block.suppressAll shouldBe listOf("Unused", "MustBeApi")
-
-    block.settings shouldBe listOf(
-      ModuleDependencyDeclaration(
-        moduleRef = StringRef(":core:android"),
-        moduleAccess = "project(':core:android')",
-        configName = ConfigurationName.api,
-        declarationText = """api project(':core:android')""",
-        statementWithSurroundingText = "  api project(':core:android')",
-        suppressed = listOf("Unused", "MustBeApi")
-      ),
-      ModuleDependencyDeclaration(
-        moduleRef = StringRef(":core:jvm"),
-        moduleAccess = "project(':core:jvm')",
-        configName = ConfigurationName.api,
-        declarationText = """api project(':core:jvm')""",
-        statementWithSurroundingText = "  //noinspection InheritedDependency\n  api project(':core:jvm')",
-        suppressed = listOf("InheritedDependency", "Unused", "MustBeApi")
+  fun `dependency block with noinspection comment should suppress those warnings in all declarations`() =
+    parse(
+      """
+      //noinspection Unused, MustBeApi
+      dependencies {
+        api project(':core:android')
+        //noinspection InheritedDependency
+        api project(':core:jvm')
+      }
+      """
+    ) {
+
+      suppressAll shouldBe listOf("Unused", "MustBeApi")
+
+      settings shouldBe listOf(
+        ModuleDependencyDeclaration(
+          moduleRef = StringRef(":core:android"),
+          moduleAccess = "project(':core:android')",
+          configName = ConfigurationName.api,
+          declarationText = """api project(':core:android')""",
+          statementWithSurroundingText = "  api project(':core:android')",
+          suppressed = listOf("Unused", "MustBeApi")
+        ),
+        ModuleDependencyDeclaration(
+          moduleRef = StringRef(":core:jvm"),
+          moduleAccess = "project(':core:jvm')",
+          configName = ConfigurationName.api,
+          declarationText = """api project(':core:jvm')""",
+          statementWithSurroundingText = "  //noinspection InheritedDependency\n  api project(':core:jvm')",
+          suppressed = listOf("InheritedDependency", "Unused", "MustBeApi")
+        )
       )
-    )
-  }
+    }
 
   @Test
-  fun `string module dependency declaration with testFixtures should be parsed`() {
-    val block = GroovyDependencyBlockParser()
-      .parse(
-        """
-       dependencies {
-          api testFixtures(project(':core:jvm'))
-       }
-        """.trimIndent()
-      ).single()
-
-    block.settings shouldBe listOf(
+  fun `string module dependency declaration with testFixtures should be parsed`() = parse(
+    """
+    dependencies {
+       api testFixtures(project(':core:jvm'))
+    }
+    """
+  ) {
+
+    settings shouldBe listOf(
       ModuleDependencyDeclaration(
         moduleRef = StringRef(":core:jvm"),
         moduleAccess = "project(':core:jvm')",
@@ -181,43 +175,39 @@ internal class GroovyDependencyBlockParserTest {
   }
 
   @Test
-  fun `type-safe module dependency declaration with testFixtures should be parsed`() {
-    val block = GroovyDependencyBlockParser()
-      .parse(
-        """
-       dependencies {
-          api testFixtures(projects.core.jvm)
-       }
-        """.trimIndent()
-      ).single()
-
-    block.settings shouldBe listOf(
+  fun `type-safe module dependency declaration with testFixtures should be parsed`() = parse(
+    """
+    dependencies {
+      api testFixtures(projects.core.jvm)
+    }
+    """
+  ) {
+
+    settings shouldBe listOf(
       ModuleDependencyDeclaration(
         moduleRef = TypeSafeRef("core.jvm"),
         moduleAccess = "projects.core.jvm",
         configName = ConfigurationName.api,
         declarationText = """api testFixtures(projects.core.jvm)""",
-        statementWithSurroundingText = """   api testFixtures(projects.core.jvm)"""
+        statementWithSurroundingText = """  api testFixtures(projects.core.jvm)"""
       )
     )
   }
 
   @Test
-  fun `module dependency with config block should split declarations properly`() {
-    val block = GroovyDependencyBlockParser()
-      .parse(
-        """
-       dependencies {
-          api project(':core:test') {
-            exclude group: 'androidx.appcompat'
-          }
-
-          api project(':core:jvm')
-       }
-        """.trimIndent()
-      ).single()
-
-    block.getOrEmpty(
+  fun `module dependency with config block should split declarations properly`() = parse(
+    """
+    dependencies {
+      api project(':core:test') {
+        exclude group: 'androidx.appcompat'
+      }
+
+      api project(':core:jvm')
+    }
+    """
+  ) {
+
+    getOrEmpty(
       ModuleRef.StringRef(":core:test"),
       ConfigurationName.api
     ) shouldBe listOf(
@@ -226,85 +216,82 @@ internal class GroovyDependencyBlockParserTest {
         moduleAccess = "project(':core:test')",
         configName = ConfigurationName.api,
         declarationText = """api project(':core:test') {
-          |     exclude group: 'androidx.appcompat'
-          |   }
+        |    exclude group: 'androidx.appcompat'
+        |  }
         """.trimMargin(),
-        statementWithSurroundingText = """   api project(':core:test') {
-          |     exclude group: 'androidx.appcompat'
-          |   }
+        statementWithSurroundingText = """  api project(':core:test') {
+        |    exclude group: 'androidx.appcompat'
+        |  }
         """.trimMargin()
       )
     )
 
-    block.getOrEmpty(":core:jvm", ConfigurationName.api) shouldBe listOf(
+    getOrEmpty(":core:jvm", ConfigurationName.api) shouldBe listOf(
       ModuleDependencyDeclaration(
         moduleRef = StringRef(":core:jvm"),
         moduleAccess = "project(':core:jvm')",
         configName = ConfigurationName.api,
         declarationText = "api project(':core:jvm')",
-        statementWithSurroundingText = "\n   api project(':core:jvm')"
+        statementWithSurroundingText = "\n  api project(':core:jvm')"
       )
     )
   }
 
   @Test
-  fun `module dependency with config block and preceding declaration should split declarations properly`() {
-    val block = GroovyDependencyBlockParser()
-      .parse(
-        """
-       dependencies {
-          api project(':core:jvm')
-
-          api project(':core:test') {
-            exclude group: 'androidx.appcompat'
-          }
-       }
-        """.trimIndent()
-      ).single()
-
-    block.getOrEmpty(":core:test", ConfigurationName.api) shouldBe listOf(
-      ModuleDependencyDeclaration(
-        moduleRef = StringRef(":core:test"),
-        moduleAccess = "project(':core:test')",
-        configName = ConfigurationName.api,
-        declarationText = """api project(':core:test') {
-          |     exclude group: 'androidx.appcompat'
-          |   }
-        """.trimMargin(),
-        statementWithSurroundingText = """
-          |
-          |   api project(':core:test') {
-          |     exclude group: 'androidx.appcompat'
-          |   }
-        """.trimMargin()
+  fun `module dependency with config block and preceding declaration should split declarations properly`() =
+    parse(
+      """
+      dependencies {
+        api project(':core:jvm')
+
+        api project(':core:test') {
+          exclude group: 'androidx.appcompat'
+        }
+      }
+      """
+    ) {
+
+      getOrEmpty(":core:test", ConfigurationName.api) shouldBe listOf(
+        ModuleDependencyDeclaration(
+          moduleRef = StringRef(":core:test"),
+          moduleAccess = "project(':core:test')",
+          configName = ConfigurationName.api,
+          declarationText = """api project(':core:test') {
+          |    exclude group: 'androidx.appcompat'
+          |  }
+          """.trimMargin(),
+          statementWithSurroundingText = """
+
+          |  api project(':core:test') {
+          |    exclude group: 'androidx.appcompat'
+          |  }
+          """.trimMargin()
+        )
       )
-    )
 
-    block.getOrEmpty(":core:jvm", ConfigurationName.api) shouldBe listOf(
-      ModuleDependencyDeclaration(
-        moduleRef = StringRef(":core:jvm"),
-        moduleAccess = "project(':core:jvm')",
-        configName = ConfigurationName.api,
-        declarationText = "api project(':core:jvm')",
-        statementWithSurroundingText = "   api project(':core:jvm')"
+      getOrEmpty(":core:jvm", ConfigurationName.api) shouldBe listOf(
+        ModuleDependencyDeclaration(
+          moduleRef = StringRef(":core:jvm"),
+          moduleAccess = "project(':core:jvm')",
+          configName = ConfigurationName.api,
+          declarationText = "api project(':core:jvm')",
+          statementWithSurroundingText = "  api project(':core:jvm')"
+        )
       )
-    )
-  }
+    }
 
   @Test
-  fun `module dependency with preceding blank line should preserve the blank line`() {
-    val block = GroovyDependencyBlockParser()
-      .parse(
-        """
-       dependencies {
-          api project(':core:test')
-
-          api project(':core:jvm')
-       }
-        """.trimIndent()
-      ).single()
-
-    block.getOrEmpty(":core:jvm", ConfigurationName.api) shouldBe listOf(
+  fun `module dependency with preceding blank line should preserve the blank line`() = parse(
+    """
+    dependencies {
+       api project(':core:test')
+
+       api project(':core:jvm')
+    }
+    """
+  ) {
+
+    getOrEmpty(":core:jvm", ConfigurationName.api) shouldBe listOf(
       ModuleDependencyDeclaration(
         moduleRef = StringRef(":core:jvm"),
         moduleAccess = "project(':core:jvm')",
@@ -316,18 +303,16 @@ internal class GroovyDependencyBlockParserTest {
   }
 
   @Test
-  fun `module dependency with two different configs should be recorded twice`() {
-    val block = GroovyDependencyBlockParser()
-      .parse(
-        """
-       dependencies {
-          implementation project(':core:jvm')
-          api project(':core:jvm')
-       }
-        """.trimIndent()
-      ).single()
-
-    block.getOrEmpty(":core:jvm", ConfigurationName.api) shouldBe listOf(
+  fun `module dependency with two different configs should be recorded twice`() = parse(
+    """
+    dependencies {
+       implementation project(':core:jvm')
+       api project(':core:jvm')
+    }
+    """
+  ) {
+
+    getOrEmpty(":core:jvm", ConfigurationName.api) shouldBe listOf(
       ModuleDependencyDeclaration(
         moduleRef = StringRef(":core:jvm"),
         moduleAccess = "project(':core:jvm')",
@@ -337,7 +322,7 @@ internal class GroovyDependencyBlockParserTest {
       )
     )
 
-    block.getOrEmpty(":core:jvm", ConfigurationName.implementation) shouldBe listOf(
+    getOrEmpty(":core:jvm", ConfigurationName.implementation) shouldBe listOf(
       ModuleDependencyDeclaration(
         moduleRef = StringRef(":core:jvm"),
         moduleAccess = "project(':core:jvm')",
@@ -349,80 +334,74 @@ internal class GroovyDependencyBlockParserTest {
   }
 
   @Test
-  fun `declaration's original string should include preceding single-line comment`() {
-    val block = GroovyDependencyBlockParser()
-      .parse(
-        """
-       dependencies {
-          api("com.foo:bar:1.2.3.4") // inline comment
-
-          // single-line comment
-          implementation project(':core:android')
-       }
-        """.trimIndent()
-      ).single()
-
-    block.getOrEmpty(":core:android", ConfigurationName.implementation) shouldBe listOf(
+  fun `declaration's original string should include preceding single-line comment`() = parse(
+    """
+    dependencies {
+      api("com.foo:bar:1.2.3.4") // inline comment
+
+      // single-line comment
+      implementation project(':core:android')
+    }
+    """
+  ) {
+
+    getOrEmpty(":core:android", ConfigurationName.implementation) shouldBe listOf(
       ModuleDependencyDeclaration(
         moduleRef = StringRef(":core:android"),
         moduleAccess = "project(':core:android')",
         configName = ConfigurationName.implementation,
         declarationText = """implementation project(':core:android')""",
         statementWithSurroundingText = """
-          |
-          |   // single-line comment
-          |   implementation project(':core:android')
+
+        |  // single-line comment
+        |  implementation project(':core:android')
         """.trimMargin()
       )
     )
   }
 
   @Test
-  fun `declaration's original string should include preceding block comment`() {
-    val block = GroovyDependencyBlockParser()
-      .parse(
-        """
-       dependencies {
-          api 'com.foo:bar:1.2.3.4' // inline comment
-
-          /*
-          block comment
-          */
-          implementation project(':core:android')
-       }
-        """.trimIndent()
-      ).single()
-
-    block.getOrEmpty(":core:android", ConfigurationName.implementation) shouldBe listOf(
+  fun `declaration's original string should include preceding block comment`() = parse(
+    """
+    dependencies {
+      api 'com.foo:bar:1.2.3.4' // inline comment
+
+      /*
+      block comment
+      */
+      implementation project(':core:android')
+    }
+    """
+  ) {
+
+    getOrEmpty(":core:android", ConfigurationName.implementation) shouldBe listOf(
       ModuleDependencyDeclaration(
         moduleRef = StringRef(":core:android"),
         moduleAccess = "project(':core:android')",
         configName = ConfigurationName.implementation,
         declarationText = """implementation project(':core:android')""",
         statementWithSurroundingText = """
-          |
-          |   /*
-          |   block comment
-          |   */
-          |   implementation project(':core:android')
+
+        |  /*
+        |  block comment
+        |  */
+        |  implementation project(':core:android')
         """.trimMargin()
       )
     )
   }
 
   @Test
-  fun `declaration's original string should include preceding in-line block comment`() {
-    val block = GroovyDependencyBlockParser()
-      .parse(
-        """
-       dependencies {
-          api 'com.foo:bar:1.2.3.4' // inline comment
-          /* single-line block comment */ implementation project(':core:android')
-       }
-        """.trimIndent()
-      ).single()
-
-    block.getOrEmpty(":core:android", ConfigurationName.implementation) shouldBe listOf(
+  fun `declaration's original string should include preceding in-line block comment`() = parse(
+    """
+    dependencies {
+       api 'com.foo:bar:1.2.3.4' // inline comment
+       /* single-line block comment */ implementation project(':core:android')
+    }
+    """
+  ) {
+
+    getOrEmpty(":core:android", ConfigurationName.implementation) shouldBe listOf(
       ModuleDependencyDeclaration(
         moduleRef = StringRef(":core:android"),
         moduleAccess = "project(':core:android')",
@@ -434,97 +413,91 @@ internal class GroovyDependencyBlockParserTest {
   }
 
   @Test
-  fun `duplicate module dependency with same config should be recorded twice`() {
-    val block = GroovyDependencyBlockParser()
-      .parse(
-        """
-       dependencies {
-          api project(':core:jvm')
-          api (   project(':core:jvm'))
-       }
-        """.trimIndent()
-      ).single()
-
-    block.getOrEmpty(":core:jvm", ConfigurationName.api) shouldBe listOf(
+  fun `duplicate module dependency with same config should be recorded twice`() = parse(
+    """
+    dependencies {
+      api project(':core:jvm')
+      api project(':core:jvm')
+    }
+    """
+  ) {
+
+    getOrEmpty(":core:jvm", ConfigurationName.api) shouldBe listOf(
       ModuleDependencyDeclaration(
         moduleRef = StringRef(":core:jvm"),
         moduleAccess = "project(':core:jvm')",
         configName = ConfigurationName.api,
         declarationText = """api project(':core:jvm')""",
-        statementWithSurroundingText = """   api project(':core:jvm')"""
+        statementWithSurroundingText = """  api project(':core:jvm')"""
       ),
       ModuleDependencyDeclaration(
         moduleRef = StringRef(":core:jvm"),
         moduleAccess = "project(':core:jvm')",
         configName = ConfigurationName.api,
-        declarationText = """api (   project(':core:jvm'))""",
-        statementWithSurroundingText = """   api (   project(':core:jvm'))"""
+        declarationText = """api project(':core:jvm')""",
+        statementWithSurroundingText = """  api project(':core:jvm')"""
       )
     )
   }
 
   @Test
-  fun `modules declared using type-safe accessors can be looked up using their path`() {
-    val block = GroovyDependencyBlockParser()
-      .parse(
-        """
-       dependencies {
-          api projects.core.test
-          implementation projects.httpLogging
-       }
-        """.trimIndent()
-      ).single()
-
-    block.getOrEmpty(":core:test", ConfigurationName.api) shouldBe listOf(
+  fun `modules declared using type-safe accessors can be looked up using their path`() = parse(
+    """
+    dependencies {
+      api projects.core.test
+      implementation projects.httpLogging
+    }
+    """
+  ) {
+
+    getOrEmpty(":core:test", ConfigurationName.api) shouldBe listOf(
       ModuleDependencyDeclaration(
         moduleRef = TypeSafeRef("core.test"),
         moduleAccess = "projects.core.test",
         configName = ConfigurationName.api,
         declarationText = """api projects.core.test""",
-        statementWithSurroundingText = """   api projects.core.test"""
+        statementWithSurroundingText = """  api projects.core.test"""
       )
     )
 
-    block.getOrEmpty(":http-logging", ConfigurationName.implementation) shouldBe listOf(
+    getOrEmpty(":http-logging", ConfigurationName.implementation) shouldBe listOf(
       ModuleDependencyDeclaration(
         moduleRef = TypeSafeRef("httpLogging"),
         moduleAccess = "projects.httpLogging",
         configName = ConfigurationName.implementation,
         declarationText = """implementation projects.httpLogging""",
-        statementWithSurroundingText = """   implementation projects.httpLogging"""
+        statementWithSurroundingText = """  implementation projects.httpLogging"""
       )
     )
   }
 
   @Test
-  fun `buildscript dependencies should not be parsed`() {
-    val block = GroovyDependencyBlockParser()
-      .parse(
-        """
-        |buildscript {
-        |  repositories {
-        |    mavenCentral()
-        |    google()
-        |    jcenter()
-        |    maven("https://plugins.gradle.org/m2/")
-        |    maven("https://oss.sonatype.org/content/repositories/snapshots")
-        |  }
-        |  dependencies {
-        |    classpath 'com.android.tools.build:gradle:7.0.2'
-        |    classpath 'com.squareup.anvil:gradle-plugin:2.3.4'
-        |    classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.30'
-        |  }
-        |}
-        |dependencies {
-        |  api libs.ktlint
-        |}
-        |
-        """.trimMargin()
-      ).single()
-
-    block.lambdaContent shouldBe "  api libs.ktlint\n"
-
-    block.settings shouldBe listOf(
+  fun `buildscript dependencies should not be parsed`() = parse(
+    """
+    buildscript {
+      repositories {
+        mavenCentral()
+        google()
+        jcenter()
+        maven("https://plugins.gradle.org/m2/")
+        maven("https://oss.sonatype.org/content/repositories/snapshots")
+      }
+      dependencies {
+        classpath 'com.android.tools.build:gradle:7.0.2'
+        classpath 'com.squareup.anvil:gradle-plugin:2.3.4'
+        classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.30'
+      }
+    }
+    dependencies {
+      api libs.ktlint
+    }
+
+    """
+  ) {
+
+    lambdaContent shouldBe "  api libs.ktlint\n"
+
+    settings shouldBe listOf(
       UnknownDependencyDeclaration(
         argument = "libs.ktlint",
         configName = ConfigurationName.api,
@@ -533,4 +506,16 @@ internal class GroovyDependencyBlockParserTest {
       )
     )
   }
+
+  inline fun parse(
+    @Language("groovy")
+    fileText: String,
+    assertions: GroovyDependenciesBlock.() -> Unit
+  ) {
+    testProjectDir.child("build.gradle")
+      .createSafely(fileText.trimIndent())
+      .let { file -> GroovyDependencyBlockParser().parse(file) }
+      .single()
+      .assertions()
+  }
 }
diff --git a/modulecheck-parsing/groovy-antlr/src/test/kotlin/modulecheck/parsing/groovy/antlr/GroovyPluginsBlockParserTest.kt b/modulecheck-parsing/groovy-antlr/src/test/kotlin/modulecheck/parsing/groovy/antlr/GroovyPluginsBlockParserTest.kt
index 5999339fcf..ad3da67373 100644
--- a/modulecheck-parsing/groovy-antlr/src/test/kotlin/modulecheck/parsing/groovy/antlr/GroovyPluginsBlockParserTest.kt
+++ b/modulecheck-parsing/groovy-antlr/src/test/kotlin/modulecheck/parsing/groovy/antlr/GroovyPluginsBlockParserTest.kt
@@ -15,26 +15,28 @@
 
 package modulecheck.parsing.groovy.antlr
 
-import io.kotest.matchers.shouldBe
 import modulecheck.parsing.gradle.PluginDeclaration
+import modulecheck.testing.BaseTest
+import modulecheck.testing.createSafely
+import modulecheck.testing.requireNotNullOrFail
+import modulecheck.utils.child
+import org.intellij.lang.annotations.Language
 import org.junit.jupiter.api.Test
 
-internal class GroovyPluginsBlockParserTest {
+internal class GroovyPluginsBlockParserTest : BaseTest() {
 
   @Test
-  fun `external declaration`() {
-    val block = GroovyPluginsBlockParser()
-      .parse(
-        """
-       plugins {
-         id 'org.jetbrains.kotlin.jvm' // trailing comment
-         // comment
-         id 'com.squareup.anvil' version '2.34.0'
-       }
-        """.trimIndent()
-      )!!
+  fun `external declaration`() = parse(
+    """
+    plugins {
+      id 'org.jetbrains.kotlin.jvm' // trailing comment
+      // comment
+      id 'com.squareup.anvil' version '2.34.0'
+    }
+    """.trimIndent()
+  ) {
 
-    block.settings shouldBe listOf(
+    settings shouldBe listOf(
       PluginDeclaration(
         declarationText = """id 'org.jetbrains.kotlin.jvm'""",
         statementWithSurroundingText = """  id 'org.jetbrains.kotlin.jvm' // trailing comment"""
@@ -49,19 +51,17 @@ internal class GroovyPluginsBlockParserTest {
   }
 
   @Test
-  fun `single declarations should only be counted once`() {
-    val block = GroovyPluginsBlockParser()
-      .parse(
-        """
-        plugins {
-          id 'io.gitlab.arturbosch.detekt' version '1.15.0'
-          javaLibrary
-          id 'org.jetbrains.kotlin.jvm'
-        }
-        """.trimIndent()
-      )!!
+  fun `single declarations should only be counted once`() = parse(
+    """
+    plugins {
+      id 'io.gitlab.arturbosch.detekt' version '1.15.0'
+      javaLibrary
+      id 'org.jetbrains.kotlin.jvm'
+    }
+    """.trimIndent()
+  ) {
 
-    block.settings shouldBe listOf(
+    settings shouldBe listOf(
       PluginDeclaration(
         declarationText = """id 'io.gitlab.arturbosch.detekt' version '1.15.0'""",
         statementWithSurroundingText = """  id 'io.gitlab.arturbosch.detekt' version '1.15.0'"""
@@ -76,4 +76,16 @@ internal class GroovyPluginsBlockParserTest {
       ),
     )
   }
+
+  inline fun parse(
+    @Language("groovy")
+    fileText: String,
+    assertions: GroovyPluginsBlock.() -> Unit
+  ) {
+    testProjectDir.child("build.gradle")
+      .createSafely(fileText.trimIndent())
+      .let { file -> GroovyPluginsBlockParser().parse(file) }
+      .requireNotNullOrFail()
+      .assertions()
+  }
 }