All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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