diff --git a/java-frontend/src/main/java/org/sonar/java/DefaultModuleMetadata.java b/java-frontend/src/main/java/org/sonar/java/DefaultModuleMetadata.java new file mode 100644 index 00000000000..c2b4a0c719b --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/DefaultModuleMetadata.java @@ -0,0 +1,73 @@ +/* + * 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; + +import javax.annotation.CheckForNull; +import org.sonar.api.batch.bootstrap.ProjectDefinition; +import org.sonar.api.config.Configuration; +import org.sonar.java.model.JavaVersionImpl; +import org.sonar.plugins.java.api.JavaVersion; +import org.sonar.plugins.java.api.internal.ModuleMetadata; + +import static org.sonar.java.SonarComponents.SONAR_IGNORE_UNNAMED_MODULE_FOR_SPLIT_PACKAGE; + +public class DefaultModuleMetadata implements ModuleMetadata { + + private final JavaVersion javaVersion; + private final ProjectDefinition projectDefinition; + private final boolean ignoreUnnamedModuleForSplitPackage; + + public DefaultModuleMetadata(ProjectDefinition projectDefinition, Configuration configuration) { + this.javaVersion = JavaVersionImpl.readFromConfiguration(configuration); + this.projectDefinition = projectDefinition; + this.ignoreUnnamedModuleForSplitPackage = configuration.getBoolean(SONAR_IGNORE_UNNAMED_MODULE_FOR_SPLIT_PACKAGE).orElse(false); + } + + @Override + public JavaVersion javaVersion() { + return javaVersion; + } + + @Override + public String moduleKey() { + var root = getRootProject(); + if (root != null && projectDefinition != null) { + var rootBase = root.getBaseDir().toPath(); + var moduleBase = projectDefinition.getBaseDir().toPath(); + return rootBase.relativize(moduleBase).toString().replace('\\', '/'); + } + return ""; + } + + @Override + public boolean shouldIgnoreUnnamedModuleForSplitPackage() { + return ignoreUnnamedModuleForSplitPackage; + } + + @CheckForNull + private ProjectDefinition getRootProject() { + ProjectDefinition current = projectDefinition; + if (current == null) { + return null; + } + while (current.getParent() != null) { + current = current.getParent(); + } + return current; + } + +} diff --git a/java-frontend/src/main/java/org/sonar/java/model/JavaVersionImpl.java b/java-frontend/src/main/java/org/sonar/java/model/JavaVersionImpl.java index 61b4dd478dd..c105d76756e 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JavaVersionImpl.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JavaVersionImpl.java @@ -17,8 +17,10 @@ package org.sonar.java.model; import java.util.Locale; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.sonar.api.config.Configuration; import org.sonar.plugins.java.api.JavaVersion; public class JavaVersionImpl implements JavaVersion { @@ -230,4 +232,22 @@ private static int convertJavaVersionString(String javaVersion) { return Integer.parseInt(cleanedVersion); } + public static JavaVersion readFromConfiguration(Configuration config) { + Optional javaVersionAsString = config.get(SOURCE_VERSION); + if (!javaVersionAsString.isPresent()) { + return new JavaVersionImpl(); + } + String enablePreviewAsString = config.get(ENABLE_PREVIEW).orElse("false"); + + JavaVersion javaVersion = fromString(javaVersionAsString.get(), enablePreviewAsString); + if (javaVersion.arePreviewFeaturesEnabled() && javaVersion.asInt() < MAX_SUPPORTED) { + LOG.warn("sonar.java.enablePreview is set but will be discarded as the Java version is less than the max" + + " supported version ({} < {})", javaVersion.asInt(), MAX_SUPPORTED); + javaVersion = new JavaVersionImpl(javaVersion.asInt(), false); + } + LOG.info("Configured Java source version ({}): {}, preview features enabled ({}): {}", + SOURCE_VERSION, javaVersion.asInt(), ENABLE_PREVIEW, javaVersion.arePreviewFeaturesEnabled()); + return javaVersion; + } + } diff --git a/java-frontend/src/main/java/org/sonar/plugins/java/api/internal/ModuleMetadata.java b/java-frontend/src/main/java/org/sonar/plugins/java/api/internal/ModuleMetadata.java new file mode 100644 index 00000000000..fc544559b2d --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/plugins/java/api/internal/ModuleMetadata.java @@ -0,0 +1,48 @@ +/* + * 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.internal; + +import org.sonar.api.batch.ScannerSide; +import org.sonar.java.annotations.Beta; +import org.sonar.plugins.java.api.JavaVersion; + + +/** + * Interface to access metadata about the module being analyzed by a Sensor. + * For internal use only, this API will not be supported for custom plugins. + */ +@Beta +@ScannerSide +public interface ModuleMetadata { + + /** + * Returns the Java version of the module being analyzed. + */ + JavaVersion javaVersion(); + + /** + * Returns the module key of the module being analyzed. + */ + String moduleKey(); + + /** + * Describes whether input files should be parsed while ignoring unnamed split modules. + */ + boolean shouldIgnoreUnnamedModuleForSplitPackage(); + +} + diff --git a/java-frontend/src/test/java/org/sonar/java/DefaultModuleMetadataTest.java b/java-frontend/src/test/java/org/sonar/java/DefaultModuleMetadataTest.java new file mode 100644 index 00000000000..28ce743c7ae --- /dev/null +++ b/java-frontend/src/test/java/org/sonar/java/DefaultModuleMetadataTest.java @@ -0,0 +1,85 @@ +/* + * 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; + +import java.io.File; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.sonar.api.batch.bootstrap.ProjectDefinition; +import org.sonar.api.config.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +class DefaultModuleMetadataTest { + + @Test + void test() { + var projectDefinition = mockProjectDefinition(); + var config = mockConfiguration(); + var defaultModuleMetadata = new DefaultModuleMetadata(projectDefinition, config); + + assertThat(defaultModuleMetadata.moduleKey()).isEqualTo("pmodule/cmodule"); + assertThat(defaultModuleMetadata.javaVersion().asInt()).isEqualTo(-1); + assertThat(defaultModuleMetadata.shouldIgnoreUnnamedModuleForSplitPackage()).isFalse(); + } + + @Test + void testWithJavaVersion() { + var projectDefinition = mockProjectDefinition(); + var config = mockConfiguration("sonar.java.source", "11"); + var defaultModuleMetadata = new DefaultModuleMetadata(projectDefinition, config); + + assertThat(defaultModuleMetadata.moduleKey()).isEqualTo("pmodule/cmodule"); + assertThat(defaultModuleMetadata.javaVersion().asInt()).isEqualTo(11); + } + + @Test + void testWithShouldIgnoreUnnamed() { + var projectDefinition = mockProjectDefinition(); + var config = mockConfiguration("sonar.java.ignoreUnnamedModuleForSplitPackage", "true"); + var defaultModuleMetadata = new DefaultModuleMetadata(projectDefinition, config); + + assertThat(defaultModuleMetadata.moduleKey()).isEqualTo("pmodule/cmodule"); + assertThat(defaultModuleMetadata.shouldIgnoreUnnamedModuleForSplitPackage()).isTrue(); + } + + private ProjectDefinition mockProjectDefinition() { + var rootProj = mock(ProjectDefinition.class); + doReturn(new File("/foo/bar/proj")).when(rootProj).getBaseDir(); + var childModule = mock(ProjectDefinition.class); + doReturn(new File("/foo/bar/proj/pmodule/cmodule")).when(childModule).getBaseDir(); + doReturn(rootProj).when(childModule).getParent(); + + return childModule; + } + + private Configuration mockConfiguration(String... keysAndValues) { + Configuration configuration = mock(Configuration.class); + for (int i = 0; i < keysAndValues.length; i++) { + String key = keysAndValues[i++]; + String value = keysAndValues[i]; + doReturn(Optional.of(value)).when(configuration).get(key); + if(value.equals("true") || value.equals("false")) { + doReturn(Optional.of(Boolean.valueOf(value))).when(configuration).getBoolean(key); + } + } + return configuration; + } + +} diff --git a/java-frontend/src/test/java/org/sonar/java/model/JavaVersionImplTest.java b/java-frontend/src/test/java/org/sonar/java/model/JavaVersionImplTest.java index 8ba32a5c32d..cb3dd5c0289 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JavaVersionImplTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JavaVersionImplTest.java @@ -16,12 +16,16 @@ */ package org.sonar.java.model; +import java.util.Optional; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.sonar.api.config.Configuration; import org.sonar.plugins.java.api.JavaVersion; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; class JavaVersionImplTest { @@ -182,5 +186,16 @@ void test_fromMap() { assertThat(version.asInt()).isEqualTo(-1); assertThat(version.arePreviewFeaturesEnabled()).isFalse(); } + + @Test + void test_preview_features_without_max_version_from_config(){ + var config = mock(Configuration.class); + doReturn(Optional.of("17")).when(config).get(JavaVersion.SOURCE_VERSION); + doReturn(Optional.of("true")).when(config).get(JavaVersion.ENABLE_PREVIEW); + var javaVersion = JavaVersionImpl.readFromConfiguration(config); + assertThat(javaVersion.asInt()).isEqualTo(17); + // Preview features should be disabled because 17 is not the max supported version + assertThat(javaVersion.arePreviewFeaturesEnabled()).isFalse(); + } } diff --git a/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaPlugin.java b/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaPlugin.java index 588f60e88a5..8ef71d7bb1d 100644 --- a/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaPlugin.java +++ b/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaPlugin.java @@ -28,6 +28,7 @@ import org.sonar.api.config.PropertyDefinition; import org.sonar.java.AnalysisWarningsWrapper; import org.sonar.java.DefaultJavaResourceLocator; +import org.sonar.java.DefaultModuleMetadata; import org.sonar.java.JavaConstants; import org.sonar.java.SonarComponents; import org.sonar.java.classpath.ClasspathForMain; @@ -117,6 +118,7 @@ public void define(Context context) { PostAnalysisIssueFilter.class)); list.add(AnalysisWarningsWrapper.class); + list.add(DefaultModuleMetadata.class); context.addExtensions(Collections.unmodifiableList(list)); } diff --git a/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaSensor.java b/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaSensor.java index b4dc7693661..2219aec774d 100644 --- a/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaSensor.java +++ b/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaSensor.java @@ -111,7 +111,7 @@ public void execute(SensorContext context) { Measurer measurer = new Measurer(context, noSonarFilter); - JavaVersion javaVersion = getJavaVersion(); + JavaVersion javaVersion = JavaVersionImpl.readFromConfiguration(settings); telemetry.aggregateAsSortedSet(JAVA_LANGUAGE_VERSION, javaVersion.toString()); telemetry.aggregateAsCounter(JAVA_MODULE_COUNT, 1L); @@ -181,24 +181,6 @@ private Iterable javaFiles(InputFile.Type type) { return fs.inputFiles(fs.predicates().and(fs.predicates().hasLanguage(Java.KEY), fs.predicates().hasType(type))); } - private JavaVersion getJavaVersion() { - Optional javaVersionAsString = settings.get(JavaVersion.SOURCE_VERSION); - if (!javaVersionAsString.isPresent()) { - return new JavaVersionImpl(); - } - String enablePreviewAsString = settings.get(JavaVersion.ENABLE_PREVIEW).orElse("false"); - - JavaVersion javaVersion = JavaVersionImpl.fromString(javaVersionAsString.get(), enablePreviewAsString); - if (javaVersion.arePreviewFeaturesEnabled() && javaVersion.asInt() < JavaVersionImpl.MAX_SUPPORTED) { - LOG.warn("sonar.java.enablePreview is set but will be discarded as the Java version is less than the max" + - " supported version ({} < {})", javaVersion.asInt(), JavaVersionImpl.MAX_SUPPORTED); - javaVersion = new JavaVersionImpl(javaVersion.asInt(), false); - } - LOG.info("Configured Java source version ({}): {}, preview features enabled ({}): {}", - JavaVersion.SOURCE_VERSION, javaVersion.asInt(), JavaVersion.ENABLE_PREVIEW, javaVersion.arePreviewFeaturesEnabled()); - return javaVersion; - } - @Override public String toString() { return getClass().getSimpleName(); diff --git a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaPluginTest.java b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaPluginTest.java index dcea09ca414..34ab016c8ab 100644 --- a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaPluginTest.java +++ b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaPluginTest.java @@ -40,7 +40,7 @@ void sonarLint_9_9_extensions() { Plugin.Context context = new Plugin.Context(runtime); javaPlugin.define(context); assertThat(context.getExtensions()) - .hasSize(19) + .hasSize(20) .contains(SonarLintCache.class); } @@ -51,7 +51,7 @@ void sonarqube_9_9_extensions() { Plugin.Context context = new Plugin.Context(sqCommunity); javaPlugin.define(context); assertThat(context.getExtensions()) - .hasSize(35) + .hasSize(36) .doesNotContain(Jasper.class); } @@ -61,7 +61,7 @@ void sonarqube_9_9_commercial_extensions() { Plugin.Context context = new Plugin.Context(sqEnterprise); javaPlugin.define(context); assertThat(context.getExtensions()) - .hasSize(36) + .hasSize(37) .contains(Jasper.class); }