From 1ff235d84292b6ea9889f70538aa61b55b6a0b9a Mon Sep 17 00:00:00 2001 From: Rick Busarow Date: Mon, 25 Oct 2021 21:17:57 -0500 Subject: [PATCH] support suppressing findings fixes #230 --- .../main/kotlin/modulecheck/api/Finding.kt | 25 +++- .../modulecheck/core/CouldUseAnvilFinding.kt | 14 +- .../modulecheck/core/DependencyFinding.kt | 20 ++- .../core/InheritedDependencyFinding.kt | 16 ++- .../modulecheck/core/MustBeApiFinding.kt | 25 +++- .../core/OverShotDependencyFinding.kt | 23 ++- .../core/context/RedundantDependencies.kt | 4 +- .../core/context/UnusedDependencies.kt | 2 + .../modulecheck/core/internal/project2.kt | 6 +- .../core/kapt/UnusedKaptPluginFinding.kt | 14 +- .../core/kapt/UnusedKaptProcessorFinding.kt | 14 +- .../DisableViewBindingGenerationFinding.kt | 14 +- .../UnusedResourcesGenerationFinding.kt | 14 +- .../core/rule/sort/SortDependenciesFinding.kt | 14 +- .../core/rule/sort/SortPluginsFinding.kt | 14 +- .../modulecheck/parsing/DependenciesBlock.kt | 20 ++- .../parsing/ModuleDependencyDeclaration.kt | 8 +- .../groovy/antlr/GroovyDependenciesBlock.kt | 5 +- .../antlr/GroovyDependencyBlockParser.kt | 51 +++++-- .../antlr/GroovyDependencyBlockParserTest.kt | 78 +++++++++- .../parsing/psi/KotlinDependenciesBlock.kt | 5 +- .../psi/KotlinDependencyBlockParser.kt | 135 +++++++++++------- .../psi/KotlinDependencyBlockParserTest.kt | 79 ++++++++++ .../gradle/task/ModuleCheckTask.kt | 104 ++++++++++---- .../modulecheck/gradle/AnvilScopesTest.kt | 2 +- .../gradle/DisableAndroidResourcesRuleTest.kt | 10 +- .../DisableAndroidViewBindingRuleTest.kt | 8 +- .../gradle/InheritedDependencyTest.kt | 6 +- .../modulecheck/gradle/MustBeApiTest.kt | 105 ++++++++++---- .../gradle/UnusedDependenciesTest.kt | 121 +++++++++++++++- .../modulecheck/gradle/UnusedKaptTest.kt | 5 +- website/docs/suppressing-findings.mdx | 66 +++++++++ website/sidebars.js | 1 + 33 files changed, 846 insertions(+), 182 deletions(-) create mode 100644 website/docs/suppressing-findings.mdx diff --git a/modulecheck-api/src/main/kotlin/modulecheck/api/Finding.kt b/modulecheck-api/src/main/kotlin/modulecheck/api/Finding.kt index 6b136045b2..2b897c45ef 100644 --- a/modulecheck-api/src/main/kotlin/modulecheck/api/Finding.kt +++ b/modulecheck-api/src/main/kotlin/modulecheck/api/Finding.kt @@ -16,6 +16,7 @@ package modulecheck.api import modulecheck.api.Finding.Position +import modulecheck.parsing.ModuleDependencyDeclaration import java.io.File interface Finding { @@ -24,9 +25,16 @@ interface Finding { val dependentPath: String val buildFile: File + val statementOrNull: ModuleDependencyDeclaration? get() = null val statementTextOrNull: String? get() = null val positionOrNull: Position? + fun logElement(): LogElement + + fun shouldSkip(): Boolean = statementOrNull?.suppressed + ?.contains(problemName) + ?: false + fun logString(): String { return "${buildFile.path}: ${positionString()} $problemName" } @@ -36,8 +44,23 @@ interface Finding { data class Position( val row: Int, val column: Int - ) { + ) : Comparable { fun logString(): String = "($row, $column): " + override fun compareTo(other: Position): Int { + return row.compareTo(other.row) + } + } + + data class LogElement( + val dependentPath: String, + val problemName: String, + val sourceOrNull: String?, + val dependencyPath: String, + val positionOrNull: Position?, + val buildFile: File, + var fixed: Boolean = false + ) { + val filePathStr = "${buildFile.path}: ${positionOrNull?.logString().orEmpty()}" } } diff --git a/modulecheck-core/src/main/kotlin/modulecheck/core/CouldUseAnvilFinding.kt b/modulecheck-core/src/main/kotlin/modulecheck/core/CouldUseAnvilFinding.kt index b60460077a..9e4b902fe6 100644 --- a/modulecheck-core/src/main/kotlin/modulecheck/core/CouldUseAnvilFinding.kt +++ b/modulecheck-core/src/main/kotlin/modulecheck/core/CouldUseAnvilFinding.kt @@ -16,6 +16,7 @@ package modulecheck.core import modulecheck.api.Finding +import modulecheck.api.Finding.LogElement import modulecheck.api.Finding.Position import modulecheck.api.Fixable import modulecheck.core.internal.positionOf @@ -29,7 +30,7 @@ data class CouldUseAnvilFinding( ) : Finding, Fixable { override val dependencyIdentifier = "com.google.dagger:dagger-compiler" - override val problemName = "could use Anvil factory generator" + override val problemName = "useAnvilFactories" override val positionOrNull: Position? by lazy { @@ -42,5 +43,16 @@ data class CouldUseAnvilFinding( ?.positionOf(statement, "kapt".asConfigurationName()) } + override fun logElement(): LogElement { + return LogElement( + dependentPath = dependentPath, + problemName = problemName, + sourceOrNull = null, + dependencyPath = dependencyIdentifier, + positionOrNull = positionOrNull, + buildFile = buildFile + ) + } + override val statementTextOrNull: String? get() = null } diff --git a/modulecheck-core/src/main/kotlin/modulecheck/core/DependencyFinding.kt b/modulecheck-core/src/main/kotlin/modulecheck/core/DependencyFinding.kt index 23fb8519e8..4a16bdf5d3 100644 --- a/modulecheck-core/src/main/kotlin/modulecheck/core/DependencyFinding.kt +++ b/modulecheck-core/src/main/kotlin/modulecheck/core/DependencyFinding.kt @@ -16,10 +16,12 @@ package modulecheck.core import modulecheck.api.Finding +import modulecheck.api.Finding.LogElement import modulecheck.api.Fixable import modulecheck.api.positionOfStatement import modulecheck.core.internal.statementOrNullIn import modulecheck.parsing.ConfigurationName +import modulecheck.parsing.ModuleDependencyDeclaration import modulecheck.parsing.Project2 abstract class DependencyFinding( @@ -37,10 +39,26 @@ abstract class DependencyFinding( .positionOfStatement(statement) } - override val statementTextOrNull: String? by lazy { + override val statementOrNull: ModuleDependencyDeclaration? by lazy { dependencyProject .statementOrNullIn(buildFile, configurationName) } + override val statementTextOrNull: String? by lazy { + statementOrNull?.statementWithSurroundingText + } + + override fun logElement(): LogElement { + return LogElement( + dependentPath = dependentPath, + problemName = problemName, + sourceOrNull = fromStringOrEmpty(), + dependencyPath = dependencyProject.path, + positionOrNull = positionOrNull, + buildFile = buildFile + ) + } + + abstract fun fromStringOrEmpty(): String override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/modulecheck-core/src/main/kotlin/modulecheck/core/InheritedDependencyFinding.kt b/modulecheck-core/src/main/kotlin/modulecheck/core/InheritedDependencyFinding.kt index c298e3e78d..d7ce19e33a 100644 --- a/modulecheck-core/src/main/kotlin/modulecheck/core/InheritedDependencyFinding.kt +++ b/modulecheck-core/src/main/kotlin/modulecheck/core/InheritedDependencyFinding.kt @@ -15,6 +15,7 @@ package modulecheck.core +import modulecheck.api.Finding.LogElement import modulecheck.api.Finding.Position import modulecheck.core.internal.positionIn import modulecheck.parsing.ConfigurationName @@ -38,11 +39,22 @@ data class InheritedDependencyFinding( source?.project?.positionIn(buildFile, configurationName) } - private fun fromStringOrEmpty(): String { + override fun logElement(): LogElement { + return LogElement( + dependentPath = dependentPath, + problemName = problemName, + sourceOrNull = fromStringOrEmpty(), + dependencyPath = dependencyProject.path, + positionOrNull = positionOrNull, + buildFile = buildFile + ) + } + + override fun fromStringOrEmpty(): String { return if (dependencyProject.path == source?.project?.path) { "" } else { - " from: ${source?.project?.path}" + "${source?.project?.path}" } } diff --git a/modulecheck-core/src/main/kotlin/modulecheck/core/MustBeApiFinding.kt b/modulecheck-core/src/main/kotlin/modulecheck/core/MustBeApiFinding.kt index 13bf618205..2c89574756 100644 --- a/modulecheck-core/src/main/kotlin/modulecheck/core/MustBeApiFinding.kt +++ b/modulecheck-core/src/main/kotlin/modulecheck/core/MustBeApiFinding.kt @@ -15,9 +15,11 @@ package modulecheck.core +import modulecheck.api.Finding.LogElement import modulecheck.core.internal.statementOrNullIn import modulecheck.parsing.ConfigurationName import modulecheck.parsing.ConfiguredProjectDependency +import modulecheck.parsing.ModuleDependencyDeclaration import modulecheck.parsing.Project2 import java.io.File @@ -31,20 +33,35 @@ data class MustBeApiFinding( override val dependencyIdentifier = dependencyProject.path + fromStringOrEmpty() - override val statementTextOrNull: String? by lazy { - super.statementTextOrNull + override val statementOrNull: ModuleDependencyDeclaration? by lazy { + super.statementOrNull ?: source?.project ?.statementOrNullIn(buildFile, configurationName) } + override val statementTextOrNull: String? by lazy { + super.statementTextOrNull + ?: statementOrNull?.statementWithSurroundingText + } - private fun fromStringOrEmpty(): String { + override fun fromStringOrEmpty(): String { return if (dependencyProject.path == source?.project?.path) { "" } else { - " from: ${source?.project?.path}" + "${source?.project?.path}" } } + override fun logElement(): LogElement { + return LogElement( + dependentPath = dependentPath, + problemName = problemName, + sourceOrNull = fromStringOrEmpty(), + dependencyPath = dependencyProject.path, + positionOrNull = positionOrNull, + buildFile = buildFile + ) + } + override fun fix(): Boolean = synchronized(buildFile) { val statement = statementTextOrNull ?: return false diff --git a/modulecheck-core/src/main/kotlin/modulecheck/core/OverShotDependencyFinding.kt b/modulecheck-core/src/main/kotlin/modulecheck/core/OverShotDependencyFinding.kt index dcdf966e97..4b73a9c808 100644 --- a/modulecheck-core/src/main/kotlin/modulecheck/core/OverShotDependencyFinding.kt +++ b/modulecheck-core/src/main/kotlin/modulecheck/core/OverShotDependencyFinding.kt @@ -27,7 +27,7 @@ data class OverShotDependencyFinding( override val dependencyProject: Project2, override val dependencyIdentifier: String, override val configurationName: ConfigurationName -) : DependencyFinding("demoted") { +) : DependencyFinding("overshot") { override fun fix(): Boolean { @@ -74,17 +74,14 @@ data class OverShotDependencyFinding( return false } - private fun matchingDeclaration(block: DependenciesBlock) = - ( - block.allDeclarations - .filterIsInstance() - .maxByOrNull { declaration -> declaration.configName == configurationName } - ?: block.allDeclarations - .filterNot { it is ModuleDependencyDeclaration } - .maxByOrNull { declaration -> declaration.configName == configurationName } - ?: block.allDeclarations - .lastOrNull() - ) + private fun matchingDeclaration(block: DependenciesBlock) = block.allDeclarations + .filterIsInstance() + .maxByOrNull { declaration -> declaration.configName == configurationName } + ?: block.allDeclarations + .filterNot { it is ModuleDependencyDeclaration } + .maxByOrNull { declaration -> declaration.configName == configurationName } + ?: block.allDeclarations + .lastOrNull() private fun newModuleDeclaration(match: DependencyDeclaration) = when (match) { is ExternalDependencyDeclaration -> dependencyProject.path @@ -114,6 +111,8 @@ data class OverShotDependencyFinding( } } + override fun fromStringOrEmpty(): String = "" + override fun toString(): String { return "OverShotDependency(\n" + "\tdependentPath='$dependentPath', \n" + diff --git a/modulecheck-core/src/main/kotlin/modulecheck/core/context/RedundantDependencies.kt b/modulecheck-core/src/main/kotlin/modulecheck/core/context/RedundantDependencies.kt index 8ba916f066..e9ab0eed5c 100644 --- a/modulecheck-core/src/main/kotlin/modulecheck/core/context/RedundantDependencies.kt +++ b/modulecheck-core/src/main/kotlin/modulecheck/core/context/RedundantDependencies.kt @@ -38,12 +38,12 @@ data class RedundantDependencyFinding( override val dependencyIdentifier = dependencyPath + fromStringOrEmpty() - private fun fromStringOrEmpty(): String { + override fun fromStringOrEmpty(): String { return if (from.all { dependencyProject.path == it.path }) { "" } else { - " from: ${from.joinToString { it.path }}" + from.joinToString { it.path } } } } diff --git a/modulecheck-core/src/main/kotlin/modulecheck/core/context/UnusedDependencies.kt b/modulecheck-core/src/main/kotlin/modulecheck/core/context/UnusedDependencies.kt index 2348c549e9..b656938b34 100644 --- a/modulecheck-core/src/main/kotlin/modulecheck/core/context/UnusedDependencies.kt +++ b/modulecheck-core/src/main/kotlin/modulecheck/core/context/UnusedDependencies.kt @@ -54,6 +54,8 @@ data class UnusedDependency( "\tconfigurationName=$configurationName\n" + ")" } + + override fun fromStringOrEmpty(): String = "" } data class UnusedDependencies( diff --git a/modulecheck-core/src/main/kotlin/modulecheck/core/internal/project2.kt b/modulecheck-core/src/main/kotlin/modulecheck/core/internal/project2.kt index 352f5338bf..3b515e378e 100644 --- a/modulecheck-core/src/main/kotlin/modulecheck/core/internal/project2.kt +++ b/modulecheck-core/src/main/kotlin/modulecheck/core/internal/project2.kt @@ -20,13 +20,14 @@ import modulecheck.api.positionOfStatement import modulecheck.core.parse import modulecheck.parsing.ConfigurationName import modulecheck.parsing.DependencyBlockParser +import modulecheck.parsing.ModuleDependencyDeclaration import modulecheck.parsing.Project2 import java.io.File fun Project2.statementOrNullIn( dependentBuildFile: File, configuration: ConfigurationName -): String? { +): ModuleDependencyDeclaration? { return DependencyBlockParser .parse(dependentBuildFile) .firstNotNullOfOrNull { block -> @@ -34,7 +35,6 @@ fun Project2.statementOrNullIn( .takeIf { it.isNotEmpty() } } ?.firstOrNull() - ?.statementWithSurroundingText } fun Project2.positionIn( @@ -45,5 +45,5 @@ fun Project2.positionIn( val statement = statementOrNullIn(dependentBuildFile, configuration) ?: return null return dependentBuildFile.readText() - .positionOfStatement(statement) + .positionOfStatement(statement.statementWithSurroundingText) } diff --git a/modulecheck-core/src/main/kotlin/modulecheck/core/kapt/UnusedKaptPluginFinding.kt b/modulecheck-core/src/main/kotlin/modulecheck/core/kapt/UnusedKaptPluginFinding.kt index d98217419a..3ade71a931 100644 --- a/modulecheck-core/src/main/kotlin/modulecheck/core/kapt/UnusedKaptPluginFinding.kt +++ b/modulecheck-core/src/main/kotlin/modulecheck/core/kapt/UnusedKaptPluginFinding.kt @@ -16,6 +16,7 @@ package modulecheck.core.kapt import modulecheck.api.Finding +import modulecheck.api.Finding.LogElement import modulecheck.api.Finding.Position import modulecheck.api.Fixable import modulecheck.core.rule.KAPT_PLUGIN_FUN @@ -31,7 +32,18 @@ data class UnusedKaptPluginFinding( override val dependencyIdentifier = KAPT_PLUGIN_ID - override val problemName = "unused kapt plugin" + override val problemName = "unusedKaptPlugin" + + override fun logElement(): LogElement { + return LogElement( + dependentPath = dependentPath, + problemName = problemName, + sourceOrNull = null, + dependencyPath = dependencyIdentifier, + positionOrNull = positionOrNull, + buildFile = buildFile + ) + } override val positionOrNull: Position? by lazy { val text = buildFile diff --git a/modulecheck-core/src/main/kotlin/modulecheck/core/kapt/UnusedKaptProcessorFinding.kt b/modulecheck-core/src/main/kotlin/modulecheck/core/kapt/UnusedKaptProcessorFinding.kt index 640343db5e..d7c5733a53 100644 --- a/modulecheck-core/src/main/kotlin/modulecheck/core/kapt/UnusedKaptProcessorFinding.kt +++ b/modulecheck-core/src/main/kotlin/modulecheck/core/kapt/UnusedKaptProcessorFinding.kt @@ -15,6 +15,7 @@ package modulecheck.core.kapt +import modulecheck.api.Finding.LogElement import modulecheck.api.Finding.Position import modulecheck.core.internal.positionOf import modulecheck.parsing.ConfigurationName @@ -29,7 +30,18 @@ data class UnusedKaptProcessorFinding( override val dependencyIdentifier = dependencyPath - override val problemName = "unused ${configurationName.value} dependency" + override val problemName = "unusedKaptProcessor" + + override fun logElement(): LogElement { + return LogElement( + dependentPath = dependentPath, + problemName = problemName, + sourceOrNull = null, + dependencyPath = dependencyPath, + positionOrNull = positionOrNull, + buildFile = buildFile + ) + } override val positionOrNull: Position? by lazy { // Kapt paths are different from other project dependencies. diff --git a/modulecheck-core/src/main/kotlin/modulecheck/core/rule/android/DisableViewBindingGenerationFinding.kt b/modulecheck-core/src/main/kotlin/modulecheck/core/rule/android/DisableViewBindingGenerationFinding.kt index 8ad04c9504..f316915978 100644 --- a/modulecheck-core/src/main/kotlin/modulecheck/core/rule/android/DisableViewBindingGenerationFinding.kt +++ b/modulecheck-core/src/main/kotlin/modulecheck/core/rule/android/DisableViewBindingGenerationFinding.kt @@ -16,6 +16,7 @@ package modulecheck.core.rule.android import modulecheck.api.Finding +import modulecheck.api.Finding.LogElement import modulecheck.api.Finding.Position import modulecheck.api.Fixable import modulecheck.api.positionOfStatement @@ -28,7 +29,7 @@ data class DisableViewBindingGenerationFinding( override val buildFile: File ) : Finding, Fixable { - override val problemName = "unused ViewBinding generation" + override val problemName = "disableViewBinding" override val dependencyIdentifier = "" @@ -47,6 +48,17 @@ data class DisableViewBindingGenerationFinding( fileText.positionOfStatement(statement) } + override fun logElement(): LogElement { + return LogElement( + dependentPath = dependentPath, + problemName = problemName, + sourceOrNull = null, + dependencyPath = "", + positionOrNull = positionOrNull, + buildFile = buildFile + ) + } + override fun fix(): Boolean = synchronized(buildFile) { val ktFile = kotlinBuildFileOrNull() ?: return false diff --git a/modulecheck-core/src/main/kotlin/modulecheck/core/rule/android/UnusedResourcesGenerationFinding.kt b/modulecheck-core/src/main/kotlin/modulecheck/core/rule/android/UnusedResourcesGenerationFinding.kt index 4768162e3c..d16cd65152 100644 --- a/modulecheck-core/src/main/kotlin/modulecheck/core/rule/android/UnusedResourcesGenerationFinding.kt +++ b/modulecheck-core/src/main/kotlin/modulecheck/core/rule/android/UnusedResourcesGenerationFinding.kt @@ -16,6 +16,7 @@ package modulecheck.core.rule.android import modulecheck.api.Finding +import modulecheck.api.Finding.LogElement import modulecheck.api.Finding.Position import modulecheck.api.Fixable import modulecheck.api.positionOfStatement @@ -28,7 +29,7 @@ data class UnusedResourcesGenerationFinding( override val buildFile: File ) : Finding, Fixable { - override val problemName = "unused R file generation" + override val problemName = "disableAndroidResources" override val dependencyIdentifier = "" @@ -47,6 +48,17 @@ data class UnusedResourcesGenerationFinding( fileText.positionOfStatement(statement) } + override fun logElement(): LogElement { + return LogElement( + dependentPath = dependentPath, + problemName = problemName, + sourceOrNull = null, + dependencyPath = "", + positionOrNull = positionOrNull, + buildFile = buildFile + ) + } + override fun fix(): Boolean = synchronized(buildFile) { val ktFile = kotlinBuildFileOrNull() ?: return false diff --git a/modulecheck-core/src/main/kotlin/modulecheck/core/rule/sort/SortDependenciesFinding.kt b/modulecheck-core/src/main/kotlin/modulecheck/core/rule/sort/SortDependenciesFinding.kt index d11f1ee482..52660f2fcf 100644 --- a/modulecheck-core/src/main/kotlin/modulecheck/core/rule/sort/SortDependenciesFinding.kt +++ b/modulecheck-core/src/main/kotlin/modulecheck/core/rule/sort/SortDependenciesFinding.kt @@ -16,6 +16,7 @@ package modulecheck.core.rule.sort import modulecheck.api.Finding +import modulecheck.api.Finding.LogElement import modulecheck.api.Finding.Position import modulecheck.api.Fixable import modulecheck.core.parse @@ -28,7 +29,7 @@ class SortDependenciesFinding( override val buildFile: File, private val comparator: Comparator ) : Finding, Fixable { - override val problemName = "unsorted dependencies" + override val problemName = "unsortedDependencies" override val dependencyIdentifier = "" @@ -48,6 +49,17 @@ class SortDependenciesFinding( return true } + + override fun logElement(): LogElement { + return LogElement( + dependentPath = dependentPath, + problemName = problemName, + sourceOrNull = null, + dependencyPath = "", + positionOrNull = positionOrNull, + buildFile = buildFile + ) + } } internal fun sortedDependenciesFileText( diff --git a/modulecheck-core/src/main/kotlin/modulecheck/core/rule/sort/SortPluginsFinding.kt b/modulecheck-core/src/main/kotlin/modulecheck/core/rule/sort/SortPluginsFinding.kt index dc49f65492..b0dbbf8a84 100644 --- a/modulecheck-core/src/main/kotlin/modulecheck/core/rule/sort/SortPluginsFinding.kt +++ b/modulecheck-core/src/main/kotlin/modulecheck/core/rule/sort/SortPluginsFinding.kt @@ -16,6 +16,7 @@ package modulecheck.core.rule.sort import modulecheck.api.Finding +import modulecheck.api.Finding.LogElement import modulecheck.api.Finding.Position import modulecheck.api.Fixable import modulecheck.core.parse @@ -29,7 +30,7 @@ class SortPluginsFinding( override val buildFile: File, val comparator: Comparator ) : Finding, Fixable { - override val problemName = "unsorted plugins" + override val problemName = "unsortedPlugins" override val dependencyIdentifier = "" @@ -48,6 +49,17 @@ class SortPluginsFinding( return true } + + override fun logElement(): LogElement { + return LogElement( + dependentPath = dependentPath, + problemName = problemName, + sourceOrNull = null, + dependencyPath = "", + positionOrNull = positionOrNull, + buildFile = buildFile + ) + } } internal fun PluginsBlock.sortedPlugins( diff --git a/modulecheck-parsing/api/src/main/kotlin/modulecheck/parsing/DependenciesBlock.kt b/modulecheck-parsing/api/src/main/kotlin/modulecheck/parsing/DependenciesBlock.kt index e72777e2c4..2c2fa9feca 100644 --- a/modulecheck-parsing/api/src/main/kotlin/modulecheck/parsing/DependenciesBlock.kt +++ b/modulecheck-parsing/api/src/main/kotlin/modulecheck/parsing/DependenciesBlock.kt @@ -15,7 +15,7 @@ package modulecheck.parsing -abstract class DependenciesBlock(var contentString: String) { +abstract class DependenciesBlock(var contentString: String, val suppressAll: List) { protected val originalLines = contentString.lines().toMutableList() @@ -34,7 +34,8 @@ abstract class DependenciesBlock(var contentString: String) { fun addNonModuleStatement( configName: ConfigurationName, parsedString: String, - coordinates: MavenCoordinates + coordinates: MavenCoordinates, + suppressed: List ) { val originalString = getOriginalString(parsedString) @@ -44,7 +45,8 @@ abstract class DependenciesBlock(var contentString: String) { statementWithSurroundingText = originalString, group = coordinates.group, moduleName = coordinates.moduleName, - version = coordinates.version + version = coordinates.version, + suppressed = suppressed + suppressAll ) _allDeclarations.add(declaration) allExternalDeclarations.getOrPut(coordinates) { mutableListOf() } @@ -54,7 +56,8 @@ abstract class DependenciesBlock(var contentString: String) { fun addUnknownStatement( configName: ConfigurationName, parsedString: String, - argument: String + argument: String, + suppressed: List ) { val originalString = getOriginalString(parsedString) @@ -62,7 +65,8 @@ abstract class DependenciesBlock(var contentString: String) { argument = argument, configName = configName, declarationText = parsedString, - statementWithSurroundingText = originalString + statementWithSurroundingText = originalString, + suppressed = suppressed + suppressAll ) _allDeclarations.add(declaration) } @@ -70,7 +74,8 @@ abstract class DependenciesBlock(var contentString: String) { fun addModuleStatement( configName: ConfigurationName, parsedString: String, - moduleRef: ModuleRef + moduleRef: ModuleRef, + suppressed: List ) { val cm = ConfiguredModule(configName = configName, moduleRef = moduleRef) @@ -80,7 +85,8 @@ abstract class DependenciesBlock(var contentString: String) { moduleRef = moduleRef, configName = configName, declarationText = parsedString, - statementWithSurroundingText = originalString + statementWithSurroundingText = originalString, + suppressed = suppressed + suppressAll ) allModuleDeclarations.getOrPut(cm) { mutableListOf() } diff --git a/modulecheck-parsing/api/src/main/kotlin/modulecheck/parsing/ModuleDependencyDeclaration.kt b/modulecheck-parsing/api/src/main/kotlin/modulecheck/parsing/ModuleDependencyDeclaration.kt index 0de0cc3028..5e14894846 100644 --- a/modulecheck-parsing/api/src/main/kotlin/modulecheck/parsing/ModuleDependencyDeclaration.kt +++ b/modulecheck-parsing/api/src/main/kotlin/modulecheck/parsing/ModuleDependencyDeclaration.kt @@ -17,26 +17,30 @@ package modulecheck.parsing sealed interface DependencyDeclaration : Declaration { val configName: ConfigurationName + val suppressed: List } data class UnknownDependencyDeclaration( val argument: String, override val configName: ConfigurationName, override val declarationText: String, - override val statementWithSurroundingText: String + override val statementWithSurroundingText: String, + override val suppressed: List = emptyList() ) : DependencyDeclaration data class ModuleDependencyDeclaration( val moduleRef: ModuleRef, override val configName: ConfigurationName, override val declarationText: String, - override val statementWithSurroundingText: String + override val statementWithSurroundingText: String, + override val suppressed: List = emptyList() ) : DependencyDeclaration data class ExternalDependencyDeclaration( override val configName: ConfigurationName, override val declarationText: String, override val statementWithSurroundingText: String, + override val suppressed: List = emptyList(), val group: String?, val moduleName: String?, val version: String? diff --git a/modulecheck-parsing/groovy-antlr/src/main/kotlin/modulecheck/parsing/groovy/antlr/GroovyDependenciesBlock.kt b/modulecheck-parsing/groovy-antlr/src/main/kotlin/modulecheck/parsing/groovy/antlr/GroovyDependenciesBlock.kt index 017cb6495b..24e6e86968 100644 --- a/modulecheck-parsing/groovy-antlr/src/main/kotlin/modulecheck/parsing/groovy/antlr/GroovyDependenciesBlock.kt +++ b/modulecheck-parsing/groovy-antlr/src/main/kotlin/modulecheck/parsing/groovy/antlr/GroovyDependenciesBlock.kt @@ -18,8 +18,9 @@ package modulecheck.parsing.groovy.antlr import modulecheck.parsing.DependenciesBlock class GroovyDependenciesBlock( - contentString: String -) : DependenciesBlock(contentString) { + contentString: String, + suppressAll: List +) : DependenciesBlock(contentString, suppressAll) { override fun originalLineMatchesParsed( originalLine: String, 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 d8b9d7560a..acb9f654ec 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 @@ -25,10 +25,7 @@ import modulecheck.parsing.ModuleRef import modulecheck.parsing.asConfigurationName import org.apache.groovy.parser.antlr4.GroovyLangLexer import org.apache.groovy.parser.antlr4.GroovyLangParser -import org.apache.groovy.parser.antlr4.GroovyParser.BlockStatementContext -import org.apache.groovy.parser.antlr4.GroovyParser.ExpressionListElementContext -import org.apache.groovy.parser.antlr4.GroovyParser.ScriptStatementContext -import org.apache.groovy.parser.antlr4.GroovyParser.StringLiteralContext +import org.apache.groovy.parser.antlr4.GroovyParser.* import org.apache.groovy.parser.antlr4.GroovyParserBaseVisitor class GroovyDependencyBlockParser { @@ -89,14 +86,22 @@ class GroovyDependencyBlockParser { val visitor = object : GroovyParserBaseVisitor() { + var pendingBlockNoInspectionComment: String? = null + + override fun visitNls(ctx: NlsContext) { + super.visitNls(ctx) + + pendingBlockNoInspectionComment = NO_INSPECTION_REGEX.find(ctx.text) + ?.destructured + ?.component1() + } + override fun visitScriptStatement(ctx: ScriptStatementContext?) { val statement = ctx?.statement() if (statement?.start?.text == "dependencies") { - super.visitScriptStatement(ctx) - val originalBlockBody = statement.parentOfType() ?.originalText(stream) ?: return @@ -107,21 +112,44 @@ class GroovyDependencyBlockParser { ?.removePrefix("\n") ?: return - val dependenciesBlock = GroovyDependenciesBlock(blockBody) + val blockSuppressed = pendingBlockNoInspectionComment?.split(",") + ?.map { it.trim() } + .orEmpty() + pendingBlockNoInspectionComment = null + + val dependenciesBlock = GroovyDependenciesBlock(blockBody, blockSuppressed) + + super.visitScriptStatement(ctx) val blockStatementVisitor = object : GroovyParserBaseVisitor() { + var pendingNoInspectionComment: String? = null + + override fun visitSep(ctx: SepContext) { + super.visitSep(ctx) + + pendingNoInspectionComment = NO_INSPECTION_REGEX.find(ctx.text) + ?.destructured + ?.component1() + } + override fun visitBlockStatement(ctx: BlockStatementContext) { val config = ctx.start.text + val suppressed = pendingNoInspectionComment?.split(",") + ?.map { it.trim() } + .orEmpty() + pendingNoInspectionComment = null + val moduleRefString = projectDepVisitor.visit(ctx) if (moduleRefString != null) { dependenciesBlock.addModuleStatement( configName = config.asConfigurationName(), parsedString = ctx.originalText(stream), - moduleRef = ModuleRef.from(moduleRefString) + moduleRef = ModuleRef.from(moduleRefString), + suppressed = suppressed ) return } @@ -133,7 +161,8 @@ class GroovyDependencyBlockParser { dependenciesBlock.addNonModuleStatement( configName = config.asConfigurationName(), parsedString = ctx.originalText(stream), - coordinates = mavenCoordinates + coordinates = mavenCoordinates, + suppressed = suppressed ) return } @@ -143,7 +172,8 @@ class GroovyDependencyBlockParser { dependenciesBlock.addUnknownStatement( configName = config.asConfigurationName(), parsedString = ctx.originalText(stream), - argument = argument + argument = argument, + suppressed = suppressed ) } } @@ -164,6 +194,7 @@ class GroovyDependencyBlockParser { companion object { val BLOCK_BODY_REGEX = """dependencies\s*\{([\s\S]*)\}""".toRegex() + val NO_INSPECTION_REGEX = """//noinspection \s*([\s\S]*)$""".toRegex() } } 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 7e5214758c..11559477a8 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 @@ -72,6 +72,79 @@ 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.allDeclarations shouldBe listOf( + ModuleDependencyDeclaration( + moduleRef = ModuleRef.StringRef(":core:android"), + configName = "api".asConfigurationName(), + declarationText = """api project(':core:android')""", + statementWithSurroundingText = " api project(':core:android')", + suppressed = listOf() + ), + ModuleDependencyDeclaration( + moduleRef = ModuleRef.StringRef(":core:jvm"), + configName = "api".asConfigurationName(), + declarationText = """api project(':core:jvm')""", + statementWithSurroundingText = " //noinspection Unused, MustBeApi\n api project(':core:jvm')", + suppressed = listOf("Unused", "MustBeApi") + ), + ModuleDependencyDeclaration( + moduleRef = ModuleRef.StringRef(":core:test"), + configName = "testImplementation".asConfigurationName(), + declarationText = """testImplementation project(':core:test')""", + statementWithSurroundingText = " testImplementation project(':core:test')", + suppressed = listOf() + ) + ) + } + + @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.allDeclarations shouldBe listOf( + ModuleDependencyDeclaration( + moduleRef = ModuleRef.StringRef(":core:android"), + configName = "api".asConfigurationName(), + declarationText = """api project(':core:android')""", + statementWithSurroundingText = " api project(':core:android')", + suppressed = listOf("Unused", "MustBeApi") + ), + ModuleDependencyDeclaration( + moduleRef = ModuleRef.StringRef(":core:jvm"), + configName = "api".asConfigurationName(), + 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() @@ -129,7 +202,10 @@ internal class GroovyDependencyBlockParserTest { """.trimIndent() ).single() - block.getOrEmpty(ModuleRef.StringRef(":core:test"), "api".asConfigurationName()) shouldBe listOf( + block.getOrEmpty( + ModuleRef.StringRef(":core:test"), + "api".asConfigurationName() + ) shouldBe listOf( ModuleDependencyDeclaration( moduleRef = ModuleRef.StringRef(":core:test"), configName = "api".asConfigurationName(), diff --git a/modulecheck-parsing/psi/src/main/kotlin/modulecheck/parsing/psi/KotlinDependenciesBlock.kt b/modulecheck-parsing/psi/src/main/kotlin/modulecheck/parsing/psi/KotlinDependenciesBlock.kt index 1ef11b4dc2..d500a9c9a4 100644 --- a/modulecheck-parsing/psi/src/main/kotlin/modulecheck/parsing/psi/KotlinDependenciesBlock.kt +++ b/modulecheck-parsing/psi/src/main/kotlin/modulecheck/parsing/psi/KotlinDependenciesBlock.kt @@ -18,8 +18,9 @@ package modulecheck.parsing.psi import modulecheck.parsing.DependenciesBlock class KotlinDependenciesBlock( - contentString: String -) : DependenciesBlock(contentString) { + contentString: String, + suppressAll: List +) : DependenciesBlock(contentString, suppressAll) { override fun originalLineMatchesParsed( originalLine: String, diff --git a/modulecheck-parsing/psi/src/main/kotlin/modulecheck/parsing/psi/KotlinDependencyBlockParser.kt b/modulecheck-parsing/psi/src/main/kotlin/modulecheck/parsing/psi/KotlinDependencyBlockParser.kt index c371e16af7..0fc456bf3d 100644 --- a/modulecheck-parsing/psi/src/main/kotlin/modulecheck/parsing/psi/KotlinDependencyBlockParser.kt +++ b/modulecheck-parsing/psi/src/main/kotlin/modulecheck/parsing/psi/KotlinDependencyBlockParser.kt @@ -18,7 +18,6 @@ package modulecheck.parsing.psi import modulecheck.parsing.MavenCoordinates import modulecheck.parsing.ModuleRef import modulecheck.parsing.asConfigurationName -import org.jetbrains.kotlin.com.intellij.psi.PsiComment import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.findDescendantOfType @@ -33,60 +32,29 @@ class KotlinDependencyBlockParser { val blocks = mutableListOf() - val blockVisitor = blockExpressionRecursiveVisitor { expression -> + fun blockVisitor( + blockSuppressed: List + ) = blockExpressionRecursiveVisitor { blockExpression -> - val block = KotlinDependenciesBlock((blockWhiteSpace ?: "") + expression.text) + val block = KotlinDependenciesBlock( + contentString = (blockWhiteSpace ?: "") + blockExpression.text, + suppressAll = blockSuppressed + ) - expression + blockExpression .children - .filterNot { it is PsiComment || it is PsiWhiteSpace } - .filterIsInstance() - .forEach { callExpression -> - - val configName = callExpression.calleeExpression!! - .text - .replace("\"", "") - - val moduleNameString = callExpression.getStringModuleNameOrNull() - ?: callExpression.getTypeSafeModuleNameOrNull() - - if (moduleNameString != null) { - block.addModuleStatement( - configName = configName.asConfigurationName(), - parsedString = callExpression.text, - moduleRef = ModuleRef.from(moduleNameString) - ) - return@forEach - } + .forEach { element -> - val mavenCoordinates = callExpression.getMavenCoordinatesOrNull() + when (element) { + is KtAnnotatedExpression -> { + val suppressed = element.suppressedNames() - if (mavenCoordinates != null) { - block.addNonModuleStatement( - configName.asConfigurationName(), - callExpression.text, - mavenCoordinates - ) - return@forEach + element.getChildOfType()?.parseStatements(block, suppressed) + } + is KtCallExpression -> { + element.parseStatements(block, listOf()) + } } - - val testFixturesModuleNameString = callExpression.getStringTestFixturesModuleNameOrNull() - ?: callExpression.getTypeSafeTestFixturesModuleNameOrNull() - - if (testFixturesModuleNameString != null) { - block.addModuleStatement( - configName = configName.asConfigurationName(), - parsedString = callExpression.text, - moduleRef = ModuleRef.from(testFixturesModuleNameString) - ) - return@forEach - } - - block.addUnknownStatement( - configName = configName.asConfigurationName(), - parsedString = callExpression.text, - argument = callExpression.getUnknownArgumentOrNull() ?: "" - ) } blocks.add(block) @@ -96,6 +64,10 @@ class KotlinDependencyBlockParser { if (expression.getChildOfType()?.text == "dependencies") { + val blockSuppressed = (expression.parent as? KtAnnotatedExpression) + ?.suppressedNames() + .orEmpty() + // recursively look for an enclosing KtCallExpression parent (`buildscript { ... }`) // then walk down to find its name reference (`buildscript`) val parentExpressionName = expression.parents @@ -111,7 +83,7 @@ class KotlinDependencyBlockParser { expression.findDescendantOfType()?.let { blockWhiteSpace = (it.prevSibling as? PsiWhiteSpace)?.text?.trimStart('\n', '\r') - blockVisitor.visitBlockExpression(it) + blockVisitor(blockSuppressed).visitBlockExpression(it) } } } @@ -122,6 +94,60 @@ class KotlinDependencyBlockParser { } } +private fun KtCallExpression.parseStatements( + block: KotlinDependenciesBlock, + suppressed: List +) { + val configName = calleeExpression!! + .text + .replace("\"", "") + + val moduleNameString = getStringModuleNameOrNull() + ?: getTypeSafeModuleNameOrNull() + + if (moduleNameString != null) { + block.addModuleStatement( + configName = configName.asConfigurationName(), + parsedString = text, + moduleRef = ModuleRef.from(moduleNameString), + suppressed = suppressed + ) + return + } + + val mavenCoordinates = getMavenCoordinatesOrNull() + + if (mavenCoordinates != null) { + block.addNonModuleStatement( + configName = configName.asConfigurationName(), + parsedString = text, + coordinates = mavenCoordinates, + suppressed = suppressed + ) + return + } + + val testFixturesModuleNameString = getStringTestFixturesModuleNameOrNull() + ?: getTypeSafeTestFixturesModuleNameOrNull() + + if (testFixturesModuleNameString != null) { + block.addModuleStatement( + configName = configName.asConfigurationName(), + parsedString = text, + moduleRef = ModuleRef.from(testFixturesModuleNameString), + suppressed = suppressed + ) + return + } + + block.addUnknownStatement( + configName = configName.asConfigurationName(), + parsedString = text, + argument = getUnknownArgumentOrNull() ?: "", + suppressed = suppressed + ) +} + internal fun KtCallExpression.getStringModuleNameOrNull(): String? { return this // implementation(project(path = ":foo:bar")) .valueArguments // [project(path = ":foo:bar")] @@ -207,3 +233,12 @@ inline fun literalStringTemplateRecursiveVisitor( block(entry) } } + +internal fun KtAnnotatedExpression.suppressedNames(): List = annotationEntries + .filter { it.typeReference?.text == "Suppress" || it.typeReference?.text == "SuppressWarnings" } + .flatMap { it.valueArgumentList?.arguments.orEmpty() } + .mapNotNull { + it.getChildOfType() // "Unused" + ?.getChildOfType() // Unused + ?.text + } diff --git a/modulecheck-parsing/psi/src/test/kotlin/modulecheck/parsing/psi/KotlinDependencyBlockParserTest.kt b/modulecheck-parsing/psi/src/test/kotlin/modulecheck/parsing/psi/KotlinDependencyBlockParserTest.kt index 851c373cbb..db4991e7af 100644 --- a/modulecheck-parsing/psi/src/test/kotlin/modulecheck/parsing/psi/KotlinDependencyBlockParserTest.kt +++ b/modulecheck-parsing/psi/src/test/kotlin/modulecheck/parsing/psi/KotlinDependencyBlockParserTest.kt @@ -97,6 +97,85 @@ internal class KotlinDependencyBlockParserTest { ) } + @Test + fun `declaration with annotation should include annotation with argument`() { + val block = KotlinDependencyBlockParser() + .parse( + """ + dependencies { + api(project(":core:android")) + @Suppress("Unused") + api(project(":core:jvm")) + testImplementation(project(":core:test")) + } + """.trimIndent() + ).single() + + block.allDeclarations shouldBe listOf( + ModuleDependencyDeclaration( + moduleRef = ModuleRef.StringRef(":core:android"), + configName = "api".asConfigurationName(), + declarationText = """api(project(":core:android"))""", + statementWithSurroundingText = " api(project(\":core:android\"))", + suppressed = listOf() + ), + ModuleDependencyDeclaration( + moduleRef = ModuleRef.StringRef(":core:jvm"), + configName = "api".asConfigurationName(), + declarationText = """api(project(":core:jvm"))""", + statementWithSurroundingText = " @Suppress(\"Unused\")\n api(project(\":core:jvm\"))", + suppressed = listOf("Unused") + ), + ModuleDependencyDeclaration( + moduleRef = ModuleRef.StringRef(":core:test"), + configName = "testImplementation".asConfigurationName(), + declarationText = """testImplementation(project(":core:test"))""", + statementWithSurroundingText = " testImplementation(project(\":core:test\"))", + suppressed = listOf() + ) + ) + } + + @Test + fun `dependency block with Suppress annotation should include annotation with argument`() { + val block = KotlinDependencyBlockParser() + .parse( + """ + @Suppress("Unused") + dependencies { + api(project(":core:android")) + @Suppress("InheritedDependency") + api(project(":core:jvm")) + testImplementation(project(":core:test")) + } + """.trimIndent() + ).single() + + block.allDeclarations shouldBe listOf( + ModuleDependencyDeclaration( + moduleRef = ModuleRef.StringRef(":core:android"), + configName = "api".asConfigurationName(), + declarationText = """api(project(":core:android"))""", + statementWithSurroundingText = " api(project(\":core:android\"))", + suppressed = listOf("Unused") + ), + ModuleDependencyDeclaration( + moduleRef = ModuleRef.StringRef(":core:jvm"), + configName = "api".asConfigurationName(), + declarationText = """api(project(":core:jvm"))""", + statementWithSurroundingText = " @Suppress(\"InheritedDependency\")\n api(project(\":core:jvm\"))", + suppressed = listOf("InheritedDependency", "Unused") + ), + ModuleDependencyDeclaration( + moduleRef = ModuleRef.StringRef(":core:test"), + configName = "testImplementation".asConfigurationName(), + declarationText = """testImplementation(project(":core:test"))""", + statementWithSurroundingText = " testImplementation(project(\":core:test\"))", + suppressed = listOf("Unused") + ) + ) + } + @Test fun `string module dependency declaration with testFixtures should be parsed`() { val block = KotlinDependencyBlockParser() diff --git a/modulecheck-plugin/src/main/kotlin/modulecheck/gradle/task/ModuleCheckTask.kt b/modulecheck-plugin/src/main/kotlin/modulecheck/gradle/task/ModuleCheckTask.kt index d53a5365f1..595339d867 100644 --- a/modulecheck-plugin/src/main/kotlin/modulecheck/gradle/task/ModuleCheckTask.kt +++ b/modulecheck-plugin/src/main/kotlin/modulecheck/gradle/task/ModuleCheckTask.kt @@ -15,11 +15,14 @@ package modulecheck.gradle.task -import modulecheck.api.* import modulecheck.api.Deletable +import modulecheck.api.Finding +import modulecheck.api.Finding.LogElement +import modulecheck.api.Fixable import modulecheck.gradle.GradleProjectProvider import modulecheck.gradle.ModuleCheckExtension -import modulecheck.parsing.* +import modulecheck.parsing.Project2 +import modulecheck.parsing.ProjectsAware import org.gradle.api.DefaultTask import org.gradle.api.GradleException import org.gradle.api.tasks.Input @@ -64,6 +67,7 @@ abstract class ModuleCheckTask : .map { projectProvider.get(it.path) } .getFindings() .distinct() + .filterNot { it.shouldSkip() } } val numIssues = results.finish() @@ -82,46 +86,84 @@ abstract class ModuleCheckTask : val secondsDouble = timeMillis / 1000.0 if (data.isNotEmpty()) { - logger.printSuccessHeader("ModuleCheck found ${data.size} issues in $secondsDouble seconds\n") + logger.printSuccessHeader( + "ModuleCheck found ${data.size} issues in $secondsDouble seconds\n" + + "To ignore any of these findings, annotate the dependency declaration.\n" + + "For Kotlin files, use @Suppress(\"\") or " + + "@SuppressWarnings(\"\").\n" + + "For Groovy files, add a comment above the declaration: " + + "//noinspection ." + ) } val unFixed = grouped .entries .sortedBy { it.key } - .flatMap { (path, list) -> + .map { (path, list) -> - logger.printHeader("\t$path") + logger.printHeader("${tab(1)}$path") - val logStrings = mutableMapOf() + val elements = list + .map { finding -> - val (fixed, toFix) = list - .distinct() - .onEach { finding -> - logStrings[finding] = finding.logString() - } - .partition { finding -> - - if (!autoCorrect) return@partition false + finding.logElement().apply { - if (deleteUnused && finding is Deletable) { - finding.delete() - } else { - (finding as? Fixable)?.fix() ?: false + fixed = when { + !autoCorrect -> false + deleteUnused && finding is Deletable -> { + finding.delete() + } + else -> { + (finding as? Fixable)?.fix() ?: false + } + } } } - fixed.forEach { finding -> - logger.printWarning("\t\t${logStrings.getValue(finding)}") - } - - toFix.forEach { finding -> - logger.printFailure("\t\t${logStrings.getValue(finding)}") + if (elements.isEmpty()) return@map 0 + + val maxDependencyPath = maxOf( + elements.maxOf { it.dependencyPath.length }, + "dependency".length + ) + val maxProblemName = elements.maxOf { it.problemName.length } + val maxSource = maxOf(elements.maxOf { it.sourceOrNull.orEmpty().length }, "source".length) + val maxFilePathStr = elements.maxOf { it.filePathStr.length } + + logger.printHeader( + tab(2) + + "dependency".padEnd(maxDependencyPath) + + tab(1) + + "name".padEnd(maxProblemName) + + tab(1) + + "source".padEnd(maxSource) + + tab(1) + + "build file".padEnd(maxFilePathStr) + ) + + elements.sortedWith( + compareBy( + { !it.fixed }, + { it.positionOrNull } + ) + ).forEach { logElement -> + + logElement.log( + tab(2) + + logElement.dependencyPath.padEnd(maxDependencyPath) + + tab(1) + + logElement.problemName.padEnd(maxProblemName) + + tab(1) + + logElement.sourceOrNull.orEmpty().padEnd(maxSource) + + tab(1) + + logElement.filePathStr.padEnd(maxFilePathStr) + ) } - toFix + elements.count { !it.fixed } } - return unFixed.size + return unFixed.sum() } inline fun T.measured(action: T.() -> R): TimedResults { @@ -135,4 +177,14 @@ abstract class ModuleCheckTask : } data class TimedResults(val timeMillis: Long, val data: R) + + private fun tab(numTabs: Int) = " ".repeat(numTabs) + + private fun LogElement.log(message: String) { + if (fixed) { + logger.printWarning(message) + } else { + logger.printFailure(message) + } + } } diff --git a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/AnvilScopesTest.kt b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/AnvilScopesTest.kt index 73a8722c79..32e639048c 100644 --- a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/AnvilScopesTest.kt +++ b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/AnvilScopesTest.kt @@ -431,7 +431,7 @@ class AnvilScopesTest : BasePluginTest() { .writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckUnusedDependency") { - it shouldContain "app/build.gradle.kts: (8, 3): unused: :lib-2" + it shouldContain ":lib-2 \\s*unused .*/app/build.gradle.kts: \\(8, 3\\):".toRegex() } } } diff --git a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/DisableAndroidResourcesRuleTest.kt b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/DisableAndroidResourcesRuleTest.kt index 1b33334be9..d687bf5824 100644 --- a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/DisableAndroidResourcesRuleTest.kt +++ b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/DisableAndroidResourcesRuleTest.kt @@ -311,7 +311,7 @@ class DisableAndroidResourcesRuleTest : BasePluginTest() { project.writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckDisableAndroidResources") { - it shouldContain "app/build.gradle.kts: unused R file generation:" + it shouldContain "\\s*disableAndroidResources .*/app/build.gradle.kts:".toRegex() } } @@ -517,7 +517,7 @@ class DisableAndroidResourcesRuleTest : BasePluginTest() { }.writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckDisableAndroidResources") { - it shouldContain "app/build.gradle.kts: (21, 3): unused R file generation:" + it shouldContain "\\s*disableAndroidResources .*/app/build.gradle.kts: \\(21, 3\\):".toRegex() } } @@ -535,7 +535,7 @@ class DisableAndroidResourcesRuleTest : BasePluginTest() { }.writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckDisableAndroidResources") { - it shouldContain "app/build.gradle.kts: (20, 1): unused R file generation:" + it shouldContain "\\s*disableAndroidResources .*/app/build.gradle.kts: \\(20, 1\\):".toRegex() } } @@ -557,7 +557,7 @@ class DisableAndroidResourcesRuleTest : BasePluginTest() { }.writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckDisableAndroidResources") { - it shouldContain "app/build.gradle.kts: (22, 5): unused R file generation:" + it shouldContain "\\s*disableAndroidResources .*/app/build.gradle.kts: \\(22, 5\\):".toRegex() } } @@ -577,7 +577,7 @@ class DisableAndroidResourcesRuleTest : BasePluginTest() { }.writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckDisableAndroidResources") { - it shouldContain "app/build.gradle.kts: (21, 3): unused R file generation:" + it shouldContain "\\s*disableAndroidResources .*/app/build.gradle.kts: \\(21, 3\\):".toRegex() } } } diff --git a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/DisableAndroidViewBindingRuleTest.kt b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/DisableAndroidViewBindingRuleTest.kt index 947cac4cc5..16031d3f78 100644 --- a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/DisableAndroidViewBindingRuleTest.kt +++ b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/DisableAndroidViewBindingRuleTest.kt @@ -618,7 +618,7 @@ class DisableAndroidViewBindingRuleTest : BasePluginTest() { }.writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckDisableViewBinding") { - it shouldContain "lib1/build.gradle.kts: (22, 3): unused ViewBinding generation:".fixPath() + it shouldContain "\\s*disableViewBinding .*/lib1/build.gradle.kts: \\(22, 3\\):".toRegex() } } @@ -636,7 +636,7 @@ class DisableAndroidViewBindingRuleTest : BasePluginTest() { }.writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckDisableViewBinding") { - it shouldContain "lib1/build.gradle.kts: (21, 1): unused ViewBinding generation:".fixPath() + it shouldContain "\\s*disableViewBinding .*/lib1/build.gradle.kts: \\(21, 1\\):".toRegex() } } @@ -658,7 +658,7 @@ class DisableAndroidViewBindingRuleTest : BasePluginTest() { }.writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckDisableViewBinding") { - it shouldContain "lib1/build.gradle.kts: (23, 5): unused ViewBinding generation:".fixPath() + it shouldContain "\\s*disableViewBinding .*/lib1/build.gradle.kts: \\(23, 5\\):".toRegex() } } @@ -678,7 +678,7 @@ class DisableAndroidViewBindingRuleTest : BasePluginTest() { }.writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckDisableViewBinding") { - it shouldContain "lib1/build.gradle.kts: (22, 3): unused ViewBinding generation:".fixPath() + it shouldContain "\\s*disableViewBinding .*/lib1/build.gradle.kts: \\(22, 3\\):".toRegex() } } } diff --git a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/InheritedDependencyTest.kt b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/InheritedDependencyTest.kt index 316e5b98be..577f94cba4 100644 --- a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/InheritedDependencyTest.kt +++ b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/InheritedDependencyTest.kt @@ -292,9 +292,9 @@ class InheritedDependencyTest : BasePluginTest() { "moduleCheckInheritedDependency", "moduleCheckSortDependencies" ) { - it shouldContain "app/build.gradle.kts: (6, 3): inheritedDependency: :lib-1 from: :lib-4" - it shouldContain "app/build.gradle.kts: (6, 3): inheritedDependency: :lib-2 from: :lib-4" - it shouldContain "app/build.gradle.kts: (6, 3): inheritedDependency: :lib-3 from: :lib-4" + it shouldContain ":lib-1 \\s*inheritedDependency \\s*:lib-4 .*/app/build.gradle.kts: \\(6, 3\\):".toRegex() + it shouldContain ":lib-2 \\s*inheritedDependency \\s*:lib-4 .*/app/build.gradle.kts: \\(6, 3\\):".toRegex() + it shouldContain ":lib-3 \\s*inheritedDependency \\s*:lib-4 .*/app/build.gradle.kts: \\(6, 3\\):".toRegex() } } } diff --git a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/MustBeApiTest.kt b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/MustBeApiTest.kt index bce44daaa6..2e9035a5bb 100644 --- a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/MustBeApiTest.kt +++ b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/MustBeApiTest.kt @@ -959,10 +959,55 @@ class MustBeApiTest : BasePluginTest() { .writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckMustBeApi") { - it shouldContain "/app/build.gradle.kts: (6, 3): mustBeApi: :lib-4" + it shouldContain ":lib-4 \\s*mustBeApi .*/app/build.gradle.kts: \\(6, 3\\):".toRegex() } } + @Test + fun `single implementation property which is suppressed should succeed`() { + val appProject = ProjectSpec("app") { + addBuildSpec( + ProjectBuildSpec { + addPlugin("""kotlin("jvm")""") + addProjectDependency("implementation", jvmSub4, "@Suppress(\"mustBeApi\")") + } + ) + addSrcSpec( + ProjectSrcSpec(Path.of("src/main/java")) { + addFileSpec( + FileSpec.builder("com.example.app", "MyApp") + .addProperty(PropertySpec.builder("lib1Class", lib1ClassName).build()) + .addProperty(PropertySpec.builder("lib2Class", lib2ClassName).build()) + .addProperty(PropertySpec.builder("lib3Class", lib3ClassName).build()) + .addProperty(PropertySpec.builder("lib4Class", lib4ClassName).build()) + .build() + ) + } + ) + } + + ProjectSpec("project") { + applyEach(projects) { project -> + addSubproject(project) + } + addSubproject(appProject) + addSubprojects(jvmSub1, jvmSub2, jvmSub3, jvmSub4) + addSettingsSpec(projectSettings.build()) + addBuildSpec( + projectBuild + .addBlock( + """moduleCheck { + | autoCorrect = false + |} + """.trimMargin() + ).build() + ) + } + .writeIn(testProjectDir.toPath()) + + build("moduleCheckMustBeApi").shouldSucceed() + } + @Test fun `multiple implementation properties should fail with message`() { val appProject = ProjectSpec("app") { @@ -1009,10 +1054,10 @@ class MustBeApiTest : BasePluginTest() { .writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckMustBeApi") { - it shouldContain "/app/build.gradle.kts: (6, 3): mustBeApi: :lib-1" - it shouldContain "/app/build.gradle.kts: (7, 3): mustBeApi: :lib-2" - it shouldContain "/app/build.gradle.kts: (8, 3): mustBeApi: :lib-3" - it shouldContain "/app/build.gradle.kts: (9, 3): mustBeApi: :lib-4" + it shouldContain ":lib-1 \\s*mustBeApi .*/app/build.gradle.kts: \\(6, 3\\):".toRegex() + it shouldContain ":lib-2 \\s*mustBeApi .*/app/build.gradle.kts: \\(7, 3\\):".toRegex() + it shouldContain ":lib-3 \\s*mustBeApi .*/app/build.gradle.kts: \\(8, 3\\):".toRegex() + it shouldContain ":lib-4 \\s*mustBeApi .*/app/build.gradle.kts: \\(9, 3\\):".toRegex() } } @@ -1059,7 +1104,7 @@ class MustBeApiTest : BasePluginTest() { .writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckMustBeApi") { - it shouldContain "/app/build.gradle.kts: (6, 3): mustBeApi: :lib-4" + it shouldContain ":lib-4 \\s*mustBeApi .*/app/build.gradle.kts: \\(6, 3\\):".toRegex() } } @@ -1112,10 +1157,10 @@ class MustBeApiTest : BasePluginTest() { .writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckMustBeApi") { - it shouldContain "/app/build.gradle.kts: (6, 3): mustBeApi: :lib-1" - it shouldContain "/app/build.gradle.kts: (7, 3): mustBeApi: :lib-2" - it shouldContain "/app/build.gradle.kts: (8, 3): mustBeApi: :lib-3" - it shouldContain "/app/build.gradle.kts: (9, 3): mustBeApi: :lib-4" + it shouldContain ":lib-1 \\s*mustBeApi .*/app/build.gradle.kts: \\(6, 3\\):".toRegex() + it shouldContain ":lib-2 \\s*mustBeApi .*/app/build.gradle.kts: \\(7, 3\\):".toRegex() + it shouldContain ":lib-3 \\s*mustBeApi .*/app/build.gradle.kts: \\(8, 3\\):".toRegex() + it shouldContain ":lib-4 \\s*mustBeApi .*/app/build.gradle.kts: \\(9, 3\\):".toRegex() } } @@ -1163,7 +1208,7 @@ class MustBeApiTest : BasePluginTest() { .writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckMustBeApi") { - it shouldContain "/app/build.gradle.kts: (6, 3): mustBeApi: :lib-4" + it shouldContain ":lib-4 \\s*mustBeApi .*/app/build.gradle.kts: \\(6, 3\\):".toRegex() } } @@ -1232,10 +1277,10 @@ class MustBeApiTest : BasePluginTest() { .writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckMustBeApi") { - it shouldContain "/app/build.gradle.kts: (6, 3): mustBeApi: :lib-1" - it shouldContain "/app/build.gradle.kts: (7, 3): mustBeApi: :lib-2" - it shouldContain "/app/build.gradle.kts: (8, 3): mustBeApi: :lib-3" - it shouldContain "/app/build.gradle.kts: (9, 3): mustBeApi: :lib-4" + it shouldContain ":lib-1 \\s*mustBeApi .*/app/build.gradle.kts: \\(6, 3\\):".toRegex() + it shouldContain ":lib-2 \\s*mustBeApi .*/app/build.gradle.kts: \\(7, 3\\):".toRegex() + it shouldContain ":lib-3 \\s*mustBeApi .*/app/build.gradle.kts: \\(8, 3\\):".toRegex() + it shouldContain ":lib-4 \\s*mustBeApi .*/app/build.gradle.kts: \\(9, 3\\):".toRegex() } } @@ -1283,7 +1328,7 @@ class MustBeApiTest : BasePluginTest() { .writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckMustBeApi") { - it shouldContain "/app/build.gradle.kts: (6, 3): mustBeApi: :lib-4" + it shouldContain ":lib-4 \\s*mustBeApi .*/app/build.gradle.kts: \\(6, 3\\):".toRegex() } } @@ -1352,10 +1397,10 @@ class MustBeApiTest : BasePluginTest() { .writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckMustBeApi") { - it shouldContain "/app/build.gradle.kts: (6, 3): mustBeApi: :lib-1" - it shouldContain "/app/build.gradle.kts: (7, 3): mustBeApi: :lib-2" - it shouldContain "/app/build.gradle.kts: (8, 3): mustBeApi: :lib-3" - it shouldContain "/app/build.gradle.kts: (9, 3): mustBeApi: :lib-4" + it shouldContain ":lib-1 \\s*mustBeApi .*/app/build.gradle.kts: \\(6, 3\\):".toRegex() + it shouldContain ":lib-2 \\s*mustBeApi .*/app/build.gradle.kts: \\(7, 3\\):".toRegex() + it shouldContain ":lib-3 \\s*mustBeApi .*/app/build.gradle.kts: \\(8, 3\\):".toRegex() + it shouldContain ":lib-4 \\s*mustBeApi .*/app/build.gradle.kts: \\(9, 3\\):".toRegex() } } @@ -1408,7 +1453,7 @@ class MustBeApiTest : BasePluginTest() { .writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckMustBeApi") { - it shouldContain "/app/build.gradle.kts: (6, 3): mustBeApi: :lib-4" + it shouldContain ":lib-4 \\s*mustBeApi .*/app/build.gradle.kts: \\(6, 3\\):".toRegex() } } @@ -1497,10 +1542,10 @@ class MustBeApiTest : BasePluginTest() { .writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckMustBeApi") { - it shouldContain "/app/build.gradle.kts: (6, 3): mustBeApi: :lib-1" - it shouldContain "/app/build.gradle.kts: (7, 3): mustBeApi: :lib-2" - it shouldContain "/app/build.gradle.kts: (8, 3): mustBeApi: :lib-3" - it shouldContain "/app/build.gradle.kts: (9, 3): mustBeApi: :lib-4" + it shouldContain ":lib-1 \\s*mustBeApi .*/app/build.gradle.kts: \\(6, 3\\):".toRegex() + it shouldContain ":lib-2 \\s*mustBeApi .*/app/build.gradle.kts: \\(7, 3\\):".toRegex() + it shouldContain ":lib-3 \\s*mustBeApi .*/app/build.gradle.kts: \\(8, 3\\):".toRegex() + it shouldContain ":lib-4 \\s*mustBeApi .*/app/build.gradle.kts: \\(9, 3\\):".toRegex() } } @@ -1554,7 +1599,7 @@ class MustBeApiTest : BasePluginTest() { .writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckMustBeApi") { - it shouldContain "/app/build.gradle.kts: (6, 3): mustBeApi: :lib-4" + it shouldContain ":lib-4 \\s*mustBeApi .*/app/build.gradle.kts: \\(6, 3\\):".toRegex() } } @@ -1647,10 +1692,10 @@ class MustBeApiTest : BasePluginTest() { .writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckMustBeApi") { - it shouldContain "/app/build.gradle.kts: (6, 3): mustBeApi: :lib-1" - it shouldContain "/app/build.gradle.kts: (7, 3): mustBeApi: :lib-2" - it shouldContain "/app/build.gradle.kts: (8, 3): mustBeApi: :lib-3" - it shouldContain "/app/build.gradle.kts: (9, 3): mustBeApi: :lib-4" + it shouldContain ":lib-1 \\s*mustBeApi .*/app/build.gradle.kts: \\(6, 3\\):".toRegex() + it shouldContain ":lib-2 \\s*mustBeApi .*/app/build.gradle.kts: \\(7, 3\\):".toRegex() + it shouldContain ":lib-3 \\s*mustBeApi .*/app/build.gradle.kts: \\(8, 3\\):".toRegex() + it shouldContain ":lib-4 \\s*mustBeApi .*/app/build.gradle.kts: \\(9, 3\\):".toRegex() } } } diff --git a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedDependenciesTest.kt b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedDependenciesTest.kt index 973261d806..56abc9d4cc 100644 --- a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedDependenciesTest.kt +++ b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedDependenciesTest.kt @@ -106,6 +106,119 @@ class UnusedDependenciesTest : BasePluginTest() { |""".trimMargin() } + @Test + fun `suppressed dependency which is unused should not be reported`() { + val appProject = ProjectSpec("app") { + addBuildSpec( + ProjectBuildSpec { + addPlugin("kotlin(\"jvm\")") + addProjectDependency("api", jvmSub1) + addProjectDependency("api", jvmSub2, "@Suppress(\"unused\")") + addProjectDependency("implementation", jvmSub3) + } + ) + addSrcSpec( + ProjectSrcSpec(Path.of("src/main/kotlin")) { + addFileSpec(myApp) + } + ) + } + + ProjectSpec("project") { + applyEach(projects) { project -> + addSubproject(project) + } + addSubproject(appProject) + addSubprojects(jvmSub1, jvmSub2, jvmSub3) + addSettingsSpec(projectSettings.build()) + addBuildSpec( + projectBuild + .addBlock( + """moduleCheck { + | autoCorrect = true + |} + """.trimMargin() + ).build() + ) + } + .writeIn(testProjectDir.toPath()) + + build("moduleCheckUnusedDependency").shouldSucceed() + + File(testProjectDir, "/app/build.gradle.kts").readText() shouldBe """plugins { + | kotlin("jvm") + |} + | + |dependencies { + | api(project(path = ":lib-1")) + | @Suppress("unused") + | api(project(path = ":lib-2")) + | // implementation(project(path = ":lib-3")) // ModuleCheck finding [unused] + |} + |""".trimMargin() + } + + @Test + fun `suppressed unused at the dependency block should not report any unused`() { + val appProject = ProjectSpec("app") { + addBuildSpec( + ProjectBuildSpec { + addPlugin("kotlin(\"jvm\")") + addBlock( + """ + |@Suppress("unused") + |dependencies { + | api(project(path = ":lib-1")) + | api(project(path = ":lib-2")) + | implementation(project(path = ":lib-3")) + |} + |""".trimMargin() + ) + } + ) + addSrcSpec( + ProjectSrcSpec(Path.of("src/main/kotlin")) { + addFileSpec(myApp) + } + ) + } + + ProjectSpec("project") { + applyEach(projects) { project -> + addSubproject(project) + } + addSubproject(appProject) + addSubprojects(jvmSub1, jvmSub2, jvmSub3) + addSettingsSpec(projectSettings.build()) + addBuildSpec( + projectBuild + .addBlock( + """moduleCheck { + | autoCorrect = true + |} + """.trimMargin() + ).build() + ) + } + .writeIn(testProjectDir.toPath()) + + build("moduleCheckUnusedDependency").shouldSucceed() + + File(testProjectDir, "/app/build.gradle.kts").readText() shouldBe """plugins { + | kotlin("jvm") + |} + | + |@Suppress("unused") + |dependencies { + | api(project(path = ":lib-1")) + | api(project(path = ":lib-2")) + | implementation(project(path = ":lib-3")) + |} + | + | + |""".trimMargin() + } + @Test fun `with autoCorrect and preceding typesafe external dependency should be commented out`() { val appProject = ProjectSpec("app") { @@ -297,8 +410,8 @@ class UnusedDependenciesTest : BasePluginTest() { shouldFailWithMessage("moduleCheckUnusedDependency") { it shouldContain "> ModuleCheck found 2 issues which were not auto-corrected." - it shouldContain "app/build.gradle.kts: (7, 3): unused: :lib-2" - it shouldContain "app/build.gradle.kts: (8, 3): unused: :lib-3" + it shouldContain ":lib-2 \\s*unused .*app/build.gradle.kts: \\(7, 3\\):".toRegex() + it shouldContain ":lib-3 \\s*unused .*app/build.gradle.kts: \\(8, 3\\):".toRegex() } } @@ -340,8 +453,8 @@ class UnusedDependenciesTest : BasePluginTest() { shouldFailWithMessage("moduleCheckUnusedDependency") { it shouldContain "> ModuleCheck found 2 issues which were not auto-corrected." - it shouldContain "app/build.gradle.kts: (7, 3): unused: :lib-2" - it shouldContain "app/build.gradle.kts: (8, 3): unused: :lib-3" + it shouldContain ":lib-2 \\s*unused .*app/build.gradle.kts: \\(7, 3\\):".toRegex() + it shouldContain ":lib-3 \\s*unused .*app/build.gradle.kts: \\(8, 3\\):".toRegex() } } diff --git a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedKaptTest.kt b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedKaptTest.kt index e0f071a48a..f9ac2d1f4a 100644 --- a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedKaptTest.kt +++ b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedKaptTest.kt @@ -19,7 +19,6 @@ import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.TypeSpec -import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain import modulecheck.specs.ProjectBuildSpec import modulecheck.specs.ProjectSettingsSpec @@ -61,8 +60,8 @@ class UnusedKaptTest : BasePluginTest() { .writeIn(testProjectDir.toPath()) shouldFailWithMessage("moduleCheckUnusedKapt") { - it shouldContain "app/build.gradle.kts: unused kapt dependency: com.google.dagger:dagger-compiler" - it shouldContain "app/build.gradle.kts: unused kaptTest dependency: com.squareup.moshi:moshi-kotlin-codegen" + it shouldContain "com.google.dagger:dagger-compiler \\s*unusedKaptProcessor .*/app/build.gradle.kts:".toRegex() + it shouldContain "com.squareup.moshi:moshi-kotlin-codegen \\s*unusedKaptProcessor .*/app/build.gradle.kts:".toRegex() } File(testProjectDir, "/app/build.gradle.kts").readText() shouldBe """plugins { diff --git a/website/docs/suppressing-findings.mdx b/website/docs/suppressing-findings.mdx new file mode 100644 index 0000000000..59fa2c8226 --- /dev/null +++ b/website/docs/suppressing-findings.mdx @@ -0,0 +1,66 @@ +--- +id: suppressing-findings +title: Suppressing Findings +sidebar_label: Suppressing Findings +--- + + +import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; + +You can disable individual ModuleCheck findings via annotation, just like with any other lint tool. + +The name of the check to disable can be found in the `name` column of console output: + +``` +> Task :moduleCheck +ModuleCheck found 3 issues in 6.157 seconds + + :app + dependency name source build file + :fat-and-leaky inheritedDependency /Users/rbusarow/projects/sample/app/build.gradle.kts: (15, 3): + :fat-and-leaky mustBeApi /Users/rbusarow/projects/sample/app/build.gradle.kts: (15, 3): + :unused-lib unused /Users/rbusarow/projects/sample/app/build.gradle.kts: (49, 3): + +``` + + + + + +```kotlin title="build.gradle.kts" +@Suppress("mustBeApi") // don't switch anything to an api config +dependencies { + + @Suppress("unused") // don't comment out or delete this dependency + implementation(project(":unused-lib")) + + @Suppress("inheritedDependency") // don't add dependencies which are inherited from this fat jar + implementation(project(":fat-and-leaky")) +} +``` + + + + + +```groovy title="build.gradle" +// don't switch anything to an api config +//noinspection mustBeApi +dependencies { + + // don't comment out or delete this dependency + //noinspection unused + implementation(project(":unused-lib")) + + // don't add dependencies which are inherited from this fat jar + //noinspection inheritedDependency + implementation(project(":fat-and-leaky")) +} +``` + + + diff --git a/website/sidebars.js b/website/sidebars.js index edac2f21af..815bfcae47 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -17,6 +17,7 @@ module.exports = { Docs: [ "quickstart", "configuration", + "suppressing-findings", "ci-workflow", { type: "category",