Skip to content

Commit

Permalink
JSR-305 probe. Checks for @Nonnull and @CheckForNull deprecated a…
Browse files Browse the repository at this point in the history
…nnotations. (#359)

Co-authored-by: Adrien Lecharpentier <[email protected]>
  • Loading branch information
Jagrutiti and alecharp authored Sep 8, 2023
1 parent 6b261ab commit f68d23b
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ protected boolean isSourceCodeRelated() {
/*
* This is counter intuitive, but this probe needs to be executed all the time.
* So even if the probe seems to be related to code, in order to not be skipped by the
* ProbeEngine, is must be `false`.
* ProbeEngine, it must be `false`.
*/
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package io.jenkins.pluginhealth.scoring.probes;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.jenkins.pluginhealth.scoring.model.Plugin;
import io.jenkins.pluginhealth.scoring.model.ProbeResult;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
* This probe checks for deprecated @Nonnull and @CheckForNull annotations in a plugin.
*/
@Component
@Order(value = JSR305Probe.ORDER)
public class JSR305Probe extends Probe {
public static final int ORDER = SCMLinkValidationProbe.ORDER + 100;
public static final String KEY = "JSR-305";
private static final Logger LOGGER = LoggerFactory.getLogger(JSR305Probe.class);

@Override
protected ProbeResult doApply(Plugin plugin, ProbeContext context) {
final Path scmRepository = context.getScmRepository();

/* The "maxDepth" is set to Integer.MAX_VALUE because a repository can have multiple modules with class files in it. We do not want to miss any ".java" file. */
try (Stream<Path> javaFiles = Files.find(scmRepository, Integer.MAX_VALUE, (path, $) -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".java"))) {
Set<String> javaFilesWithDetectedImports = javaFiles
.filter(this::containsImports)
.map(javaFile -> javaFile.getFileName().toString())
.collect(Collectors.toSet());

return javaFilesWithDetectedImports.isEmpty()
? ProbeResult.success(key(), String.format(getSuccessMessage() + " at %s plugin.", plugin.getName()))
: ProbeResult.failure(key(), String.format(getFailureMessage() + " at %s plugin for ", plugin.getName()) + javaFilesWithDetectedImports.stream().sorted(Comparator.naturalOrder()).collect(Collectors.joining(", ")));
} catch (IOException ex) {
LOGGER.error("Could not browse the plugin folder during {} probe. {}", key(), ex);
return ProbeResult.error(key(), String.format("Could not browse the plugin folder during {} probe.", plugin.getName()));
}
}

@Override
public String[] getProbeResultRequirement() {
return new String[]{SCMLinkValidationProbe.KEY};
}

@Override
public String key() {
return KEY;
}

@Override
public String getDescription() {
return "The probe checks for deprecated annotations.";
}


/**
* Fetches all the import for the given file.
*
* @param javaFile The file to read and fetch the import from.
* @return a List with imports is returned when imports are found. Otherwise, an empty list is returned.
*/
private List<String> getAllImportsInTheFile(Path javaFile) {
try (Stream<String> importStatements = Files.lines(javaFile).filter(line -> line.startsWith("import")).map(this::getFullyQualifiedImportName)) {
return importStatements.toList();
} catch (IOException ex) {
LOGGER.error("Could not browse the {} plugin folder during probe. {}", key(), ex);
}
return List.of();
}

/**
* @return the import String that should be checked.
*/
public String getImportToCheck() {
return "javax.annotation";
}

/**
* @return a success message.
*/
String getSuccessMessage() {
return "Latest version of imports found";
}

/**
* @return a failure message.
*/
String getFailureMessage() {
return "Deprecated imports found";
}

/**
* Gets a fully qualified name of the imported class/library from an {@code import} statement.
*
* @param importStatement the statement from which the fully qualified name should be fetched.
* @return a String that contains only the fully qualified name of the class/library.
*/
private String getFullyQualifiedImportName(String importStatement) {
return importStatement.replace("import ", "").replace(";", "").trim();
}

/**
* Checks whether the file contains the required imports.
*
* @param javaFile The file to fetch all the imports for.
* @return boolean {@code true} if the file contains all imports. {@code false} otherwise.
*/
private boolean containsImports(Path javaFile) {
List<String> imports = getAllImportsInTheFile(javaFile);
return imports.stream().anyMatch(line -> line.startsWith(getImportToCheck()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package io.jenkins.pluginhealth.scoring.probes;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;

import io.jenkins.pluginhealth.scoring.model.Plugin;
import io.jenkins.pluginhealth.scoring.model.ProbeResult;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class JSR305ProbeTest extends AbstractProbeTest<JSR305Probe> {
private Plugin plugin;
private ProbeContext ctx;
private JSR305Probe probe;

@BeforeEach
public void init() {
plugin = mock(Plugin.class);
ctx = mock(ProbeContext.class);
probe = getSpy();

when(plugin.getDetails()).thenReturn(Map.of(
SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, "")
));
}

@Override
JSR305Probe getSpy() {
return spy(JSR305Probe.class);
}

@Test
void shouldReturnPluginsThatUseDeprecatedAnnotations() throws IOException {
final Path repo = Files.createTempDirectory("foo");
Path directory = Files.createDirectories(repo.resolve("src/main/java"));
final Path javaFileWithAllDeprecatedAnnotation = Files.createFile(directory.resolve("test-class-1.java"));
final Path javaFileWithNonnullDeprecatedAnnotation = Files.createFile(directory.resolve("test-class-2.java"));
final Path javaFileWithCheckForNullDeprecatedAnnotation = Files.createFile(directory.resolve("test-class-3.java"));
final Path javaFileWithNoDeprecatedAnnotation = Files.createFile(directory.resolve("test-class-4.java"));
final Path txtFileShouldNotBeFound = Files.createFile(directory.resolve("file.txt"));
Files.createFile(directory.resolve("test-dummy-class-should-not-be-returned.java"));

Files.write(javaFileWithAllDeprecatedAnnotation, List.of(
"package test;",
"",
"import javax.annotation.Nonnull;",
"import javax.annotation.CheckForNull;",
"",
"import java.util.HashMap;",
"import java.util.Map;"
));

Files.write(javaFileWithNonnullDeprecatedAnnotation, List.of(
"package test;",
"",
"import javax.annotation.Nonnull;"
));

Files.write(javaFileWithCheckForNullDeprecatedAnnotation, List.of(
"package test;",
"",
"import javax.annotation.CheckForNull;"
));

Files.write(javaFileWithNoDeprecatedAnnotation, List.of(
"package test;"
));

Files.write(txtFileShouldNotBeFound, List.of(
"This-file-should-not-returned."
));

when(ctx.getScmRepository()).thenReturn(repo);
when(plugin.getName()).thenReturn("foo");

assertThat(probe.apply(plugin, ctx))
.usingRecursiveComparison()
.comparingOnlyFields("id", "message", "status")
.isEqualTo(ProbeResult.failure(JSR305Probe.KEY, "Deprecated imports found at foo plugin for test-class-1.java, test-class-2.java, test-class-3.java"));
verify(probe).doApply(any(Plugin.class), any(ProbeContext.class));

}

@Test
void shouldNotReturnPluginsWithNoDeprecatedImports() throws IOException {
final Path repo = Files.createTempDirectory("foo");
Path directory = Files.createDirectories(repo.resolve("src/main/java"));
final Path javaFileWithUpdatedAnnotation = Files.createFile(directory.resolve("test-class-3.java"));
Files.createFile(directory.resolve("test-dummy-class-should-not-be-returned.java"));

Files.write(javaFileWithUpdatedAnnotation, List.of(
"package test;",
"",
"import edu.umd.cs.findbugs.annotations.NonNull;",
"import edu.umd.cs.findbugs.annotations.CheckForNull;",
"",
"import java.util.HashMap;",
"import java.util.Map;"
));

when(ctx.getScmRepository()).thenReturn(repo);
when(plugin.getName()).thenReturn("foo");

assertThat(probe.apply(plugin, ctx))
.usingRecursiveComparison()
.comparingOnlyFields("id", "message", "status")
.isEqualTo(ProbeResult.success(JSR305Probe.KEY, "Latest version of imports found at foo plugin."));
verify(probe).doApply(any(Plugin.class), any(ProbeContext.class));
}


}

0 comments on commit f68d23b

Please sign in to comment.