From 28aa3fa18828fe6f5937e717cbb6daaaba171913 Mon Sep 17 00:00:00 2001
From: David Malcolm <dmalcolm@redhat.com>
Date: Wed, 2 Aug 2017 14:54:36 -0400
Subject: [PATCH 04/22] Add firehose.h/cc

"Firehose" is a serialization format for results from code
analysis tools:

  http://firehose.readthedocs.io/en/latest/index.html

(along with a Python module for working with the format).

This patch implements a set of C++ classes modeling the format,
with support for populating them from a JSON dump, so that we
can lossly serialize diagnostics and other static analysis results.

gcc/ChangeLog:
	* Makefile.in (OBJS): Add firehose.o.
	* firehose.cc: New file.
	* firehose.h: New file.
	* selftest-run-tests.c (selftest::run_tests): Call
	selftest::firehose_cc_tests.
	* selftest.h (selftest::firehose_cc_tests): New decl.

gcc/testsuite/ChangeLog:
	* selftests/checker-output/test-clang-analyzer.json: New file.
	* selftests/checker-output/test-cppcheck.json: New file.
	* selftests/checker-output/test-failure.json: New file.
---
 gcc/Makefile.in                                    |   1 +
 gcc/firehose.cc                                    | 709 +++++++++++++++++++++
 gcc/firehose.h                                     | 199 ++++++
 gcc/selftest-run-tests.c                           |   1 +
 gcc/selftest.h                                     |   1 +
 .../checker-output/test-clang-analyzer.json        | 122 ++++
 .../selftests/checker-output/test-cppcheck.json    |  50 ++
 .../selftests/checker-output/test-failure.json     |  38 ++
 8 files changed, 1121 insertions(+)
 create mode 100644 gcc/firehose.cc
 create mode 100644 gcc/firehose.h
 create mode 100644 gcc/testsuite/selftests/checker-output/test-clang-analyzer.json
 create mode 100644 gcc/testsuite/selftests/checker-output/test-cppcheck.json
 create mode 100644 gcc/testsuite/selftests/checker-output/test-failure.json

diff --git a/gcc/Makefile.in b/gcc/Makefile.in
index 4f7fd0c..488f699 100644
--- a/gcc/Makefile.in
+++ b/gcc/Makefile.in
@@ -1280,6 +1280,7 @@ OBJS = \
 	expr.o \
 	fibonacci_heap.o \
 	final.o \
+	firehose.o \
 	fixed-value.o \
 	fold-const.o \
 	fold-const-call.o \
