Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ public void invalid_binaries_dir_should_fail_analysis() {
SonarScanner scanner = ditProjectSonarScanner();
scanner.setProperty("sonar.java.binaries", "target/dummy__Dir");
BuildResult buildResult = ORCHESTRATOR.executeBuildQuietly(scanner);
assertThat(buildResult.getLastStatus()).isNotZero();
assertThat(buildResult.getLogs()).contains("No files nor directories matching 'target/dummy__Dir'");
assertThat(buildResult.getLastStatus()).isZero();
assertThat(buildResult.getLogs()).contains("WARN Invalid value for 'sonar.java.binaries', no files nor directories matching 'target/dummy__Dir'.");
}

@Test
Expand Down Expand Up @@ -154,27 +154,13 @@ public void should_keep_order_libs() {
assertThat(getNumberOfViolations(projectKey)).isEqualTo(1);
}

@Test
public void should_support_the_old_binaries_and_libraries_properties() {
SonarScanner scanner = ditProjectSonarScanner();
scanner.setProperty("sonar.binaries", "target/classes");
scanner.setProperty("sonar.libraries", guavaJarPathEscaped);
BuildResult buildResult = ORCHESTRATOR.executeBuildQuietly(scanner);

assertThat(buildResult.getLogs()).contains(
"sonar.binaries and sonar.libraries are not supported since version 4.0 of the SonarSource Java Analyzer," +
" please use sonar.java.binaries and sonar.java.libraries instead");
assertThat(buildResult.isSuccess()).isFalse();
}

