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+ }
0 commit comments