EvidenceReportWriter.java

1
// SPDX-License-Identifier: Apache-2.0
2
// Copyright 2026 Egothor
3
// Copyright 2026 Accenture
4
package org.egothor.methodatlas.evidencereport;
5
6
import java.io.IOException;
7
import java.io.PrintWriter;
8
import java.nio.charset.StandardCharsets;
9
import java.nio.file.Files;
10
import java.nio.file.Path;
11
import java.util.LinkedHashMap;
12
import java.util.List;
13
import java.util.Map;
14
15
/**
16
 * Serialises an {@link EvidenceReport} to disk as a Markdown document.
17
 *
18
 * <p>
19
 * Package-private because nothing outside the {@code evidencereport} package needs
20
 * direct access; {@link EvidenceReportFacade} is the sole external caller.
21
 * </p>
22
 */
23
final class EvidenceReportWriter {
24
25
    private EvidenceReportWriter() {
26
        // Utility class.
27
    }
28
29
    /**
30
     * Writes {@code report} to {@code outputFile} as UTF-8 Markdown.
31
     *
32
     * @param report     the evidence report to serialise
33
     * @param outputFile destination path; parent directory must exist
34
     * @throws IOException if the file cannot be written
35
     */
36
    /* default */ static void write(EvidenceReport report, Path outputFile) throws IOException {
37
        try (PrintWriter out = new PrintWriter(
38
                Files.newBufferedWriter(outputFile, StandardCharsets.UTF_8))) {
39
            writeHeader(out, report);
40
            writeSummary(out, report);
41
            writeMethods(out, report);
42
        }
43
    }
44
45
    private static void writeHeader(PrintWriter out, EvidenceReport report) {
46
        out.println("# MethodAtlas Security Evidence Report");
47
        out.println();
48
        out.println("**Generated:** " + report.generatedUtc() + "  ");
49
        out.println("**Tool version:** " + report.toolVersion() + "  ");
50 2 1. writeHeader : removed conditional - replaced equality check with false → NO_COVERAGE
2. writeHeader : removed conditional - replaced equality check with true → NO_COVERAGE
        if (!report.scanRoots().isEmpty()) {
51
            out.print("**Scan roots:** ");
52
            StringBuilder sb = new StringBuilder();
53
            for (String root : report.scanRoots()) {
54 3 1. writeHeader : removed conditional - replaced comparison check with true → NO_COVERAGE
2. writeHeader : changed conditional boundary → NO_COVERAGE
3. writeHeader : removed conditional - replaced comparison check with false → NO_COVERAGE
                if (sb.length() > 0) {
55
                    sb.append(", ");
56
                }
57
                sb.append('`').append(root).append('`');
58
            }
59
            out.println(sb + "  ");
60
        }
61
        out.println("**Total methods scanned:** " + report.totalScanned() + "  ");
62
        out.println("**Security-relevant methods:** " + report.securityRelevantCount() + "  ");
63
        out.println();
64
    }
65
66
    private static void writeSummary(PrintWriter out, EvidenceReport report) {
67
        out.println("## Summary");
68
        out.println();
69
        Map<String, Integer> domainCounts = new LinkedHashMap<>();
70
        for (List<EvidenceReportEntry> entries : report.byClass().values()) {
71
            for (EvidenceReportEntry entry : entries) {
72 2 1. writeSummary : removed conditional - replaced equality check with true → NO_COVERAGE
2. writeSummary : removed conditional - replaced equality check with false → NO_COVERAGE
                if (entry.aiTags() != null) {
73
                    for (String tag : entry.aiTags()) {
74
                        domainCounts.merge(tag, 1, Integer::sum);
75
                    }
76
                }
77
            }
78
        }
79 2 1. writeSummary : removed conditional - replaced equality check with false → NO_COVERAGE
2. writeSummary : removed conditional - replaced equality check with true → NO_COVERAGE
        if (domainCounts.isEmpty()) {
80
            out.println("No security-relevant methods were found in this scan.");
81
        } else {
82
            out.println("| Domain | Count |");
83
            out.println("|--------|-------|");
84
            for (Map.Entry<String, Integer> e : domainCounts.entrySet()) {
85
                out.println("| " + e.getKey() + " | " + e.getValue() + " |");
86
            }
87
        }
88
        out.println();
89
    }
90
91
    private static void writeMethods(PrintWriter out, EvidenceReport report) {
92 2 1. writeMethods : removed conditional - replaced equality check with false → NO_COVERAGE
2. writeMethods : removed conditional - replaced equality check with true → NO_COVERAGE
        if (report.byClass().isEmpty()) {
93
            return;
94
        }
95
        out.println("## Security Test Methods");
96
        out.println();
97
        for (Map.Entry<String, List<EvidenceReportEntry>> classEntry : report.byClass().entrySet()) {
98
            out.println("### " + classEntry.getKey());
99
            out.println();
100
            for (EvidenceReportEntry entry : classEntry.getValue()) {
101
                writeEntry(out, entry);
102
            }
103
        }
104
    }
105
106
    @SuppressWarnings("PMD.NPathComplexity")
107
    private static void writeEntry(PrintWriter out, EvidenceReportEntry entry) {
108
        out.println("#### " + entry.method());
109
        out.println();
110
        out.println("| Field | Value |");
111
        out.println("|-------|-------|");
112 2 1. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE
2. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE
        String dispName = entry.aiDisplayName() != null ? entry.aiDisplayName()
113 2 1. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE
2. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE
                : entry.displayName() != null ? entry.displayName() : "";
114 2 1. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE
2. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE
        if (!dispName.isEmpty()) {
115
            out.println("| Display name | " + mdEscape(dispName) + " |");
116
        }
117 2 1. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE
2. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE
        if (!entry.sourceTags().isEmpty()) {
118
            out.println("| Source tags | " + String.join(", ", entry.sourceTags()) + " |");
119
        }
120 4 1. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE
2. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE
3. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE
4. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE
        if (entry.aiTags() != null && !entry.aiTags().isEmpty()) {
121
            out.println("| AI tags | " + String.join(", ", entry.aiTags()) + " |");
122
        }
123 2 1. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE
2. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE
        if (entry.aiSecurityRelevant() != null) {
124
            out.println("| AI security relevant | " + entry.aiSecurityRelevant() + " |");
125
        }
126 2 1. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE
2. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE
        if (entry.aiConfidence() != null) {
127
            out.println("| Confidence | " + entry.aiConfidence() + " |");
128
        }
129 2 1. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE
2. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE
        if (entry.aiInteractionScore() != null) {
130
            out.println("| Interaction score | " + entry.aiInteractionScore() + " |");
131
        }
132 2 1. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE
2. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE
        if (entry.contentHash() != null) {
133
            out.println("| Content hash | `" + entry.contentHash() + "` |");
134
        }
135 4 1. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE
2. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE
3. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE
4. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE
        if (entry.aiReason() != null && !entry.aiReason().isBlank()) {
136
            out.println("| Reason | " + mdEscape(entry.aiReason()) + " |");
137
        }
138 2 1. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE
2. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE
        if (!entry.aiCwe().isEmpty()) {
139
            out.println("| CWE | " + String.join(", ", entry.aiCwe()) + " |");
140
        }
141 3 1. writeEntry : removed conditional - replaced comparison check with false → NO_COVERAGE
2. writeEntry : changed conditional boundary → NO_COVERAGE
3. writeEntry : removed conditional - replaced comparison check with true → NO_COVERAGE
        if (entry.beginLine() > 0) {
142
            out.println("| Line | " + entry.beginLine() + " |");
143
        }
144
        out.println();
145
    }
146
147
    /** Escapes pipe characters so they do not break Markdown table cells. */
148
    private static String mdEscape(String value) {
149 1 1. mdEscape : replaced return value with "" for org/egothor/methodatlas/evidencereport/EvidenceReportWriter::mdEscape → NO_COVERAGE
        return value.replace("|", "\\|");
150
    }
151
}