@Test
public void should_log_warnings_if_binaries_missing() {
SonarScanner scanner = ditProjectSonarScanner();
BuildResult buildResult = ORCHESTRATOR.executeBuildQuietly(scanner);
String logs = buildResult.getLogs();
assertThat(logs).contains("Your project contains .java files, please provide compiled classes with sonar.java.binaries property,"
+ " or exclude them from the analysis with sonar.exclusions property.");
assertThat(buildResult.isSuccess()).isFalse();
assertThat(logs).contains("WARN Missing 'sonar.java.binaries' and 'sonar.java.libraries' properties. You might end up with less precise analysis results.");
assertThat(buildResult.isSuccess()).isTrue();
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -616,11 +616,11 @@ public void logUndefinedTypes() {
if (problemsToFilePaths.isEmpty()) {
return;
}
javaClasspath.logSuspiciousEmptyLibraries();
javaClasspath.logClasspathWarnings();
if (!isAutoScan()) {
// In autoscan, test + main code are analyzed in the same batch, and we do not make the distinction between
// test and main libraries, everything is inside "sonar.java.libraries", it is expected to let the test property empty.
javaTestClasspath.logSuspiciousEmptyLibraries();
javaTestClasspath.logClasspathWarnings();
}
logUndefinedTypes(LOGGED_MAX_NUMBER_UNDEFINED_TYPES);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
Expand All @@ -42,10 +43,12 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.batch.ScannerSide;
import org.sonar.api.batch.fs.FilePredicate;
import org.sonar.api.batch.fs.FileSystem;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.config.Configuration;
import org.sonar.java.collections.CollectionUtils;
import org.sonar.java.AnalysisWarningsWrapper;
import org.sonar.java.SonarComponents;
import org.sonarsource.api.sonarlint.SonarLintSide;

@ScannerSide
Expand All @@ -58,26 +61,67 @@ public abstract class AbstractClasspath {
protected final Configuration settings;
protected final FileSystem fs;
private final InputFile.Type fileType;
protected final String binariesProperty;
protected final String librariesProperty;
private static final Path[] STANDARD_CLASSES_DIRS = {Paths.get("target", "classes"), Paths.get("target", "test-classes")};

protected final List<File> binaries;
protected final List<File> elements;
protected List<File> elements;
protected boolean validateLibraries;
protected boolean initialized;
private boolean inAndroidContext = false;
private final Set<String> classpathWarnings;
protected final AnalysisWarningsWrapper analysisWarnings;

protected AbstractClasspath(Configuration settings, FileSystem fs, InputFile.Type fileType) {
protected AbstractClasspath(Configuration settings, FileSystem fs, InputFile.Type fileType, String binariesProperty, String librariesProperty,
AnalysisWarningsWrapper analysisWarnings) {
this.settings = settings;
this.fs = fs;
this.fileType = fileType;
this.binariesProperty = binariesProperty;
this.librariesProperty = librariesProperty;
this.binaries = new ArrayList<>();
this.elements = new ArrayList<>();
this.elements = List.of();
this.analysisWarnings = analysisWarnings;
classpathWarnings = new LinkedHashSet<>();
initialized = false;
}

protected void init() {
if (!initialized) {
initialized = true;
validateLibraries = hasJavaFiles();
validatePropertiesPresence(binariesProperty, librariesProperty);
binaries.addAll(getFilesFromProperty(binariesProperty));
Set<File> libraries = new LinkedHashSet<>(getJdkJars());
Set<File> extraLibraries = getFilesFromProperty(librariesProperty);
libraries.addAll(extraLibraries);
logResolvedFiles(binariesProperty, binaries);
logResolvedFiles(librariesProperty, libraries);
Set<File> all = new LinkedHashSet<>(binaries);
all.addAll(libraries);
elements = List.copyOf(all);
}
}

protected void validatePropertiesPresence(String binariesProperty, String librariesProperty) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two parameters are already class attributes, we should be able to drop them, right?

if (settings.getBoolean(SonarComponents.SONAR_AUTOSCAN).orElse(false)) {
return;
}
boolean missingBinary = !settings.hasKey(binariesProperty) && hasMoreThanOneJavaFile();
boolean missingLibraries = !settings.hasKey(librariesProperty) && hasJavaFiles();
if (missingBinary && missingLibraries) {
classpathWarnings.add(String.format("Missing '%s' and '%s' properties. You might end up with less precise analysis results.", binariesProperty, librariesProperty));
} else if (missingBinary) {
classpathWarnings.add(String.format("Missing '%s' property. You might end up with less precise analysis results.", binariesProperty));
} else if (missingLibraries) {
classpathWarnings.add(String.format("Missing '%s' property. You might end up with less precise analysis results.", librariesProperty));
}
}

protected List<File> getJdkJars() {
List<File> jdkClassesRoots = settings.get(ClasspathProperties.SONAR_JAVA_JDK_HOME)
.flatMap(AbstractClasspath::existingDirectoryOrLog)
.flatMap(this::existingDirectoryOrLog)
.map(File::toPath)
.map(JavaSdkUtil::getJdkClassesRoots)
.orElse(Collections.emptyList());
Expand All @@ -87,26 +131,29 @@ protected List<File> getJdkJars() {

static void logResolvedFiles(String property, Collection<File> files) {
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Property '%s' resolved with:%n%s", property, files.stream()
LOG.debug(String.format("Property '%s' resolved with: %s", property, files.stream()
.map(File::getAbsolutePath)
.collect(Collectors.joining("," + System.lineSeparator(), "[", "]"))));
.collect(Collectors.joining(",", "[", "]"))));
}
}

private static Optional<File> existingDirectoryOrLog(String path) {
private Optional<File> existingDirectoryOrLog(String path) {
LOG.debug("Property '{}' set with: {}", ClasspathProperties.SONAR_JAVA_JDK_HOME, path);
File file = new File(path);
if (!file.exists() || !file.isDirectory()) {
LOG.warn("Invalid value for '{}' property, defaulting to runtime JDK.{}Configured location does not exists: '{}'",
ClasspathProperties.SONAR_JAVA_JDK_HOME, System.lineSeparator(), file.getAbsolutePath());
classpathWarnings.add(String.format("Invalid value '%s' for '%s' property, defaulting to runtime JDK.", file.getAbsolutePath(), ClasspathProperties.SONAR_JAVA_JDK_HOME));
return Optional.empty();
}
return Optional.of(file);
}

protected abstract void init();

public abstract void logSuspiciousEmptyLibraries();
public void logClasspathWarnings() {
for (String warning : classpathWarnings) {
LOG.warn(warning);
analysisWarnings.addUnique(warning);
}
Comment on lines +140 to +154

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: Classpath warnings dropped when no undefined types are reported

All classpath diagnostics are now accumulated into classpathWarnings and only flushed by AbstractClasspath.logClasspathWarnings(). The only production call site is SonarComponents.logUndefinedTypes() (java-frontend/src/main/java/org/sonar/java/SonarComponents.java:615-624), but the two logClasspathWarnings() calls are placed after the early-return guard:

public void logUndefinedTypes() {
  if (problemsToFilePaths.isEmpty()) {
    return;            // <-- returns before warnings are flushed
  }
  javaClasspath.logClasspathWarnings();
  ...
}

Consequently, whenever an analysis produces no undefined types, every accumulated classpath warning is silently discarded — including warnings that were previously surfaced unconditionally. In particular, the invalid sonar.java.jdkHome warning used to be emitted immediately via LOG.warn(...) inside existingDirectoryOrLog (see the removed code in AbstractClasspath.java:140-148); it is now deferred into classpathWarnings and will not be logged at all if problemsToFilePaths is empty. The same applies to the new 'Missing sonar.java.binaries/libraries' and 'Invalid value for ...' warnings (AbstractClasspath.java:114-119,170,144). This is a regression in user-facing diagnostics introduced by this PR.

Fix: flush the classpath warnings before the isEmpty() early-return so they are always emitted regardless of undefined types.

Move the classpath-warning flush above the early-return guard so warnings are always emitted.:

public void logUndefinedTypes() {
  javaClasspath.logClasspathWarnings();
  if (!isAutoScan()) {
    // In autoscan, test + main code are analyzed in the same batch ...
    javaTestClasspath.logClasspathWarnings();
  }
  if (problemsToFilePaths.isEmpty()) {
    return;
  }
  logUndefinedTypes(LOGGED_MAX_NUMBER_UNDEFINED_TYPES);
  // clear the set so only new undefined types will be logged
  problemsToFilePaths.clear();
}
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

classpathWarnings.clear();
}
Comment on lines +150 to +156

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: Classpath warnings dropped when no undefined types collected

This PR converts fatal classpath errors (missing/invalid sonar.java.binaries, sonar.java.libraries, invalid sonar.java.jdkHome, and the new "Missing properties" notices) into warnings accumulated in AbstractClasspath.classpathWarnings and flushed by logClasspathWarnings().

However, the only production caller of logClasspathWarnings() is SonarComponents.logUndefinedTypes() (SonarComponents.java:619, 623), which begins with an early return:

public void logUndefinedTypes() {
  if (problemsToFilePaths.isEmpty()) {
    return;
  }
  javaClasspath.logClasspathWarnings();
  ...
}

logUndefinedTypes() is invoked once at the end of analysis from JavaAstScanner.endOfAnalysis() (JavaAstScanner.java:143). When no undefined types were collected during the scan, the method returns before reaching logClasspathWarnings(), so every accumulated classpath warning is silently discarded.

This defeats the intent of the fix: a project that supplies an invalid sonar.java.binaries directory (or omits the property) but whose types all happen to resolve (e.g. from the JDK) will get no warning at all — neither in the logs nor in the SonarQube analysis warnings. Previously such cases failed loudly; now they can be silently ignored.

Suggested fix: flush the classpath warnings unconditionally, before the early return based on problemsToFilePaths. Since logClasspathWarnings() clears its own state, calling it unconditionally is safe.

Flush classpath warnings before the empty-problems early return so they are always emitted.:

public void logUndefinedTypes() {
  javaClasspath.logClasspathWarnings();
  if (!isAutoScan()) {
    javaTestClasspath.logClasspathWarnings();
  }
  if (problemsToFilePaths.isEmpty()) {
    return;
  }
  logUndefinedTypes(LOGGED_MAX_NUMBER_UNDEFINED_TYPES);
  // clear the set so only new undefined types will be logged
  problemsToFilePaths.clear();
}
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎


protected Set<File> getFilesFromProperty(String property) {
Set<File> result = new LinkedHashSet<>();
Expand All @@ -120,9 +167,7 @@ protected Set<File> getFilesFromProperty(String property) {
for (String pathPattern : fileNames) {
Set<File> libraryFilesForPattern = getFilesForPattern(baseDir.toPath(), pathPattern, isLibraryProperty);
if (validateLibraries && libraryFilesForPattern.isEmpty() && hasJavaSources) {
LOG.error("Invalid value for '{}' property.", property);
String message = "No files nor directories matching '" + pathPattern + "'";
throw new IllegalStateException(message);
classpathWarnings.add(String.format("Invalid value for '%s', no files nor directories matching '%s'.", property, pathPattern));
}
validateLibraries = validateLibs;
result.addAll(libraryFilesForPattern);
Expand All @@ -135,11 +180,25 @@ protected Set<File> getFilesFromProperty(String property) {
}

protected boolean hasJavaSources() {
return fs.hasFiles(fs.predicates().and(fs.predicates().hasLanguage("java"), fs.predicates().hasType(fileType)));
return fs.hasFiles(sourcePredicate());
}

protected boolean hasMoreThanOneJavaFile() {
return CollectionUtils.size(fs.inputFiles(fs.predicates().and(fs.predicates().hasLanguage("java"), fs.predicates().hasType(fileType)))) > 1;
// No need to iterate over the entire collection, checking that there are two elements is enough
Iterator<InputFile> iterator = fs.inputFiles(sourcePredicate()).iterator();
if (iterator.hasNext()) {
iterator.next();
return iterator.hasNext();
}
return false;
}

protected boolean hasJavaFiles() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is exactly the same as the hasJavaSources method line 182

return fs.hasFiles(sourcePredicate());
}

private FilePredicate sourcePredicate() {
return fs.predicates().and(fs.predicates().hasLanguage("java"), fs.predicates().hasType(fileType));
}

private Set<File> getFilesForPattern(Path baseDir, String pathPattern, boolean libraryProperty) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,86 +16,22 @@
*/
package org.sonar.java.classpath;

import java.io.File;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.batch.fs.FileSystem;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.config.Configuration;
import org.sonar.java.AnalysisException;
import org.sonar.java.AnalysisWarningsWrapper;

public class ClasspathForMain extends AbstractClasspath {

private static final Logger LOG = LoggerFactory.getLogger(ClasspathForMain.class);
import static org.sonar.java.classpath.ClasspathProperties.SONAR_JAVA_BINARIES;
import static org.sonar.java.classpath.ClasspathProperties.SONAR_JAVA_LIBRARIES;

private final AnalysisWarningsWrapper analysisWarnings;
private boolean hasSuspiciousEmptyLibraries = false;
private boolean alreadyReported = false;
public class ClasspathForMain extends AbstractClasspath {

public ClasspathForMain(Configuration settings, FileSystem fs, AnalysisWarningsWrapper analysisWarnings) {
super(settings, fs, InputFile.Type.MAIN);
this.analysisWarnings = analysisWarnings;
super(settings, fs, InputFile.Type.MAIN, SONAR_JAVA_BINARIES, SONAR_JAVA_LIBRARIES, analysisWarnings);
}

public ClasspathForMain(Configuration settings, FileSystem fs) {
this(settings, fs, AnalysisWarningsWrapper.NOOP_ANALYSIS_WARNINGS);
}

@Override
protected void init() {
if (!initialized) {
validateLibraries = fs.hasFiles(fs.predicates().all());
initialized = true;
binaries.addAll(getFilesFromProperty(ClasspathProperties.SONAR_JAVA_BINARIES));

Set<File> libraries = new LinkedHashSet<>(getJdkJars());
Set<File> extraLibraries = getFilesFromProperty(ClasspathProperties.SONAR_JAVA_LIBRARIES);
logResolvedFiles(ClasspathProperties.SONAR_JAVA_LIBRARIES, extraLibraries);
libraries.addAll(extraLibraries);
if (binaries.isEmpty() && libraries.isEmpty() && useDeprecatedProperties()) {
throw new AnalysisException(
"sonar.binaries and sonar.libraries are not supported since version 4.0 of the SonarSource Java Analyzer,"
+ " please use sonar.java.binaries and sonar.java.libraries instead");
}
hasSuspiciousEmptyLibraries = libraries.isEmpty() && hasJavaSources();

if (binaries.isEmpty() && hasMoreThanOneJavaFile()) {
if(isSonarLint()) {
LOG.warn("sonar.java.binaries is empty, please double check your configuration");
} else {
throw new AnalysisException("Your project contains .java files, please provide compiled classes with sonar.java.binaries property,"
+ " or exclude them from the analysis with sonar.exclusions property.");
}
}

elements.addAll(binaries);
elements.addAll(libraries);
}
}

protected boolean isSonarLint() {
return false;
}

private boolean useDeprecatedProperties() {
return isNotNullOrEmpty(settings.get("sonar.binaries").orElse(null)) && isNotNullOrEmpty(settings.get("sonar.libraries").orElse(null));
}

private static boolean isNotNullOrEmpty(@Nullable String string) {
return string != null && !string.isEmpty();
}

@Override
public void logSuspiciousEmptyLibraries() {
if (hasSuspiciousEmptyLibraries && !alreadyReported) {
String warning = String.format(ClasspathProperties.EMPTY_LIBRARIES_WARNING_TEMPLATE, "SOURCE", ClasspathProperties.SONAR_JAVA_LIBRARIES);
LOG.warn(warning);
analysisWarnings.addUnique(warning);
alreadyReported = true;
}
}
}

This file was deleted.

Loading
Loading