diff --git a/gcc/firehose.cc b/gcc/firehose.cc
new file mode 100644
index 0000000..b2aa167
--- /dev/null
+++ b/gcc/firehose.cc
@@ -0,0 +1,709 @@
+/* Serialization format for checker results.
+   Copyright (C) 2017 Free Software Foundation, Inc.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "firehose.h"
+#include "selftest.h"
+#include "selftest-input.h"
+
+namespace firehose {
+
+/* Attempt to parse JV as a json object containing "line" and "column"
+   attributes (a serialization of a firehose.model.Point python object).
+
+   If successful, write a location_t to OUT_VALUE, using GIVENPATH as the
+   filename, and return true.
+   Otherwise, write an error message to OUT_ERR (which must be freed by
+   the caller) and return false. */
+
+static bool
+get_location_from_point (const char *givenpath, const json::value *jv,
+			 location_t &out_value, char *&out_err)
+{
+  int line;
+  if (!jv->get_int_by_key ("line", line, out_err))
+    return false;
+
+  int column;
+  if (!jv->get_int_by_key ("column", column, out_err))
+    return false;
+
+  out_value
+   = linemap_position_for_file_line_and_column (line_table,
+						givenpath, line, column);
+  return true;
+}
+
+/* As above, but expect JV to be a json object containing a "start"
+   and "end" (a serialization of a firehose.model.Range python object).  */
+
+static bool
+get_location_from_range (const char *givenpath, const json::value *jv,
+			 location_t &out_value, char *&out_err)
+{
+  const json::value *jv_start;
+  if (!jv->get_value_by_key ("start", jv_start, out_err))
+    return false;
+
+  location_t start;
+  if (!get_location_from_point (givenpath, jv_start,
+				start, out_err))
+    return false;
+
+  const json::value *jv_end;
+  if (!jv->get_value_by_key ("end", jv_end, out_err))
+    return false;
+  location_t end;
+  if (!get_location_from_point (givenpath, jv_end,
+				end, out_err))
+    return false;
+
+  out_value = make_location (start, start, end);
+  return true;
+}
+
+/* Attempt to extract an attribute "location" from JV, where the value
+   ought to be a serialization of a firehose.model.Location python object.
+
+   If successful, write a location_t to OUT_VALUE and return true.
+   Otherwise, write an error message to OUT_ERR (which must be freed by
+   the caller) and return false. */
+
+static bool
+get_location (const json::value *jv, location_t &out_value, char *&out_err)
+{
+  const json::value *location;
+  if (!jv->get_value_by_key ("location", location, out_err))
+    return false;
+
+  const json::value *file;
+  if (!location->get_value_by_key ("file", file, out_err))
+    return false;
+  const char *givenpath;
+  if (!file->get_string_by_key ("givenpath", givenpath, out_err))
+    return false;
+
+  const json::value *point = location->as_object ()->get_if_nonnull ("point");
+  if (point)
+    {
+      if (!get_location_from_point (givenpath, point, out_value, out_err))
+	return false;
+    }
+  else
+    {
+      const json::value *range
+	= location->as_object ()->get_if_nonnull ("range_");
+
+      if (range)
+	{
+	  if (!get_location_from_range (givenpath, range, out_value,
+					out_err))
+	    return false;
+	}
+    }
+
+  // ignore "function" for now
+  return true;
+}
+
+/* firehose::state's ctor.  */
+
+state::state () : m_location (UNKNOWN_LOCATION), m_notes (NULL)
+{
+}
+
+/* firehose::state's dtor.  */
+
+state::~state ()
+{
+  free (m_notes);
+}
+
+/* Attempt to allocate a new firehose::state based on JV, which ought to be a
+   serialization of a firehose.model.State python object.
+
+   Return the new state if successful.
+   Otherwise return NULL and write an error message to OUT_ERR
+   (which must be freed by the caller).  */
+
+state *
+state::from_json (const json::value *jv, char *&out_err)
+{
+  state *s = new state ();
+
+  /* Extract the state's location to m_location.  */
+  if (!get_location (jv, s->m_location, out_err))
+    {
+      delete s;
+      return NULL;
+    }
+
+  /* Get any notes.  */
+  json::value *notes = jv->as_object ()->get_if_nonnull ("notes");
+  if (notes)
+    {
+      const char *text;
+      if (!notes->get_string_by_key ("text", text, out_err))
+	{
+	  delete s;
+	  return NULL;
+	}
+      s->m_notes = xstrdup (text);
+    }
+
+  return s;
+}
+
+/* firehose::trace's dtor.  */
+
+trace::~trace ()
+{
+  int i;
+  state *state;
+  FOR_EACH_VEC_ELT (m_states, i, state)
+    delete state;
+}
+
+/* Attempt to allocate a new firehose::trace based on JV, which ought to be a
+   serialization of a firehose.model.State python object.
+
+   Return the new state if successful.
+   Otherwise return NULL and write an error message to OUT_ERR
+   (which must be freed by the caller).  */
+
+trace *
+trace::from_json (const json::value *jv, char *&out_err)
+{
+  const json::array *states;
+  if (!jv->get_array_by_key ("states", states, out_err))
+    return NULL;
+
+  trace *t = new trace ();
+  for (unsigned idx = 0; idx < states->get_length (); idx++)
+    {
+      const json::value *item = states->get (idx);
+      if (0)
+	{
+	  fprintf (stderr, "got state %i: ", idx);
+	  item->dump (stderr);
+	  fprintf (stderr, "\n");
+	}
+      firehose::state *state = state::from_json (item, out_err);
+      if (!state)
+	{
+	  delete t;
+	  return NULL;
+	}
+      t->m_states.safe_push (state);
+    }
+
+  return t;
+}
+
+/* Filter out the states to just those with notes.  */
+
+void
+trace::filter ()
+{
+  unsigned idx = 0;
+  while (idx < m_states.length ())
+    {
+      if (m_states[idx]->m_notes == NULL)
+	{
+	  delete m_states[idx];
+	  m_states.ordered_remove (idx);
+	}
+      else
+	idx++;
+    }
+}
+
+/* Determine if THIS trace is merely a single state that duplicates
+   the information within ISSUE.  */
+
+bool
+trace::is_redundant_p (const issue& issue) const
+{
+  if (m_states.length () > 1)
+    return false;
+  if (m_states.length () < 1)
+    return true;
+
+  state *s0 = m_states[0];
+
+  if (s0->m_location != issue.m_location)
+    return false;
+  if (s0->m_notes)
+    if (0 != strcmp (s0->m_notes, issue.m_message))
+      return false;
+
+  /* Single state, with same location, and same message as ISSUE.  */
+  return true;
+}
+
+/* firehose::result's ctor.  */
+
+result::result ()
+: m_message (NULL), m_location (UNKNOWN_LOCATION)
+{
+}
+
+/* firehose::result's dtor.  */
+
+result::~result ()
+{
+  free (m_message);
+}
+
+/* Attempt to allocate a new firehose::result based on JV, which ought to be a
+   serialization of a firehose.model.Result python object.
+
+   Return the new state if successful.
+   Otherwise return NULL and write an error message to OUT_ERR
+   (which must be freed by the caller).  */
+
+result *
+result::from_json (const json::value *jv, char *&out_err)
+{
+  const char *type;
+  if (!jv->get_string_by_key ("type", type, out_err))
+    return NULL;
+  result *result = NULL;
+  if (0 == strcmp (type, "Issue"))
+    {
+      result = issue::from_json (jv, out_err);
+    }
+  if (0 == strcmp (type, "Info"))
+    {
+      result = info::from_json (jv, out_err);
+    }
+  if (0 == strcmp (type, "Failure"))
+    {
+      result = failure::from_json (jv, out_err);
+    }
+  if (!result)
+    {
+      out_err = xstrdup ("unrecognized type of result");
+      delete result;
+      return NULL;
+    }
+
+  /* Extract the results's message's text to m_message.  */
+  const json::value *message;
+  if (!jv->get_value_by_key ("message", message, out_err))
+    {
+      delete result;
+      return NULL;
+    }
+  const char *message_text;
+  if (!message->get_string_by_key ("text", message_text, out_err))
+    {
+      delete result;
+      return NULL;
+    }
+  result->m_message = xstrdup (message_text);
+
+  /* Extract the result's location to m_location.  */
+  if (!get_location (jv, result->m_location, out_err))
+    {
+      delete result;
+      return NULL;
+    }
+
+  return result;
+}
+
+/* firehose::issue's ctor.  */
+
+issue::issue () : result (), m_testid (NULL), m_trace (NULL)
+{
+}
+
+/* firehose::issue's dtor.  */
+
+issue::~issue ()
+{
+  free (m_testid);
+  delete m_trace;
+}
+
+/* Attempt to allocate a new firehose::issue based on JV, which ought to be a
+   serialization of a firehose.model.Issue python object.
+
+   Return the new state if successful.
+   Otherwise return NULL and write an error message to OUT_ERR
+   (which must be freed by the caller).  */
+
+issue *
+issue::from_json (const json::value *jv, char *&out_err)
+{
+  issue *r = new issue ();
+
+  /* FIXME: get any testid.  */
+  const char *testid_text = NULL;
+  if (!jv->get_optional_string_by_key ("testid", testid_text, out_err))
+    {
+      delete r;
+      return NULL;
+    }
+  if (testid_text)
+    r->m_testid = xstrdup (testid_text);
+
+  /* Get any trace as m_trace.  */
+  const json::value *trace = jv->as_object ()->get_if_nonnull ("trace");
+  if (trace)
+    {
+      r->m_trace = trace::from_json (trace, out_err);
+      if (!r->m_trace)
+	{
+	  delete r;
+	  return NULL;
+	}
+    }
+
+  return r;
+}
+
+/* firehose::info's ctor.  */
+
+info::info () : result (), m_infoid (NULL)
+{
+}
+
+/* firehose::info's dtor.  */
+
+info::~info ()
+{
+  free (m_infoid);
+}
+
+/* Attempt to allocate a new firehose::info based on JV, which ought to be a
+   serialization of a firehose.model.Info python object.
+
+   Return the new state if successful.
+   Otherwise return NULL and write an error message to OUT_ERR
+   (which must be freed by the caller).  */
+
+info *
+info::from_json (const json::value *jv, char *&out_err)
+{
+  info *r = new info ();
+
+  /* FIXME: get any infoid.  */
+  const char *infoid_text = NULL;
+  if (!jv->get_optional_string_by_key ("infoid", infoid_text, out_err))
+    {
+      delete r;
+      return NULL;
+    }
+  if (infoid_text)
+    r->m_infoid = xstrdup (infoid_text);
+
+  return r;
+}
+
+/* firehose::failure's ctor.  */
+
+failure::failure () : result (), m_failureid (NULL)
+{
+}
+
+/* firehose::failure's dtor.  */
+
+failure::~failure ()
+{
+  free (m_failureid);
+}
+
+/* Attempt to allocate a new firehose::failure based on JV, which ought to be a
+   serialization of a firehose.model.Failure python object.
+
+   Return the new state if successful.
+   Otherwise return NULL and write an error message to OUT_ERR
+   (which must be freed by the caller).  */
+
+failure *
+failure::from_json (const json::value *jv, char *&out_err)
+{
+  failure *r = new failure ();
+
+  /* FIXME: get any failureid.  */
+  const char *failureid_text = NULL;
+  if (!jv->get_optional_string_by_key ("failureid", failureid_text, out_err))
+    {
+      delete r;
+      return NULL;
+    }
+  if (failureid_text)
+    r->m_failureid = xstrdup (failureid_text);
+
+  return r;
+}
+
+/* firehose::generator's ctor.  */
+
+generator::generator ()
+: m_name (NULL), m_version (NULL)
+{
+}
+
+/* firehose::generator's dtor.  */
+
+generator::~generator ()
+{
+  free (m_name);
+  free (m_version);
+}
+
+/* Attempt to populate this firehose::generator based on JV, which ought to be a
+   serialization of a firehose.model.Generator python object.
+
+   Return true if successful.
+   Otherwise return false and write an error message to OUT_ERR
+   (which must be freed by the caller).  */
+
+bool
+generator::from_json (const json::value *jv, char *&out_err)
+{
+  const char *name;
+  if (!jv->get_string_by_key ("name", name, out_err))
+    return false;
+  m_name = xstrdup (name);
+
+  const char *version = NULL;
+  if (!jv->get_optional_string_by_key ("version", version, out_err))
+    return false;
+  if (version)
+    m_version = xstrdup (version);
+
+  return true;
+}
+
+/* Attempt to populate this firehose::metadata based on JV, which ought to be a
+   serialization of a firehose.model.Metadata python object.
+
+   Return true if successful.
+   Otherwise return false and write an error message to OUT_ERR
+   (which must be freed by the caller).  */
+
+bool
+metadata::from_json (const json::value *jv, char *&out_err)
+{
+  const json::value *jv_generator = NULL;
+  if (!jv->get_value_by_key ("generator", jv_generator, out_err))
+    return false;
+  if (!m_generator.from_json (jv_generator, out_err))
+    return false;
+
+  return true;
+}
+
+/* firehose::analysis's dtor.  */
+
+analysis::~analysis ()
+{
+  int i;
+  result *result;
+  FOR_EACH_VEC_ELT (m_results, i, result)
+    delete result;
+}
+
+/* Attempt to populate this firehose::analysis based on JV, which ought to be a
+   serialization of a firehose.model.Analysis python object.
+
+   Return true if successful.
+   Otherwise return false and write an error message to OUT_ERR
+   (which must be freed by the caller).  */
+
+bool
+analysis::from_json (const json::value *jv, char *&out_err)
+{
+  const json::value *jv_metadata = NULL;
+  if (!jv->get_value_by_key ("metadata", jv_metadata, out_err))
+    return false;
+  if (!m_metadata.from_json (jv_metadata, out_err))
+    return false;
+
+  const json::array *results;
+  if (!jv->get_array_by_key ("results", results, out_err))
+    return false;
+
+  for (unsigned i = 0; i < results->get_length (); i++)
+    {
+      json::value *item = results->get (i);
+      //error ("%s", item->to_str ());
+      result *r = result::from_json (item, out_err);
+      if (!r)
+	return false;
+      m_results.safe_push (r);
+  }
+
+  // FIXME: custom fields
+  // FIXME: selftests for all of this
+
+  return true;
+}
+
+} // namespace firehose
+
+
+#if CHECKING_P
+
+namespace selftest {
+
+/* Selftests.  */
+
+/* Given JSONFILE, a path relative to SRCDIR/gcc/testsuite/selftests,
+   load the json Firehose file there, populating OUT.
+   Fail if any errors occur.  */
+
+static void
+get_analysis (firehose::analysis &out, const char *jsonfile)
+{
+  char *filename = locate_file (jsonfile);
+  char *buffer = selftest::read_file (SELFTEST_LOCATION, filename);
+  ASSERT_TRUE (buffer != NULL);
+  free (filename);
+
+  char *err = NULL;
+  json::value *jv = json::parse_utf8_string (buffer, &err);
+  free (buffer);
+  ASSERT_TRUE (err == NULL);
+  ASSERT_TRUE (jv != NULL);
+
+  //jv->dump(stderr);
+  out.from_json (jv, err);
+  ASSERT_TRUE (err == NULL);
+  delete jv;
+}
+
+/* Parse a sample JSON output generated via the firehose parser for the
+   clang analyzer's plist output, and verify various properties
+   about it.  */
+
+static void
+test_parsing_clang_analyzer ()
+{
+  firehose::analysis analysis;
+  get_analysis (analysis, "checker-output/test-clang-analyzer.json");
+
+  ASSERT_STREQ ("clang-analyzer", analysis.m_metadata.m_generator.m_name);
+  ASSERT_EQ (NULL, analysis.m_metadata.m_generator.m_version);
+
+  ASSERT_EQ (1, analysis.m_results.length ());
+  firehose::result *r = analysis.m_results[0];
+  ASSERT_EQ (r->get_kind (), firehose::result::FIREHOSE_ISSUE);
+
+  firehose::issue *issue = (firehose::issue *)r;
+  ASSERT_STREQ ("Address of stack memory associated with"
+		" local variable 'tmp' returned to caller",
+		issue->m_message);
+  ASSERT_EQ (NULL, issue->m_testid);
+
+  ASSERT_LOCEQ ("../../src/bogus.c", 5, 3, issue->m_location);
+
+  ASSERT_TRUE (issue->m_trace != NULL);
+  ASSERT_EQ (3, issue->m_trace->m_states.length ());
+  firehose::state *state0 = issue->m_trace->m_states[0];
+  ASSERT_LOCEQ ("../../src/bogus.c", 3, 3, state0->m_location);
+  ASSERT_EQ (NULL, state0->m_notes);
+
+  firehose::state *state1 = issue->m_trace->m_states[1];
+  ASSERT_LOCEQ ("../../src/bogus.c", 5, 3, state1->m_location);
+  ASSERT_EQ (NULL, state1->m_notes);
+
+  firehose::state *state2 = issue->m_trace->m_states[2];
+  ASSERT_LOCEQ ("../../src/bogus.c", 5, 3, state2->m_location);
+  ASSERT_STREQ ("Address of stack memory associated with"
+		" local variable 'tmp' returned to caller",
+		state2->m_notes);
+  ASSERT_FALSE (issue->m_trace->is_redundant_p (*issue));
+
+  /* Verify filtering out non-textual states from the trace.  */
+  issue->m_trace->filter ();
+  ASSERT_EQ (1, issue->m_trace->m_states.length ());
+
+  /* Verify that the filtered trace is redundant.  */
+  ASSERT_TRUE (issue->m_trace->is_redundant_p (*issue));
+}
+
+/* Parse a sample JSON output generated via the firehose parser for
+   cppchecks's output, and verify various properties about it.  */
+
+static void
+test_parsing_cppcheck ()
+{
+  firehose::analysis analysis;
+  get_analysis (analysis, "checker-output/test-cppcheck.json");
+
+  ASSERT_STREQ ("cppcheck", analysis.m_metadata.m_generator.m_name);
+  ASSERT_STREQ ("1.63", analysis.m_metadata.m_generator.m_version);
+
+  ASSERT_EQ (1, analysis.m_results.length ());
+  firehose::result *r = analysis.m_results[0];
+  ASSERT_EQ (r->get_kind (), firehose::result::FIREHOSE_ISSUE);
+
+  firehose::issue *issue = (firehose::issue *)r;
+  ASSERT_STREQ ("Memory leak: ptr_1", issue->m_message);
+  ASSERT_STREQ ("memleak", issue->m_testid);
+
+  ASSERT_LOCEQ ("../../src/test-sources/conditional-leak.c", 11, 0,
+		issue->m_location);
+
+  ASSERT_TRUE (issue->m_trace == NULL);
+}
+
+/* Parse a JSON file describing a failure to run a checker, and verify
+   various properties about it.  */
+
+static void
+test_parsing_failure ()
+{
+  firehose::analysis analysis;
+  get_analysis (analysis, "checker-output/test-failure.json");
+
+  ASSERT_STREQ ("always-fails", analysis.m_metadata.m_generator.m_name);
+
+  ASSERT_EQ (1, analysis.m_results.length ());
+  firehose::result *r = analysis.m_results[0];
+  ASSERT_EQ (r->get_kind (), firehose::result::FIREHOSE_FAILURE);
+
+  firehose::failure *failure = (firehose::failure *)r;
+  ASSERT_STREQ ("Exception running always-fails: [Errno 2]"
+		" No such file or directory:"
+		" '/this/executable/does/not/exist'", failure->m_message);
+  ASSERT_STREQ ("exception", failure->m_failureid);
+}
+
+/* Run all of the selftests within this file.  */
+
+void
+firehose_cc_tests ()
+{
+  test_parsing_clang_analyzer ();
+  test_parsing_cppcheck ();
+  //test_parsing_info ();
+  test_parsing_failure ();
+}
+
+} // namespace selftest
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/firehose.h b/gcc/firehose.h
new file mode 100644
index 0000000..6c67b45
--- /dev/null
+++ b/gcc/firehose.h
@@ -0,0 +1,199 @@
+/* Serialization format for checker results.
+   Copyright (C) 2017 Free Software Foundation, Inc.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef GCC_FIREHOSE_H
+#define GCC_FIREHOSE_H
+
+/* "Firehose" is a serialization format for results from code
+   analysis tools:
+
+     http://firehose.readthedocs.io/en/latest/index.html
+
+   (along with a Python module for working with the format).
+
+   This file implements a set of C++ classes modeling the format,
+   with support for populating them from a JSON dump, so that we
+   can lossly serialize diagnostics and other static analysis results.  */
+
+#include "json.h"
+
+namespace firehose {
+
+/* Forward decls.  */
+
+struct issue;
+
+/* A state within a firehose::trace.  */
+
+struct state
+{
+  state ();
+  ~state ();
+
+  static state *from_json (const json::value *jv, char *&out_err);
+
+  location_t m_location;
+  char *m_notes;
+};
+
+/* An optional list of events within an issue that describe the circumstances
+   leading up to a problem.  */
+
+struct trace
+{
+  ~trace ();
+
+  static trace *from_json (const json::value *jv, char *&out_err);
+
+  void filter ();
+
+  /* If we're just left with a single state that duplicates what we
+     already printed for the issue, don't bother printing it.  */
+  bool is_redundant_p (const issue& issue) const;
+
+  auto_vec <state *> m_states;
+};
+
+/* firehose::result is a base class.
+
+   There are three subclasses:
+
+   - a firehose::issue represents a report from an analyzer about a possible
+     problem with the software under test.
+   - a firehose::info represents additional kinds of information generated by
+     an analyzer that isn't a problem per-se e.g. code metrics, licensing info,
+     etc.
+   - a firehose::failure represents a report about a failure of the analyzer
+     itself (e.g. if the analyzer crashed).  */
+
+struct result
+{
+  enum kind
+  {
+    FIREHOSE_ISSUE,
+    FIREHOSE_INFO,
+    FIREHOSE_FAILURE
+  };
+
+  result ();
+  virtual ~result ();
+
+  static result *from_json (const json::value *jv, char *&out_err);
+
+  virtual enum kind get_kind () const = 0;
+
+  char *m_message;
+  location_t m_location;
+};
+
+/* An issue represents a report from an analyzer about a possible problem
+   with the software under test.  */
+
+struct issue : public result
+{
+  issue ();
+  ~issue ();
+
+  static issue *from_json (const json::value *jv, char *&out_err);
+  enum kind get_kind () const FINAL OVERRIDE { return FIREHOSE_ISSUE; }
+
+  char *m_testid;
+  trace *m_trace;
+};
+
+/* An info represents additional kinds of information generated by an analyzer
+   that isn't a problem per-se e.g. code metrics, licensing info,
+   cross-referencing information, etc.  */
+
+struct info : public result
+{
+  info ();
+  ~info ();
+
+  static info *from_json (const json::value *jv, char *&out_err);
+  enum kind get_kind () const FINAL OVERRIDE { return FIREHOSE_INFO; }
+
+  char *m_infoid;
+};
+
+/* A failure represents a report about a failure of the analyzer itself
+   (e.g. if the analyzer crashed).
+
+   If any of these are present then we don't have full coverage.
+
+   For some analyzers this is an all-or-nothing affair: we either get
+   issues reported, or a failure happens (e.g. a segfault of the
+   analysis tool).
+
+   Other analyzers may be more fine-grained: able to report some
+   issues, but choke on some subset of the code under analysis.
+   For example cpychecker runs once per function, and any unhandled
+   Python exceptions only affect one function.  */
+
+struct failure : public result
+{
+  failure ();
+  ~failure ();
+
+  static failure *from_json (const json::value *jv, char *&out_err);
+  enum kind get_kind () const FINAL OVERRIDE { return FIREHOSE_FAILURE; }
+
+  char *m_failureid;
+};
+
+/* A class describing a static analyzer, for use within firehose::metadata.  */
+
+struct generator
+{
+  generator ();
+  ~generator ();
+
+  bool from_json (const json::value *jv, char *&out_err);
+
+  char *m_name;
+  char *m_version;
+};
+
+/* The firehose::metadata class contains metadata about a static analyzer
+   invocation.  */
+
+struct metadata
+{
+  bool from_json (const json::value *jv, char *&out_err);
+
+  generator m_generator;
+};
+
+/* The firehose::analysis class represents one invocation of a code analysis
+   tool.  */
+
+struct analysis
+{
+  ~analysis ();
+
+  bool from_json (const json::value *jv, char *&out_err);
+
+  metadata m_metadata;
+  auto_vec<result *> m_results;
+  //custom_fields *m_custom_fields;
+};
+
+} // namespace firehose
+
+#endif  /* GCC_FIREHOSE_H  */
diff --git a/gcc/selftest-run-tests.c b/gcc/selftest-run-tests.c
index 025e574..8afcd43 100644
--- a/gcc/selftest-run-tests.c
+++ b/gcc/selftest-run-tests.c
@@ -74,6 +74,7 @@ selftest::run_tests ()
   gimple_c_tests ();
   rtl_tests_c_tests ();
   read_rtl_function_c_tests ();