Mutations

50

1.1
Location : writeHeader
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

2.2
Location : writeHeader
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

54

1.1
Location : writeHeader
Killed by : none
removed conditional - replaced comparison check with true → NO_COVERAGE

2.2
Location : writeHeader
Killed by : none
changed conditional boundary → NO_COVERAGE

3.3
Location : writeHeader
Killed by : none
removed conditional - replaced comparison check with false → NO_COVERAGE

72

1.1
Location : writeSummary
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

2.2
Location : writeSummary
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

79

1.1
Location : writeSummary
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

2.2
Location : writeSummary
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

92

1.1
Location : writeMethods
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

2.2
Location : writeMethods
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

112

1.1
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

2.2
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

113

1.1
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

2.2
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

114

1.1
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

2.2
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

117

1.1
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

2.2
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

120

1.1
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

2.2
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

3.3
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

4.4
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

123

1.1
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

2.2
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

126

1.1
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

2.2
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

129

1.1
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

2.2
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

132

1.1
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

2.2
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

135

1.1
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

2.2
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

3.3
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

4.4
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

138

1.1
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

2.2
Location : writeEntry
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

141

1.1
Location : writeEntry
Killed by : none
removed conditional - replaced comparison check with false → NO_COVERAGE

2.2
Location : writeEntry
Killed by : none
changed conditional boundary → NO_COVERAGE

3.3
Location : writeEntry
Killed by : none
removed conditional - replaced comparison check with true → NO_COVERAGE

149

1.1
Location : mdEscape
Killed by : none
replaced return value with "" for org/egothor/methodatlas/evidencereport/EvidenceReportWriter::mdEscape → NO_COVERAGE

Active mutators

Tests examined


Report generated by PIT 1.30.1 support