diff --git a/.cirrus.yml b/.cirrus.yml index 48e24b99e7..8715217681 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -86,7 +86,7 @@ build_task: build_script: - *log_develocity_url_script - source cirrus-env BUILD - - regular_mvn_build_deploy_analyze -Dmaven.test.skip=true -Dsonar.skip=true -pl '!java-checks-test-sources/default,!java-checks-test-sources/aws' + - regular_mvn_build_deploy_analyze -Dmaven.test.skip=true -Dsonar.skip=true -pl '!java-checks-test-sources/default,!java-checks-test-sources/aws,!java-checks-test-sources/spring-web-4.0' cleanup_before_cache_script: cleanup_maven_repository test_analyze_task: @@ -118,7 +118,7 @@ ws_scan_task: whitesource_script: - source cirrus-env QA - source set_maven_build_version $BUILD_NUMBER - - mvn clean install --batch-mode -Dmaven.test.skip=true -pl '!java-checks-test-sources,!java-checks-test-sources/default,!java-checks-test-sources/aws,!java-checks-test-sources/spring-3.2' + - mvn clean install --batch-mode -Dmaven.test.skip=true -pl '!java-checks-test-sources,!java-checks-test-sources/default,!java-checks-test-sources/aws,!java-checks-test-sources/spring-3.2,!java-checks-test-sources/spring-web-4.0' - source ws_scan.sh allow_failures: "true" always: diff --git a/its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java b/its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java index cb7fa747db..293d3fe5c8 100644 --- a/its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java +++ b/its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java @@ -120,7 +120,7 @@ public void javaCheckTestSources() throws Exception { .setProjectName(PROJECT_NAME) .setProjectVersion("0.1.0-SNAPSHOT") .setSourceEncoding("UTF-8") - .setSourceDirs("aws/src/main/java/,default/src/main/java/,java-17/src/main/java/,spring-3.2/src/main/java/") + .setSourceDirs("aws/src/main/java/,default/src/main/java/,java-17/src/main/java/,spring-3.2/src/main/java/,spring-web-4.0/src/main/java/") .setTestDirs("default/src/test/java/,test-classpath-reader/src/test/java") .setProperty("sonar.java.source", "22") // common properties diff --git a/java-checks-test-sources/pom.xml b/java-checks-test-sources/pom.xml index cf861824b3..99edd30931 100644 --- a/java-checks-test-sources/pom.xml +++ b/java-checks-test-sources/pom.xml @@ -21,6 +21,7 @@ java-17 test-classpath-reader spring-3.2 + spring-web-4.0 diff --git a/java-checks-test-sources/spring-web-4.0/pom.xml b/java-checks-test-sources/spring-web-4.0/pom.xml new file mode 100644 index 0000000000..299044b417 --- /dev/null +++ b/java-checks-test-sources/spring-web-4.0/pom.xml @@ -0,0 +1,121 @@ + + + 4.0.0 + + org.sonarsource.java + java-checks-test-sources + 8.13.0-SNAPSHOT + + + spring-web-4.0 + SonarQube Java :: Checks Test Sources :: Spring Web 4.0 + + + true + true + true + true + + + + + org.springframework + spring-web + 4.0.0.RELEASE + provided + + + + + + analyze-tests + + false + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 22 + 22 + 22 + + + + org.apache.maven.plugins + maven-surefire-plugin + + --enable-preview + + + + org.simplify4u.plugins + sign-maven-plugin + + + sign-artifacts + none + + + + + maven-jar-plugin + + + default-jar + none + + + + + maven-source-plugin + + + attach-sources + none + + + + + maven-javadoc-plugin + + + attach-javadocs + none + + + + + maven-install-plugin + + + default-install + none + + + + + com.mycila + license-maven-plugin + + + + + src/main/java/** + src/test/java/** + + + + + + + + + diff --git a/java-checks-test-sources/spring-web-4.0/src/main/java/checks/SpringComposedRequestMappingCheckSample.java b/java-checks-test-sources/spring-web-4.0/src/main/java/checks/SpringComposedRequestMappingCheckSample.java new file mode 100644 index 0000000000..90f277949e --- /dev/null +++ b/java-checks-test-sources/spring-web-4.0/src/main/java/checks/SpringComposedRequestMappingCheckSample.java @@ -0,0 +1,38 @@ +package checks; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import static org.springframework.web.bind.annotation.RequestMethod.POST; + + +/** + * This class serves as a sample for older spring-web version (4.0) where @GetMapping, @PostMapping and so on were not present + * hence no issues are expected to be reported here when using the generic @RequestMapping + */ +@RestController +@RequestMapping("/home") +public class SpringComposedRequestMappingCheckSample { + + @RequestMapping(method = RequestMethod.GET) + String m2() { + return ""; + } + + @RequestMapping(method = {POST}) + String m3() { + return ""; + } + + @RequestMapping(method = {RequestMethod.PUT}) + String m4() { + return ""; + } + + @RequestMapping(method = RequestMethod.PATCH) + String m5() { + return ""; + } + +} diff --git a/java-checks-testkit/src/main/java/org/sonar/java/checks/Constants.java b/java-checks-testkit/src/main/java/org/sonar/java/checks/Constants.java index 4ce42b52b5..13b4308ffa 100644 --- a/java-checks-testkit/src/main/java/org/sonar/java/checks/Constants.java +++ b/java-checks-testkit/src/main/java/org/sonar/java/checks/Constants.java @@ -23,4 +23,6 @@ private Constants() { public static final String SPRING_3_2 = "../java-checks-test-sources/spring-3.2"; public static final String SPRING_3_2_CLASSPATH = SPRING_3_2 + "/target/test-classpath.txt"; + public static final String SPRING_WEB_4_0_TEST_SOURCES = "../java-checks-test-sources/spring-web-4.0"; + public static final String SPRING_WEB_4_0_CLASSPATH = SPRING_WEB_4_0_TEST_SOURCES + "/target/test-classpath.txt"; } diff --git a/java-checks/src/main/java/org/sonar/java/checks/spring/SpringComposedRequestMappingCheck.java b/java-checks/src/main/java/org/sonar/java/checks/spring/SpringComposedRequestMappingCheck.java index da3bcba3b0..1414447a85 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/spring/SpringComposedRequestMappingCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/spring/SpringComposedRequestMappingCheck.java @@ -20,10 +20,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.function.Function; import java.util.stream.Stream; import org.sonar.check.Rule; +import org.sonar.plugins.java.api.DependencyVersionAware; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; import org.sonar.plugins.java.api.JavaFileScannerContext; +import org.sonar.plugins.java.api.Version; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.tree.AnnotationTree; import org.sonar.plugins.java.api.tree.AssignmentExpressionTree; @@ -34,7 +38,7 @@ import org.sonar.plugins.java.api.tree.Tree; @Rule(key = "S4488") -public class SpringComposedRequestMappingCheck extends IssuableSubscriptionVisitor { +public class SpringComposedRequestMappingCheck extends IssuableSubscriptionVisitor implements DependencyVersionAware { private static final Map PREFERRED_METHOD_MAP = buildPreferredMethodMap(); @@ -110,4 +114,11 @@ private static Stream extractValues(ExpressionTree argument) { } return Stream.of(expression); } + + @Override + public boolean isCompatibleWithDependencies(Function> dependencyFinder) { + return dependencyFinder.apply("spring-web") + .map(v -> v.isGreaterThanOrEqualTo("4.3")) + .orElse(false); + } } diff --git a/java-checks/src/test/java/org/sonar/java/checks/spring/SpringComposedRequestMappingCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/spring/SpringComposedRequestMappingCheckTest.java index 29fe5cc6e5..6d7d094060 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/spring/SpringComposedRequestMappingCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/spring/SpringComposedRequestMappingCheckTest.java @@ -17,7 +17,11 @@ package org.sonar.java.checks.spring; import org.junit.jupiter.api.Test; +import org.sonar.java.checks.Constants; import org.sonar.java.checks.verifier.CheckVerifier; +import org.sonar.java.test.classpath.TestClasspathUtils; + +import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPathInModule; class SpringComposedRequestMappingCheckTest { @@ -34,4 +38,13 @@ void test() { .verifyNoIssues(); } + @Test + void test_spring_web_4_0() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPathInModule(Constants.SPRING_WEB_4_0_TEST_SOURCES, "checks/SpringComposedRequestMappingCheckSample.java")) + .withCheck(new SpringComposedRequestMappingCheck()) + .withClassPath(TestClasspathUtils.loadFromFile(Constants.SPRING_WEB_4_0_CLASSPATH)) + .verifyNoIssues(); + } + } diff --git a/java-frontend/src/main/java/org/sonar/java/classpath/DependencyVersionInference.java b/java-frontend/src/main/java/org/sonar/java/classpath/DependencyVersionInference.java new file mode 100644 index 0000000000..36c8155106 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/classpath/DependencyVersionInference.java @@ -0,0 +1,51 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.classpath; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.sonar.plugins.java.api.Version; + +public class DependencyVersionInference { + + /** Cache for dependency retrieval. Indexed by artifactId. */ + private final Map> dependencyVersionsCache = new HashMap<>(); + + static Pattern makeJarPattern(String artifactId) { + return Pattern.compile(artifactId + "-" + VersionImpl.VERSION_REGEX + "\\.jar"); + } + + public Optional infer(String artifactId, List classpath) { + return dependencyVersionsCache + .computeIfAbsent(artifactId, key -> infer(makeJarPattern(key), classpath)); + } + + private static Optional infer(Pattern jarPattern, List classpath) { + for (File file : classpath) { + Matcher matcher = jarPattern.matcher(file.getName()); + if (matcher.matches()) { + return Optional.of(VersionImpl.matcherToVersion(matcher)); + } + } + return Optional.empty(); + } +} diff --git a/java-frontend/src/main/java/org/sonar/java/classpath/VersionImpl.java b/java-frontend/src/main/java/org/sonar/java/classpath/VersionImpl.java new file mode 100644 index 0000000000..baf7020ef5 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/classpath/VersionImpl.java @@ -0,0 +1,125 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.classpath; + +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.Nullable; +import org.sonar.plugins.java.api.Version; + +/** + * Class to parse and compare versions of library jars. + */ +public record VersionImpl(Integer major, Integer minor, @Nullable Integer patch, @Nullable String qualifier) implements Comparable, Version { + + public static String VERSION_REGEX = "([0-9]+).([0-9]+)(.[0-9]+)?([^0-9].*)?"; + + private static final Pattern VERSION_PATTERN = Pattern.compile(VERSION_REGEX); + + /** + * matcher must come from a match against a pattern that contains {@link #VERSION_REGEX} and no other groups. + */ + public static VersionImpl matcherToVersion(Matcher matcher) { + return new VersionImpl( + Integer.parseInt(matcher.group(1)), + Integer.parseInt(matcher.group(2)), + matcher.group(3) != null ? Integer.parseInt(matcher.group(3).substring(1)) : null, + matcher.group(4)); + } + + public static VersionImpl parse(String versionString) { + Matcher matcher = VERSION_PATTERN.matcher(versionString); + if (matcher.matches()) { + return matcherToVersion(matcher); + } + throw new IllegalArgumentException("Not a valid version string: " + versionString); + } + + /** + * Warning: this is a partial order: 2.5 and 2.5.1 are incomparable. + * Qualifiers are ignored. + */ + @Override + public int compareTo(Version o) { + if (!Objects.equals(major, o.major())) { + return major - o.major(); + } + if (!Objects.equals(minor, o.minor())) { + return minor - o.minor(); + } + if (!Objects.equals(patch, o.patch())) { + if (patch == null || o.patch() == null) return 0; + return patch - o.patch(); + } + return 0; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Version version)) return false; + return Objects.equals(major, version.major()) + && Objects.equals(minor, version.minor()) + && Objects.equals(patch, version.patch()) + && Objects.equals(qualifier, version.qualifier()); + } + + @Override + public int hashCode() { + return Objects.hash(major, minor, patch, qualifier); + } + + @Override + public String toString() { + return major + "." + minor + + (patch == null ? "" : ("." + patch)) + + (qualifier == null ? "" : qualifier); + } + + public boolean isGreaterThanOrEqualTo(Version version) { + return compareTo(version) >= 0; + } + + public boolean isGreaterThan(Version version) { + return compareTo(version) > 0; + } + + public boolean isLowerThanOrEqualTo(Version version) { + return compareTo(version) <= 0; + } + + public boolean isLowerThan(Version version) { + return compareTo(version) < 0; + } + + public boolean isGreaterThanOrEqualTo(String version) { + return isGreaterThanOrEqualTo(VersionImpl.parse(version)); + } + + public boolean isGreaterThan(String version) { + return isGreaterThan(VersionImpl.parse(version)); + } + + public boolean isLowerThanOrEqualTo(String version) { + return isLowerThanOrEqualTo(VersionImpl.parse(version)); + } + + public boolean isLowerThan(String version) { + return isLowerThan(VersionImpl.parse(version)); + } + +} diff --git a/java-frontend/src/main/java/org/sonar/java/model/VisitorsBridge.java b/java-frontend/src/main/java/org/sonar/java/model/VisitorsBridge.java index 9c25c6e43c..e3f500c2df 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/VisitorsBridge.java +++ b/java-frontend/src/main/java/org/sonar/java/model/VisitorsBridge.java @@ -43,8 +43,10 @@ import org.sonar.java.ast.visitors.SonarSymbolTableVisitor; import org.sonar.java.ast.visitors.SubscriptionVisitor; import org.sonar.java.caching.CacheContextImpl; +import org.sonar.java.classpath.DependencyVersionInference; import org.sonar.java.exceptions.ApiMismatchException; import org.sonar.java.exceptions.ThrowableUtils; +import org.sonar.plugins.java.api.DependencyVersionAware; import org.sonar.plugins.java.api.InputFileScannerContext; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; import org.sonar.plugins.java.api.JavaCheck; @@ -78,6 +80,7 @@ public class VisitorsBridge { private int skippedFileCount = 0; @VisibleForTesting CacheContext cacheContext; + private final DependencyVersionInference dependencyService; @VisibleForTesting public VisitorsBridge(JavaFileScanner visitor) { @@ -98,6 +101,7 @@ public VisitorsBridge(Iterable visitors, List project this.cacheContext = CacheContextImpl.of(sonarComponents); this.javaVersion = javaVersion; + dependencyService = new DependencyVersionInference(); updateScanners(); } @@ -105,12 +109,21 @@ private void updateScanners() { allScanners.clear(); scannersThatCannotBeSkipped.clear(); - allScanners.addAll(filterVisitors(visitors, this::isVisitorJavaVersionCompatible)); + allScanners.addAll(filterVisitors(visitors, v -> + isVisitorJavaVersionCompatible(v) && isVisitorDependencyVersionCompatible(v))); if (canSkipScanningOfUnchangedFiles()) { scannersThatCannotBeSkipped.addAll(filterVisitors(visitors, this::isUnskippableVisitor)); } } + private boolean isVisitorDependencyVersionCompatible(Object v) { + if (v instanceof DependencyVersionAware versionAware) { + return versionAware.isCompatibleWithDependencies(artifactId -> + dependencyService.infer(artifactId, classpath)); + } + return true; + } + private List filterVisitors(Iterable visitors, Predicate predicate) { List scanners = new ArrayList<>(); final IssuableSubscriptionVisitorsRunner runner = new IssuableSubscriptionVisitorsRunner(); @@ -171,7 +184,7 @@ public void setCacheContext(CacheContext cacheContext) { /** * In cases where incremental analysis is enabled, try to scan a raw file without parsing its content. * - * @param inputFile The file to scan + * @param inputFile The file to scan * @return True if all scanners successfully scan the file without contents. False otherwise. */ public boolean scanWithoutParsing(InputFile inputFile) { @@ -183,7 +196,7 @@ public boolean scanWithoutParsing(InputFile inputFile) { List scannersNotRequiringParsing = new ArrayList<>(); var fileScannerContext = createScannerContext(sonarComponents, inputFile, javaVersion, inAndroidContext, cacheContext); - for (var scanner: scannersThatCannotBeSkipped) { + for (var scanner : scannersThatCannotBeSkipped) { boolean exceptionIsBlownUp = false; PerformanceMeasure.Duration scannerDuration = PerformanceMeasure.start(scanner); try { @@ -288,7 +301,8 @@ private void runScanner(Runnable action, JavaFileScanner scanner) throws CheckFa } String message = String.format( - "Unable to run check %s - %s on file '%s', To help improve the SonarSource Java Analyzer, please report this problem to SonarSource: see https://community.sonarsource.com/", + "Unable to run check %s - %s on file '%s', To help improve the SonarSource Java Analyzer, please report this problem to SonarSource: see https://community.sonarsource" + + ".com/", scanner.getClass(), ruleKey(scanner), currentFile); LOG.error(message, e); diff --git a/java-frontend/src/main/java/org/sonar/plugins/java/api/DependencyVersionAware.java b/java-frontend/src/main/java/org/sonar/plugins/java/api/DependencyVersionAware.java new file mode 100644 index 0000000000..e40d5a3cde --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/plugins/java/api/DependencyVersionAware.java @@ -0,0 +1,51 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.plugins.java.api; + +import java.util.Optional; +import java.util.function.Function; + +/** + * Implementing this interface allows a check to be executed - or not - during analysis, depending on the + * version of desired libraries among dependencies of the current module. + * + *

For example, if a rule is to be used only on modules of a project that use spring-web version at least 4.3, + * it should implement {@link DependencyVersionAware} with the following method: + *

+ *  {@literal @}Override
+ *   public boolean isCompatibleWithDependencies(Function> dependencyFinder) {
+ *     return dependencyFinder.apply("spring-web")
+ *       .map(v -> v.isGreaterThanOrEqualTo("4.3"))
+ *       .orElse(false);
+ *   }
+ * 
+ */ +public interface DependencyVersionAware { + + /** + * Control whether the check is compatible with the dependencies of the project being analysed. + * + * @param dependencyFinder is a function that takes in the name of an artifact that may be found in the classpath + * of the project. It returns the version of that artifact that was detected, or an empty + * optional if it is not detected in the classpath. + * Note that we cannot guarantee that a dependency will always be detected in the case were + * the classpath doesn't come from a standard build system like maven or gradle. + * @return true if the check is compatible with the detected dependencies and should be executed on sources, false otherwise. + */ + boolean isCompatibleWithDependencies(Function> dependencyFinder); + +} diff --git a/java-frontend/src/main/java/org/sonar/plugins/java/api/Version.java b/java-frontend/src/main/java/org/sonar/plugins/java/api/Version.java new file mode 100644 index 0000000000..b54814192a --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/plugins/java/api/Version.java @@ -0,0 +1,35 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.plugins.java.api; + +/** Versions of libraries. This provides methods to compare the version with other ones. */ +public interface Version { + + Integer major(); + Integer minor(); + Integer patch(); + String qualifier(); + + boolean isGreaterThanOrEqualTo(Version version); + boolean isGreaterThanOrEqualTo(String version); + boolean isGreaterThan(Version version); + boolean isLowerThanOrEqualTo(Version version); + boolean isLowerThan(Version version); + boolean isGreaterThan(String version); + boolean isLowerThanOrEqualTo(String version); + boolean isLowerThan(String version); +} diff --git a/java-frontend/src/test/java/org/sonar/java/classpath/DependencyVersionInferenceTest.java b/java-frontend/src/test/java/org/sonar/java/classpath/DependencyVersionInferenceTest.java new file mode 100644 index 0000000000..720c0996ad --- /dev/null +++ b/java-frontend/src/test/java/org/sonar/java/classpath/DependencyVersionInferenceTest.java @@ -0,0 +1,61 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.classpath; + +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.File; +import org.sonar.java.test.classpath.TestClasspathUtils; +import org.sonar.plugins.java.api.Version; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DependencyVersionInferenceTest { + + @Test + void inferLombok() { + // Arrange + List lombokClasspath = TestClasspathUtils + .loadFromFile("../java-checks-test-sources/default/target/test-classpath.txt"); + + // Act + Optional version = new DependencyVersionInference().infer("lombok", lombokClasspath); + + // Assert + Assertions.assertTrue(version.isPresent()); + assertEquals(new VersionImpl(1, 18, 30, null), version.get()); + } + + + @Test + void inferenceSpringBoot() { + // Arrange + List classpath = TestClasspathUtils + .loadFromFile("../java-checks-test-sources/spring-3.2/target/test-classpath.txt"); + + // Act + Optional version = + new DependencyVersionInference().infer("spring-boot", classpath); + + // Assert + Assertions.assertTrue(version.isPresent()); + assertEquals(new VersionImpl(3, 2, 4, null), version.get()); + } +} diff --git a/java-frontend/src/test/java/org/sonar/java/classpath/VersionTest.java b/java-frontend/src/test/java/org/sonar/java/classpath/VersionTest.java new file mode 100644 index 0000000000..ffe97716ec --- /dev/null +++ b/java-frontend/src/test/java/org/sonar/java/classpath/VersionTest.java @@ -0,0 +1,100 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.classpath; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class VersionTest { + + @Test + void testCompareTo() { + VersionImpl version = VersionImpl.parse("3.2.6-rc1"); + + assertTrue(version.isGreaterThanOrEqualTo("3.2")); + assertTrue(version.isGreaterThanOrEqualTo("3.2.5")); + assertTrue(version.isGreaterThanOrEqualTo("2.9.4-rc2")); + assertFalse(version.isGreaterThanOrEqualTo("3.2.11")); + + assertTrue(version.isLowerThan("4.0.0.RELEASE")); + assertTrue(version.isLowerThan("3.3.0")); + assertFalse(version.isLowerThan("3.2.6-rc1")); + + assertTrue(version.isGreaterThan("3.0.77")); + assertFalse(version.isGreaterThan("3.2")); + assertFalse(version.isGreaterThan("3.2.6-rc1")); + + assertTrue(version.isLowerThanOrEqualTo("3.2.6-rc1")); + assertFalse(version.isLowerThanOrEqualTo("3.2.5")); + } + + @Test + void testCompareTo_withNoPatchNumber() { + VersionImpl version = VersionImpl.parse("3.2"); + + assertTrue(version.isGreaterThanOrEqualTo("3.2")); + assertTrue(version.isGreaterThanOrEqualTo("3.2.5")); + assertTrue(version.isGreaterThanOrEqualTo("2.9.4-rc2")); + assertTrue(version.isGreaterThanOrEqualTo("3.2.11")); + assertFalse(version.isGreaterThanOrEqualTo("3.3.11")); + assertFalse(version.isGreaterThanOrEqualTo("4.0")); + + assertTrue(version.isLowerThan("4.0.0.RELEASE")); + assertTrue(version.isLowerThan("3.3.0")); + assertFalse(version.isLowerThan("3.2.6-rc1")); + + assertTrue(version.isGreaterThan("3.0.77")); + assertFalse(version.isGreaterThan("3.2")); + assertFalse(version.isGreaterThan("3.2.6-rc1")); + + assertTrue(version.isLowerThanOrEqualTo("3.2.5")); + assertFalse(version.isLowerThanOrEqualTo("3.1")); + } + + @Test + void testParse() { + assertThrows(IllegalArgumentException.class, () -> VersionImpl.parse("foo")); + } + + @Test + void testToString() { + assertEquals("5.4.3-rc1", VersionImpl.parse("5.4.3-rc1").toString()); + assertEquals("5.43-rc1", VersionImpl.parse("5.43-rc1").toString()); + assertEquals("5.431", VersionImpl.parse("5.431").toString()); + } + + @Test + void testEqualsAndHashCode() { + VersionImpl version1 = VersionImpl.parse("1.2"); + VersionImpl version2 = VersionImpl.parse("1.2"); + VersionImpl version3 = VersionImpl.parse("1.2.3"); + + assertEquals(version1, version2); + assertEquals(version1.hashCode(), version2.hashCode()); + assertNotEquals(version1, version3); + assertNotEquals(version3, version1); + assertNotEquals(version1.hashCode(), version3.hashCode()); + assertFalse(version1.equals("foo")); + assertFalse(version1.equals(null)); + + VersionImpl version23 = VersionImpl.parse("2.3"); + VersionImpl version24 = VersionImpl.parse("2.4"); + assertNotEquals(version1, version23); + assertNotEquals(version23, version24); + } +} diff --git a/java-frontend/src/test/java/org/sonar/java/model/VisitorsBridgeTest.java b/java-frontend/src/test/java/org/sonar/java/model/VisitorsBridgeTest.java index 010b226456..7d03eb6068 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/VisitorsBridgeTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/VisitorsBridgeTest.java @@ -23,6 +23,8 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Optional; +import java.util.function.Function; import org.assertj.core.api.Fail; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -42,6 +44,7 @@ import org.sonar.java.exceptions.ApiMismatchException; import org.sonar.java.notchecks.VisitorNotInChecksPackage; import org.sonar.java.testing.ThreadLocalLogTester; +import org.sonar.plugins.java.api.DependencyVersionAware; import org.sonar.plugins.java.api.InputFileScannerContext; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; import org.sonar.plugins.java.api.JavaFileScanner; @@ -49,6 +52,7 @@ import org.sonar.plugins.java.api.JavaVersion; import org.sonar.plugins.java.api.JavaVersionAwareVisitor; import org.sonar.plugins.java.api.ModuleScannerContext; +import org.sonar.plugins.java.api.Version; import org.sonar.plugins.java.api.caching.CacheContext; import org.sonar.plugins.java.api.internal.EndOfAnalysis; import org.sonar.plugins.java.api.tree.CompilationUnitTree; @@ -283,6 +287,43 @@ public void endOfAnalysis(ModuleScannerContext context) { assertThat(trace).containsExactly("RuleForAllJavaVersion", "RuleForJava15", "SubscriptionVisitorForJava10"); } + @Test + void testfilterScanner_byDependencies() { + List trace = new ArrayList<>(); + class SubscriptionVisitorForSpring8 extends IssuableSubscriptionVisitor implements DependencyVersionAware, EndOfAnalysis { + + @Override + public List nodesToVisit() { + return List.of(); + } + + @Override + public boolean isCompatibleWithDependencies(Function> dependencyFinder) { + return dependencyFinder.apply("spring-core") + .map(v -> v.isGreaterThanOrEqualTo("8.0")) + .orElse(false); + } + + @Override + public void endOfAnalysis(ModuleScannerContext context) { + trace.add(this.getClass().getSimpleName()); + } + } + + List visitors = Collections.singletonList(new SubscriptionVisitorForSpring8()); + VisitorsBridge visitorsBridge = new VisitorsBridge(visitors, Collections.emptyList(), null); + visitorsBridge.endOfAnalysis(); + assertThat(trace).isEmpty(); + trace.clear(); + + + visitorsBridge = new VisitorsBridge(visitors, Collections.singletonList( + new File("/home/user/.m2/path/spring-core-8.9.12.jar")), null); + visitorsBridge.endOfAnalysis(); + assertThat(trace).containsExactly("SubscriptionVisitorForSpring8"); + trace.clear(); + } + @Test void canSkipScanningOfUnchangedFiles_returns_false_by_default() { VisitorsBridge vb = visitorsBridge(Collections.emptyList(), true); diff --git a/sonar-java-plugin/src/main/resources/static/documentation.md b/sonar-java-plugin/src/main/resources/static/documentation.md index 5050d0706e..71d76bfc6d 100644 --- a/sonar-java-plugin/src/main/resources/static/documentation.md +++ b/sonar-java-plugin/src/main/resources/static/documentation.md @@ -144,6 +144,11 @@ The tutorial [Writing Custom Java Rules 101](https://redirect.sonarsource.com/do ### API changes +#### **8.12** + +* New type: `Version` This will allow comparing different versions of the same artifact, and is used by the new `DependencyVersionAware` interface. +* New interface: `DependencyVersionAware`. Implementations of `JavaCheck` that implement this interface will be activated or deactivated depending on the version of dependencies available in the project. + #### **8.10** * New method: `IssuableSubscriptionVisitor#reportIssue(Tree startTree, Tree endTree, String message, List flow, @Nullable Integer cost)`