Skip to content

Commit 1389847

Browse files
Add a parser for Phan JSON output (#1492)
1 parent 813786a commit 1389847

7 files changed

Lines changed: 586 additions & 1 deletion

File tree

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package edu.hm.hafner.analysis.parser;
2+
3+
import org.json.JSONArray;
4+
import org.json.JSONObject;
5+
6+
import edu.hm.hafner.analysis.Issue;
7+
import edu.hm.hafner.analysis.IssueBuilder;
8+
import edu.hm.hafner.analysis.Report;
9+
import edu.hm.hafner.analysis.Severity;
10+
11+
import java.io.Serial;
12+
13+
/**
14+
* A parser for Phan JSON output.
15+
*
16+
* @author Akash Manna
17+
* @see <a href="https://github.com/phan/phan">Phan on GitHub</a>
18+
* @see <a href="https://phpqa.io/projects/phan.html">Phan documentation</a>
19+
*/
20+
public class PhanParser extends JsonIssueParser {
21+
@Serial
22+
private static final long serialVersionUID = 1719197864112329217L;
23+
24+
private static final String ISSUES = "issues";
25+
private static final String TYPE = "type";
26+
private static final String CHECK_NAME = "check_name";
27+
private static final String DESCRIPTION = "description";
28+
private static final String SEVERITY = "severity";
29+
private static final String LOCATION = "location";
30+
private static final String PATH = "path";
31+
private static final String LINES = "lines";
32+
private static final String BEGIN = "begin";
33+
private static final String END = "end";
34+
private static final String BEGIN_COLUMN = "begin_column";
35+
private static final String COLUMN = "column";
36+
private static final int SPLIT_LIMIT = 3;
37+
38+
@Override
39+
protected void parseJsonObject(final Report report, final JSONObject jsonReport, final IssueBuilder issueBuilder) {
40+
var issues = jsonReport.optJSONArray(ISSUES);
41+
if (issues != null) {
42+
parseIssues(report, issues, issueBuilder);
43+
return;
44+
}
45+
46+
if (isIssue(jsonReport)) {
47+
report.add(convertToIssue(jsonReport, issueBuilder));
48+
}
49+
}
50+
51+
@Override
52+
protected void parseJsonArray(final Report report, final JSONArray jsonReport, final IssueBuilder issueBuilder) {
53+
parseIssues(report, jsonReport, issueBuilder);
54+
}
55+
56+
private void parseIssues(final Report report, final JSONArray issues, final IssueBuilder issueBuilder) {
57+
for (int i = 0; i < issues.length(); i++) {
58+
var issue = issues.optJSONObject(i);
59+
if (issue != null && isIssue(issue)) {
60+
report.add(convertToIssue(issue, issueBuilder));
61+
}
62+
}
63+
}
64+
65+
private boolean isIssue(final JSONObject jsonIssue) {
66+
return jsonIssue.has(CHECK_NAME) || jsonIssue.has(TYPE) || jsonIssue.has(DESCRIPTION) || jsonIssue.has(LOCATION);
67+
}
68+
69+
private Issue convertToIssue(final JSONObject jsonIssue, final IssueBuilder issueBuilder) {
70+
var description = jsonIssue.optString(DESCRIPTION, "");
71+
var checkName = jsonIssue.optString(CHECK_NAME, jsonIssue.optString(TYPE, "-"));
72+
73+
issueBuilder
74+
.setType(checkName)
75+
.setMessage(extractMessage(description, checkName))
76+
.setDescription(description)
77+
.setSeverity(parseSeverity(jsonIssue.opt(SEVERITY)));
78+
79+
applyLocation(jsonIssue.optJSONObject(LOCATION), issueBuilder);
80+
81+
return issueBuilder.buildAndClean();
82+
}
83+
84+
private Severity parseSeverity(final Object severityValue) {
85+
if (severityValue instanceof Number number) {
86+
return switch (number.intValue()) {
87+
case 10 -> Severity.ERROR;
88+
case 5 -> Severity.WARNING_NORMAL;
89+
case 0 -> Severity.WARNING_LOW;
90+
default -> number.intValue() > 5 ? Severity.ERROR : Severity.WARNING_LOW;
91+
};
92+
}
93+
if (severityValue instanceof String string) {
94+
try {
95+
return parseSeverity(Integer.valueOf(string));
96+
}
97+
catch (NumberFormatException exception) {
98+
return Severity.guessFromString(string);
99+
}
100+
}
101+
102+
return Severity.WARNING_LOW;
103+
}
104+
105+
private void applyLocation(final JSONObject location, final IssueBuilder issueBuilder) {
106+
if (location == null) {
107+
return;
108+
}
109+
110+
issueBuilder.setFileName(location.optString(PATH, ""));
111+
112+
var lines = location.optJSONObject(LINES);
113+
if (lines == null) {
114+
return;
115+
}
116+
117+
var lineStart = lines.optInt(BEGIN, 0);
118+
if (lines.has(BEGIN)) {
119+
issueBuilder.setLineStart(lineStart).setLineEnd(lines.optInt(END, lineStart));
120+
}
121+
122+
var columnStart = lines.has(BEGIN_COLUMN) ? lines.optInt(BEGIN_COLUMN, 0) : lines.optInt(COLUMN, 0);
123+
if (lines.has(BEGIN_COLUMN) || lines.has(COLUMN)) {
124+
issueBuilder.setColumnStart(columnStart).setColumnEnd(columnStart);
125+
}
126+
}
127+
128+
private String extractMessage(final String description, final String checkName) {
129+
if (description.isBlank()) {
130+
return description;
131+
}
132+
133+
var checkNameIndex = description.indexOf(checkName);
134+
if (checkNameIndex >= 0) {
135+
var messageStart = checkNameIndex + checkName.length();
136+
if (messageStart < description.length()) {
137+
return description.substring(messageStart).stripLeading();
138+
}
139+
}
140+
141+
var parts = description.split(" ", SPLIT_LIMIT);
142+
if (parts.length == SPLIT_LIMIT) {
143+
return parts[2];
144+
}
145+
146+
return description;
147+
}
148+
}

src/main/java/edu/hm/hafner/analysis/registry/ParserRegistry.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ public class ParserRegistry {
140140
new PerlCriticDescriptor(),
141141
new PhpCodeSnifferDescriptor(),
142142
new PhpDescriptor(),
143+
new PhanDescriptor(),
143144
new PhpStanDescriptor(),
144145
new PitDescriptor(),
145146
new PmdDescriptor(),
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package edu.hm.hafner.analysis.registry;
2+
3+
import edu.hm.hafner.analysis.IssueParser;
4+
import edu.hm.hafner.analysis.parser.PhanParser;
5+
6+
/**
7+
* A descriptor for Phan JSON reports.
8+
*
9+
* @author Akash Manna
10+
*/
11+
class PhanDescriptor extends ParserDescriptor {
12+
private static final String ID = "phan";
13+
private static final String NAME = "Phan";
14+
15+
PhanDescriptor() {
16+
super(ID, NAME);
17+
}
18+
19+
@Override
20+
public IssueParser create(final Option... options) {
21+
return new PhanParser();
22+
}
23+
24+
@Override
25+
public String getPattern() {
26+
return "**/phan-report.json";
27+
}
28+
29+
@Override
30+
public String getHelp() {
31+
return "Use <code>phan --output-mode json &gt; phan-report.json</code> to generate JSON output.<br/>"
32+
+ "See <a href='https://github.com/phan/phan'>Phan on GitHub</a> for usage details. "
33+
+ "See <a href='https://phpqa.io/projects/phan.html'>Phan documentation</a> for usage details.";
34+
}
35+
36+
@Override
37+
public String getUrl() {
38+
return "https://github.com/phan/phan";
39+
}
40+
}

0 commit comments

Comments
 (0)