Skip to content

Commit

Permalink
Add target filters (#46)
Browse files Browse the repository at this point in the history
Support new targetSourcePackages/targetDestinationPackages settings in missinglink
  • Loading branch information
clairemcginty authored Dec 5, 2022
1 parent 2f855f9 commit d6b9520
Show file tree
Hide file tree
Showing 12 changed files with 174 additions and 15 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ lazy val `sbt-missinglink` = project
.enablePlugins(SbtPlugin)
.settings(
libraryDependencies ++= Seq(
"com.spotify" % "missinglink-core" % "0.2.5"
"com.spotify" % "missinglink-core" % "0.2.6"
),
// configuration fro scripted
scriptedLaunchOpts := {
Expand Down
90 changes: 76 additions & 14 deletions src/main/scala/ch/epfl/scala/sbtmissinglink/MissingLinkPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ object MissingLinkPlugin extends AutoPlugin {
object autoImport {

final case class IgnoredPackage(name: String, ignoreSubpackages: Boolean = true)
extends PackageFilter {
override def apply(packageName: String): Boolean =
packageName != name && (!ignoreSubpackages || !packageName.startsWith(s"$name."))
}

final case class TargetedPackage(name: String, targetSubpackages: Boolean = true)
extends PackageFilter {
override def apply(packageName: String): Boolean =
packageName == name || (targetSubpackages && packageName.startsWith(s"$name."))
}

val missinglinkCheck: TaskKey[Unit] =
taskKey[Unit]("Run the missinglink checks")
Expand All @@ -38,12 +48,24 @@ object MissingLinkPlugin extends AutoPlugin {
"is in one of the specified packages."
)

val missinglinkTargetSourcePackages: SettingKey[Seq[TargetedPackage]] =
settingKey[Seq[TargetedPackage]](
"Optional list of source packages to specifically target conflicts in. " +
"Cannot be used with missinglinkIgnoreSourcePackages."
)

val missinglinkIgnoreDestinationPackages: SettingKey[Seq[IgnoredPackage]] =
settingKey[Seq[IgnoredPackage]](
"Optional list of packages to ignore conflicts where the destination/called-side " +
"of the conflict is in one of the specified packages."
)

val missinglinkTargetDestinationPackages: SettingKey[Seq[TargetedPackage]] =
settingKey[Seq[TargetedPackage]](
"Optional list of source packages to specifically target conflicts in. " +
"Cannot be used with missinglinkIgnoreDestinationPackages."
)

val missinglinkExcludedDependencies =
settingKey[Seq[ModuleFilter]]("Dependencies that are excluded from analysis")
}
Expand All @@ -66,12 +88,30 @@ object MissingLinkPlugin extends AutoPlugin {
val failOnConflicts = missinglinkFailOnConflicts.value
val ignoreSourcePackages = missinglinkIgnoreSourcePackages.value
val ignoreDestinationPackages = missinglinkIgnoreDestinationPackages.value
val targetSourcePackages = missinglinkTargetSourcePackages.value
val targetDestinationPackages = missinglinkTargetDestinationPackages.value

assert(
ignoreSourcePackages.isEmpty || targetSourcePackages.isEmpty,
"ignoreSourcePackages and targetSourcePackages cannot be defined in the same project."
)

assert(
ignoreDestinationPackages.isEmpty || targetDestinationPackages.isEmpty,
"ignoreDestinationPackages and targetDestinationPackages cannot be defined in the same project."
)

val filter =
missinglinkExcludedDependencies.value.foldLeft[ModuleFilter](_ => true)((k, v) => k - v)

val conflicts = loadArtifactsAndCheckConflicts(cp, classDir, filter, log)
val filteredConflicts =
filterConflicts(conflicts, ignoreSourcePackages, ignoreDestinationPackages, log)
filterConflicts(
conflicts,
ignoreSourcePackages ++ targetSourcePackages,
ignoreDestinationPackages ++ targetDestinationPackages,
log
)

if (filteredConflicts.nonEmpty) {
val initialTotal = conflicts.length
Expand Down Expand Up @@ -100,7 +140,9 @@ object MissingLinkPlugin extends AutoPlugin {
override def globalSettings: Seq[Def.Setting[_]] = Seq(
missinglinkFailOnConflicts := true,
missinglinkIgnoreSourcePackages := Nil,
missinglinkTargetSourcePackages := Nil,
missinglinkIgnoreDestinationPackages := Nil,
missinglinkTargetDestinationPackages := Nil,
missinglinkExcludedDependencies := Nil,
)

Expand Down Expand Up @@ -212,32 +254,29 @@ object MissingLinkPlugin extends AutoPlugin {

private def filterConflicts(
conflicts: Seq[Conflict],
ignoreSourcePackages: Seq[IgnoredPackage],
ignoreDestinationPackages: Seq[IgnoredPackage],
sourceFilters: Seq[PackageFilter],
destinationFilters: Seq[PackageFilter],
log: Logger
): Seq[Conflict] = {

def filter(
ignoredPackages: Seq[IgnoredPackage],
packageFilters: Seq[PackageFilter],
name: String,
setting: String,
field: Dependency => ClassTypeDescriptor
): Seq[Conflict] => Seq[Conflict] = { input =>
if (ignoredPackages.nonEmpty) {
log.debug(s"Ignoring $name packages: ${ignoredPackages.mkString(", ")}")
if (packageFilters.nonEmpty) {
log.debug(s"Applying filters on $name packages: ${packageFilters.mkString(", ")}")

def isIgnored(conflict: Conflict): Boolean = {
def isFiltered(conflict: Conflict): Boolean = {
val descriptor = field(conflict.dependency())
val className = descriptor.getClassName.replace('/', '.')
val conflictPackageName = className.substring(0, className.lastIndexOf('.'))

ignoredPackages.exists { p =>
conflictPackageName == p.name ||
(p.ignoreSubpackages && conflictPackageName.startsWith(p.name + "."))
}
packageFilters.exists(_.apply(conflictPackageName))
}

val filtered = input.filterNot(isIgnored)
val filtered = input.filter(isFiltered)
val diff = input.length - filtered.length

if (diff != 0) {
Expand All @@ -256,8 +295,30 @@ object MissingLinkPlugin extends AutoPlugin {
}

val filters = List(
filter(ignoreSourcePackages, "source", "ignoreSourcePackages", _.fromClass),
filter(ignoreDestinationPackages, "destination", "ignoreDestinationPackages", _.targetClass)
filter(
sourceFilters.collect { case p: IgnoredPackage => p },
"source",
"ignoreSourcePackages",
_.fromClass
),
filter(
sourceFilters.collect { case p: TargetedPackage => p },
"source",
"targetSourcePackages",
_.fromClass
),
filter(
destinationFilters.collect { case p: IgnoredPackage => p },
"destination",
"ignoreSourcePackages",
_.targetClass
),
filter(
destinationFilters.collect { case p: TargetedPackage => p },
"destination",
"targetDestinationPackages",
_.targetClass
)
)

Function.chain(filters).apply(conflicts)
Expand Down Expand Up @@ -317,4 +378,5 @@ object MissingLinkPlugin extends AutoPlugin {

private final case class ModuleArtifact(artifact: Artifact, module: Option[ModuleID])

private[sbtmissinglink] sealed trait PackageFilter extends (String => Boolean)
}
19 changes: 19 additions & 0 deletions src/sbt-test/missinglink/target-destination-package/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
inThisBuild(Def.settings(
version := "0.1.0",
scalaVersion := "2.12.8",
))

lazy val `target-destination-package` = project
.in(file("."))
.settings(
libraryDependencies ++= Seq(
"com.google.guava" % "guava" % "14.0",
"com.google.guava" % "guava" % "18.0" % Runtime,
),

// Speed up compilation a bit. Our .java files do not need to see the .scala files.
compileOrder := CompileOrder.JavaThenScala,

// Will ignore Guava conflict
missinglinkTargetDestinationPackages += TargetedPackage("test")
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
System.getProperty("plugin.version") match {
case null =>
throw new MessageOnlyException(
"The system property 'plugin.version' is not defined. " +
"Specify this property using the scriptedLaunchOpts -D.")
case pluginVersion =>
addSbtPlugin("ch.epfl.scala" % "sbt-missinglink" % pluginVersion)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package test;

public enum Foo {
BAR
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package test

import com.google.common.base.Enums

/**
* Calls a method in the Guava Enums class which was removed in guava 18. If a project calls this
* method while overriding Guava to >= 18, it will cause a NoSuchMethodError at runtime.
*/
object ProblematicDependency {

def reliesOnRemovedMethod(): AnyRef = {
Enums.valueOfFunction(classOf[Foo])
}
}
3 changes: 3 additions & 0 deletions src/sbt-test/missinglink/target-destination-package/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
> compile
> compile:missinglinkCheck
> runtime:missinglinkCheck
18 changes: 18 additions & 0 deletions src/sbt-test/missinglink/target-source-package/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
inThisBuild(Def.settings(
version := "0.1.0",
scalaVersion := "2.12.8",
))

lazy val `target-source-package` = project
.in(file("."))
.settings(
libraryDependencies ++= Seq(
"com.google.guava" % "guava" % "14.0",
"com.google.guava" % "guava" % "18.0" % Runtime,
),

// Speed up compilation a bit. Our .java files do not need to see the .scala files.
compileOrder := CompileOrder.JavaThenScala,

missinglinkTargetSourcePackages += TargetedPackage("test")
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
System.getProperty("plugin.version") match {
case null =>
throw new MessageOnlyException(
"The system property 'plugin.version' is not defined. " +
"Specify this property using the scriptedLaunchOpts -D.")
case pluginVersion =>
addSbtPlugin("ch.epfl.scala" % "sbt-missinglink" % pluginVersion)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package test;

public enum Foo {
BAR
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package test

import com.google.common.base.Enums

/**
* Calls a method in the Guava Enums class which was removed in guava 18. If a project calls this
* method while overriding Guava to >= 18, it will cause a NoSuchMethodError at runtime.
*/
object ProblematicDependency {

def reliesOnRemovedMethod(): AnyRef = {
Enums.valueOfFunction(classOf[Foo])
}
}
3 changes: 3 additions & 0 deletions src/sbt-test/missinglink/target-source-package/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
> compile
> compile:missinglinkCheck
-> runtime:missinglinkCheck

0 comments on commit d6b9520

Please sign in to comment.