diff --git a/source/dub/dependency.d b/source/dub/dependency.d index 40b48637b5..f9d6f16fb5 100644 --- a/source/dub/dependency.d +++ b/source/dub/dependency.d @@ -49,10 +49,7 @@ struct Dependency { private { // Shortcut to create >=0.0.0 enum ANY_IDENT = "*"; - bool m_inclusiveA = true; // A comparison > (true) or >= (false) - Version m_versA; - bool m_inclusiveB = true; // B comparison < (true) or <= (false) - Version m_versB; + Interval!Version m_versionRange; Path m_path; bool m_optional = false; bool m_default = false; @@ -62,7 +59,8 @@ struct Dependency { static @property Dependency any() { return Dependency(ANY_IDENT); } /// An invalid dependency (with no possible version matches). - static @property Dependency invalid() { Dependency ret; ret.m_versA = Version.maxRelease; ret.m_versB = Version.minRelease; return ret; } + static @property Dependency invalid() { return Dependency.init; } + static assert(!invalid.valid); /** Constructs a new dependency specification from a string @@ -79,9 +77,7 @@ struct Dependency { */ this(in Version ver) { - m_inclusiveA = m_inclusiveB = true; - m_versA = ver; - m_versB = ver; + m_versionRange = Interval!Version(ver, ver, "[]"); } /** Constructs a new dependency specification that matches a specific @@ -109,12 +105,12 @@ struct Dependency { @property void default_(bool value) { m_default = value; } /// Returns true $(I iff) the version range only matches a specific version. - @property bool isExactVersion() const { return m_versA == m_versB; } + @property bool isExactVersion() const { return m_versionRange.singular; } /// Returns the exact version matched by the version range. @property Version version_() const { - enforce(m_versA == m_versB, "Dependency "~this.versionSpec~" is no exact version."); - return m_versA; + enforce(m_versionRange.singular, "Dependency "~this.versionSpec~" is no exact version."); + return m_versionRange.min; } /** Sets/gets the matching version range as a specification string. @@ -149,51 +145,37 @@ struct Dependency { if (ves.startsWith("~>")) { // Shortcut: "~>x.y.z" variant. Last non-zero number will indicate // the base for this so something like this: ">=x.y.z <=", ves[0]) == -1) { - m_inclusiveA = true; - m_inclusiveB = true; - m_versA = m_versB = Version(ves); + m_versionRange = Interval!Version(Version(ves), Version(ves), "[]"); } else { auto cmpa = skipComp(ves); size_t idx2 = std.string.indexOf(ves, " "); if (idx2 == -1) { if (cmpa == "<=" || cmpa == "<") { - m_versA = Version.minRelease; - m_inclusiveA = true; - m_versB = Version(ves); - m_inclusiveB = cmpa == "<="; + m_versionRange = Interval!Version(Version.minRelease, Version(ves), cmpa == "<=" ? "[]" : "[)"); } else if (cmpa == ">=" || cmpa == ">") { - m_versA = Version(ves); - m_inclusiveA = cmpa == ">="; - m_versB = Version.maxRelease; - m_inclusiveB = true; + m_versionRange = Interval!Version(Version(ves), Version.maxRelease, cmpa == ">=" ? "[]" : "(]"); } else { - // Converts "==" to ">=a&&<=a", which makes merging easier - m_versA = m_versB = Version(ves); - m_inclusiveA = m_inclusiveB = true; + m_versionRange = Interval!Version(Version(ves), Version(ves), "[]"); } } else { enforce(cmpa == ">" || cmpa == ">=", "First comparison operator expected to be either > or >=, not "~cmpa); assert(ves[idx2] == ' '); - m_versA = Version(ves[0..idx2]); - m_inclusiveA = cmpa == ">="; + auto vmin = Version(ves[0..idx2]); + bool vmininc = cmpa == ">="; string v2 = ves[idx2+1..$]; auto cmpb = skipComp(v2); enforce(cmpb == "<" || cmpb == "<=", "Second comparison operator expected to be either < or <=, not "~cmpb); - m_versB = Version(v2); - m_inclusiveB = cmpb == "<="; + auto vmax = Version(v2); + bool vmaxinc = cmpb == "<="; - enforce(!m_versA.isBranch && !m_versB.isBranch, format("Cannot compare branches: %s", ves)); - enforce(m_versA <= m_versB, "First version must not be greater than the second one."); + enforce(!vmin.isBranch && !vmax.isBranch, format("Cannot compare branches: %s", ves)); + enforce(vmin <= vmax, "First version must not be greater than the second one."); + + m_versionRange = Interval!Version(vmin, vmax, vmininc ? vmaxinc ? "[]" : "[)" : vmaxinc ? "(]" : "()"); } } } @@ -202,17 +184,16 @@ struct Dependency { const { string r; - if (this == invalid) return "invalid"; + if (m_versionRange.empty) return "invalid"; - if (m_versA == m_versB && m_inclusiveA && m_inclusiveB) { - // Special "==" case - if (m_versA == Version.masterBranch) return "~master"; - else return m_versA.toString(); - } + if (m_versionRange == any.m_versionRange) return "*"; + + // Special "==" case + if (m_versionRange.singular) return m_versionRange.min.toString(); // "~>" case - if (m_inclusiveA && !m_inclusiveB && !m_versA.isBranch) { - auto vs = m_versA.toString(); + if (m_versionRange.minInclusive && !m_versionRange.maxInclusive && !m_versionRange.min.isBranch) { + auto vs = m_versionRange.min.toString(); auto i1 = std.string.indexOf(vs, '-'), i2 = std.string.indexOf(vs, '+'); auto i12 = i1 >= 0 ? i2 >= 0 ? i1 < i2 ? i1 : i2 : i1 : i2; auto va = i12 >= 0 ? vs[0 .. i12] : vs; @@ -223,13 +204,14 @@ struct Dependency { auto vp = parts[0 .. i+1].join("."); auto ve = Version(expandVersion(vp)); auto veb = Version(bumpVersion(vp) ~ "-0"); - if (ve == m_versA && veb == m_versB) return "~>" ~ vp; + if (ve == m_versionRange.min && veb == m_versionRange.max) return "~>" ~ vp; } } - if (m_versA != Version.minRelease) r = (m_inclusiveA ? ">=" : ">") ~ m_versA.toString(); - if (m_versB != Version.maxRelease) r ~= (r.length==0 ? "" : " ") ~ (m_inclusiveB ? "<=" : "<") ~ m_versB.toString(); - if (m_versA == Version.minRelease && m_versB == Version.maxRelease) r = ">=0.0.0"; + if (m_versionRange.min != Version.minRelease || !m_versionRange.minInclusive) + r = (m_versionRange.minInclusive ? ">=" : ">") ~ m_versionRange.min.toString(); + if (m_versionRange.max != Version.maxRelease || !m_versionRange.maxInclusive) + r ~= (r.length==0 ? "" : " ") ~ (m_versionRange.maxInclusive ? "<=" : "<") ~ m_versionRange.max.toString(); return r; } @@ -350,18 +332,16 @@ struct Dependency { bool opEquals(in Dependency o) const { // TODO(mdondorff): Check if not comparing the path is correct for all clients. - return o.m_inclusiveA == m_inclusiveA && o.m_inclusiveB == m_inclusiveB - && o.m_versA == m_versA && o.m_versB == m_versB - && o.m_optional == m_optional && o.m_default == m_default; + if (m_optional != o.m_optional) return false; + if (m_default != o.m_default) return false; + return m_versionRange == o.m_versionRange; } /// ditto int opCmp(in Dependency o) const { - if (m_inclusiveA != o.m_inclusiveA) return m_inclusiveA < o.m_inclusiveA ? -1 : 1; - if (m_inclusiveB != o.m_inclusiveB) return m_inclusiveB < o.m_inclusiveB ? -1 : 1; - if (m_versA != o.m_versA) return m_versA < o.m_versA ? -1 : 1; - if (m_versB != o.m_versB) return m_versB < o.m_versB ? -1 : 1; + if (m_versionRange != o.m_versionRange) + return m_versionRange.opCmp(o.m_versionRange); if (m_optional != o.m_optional) return m_optional ? -1 : 1; return 0; } @@ -379,9 +359,7 @@ struct Dependency { A specification is valid if it can match at least one version. */ - bool valid() const { - return m_versA <= m_versB && doCmp(m_inclusiveA && m_inclusiveB, m_versA, m_versB); - } + bool valid() const { return !m_versionRange.empty; } /** Determines if this dependency specification matches arbitrary versions. @@ -403,18 +381,8 @@ struct Dependency { bool matches(ref const(Version) v) const { if (this.matchesAny) return true; //logDebug(" try match: %s with: %s", v, this); - // Master only matches master - if(m_versA.isBranch) { - enforce(m_versA == m_versB); - return m_versA == v; - } - if(v.isBranch || m_versA.isBranch) - return m_versA == v; - if( !doCmp(m_inclusiveA, m_versA, v) ) - return false; - if( !doCmp(m_inclusiveB, v, m_versB) ) - return false; - return true; + + return m_versionRange.contains(v); } /** Merges two dependency specifications. @@ -428,23 +396,13 @@ struct Dependency { if (this.matchesAny) return o; if (o.matchesAny) return this; if (!this.valid || !o.valid) return invalid; - if (m_versA.isBranch != o.m_versA.isBranch) return invalid; - if (m_versB.isBranch != o.m_versB.isBranch) return invalid; - if (m_versA.isBranch) return m_versA == o.m_versA ? this : invalid; if (this.path != o.path) return invalid; - Version a = m_versA > o.m_versA ? m_versA : o.m_versA; - Version b = m_versB < o.m_versB ? m_versB : o.m_versB; - - Dependency d = this; - d.m_inclusiveA = !m_inclusiveA && m_versA >= o.m_versA ? false : o.m_inclusiveA; - d.m_versA = a; - d.m_inclusiveB = !m_inclusiveB && m_versB <= o.m_versB ? false : o.m_inclusiveB; - d.m_versB = b; - d.m_optional = m_optional && o.m_optional; - if (!d.valid) return invalid; - - return d; + Dependency ret; + ret.m_path = m_path; + ret.m_optional = m_optional && o.m_optional; + ret.m_versionRange = m_versionRange & o.m_versionRange; + return ret; } private static bool isDigit(char ch) { return ch >= '0' && ch <= '9'; } @@ -461,10 +419,6 @@ struct Dependency { case "==": return cmp; } } - - private static bool doCmp(bool inclusive, ref const Version a, ref const Version b) { - return inclusive ? a <= b : a < b; - } } unittest { @@ -619,6 +573,21 @@ unittest { assert(a.merge(b) == b); assert(b.merge(a) == b); + assert(Dependency.any.toString() == "*"); + assert(Dependency("==1.0.0").toString() == "1.0.0"); + assert(Dependency("1.0.0").toString() == "1.0.0"); + assert(Dependency("~master").toString() == "~master"); + assert(Dependency("~branch").toString() == "~branch"); + assert(Dependency("*").toString() == "*"); + assert(Dependency(">1.0.0").toString() == ">1.0.0"); + assert(Dependency(">=1.0.0").toString() == ">=1.0.0"); + assert(Dependency("<=1.0.0").toString() == "<=1.0.0"); + assert(Dependency("<1.0.0").toString() == "<1.0.0"); + assert(Dependency(">1.0.0 <2.0.0").toString() == ">1.0.0 <2.0.0"); + assert(Dependency(">=1.0.0 <2.0.0").toString() == ">=1.0.0 <2.0.0"); + assert(Dependency(">1.0.0 <=2.0.0").toString() == ">1.0.0 <=2.0.0"); + assert(Dependency(">=1.0.0 <=2.0.0").toString() == ">=1.0.0 <=2.0.0"); + logDebug("Dependency unittest success."); } @@ -644,7 +613,7 @@ struct Version { enum UNKNOWN_VERS = "unknown"; enum branchPrefix = '~'; enum masterString = "~master"; - string m_version; + string m_version = "0.0.0"; } static @property Version minRelease() { return Version("0.0.0"); } @@ -786,4 +755,180 @@ unittest { assertThrown(a == b, "Failed: UNKNOWN == UNKNOWN"); assert(Version("1.0.0+a") == Version("1.0.0+b")); + assert(Version() <= Version()); +} + + +/// Represents a generic mathematical interval +private struct Interval(T) +{ + static assert(Interval.init.empty); + + private { + T m_min; + T m_max; + bool m_minInclusive = false; + bool m_maxInclusive = false; + } + + this(T min, T max, string bounds = "[)") + in { + assert(min <= max, format("Minumum must be lower or equal to the maximum value: %s !<= %s", min, max)); + assert(bounds.length == 2 && "[(".canFind(bounds[0]) && "])".canFind(bounds[1]), + "Bounds must be a string composed of round and square brackets."); + } + body { + + m_min = min; + m_max = max; + m_minInclusive = bounds[0] == '['; + m_maxInclusive = bounds[1] == ']'; + } + + // BUG: 1 < x < 2 is also empty for integers!! + @property bool empty() const { + if (m_min > m_max) return true; + if (m_min < m_max) return false; + return !m_minInclusive || !m_maxInclusive; + } + + /// True iff `min` equals `max` and both ends of the interval are inclusive. + @property bool singular() const { return m_min == m_max && m_minInclusive && m_maxInclusive; } + + @property inout(T) min() inout { return m_min; } + @property inout(T) max() inout { return m_max; } + @property bool minInclusive() const { return m_minInclusive; } + @property bool maxInclusive() const { return m_maxInclusive; } + + bool contains(T value) + const { + if (!m_minInclusive && value <= m_min) return false; + if (value < m_min) return false; + + if (!m_maxInclusive && value >= m_max) return false; + if (value > m_max) return false; + + return true; + } + + bool opEquals(Interval other) + const { + if (m_min == other.m_min && m_max == other.m_max && m_minInclusive == other.m_minInclusive && m_maxInclusive == other.m_maxInclusive) + return true; + // BUG: integral ranges [1, 10] and (0, 11) are equal in a strict sense! + return false; + } + + int opCmp(in Interval o) + const { + if (m_minInclusive != o.m_minInclusive) return m_minInclusive < o.m_minInclusive ? -1 : 1; + if (m_maxInclusive != o.m_maxInclusive) return m_maxInclusive < o.m_maxInclusive ? -1 : 1; + if (m_min != o.m_min) return m_min < o.m_min ? -1 : 1; + if (m_max != o.m_max) return m_max < o.m_max ? -1 : 1; + return 0; + } + + Interval opBinary(string op)(Interval other) const if (op == "&") + { + Interval ret; + + if (this.m_min > other.m_min) { + ret.m_min = this.m_min; + ret.m_minInclusive = this.m_minInclusive; + } else if (other.m_min > this.m_min) { + ret.m_min = other.m_min; + ret.m_minInclusive = other.m_minInclusive; + } else { + ret.m_min = this.m_min; + ret.m_minInclusive = this.m_minInclusive && other.m_minInclusive; + } + + if (this.max < other.max) { + ret.m_max = this.m_max; + ret.m_maxInclusive = this.m_maxInclusive; + } else if (other.m_max < this.m_max) { + ret.m_max = other.m_max; + ret.m_maxInclusive = other.m_maxInclusive; + } else { + ret.m_max = this.m_max; + ret.m_maxInclusive = this.m_maxInclusive && other.m_maxInclusive; + } + + return ret; + } + + Interval opBinary(string op)(Interval other) const if (op == "|") + { + Interval ret; + + if (this.m_min < other.m_min) { + ret.m_min = this.m_min; + ret.m_minInclusive = this.m_minInclusive; + } else if (other.m_min < this.m_min) { + ret.m_min = other.m_min; + ret.m_minInclusive = other.m_minInclusive; + } else { + ret.m_min = this.m_min; + ret.m_minInclusive = this.m_minInclusive || other.m_minInclusive; + } + + if (this.max > other.max) { + ret.m_max = this.m_max; + ret.m_maxInclusive = this.m_maxInclusive; + } else if (other.m_max > this.m_max) { + ret.m_max = other.m_max; + ret.m_maxInclusive = other.m_maxInclusive; + } else { + ret.m_max = this.m_max; + ret.m_maxInclusive = this.m_maxInclusive || other.m_maxInclusive; + } + + if (ret.m_min > ret.m_max) return Interval.init; + + return ret; + } +} + +unittest { + alias Inti = Interval!int; + + assert(!Inti(-1, 1).contains(-2)); + assert(Inti(-1, 1).contains(-1)); + assert(Inti(-1, 1).contains(0)); + assert(!Inti(-1, 1).contains(1)); + assert(!Inti(-1, 1).contains(2)); + + assert(!Inti(-1, 1, "[]").contains(-2)); + assert(Inti(-1, 1, "[]").contains(-1)); + assert(Inti(-1, 1, "[]").contains(0)); + assert(Inti(-1, 1, "[]").contains(1)); + assert(!Inti(-1, 1, "[]").contains(2)); + + assert(!Inti(-1, 1, "()").contains(-2)); + assert(!Inti(-1, 1, "()").contains(-1)); + assert(Inti(-1, 1, "()").contains(0)); + assert(!Inti(-1, 1, "()").contains(1)); + assert(!Inti(-1, 1, "()").contains(2)); + + assert((Inti(1, 10, "[]") | Inti(1, 10, "()")) == Inti(1, 10, "[]")); + assert((Inti(1, 10, "[]") & Inti(1, 10, "()")) == Inti(1, 10, "()")); + assert((Inti(1, 10, "()") | Inti(1, 10, "[]")) == Inti(1, 10, "[]")); + assert((Inti(1, 10, "()") & Inti(1, 10, "[]")) == Inti(1, 10, "()")); + + assert((Inti(1, 10, "[]") | Inti(2, 11, "()")) == Inti(1, 11, "[)")); + assert((Inti(1, 10, "[]") & Inti(2, 11, "()")) == Inti(2, 10, "(]")); + assert((Inti(2, 11, "()") | Inti(1, 10, "[]")) == Inti(1, 11, "[)")); + assert((Inti(2, 11, "()") & Inti(1, 10, "[]")) == Inti(2, 10, "(]")); + + assert(Inti(0, 1) == Inti(0, 1, "[)")); + assert(Inti(0, 1) != Inti(0, 1, "[]")); + assert(Inti(0, 1) != Inti(0, 1, "()")); + assert(Inti(0, 1) != Inti(0, 1, "(]")); + + assert(!Inti(0, 1).empty); + assert(Inti(0, 0).empty); + + // failing integer unit tests: + //assert(Inti(0, 1, "()").empty); + //assert(Inti(0, 1, "[)") == Inti(0, 0, "[]")); }