-
-
Notifications
You must be signed in to change notification settings - Fork 48
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
JSR-305 probe. Checks for
@Nonnull
and @CheckForNull
deprecated a…
…nnotations. (#359) Co-authored-by: Adrien Lecharpentier <[email protected]>
- Loading branch information
Showing
3 changed files
with
246 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
122 changes: 122 additions & 0 deletions
122
core/src/main/java/io/jenkins/pluginhealth/scoring/probes/JSR305Probe.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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())); | ||
} | ||
} |
123 changes: 123 additions & 0 deletions
123
core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JSR305ProbeTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
|
||
|
||
} |