From 54338f7b049d9f18907468218e1713658e6112ec Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Thu, 25 Jun 2015 00:18:47 +0100 Subject: [PATCH] Refactor exclusions, add partial orders / minifications --- .../main/scala/coursier/core/Exclusions.scala | 53 +++++++ .../src/main/scala/coursier/core/Orders.scala | 142 ++++++++++++++++++ .../main/scala/coursier/core/Resolver.scala | 44 +----- .../scala/coursier/test/ExclusionsTests.scala | 4 +- 4 files changed, 201 insertions(+), 42 deletions(-) create mode 100644 core/src/main/scala/coursier/core/Exclusions.scala create mode 100644 core/src/main/scala/coursier/core/Orders.scala diff --git a/core/src/main/scala/coursier/core/Exclusions.scala b/core/src/main/scala/coursier/core/Exclusions.scala new file mode 100644 index 000000000..742961223 --- /dev/null +++ b/core/src/main/scala/coursier/core/Exclusions.scala @@ -0,0 +1,53 @@ +package coursier.core + +object Exclusions { + + def partition(exclusions: Set[(String, String)]): (Boolean, Set[String], Set[String], Set[(String, String)]) = { + + val (wildCards, remaining) = exclusions + .partition{case (org, name) => org == "*" || name == "*" } + + val all = wildCards + .contains(("*", "*")) + + val excludeByOrg = wildCards + .collect{case (org, "*") if org != "*" => org } + val excludeByName = wildCards + .collect{case ("*", name) if name != "*" => name } + + (all, excludeByOrg, excludeByName, remaining) + } + + def apply(exclusions: Set[(String, String)]): (String, String) => Boolean = { + + val (all, excludeByOrg, excludeByName, remaining) = partition(exclusions) + + if (all) (_, _) => false + else + (org, name) => { + !excludeByName(name) && + !excludeByOrg(org) && + !remaining((org, name)) + } + } + + def minimize(exclusions: Set[(String, String)]): Set[(String, String)] = { + + val (all, excludeByOrg, excludeByName, remaining) = partition(exclusions) + + if (all) Set(("*", "*")) + else { + val filteredRemaining = remaining + .filter{case (org, name) => + !excludeByOrg(org) && + !excludeByName(name) + } + + excludeByOrg.map((_, "*")) ++ + excludeByName.map(("*", _)) ++ + filteredRemaining + } + } + + +} diff --git a/core/src/main/scala/coursier/core/Orders.scala b/core/src/main/scala/coursier/core/Orders.scala new file mode 100644 index 000000000..1653e99d7 --- /dev/null +++ b/core/src/main/scala/coursier/core/Orders.scala @@ -0,0 +1,142 @@ +package coursier.core + +object Orders { + + /** Minimal ad-hoc partial order */ + trait PartialOrder[A] { + /** + * x < y: Some(neg. integer) + * x == y: Some(0) + * x > y: Some(pos. integer) + * x, y not related: None + */ + def cmp(x: A, y: A): Option[Int] + } + + /** + * Only relations: + * Compile < Runtime < Test + */ + implicit val mavenScopePartialOrder: PartialOrder[Scope] = + new PartialOrder[Scope] { + val higher = Map[Scope, Set[Scope]]( + Scope.Compile -> Set(Scope.Runtime, Scope.Test), + Scope.Runtime -> Set(Scope.Test) + ) + + def cmp(x: Scope, y: Scope) = + if (x == y) Some(0) + else if (higher.get(x).exists(_(y))) Some(-1) + else if (higher.get(y).exists(_(x))) Some(1) + else None + } + + /** Non-optional < optional */ + implicit val optionalPartialOrder: PartialOrder[Boolean] = + new PartialOrder[Boolean] { + def cmp(x: Boolean, y: Boolean) = + Some( + if (x == y) 0 + else if (x) 1 + else -1 + ) + } + + /** + * Exclusions partial order. + * + * x <= y iff all that x excludes is also excluded by y. + * x and y not related iff x excludes some elements not excluded by y AND + * y excludes some elements not excluded by x. + * + * In particular, no exclusions <= anything <= Set(("*", "*")) + */ + implicit val exclusionsPartialOrder: PartialOrder[Set[(String, String)]] = + new PartialOrder[Set[(String, String)]] { + def boolCmp(a: Boolean, b: Boolean) = (a, b) match { + case (true, true) => Some(0) + case (true, false) => Some(1) + case (false, true) => Some(-1) + case (false, false) => None + } + + def cmp(x: Set[(String, String)], y: Set[(String, String)]) = { + val (xAll, xExcludeByOrg1, xExcludeByName1, xRemaining0) = Exclusions.partition(x) + val (yAll, yExcludeByOrg1, yExcludeByName1, yRemaining0) = Exclusions.partition(y) + + boolCmp(xAll, yAll).orElse { + def filtered(e: Set[(String, String)]) = + e.filter{case (org, name) => + !xExcludeByOrg1(org) && !yExcludeByOrg1(org) && + !xExcludeByName1(name) && !yExcludeByName1(name) + } + + def removeIntersection[T](a: Set[T], b: Set[T]) = + (a -- b, b -- a) + + def allEmpty(set: Set[_]*) = set.forall(_.isEmpty) + + val (xRemaining1, yRemaining1) = + (filtered(xRemaining0), filtered(yRemaining0)) + + val (xProperRemaining, yProperRemaining) = + removeIntersection(xRemaining1, yRemaining1) + + val (onlyXExcludeByOrg, onlyYExcludeByOrg) = + removeIntersection(xExcludeByOrg1, yExcludeByOrg1) + + val (onlyXExcludeByName, onlyYExcludeByName) = + removeIntersection(xExcludeByName1, yExcludeByName1) + + val (noXProper, noYProper) = ( + allEmpty(xProperRemaining, onlyXExcludeByOrg, onlyXExcludeByName), + allEmpty(yProperRemaining, onlyYExcludeByOrg, onlyYExcludeByName) + ) + + boolCmp(noYProper, noXProper) // order matters + } + } + } + + /** + * Assume all dependencies have same `module`, `version`, and `artifact`; see `minDependencies` + * if they don't. + */ + def minDependenciesUnsafe(dependencies: Set[Dependency]): Set[Dependency] = { + val groupedDependencies = dependencies + .groupBy(dep => (dep.optional, dep.scope)) + .toList + + val remove = + for { + List(((xOpt, xScope), xDeps), ((yOpt, yScope), yDeps)) <- groupedDependencies.combinations(2) + optCmp <- optionalPartialOrder.cmp(xOpt, yOpt).iterator + scopeCmp <- mavenScopePartialOrder.cmp(xScope, yScope).iterator + if optCmp*scopeCmp >= 0 + xDep <- xDeps.iterator + yDep <- yDeps.iterator + exclCmp <- exclusionsPartialOrder.cmp(xDep.exclusions, yDep.exclusions).iterator + if optCmp*exclCmp >= 0 + if scopeCmp*exclCmp >= 0 + xIsMin = optCmp < 0 || scopeCmp < 0 || exclCmp < 0 + yIsMin = optCmp > 0 || scopeCmp > 0 || exclCmp > 0 + if xIsMin || yIsMin // should be always true, unless xDep == yDep, which shouldn't happen + } yield if (xIsMin) yDep else xDep + + dependencies -- remove + } + + /** + * Minified representation of `dependencies`. + * + * The returned set brings exactly the same things as `dependencies`, with no redundancy. + */ + def minDependencies(dependencies: Set[Dependency]): Set[Dependency] = { + dependencies + .groupBy(_.copy(scope = Scope.Other(""), exclusions = Set.empty, optional = false)) + .mapValues(minDependenciesUnsafe) + .valuesIterator + .fold(Set.empty)(_ ++ _) + } + +} diff --git a/core/src/main/scala/coursier/core/Resolver.scala b/core/src/main/scala/coursier/core/Resolver.scala index 936bc21d2..035c047aa 100644 --- a/core/src/main/scala/coursier/core/Resolver.scala +++ b/core/src/main/scala/coursier/core/Resolver.scala @@ -223,34 +223,6 @@ object Resolver { } } - /** - * Addition of exclusions. A module is excluded by the result if it is excluded - * by `first`, by `second`, or by both. - */ - def exclusionsAdd(first: Set[(String, String)], - second: Set[(String, String)]): Set[(String, String)] = { - - val (firstAll, firstNonAll) = first.partition{case ("*", "*") => true; case _ => false } - val (secondAll, secondNonAll) = second.partition{case ("*", "*") => true; case _ => false } - - if (firstAll.nonEmpty || secondAll.nonEmpty) Set(("*", "*")) - else { - val firstOrgWildcards = firstNonAll.collect{ case ("*", name) => name } - val firstNameWildcards = firstNonAll.collect{ case (org, "*") => org } - val secondOrgWildcards = secondNonAll.collect{ case ("*", name) => name } - val secondNameWildcards = secondNonAll.collect{ case (org, "*") => org } - - val orgWildcards = firstOrgWildcards ++ secondOrgWildcards - val nameWildcards = firstNameWildcards ++ secondNameWildcards - - val firstRemaining = firstNonAll.filter{ case (org, name) => org != "*" && name != "*" } - val secondRemaining = secondNonAll.filter{ case (org, name) => org != "*" && name != "*" } - - val remaining = (firstRemaining ++ secondRemaining).filterNot{case (org, name) => orgWildcards(name) || nameWildcards(org) } - - orgWildcards.map(name => ("*", name)) ++ nameWildcards.map(org => (org, "*")) ++ remaining - } - } def withDefaultScope(dep: Dependency): Dependency = if (dep.scope.name.isEmpty) dep.copy(scope = Scope.Compile) @@ -262,22 +234,12 @@ object Resolver { def withExclusions(dependencies: Seq[Dependency], exclusions: Set[(String, String)]): Seq[Dependency] = { - val (all, notAll) = exclusions.partition{case ("*", "*") => true; case _ => false} - - val orgWildcards = notAll.collect{case ("*", name) => name } - val nameWildcards = notAll.collect{case (org, "*") => org } - - val remaining = notAll.filterNot{case (org, name) => org == "*" || name == "*" } + val filter = Exclusions(exclusions) dependencies - .filter(dep => - all.isEmpty && - !orgWildcards(dep.module.name) && - !nameWildcards(dep.module.organization) && - !remaining((dep.module.organization, dep.module.name)) - ) + .filter(dep => filter(dep.module.organization, dep.module.name)) .map(dep => - dep.copy(exclusions = exclusionsAdd(dep.exclusions, exclusions)) + dep.copy(exclusions = Exclusions.minimize(dep.exclusions ++ exclusions)) ) } diff --git a/core/src/test/scala/coursier/test/ExclusionsTests.scala b/core/src/test/scala/coursier/test/ExclusionsTests.scala index 3eb775d95..051421384 100644 --- a/core/src/test/scala/coursier/test/ExclusionsTests.scala +++ b/core/src/test/scala/coursier/test/ExclusionsTests.scala @@ -2,10 +2,12 @@ package coursier package test import utest._ -import core.Resolver.exclusionsAdd object ExclusionsTests extends TestSuite { + def exclusionsAdd(e1: Set[(String, String)], e2: Set[(String, String)]) = + core.Exclusions.minimize(e1 ++ e2) + val tests = TestSuite { val e1 = Set(("org1", "name1")) val e2 = Set(("org2", "name2"))