From 066b7fdc873a4142e60df19d235e46c010fd451a Mon Sep 17 00:00:00 2001 From: Julien HENRY Date: Fri, 27 Feb 2026 10:27:13 +0100 Subject: [PATCH 1/2] Publish the manifest jar --- manifest/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/manifest/build.gradle b/manifest/build.gradle index be904698..47a21dc9 100644 --- a/manifest/build.gradle +++ b/manifest/build.gradle @@ -17,6 +17,7 @@ publishing { publications { mavenJava(MavenPublication) { artifactId = 'sonar-plugin-manifest' + from components.java pom { name = 'Sonar Plugin API - Manifest' description = 'Sonar Plugin API - Manifest' From f1225b177b4ed79868a8cf48d389ae0ad7d3a1ad Mon Sep 17 00:00:00 2001 From: Julien HENRY Date: Fri, 27 Feb 2026 13:15:35 +0100 Subject: [PATCH 2/2] PLUGINAPI-182 Create a shared module for the Version class --- manifest/build.gradle | 1 + .../plugin/api/manifest/PluginManifest.java | 1 + .../plugin/api/manifest/RequiredPlugin.java | 1 + .../sonar/plugin/api/manifest/Version.java | 149 -------- .../api/manifest/PluginManifestTest.java | 1 + .../api/manifest/RequiredPluginTest.java | 2 +- .../plugin/api/manifest/VersionTests.java | 262 -------------- plugin-api/build.gradle | 1 + .../java/org/sonar/api/utils/VersionTest.java | 132 ------- settings.gradle | 1 + shared/build.gradle | 51 +++ .../java/org/sonar/api/utils/Version.java | 86 +++-- .../org/sonar/api/utils/package-info.java | 23 ++ .../java/org/sonar/api/utils/VersionTest.java | 338 ++++++++++++++++++ 14 files changed, 482 insertions(+), 567 deletions(-) delete mode 100644 manifest/src/main/java/org/sonar/plugin/api/manifest/Version.java delete mode 100644 manifest/src/test/java/org/sonar/plugin/api/manifest/VersionTests.java delete mode 100644 plugin-api/src/test/java/org/sonar/api/utils/VersionTest.java create mode 100644 shared/build.gradle rename {plugin-api => shared}/src/main/java/org/sonar/api/utils/Version.java (72%) create mode 100644 shared/src/main/java/org/sonar/api/utils/package-info.java create mode 100644 shared/src/test/java/org/sonar/api/utils/VersionTest.java diff --git a/manifest/build.gradle b/manifest/build.gradle index 47a21dc9..a7a8300b 100644 --- a/manifest/build.gradle +++ b/manifest/build.gradle @@ -3,6 +3,7 @@ description = 'Sonar Plugin API - Manifest' dependencies { compileOnly libs.jsr305 + api project(':shared') implementation libs.commons.lang3 testImplementation libs.junit5 diff --git a/manifest/src/main/java/org/sonar/plugin/api/manifest/PluginManifest.java b/manifest/src/main/java/org/sonar/plugin/api/manifest/PluginManifest.java index 54ba5d92..3755fe90 100644 --- a/manifest/src/main/java/org/sonar/plugin/api/manifest/PluginManifest.java +++ b/manifest/src/main/java/org/sonar/plugin/api/manifest/PluginManifest.java @@ -35,6 +35,7 @@ import javax.annotation.Nullable; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.sonar.api.utils.Version; /** * This class loads Sonar plugin metadata from JAR manifest. diff --git a/manifest/src/main/java/org/sonar/plugin/api/manifest/RequiredPlugin.java b/manifest/src/main/java/org/sonar/plugin/api/manifest/RequiredPlugin.java index e56db1ab..00149f12 100644 --- a/manifest/src/main/java/org/sonar/plugin/api/manifest/RequiredPlugin.java +++ b/manifest/src/main/java/org/sonar/plugin/api/manifest/RequiredPlugin.java @@ -21,6 +21,7 @@ import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; +import org.sonar.api.utils.Version; public class RequiredPlugin { diff --git a/manifest/src/main/java/org/sonar/plugin/api/manifest/Version.java b/manifest/src/main/java/org/sonar/plugin/api/manifest/Version.java deleted file mode 100644 index 39d7a7f3..00000000 --- a/manifest/src/main/java/org/sonar/plugin/api/manifest/Version.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Sonar Plugin API - * Copyright (C) 2009-2025 SonarSource Sàrl - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.plugin.api.manifest; - -import java.util.Arrays; - -public class Version implements Comparable { - - private final String name; - private final String nameWithoutQualifier; - private final int[] numbers; - private final String qualifier; - - private Version(String version) { - this.name = version.trim(); - var qualifierPosition = name.indexOf("-"); - if (qualifierPosition != -1) { - this.qualifier = name.substring(qualifierPosition + 1); - this.nameWithoutQualifier = name.substring(0, qualifierPosition); - } else { - this.qualifier = ""; - this.nameWithoutQualifier = this.name; - } - final var split = this.nameWithoutQualifier.split("\\."); - numbers = new int[split.length]; - for (var i = 0; i < split.length; i++) { - numbers[i] = Integer.parseInt(split[i]); - } - } - - private Version(String name, String nameWithoutQualifier, int[] numbers, String qualifier) { - this.name = name; - this.nameWithoutQualifier = nameWithoutQualifier; - this.numbers = Arrays.copyOf(numbers, numbers.length); - this.qualifier = qualifier; - } - - public int getMajor() { - return numbers.length > 0 ? numbers[0] : 0; - } - - public int getMinor() { - return numbers.length > 1 ? numbers[1] : 0; - } - - public int getPatch() { - return numbers.length > 2 ? numbers[2] : 0; - } - - public int getBuild() { - return numbers.length > 3 ? numbers[3] : 0; - } - - public String getName() { - return name; - } - - public String getQualifier() { - return qualifier; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof Version)) { - return false; - } - Version other = (Version) o; - return getMajor() == other.getMajor() - && getMinor() == other.getMinor() - && getPatch() == other.getPatch() - && getBuild() == other.getBuild() - && qualifier.equals(other.qualifier); - } - - @Override - public int hashCode() { - var result = Integer.hashCode(getMajor()); - result = 31 * result + Integer.hashCode(getMinor()); - result = 31 * result + Integer.hashCode(getPatch()); - result = 31 * result + Integer.hashCode(getBuild()); - result = 31 * result + qualifier.hashCode(); - return result; - } - - @Override - public int compareTo(Version other) { - var c = compareToIgnoreQualifier(other); - if (c == 0) { - if ("".equals(qualifier)) { - c = "".equals(other.qualifier) ? 0 : 1; - } else if ("".equals(other.qualifier)) { - c = -1; - } else { - c = qualifier.compareTo(other.qualifier); - } - } - return c; - } - - public int compareToIgnoreQualifier(Version other) { - var maxNumbers = Math.max(numbers.length, other.numbers.length); - var myNumbers = Arrays.copyOf(numbers, maxNumbers); - var otherNumbers = Arrays.copyOf(other.numbers, maxNumbers); - for (var i = 0; i < maxNumbers; i++) { - var compare = Integer.compare(myNumbers[i], otherNumbers[i]); - if (compare != 0) { - return compare; - } - } - return 0; - } - - @Override - public String toString() { - return name; - } - - public static Version create(String version) { - return new Version(version); - } - - public Version removeQualifier() { - return new Version(nameWithoutQualifier, nameWithoutQualifier, numbers, ""); - } - - public boolean satisfiesMinRequirement(Version minRequirement) { - return this.compareToIgnoreQualifier(minRequirement) >= 0; - } -} diff --git a/manifest/src/test/java/org/sonar/plugin/api/manifest/PluginManifestTest.java b/manifest/src/test/java/org/sonar/plugin/api/manifest/PluginManifestTest.java index b2e6a421..08b69935 100644 --- a/manifest/src/test/java/org/sonar/plugin/api/manifest/PluginManifestTest.java +++ b/manifest/src/test/java/org/sonar/plugin/api/manifest/PluginManifestTest.java @@ -28,6 +28,7 @@ import java.util.jar.Attributes; import java.util.jar.Manifest; import org.junit.jupiter.api.Test; +import org.sonar.api.utils.Version; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/manifest/src/test/java/org/sonar/plugin/api/manifest/RequiredPluginTest.java b/manifest/src/test/java/org/sonar/plugin/api/manifest/RequiredPluginTest.java index f6a196dc..da4c1dba 100644 --- a/manifest/src/test/java/org/sonar/plugin/api/manifest/RequiredPluginTest.java +++ b/manifest/src/test/java/org/sonar/plugin/api/manifest/RequiredPluginTest.java @@ -30,7 +30,7 @@ class RequiredPluginTest { void test_RequiredPlugin() { var plugin = RequiredPlugin.parse("java:1.1"); assertThat(plugin.getKey()).isEqualTo("java"); - assertThat(plugin.getMinimalVersion().getName()).isEqualTo("1.1"); + assertThat(plugin.getMinimalVersion()).hasToString("1.1"); assertThrows(IllegalArgumentException.class, () -> RequiredPlugin.parse("java")); } diff --git a/manifest/src/test/java/org/sonar/plugin/api/manifest/VersionTests.java b/manifest/src/test/java/org/sonar/plugin/api/manifest/VersionTests.java deleted file mode 100644 index 3de0f653..00000000 --- a/manifest/src/test/java/org/sonar/plugin/api/manifest/VersionTests.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Sonar Plugin API - * Copyright (C) 2009-2025 SonarSource Sàrl - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.plugin.api.manifest; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -class VersionTests { - - @Test - void test_fields_of_snapshot_versions() { - var version = Version.create("1.2.3-SNAPSHOT"); - assertThat(version.getMajor()).isEqualTo(1); - assertThat(version.getMinor()).isEqualTo(2); - assertThat(version.getPatch()).isEqualTo(3); - assertThat(version.getBuild()).isZero(); - assertThat(version.getQualifier()).isEqualTo("SNAPSHOT"); - } - - @Test - void test_fields_of_releases() { - var version = Version.create("1.2"); - assertThat(version.getMajor()).isEqualTo(1); - assertThat(version.getMinor()).isEqualTo(2); - assertThat(version.getPatch()).isZero(); - assertThat(version.getBuild()).isZero(); - assertThat(version.getQualifier()).isEmpty(); - } - - @Test - void compare_releases() { - var version12 = Version.create("1.2"); - var version121 = Version.create("1.2.1"); - - assertThat(version12) - .hasToString("1.2") - .isEqualByComparingTo(version12); - assertThat(version121) - .isEqualByComparingTo(version121) - .isGreaterThan(version12); - } - - @Test - void compare_snapshots() { - var version12 = Version.create("1.2"); - var version12Snapshot = Version.create("1.2-SNAPSHOT"); - var version121Snapshot = Version.create("1.2.1-SNAPSHOT"); - var version12RC = Version.create("1.2-RC1"); - - assertThat(version12).isGreaterThan(version12Snapshot); - assertThat(version12Snapshot).isEqualByComparingTo(version12Snapshot); - assertThat(version121Snapshot).isGreaterThan(version12Snapshot); - assertThat(version12Snapshot).isGreaterThan(version12RC); - } - - @Test - void compare_release_candidates() { - var version12 = Version.create("1.2"); - var version12Snapshot = Version.create("1.2-SNAPSHOT"); - var version12RC1 = Version.create("1.2-RC1"); - var version12RC2 = Version.create("1.2-RC2"); - - assertThat(version12RC1) - .isLessThan(version12Snapshot) - .isEqualByComparingTo(version12RC1) - .isLessThan(version12RC2) - .isLessThan(version12); - } - - @Test - void testTrim() { - var version12 = Version.create(" 1.2 "); - - assertThat(version12.getName()).isEqualTo("1.2"); - assertThat(version12).isEqualTo(Version.create("1.2")); - } - - @Test - void testDefaultNumberIsZero() { - var version12 = Version.create("1.2"); - var version120 = Version.create("1.2.0"); - - assertThat(version12).isEqualTo(version120); - assertThat(version120).isEqualTo(version12); - } - - @Test - void testCompareOnTwoDigits() { - var version1dot10 = Version.create("1.10"); - var version1dot1 = Version.create("1.1"); - var version1dot9 = Version.create("1.9"); - - assertThat(version1dot10) - .isGreaterThan(version1dot1) - .isGreaterThan(version1dot9); - } - - @Test - void testFields() { - var version = Version.create("1.10.2"); - - assertThat(version.getName()).isEqualTo("1.10.2"); - assertThat(version).hasToString("1.10.2"); - assertThat(version.getMajor()).isEqualTo(1); - assertThat(version.getMinor()).isEqualTo(10); - assertThat(version.getPatch()).isEqualTo(2); - assertThat(version.getBuild()).isZero(); - } - - @Test - void testPatchFieldsEquals() { - var version = Version.create("1.2.3.4"); - - assertThat(version.getPatch()).isEqualTo(3); - assertThat(version.getBuild()).isEqualTo(4); - - assertThat(version) - .isEqualTo(version) - .isEqualTo(Version.create("1.2.3.4")) - .isNotEqualTo(Version.create("1.2.3.5")); - } - - @Test - void removeQualifier() { - var version = Version.create("1.2.3-SNAPSHOT").removeQualifier(); - - assertThat(version.getMajor()).isEqualTo(1); - assertThat(version.getMinor()).isEqualTo(2); - assertThat(version.getPatch()).isEqualTo(3); - assertThat(version.getQualifier()).isEmpty(); - } - - @Test - void removeQualifier_when_no_qualifier() { - var version = Version.create("1.2.3").removeQualifier(); - - assertThat(version.getMajor()).isEqualTo(1); - assertThat(version.getMinor()).isEqualTo(2); - assertThat(version.getPatch()).isEqualTo(3); - assertThat(version.getQualifier()).isEmpty(); - assertThat(version.getName()).isEqualTo("1.2.3"); - } - - @Test - void satisfiesMinRequirement_should_return_true_when_equal() { - var version = Version.create("1.2.3"); - var minRequirement = Version.create("1.2.3"); - - assertThat(version.satisfiesMinRequirement(minRequirement)).isTrue(); - } - - @Test - void satisfiesMinRequirement_should_return_true_when_greater() { - var version = Version.create("1.3.0"); - var minRequirement = Version.create("1.2.3"); - - assertThat(version.satisfiesMinRequirement(minRequirement)).isTrue(); - } - - @Test - void satisfiesMinRequirement_should_return_false_when_lower() { - var version = Version.create("1.2.2"); - var minRequirement = Version.create("1.2.3"); - - assertThat(version.satisfiesMinRequirement(minRequirement)).isFalse(); - } - - @Test - void satisfiesMinRequirement_should_ignore_qualifier() { - var version = Version.create("1.2.3-SNAPSHOT"); - var minRequirement = Version.create("1.2.3-RC1"); - - assertThat(version.satisfiesMinRequirement(minRequirement)).isTrue(); - } - - @Test - void compareToIgnoreQualifier_should_return_zero_when_equal() { - var version1 = Version.create("1.2.3-SNAPSHOT"); - var version2 = Version.create("1.2.3-RC1"); - - assertThat(version1.compareToIgnoreQualifier(version2)).isZero(); - } - - @Test - void compareToIgnoreQualifier_should_return_positive_when_greater() { - var version1 = Version.create("1.3.0-SNAPSHOT"); - var version2 = Version.create("1.2.3-SNAPSHOT"); - - assertThat(version1.compareToIgnoreQualifier(version2)).isPositive(); - } - - @Test - void compareToIgnoreQualifier_should_return_negative_when_lower() { - var version1 = Version.create("1.2.2-SNAPSHOT"); - var version2 = Version.create("1.2.3-SNAPSHOT"); - - assertThat(version1.compareToIgnoreQualifier(version2)).isNegative(); - } - - @Test - void equals_should_return_false_for_null() { - var version = Version.create("1.2.3"); - - assertThat(version.equals(null)).isFalse(); - } - - @Test - void equals_should_return_false_for_different_type() { - var version = Version.create("1.2.3"); - - assertThat(version.equals("1.2.3")).isFalse(); - } - - @Test - void equals_should_return_true_for_same_instance() { - var version = Version.create("1.2.3"); - - assertThat(version.equals(version)).isTrue(); - } - - @Test - void hashCode_should_be_consistent() { - var version1 = Version.create("1.2.3-SNAPSHOT"); - var version2 = Version.create("1.2.3-SNAPSHOT"); - - assertThat(version1).hasSameHashCodeAs(version2); - } - - @Test - void hashCode_should_differ_for_different_versions() { - var version1 = Version.create("1.2.3"); - var version2 = Version.create("1.2.4"); - - assertThat(version1.hashCode()).isNotEqualTo(version2.hashCode()); - } - - @Test - void hashCode_should_differ_for_different_qualifiers() { - var version1 = Version.create("1.2.3-SNAPSHOT"); - var version2 = Version.create("1.2.3-RC1"); - - assertThat(version1.hashCode()).isNotEqualTo(version2.hashCode()); - } -} diff --git a/plugin-api/build.gradle b/plugin-api/build.gradle index adcf4284..d8b784ac 100644 --- a/plugin-api/build.gradle +++ b/plugin-api/build.gradle @@ -2,6 +2,7 @@ apply plugin: 'com.github.johnrengelman.shadow' dependencies { api libs.slf4j + api project(':shared') // please keep the list grouped by configuration and ordered by name implementation libs.commons.io diff --git a/plugin-api/src/test/java/org/sonar/api/utils/VersionTest.java b/plugin-api/src/test/java/org/sonar/api/utils/VersionTest.java deleted file mode 100644 index a308185c..00000000 --- a/plugin-api/src/test/java/org/sonar/api/utils/VersionTest.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Sonar Plugin API - * Copyright (C) 2009-2025 SonarSource Sàrl - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.api.utils; - -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.sonar.api.utils.Version.parse; - -public class VersionTest { - - @Test - public void test_parse() { - assertVersion(parse(""), 0, 0, 0, 0, ""); - assertVersion(parse("1"), 1, 0, 0, 0, ""); - assertVersion(parse("1.2"), 1, 2, 0, 0,""); - assertVersion(parse("1.2.3"), 1, 2, 3, 0,""); - assertVersion(parse("1.2-beta-1"), 1, 2, 0, 0,"beta-1"); - assertVersion(parse("1.2.3-beta1"), 1, 2, 3, 0,"beta1"); - assertVersion(parse("1.2.3-beta-1"), 1, 2, 3, 0,"beta-1"); - assertVersion(parse("1.2.3.4567"), 1, 2, 3, 4567,""); - assertVersion(parse("1.2.3.4567-alpha"), 1, 2, 3, 4567,"alpha"); - } - - @Test - public void parse_throws_IAE_if_more_than_4_fields() { - assertThatThrownBy(() -> parse("1.2.3.456.7")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Maximum 4 fields are accepted: 1.2.3.456.7"); - } - - @Test - public void test_equals() { - Version one = parse("1"); - assertThat(one).isEqualTo(one); - assertThat(one).isEqualTo(parse("1")); - assertThat(one).isEqualTo(parse("1.0")); - assertThat(one).isEqualTo(parse("1.0.0")); - assertThat(one).isNotEqualTo(parse("1.2.3")); - assertThat(one).isNotEqualTo("1"); - - assertThat(parse("1.2.3")).isEqualTo(parse("1.2.3")); - assertThat(parse("1.2.3")).isNotEqualTo(parse("1.2.4")); - assertThat(parse("1.2.3")).isEqualTo(parse("1.2.3-b1")); - assertThat(parse("1.2.3-b1")).isEqualTo(parse("1.2.3-b2")); - } - - @Test - public void test_hashCode() { - assertThat(parse("1")).hasSameHashCodeAs(parse("1")); - assertThat(parse("1")).hasSameHashCodeAs(parse("1.0.0")); - assertThat(parse("1.2.3-beta1")).hasSameHashCodeAs(parse("1.2.3")); - } - - @Test - public void test_compareTo() { - assertThat(parse("1.2")).isEqualByComparingTo(parse("1.2.0")); - assertThat(parse("1.2.3")).isEqualByComparingTo(parse("1.2.3")); - assertThat(parse("1.2.3").compareTo(parse("1.2.4"))).isLessThan(0); - assertThat(parse("1.2.3").compareTo(parse("1.3"))).isLessThan(0); - assertThat(parse("1.2.3").compareTo(parse("2.1"))).isLessThan(0); - assertThat(parse("1.2.3").compareTo(parse("2.0.0"))).isLessThan(0); - assertThat(parse("2.0").compareTo(parse("1.2"))).isGreaterThan(0); - } - - @Test - public void compareTo_handles_build_number() { - assertThat(parse("1.2")).isEqualByComparingTo(parse("1.2.0.0")); - assertThat(parse("1.2.3.1234").compareTo(parse("1.2.3.4567"))).isLessThan(0); - assertThat(parse("1.2.3.1234").compareTo(parse("1.2.3"))).isGreaterThan(0); - assertThat(parse("1.2.3.1234").compareTo(parse("1.2.4"))).isLessThan(0); - assertThat(parse("1.2.3.9999").compareTo(parse("1.2.4.1111"))).isLessThan(0); - } - - @Test - public void qualifier_is_ignored_from_comparison() { - assertThat(parse("1.2.3")).isEqualTo(parse("1.2.3-build1")); - assertThat(parse("1.2.3")).isEqualTo(parse("1.2.3-build1")); - assertThat(parse("1.2.3")).isEqualByComparingTo(parse("1.2.3-build1")); - } - - @Test - public void test_toString() { - assertThat(parse("1").toString()).isEqualTo("1.0"); - assertThat(parse("1.2").toString()).isEqualTo("1.2"); - assertThat(parse("1.2.3").toString()).isEqualTo("1.2.3"); - assertThat(parse("1.2-b1").toString()).isEqualTo("1.2-b1"); - assertThat(parse("1.2.3-b1").toString()).isEqualTo("1.2.3-b1"); - assertThat(parse("1.2.3.4567").toString()).isEqualTo("1.2.3.4567"); - assertThat(parse("1.2.3.4567-beta1").toString()).isEqualTo("1.2.3.4567-beta1"); - - // do not display zero numbers when possible - assertThat(parse("1.2.0.0").toString()).isEqualTo("1.2"); - assertThat(parse("1.2.0.1").toString()).isEqualTo("1.2.0.1"); - assertThat(parse("1.2.1.0").toString()).isEqualTo("1.2.1"); - assertThat(parse("1.2.1.0-beta").toString()).isEqualTo("1.2.1-beta"); - } - - @Test - public void test_create() { - assertVersion(Version.create(1, 2), 1, 2, 0, 0, ""); - assertVersion(Version.create(1, 2, 3), 1, 2, 3, 0, ""); - - } - - private static void assertVersion(Version version, - int expectedMajor, int expectedMinor, int expectedPatch, long expectedBuildNumber, String expectedQualifier) { - assertThat(version.major()).isEqualTo(expectedMajor); - assertThat(version.minor()).isEqualTo(expectedMinor); - assertThat(version.patch()).isEqualTo(expectedPatch); - assertThat(version.buildNumber()).isEqualTo(expectedBuildNumber); - assertThat(version.qualifier()).isEqualTo(expectedQualifier); - } -} diff --git a/settings.gradle b/settings.gradle index ead0da70..6d999c99 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,6 +3,7 @@ include 'plugin-api' include 'check-api' include 'test-fixtures' include 'manifest' +include 'shared' dependencyResolutionManagement { versionCatalogs { diff --git a/shared/build.gradle b/shared/build.gradle new file mode 100644 index 00000000..50e39206 --- /dev/null +++ b/shared/build.gradle @@ -0,0 +1,51 @@ +description = 'Sonar Plugin API - Shared' + +dependencies { + compileOnly libs.jsr305 + + testImplementation libs.junit5 + testImplementation libs.assertj + testRuntimeOnly libs.jupiter.engine +} + +artifactoryPublish.skip = false + +publishing { + publications { + mavenJava(MavenPublication) { + artifactId = 'sonar-plugin-api-shared' + from components.java + pom { + name = 'Sonar Plugin API - Shared' + description = 'Sonar Plugin API - Shared' + url = 'https://www.sonarsource.com/' + organization { + name = 'SonarSource' + url = 'https://www.sonarsource.com/' + } + licenses { + license { + name = 'GNU LGPL 3' + url = 'https://www.gnu.org/licenses/lgpl-3.0.txt' + distribution = 'repo' + } + } + scm { + url = 'https://github.com/SonarSource/sonar-plugin-api' + } + developers { + developer { + id = 'sonarsource-team' + name = 'SonarSource Team' + } + } + } + artifact sourcesJar + artifact javadocJar + } + } +} + +test { + useJUnitPlatform() +} diff --git a/plugin-api/src/main/java/org/sonar/api/utils/Version.java b/shared/src/main/java/org/sonar/api/utils/Version.java similarity index 72% rename from plugin-api/src/main/java/org/sonar/api/utils/Version.java rename to shared/src/main/java/org/sonar/api/utils/Version.java index f2e25d41..fc627d59 100644 --- a/plugin-api/src/main/java/org/sonar/api/utils/Version.java +++ b/shared/src/main/java/org/sonar/api/utils/Version.java @@ -19,31 +19,25 @@ */ package org.sonar.api.utils; -import java.util.regex.Pattern; import javax.annotation.concurrent.Immutable; import static java.lang.Integer.parseInt; import static java.lang.Long.parseLong; import static java.util.Objects.requireNonNull; -import static org.apache.commons.lang3.StringUtils.substringAfter; -import static org.apache.commons.lang3.StringUtils.substringBefore; -import static org.apache.commons.lang3.StringUtils.trimToEmpty; /** - * Version composed of maximum four fields (major, minor, patch and build ID numbers) and optionally a qualifier. + * Version composed of maximum four fields (major, minor, patch and build number) and optionally a qualifier. *

* Examples: 1.0, 1.0.0, 1.2.3, 1.2-beta1, 1.2.1-beta-1, 1.2.3.4567 *

- *

IMPORTANT NOTE

- * Qualifier is ignored when comparing objects (methods {@link #equals(Object)}, {@link #hashCode()} - * and {@link #compareTo(Version)}). + *

Qualifier ordering

+ * When numeric fields are equal, the qualifier determines ordering: + * a release (empty qualifier) is greater than any pre-release qualifier, + * and pre-release qualifiers are compared lexicographically. *

- *

- *   assertThat(Version.parse("1.2")).isEqualTo(Version.parse("1.2-beta1"));
- *   assertThat(Version.parse("1.2").compareTo(Version.parse("1.2-beta1"))).isZero();
- * 
- * - * @since 5.5 + * The default comparison implemented in {@link #compareTo(Version)} ignores the qualifier, so that for example "1.2.3" is considered equal to "1.2.3-beta-1". + *

+ * Use {@link #compareToIncludingQualifier(Version)} to compare including qualifier. */ @Immutable public class Version implements Comparable { @@ -83,8 +77,6 @@ public int patch() { /** * Build number if the fourth field, for example {@code 12345} for "6.3.0.12345". * If absent, then value is zero. - * - * @since 6.3 */ public long buildNumber() { return buildNumber; @@ -115,12 +107,14 @@ public String qualifier() { * if it defines 5 integer-sequences. */ public static Version parse(String text) { - String s = trimToEmpty(text); - String qualifier = substringAfter(s, QUALIFIER_SEPARATOR); - if (!qualifier.isEmpty()) { - s = substringBefore(s, QUALIFIER_SEPARATOR); + String s = text.trim(); + String qualifier = DEFAULT_QUALIFIER; + int dashIdx = s.indexOf(QUALIFIER_SEPARATOR); + if (dashIdx >= 0) { + qualifier = s.substring(dashIdx + 1); + s = s.substring(0, dashIdx); } - String[] fields = s.split(Pattern.quote(SEQUENCE_SEPARATOR)); + String[] fields = s.isEmpty() ? new String[0] : s.split("\\."); int major = 0; int minor = 0; int patch = DEFAULT_PATCH; @@ -144,6 +138,13 @@ public static Version parse(String text) { return new Version(major, minor, patch, buildNumber, qualifier); } + /** + * Alias for {@link #parse(String)}. + */ + public static Version create(String text) { + return parse(text); + } + public static Version create(int major, int minor) { return new Version(major, minor, DEFAULT_PATCH, DEFAULT_BUILD_NUMBER, DEFAULT_QUALIFIER); } @@ -170,6 +171,38 @@ public boolean isGreaterThanOrEqual(Version than) { return this.compareTo(than) >= 0; } + /** + * Returns a new Version with the same numeric fields but an empty qualifier. + */ + public Version removeQualifier() { + return new Version(major, minor, patch, buildNumber, DEFAULT_QUALIFIER); + } + + /** + * Returns true if this version is greater than or equal to the given minimum requirement, + * ignoring qualifier. + */ + public boolean satisfiesMinRequirement(Version minRequirement) { + return this.compareToIncludingQualifier(minRequirement) >= 0; + } + + /** + * Compares this version to another, including qualifier. + */ + public int compareToIncludingQualifier(Version other) { + int c = compareTo(other); + if (c == 0) { + if (qualifier.isEmpty()) { + c = other.qualifier.isEmpty() ? 0 : 1; + } else if (other.qualifier.isEmpty()) { + c = -1; + } else { + c = qualifier.compareTo(other.qualifier); + } + } + return c; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -188,7 +221,10 @@ public boolean equals(Object o) { if (patch != version.patch) { return false; } - return buildNumber == version.buildNumber; + if (buildNumber != version.buildNumber) { + return false; + } + return qualifier.equals(version.qualifier); } @Override @@ -196,10 +232,14 @@ public int hashCode() { int result = major; result = 31 * result + minor; result = 31 * result + patch; - result = 31 * result + (int) (buildNumber ^ (buildNumber >>> 32)); + result = 31 * result + Long.hashCode(buildNumber); + result = 31 * result + qualifier.hashCode(); return result; } + /** + * Compares this version to another, ignoring qualifier. + */ @Override public int compareTo(Version other) { int c = major - other.major; diff --git a/shared/src/main/java/org/sonar/api/utils/package-info.java b/shared/src/main/java/org/sonar/api/utils/package-info.java new file mode 100644 index 00000000..9be3a9ac --- /dev/null +++ b/shared/src/main/java/org/sonar/api/utils/package-info.java @@ -0,0 +1,23 @@ +/* + * Sonar Plugin API + * Copyright (C) 2009-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.api.utils; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/shared/src/test/java/org/sonar/api/utils/VersionTest.java b/shared/src/test/java/org/sonar/api/utils/VersionTest.java new file mode 100644 index 00000000..5628d7f7 --- /dev/null +++ b/shared/src/test/java/org/sonar/api/utils/VersionTest.java @@ -0,0 +1,338 @@ +/* + * Sonar Plugin API + * Copyright (C) 2009-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.api.utils; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.sonar.api.utils.Version.parse; + +class VersionTest { + + @Test + void test_parse() { + assertVersion(parse(""), 0, 0, 0, 0L, ""); + assertVersion(parse("1"), 1, 0, 0, 0L, ""); + assertVersion(parse("1.2"), 1, 2, 0, 0L, ""); + assertVersion(parse("1.2.3"), 1, 2, 3, 0L, ""); + assertVersion(parse("1.2-beta-1"), 1, 2, 0, 0L, "beta-1"); + assertVersion(parse("1.2.3-beta1"), 1, 2, 3, 0L, "beta1"); + assertVersion(parse("1.2.3-beta-1"), 1, 2, 3, 0L, "beta-1"); + assertVersion(parse("1.2.3.4567"), 1, 2, 3, 4567L, ""); + assertVersion(parse("1.2.3.4567-alpha"), 1, 2, 3, 4567L, "alpha"); + } + + @Test + void parse_throws_IAE_if_more_than_4_fields() { + assertThatThrownBy(() -> parse("1.2.3.456.7")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Maximum 4 fields are accepted: 1.2.3.456.7"); + } + + @Test + void test_create_int() { + assertVersion(Version.create(1, 2), 1, 2, 0, 0L, ""); + assertVersion(Version.create(1, 2, 3), 1, 2, 3, 0L, ""); + } + + @Test + void test_fields_of_snapshot_versions() { + assertVersion(Version.create("1.2.3-SNAPSHOT"), 1, 2, 3, 0L, "SNAPSHOT"); + } + + @Test + void test_fields_of_releases() { + assertVersion(Version.create("1.2"), 1, 2, 0, 0L, ""); + } + + @Test + void test_equals() { + Version one = parse("1"); + assertThat(one) + .isEqualTo(one) + .isEqualTo(parse("1")) + .isEqualTo(parse("1.0")) + .isEqualTo(parse("1.0.0")) + .isNotEqualTo(parse("1.2.3")) + .isNotEqualTo("1"); + + assertThat(parse("1.2.3")).isEqualTo(parse("1.2.3")); + assertThat(parse("1.2.3")).isNotEqualTo(parse("1.2.4")); + assertThat(parse("1.2.3")).isNotEqualTo(parse("1.2.3-b1")); + assertThat(parse("1.2.3-b1")).isNotEqualTo(parse("1.2.3-b2")); + assertThat(parse("1.2.3-b1")).isEqualTo(parse("1.2.3-b1")); + } + + @Test + void equals_should_return_false_for_null() { + var version = Version.create("1.2.3"); + assertThat(version.equals(null)).isFalse(); + } + + @Test + void equals_should_return_false_for_different_type() { + var version = Version.create("1.2.3"); + assertThat(version.equals("1.2.3")).isFalse(); + } + + @Test + void equals_should_return_true_for_same_instance() { + var version = Version.create("1.2.3"); + assertThat(version.equals(version)).isTrue(); + } + + @Test + void test_hashCode() { + assertThat(parse("1")).hasSameHashCodeAs(parse("1")); + assertThat(parse("1")).hasSameHashCodeAs(parse("1.0.0")); + assertThat(parse("1.2.3-beta1")).isNotEqualTo(parse("1.2.3")); + } + + @Test + void hashCode_should_be_consistent() { + var version1 = Version.create("1.2.3-SNAPSHOT"); + var version2 = Version.create("1.2.3-SNAPSHOT"); + assertThat(version1).hasSameHashCodeAs(version2); + } + + @Test + void hashCode_should_differ_for_different_versions() { + var version1 = Version.create("1.2.3"); + var version2 = Version.create("1.2.4"); + assertThat(version1.hashCode()).isNotEqualTo(version2.hashCode()); + } + + @Test + void test_compareTo() { + assertThat(parse("1.2")).isEqualByComparingTo(parse("1.2.0")); + assertThat(parse("1.2.3")).isEqualByComparingTo(parse("1.2.3")); + assertThat(parse("1.2.3").compareTo(parse("1.2.4"))).isLessThan(0); + assertThat(parse("1.2.3").compareTo(parse("1.3"))).isLessThan(0); + assertThat(parse("1.2.3").compareTo(parse("2.1"))).isLessThan(0); + assertThat(parse("1.2.3").compareTo(parse("2.0.0"))).isLessThan(0); + assertThat(parse("2.0").compareTo(parse("1.2"))).isGreaterThan(0); + } + + @Test + void test_isGreaterThanOrEqual() { + assertThat(parse("1.2").isGreaterThanOrEqual(parse("1.2.0"))).isTrue(); + assertThat(parse("1.2.3").isGreaterThanOrEqual(parse("1.2.4"))).isFalse(); + assertThat(parse("2.0").isGreaterThanOrEqual(parse("1.2.4"))).isTrue(); + } + + @Test + void compareTo_handles_build_number() { + assertThat(parse("1.2")).isEqualByComparingTo(parse("1.2.0.0")); + assertThat(parse("1.2.3.1234").compareTo(parse("1.2.3.4567"))).isLessThan(0); + assertThat(parse("1.2.3.1234").compareTo(parse("1.2.3"))).isGreaterThan(0); + assertThat(parse("1.2.3.1234").compareTo(parse("1.2.4"))).isLessThan(0); + assertThat(parse("1.2.3.9999").compareTo(parse("1.2.4.1111"))).isLessThan(0); + } + + @Test + void compareToIncludingQualifier_is_qualifier_aware() { + assertThat(parse("1.2.3").compareToIncludingQualifier(parse("1.2.3-build1"))).isPositive(); + assertThat(parse("1.2").compareToIncludingQualifier(parse("1.2-SNAPSHOT"))).isPositive(); + assertThat(parse("1.2-SNAPSHOT").compareToIncludingQualifier(parse("1.2-RC1"))).isPositive(); + } + + @Test + void compareToIncludingQualifier_snapshots() { + var version12 = Version.create("1.2"); + var version12Snapshot = Version.create("1.2-SNAPSHOT"); + var version121Snapshot = Version.create("1.2.1-SNAPSHOT"); + var version12RC = Version.create("1.2-RC1"); + + assertThat(version12.compareToIncludingQualifier(version12Snapshot)).isPositive(); + assertThat(version12Snapshot.compareToIncludingQualifier(version12Snapshot)).isZero(); + assertThat(version121Snapshot.compareToIncludingQualifier(version12Snapshot)).isPositive(); + assertThat(version12Snapshot.compareToIncludingQualifier(version12RC)).isPositive(); + } + + @Test + void compareToIncludingQualifier_release_candidates() { + var version12 = Version.create("1.2"); + var version12Snapshot = Version.create("1.2-SNAPSHOT"); + var version12RC1 = Version.create("1.2-RC1"); + var version12RC2 = Version.create("1.2-RC2"); + + assertThat(version12RC1.compareToIncludingQualifier(version12Snapshot)).isNegative(); + assertThat(version12RC1.compareToIncludingQualifier(version12RC1)).isZero(); + assertThat(version12RC1.compareToIncludingQualifier(version12RC2)).isNegative(); + assertThat(version12RC1.compareToIncludingQualifier(version12)).isNegative(); + } + + @Test + void compare_releases() { + var version12 = Version.create("1.2"); + var version121 = Version.create("1.2.1"); + + assertThat(version12) + .hasToString("1.2") + .isEqualByComparingTo(version12); + assertThat(version121) + .isEqualByComparingTo(version121) + .isGreaterThan(version12); + } + + @Test + void testTrim() { + var version12 = Version.create(" 1.2 "); + + assertThat(version12) + .hasToString("1.2") + .isEqualTo(Version.create("1.2")); + } + + @Test + void testDefaultNumberIsZero() { + var version12 = Version.create("1.2"); + var version120 = Version.create("1.2.0"); + + assertThat(version12).isEqualTo(version120); + assertThat(version120).isEqualTo(version12); + } + + @Test + void testCompareOnTwoDigits() { + var version1dot10 = Version.create("1.10"); + var version1dot1 = Version.create("1.1"); + var version1dot9 = Version.create("1.9"); + + assertThat(version1dot10) + .isGreaterThan(version1dot1) + .isGreaterThan(version1dot9); + } + + @Test + void testFields() { + var version = Version.create("1.10.2"); + + assertThat(version).hasToString("1.10.2"); + assertVersion(version, 1, 10, 2, 0L, ""); + } + + @Test + void testPatchFieldsEquals() { + var version = Version.create("1.2.3.4"); + + assertThat(version.patch()).isEqualTo(3); + assertThat(version.buildNumber()).isEqualTo(4L); + + assertThat(version) + .isEqualTo(version) + .isEqualTo(Version.create("1.2.3.4")) + .isNotEqualTo(Version.create("1.2.3.5")); + } + + @Test + void test_toString() { + assertThat(parse("1")).hasToString("1.0"); + assertThat(parse("1.2")).hasToString("1.2"); + assertThat(parse("1.2.3")).hasToString("1.2.3"); + assertThat(parse("1.2-b1")).hasToString("1.2-b1"); + assertThat(parse("1.2.3-b1")).hasToString("1.2.3-b1"); + assertThat(parse("1.2.3.4567")).hasToString("1.2.3.4567"); + assertThat(parse("1.2.3.4567-beta1")).hasToString("1.2.3.4567-beta1"); + assertThat(parse("1.2.0.0")).hasToString("1.2"); + assertThat(parse("1.2.0.1")).hasToString("1.2.0.1"); + assertThat(parse("1.2.1.0")).hasToString("1.2.1"); + assertThat(parse("1.2.1.0-beta")).hasToString("1.2.1-beta"); + } + + @Test + void removeQualifier() { + assertVersion(Version.create("1.2.3-SNAPSHOT").removeQualifier(), 1, 2, 3, 0L, ""); + } + + @Test + void removeQualifier_when_no_qualifier() { + var version = Version.create("1.2.3").removeQualifier(); + + assertThat(version).hasToString("1.2.3"); + assertVersion(version, 1, 2, 3, 0L, ""); + } + + @Test + void satisfiesMinRequirement_should_return_true_when_equal() { + var version = Version.create("1.2.3"); + var minRequirement = Version.create("1.2.3"); + + assertThat(version.satisfiesMinRequirement(minRequirement)).isTrue(); + } + + @Test + void satisfiesMinRequirement_should_return_true_when_greater() { + var version = Version.create("1.3.0"); + var minRequirement = Version.create("1.2.3"); + + assertThat(version.satisfiesMinRequirement(minRequirement)).isTrue(); + } + + @Test + void satisfiesMinRequirement_should_return_false_when_lower() { + var version = Version.create("1.2.2"); + var minRequirement = Version.create("1.2.3"); + + assertThat(version.satisfiesMinRequirement(minRequirement)).isFalse(); + } + + @Test + void satisfiesMinRequirement_should_ignore_qualifier() { + var version = Version.create("1.2.3-SNAPSHOT"); + var minRequirement = Version.create("1.2.3-RC1"); + + assertThat(version.satisfiesMinRequirement(minRequirement)).isTrue(); + } + + @Test + void compareToIncludingQualifier_should_return_zero_when_equal() { + var version1 = Version.create("1.2.3-SNAPSHOT"); + var version2 = Version.create("1.2.3-SNAPSHOT"); + + assertThat(version1.compareToIncludingQualifier(version2)).isZero(); + } + + @Test + void compareToIncludingQualifier_should_return_positive_when_greater() { + var version1 = Version.create("1.3.0-SNAPSHOT"); + var version2 = Version.create("1.2.3-SNAPSHOT"); + + assertThat(version1.compareToIncludingQualifier(version2)).isPositive(); + } + + @Test + void compareToIncludingQualifier_should_return_negative_when_lower() { + var version1 = Version.create("1.2.2-SNAPSHOT"); + var version2 = Version.create("1.2.3-SNAPSHOT"); + + assertThat(version1.compareToIncludingQualifier(version2)).isNegative(); + } + + private static void assertVersion(Version version, + int expectedMajor, int expectedMinor, int expectedPatch, long expectedBuildNumber, String expectedQualifier) { + assertThat(version.major()).isEqualTo(expectedMajor); + assertThat(version.minor()).isEqualTo(expectedMinor); + assertThat(version.patch()).isEqualTo(expectedPatch); + assertThat(version.buildNumber()).isEqualTo(expectedBuildNumber); + assertThat(version.qualifier()).isEqualTo(expectedQualifier); + } +}