diff --git a/src/main/groovy/com/netflix/nebula/lint/VersionNumber.groovy b/src/main/groovy/com/netflix/nebula/lint/VersionNumber.groovy
new file mode 100644
index 00000000..686b7d89
--- /dev/null
+++ b/src/main/groovy/com/netflix/nebula/lint/VersionNumber.groovy
@@ -0,0 +1,271 @@
+package com.netflix.nebula.lint
+
+
+import com.google.common.base.Objects
+import com.google.common.collect.Ordering
+
+import javax.annotation.Nullable
+
+/**
+ * Represents, parses, and compares version numbers. Supports a couple of different schemes:
- MAJOR.MINOR.MICRO-QUALIFIER (the default).
- MAJOR.MINOR.MICRO.PATCH-QUALIFIER.
+ *
+ * The {@link #parse} method handles missing parts and allows "." to be used instead of "-", and "_" to be used instead of "." for the patch number.
+ *
+ *
This class considers missing parts to be 0, so that "1.0" == "1.0.0" == "1.0.0_0".
+ *
+ * Note that this class considers "1.2.3-something" less than "1.2.3". Qualifiers are compared lexicographically ("1.2.3-alpha" < "1.2.3-beta") and case-insensitive ("1.2.3-alpha" <
+ * "1.2.3.RELEASE").
+ *
+ *
To check if a version number is at least "1.2.3", disregarding a potential qualifier like "beta", use {@code version.getBaseVersion().compareTo(VersionNumber.parse("1.2.3")) >= 0}.
+ */
+class VersionNumber implements Comparable {
+ private static final DefaultScheme DEFAULT_SCHEME = new VersionNumber.DefaultScheme()
+ private static final VersionNumber.SchemeWithPatchVersion PATCH_SCHEME = new VersionNumber.SchemeWithPatchVersion()
+ static final VersionNumber UNKNOWN = version(0)
+
+ private final int major
+ private final int minor
+ private final int micro
+ private final int patch
+ private final String qualifier
+ private final AbstractScheme scheme
+
+ VersionNumber(int major, int minor, int micro, @Nullable String qualifier) {
+ this(major, minor, micro, 0, qualifier, DEFAULT_SCHEME)
+ }
+
+ VersionNumber(int major, int minor, int micro, int patch, @Nullable String qualifier) {
+ this(major, minor, micro, patch, qualifier, PATCH_SCHEME)
+ }
+
+ private VersionNumber(int major, int minor, int micro, int patch, @Nullable String qualifier, AbstractScheme scheme) {
+ this.major = major
+ this.minor = minor
+ this.micro = micro
+ this.patch = patch
+ this.qualifier = qualifier
+ this.scheme = scheme
+ }
+
+ int getMajor() {
+ return major;
+ }
+
+ int getMinor() {
+ return minor;
+ }
+
+ int getMicro() {
+ return micro
+ }
+
+ int getPatch() {
+ return patch
+ }
+
+ @Nullable
+ String getQualifier() {
+ return qualifier
+ }
+
+ VersionNumber getBaseVersion() {
+ return new VersionNumber(major, minor, micro, patch, null, scheme)
+ }
+
+ @Override
+ int compareTo(VersionNumber other) {
+ if (major != other.major) {
+ return major - other.major
+ }
+ if (minor != other.minor) {
+ return minor - other.minor
+ }
+ if (micro != other.micro) {
+ return micro - other.micro
+ }
+ if (patch != other.patch) {
+ return patch - other.patch
+ }
+ return Ordering.natural().nullsLast().compare(toLowerCase(qualifier), toLowerCase(other.qualifier))
+ }
+
+ boolean equals(@Nullable Object other) {
+ return other instanceof VersionNumber && compareTo((VersionNumber) other) == 0
+ }
+
+ int hashCode() {
+ int result = major
+ result = 31 * result + minor
+ result = 31 * result + micro
+ result = 31 * result + patch
+ result = 31 * result + Objects.hashCode(qualifier)
+ return result
+ }
+
+ String toString() {
+ return scheme.format(this)
+ }
+
+ static VersionNumber version(int major) {
+ return new VersionNumber(major, 0, 0, 0, null, DEFAULT_SCHEME)
+ }
+
+ /**
+ * Returns the default MAJOR.MINOR.MICRO-QUALIFIER scheme.
+ */
+ static VersionNumber.Scheme scheme() {
+ return DEFAULT_SCHEME
+ }
+
+ /**
+ * Returns the MAJOR.MINOR.MICRO.PATCH-QUALIFIER scheme.
+ */
+ static VersionNumber.Scheme withPatchNumber() {
+ return PATCH_SCHEME
+ }
+
+ static VersionNumber parse(String versionString) {
+ return DEFAULT_SCHEME.parse(versionString)
+ }
+
+ @Nullable
+ private String toLowerCase(@Nullable String string) {
+ return string == null ? null : string.toLowerCase()
+ }
+
+ interface Scheme {
+ VersionNumber parse(String value)
+
+ String format(VersionNumber versionNumber)
+ }
+
+ private abstract static class AbstractScheme implements VersionNumber.Scheme {
+ final int depth
+
+ protected AbstractScheme(int depth) {
+ this.depth = depth
+ }
+
+ @Override
+ VersionNumber parse(@Nullable String versionString) {
+ if (versionString == null || versionString.length() == 0) {
+ return UNKNOWN
+ }
+ VersionNumber.AbstractScheme.Scanner scanner = new VersionNumber.AbstractScheme.Scanner(versionString)
+
+ int major = 0
+ int minor = 0
+ int micro = 0
+ int patch = 0
+
+ if (!scanner.hasDigit()) {
+ return UNKNOWN
+ }
+ major = scanner.scanDigit()
+ if (scanner.isSeparatorAndDigit('.')) {
+ scanner.skipSeparator()
+ minor = scanner.scanDigit()
+ if (scanner.isSeparatorAndDigit('.')) {
+ scanner.skipSeparator()
+ micro = scanner.scanDigit()
+ if (depth > 3 && scanner.isSeparatorAndDigit('.', '_')) {
+ scanner.skipSeparator()
+ patch = scanner.scanDigit()
+ }
+ }
+ }
+
+ if (scanner.isEnd()) {
+ return new VersionNumber(major, minor, micro, patch, null, this)
+ }
+
+ if (scanner.isQualifier()) {
+ scanner.skipSeparator()
+ return new VersionNumber(major, minor, micro, patch, scanner.remainder(), this)
+ }
+
+ return UNKNOWN
+ }
+
+ private static class Scanner {
+ int pos
+ final String str
+
+ private Scanner(String string) {
+ this.str = string
+ }
+
+ boolean hasDigit() {
+ return pos < str.length() && Character.isDigit(str.charAt(pos))
+ }
+
+ boolean isSeparatorAndDigit(char... separators) {
+ return pos < str.length() - 1 && oneOf(separators) && Character.isDigit(str.charAt(pos + 1))
+ }
+
+ private boolean oneOf(char... separators) {
+ char current = str.charAt(pos);
+ for (int i = 0; i < separators.length; i++) {
+ char separator = separators[i]
+ if (current == separator) {
+ return true
+ }
+ }
+ return false
+ }
+
+ boolean isQualifier() {
+ return pos < str.length() - 1 && oneOf('.', '-')
+ }
+
+ int scanDigit() {
+ int start = pos
+ while (hasDigit()) {
+ pos++
+ }
+ return Integer.parseInt(str.substring(start, pos))
+ }
+
+ boolean isEnd() {
+ return pos == str.length()
+ }
+
+ void skipSeparator() {
+ pos++
+ }
+
+ @Nullable
+ String remainder() {
+ return pos == str.length() ? null : str.substring(pos)
+ }
+ }
+ }
+
+ private static class DefaultScheme extends AbstractScheme {
+ private static final String VERSION_TEMPLATE = "%d.%d.%d%s"
+
+ DefaultScheme() {
+ super(3)
+ }
+
+ @Override
+ String format(VersionNumber versionNumber) {
+ return String.format(VERSION_TEMPLATE, versionNumber.major, versionNumber.minor, versionNumber.micro, versionNumber.qualifier == null ? "" : "-" + versionNumber.qualifier)
+ }
+ }
+
+ private static class SchemeWithPatchVersion extends AbstractScheme {
+ private static final String VERSION_TEMPLATE = "%d.%d.%d.%d%s"
+
+ private SchemeWithPatchVersion() {
+ super(4)
+ }
+
+ @Override
+ String format(VersionNumber versionNumber) {
+ return String.format(VERSION_TEMPLATE, versionNumber.major, versionNumber.minor, versionNumber.micro, versionNumber.patch, versionNumber.qualifier == null ? "" : "-" + versionNumber.qualifier)
+ }
+ }
+
+}
+
diff --git a/src/main/groovy/com/netflix/nebula/lint/rule/dependency/DependencyService.groovy b/src/main/groovy/com/netflix/nebula/lint/rule/dependency/DependencyService.groovy
index c12a381a..8c1ea921 100644
--- a/src/main/groovy/com/netflix/nebula/lint/rule/dependency/DependencyService.groovy
+++ b/src/main/groovy/com/netflix/nebula/lint/rule/dependency/DependencyService.groovy
@@ -2,6 +2,7 @@ package com.netflix.nebula.lint.rule.dependency
import com.netflix.nebula.interop.GradleKt
import com.netflix.nebula.lint.SourceSetUtils
+import com.netflix.nebula.lint.VersionNumber
import groovy.transform.Memoized
import groovyx.gpars.GParsPool
import org.gradle.api.Project
@@ -15,7 +16,6 @@ import org.gradle.api.file.FileCollection
import org.gradle.api.internal.artifacts.DefaultModuleIdentifier
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.SourceSetContainer
-import org.gradle.util.VersionNumber
import org.objectweb.asm.ClassReader
import org.slf4j.Logger
import org.slf4j.LoggerFactory
diff --git a/src/main/groovy/com/netflix/nebula/lint/rule/dependency/MinimumDependencyVersionRule.groovy b/src/main/groovy/com/netflix/nebula/lint/rule/dependency/MinimumDependencyVersionRule.groovy
index 78c45913..d2470b9d 100644
--- a/src/main/groovy/com/netflix/nebula/lint/rule/dependency/MinimumDependencyVersionRule.groovy
+++ b/src/main/groovy/com/netflix/nebula/lint/rule/dependency/MinimumDependencyVersionRule.groovy
@@ -1,5 +1,6 @@
package com.netflix.nebula.lint.rule.dependency
+import com.netflix.nebula.lint.VersionNumber
import com.netflix.nebula.lint.rule.GradleDependency
import com.netflix.nebula.lint.rule.GradleLintRule
import com.netflix.nebula.lint.rule.GradleModelAware
@@ -10,7 +11,6 @@ import org.codehaus.groovy.ast.expr.Expression
import org.codehaus.groovy.ast.expr.MethodCallExpression
import org.gradle.api.Incubating
import org.gradle.api.artifacts.Configuration
-import org.gradle.util.VersionNumber
/**
* This is like a declarative form of the use of a Substitute Nebula Resolution Rule:
diff --git a/src/test/groovy/com/netflix/nebula/lint/VersionNumberTest.groovy b/src/test/groovy/com/netflix/nebula/lint/VersionNumberTest.groovy
new file mode 100644
index 00000000..8faf5f00
--- /dev/null
+++ b/src/test/groovy/com/netflix/nebula/lint/VersionNumberTest.groovy
@@ -0,0 +1,173 @@
+package com.netflix.nebula.lint
+
+
+import org.gradle.util.internal.VersionNumber
+import org.hamcrest.BaseMatcher
+import org.hamcrest.Description
+import org.hamcrest.Matcher
+import org.hamcrest.Matchers
+import spock.lang.Specification
+
+class VersionNumberTest extends Specification {
+ def "construction"() {
+ expect:
+ VersionNumber.version(5) == new VersionNumber(5, 0, 0, null)
+ }
+
+ def "parsing"() {
+ expect:
+ VersionNumber.parse("1") == new VersionNumber(1, 0, 0, null)
+ VersionNumber.parse("1.0") == new VersionNumber(1, 0, 0, null)
+ VersionNumber.parse("1.0.0") == new VersionNumber(1, 0, 0, null)
+
+ VersionNumber.parse("1.2") == new VersionNumber(1, 2, 0, null)
+ VersionNumber.parse("1.2.3") == new VersionNumber(1, 2, 3, null)
+
+ VersionNumber.parse("1-rc1-SNAPSHOT") == new VersionNumber(1, 0, 0, "rc1-SNAPSHOT")
+ VersionNumber.parse("1.2-rc1-SNAPSHOT") == new VersionNumber(1, 2, 0, "rc1-SNAPSHOT")
+ VersionNumber.parse("1.2.3-rc1-SNAPSHOT") == new VersionNumber(1, 2, 3, "rc1-SNAPSHOT")
+
+ VersionNumber.parse("1.rc1-SNAPSHOT") == new VersionNumber(1, 0, 0, "rc1-SNAPSHOT")
+ VersionNumber.parse("1.2.rc1-SNAPSHOT") == new VersionNumber(1, 2, 0, "rc1-SNAPSHOT")
+ VersionNumber.parse("1.2.3.rc1-SNAPSHOT") == new VersionNumber(1, 2, 3, "rc1-SNAPSHOT")
+
+ VersionNumber.parse("11.22") == new VersionNumber(11, 22, 0, null)
+ VersionNumber.parse("11.22.33") == new VersionNumber(11, 22, 33, null)
+ VersionNumber.parse("11.22.33-eap") == new VersionNumber(11, 22, 33, "eap")
+
+ VersionNumber.parse("11.fortyfour") == new VersionNumber(11, 0, 0, "fortyfour")
+
+ VersionNumber.parse("1.0.0.0") == new VersionNumber(1, 0, 0, "0")
+ VersionNumber.parse("1.0.0.0.0.0.0") == new VersionNumber(1, 0, 0, "0.0.0.0")
+ VersionNumber.parse("1.2.3.4-rc1-SNAPSHOT") == new VersionNumber(1, 2, 3, "4-rc1-SNAPSHOT")
+ VersionNumber.parse("1.2.3.4.rc1-SNAPSHOT") == new VersionNumber(1, 2, 3, "4.rc1-SNAPSHOT")
+ }
+
+ def "parsing with patch number"() {
+ expect:
+ def defaultScheme = VersionNumber.scheme()
+ defaultScheme.parse("1") == new VersionNumber(1, 0, 0, null)
+ defaultScheme.parse("1.2") == new VersionNumber(1, 2, 0, null)
+ defaultScheme.parse("1.2.3") == new VersionNumber(1, 2, 3, null)
+ defaultScheme.parse("1.2.3-qualifier") == new VersionNumber(1, 2, 3, "qualifier")
+ defaultScheme.parse("1.2.3.4") == new VersionNumber(1, 2, 3, "4")
+
+ def patchScheme = VersionNumber.withPatchNumber()
+ patchScheme.parse("1") == new VersionNumber(1, 0, 0, null)
+ patchScheme.parse("1.2") == new VersionNumber(1, 2, 0, null)
+ patchScheme.parse("1.2.3") == new VersionNumber(1, 2, 3, null)
+ patchScheme.parse("1.2.3.4") == new VersionNumber(1, 2, 3, 4, null)
+ patchScheme.parse("1.2.3_4") == new VersionNumber(1, 2, 3, 4, null)
+ patchScheme.parse("1.2.3.4-qualifier") == new VersionNumber(1, 2, 3, 4, "qualifier")
+ patchScheme.parse("1.2.3.4.qualifier") == new VersionNumber(1, 2, 3, 4, "qualifier")
+ patchScheme.parse("1.2.3.4.5.6") == new VersionNumber(1, 2, 3, 4, "5.6")
+ }
+
+ def "unparseable version number is represented as UNKNOWN (0.0.0.0)"() {
+ expect:
+ VersionNumber.parse(null) == VersionNumber.UNKNOWN
+ VersionNumber.parse("") == VersionNumber.UNKNOWN
+ VersionNumber.parse("foo") == VersionNumber.UNKNOWN
+ VersionNumber.parse("1.") == VersionNumber.UNKNOWN
+ VersionNumber.parse("1.2.3-") == VersionNumber.UNKNOWN
+ VersionNumber.parse(".") == VersionNumber.UNKNOWN
+ VersionNumber.parse("_") == VersionNumber.UNKNOWN
+ VersionNumber.parse("-") == VersionNumber.UNKNOWN
+ VersionNumber.parse(".1") == VersionNumber.UNKNOWN
+ VersionNumber.parse("a.1") == VersionNumber.UNKNOWN
+ VersionNumber.parse("1_2") == VersionNumber.UNKNOWN
+ VersionNumber.parse("1_2_2") == VersionNumber.UNKNOWN
+ VersionNumber.parse("1.2.3_4") == VersionNumber.UNKNOWN
+ }
+ def "accessors"() {
+ when:
+ def version = new VersionNumber(1, 2, 3, 4, "foo")
+ then:
+ version.major == 1
+ version.minor == 2
+ version.micro == 3
+ version.patch == 4
+ version.qualifier == "foo"
+ }
+ def "string representation"() {
+ expect:
+ VersionNumber.parse("1.0").toString() == "1.0.0"
+ VersionNumber.parse("1.2.3").toString() == "1.2.3"
+ VersionNumber.parse("1.2.3.4").toString() == "1.2.3-4"
+ VersionNumber.parse("1-rc-1").toString() == "1.0.0-rc-1"
+ VersionNumber.parse("1.2.3-rc-1").toString() == "1.2.3-rc-1"
+ def patchScheme = VersionNumber.withPatchNumber()
+ patchScheme.parse("1").toString() == "1.0.0.0"
+ patchScheme.parse("1.2").toString() == "1.2.0.0"
+ patchScheme.parse("1.2.3").toString() == "1.2.3.0"
+ patchScheme.parse("1.2.3.4").toString() == "1.2.3.4"
+ patchScheme.parse("1.2-rc-1").toString() == "1.2.0.0-rc-1"
+ }
+ def "equality"() {
+ def version = new VersionNumber(1, 1, 1, 1, null)
+ def qualified = new VersionNumber(1, 1, 1, 1, "beta-2")
+ expect:
+ new VersionNumber(1, 1, 1, 1, null) strictlyEqual(version)
+ new VersionNumber(2, 1, 1, 1, null) != version
+ new VersionNumber(1, 2, 1, 1, null) != version
+ new VersionNumber(1, 1, 2, 1, null) != version
+ new VersionNumber(1, 1, 1, 2, null) != version
+ new VersionNumber(1, 1, 1, 1, "rc") != version
+ new VersionNumber(1, 1, 1, 1, "beta-2") strictlyEqual(qualified)
+ new VersionNumber(1, 1, 1, 1, "beta-3") != qualified
+ }
+ def "comparison"() {
+ expect:
+ (new VersionNumber(1, 1, 1, null) <=> new VersionNumber(1, 1, 1, null)) == 0
+ (new VersionNumber(1, 1, 1, null) <=> new VersionNumber(1, 1, 1, 0, null)) == 0
+ new VersionNumber(2, 1, 1, null) > new VersionNumber(1, 1, 1, null)
+ new VersionNumber(1, 2, 1, null) > new VersionNumber(1, 1, 1, null)
+ new VersionNumber(1, 1, 2, null) > new VersionNumber(1, 1, 1, null)
+ new VersionNumber(1, 1, 1, 2, null) > new VersionNumber(1, 1, 1, null)
+ new VersionNumber(1, 1, 1, "rc") < new VersionNumber(1, 1, 1, null)
+ new VersionNumber(1, 1, 1, "beta") > new VersionNumber(1, 1, 1, "alpha")
+ new VersionNumber(1, 1, 1, "RELEASE") > new VersionNumber(1, 1, 1, "beta")
+ new VersionNumber(1, 1, 1, "SNAPSHOT") < new VersionNumber(1, 1, 1, null)
+ new VersionNumber(1, 1, 1, null) < new VersionNumber(2, 1, 1, null)
+ new VersionNumber(1, 1, 1, null) < new VersionNumber(1, 2, 1, null)
+ new VersionNumber(1, 1, 1, null) < new VersionNumber(1, 1, 2, null)
+ new VersionNumber(1, 1, 1, null) > new VersionNumber(1, 1, 1, "rc")
+ new VersionNumber(1, 1, 1, "alpha") < new VersionNumber(1, 1, 1, "beta")
+ new VersionNumber(1, 1, 1, "beta") < new VersionNumber(1, 1, 1, "RELEASE")
+ }
+ def "base version"() {
+ expect:
+ new VersionNumber(1, 2, 3, null).baseVersion == new VersionNumber(1, 2, 3, null)
+ new VersionNumber(1, 2, 3, "beta").baseVersion == new VersionNumber(1, 2, 3, null)
+ }
+
+ static Matcher strictlyEqual(final T other) {
+ return new BaseMatcher() {
+ @Override
+ boolean matches(Object o) {
+ return strictlyEquals(o, other)
+ }
+
+ @Override
+ void describeTo(Description description) {
+ description.appendText("an Object that strictly equals ").appendValue(other)
+ }
+ }
+ }
+
+ static boolean strictlyEquals(Object a, Object b) {
+ if (!a.equals(b)) {
+ return false
+ }
+ if (!b.equals(a)) {
+ return false
+ }
+ if (!a.equals(a)) {
+ return false
+ }
+ if (b.equals(new Object())) {
+ return false
+ }
+ return a.hashCode() == b.hashCode()
+ }
+}