Skip to content

Commit dd21c0d

Browse files
SONARJAVA-5845 Add new ModuleMetadata public API (#5343)
1 parent b05c4a5 commit dd21c0d

File tree

14 files changed

+364
-44
lines changed

14 files changed

+364
-44
lines changed

java-checks-test-sources/default/src/test/java/checks/tests/SpringBootEmptyMethodsCheckSample.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
class SpringBootSanityTest {
88
// Compliant, first time we encounter a spring sanity test
99
@Test
10-
void contextLoads() {
11-
}
10+
void contextLoads() {}
1211

1312
// Noncompliant@+2
1413
@Test
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.java;
18+
19+
import org.slf4j.Logger;
20+
import org.slf4j.LoggerFactory;
21+
import org.sonar.api.batch.bootstrap.ProjectDefinition;
22+
import org.sonar.api.config.Configuration;
23+
import org.sonar.java.model.JavaVersionImpl;
24+
import org.sonar.java.utils.ModuleMetadataUtils;
25+
import org.sonar.plugins.java.api.JavaVersion;
26+
import org.sonar.plugins.java.api.internal.ModuleMetadata;
27+
28+
import static org.sonar.java.SonarComponents.SONAR_IGNORE_UNNAMED_MODULE_FOR_SPLIT_PACKAGE;
29+
30+
public class DefaultModuleMetadata implements ModuleMetadata {
31+
32+
private static final Logger LOG = LoggerFactory.getLogger(DefaultModuleMetadata.class);
33+
34+
private final JavaVersion javaVersion;
35+
private final ProjectDefinition projectDefinition;
36+
private final boolean ignoreUnnamedModuleForSplitPackage;
37+
38+
public DefaultModuleMetadata(ProjectDefinition projectDefinition, Configuration configuration) {
39+
this.javaVersion = JavaVersionImpl.readFromConfiguration(configuration);
40+
this.projectDefinition = projectDefinition;
41+
this.ignoreUnnamedModuleForSplitPackage = configuration.getBoolean(SONAR_IGNORE_UNNAMED_MODULE_FOR_SPLIT_PACKAGE).orElse(false);
42+
}
43+
44+
@Override
45+
public JavaVersion javaVersion() {
46+
return javaVersion;
47+
}
48+
49+
@Override
50+
public String moduleKey() {
51+
var moduleKey = ModuleMetadataUtils.getModuleKey(projectDefinition);
52+
if (moduleKey.isEmpty()) {
53+
LOG.warn("Unable to determine module key, using empty string as fallback");
54+
}
55+
return moduleKey;
56+
}
57+
58+
@Override
59+
public boolean shouldIgnoreUnnamedModuleForSplitPackage() {
60+
return ignoreUnnamedModuleForSplitPackage;
61+
}
62+
63+
}

java-frontend/src/main/java/org/sonar/java/SonarComponents.java

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import org.sonar.java.model.LineUtils;
6969
import org.sonar.java.reporting.AnalyzerMessage;
7070
import org.sonar.java.reporting.JavaIssue;
71+
import org.sonar.java.utils.ModuleMetadataUtils;
7172
import org.sonar.plugins.java.api.CheckRegistrar;
7273
import org.sonar.plugins.java.api.JavaCheck;
7374
import org.sonar.plugins.java.api.JavaFileScanner;
@@ -513,7 +514,7 @@ private static long computeIdealBatchSize() {
513514
}
514515

515516
public File projectLevelWorkDir() {
516-
var root = getRootProject();
517+
var root = ModuleMetadataUtils.getRootProject(projectDefinition);
517518
if (root != null) {
518519
return root.getWorkDir();
519520
} else {
@@ -527,25 +528,7 @@ public File projectLevelWorkDir() {
527528
* @return A key representing the module
528529
*/
529530
public String getModuleKey() {
530-
var root = getRootProject();
531-
if (root != null && projectDefinition != null) {
532-
var rootBase = root.getBaseDir().toPath();
533-
var moduleBase = projectDefinition.getBaseDir().toPath();
534-
return rootBase.relativize(moduleBase).toString().replace('\\', '/');
535-
}
536-
return "";
537-
}
538-
539-
@CheckForNull
540-
private ProjectDefinition getRootProject() {
541-
ProjectDefinition current = projectDefinition;
542-
if (current == null) {
543-
return null;
544-
}
545-
while (current.getParent() != null) {
546-
current = current.getParent();
547-
}
548-
return current;
531+
return ModuleMetadataUtils.getModuleKey(projectDefinition);
549532
}
550533

551534
public boolean canSkipUnchangedFiles() throws ApiMismatchException {

java-frontend/src/main/java/org/sonar/java/model/JavaVersionImpl.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
package org.sonar.java.model;
1818

1919
import java.util.Locale;
20+
import java.util.Optional;
2021
import org.slf4j.Logger;
2122
import org.slf4j.LoggerFactory;
23+
import org.sonar.api.config.Configuration;
2224
import org.sonar.plugins.java.api.JavaVersion;
2325

2426
public class JavaVersionImpl implements JavaVersion {
@@ -230,4 +232,22 @@ private static int convertJavaVersionString(String javaVersion) {
230232
return Integer.parseInt(cleanedVersion);
231233
}
232234

235+
public static JavaVersion readFromConfiguration(Configuration config) {
236+
Optional<String> javaVersionAsString = config.get(SOURCE_VERSION);
237+
if (!javaVersionAsString.isPresent()) {
238+
return new JavaVersionImpl();
239+
}
240+
String enablePreviewAsString = config.get(ENABLE_PREVIEW).orElse("false");
241+
242+
JavaVersion javaVersion = fromString(javaVersionAsString.get(), enablePreviewAsString);
243+
if (javaVersion.arePreviewFeaturesEnabled() && javaVersion.asInt() < MAX_SUPPORTED) {
244+
LOG.warn("sonar.java.enablePreview is set but will be discarded as the Java version is less than the max" +
245+
" supported version ({} < {})", javaVersion.asInt(), MAX_SUPPORTED);
246+
javaVersion = new JavaVersionImpl(javaVersion.asInt(), false);
247+
}
248+
LOG.info("Configured Java source version ({}): {}, preview features enabled ({}): {}",
249+
SOURCE_VERSION, javaVersion.asInt(), ENABLE_PREVIEW, javaVersion.arePreviewFeaturesEnabled());
250+
return javaVersion;
251+
}
252+
233253
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.java.utils;
18+
19+
import javax.annotation.CheckForNull;
20+
import javax.annotation.Nullable;
21+
import org.sonar.api.batch.bootstrap.ProjectDefinition;
22+
23+
public class ModuleMetadataUtils {
24+
25+
private ModuleMetadataUtils() {
26+
// utility class
27+
}
28+
29+
public static String getModuleKey(@Nullable ProjectDefinition projectDefinition) {
30+
var root = getRootProject(projectDefinition);
31+
if (root != null && projectDefinition != null) {
32+
var rootBase = root.getBaseDir().toPath();
33+
var moduleBase = projectDefinition.getBaseDir().toPath();
34+
return rootBase.relativize(moduleBase).toString().replace('\\', '/');
35+
}
36+
return "";
37+
}
38+
39+
@CheckForNull
40+
public static ProjectDefinition getRootProject(@Nullable ProjectDefinition projectDefinition) {
41+
ProjectDefinition current = projectDefinition;
42+
if (current == null) {
43+
return null;
44+
}
45+
while (current.getParent() != null) {
46+
current = current.getParent();
47+
}
48+
return current;
49+
}
50+
51+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
@ParametersAreNonnullByDefault
18+
package org.sonar.java.utils;
19+
20+
import javax.annotation.ParametersAreNonnullByDefault;
21+
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.plugins.java.api.internal;
18+
19+
import org.sonar.api.batch.ScannerSide;
20+
import org.sonar.java.annotations.Beta;
21+
import org.sonar.plugins.java.api.JavaVersion;
22+
23+
24+
/**
25+
* Interface to access metadata about the module being analyzed by a Sensor.
26+
* For internal use only, this API will not be supported for custom plugins.
27+
*/
28+
@Beta
29+
@ScannerSide
30+
public interface ModuleMetadata {
31+
32+
/**
33+
* Returns the Java version of the module being analyzed.
34+
*/
35+
JavaVersion javaVersion();
36+
37+
/**
38+
* Returns the module key of the module being analyzed.
39+
*/
40+
String moduleKey();
41+
42+
/**
43+
* Describes whether input files should be parsed while ignoring unnamed split modules.
44+
*/
45+
boolean shouldIgnoreUnnamedModuleForSplitPackage();
46+
47+
}
48+
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.java;
18+
19+
import java.util.Optional;
20+
import org.junit.jupiter.api.Test;
21+
import org.sonar.api.config.Configuration;
22+
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
import static org.mockito.Mockito.doReturn;
25+
import static org.mockito.Mockito.mock;
26+
import static org.sonar.java.TestUtils.mockProjectDefinition;
27+
28+
class DefaultModuleMetadataTest {
29+
30+
@Test
31+
void test() {
32+
var projectDefinition = mockProjectDefinition();
33+
var config = mockConfiguration();
34+
var defaultModuleMetadata = new DefaultModuleMetadata(projectDefinition, config);
35+
36+
assertThat(defaultModuleMetadata.moduleKey()).isEqualTo("pmodule/cmodule");
37+
assertThat(defaultModuleMetadata.javaVersion().asInt()).isEqualTo(-1);
38+
assertThat(defaultModuleMetadata.shouldIgnoreUnnamedModuleForSplitPackage()).isFalse();
39+
}
40+
41+
@Test
42+
void testNullProjectDefinition() {
43+
var config = mockConfiguration();
44+
var defaultModuleMetadata = new DefaultModuleMetadata(null, config);
45+
46+
assertThat(defaultModuleMetadata.moduleKey()).isEmpty();
47+
}
48+
49+
@Test
50+
void testWithJavaVersion() {
51+
var projectDefinition = mockProjectDefinition();
52+
var config = mockConfiguration("sonar.java.source", "11");
53+
var defaultModuleMetadata = new DefaultModuleMetadata(projectDefinition, config);
54+
55+
assertThat(defaultModuleMetadata.moduleKey()).isEqualTo("pmodule/cmodule");
56+
assertThat(defaultModuleMetadata.javaVersion().asInt()).isEqualTo(11);
57+
}
58+
59+
@Test
60+
void testWithShouldIgnoreUnnamed() {
61+
var projectDefinition = mockProjectDefinition();
62+
var config = mockConfiguration("sonar.java.ignoreUnnamedModuleForSplitPackage", "true");
63+
var defaultModuleMetadata = new DefaultModuleMetadata(projectDefinition, config);
64+
65+
assertThat(defaultModuleMetadata.moduleKey()).isEqualTo("pmodule/cmodule");
66+
assertThat(defaultModuleMetadata.shouldIgnoreUnnamedModuleForSplitPackage()).isTrue();
67+
}
68+
69+
private Configuration mockConfiguration(String... keysAndValues) {
70+
Configuration configuration = mock(Configuration.class);
71+
for (int i = 0; i < keysAndValues.length; i++) {
72+
String key = keysAndValues[i++];
73+
String value = keysAndValues[i];
74+
doReturn(Optional.of(value)).when(configuration).get(key);
75+
if (value.equals("true") || value.equals("false")) {
76+
doReturn(Optional.of(Boolean.valueOf(value))).when(configuration).getBoolean(key);
77+
}
78+
}
79+
return configuration;
80+
}
81+
82+
}

java-frontend/src/test/java/org/sonar/java/TestUtils.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121
import java.nio.file.Files;
2222
import java.util.List;
2323
import java.util.stream.Stream;
24+
import org.sonar.api.batch.bootstrap.ProjectDefinition;
2425
import org.sonar.api.batch.fs.InputFile;
2526
import org.sonar.api.batch.fs.internal.TestInputFileBuilder;
2627

2728
import static java.nio.charset.StandardCharsets.UTF_8;
2829
import static org.junit.jupiter.api.Assertions.assertTrue;
30+
import static org.mockito.Mockito.doReturn;
2931
import static org.mockito.Mockito.mock;
3032
import static org.mockito.Mockito.when;
3133

@@ -144,4 +146,15 @@ public static SonarComponents mockSonarComponents() {
144146
when(mock.jspChecks()).thenReturn(List.of());
145147
return mock;
146148
}
149+
150+
public static ProjectDefinition mockProjectDefinition() {
151+
var rootProj = mock(ProjectDefinition.class);
152+
doReturn(new File("/foo/bar/proj")).when(rootProj).getBaseDir();
153+
var childModule = mock(ProjectDefinition.class);
154+
doReturn(new File("/foo/bar/proj/pmodule/cmodule")).when(childModule).getBaseDir();
155+
doReturn(rootProj).when(childModule).getParent();
156+
157+
return childModule;
158+
}
159+
147160
}

0 commit comments

Comments
 (0)