-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTextReporter.java
More file actions
147 lines (133 loc) · 5.97 KB
/
Copy pathTextReporter.java
File metadata and controls
147 lines (133 loc) · 5.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package io.github.randomcodespace.sonarpredict.cli;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import io.github.randomcodespace.sonarpredict.cli.coverage.CoverageReport;
import io.github.randomcodespace.sonarpredict.cli.coverage.FileCoverage;
import io.github.randomcodespace.sonarpredict.protocol.dto.AnalysisWarning;
import io.github.randomcodespace.sonarpredict.protocol.dto.AnalyzeResponse;
import io.github.randomcodespace.sonarpredict.protocol.dto.Issue;
import io.github.randomcodespace.sonarpredict.protocol.dto.RuleMetadata;
/**
* Renders an {@link AnalyzeResponse} as a human-readable report: issues grouped
* under a per-file header, each line carrying the location, severity, rule key,
* and message. When the {@link RuleMetadataIndex} carries metadata for a rule,
* an indented rationale/fix line is added beneath the issue. A clean response
* is stated plainly; analysis warnings are listed in their own trailing section.
*/
public final class TextReporter implements Reporter {
@Override
public String render(AnalyzeResponse response, RuleMetadataIndex index,
CoverageReport coverage) {
StringBuilder out = new StringBuilder();
Map<String, List<Issue>> byFile = IssueGrouping.byFile(response.issues());
if (byFile.isEmpty()) {
out.append("No issues found.\n");
} else {
appendIssues(out, byFile, index, response.issues().size());
}
appendWarnings(out, response.warnings());
if (coverage != null) {
appendCoverage(out, coverage);
}
return out.toString();
}
/** Renders every per-file issue block and the trailing summary line. */
private static void appendIssues(StringBuilder out,
Map<String, List<Issue>> byFile,
RuleMetadataIndex index, int totalCount) {
for (Map.Entry<String, List<Issue>> entry : byFile.entrySet()) {
out.append(entry.getKey()).append('\n');
for (Issue issue : entry.getValue()) {
appendIssue(out, issue, index);
}
}
out.append('\n')
.append(totalCount).append(totalCount == 1 ? " issue" : " issues")
.append(" in ").append(byFile.size())
.append(byFile.size() == 1 ? " file.\n" : " files.\n");
}
/** Renders one issue line plus any indented rule guidance beneath it. */
private static void appendIssue(StringBuilder out, Issue issue,
RuleMetadataIndex index) {
out.append(" ")
.append(issue.startLine()).append(':').append(issue.startColumn())
.append(" ").append(issue.severity())
.append(" ").append(issue.ruleKey())
.append(" ").append(issue.message())
.append('\n');
appendRuleGuidance(out, index.lookup(issue.ruleKey()));
}
/** Renders the analysis-warnings section, if there are any. */
private static void appendWarnings(StringBuilder out, List<AnalysisWarning> warnings) {
if (warnings == null || warnings.isEmpty()) {
return;
}
out.append('\n').append("Warnings:\n");
for (AnalysisWarning warning : warnings) {
out.append(" ");
if (warning.filePath() != null && !warning.filePath().isBlank()) {
out.append(warning.filePath()).append(": ");
}
out.append(warning.message()).append('\n');
}
}
/**
* Appends a coverage summary: the overall percentage followed by a
* per-file breakdown, each line {@code <pct> <path>}.
*/
private static void appendCoverage(StringBuilder out, CoverageReport coverage) {
out.append('\n').append("Coverage: ")
.append(formatPercent(coverage.overallPercent()))
.append(" overall");
List<FileCoverage> files = coverage.files();
if (files.isEmpty()) {
out.append(" (no files reported).\n");
return;
}
out.append('\n');
for (FileCoverage file : files) {
out.append(" ").append(formatPercent(file.percent()))
.append(" ").append(file.path()).append('\n');
}
}
/** Formats a percentage to one decimal place with a trailing {@code %}. */
private static String formatPercent(double percent) {
return String.format(Locale.ROOT, "%.1f%%", percent);
}
/**
* Appends an indented rule rationale and fix line beneath an issue, when
* the metadata is present. The how-to-fix guidance is preferred; if the
* rule provides none, a short plain-text rationale from the description is
* shown instead.
*/
private static void appendRuleGuidance(StringBuilder out, RuleMetadata metadata) {
if (metadata == null) {
return;
}
if (metadata.name() != null && !metadata.name().isBlank()) {
out.append(" rule: ").append(metadata.name()).append('\n');
}
String guidance = metadata.howToFix() != null && !metadata.howToFix().isBlank()
? metadata.howToFix()
: rationaleFrom(metadata.descriptionHtml());
if (guidance != null && !guidance.isBlank()) {
out.append(" fix: ").append(guidance).append('\n');
}
}
/** Strips HTML tags and trims the description to a single short sentence. */
private static String rationaleFrom(String descriptionHtml) {
if (descriptionHtml == null || descriptionHtml.isBlank()) {
return null;
}
String plain = descriptionHtml
.replaceAll("<[^>]+>", " ")
.replaceAll("\\s+", " ")
.strip();
int dot = plain.indexOf(". ");
if (dot > 0 && dot < 200) {
return plain.substring(0, dot + 1);
}
return plain.length() > 200 ? plain.substring(0, 200) + "…" : plain;
}
}