+  firehose_cc_tests ();
 
   /* Higher-level tests, or for components that other selftests don't
      rely on.  */
diff --git a/gcc/selftest.h b/gcc/selftest.h
index 4e8891c..e86ce38 100644
--- a/gcc/selftest.h
+++ b/gcc/selftest.h
@@ -177,6 +177,7 @@ extern void edit_context_c_tests ();
 extern void et_forest_c_tests ();
 extern void fold_const_c_tests ();
 extern void fibonacci_heap_c_tests ();
+extern void firehose_cc_tests ();
 extern void function_tests_c_tests ();
 extern void gimple_c_tests ();
 extern void ggc_tests_c_tests ();
diff --git a/gcc/testsuite/selftests/checker-output/test-clang-analyzer.json b/gcc/testsuite/selftests/checker-output/test-clang-analyzer.json
new file mode 100644
index 0000000..eda9abc
--- /dev/null
+++ b/gcc/testsuite/selftests/checker-output/test-clang-analyzer.json
@@ -0,0 +1,122 @@
+{
+ "customfields": {
+  "scan-build-invocation": "scan-build -v -plist --use-analyzer /usr/bin/clang -o /tmp/tmp8ytuRj gcc -B. -c ../../src/bogus.c", 
+  "returncode": 0, 
+  "stdout": "scan-build: Using '/usr/bin/clang' for static analysis\nscan-build: Emitting reports for this run to '/tmp/tmp8ytuRj/2017-05-24-001755-39710-1'.\nscan-build: Analysis run complete.\nscan-build: Analysis results (plist files) deposited in '/tmp/tmp8ytuRj/2017-05-24-001755-39710-1'\n", 
+  "stderr": "../../src/bogus.c: In function \u2018test\u2019:\n../../src/bogus.c:5:10: warning: function returns address of local variable [-Wreturn-local-addr]\n   return tmp;\n          ^~~\n../../src/bogus.c:5:3: warning: Address of stack memory associated with local variable 'tmp' returned to caller\n  return tmp;\n  ^~~~~~~~~~\n1 warning generated.\n", 
+  "plistpath": "/tmp/tmp8ytuRj/2017-05-24-001755-39710-1/report-DEoPmt.plist"
+ }, 
+ "results": [
+  {
+   "severity": null, 
+   "trace": {
+    "states": [
+     {
+      "notes": null, 
+      "location": {
+       "function": {
+        "name": ""
+       }, 
+       "range_": {
+        "start": {
+         "column": 3, 
+         "line": 3
+        }, 
+        "end": {
+         "column": 6, 
+         "line": 3
+        }
+       }, 
+       "file": {
+        "abspath": null, 
+        "givenpath": "../../src/bogus.c", 
+        "hash_": null
+       }, 
+       "point": null
+      }
+     }, 
+     {
+      "notes": null, 
+      "location": {
+       "function": {
+        "name": ""
+       }, 
+       "range_": {
+        "start": {
+         "column": 3, 
+         "line": 5
+        }, 
+        "end": {
+         "column": 8, 
+         "line": 5
+        }
+       }, 
+       "file": {
+        "abspath": null, 
+        "givenpath": "../../src/bogus.c", 
+        "hash_": null
+       }, 
+       "point": null
+      }
+     }, 
+     {
+      "notes": {
+       "text": "Address of stack memory associated with local variable 'tmp' returned to caller"
+      }, 
+      "location": {
+       "function": {
+        "name": ""
+       }, 
+       "range_": null, 
+       "file": {
+        "abspath": null, 
+        "givenpath": "../../src/bogus.c", 
+        "hash_": null
+       }, 
+       "point": {
+        "column": 3, 
+        "line": 5
+       }
+      }
+     }
+    ]
+   }, 
+   "type": "Issue", 
+   "notes": null, 
+   "testid": null, 
+   "message": {
+    "text": "Address of stack memory associated with local variable 'tmp' returned to caller"
+   }, 
+   "cwe": null, 
+   "customfields": null, 
+   "location": {
+    "function": null, 
+    "range_": null, 
+    "file": {
+     "abspath": null, 
+     "givenpath": "../../src/bogus.c", 
+     "hash_": null
+    }, 
+    "point": {
+     "column": 3, 
+     "line": 5
+    }
+   }
+  }
+ ], 
+ "metadata": {
+  "stats": {
+   "wallclocktime": 0.22788214683532715
+  }, 
+  "sut": null, 
+  "file_": {
+   "abspath": "/home/david/coding-3/gcc-git-static-analysis/build/gcc/../../src/bogus.c", 
+   "givenpath": "../../src/bogus.c", 
+   "hash_": null
+  }, 
+  "generator": {
+   "version": null, 
+   "name": "clang-analyzer"
+  }
+ }
+}
\ No newline at end of file
diff --git a/gcc/testsuite/selftests/checker-output/test-cppcheck.json b/gcc/testsuite/selftests/checker-output/test-cppcheck.json
new file mode 100644
index 0000000..c9651ee
--- /dev/null
+++ b/gcc/testsuite/selftests/checker-output/test-cppcheck.json
@@ -0,0 +1,50 @@
+{
+ "customfields": {
+  "cppcheck-invocation": "cppcheck --xml --xml-version=2 ../../src/test-sources/conditional-leak.c", 
+  "returncode": 0, 
+  "stdout": "Checking ../../src/test-sources/conditional-leak.c...\n", 
+  "stderr": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<results version=\"2\">\n    <cppcheck version=\"1.63\"/>\n    <errors>\n        <error id=\"memleak\" severity=\"error\" msg=\"Memory leak: ptr_1\" verbose=\"Memory leak: ptr_1\">\n            <location file=\"../../src/test-sources/conditional-leak.c\" line=\"11\"/>\n        </error>\n    </errors>\n</results>\n"
+ }, 
+ "results": [
+  {
+   "severity": "error", 
+   "trace": null, 
+   "type": "Issue", 
+   "notes": null, 
+   "testid": "memleak", 
+   "message": {
+    "text": "Memory leak: ptr_1"
+   }, 
+   "cwe": null, 
+   "customfields": null, 
+   "location": {
+    "function": null, 
+    "range_": null, 
+    "file": {
+     "abspath": null, 
+     "givenpath": "../../src/test-sources/conditional-leak.c", 
+     "hash_": null
+    }, 
+    "point": {
+     "column": 0, 
+     "line": 11
+    }
+   }
+  }
+ ], 
+ "metadata": {
+  "stats": {
+   "wallclocktime": 0.006749868392944336
+  }, 
+  "sut": null, 
+  "file_": {
+   "abspath": "/home/david/coding-3/gcc-git-static-analysis/build/gcc/../../src/test-sources/conditional-leak.c", 
+   "givenpath": "../../src/test-sources/conditional-leak.c", 
+   "hash_": null
+  }, 
+  "generator": {
+   "version": "1.63", 
+   "name": "cppcheck"
+  }
+ }
+}
\ No newline at end of file
diff --git a/gcc/testsuite/selftests/checker-output/test-failure.json b/gcc/testsuite/selftests/checker-output/test-failure.json
new file mode 100644
index 0000000..fd07cab
--- /dev/null
+++ b/gcc/testsuite/selftests/checker-output/test-failure.json
@@ -0,0 +1,38 @@
+{
+ "customfields": {
+  "traceback": "Traceback (most recent call last):\n  File \"/home/david/coding-3/gcc-git-static-analysis/src/checkers/checker.py\", line 142, in checked_invoke\n    analysis = self.raw_invoke(gccinv, sourcefile)\n  File \"./checkers/always_fails.py\", line 40, in raw_invoke\n    return self._run_subprocess(sourcefile, args)\n  File \"/home/david/coding-3/gcc-git-static-analysis/src/checkers/checker.py\", line 213, in _run_subprocess\n    stdout=PIPE, stderr=PIPE, env=env)\n  File \"/usr/lib64/python2.7/site-packages/subprocess32.py\", line 812, in __init__\n    restore_signals, start_new_session)\n  File \"/usr/lib64/python2.7/site-packages/subprocess32.py\", line 1557, in _execute_child\n    raise child_exception_type(errno_num, err_msg)\nOSError: [Errno 2] No such file or directory: '/this/executable/does/not/exist'\n"
+ }, 
+ "results": [
+  {
+   "type": "Failure", 
+   "message": {
+    "text": "Exception running always-fails: [Errno 2] No such file or directory: '/this/executable/does/not/exist'"
+   }, 
+   "failureid": "exception", 
+   "location": {
+    "function": null, 
+    "range_": null, 
+    "file": {
+     "abspath": null, 
+     "givenpath": "checkers/test-sources/harmless.c", 
+     "hash_": null
+    }, 
+    "point": null
+   }, 
+   "customfields": null
+  }
+ ], 
+ "metadata": {
+  "stats": null, 
+  "sut": null, 
+  "file_": {
+   "abspath": "/home/david/coding-3/gcc-git-static-analysis/src/checkers/test-sources/harmless.c", 
+   "givenpath": "checkers/test-sources/harmless.c", 
+   "hash_": null
+  }, 
+  "generator": {
+   "version": null, 
+   "name": "always-fails"
+  }
+ }
+}
\ No newline at end of file
-- 
1.8.5.3

