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