From 54a11cd7ba5d89b0f85309ad360a2222b27cbfbe Mon Sep 17 00:00:00 2001 From: Roberto Perez Alcolea Date: Sat, 29 Oct 2022 12:07:56 -0700 Subject: [PATCH] port Gradle's VersionNumber to avoid deprecations --- .../netflix/nebula/lint/VersionNumber.groovy | 271 ++++++++++++++++++ .../rule/dependency/DependencyService.groovy | 2 +- .../MinimumDependencyVersionRule.groovy | 2 +- .../nebula/lint/VersionNumberTest.groovy | 173 +++++++++++ 4 files changed, 446 insertions(+), 2 deletions(-) create mode 100644 src/main/groovy/com/netflix/nebula/lint/VersionNumber.groovy create mode 100644 src/test/groovy/com/netflix/nebula/lint/VersionNumberTest.groovy 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: + * + *

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() + } +}