1 | /* JSON output for diagnostics |
2 | Copyright (C) 2018-2023 Free Software Foundation, Inc. |
3 | Contributed by David Malcolm <dmalcolm@redhat.com>. |
4 | |
5 | This file is part of GCC. |
6 | |
7 | GCC is free software; you can redistribute it and/or modify it under |
8 | the terms of the GNU General Public License as published by the Free |
9 | Software Foundation; either version 3, or (at your option) any later |
10 | version. |
11 | |
12 | GCC is distributed in the hope that it will be useful, but WITHOUT ANY |
13 | WARRANTY; without even the implied warranty of MERCHANTABILITY or |
14 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
15 | for more details. |
16 | |
17 | You should have received a copy of the GNU General Public License |
18 | along with GCC; see the file COPYING3. If not see |
19 | <http://www.gnu.org/licenses/>. */ |
20 | |
21 | |
22 | #include "config.h" |
23 | #include "system.h" |
24 | #include "coretypes.h" |
25 | #include "diagnostic.h" |
26 | #include "selftest-diagnostic.h" |
27 | #include "diagnostic-metadata.h" |
28 | #include "json.h" |
29 | #include "selftest.h" |
30 | |
31 | /* Subclass of diagnostic_output_format for JSON output. */ |
32 | |
33 | class json_output_format : public diagnostic_output_format |
34 | { |
35 | public: |
36 | void on_begin_group () final override |
37 | { |
38 | /* No-op. */ |
39 | } |
40 | void on_end_group () final override |
41 | { |
42 | m_cur_group = nullptr; |
43 | m_cur_children_array = nullptr; |
44 | } |
45 | void |
46 | on_begin_diagnostic (diagnostic_info *) final override |
47 | { |
48 | /* No-op. */ |
49 | } |
50 | void |
51 | on_end_diagnostic (diagnostic_info *diagnostic, |
52 | diagnostic_t orig_diag_kind) final override; |
53 | void on_diagram (const diagnostic_diagram &) final override |
54 | { |
55 | /* No-op. */ |
56 | } |
57 | |
58 | protected: |
59 | json_output_format (diagnostic_context &context) |
60 | : diagnostic_output_format (context), |
61 | m_toplevel_array (new json::array ()), |
62 | m_cur_group (nullptr), |
63 | m_cur_children_array (nullptr) |
64 | { |
65 | } |
66 | |
67 | /* Flush the top-level array to OUTF. */ |
68 | void |
69 | flush_to_file (FILE *outf) |
70 | { |
71 | m_toplevel_array->dump (outf); |
72 | fprintf (stream: outf, format: "\n" ); |
73 | delete m_toplevel_array; |
74 | m_toplevel_array = nullptr; |
75 | } |
76 | |
77 | private: |
78 | /* The top-level JSON array of pending diagnostics. */ |
79 | json::array *m_toplevel_array; |
80 | |
81 | /* The JSON object for the current diagnostic group. */ |
82 | json::object *m_cur_group; |
83 | |
84 | /* The JSON array for the "children" array within the current diagnostic |
85 | group. */ |
86 | json::array *m_cur_children_array; |
87 | }; |
88 | |
89 | /* Generate a JSON object for LOC. */ |
90 | |
91 | json::value * |
92 | json_from_expanded_location (diagnostic_context *context, location_t loc) |
93 | { |
94 | expanded_location exploc = expand_location (loc); |
95 | json::object *result = new json::object (); |
96 | if (exploc.file) |
97 | result->set (key: "file" , v: new json::string (exploc.file)); |
98 | result->set (key: "line" , v: new json::integer_number (exploc.line)); |
99 | |
100 | const enum diagnostics_column_unit orig_unit = context->m_column_unit; |
101 | struct |
102 | { |
103 | const char *name; |
104 | enum diagnostics_column_unit unit; |
105 | } column_fields[] = { |
106 | {.name: "display-column" , .unit: DIAGNOSTICS_COLUMN_UNIT_DISPLAY}, |
107 | {.name: "byte-column" , .unit: DIAGNOSTICS_COLUMN_UNIT_BYTE} |
108 | }; |
109 | int the_column = INT_MIN; |
110 | for (int i = 0; i != ARRAY_SIZE (column_fields); ++i) |
111 | { |
112 | context->m_column_unit = column_fields[i].unit; |
113 | const int col = context->converted_column (s: exploc); |
114 | result->set (key: column_fields[i].name, v: new json::integer_number (col)); |
115 | if (column_fields[i].unit == orig_unit) |
116 | the_column = col; |
117 | } |
118 | gcc_assert (the_column != INT_MIN); |
119 | result->set (key: "column" , v: new json::integer_number (the_column)); |
120 | context->m_column_unit = orig_unit; |
121 | return result; |
122 | } |
123 | |
124 | /* Generate a JSON object for LOC_RANGE. */ |
125 | |
126 | static json::object * |
127 | json_from_location_range (diagnostic_context *context, |
128 | const location_range *loc_range, unsigned range_idx) |
129 | { |
130 | location_t caret_loc = get_pure_location (loc: loc_range->m_loc); |
131 | |
132 | if (caret_loc == UNKNOWN_LOCATION) |
133 | return NULL; |
134 | |
135 | location_t start_loc = get_start (loc: loc_range->m_loc); |
136 | location_t finish_loc = get_finish (loc: loc_range->m_loc); |
137 | |
138 | json::object *result = new json::object (); |
139 | result->set (key: "caret" , v: json_from_expanded_location (context, loc: caret_loc)); |
140 | if (start_loc != caret_loc |
141 | && start_loc != UNKNOWN_LOCATION) |
142 | result->set (key: "start" , v: json_from_expanded_location (context, loc: start_loc)); |
143 | if (finish_loc != caret_loc |
144 | && finish_loc != UNKNOWN_LOCATION) |
145 | result->set (key: "finish" , v: json_from_expanded_location (context, loc: finish_loc)); |
146 | |
147 | if (loc_range->m_label) |
148 | { |
149 | label_text text (loc_range->m_label->get_text (range_idx)); |
150 | if (text.get ()) |
151 | result->set (key: "label" , v: new json::string (text.get ())); |
152 | } |
153 | |
154 | return result; |
155 | } |
156 | |
157 | /* Generate a JSON object for HINT. */ |
158 | |
159 | static json::object * |
160 | json_from_fixit_hint (diagnostic_context *context, const fixit_hint *hint) |
161 | { |
162 | json::object *fixit_obj = new json::object (); |
163 | |
164 | location_t start_loc = hint->get_start_loc (); |
165 | fixit_obj->set (key: "start" , v: json_from_expanded_location (context, loc: start_loc)); |
166 | location_t next_loc = hint->get_next_loc (); |
167 | fixit_obj->set (key: "next" , v: json_from_expanded_location (context, loc: next_loc)); |
168 | fixit_obj->set (key: "string" , v: new json::string (hint->get_string ())); |
169 | |
170 | return fixit_obj; |
171 | } |
172 | |
173 | /* Generate a JSON object for METADATA. */ |
174 | |
175 | static json::object * |
176 | json_from_metadata (const diagnostic_metadata *metadata) |
177 | { |
178 | json::object *metadata_obj = new json::object (); |
179 | |
180 | if (metadata->get_cwe ()) |
181 | metadata_obj->set (key: "cwe" , |
182 | v: new json::integer_number (metadata->get_cwe ())); |
183 | |
184 | return metadata_obj; |
185 | } |
186 | |
187 | /* Implementation of "on_end_diagnostic" vfunc for JSON output. |
188 | Generate a JSON object for DIAGNOSTIC, and store for output |
189 | within current diagnostic group. */ |
190 | |
191 | void |
192 | json_output_format::on_end_diagnostic (diagnostic_info *diagnostic, |
193 | diagnostic_t orig_diag_kind) |
194 | { |
195 | json::object *diag_obj = new json::object (); |
196 | |
197 | /* Get "kind" of diagnostic. */ |
198 | { |
199 | static const char *const diagnostic_kind_text[] = { |
200 | #define DEFINE_DIAGNOSTIC_KIND(K, T, C) (T), |
201 | #include "diagnostic.def" |
202 | #undef DEFINE_DIAGNOSTIC_KIND |
203 | "must-not-happen" |
204 | }; |
205 | /* Lose the trailing ": ". */ |
206 | const char *kind_text = diagnostic_kind_text[diagnostic->kind]; |
207 | size_t len = strlen (s: kind_text); |
208 | gcc_assert (len > 2); |
209 | gcc_assert (kind_text[len - 2] == ':'); |
210 | gcc_assert (kind_text[len - 1] == ' '); |
211 | char *rstrip = xstrdup (kind_text); |
212 | rstrip[len - 2] = '\0'; |
213 | diag_obj->set (key: "kind" , v: new json::string (rstrip)); |
214 | free (ptr: rstrip); |
215 | } |
216 | |
217 | // FIXME: encoding of the message (json::string requires UTF-8) |
218 | diag_obj->set (key: "message" , |
219 | v: new json::string (pp_formatted_text (m_context.printer))); |
220 | pp_clear_output_area (m_context.printer); |
221 | |
222 | char *option_text; |
223 | option_text = m_context.m_option_name (&m_context, diagnostic->option_index, |
224 | orig_diag_kind, diagnostic->kind); |
225 | if (option_text) |
226 | { |
227 | diag_obj->set (key: "option" , v: new json::string (option_text)); |
228 | free (ptr: option_text); |
229 | } |
230 | |
231 | if (m_context.m_get_option_url) |
232 | { |
233 | char *option_url = m_context.m_get_option_url (&m_context, |
234 | diagnostic->option_index); |
235 | if (option_url) |
236 | { |
237 | diag_obj->set (key: "option_url" , v: new json::string (option_url)); |
238 | free (ptr: option_url); |
239 | } |
240 | } |
241 | |
242 | /* If we've already emitted a diagnostic within this auto_diagnostic_group, |
243 | then add diag_obj to its "children" array. */ |
244 | if (m_cur_group) |
245 | { |
246 | gcc_assert (m_cur_children_array); |
247 | m_cur_children_array->append (v: diag_obj); |
248 | } |
249 | else |
250 | { |
251 | /* Otherwise, make diag_obj be the top-level object within the group; |
252 | add a "children" array and record the column origin. */ |
253 | m_toplevel_array->append (v: diag_obj); |
254 | m_cur_group = diag_obj; |
255 | m_cur_children_array = new json::array (); |
256 | diag_obj->set (key: "children" , v: m_cur_children_array); |
257 | diag_obj->set (key: "column-origin" , |
258 | v: new json::integer_number (m_context.m_column_origin)); |
259 | } |
260 | |
261 | const rich_location *richloc = diagnostic->richloc; |
262 | |
263 | json::array *loc_array = new json::array (); |
264 | diag_obj->set (key: "locations" , v: loc_array); |
265 | |
266 | for (unsigned int i = 0; i < richloc->get_num_locations (); i++) |
267 | { |
268 | const location_range *loc_range = richloc->get_range (idx: i); |
269 | json::object *loc_obj |
270 | = json_from_location_range (context: &m_context, loc_range, range_idx: i); |
271 | if (loc_obj) |
272 | loc_array->append (v: loc_obj); |
273 | } |
274 | |
275 | if (richloc->get_num_fixit_hints ()) |
276 | { |
277 | json::array *fixit_array = new json::array (); |
278 | diag_obj->set (key: "fixits" , v: fixit_array); |
279 | for (unsigned int i = 0; i < richloc->get_num_fixit_hints (); i++) |
280 | { |
281 | const fixit_hint *hint = richloc->get_fixit_hint (idx: i); |
282 | json::object *fixit_obj = json_from_fixit_hint (context: &m_context, hint); |
283 | fixit_array->append (v: fixit_obj); |
284 | } |
285 | } |
286 | |
287 | /* TODO: tree-ish things: |
288 | TODO: functions |
289 | TODO: inlining information |
290 | TODO: macro expansion information. */ |
291 | |
292 | if (diagnostic->metadata) |
293 | { |
294 | json::object *metadata_obj = json_from_metadata (metadata: diagnostic->metadata); |
295 | diag_obj->set (key: "metadata" , v: metadata_obj); |
296 | } |
297 | |
298 | const diagnostic_path *path = richloc->get_path (); |
299 | if (path && m_context.m_make_json_for_path) |
300 | { |
301 | json::value *path_value |
302 | = m_context.m_make_json_for_path (&m_context, path); |
303 | diag_obj->set (key: "path" , v: path_value); |
304 | } |
305 | |
306 | diag_obj->set (key: "escape-source" , |
307 | v: new json::literal (richloc->escape_on_output_p ())); |
308 | } |
309 | |
310 | class json_stderr_output_format : public json_output_format |
311 | { |
312 | public: |
313 | json_stderr_output_format (diagnostic_context &context) |
314 | : json_output_format (context) |
315 | { |
316 | } |
317 | ~json_stderr_output_format () |
318 | { |
319 | flush_to_file (stderr); |
320 | } |
321 | }; |
322 | |
323 | class json_file_output_format : public json_output_format |
324 | { |
325 | public: |
326 | json_file_output_format (diagnostic_context &context, |
327 | const char *base_file_name) |
328 | : json_output_format (context), |
329 | m_base_file_name (xstrdup (base_file_name)) |
330 | { |
331 | } |
332 | |
333 | ~json_file_output_format () |
334 | { |
335 | char *filename = concat (m_base_file_name, ".gcc.json" , NULL); |
336 | free (ptr: m_base_file_name); |
337 | m_base_file_name = nullptr; |
338 | FILE *outf = fopen (filename: filename, modes: "w" ); |
339 | if (!outf) |
340 | { |
341 | const char *errstr = xstrerror (errno); |
342 | fnotice (stderr, "error: unable to open '%s' for writing: %s\n" , |
343 | filename, errstr); |
344 | free (ptr: filename); |
345 | return; |
346 | } |
347 | flush_to_file (outf); |
348 | fclose (stream: outf); |
349 | free (ptr: filename); |
350 | } |
351 | |
352 | private: |
353 | char *m_base_file_name; |
354 | }; |
355 | |
356 | /* Populate CONTEXT in preparation for JSON output (either to stderr, or |
357 | to a file). */ |
358 | |
359 | static void |
360 | diagnostic_output_format_init_json (diagnostic_context *context) |
361 | { |
362 | /* Override callbacks. */ |
363 | context->m_print_path = nullptr; /* handled in json_end_diagnostic. */ |
364 | |
365 | /* The metadata is handled in JSON format, rather than as text. */ |
366 | context->set_show_cwe (false); |
367 | context->set_show_rules (false); |
368 | |
369 | /* The option is handled in JSON format, rather than as text. */ |
370 | context->set_show_option_requested (false); |
371 | |
372 | /* Don't colorize the text. */ |
373 | pp_show_color (context->printer) = false; |
374 | } |
375 | |
376 | /* Populate CONTEXT in preparation for JSON output to stderr. */ |
377 | |
378 | void |
379 | diagnostic_output_format_init_json_stderr (diagnostic_context *context) |
380 | { |
381 | diagnostic_output_format_init_json (context); |
382 | context->set_output_format (new json_stderr_output_format (*context)); |
383 | } |
384 | |
385 | /* Populate CONTEXT in preparation for JSON output to a file named |
386 | BASE_FILE_NAME.gcc.json. */ |
387 | |
388 | void |
389 | diagnostic_output_format_init_json_file (diagnostic_context *context, |
390 | const char *base_file_name) |
391 | { |
392 | diagnostic_output_format_init_json (context); |
393 | context->set_output_format (new json_file_output_format (*context, |
394 | base_file_name)); |
395 | } |
396 | |
397 | #if CHECKING_P |
398 | |
399 | namespace selftest { |
400 | |
401 | /* We shouldn't call json_from_expanded_location on UNKNOWN_LOCATION, |
402 | but verify that we handle this gracefully. */ |
403 | |
404 | static void |
405 | test_unknown_location () |
406 | { |
407 | test_diagnostic_context dc; |
408 | delete json_from_expanded_location (context: &dc, UNKNOWN_LOCATION); |
409 | } |
410 | |
411 | /* Verify that we gracefully handle attempts to serialize bad |
412 | compound locations. */ |
413 | |
414 | static void |
415 | test_bad_endpoints () |
416 | { |
417 | location_t bad_endpoints |
418 | = make_location (BUILTINS_LOCATION, |
419 | UNKNOWN_LOCATION, UNKNOWN_LOCATION); |
420 | |
421 | location_range loc_range; |
422 | loc_range.m_loc = bad_endpoints; |
423 | loc_range.m_range_display_kind = SHOW_RANGE_WITH_CARET; |
424 | loc_range.m_label = NULL; |
425 | |
426 | test_diagnostic_context dc; |
427 | json::object *obj = json_from_location_range (context: &dc, loc_range: &loc_range, range_idx: 0); |
428 | /* We should have a "caret" value, but no "start" or "finish" values. */ |
429 | ASSERT_TRUE (obj != NULL); |
430 | ASSERT_TRUE (obj->get ("caret" ) != NULL); |
431 | ASSERT_TRUE (obj->get ("start" ) == NULL); |
432 | ASSERT_TRUE (obj->get ("finish" ) == NULL); |
433 | delete obj; |
434 | } |
435 | |
436 | /* Run all of the selftests within this file. */ |
437 | |
438 | void |
439 | diagnostic_format_json_cc_tests () |
440 | { |
441 | test_unknown_location (); |
442 | test_bad_endpoints (); |
443 | } |
444 | |
445 | } // namespace selftest |
446 | |
447 | #endif /* #if CHECKING_P */ |
448 | |