
edu.hm.hafner.analysis.IssueDifference Maven / Gradle / Ivy
package edu.hm.hafner.analysis;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.UUID;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
* Computes old, new, and fixed issues based on the reports of two consecutive static analysis runs for the same
* software artifact.
*
* @author Ullrich Hafner
*/
@SuppressWarnings("PMD.DataClass")
public class IssueDifference {
private static final List EMPTY = Collections.emptyList();
private final Report newIssues;
private final Report newIssuesInChangedCode;
private final Report fixedIssues;
private final Report outstandingIssues;
private final Map> referencesByHash;
private final Map> referencesByFingerprint;
/**
* Creates a new instance of {@link IssueDifference}.
*
* @param currentIssues
* the issues of the current report
* @param referenceId
* ID identifying the reference report
* @param referenceIssues
* the issues of a previous report (reference)
*/
public IssueDifference(final Report currentIssues, final String referenceId,
final Report referenceIssues) {
this(currentIssues, referenceId, referenceIssues, Map.of());
}
/**
* Creates a new instance of {@link IssueDifference}.
*
* @param currentIssues
* the issues of the current report
* @param referenceId
* ID identifying the reference report
* @param referenceIssues
* the issues of a previous report (reference)
* @param includes
* A mapping of files to changed lines. Using this mapping, we can identify which new issues are part
* of the changes and which issues are indirectly caused by the changes.
*/
public IssueDifference(final Report currentIssues, final String referenceId,
final Report referenceIssues, final Map includes) {
newIssues = currentIssues.copy();
fixedIssues = referenceIssues.copy();
outstandingIssues = new Report();
newIssuesInChangedCode = new Report();
referencesByHash = new HashMap<>();
referencesByFingerprint = new HashMap<>();
for (Issue issue : referenceIssues) {
addIssueToMap(referencesByHash, issue.hashCode(), issue);
addIssueToMap(referencesByFingerprint, issue.getFingerprint(), issue);
}
List removed = matchIssuesByEquals(currentIssues);
Report secondPass = currentIssues.copy();
removed.forEach(secondPass::remove);
matchIssuesByFingerprint(secondPass);
newIssues.forEach(issue -> issue.setReference(referenceId));
if (!includes.isEmpty()) {
findIssuesInChangedCode(includes);
}
}
private void findIssuesInChangedCode(final Map includes) {
for (Entry include : includes.entrySet()) {
newIssues.filter(issue -> filter(issue, include.getKey(), include.getValue()))
.stream()
.map(Issue::getId)
.map(newIssues::remove)
.forEach(newIssuesInChangedCode::add);
}
}
private boolean filter(final Issue issue, final String fileName, final int line) {
if (!issue.getFileName().endsWith(fileName)) {
return false;
}
return issue.affectsLine(line);
}
private List matchIssuesByEquals(final Report currentIssues) {
List removedIds = new ArrayList<>();
for (Issue current : currentIssues) {
List equalIssues = findReferenceByEquals(current);
if (!equalIssues.isEmpty()) {
removedIds.add(remove(current, selectIssueWithSameFingerprint(current, equalIssues)));
}
}
return removedIds;
}
private void matchIssuesByFingerprint(final Report currentIssues) {
for (Issue current : currentIssues) {
findReferenceByFingerprint(current).ifPresent(issue -> remove(current, issue));
}
}
private void addIssueToMap(final Map> map, final K key, final Issue issue) {
map.computeIfAbsent(key, k -> new ArrayList<>()).add(issue);
}
@SuppressWarnings("NullAway")
private void removeIssueFromMap(final Map> map, final K key, final Issue issue) {
List issues = map.get(key);
issues.remove(issue);
if (issues.isEmpty()) {
map.remove(key);
}
}
private UUID remove(final Issue current, final Issue oldIssue) {
UUID id = current.getId();
Issue issueWithLatestProperties = newIssues.remove(id);
issueWithLatestProperties.setReference(oldIssue.getReference());
outstandingIssues.add(issueWithLatestProperties);
fixedIssues.remove(oldIssue.getId());
removeIssueFromMap(referencesByFingerprint, oldIssue.getFingerprint(), oldIssue);
removeIssueFromMap(referencesByHash, oldIssue.hashCode(), oldIssue);
return id;
}
private Issue selectIssueWithSameFingerprint(final Issue current, final List equalIssues) {
return equalIssues.stream()
.filter(issue -> issue.getFingerprint().equals(current.getFingerprint()))
.findFirst()
.orElse(equalIssues.get(0));
}
private Optional findReferenceByFingerprint(final Issue current) {
return referencesByFingerprint.getOrDefault(current.getFingerprint(), EMPTY).stream().findAny();
}
private List findReferenceByEquals(final Issue current) {
List equalIssues = new ArrayList<>();
for (Issue reference : referencesByHash.getOrDefault(current.hashCode(), EMPTY)) {
if (current.equals(reference)) {
equalIssues.add(reference);
}
}
return equalIssues;
}
/**
* Returns the outstanding issues. I.e., all issues that are part of the previous report and that are still part of
* the current report.
*
* @return the outstanding issues
*/
@SuppressFBWarnings("EI")
public Report getOutstandingIssues() {
return outstandingIssues;
}
/**
* Returns the new issues. I.e., all issues that are part of the current report but that have not been shown up in
* the previous report. If the difference is computed for a specific set of changed files, then this set contains
* only the new issues that are not part of the changes. These issues are indirectly caused by the changes.
*
* @return the new issues
*/
@SuppressFBWarnings("EI")
public Report getNewIssues() {
return newIssues;
}
/**
* Returns the fixed issues. I.e., all issues that are part of the previous report but that are not present in the
* current report anymore.
*
* @return the fixed issues
*/
@SuppressFBWarnings("EI")
public Report getFixedIssues() {
return fixedIssues;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy