Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge dependency_analyzer and unused_dependency_checker #954

Merged
merged 2 commits into from
Jan 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions scala/private/common_attributes.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 7 additions & 7 deletions scala/private/rule_impls.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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(
Expand All @@ -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,
)
Expand Down
4 changes: 2 additions & 2 deletions src/java/io/bazel/rulesscala/scalac/CompileOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -59,7 +59,7 @@ public CompileOptions(List<String> 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");

Expand Down
16 changes: 9 additions & 7 deletions src/java/io/bazel/rulesscala/scalac/ScalacProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -173,16 +173,17 @@ private static boolean isModeEnabled(String mode) {
private static String[] getPluginParamsFrom(CompileOptions ops) {
ArrayList<String> pluginParams = new ArrayList<>(0);

if (isModeEnabled(ops.dependencyAnalyzerMode)) {
if (isModeEnabled(ops.strictDepsMode)) {
String[] indirectTargets = encodeBazelTargets(ops.indirectTargets);
String currentTarget = encodeBazelTarget(ops.currentTarget);

String[] dependencyAnalyzerParams = {
"-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)) {
Expand All @@ -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));
}
Expand Down
3 changes: 3 additions & 0 deletions third_party/dependency_analyzer/src/main/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
}
}
Loading