diff --git a/scala/private/common_attributes.bzl b/scala/private/common_attributes.bzl index 2982536fe..bd6c3fc3c 100644 --- a/scala/private/common_attributes.bzl +++ b/scala/private/common_attributes.bzl @@ -66,13 +66,6 @@ common_attrs.update({ ], mandatory = False, ), - "_unused_dependency_checker_plugin": attr.label( - default = Label( - "@io_bazel_rules_scala//third_party/unused_dependency_checker/src/main:unused_dependency_checker", - ), - allow_files = [".jar"], - mandatory = False, - ), "unused_dependency_checker_ignored_targets": attr.label_list(default = []), "_code_coverage_instrumentation_worker": attr.label( default = "@io_bazel_rules_scala//src/java/io/bazel/rulesscala/coverage/instrumenter", diff --git a/scala/private/rule_impls.bzl b/scala/private/rule_impls.bzl index b4d0539c7..c5747d1ca 100644 --- a/scala/private/rule_impls.bzl +++ b/scala/private/rule_impls.bzl @@ -66,7 +66,7 @@ def compile_scala( input_plugins = plugins plugins = _collect_plugin_paths(plugins) internal_plugin_jars = [] - dependency_analyzer_mode = "off" + strict_deps_mode = "off" compiler_classpath_jars = cjars optional_scalac_args = "" classpath_resources = [] @@ -75,7 +75,7 @@ def compile_scala( if is_dependency_analyzer_on(ctx): # "off" mode is used as a feature toggle, that preserves original behaviour - dependency_analyzer_mode = ctx.fragments.java.strict_java_deps + strict_deps_mode = ctx.fragments.java.strict_java_deps dep_plugin = ctx.attr._dependency_analyzer_plugin plugins = depset(transitive = [plugins, dep_plugin.files]) internal_plugin_jars = ctx.files._dependency_analyzer_plugin @@ -102,9 +102,9 @@ CurrentTarget: {current_target} ) elif unused_dependency_checker_mode != "off": - unused_dependency_plugin = ctx.attr._unused_dependency_checker_plugin - plugins = depset(transitive = [plugins, unused_dependency_plugin.files]) - internal_plugin_jars = ctx.files._unused_dependency_checker_plugin + dependency_analyzer_plugin = ctx.attr._dependency_analyzer_plugin + plugins = depset(transitive = [plugins, dependency_analyzer_plugin.files]) + internal_plugin_jars = ctx.files._dependency_analyzer_plugin cjars_list = cjars.to_list() direct_jars = _join_path(cjars_list) @@ -152,7 +152,7 @@ ResourceSources: {resource_sources} ResourceJars: {resource_jars} ScalacOpts: {scala_opts} SourceJars: {srcjars} -DependencyAnalyzerMode: {dependency_analyzer_mode} +StrictDepsMode: {strict_deps_mode} UnusedDependencyCheckerMode: {unused_dependency_checker_mode} StatsfileOutput: {statsfile_output} """.format( @@ -170,7 +170,7 @@ StatsfileOutput: {statsfile_output} resource_targets = ",".join([p[0] for p in resource_paths]), resource_sources = ",".join([p[1] for p in resource_paths]), resource_jars = _join_path(resource_jars), - dependency_analyzer_mode = dependency_analyzer_mode, + strict_deps_mode = strict_deps_mode, unused_dependency_checker_mode = unused_dependency_checker_mode, statsfile_output = statsfile.path, ) diff --git a/src/java/io/bazel/rulesscala/scalac/CompileOptions.java b/src/java/io/bazel/rulesscala/scalac/CompileOptions.java index 464c159bb..5f3a84885 100644 --- a/src/java/io/bazel/rulesscala/scalac/CompileOptions.java +++ b/src/java/io/bazel/rulesscala/scalac/CompileOptions.java @@ -24,7 +24,7 @@ public class CompileOptions { public final String[] ignoredTargets; public final String[] indirectJars; public final String[] indirectTargets; - public final String dependencyAnalyzerMode; + public final String strictDepsMode; public final String unusedDependencyCheckerMode; public final String currentTarget; public final String statsfile; @@ -59,7 +59,7 @@ public CompileOptions(List args) { indirectJars = getCommaList(argMap, "IndirectJars"); indirectTargets = getCommaList(argMap, "IndirectTargets"); - dependencyAnalyzerMode = getOrElse(argMap, "DependencyAnalyzerMode", "off"); + strictDepsMode = getOrElse(argMap, "StrictDepsMode", "off"); unusedDependencyCheckerMode = getOrElse(argMap, "UnusedDependencyCheckerMode", "off"); currentTarget = getOrElse(argMap, "CurrentTarget", "NA"); diff --git a/src/java/io/bazel/rulesscala/scalac/ScalacProcessor.java b/src/java/io/bazel/rulesscala/scalac/ScalacProcessor.java index 76f06c06a..31a33a554 100644 --- a/src/java/io/bazel/rulesscala/scalac/ScalacProcessor.java +++ b/src/java/io/bazel/rulesscala/scalac/ScalacProcessor.java @@ -173,7 +173,7 @@ private static boolean isModeEnabled(String mode) { private static String[] getPluginParamsFrom(CompileOptions ops) { ArrayList pluginParams = new ArrayList<>(0); - if (isModeEnabled(ops.dependencyAnalyzerMode)) { + if (isModeEnabled(ops.strictDepsMode)) { String[] indirectTargets = encodeBazelTargets(ops.indirectTargets); String currentTarget = encodeBazelTarget(ops.currentTarget); @@ -181,8 +181,9 @@ private static String[] getPluginParamsFrom(CompileOptions ops) { "-P:dependency-analyzer:direct-jars:" + String.join(":", ops.directJars), "-P:dependency-analyzer:indirect-jars:" + String.join(":", ops.indirectJars), "-P:dependency-analyzer:indirect-targets:" + String.join(":", indirectTargets), - "-P:dependency-analyzer:mode:" + ops.dependencyAnalyzerMode, + "-P:dependency-analyzer:strict-deps-mode:" + ops.strictDepsMode, "-P:dependency-analyzer:current-target:" + currentTarget, + "-P:dependency-analyzer:dependency-tracking-method:" + "high-level", }; pluginParams.addAll(Arrays.asList(dependencyAnalyzerParams)); } else if (isModeEnabled(ops.unusedDependencyCheckerMode)) { @@ -191,11 +192,12 @@ private static String[] getPluginParamsFrom(CompileOptions ops) { String currentTarget = encodeBazelTarget(ops.currentTarget); String[] unusedDependencyCheckerParams = { - "-P:unused-dependency-checker:direct-jars:" + String.join(":", ops.directJars), - "-P:unused-dependency-checker:direct-targets:" + String.join(":", directTargets), - "-P:unused-dependency-checker:ignored-targets:" + String.join(":", ignoredTargets), - "-P:unused-dependency-checker:mode:" + ops.unusedDependencyCheckerMode, - "-P:unused-dependency-checker:current-target:" + currentTarget, + "-P:dependency-analyzer:direct-jars:" + String.join(":", ops.directJars), + "-P:dependency-analyzer:direct-targets:" + String.join(":", directTargets), + "-P:dependency-analyzer:unused-deps-ignored-targets:" + String.join(":", ignoredTargets), + "-P:dependency-analyzer:unused-deps-mode:" + ops.unusedDependencyCheckerMode, + "-P:dependency-analyzer:current-target:" + currentTarget, + "-P:dependency-analyzer:dependency-tracking-method:" + "high-level", }; pluginParams.addAll(Arrays.asList(unusedDependencyCheckerParams)); } diff --git a/third_party/dependency_analyzer/src/main/BUILD b/third_party/dependency_analyzer/src/main/BUILD index 40fb822a6..a5da0f019 100644 --- a/third_party/dependency_analyzer/src/main/BUILD +++ b/third_party/dependency_analyzer/src/main/BUILD @@ -6,6 +6,9 @@ scala_library_for_plugin_bootstrapping( name = "dependency_analyzer", srcs = [ "io/bazel/rulesscala/dependencyanalyzer/DependencyAnalyzer.scala", + "io/bazel/rulesscala/dependencyanalyzer/DependencyAnalyzerSettings.scala", + "io/bazel/rulesscala/dependencyanalyzer/HighLevelCrawlUsedJarFinder.scala", + "io/bazel/rulesscala/dependencyanalyzer/OptionsParser.scala", ], resources = ["resources/scalac-plugin.xml"], visibility = ["//visibility:public"], diff --git a/third_party/dependency_analyzer/src/main/io/bazel/rulesscala/dependencyanalyzer/DependencyAnalyzer.scala b/third_party/dependency_analyzer/src/main/io/bazel/rulesscala/dependencyanalyzer/DependencyAnalyzer.scala index b04724566..a74455cdb 100644 --- a/third_party/dependency_analyzer/src/main/io/bazel/rulesscala/dependencyanalyzer/DependencyAnalyzer.scala +++ b/third_party/dependency_analyzer/src/main/io/bazel/rulesscala/dependencyanalyzer/DependencyAnalyzer.scala @@ -1,47 +1,36 @@ package third_party.dependency_analyzer.src.main.io.bazel.rulesscala.dependencyanalyzer import scala.reflect.io.AbstractFile -import scala.tools.nsc.plugins.{Plugin, PluginComponent} -import scala.tools.nsc.{Global, Phase} +import scala.tools.nsc.plugins.Plugin +import scala.tools.nsc.plugins.PluginComponent +import scala.tools.nsc.Global +import scala.tools.nsc.Phase class DependencyAnalyzer(val global: Global) extends Plugin { - val name = "dependency-analyzer" - val description = - "Analyzes the used dependencies and fails the compilation " + - "if they are not explicitly used as direct dependencies (only declared transitively)" - val components = List[PluginComponent](Component) - - var indirect: Map[String, String] = Map.empty - var direct: Set[String] = Set.empty - var analyzerMode: String = "error" - var currentTarget: String = "NA" - - override def processOptions(options: List[String], error: (String) => Unit): Unit = { - var indirectJars: Seq[String] = Seq.empty - var indirectTargets: Seq[String] = Seq.empty - - for (option <- options) { - option.split(":").toList match { - case "direct-jars" :: data => direct = data.toSet - case "indirect-jars" :: data => indirectJars = data; - case "indirect-targets" :: data => indirectTargets = data.map(_.replace(";", ":")) - case "current-target" :: target => currentTarget = target.map(_.replace(";", ":")).head - case "mode" :: mode => analyzerMode = mode.head - case unknown :: _ => error(s"unknown param $unknown") - case Nil => - } - } - indirect = indirectJars.zip(indirectTargets).toMap + override val name = "dependency-analyzer" + override val description = + "Analyzes the used dependencies. Can check and warn or fail the " + + "compilation for issues including not directly including " + + "dependencies which are directly included in the code, or " + + "including unused dependencies." + override val components = List[PluginComponent](Component) + + private val isWindows: Boolean = System.getProperty("os.name").toLowerCase.contains("windows") + private var settings: DependencyAnalyzerSettings = null + + override def init( + options: List[String], + error: String => Unit + ): Boolean = { + settings = DependencyAnalyzerSettings.parseSettings(options = options, error = error) + true } - private object Component extends PluginComponent { val global: DependencyAnalyzer.this.global.type = DependencyAnalyzer.this.global - import global._ - override val runsAfter = List("jvm") val phaseName = DependencyAnalyzer.this.name @@ -52,59 +41,77 @@ class DependencyAnalyzer(val global: Global) extends Plugin { super.run() val usedJars = findUsedJars + val usedJarPaths = if (!isWindows) usedJars.map(_.path) else usedJars.map(_.path.replaceAll("\\\\", "/")) - warnOnIndirectTargetsFoundIn(usedJars) - } + if (settings.unusedDepsMode != AnalyzerMode.Off) { + reportUnusedDepsFoundIn(usedJarPaths) + } - private def warnOnIndirectTargetsFoundIn(usedJars: Set[AbstractFile]) = { - for (usedJar <- usedJars; - usedJarPath = usedJar.path; - target <- indirect.get(usedJarPath) if !direct.contains(usedJarPath)) { - val errorMessage = - s"""Target '$target' is used but isn't explicitly declared, please add it to the deps. - |You can use the following buildozer command: - |buildozer 'add deps $target' $currentTarget""".stripMargin - - analyzerMode match { - case "error" => reporter.error(NoPosition, errorMessage) - case "warn" => reporter.warning(NoPosition, errorMessage) - } + if (settings.strictDepsMode != AnalyzerMode.Off) { + reportIndirectTargetsFoundIn(usedJarPaths) } } - override def apply(unit: CompilationUnit): Unit = () + override def apply(unit: global.CompilationUnit): Unit = () } } - import global._ - - private def findUsedJars: Set[AbstractFile] = { - val jars = collection.mutable.Set[AbstractFile]() - - def walkTopLevels(root: Symbol): Unit = { - def safeInfo(sym: Symbol): Type = - if (sym.hasRawInfo && sym.rawInfo.isComplete) sym.info else NoType - - def packageClassOrSelf(sym: Symbol): Symbol = - if (sym.hasPackageFlag && !sym.isModuleClass) sym.moduleClass else sym - - for (x <- safeInfo(packageClassOrSelf(root)).decls) { - if (x == root) () - else if (x.hasPackageFlag) walkTopLevels(x) - else if (x.owner != root) { // exclude package class members - if (x.hasRawInfo && x.rawInfo.isComplete) { - val assocFile = x.associatedFile - if (assocFile.path.endsWith(".class") && assocFile.underlyingSource.isDefined) - assocFile.underlyingSource.foreach(jars += _) - } + private def reportIndirectTargetsFoundIn(usedJarPaths: Set[String]): Unit = { + val errors = + usedJarPaths + .filterNot(settings.directTargetSet.jarSet.contains) + .flatMap(settings.indirectTargetSet.targetFromJarOpt) + .map { target => + s"""Target '$target' is used but isn't explicitly declared, please add it to the deps. + |You can use the following buildozer command: + |buildozer 'add deps $target' ${settings.currentTarget}""".stripMargin } + + warnOrError(settings.strictDepsMode, errors) + } + + private def reportUnusedDepsFoundIn(usedJarPaths: Set[String]): Unit = { + val directJarPaths = settings.directTargetSet.jarSet + + val usedTargets = + usedJarPaths.flatMap(settings.directTargetSet.targetFromJarOpt) + + val unusedTargets = directJarPaths + // This .get is safe because [jar] was gotten from [directJarPaths] + // which is the set of keys of the direct targets. + .filter(jar => !usedTargets.contains(settings.directTargetSet.targetFromJarOpt(jar).get)) + .flatMap(settings.directTargetSet.targetFromJarOpt) + .diff(settings.ignoredUnusedDependencyTargets) + + val toWarnOrError = + unusedTargets.map { target => + s"""Target '$target' is specified as a dependency to ${settings.currentTarget} but isn't used, please remove it from the deps. + |You can use the following buildozer command: + |buildozer 'remove deps $target' ${settings.currentTarget} + |""".stripMargin } + + warnOrError(settings.unusedDepsMode, toWarnOrError) + } + + private def warnOrError( + analyzerMode: AnalyzerMode, + errors: Set[String] + ): Unit = { + val reportFunction: String => Unit = analyzerMode match { + case AnalyzerMode.Error => global.reporter.error(global.NoPosition, _) + case AnalyzerMode.Warn => global.reporter.warning(global.NoPosition, _) + case AnalyzerMode.Off => _ => () } - exitingTyper { - walkTopLevels(RootClass) + errors.foreach(reportFunction) + } + + private def findUsedJars: Set[AbstractFile] = { + settings.dependencyTrackingMethod match { + case DependencyTrackingMethod.HighLevel => + new HighLevelCrawlUsedJarFinder(global).findUsedJars } - jars.toSet } } diff --git a/third_party/dependency_analyzer/src/main/io/bazel/rulesscala/dependencyanalyzer/DependencyAnalyzerSettings.scala b/third_party/dependency_analyzer/src/main/io/bazel/rulesscala/dependencyanalyzer/DependencyAnalyzerSettings.scala new file mode 100644 index 000000000..f6f87ca33 --- /dev/null +++ b/third_party/dependency_analyzer/src/main/io/bazel/rulesscala/dependencyanalyzer/DependencyAnalyzerSettings.scala @@ -0,0 +1,119 @@ +package third_party.dependency_analyzer.src.main.io.bazel.rulesscala.dependencyanalyzer + +object AnalyzerMode { + case object Error extends AnalyzerMode + case object Warn extends AnalyzerMode + case object Off extends AnalyzerMode + + def parse(mode: String): Option[AnalyzerMode] = { + mode match { + case "error" => Some(Error) + case "warn" => Some(Warn) + case "off" => Some(Off) + case _ => None + } + } +} + +sealed trait AnalyzerMode + +object DependencyTrackingMethod { + case object HighLevel extends DependencyTrackingMethod + + def parse(mode: String): Option[DependencyTrackingMethod] = { + mode match { + case "high-level" => Some(HighLevel) + case _ => None + } + } +} + +sealed trait DependencyTrackingMethod + +class TargetSet( + prefix: String, + jarsSeq: Seq[String], + targetsSeq: Seq[String] +) { + private lazy val jarToTargetMap: Map[String, String] = { + require(targetsSeq.size == jarsSeq.size, s"Arguments $prefix-jars and $prefix-targets had mismatched size") + + jarsSeq.zip(targetsSeq).toMap + } + + def targetFromJarOpt(jar: String): Option[String] = { + jarToTargetMap.get(jar) + } + + lazy val jarSet: Set[String] = { + jarsSeq.toSet + } +} + +object DependencyAnalyzerSettings { + def parseSettings( + options: List[String], + error: String => Unit + ): DependencyAnalyzerSettings = { + + val optionsParser = OptionsParser.create(options, error) + + def decodeTarget(target: String): String = { + target.replace(";", ":") + } + + def parseTargetSet(prefix: String): TargetSet = { + new TargetSet( + prefix = prefix, + jarsSeq = optionsParser.takeStringSeqOpt(s"$prefix-jars").getOrElse(Seq.empty), + targetsSeq = optionsParser.takeStringSeqOpt(s"$prefix-targets").map(_.map(decodeTarget)).getOrElse(Seq.empty) + ) + } + + def extractAnalyzerMode(key: String): AnalyzerMode = { + optionsParser + .takeStringOpt(key) + .map { str => + AnalyzerMode.parse(str).getOrElse { + error(s"Failed to parse option $key") + AnalyzerMode.Error + } + } + .getOrElse(AnalyzerMode.Off) + } + + val settings = + DependencyAnalyzerSettings( + currentTarget = decodeTarget(optionsParser.takeString("current-target")), + dependencyTrackingMethod = + DependencyTrackingMethod + .parse(optionsParser.takeString("dependency-tracking-method")) + .getOrElse { + error("Failed to parse option dependency-tracking-method") + DependencyTrackingMethod.HighLevel + }, + indirectTargetSet = parseTargetSet("indirect"), + directTargetSet = parseTargetSet("direct"), + unusedDepsMode = extractAnalyzerMode("unused-deps-mode"), + strictDepsMode = extractAnalyzerMode("strict-deps-mode"), + ignoredUnusedDependencyTargets = + optionsParser + .takeStringSeqOpt(s"unused-deps-ignored-targets") + .getOrElse(Seq.empty) + .map(decodeTarget) + .toSet + ) + optionsParser.failOnUnparsedOptions() + settings + } +} + +case class DependencyAnalyzerSettings( + indirectTargetSet: TargetSet, + directTargetSet: TargetSet, + currentTarget: String, + dependencyTrackingMethod: DependencyTrackingMethod, + unusedDepsMode: AnalyzerMode, + strictDepsMode: AnalyzerMode, + ignoredUnusedDependencyTargets: Set[String] +) diff --git a/third_party/dependency_analyzer/src/main/io/bazel/rulesscala/dependencyanalyzer/HighLevelCrawlUsedJarFinder.scala b/third_party/dependency_analyzer/src/main/io/bazel/rulesscala/dependencyanalyzer/HighLevelCrawlUsedJarFinder.scala new file mode 100644 index 000000000..cbb20c550 --- /dev/null +++ b/third_party/dependency_analyzer/src/main/io/bazel/rulesscala/dependencyanalyzer/HighLevelCrawlUsedJarFinder.scala @@ -0,0 +1,39 @@ +package third_party.dependency_analyzer.src.main.io.bazel.rulesscala.dependencyanalyzer + +import scala.reflect.io.AbstractFile +import scala.tools.nsc.Global + +class HighLevelCrawlUsedJarFinder( + global: Global +) { + import global.Symbol + + def findUsedJars: Set[AbstractFile] = { + val jars = collection.mutable.Set[AbstractFile]() + + global.exitingTyper { + walkTopLevels(global.RootClass, jars) + } + jars.toSet + } + + private def walkTopLevels(root: Symbol, jars: collection.mutable.Set[AbstractFile]): Unit = { + def safeInfo(sym: Symbol): global.Type = + if (sym.hasRawInfo && sym.rawInfo.isComplete) sym.info else global.NoType + + def packageClassOrSelf(sym: Symbol): Symbol = + if (sym.hasPackageFlag && !sym.isModuleClass) sym.moduleClass else sym + + for (x <- safeInfo(packageClassOrSelf(root)).decls) { + if (x == root) () + else if (x.hasPackageFlag) walkTopLevels(x, jars) + else if (x.owner != root) { // exclude package class members + if (x.hasRawInfo && x.rawInfo.isComplete) { + val assocFile = x.associatedFile + if (assocFile.path.endsWith(".class") && assocFile.underlyingSource.isDefined) + assocFile.underlyingSource.foreach(jars += _) + } + } + } + } +} diff --git a/third_party/dependency_analyzer/src/main/io/bazel/rulesscala/dependencyanalyzer/OptionsParser.scala b/third_party/dependency_analyzer/src/main/io/bazel/rulesscala/dependencyanalyzer/OptionsParser.scala new file mode 100644 index 000000000..f36c93ca1 --- /dev/null +++ b/third_party/dependency_analyzer/src/main/io/bazel/rulesscala/dependencyanalyzer/OptionsParser.scala @@ -0,0 +1,51 @@ +package third_party.dependency_analyzer.src.main.io.bazel.rulesscala.dependencyanalyzer + +import scala.collection.mutable + +object OptionsParser { + def create( + options: List[String], + error: String => Unit + ): OptionsParser = { + val optionsMap = mutable.Map[String, String]() + options.foreach { option => + option.split(":", 2) match { + case Array(key) => + error(s"Argument $key missing value") + case Array(key, value) => + if (optionsMap.contains(key)) { + error(s"Argument $key found multiple times") + } + optionsMap.put(key, value) + } + } + + new OptionsParser(error = error, options = optionsMap) + } +} + +class OptionsParser private( + error: String => Unit, + options: mutable.Map[String, String] +) { + def failOnUnparsedOptions(): Unit = { + options.keys.foreach { key => + error(s"Unrecognized option $key") + } + } + + def takeStringOpt(key: String): Option[String] = { + options.remove(key) + } + + def takeString(key: String): String = { + takeStringOpt(key).getOrElse { + error(s"Missing required option $key") + "NA" + } + } + + def takeStringSeqOpt(key: String): Option[Seq[String]] = { + takeStringOpt(key).map(_.split(":")) + } +} diff --git a/third_party/dependency_analyzer/src/test/BUILD b/third_party/dependency_analyzer/src/test/BUILD index 0b279289b..6428762b4 100644 --- a/third_party/dependency_analyzer/src/test/BUILD +++ b/third_party/dependency_analyzer/src/test/BUILD @@ -2,11 +2,11 @@ licenses(["notice"]) # 3-clause BSD load("//scala:scala.bzl", "scala_junit_test", "scala_test") -scala_junit_test( - name = "dependency_analyzer_test", +scala_test( + name = "strict_deps_test", size = "small", srcs = [ - "io/bazel/rulesscala/dependencyanalyzer/DependencyAnalyzerTest.scala", + "io/bazel/rulesscala/dependencyanalyzer/StrictDepsTest.scala", ], jvm_flags = [ "-Dplugin.jar.location=$(location //third_party/dependency_analyzer/src/main:dependency_analyzer)", @@ -14,7 +14,6 @@ scala_junit_test( "-Dguava.jar.location=$(location @com_google_guava_guava_21_0_with_file//jar)", "-Dapache.commons.jar.location=$(location @org_apache_commons_commons_lang_3_5_without_file//:linkable_org_apache_commons_commons_lang_3_5_without_file)", ], - suffixes = ["Test"], unused_dependency_checker_mode = "off", deps = [ "//external:io_bazel_rules_scala/dependency/scala/scala_compiler", @@ -26,3 +25,25 @@ scala_junit_test( "@org_apache_commons_commons_lang_3_5_without_file//:linkable_org_apache_commons_commons_lang_3_5_without_file", ], ) + +scala_test( + name = "unused_dependency_checker_test", + size = "small", + srcs = [ + "io/bazel/rulesscala/dependencyanalyzer/UnusedDependencyCheckerTest.scala", + ], + jvm_flags = [ + "-Dplugin.jar.location=$(location //third_party/dependency_analyzer/src/main:dependency_analyzer)", + "-Dscala.library.location=$(location //external:io_bazel_rules_scala/dependency/scala/scala_library)", + "-Dapache.commons.jar.location=$(location @org_apache_commons_commons_lang_3_5_without_file//:linkable_org_apache_commons_commons_lang_3_5_without_file)", + ], + unused_dependency_checker_mode = "off", + deps = [ + "//external:io_bazel_rules_scala/dependency/scala/scala_compiler", + "//external:io_bazel_rules_scala/dependency/scala/scala_library", + "//external:io_bazel_rules_scala/dependency/scala/scala_reflect", + "//third_party/dependency_analyzer/src/main:dependency_analyzer", + "//third_party/utils/src/test:test_util", + "@org_apache_commons_commons_lang_3_5_without_file//:linkable_org_apache_commons_commons_lang_3_5_without_file", + ], +) diff --git a/third_party/dependency_analyzer/src/test/io/bazel/rulesscala/dependencyanalyzer/DependencyAnalyzerTest.scala b/third_party/dependency_analyzer/src/test/io/bazel/rulesscala/dependencyanalyzer/StrictDepsTest.scala similarity index 89% rename from third_party/dependency_analyzer/src/test/io/bazel/rulesscala/dependencyanalyzer/DependencyAnalyzerTest.scala rename to third_party/dependency_analyzer/src/test/io/bazel/rulesscala/dependencyanalyzer/StrictDepsTest.scala index 7ec3d70df..fdbcc727f 100644 --- a/third_party/dependency_analyzer/src/test/io/bazel/rulesscala/dependencyanalyzer/DependencyAnalyzerTest.scala +++ b/third_party/dependency_analyzer/src/test/io/bazel/rulesscala/dependencyanalyzer/StrictDepsTest.scala @@ -1,14 +1,10 @@ package third_party.dependency_analyzer.src.test.io.bazel.rulesscala.dependencyanalyzer -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 +import org.scalatest._ import java.nio.file.Paths - import third_party.utils.src.test.io.bazel.rulesscala.utils.TestUtil._ -@RunWith(classOf[JUnit4]) -class DependencyAnalyzerTest { +class StrictDepsTest extends FunSuite { val pluginName = "dependency_analyzer" def compileWithDependencyAnalyzer(code: String, withDirect: List[String] = Nil, withIndirect: List[(String, String)] = Nil): List[String] = { @@ -26,7 +22,9 @@ class DependencyAnalyzerTest { constructParam("direct-jars", withDirect), constructParam("indirect-jars", withIndirect.map(_._1)), constructParam("indirect-targets", withIndirect.map(_._2)), - constructParam("current-target", Seq(defaultTarget)) + constructParam("current-target", Seq(defaultTarget)), + constructParam("strict-deps-mode", Seq("error")), + constructParam("dependency-tracking-method", Seq("high-level")) ).mkString(" ") val extraClasspath = withDirect ++ withIndirect.map(_._1) @@ -34,9 +32,7 @@ class DependencyAnalyzerTest { runCompiler(code, compileOptions, extraClasspath, toolboxPluginOptions) } - - @Test - def `error on indirect dependency target`(): Unit = { + test("error on indirect dependency target") { val testCode = """object Foo { | org.apache.commons.lang3.ArrayUtils.EMPTY_BOOLEAN_ARRAY.length @@ -49,8 +45,7 @@ class DependencyAnalyzerTest { compileWithDependencyAnalyzer(testCode, withIndirect = indirect).expectErrorOn(commonsTarget) } - @Test - def `error on multiple indirect dependency targets`(): Unit = { + test("error on multiple indirect dependency targets") { val testCode = """object Foo { | org.apache.commons.lang3.ArrayUtils.EMPTY_BOOLEAN_ARRAY.length @@ -65,8 +60,7 @@ class DependencyAnalyzerTest { compileWithDependencyAnalyzer(testCode, withIndirect = indirect).expectErrorOn(commonsTarget, guavaTarget) } - @Test - def `do not give error on direct dependency target`(): Unit = { + test("do not give error on direct dependency target") { val testCode = """object Foo { | org.apache.commons.lang3.ArrayUtils.EMPTY_BOOLEAN_ARRAY.length diff --git a/third_party/unused_dependency_checker/src/test/io/bazel/rulesscala/dependencyanalyzer/UnusedDependencyCheckerTest.scala b/third_party/dependency_analyzer/src/test/io/bazel/rulesscala/dependencyanalyzer/UnusedDependencyCheckerTest.scala similarity index 84% rename from third_party/unused_dependency_checker/src/test/io/bazel/rulesscala/dependencyanalyzer/UnusedDependencyCheckerTest.scala rename to third_party/dependency_analyzer/src/test/io/bazel/rulesscala/dependencyanalyzer/UnusedDependencyCheckerTest.scala index 3b928d983..74b4dd2e4 100644 --- a/third_party/unused_dependency_checker/src/test/io/bazel/rulesscala/dependencyanalyzer/UnusedDependencyCheckerTest.scala +++ b/third_party/dependency_analyzer/src/test/io/bazel/rulesscala/dependencyanalyzer/UnusedDependencyCheckerTest.scala @@ -1,7 +1,6 @@ -package third_party.unused_dependency_checker.src.test.io.bazel.rulesscala.dependencyanalyzer +package third_party.dependency_analyzer.src.test.io.bazel.rulesscala.dependencyanalyzer import java.nio.file.Paths - import org.scalatest._ import third_party.utils.src.test.io.bazel.rulesscala.utils.TestUtil._ @@ -9,18 +8,20 @@ class UnusedDependencyCheckerTest extends FunSuite { def compileWithUnusedDependencyChecker(code: String, withDirect: List[(String, String)] = Nil): List[String] = { val toolboxPluginOptions: String = { val jar = System.getProperty("plugin.jar.location") - val start = jar.indexOf("/third_party/unused_dependency_checker") + val start = jar.indexOf("/third_party/dependency_analyzer") // this substring is needed due to issue: https://github.com/bazelbuild/bazel/issues/2475 val jarInRelationToBaseDir = jar.substring(start, jar.length) val pluginPath = Paths.get(baseDir, jarInRelationToBaseDir).toAbsolutePath s"-Xplugin:$pluginPath -Jdummy=${pluginPath.toFile.lastModified}" } - val constructParam: (String, Iterable[String]) => String = constructPluginParam("unused-dependency-checker") + val constructParam: (String, Iterable[String]) => String = constructPluginParam("dependency-analyzer") val compileOptions = List( constructParam("direct-jars", withDirect.map(_._1)), constructParam("direct-targets", withDirect.map(_._2)), - constructParam("current-target", Seq(defaultTarget)) + constructParam("current-target", Seq(defaultTarget)), + constructParam("dependency-tracking-method", Seq("high-level")), + constructParam("unused-deps-mode", Seq("error")) ).mkString(" ") val extraClasspath = withDirect.map(_._1) diff --git a/third_party/unused_dependency_checker/LICENSE b/third_party/unused_dependency_checker/LICENSE deleted file mode 100644 index ec832b9a1..000000000 --- a/third_party/unused_dependency_checker/LICENSE +++ /dev/null @@ -1,29 +0,0 @@ -******************************************************************************* -* Classpath Shrinker: a scalac plugin to detect unused classpath entries -* Copyright (c) Scala Center -* All rights reserved. -* -* Redistribution and use in source and binary forms, with or without -* modification, are permitted provided that the following conditions -* are met: -* 1. Redistributions of source code must retain the above copyright -* notice, this list of conditions and the following disclaimer. -* 2. Redistributions in binary form must reproduce the above copyright -* notice, this list of conditions and the following disclaimer in the -* documentation and/or other materials provided with the distribution. -* 3. Neither the name of the copyright holders nor the names of its -* contributors may be used to endorse or promote products derived from -* this software without specific prior written permission. -* -* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF -* THE POSSIBILITY OF SUCH DAMAGE. -******************************************************************************* \ No newline at end of file diff --git a/third_party/unused_dependency_checker/src/main/BUILD b/third_party/unused_dependency_checker/src/main/BUILD deleted file mode 100644 index d28702892..000000000 --- a/third_party/unused_dependency_checker/src/main/BUILD +++ /dev/null @@ -1,16 +0,0 @@ -licenses(["notice"]) # 3-clause BSD - -load("//scala:scala.bzl", "scala_library_for_plugin_bootstrapping") - -scala_library_for_plugin_bootstrapping( - name = "unused_dependency_checker", - srcs = [ - "io/bazel/rulesscala/unuseddependencychecker/UnusedDependencyChecker.scala", - ], - resources = ["resources/scalac-plugin.xml"], - visibility = ["//visibility:public"], - deps = [ - "//external:io_bazel_rules_scala/dependency/scala/scala_compiler", - "//external:io_bazel_rules_scala/dependency/scala/scala_reflect", - ], -) diff --git a/third_party/unused_dependency_checker/src/main/io/bazel/rulesscala/unuseddependencychecker/UnusedDependencyChecker.scala b/third_party/unused_dependency_checker/src/main/io/bazel/rulesscala/unuseddependencychecker/UnusedDependencyChecker.scala deleted file mode 100644 index 469db9093..000000000 --- a/third_party/unused_dependency_checker/src/main/io/bazel/rulesscala/unuseddependencychecker/UnusedDependencyChecker.scala +++ /dev/null @@ -1,144 +0,0 @@ -package third_party.unused_dependency_checker.src.main.io.bazel.rulesscala.unused_dependency_checker - -import scala.reflect.io.AbstractFile -import scala.tools.nsc.plugins.{Plugin, PluginComponent} -import scala.tools.nsc.{Global, Phase} -import UnusedDependencyChecker._ - -class UnusedDependencyChecker(val global: Global) extends Plugin { self => - val name = "unused-dependency-checker" - val description = "Errors if there exists dependencies that are not used" - - val components: List[PluginComponent] = List[PluginComponent](Component) - - var direct: Map[String, String] = Map.empty - var ignoredTargets: Set[String] = Set.empty - var analyzerMode: AnalyzerMode = Error - var currentTarget: String = "NA" - - val isWindows: Boolean = System.getProperty("os.name").toLowerCase.contains("windows") - - override def init(options: List[String], error: (String) => Unit): Boolean = { - var directJars: Seq[String] = Seq.empty - var directTargets: Seq[String] = Seq.empty - - for (option <- options) { - option.split(":").toList match { - case "direct-jars" :: data => directJars = data.map(decodeTarget) - case "direct-targets" :: data => directTargets = data.map(decodeTarget) - case "ignored-targets" :: data => ignoredTargets = data.map(decodeTarget).toSet - case "current-target" :: target :: _ => currentTarget = decodeTarget(target) - case "mode" :: mode :: _ => parseAnalyzerMode(mode).foreach(analyzerMode = _) - case unknown :: _ => error(s"unknown param $unknown") - case Nil => - } - } - - direct = directJars.zip(directTargets).toMap - - true - } - - - private object Component extends PluginComponent { - val global: Global = self.global - - import global._ - - override val runsAfter = List("jvm") - - val phaseName: String = self.name - - private def warnOrError(messages: Set[String]): Unit = { - val reportFunction: String => Unit = analyzerMode match { - case Error => reporter.error(NoPosition, _) - case Warn => reporter.warning(NoPosition, _) - } - - messages.foreach(reportFunction) - } - - override def newPhase(prev: Phase): StdPhase = new StdPhase(prev) { - override def run(): Unit = { - super.run() - - warnOrError(unusedDependenciesFound) - } - - private def unusedDependenciesFound: Set[String] = { - val usedJars: Set[AbstractFile] = findUsedJars - val directJarPaths = direct.keys.toSet - val usedJarPaths = if (!isWindows) usedJars.map(_.path) else usedJars.map(_.path.replaceAll("\\\\", "/")) - - val usedTargets = usedJarPaths - .map(direct.get) - .collect { - case Some(target) => target - } - - val unusedTargets = directJarPaths - .filter(jar => !usedTargets.contains(direct(jar))) - .map(direct.get) - .collect { - case Some(target) if !ignoredTargets.contains(target) => target - } - - unusedTargets.map { target => - s"""Target '$target' is specified as a dependency to $currentTarget but isn't used, please remove it from the deps. - |You can use the following buildozer command: - |buildozer 'remove deps $target' $currentTarget - |""".stripMargin - } - } - - override def apply(unit: CompilationUnit): Unit = () - } - - def findUsedJars: Set[AbstractFile] = { - val jars = collection.mutable.Set[AbstractFile]() - - def walkTopLevels(root: Symbol): Unit = { - def safeInfo(sym: Symbol): Type = - if (sym.hasRawInfo && sym.rawInfo.isComplete) sym.info else NoType - - def packageClassOrSelf(sym: Symbol): Symbol = - if (sym.hasPackageFlag && !sym.isModuleClass) sym.moduleClass else sym - - for (x <- safeInfo(packageClassOrSelf(root)).decls) { - if (x == root) () - else if (x.hasPackageFlag) walkTopLevels(x) - else if (x.owner != root) { // exclude package class members - if (x.hasRawInfo && x.rawInfo.isComplete) { - val assocFile = x.associatedFile - if (assocFile.path.endsWith(".class") && assocFile.underlyingSource.isDefined) - assocFile.underlyingSource.foreach(jars += _) - } - } - } - } - - exitingTyper { - walkTopLevels(RootClass) - } - jars.toSet - } - } - -} - -object UnusedDependencyChecker { - - sealed trait AnalyzerMode - - case object Error extends AnalyzerMode - - case object Warn extends AnalyzerMode - - def parseAnalyzerMode(mode: String): Option[AnalyzerMode] = mode match { - case "error" => Some(Error) - case "warn" => Some(Warn) - case _ => None - } - - def decodeTarget(target: String): String = target.replace(";", ":") -} diff --git a/third_party/unused_dependency_checker/src/main/resources/scalac-plugin.xml b/third_party/unused_dependency_checker/src/main/resources/scalac-plugin.xml deleted file mode 100644 index ae5ddca8f..000000000 --- a/third_party/unused_dependency_checker/src/main/resources/scalac-plugin.xml +++ /dev/null @@ -1,4 +0,0 @@ - - unused-dependency-checker - third_party.unused_dependency_checker.src.main.io.bazel.rulesscala.unused_dependency_checker.UnusedDependencyChecker - diff --git a/third_party/unused_dependency_checker/src/test/BUILD b/third_party/unused_dependency_checker/src/test/BUILD deleted file mode 100644 index f8574859f..000000000 --- a/third_party/unused_dependency_checker/src/test/BUILD +++ /dev/null @@ -1,25 +0,0 @@ -licenses(["notice"]) # 3-clause BSD - -load("//scala:scala.bzl", "scala_junit_test", "scala_test") - -scala_test( - name = "unused_dependency_checker_test", - size = "small", - srcs = [ - "io/bazel/rulesscala/dependencyanalyzer/UnusedDependencyCheckerTest.scala", - ], - jvm_flags = [ - "-Dplugin.jar.location=$(location //third_party/unused_dependency_checker/src/main:unused_dependency_checker)", - "-Dscala.library.location=$(location //external:io_bazel_rules_scala/dependency/scala/scala_library)", - "-Dapache.commons.jar.location=$(location @org_apache_commons_commons_lang_3_5_without_file//:linkable_org_apache_commons_commons_lang_3_5_without_file)", - ], - unused_dependency_checker_mode = "off", - deps = [ - "//external:io_bazel_rules_scala/dependency/scala/scala_compiler", - "//external:io_bazel_rules_scala/dependency/scala/scala_library", - "//external:io_bazel_rules_scala/dependency/scala/scala_reflect", - "//third_party/unused_dependency_checker/src/main:unused_dependency_checker", - "//third_party/utils/src/test:test_util", - "@org_apache_commons_commons_lang_3_5_without_file//:linkable_org_apache_commons_commons_lang_3_5_without_file", - ], -)