From 48dfe6d595f745fa96e1b382430582074dae7e3f Mon Sep 17 00:00:00 2001
From: David Malcolm <dmalcolm@redhat.com>
Date: Tue, 23 May 2017 17:34:27 -0400
Subject: [PATCH 3/3] FIXME: initial WIP of invoking 3rd-party analysis tools

---
 bogus.c                          |   6 +
 fakegcc.py                       | 551 +++++++++++++++++++++++++++++++++++++++
 fakegcc_clang_analyzer.py        | 155 +++++++++++
 fakegcc_cppcheck.py              | 146 +++++++++++
 gcc/Makefile.in                  |   2 +
 gcc/common.opt                   |   4 +
 gcc/diagnostic.c                 |  46 ++++
 gcc/diagnostic.h                 |   5 +
 gcc/firehose.c                   | 523 +++++++++++++++++++++++++++++++++++++
 gcc/firehose.h                   | 159 +++++++++++
 gcc/input.c                      |  39 +--
 gcc/run-analyzers.c              | 252 ++++++++++++++++++
 gcc/selftest-input.h             |  54 ++++
 gcc/selftest-run-tests.c         |   1 +
 gcc/selftest.h                   |   1 +
 gcc/toplev.c                     |   6 +
 invoke-cppcheck.py               | 133 ++++++++++
 test-clang-analyzer.json         | 122 +++++++++
 test-cppcheck.json               |  50 ++++
 test-sources/conditional-leak.c  |  17 ++
 test-sources/cpychecker-demo.c   | 110 ++++++++
 test-sources/divide-by-zero.c    |   4 +
 test-sources/harmless.c          |   9 +
 test-sources/multiple-1.c        |   6 +
 test-sources/multiple-2.c        |   9 +
 test-sources/out-of-bounds.c     |   6 +
 test-sources/read-through-null.c |   4 +
 27 files changed, 2402 insertions(+), 18 deletions(-)
 create mode 100644 bogus.c
 create mode 100755 fakegcc.py
 create mode 100755 fakegcc_clang_analyzer.py
 create mode 100755 fakegcc_cppcheck.py
 create mode 100644 gcc/firehose.c
 create mode 100644 gcc/firehose.h
 create mode 100644 gcc/run-analyzers.c
 create mode 100644 gcc/selftest-input.h
 create mode 100644 invoke-cppcheck.py
 create mode 100644 test-clang-analyzer.json
 create mode 100644 test-cppcheck.json
 create mode 100644 test-sources/conditional-leak.c
 create mode 100644 test-sources/cpychecker-demo.c
 create mode 100644 test-sources/divide-by-zero.c
 create mode 100644 test-sources/harmless.c
 create mode 100644 test-sources/multiple-1.c
 create mode 100644 test-sources/multiple-2.c
 create mode 100644 test-sources/out-of-bounds.c
 create mode 100644 test-sources/read-through-null.c

diff --git a/bogus.c b/bogus.c
new file mode 100644
index 0000000..66c8893
--- /dev/null
+++ b/bogus.c
@@ -0,0 +1,6 @@
+void *test (void)
+{
+  char tmp[16];
+
+  return tmp;
+}
diff --git a/fakegcc.py b/fakegcc.py
new file mode 100755
index 0000000..2b6d65f
--- /dev/null
+++ b/fakegcc.py
@@ -0,0 +1,551 @@
+#!/usr/bin/env python
+#   Copyright 2012, 2013, 2015, 2017 David Malcolm <dmalcolm@redhat.com>
+#   Copyright 2012, 2013, 2015, 2017 Red Hat, Inc.
+#
+#   This 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 of the License, or
+#   (at your option) any later version.
+#
+#   This program 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 this program.  If not, see
+#   <http://www.gnu.org/licenses/>.
+
+import json
+import logging
+import os
+import re
+import StringIO
+import sys
+import tempfile
+import time
+import traceback
+import unittest
+
+# http://pypi.python.org/pypi/subprocess32
+# so that we can use timeouts
+from subprocess32 import Popen, PIPE, STDOUT, TimeoutExpired
+
+from firehose.model import Analysis, Generator, Metadata, Failure, \
+    Location, File, Message, Issue, Trace
+
+from gccinvocation import GccInvocation
+
+def in_chroot():
+    return os.path.exists('/builddir')
+
+def make_file(givenpath):
+    from firehose.model import File
+    return File(givenpath=givenpath,
+                abspath=None,
+                hash_=None)
+
+def make_stats(timer):
+    from firehose.model import Stats
+    return Stats(wallclocktime=timer.get_elapsed_time())
+
+class Timer:
+    """
+    Simple measurement of wallclock time taken
+    """
+    def __init__(self):
+        self.starttime = time.time()
+
+    def get_elapsed_time(self):
+        """Get elapsed time in seconds as a float"""
+        curtime = time.time()
+        return curtime - self.starttime
+
+    def elapsed_time_as_str(self):
+        """Get elapsed time as a string (with units)"""
+        elapsed = self.get_elapsed_time()
+        result = '%0.3f seconds' % elapsed
+        if elapsed > 120:
+            result += ' (%i minutes)' % int(elapsed / 60)
+        return result
+
+class Context:
+    def __init__(self, enable_logging=False, capture_exceptions=True):
+        self.enable_logging = enable_logging
+        self.capture_exceptions = capture_exceptions
+        if self.enable_logging:
+            if in_chroot():
+                logging.basicConfig(format='%(asctime)s %(message)s',
+                                    datefmt='%H:%M:%S',
+                                    level=logging.INFO,
+                                    filename='/builddir/fakegcc.log')
+            else:
+                logging.basicConfig(format='%(asctime)s %(message)s',
+                                    #datefmt='%H:%M:%S',
+                                    level=logging.INFO,
+                                    stream=sys.stdout)
+            self.log('logging initialized')
+
+        self.stdout = sys.stdout
+        self.stderr = sys.stderr
+        self.returncode = None
+
+    def log(self, msg):
+        if self.enable_logging:
+            logging.info(msg)
+
+    def write_streams(self, toolname, out, err):
+        for line in out.splitlines():
+            self.log('stdout from %r: %s\n' % (toolname, line))
+        for line in err.splitlines():
+            self.log('stderr from %r: %s\n' % (toolname, line))
+
+class SubprocessResult:
+    """
+    A bundle of information relating to a subprocess invocation.
+    """
+    def __init__(self, sourcefile, argv, returncode, out, err, timer):
+        self.sourcefile = sourcefile
+        self.argv = argv
+        self.returncode = returncode
+        self.out = out
+        self.err = err
+        self.timer = timer
+
+    def set_custom_fields(self, analysis):
+        analysis.set_custom_field('returncode', self.returncode)
+        analysis.set_custom_field('stdout', self.out.decode('utf-8'))
+        analysis.set_custom_field('stderr', self.err.decode('utf-8'))
+
+class Tool:
+    def __init__(self, name, ctxt):
+        self.name = name
+        self.timeout = 60
+        self.ctxt = ctxt
+
+    def log(self, msg):
+        self.ctxt.log(msg)
+
+    def raw_invoke(self, gccinv, sourcefile):
+        """
+        Run the tool, with a timeout, returning an Analysis instance.
+        May well raise an exception if something major went wrong.
+        """
+        raise NotImplementedError
+
+    def checked_invoke(self, gccinv, sourcefile):
+        """
+        Call "invoke", handling exceptions.  Return an Analysis instance.
+        """
+        try:
+            self.log('about to invoke: %s with %r' % (self.name, gccinv))
+            analysis = self.raw_invoke(gccinv, sourcefile)
+        except TimeoutExpired:
+            analysis = self._make_failed_analysis(
+                sourcefile, t, msgtext='Timeout running %s' % self.name,
+                failureid='timeout')
+            analysis.set_custom_field('timeout', TIMEOUT)
+        except Exception, exc:
+            # Capture the exception as a Failure instance.
+            # Alternatively when debugging such failures, it can
+            # be easier to re-raise the exception:
+            if not self.ctxt.capture_exceptions:
+                raise
+            analysis = \
+                self._make_failed_analysis(
+                    sourcefile, None,
+                    msgtext=('Exception running %s: %s'
+                             % (self.name, exc)),
+                    failureid='exception')
+            tb_str = traceback.format_exc()
+            analysis.set_custom_field('traceback', tb_str)
+        if sourcefile:
+            analysis.metadata.file_.givenpath = sourcefile
+            analysis.metadata.file_.abspath = os.path.join(os.getcwd(),
+                                                           sourcefile)
+        return analysis
+
+    def handle_output(self, result):
+        """
+        Given a SubprocessResult, return an Analysis instance.
+        """
+        raise NotImplementedError
+
+    def _make_failed_analysis(self, sourcefile, t, msgtext, failureid):
+        """
+        Something went wrong; build a failure report.
+        """
+        generator = Generator(name=self.name,
+                              version=None)
+        if t:
+            stats = make_stats(t)
+        else:
+            stats = None
+
+        metadata = Metadata(generator=generator,
+                            sut=None,
+                            file_ = make_file(sourcefile),
+                            stats=stats)
+        file_ = File(givenpath=sourcefile,
+                     abspath=None,
+                     hash_=None)
+        location = Location(file=file_,
+                            function=None,
+                            point=None,
+                            range_=None)
+        message = Message(msgtext)
+        results = [Failure(failureid=failureid,
+                           location=location,
+                           message=message,
+                           customfields=None)]
+        analysis = Analysis(metadata, results)
+        return analysis
+
+    def _run_subprocess(self, sourcefile, argv, env=None):
+        """
+        Support for running the bulk of the side effect in a subprocess,
+        with timeout support.
+        """
+        self.log('%s: _run_subprocess(%r, %r)' % (self.name, sourcefile, argv))
+        if 0:
+            self.log('env: %s' % env)
+        p = Popen(argv,
+                  stdout=PIPE, stderr=PIPE, env=env)
+        try:
+            t = Timer()
+            out, err = p.communicate(timeout=self.timeout)
+            self.ctxt.write_streams(argv[0], out, err)
+            result = SubprocessResult(sourcefile, argv, p.returncode, out, err, t)
+            analysis = self.handle_output(result)
+            return analysis
+        except TimeoutExpired:
+            analysis = self._make_failed_analysis(sourcefile, t,
+                                                  msgtext='Timeout running %s' % self.name,
+                                                  failureid='timeout')
+            analysis.set_custom_field('timeout', self.timeout)
+            analysis.set_custom_field('command-line', ' '.join(argv))
+            return analysis
+
+############################################################################
+# Tool subclasses
+############################################################################
+
+class InvokeRealGcc(Tool):
+    """
+    Tool subclass that invokes a real gcc compiler binary
+    """
+    def __init__(self, executable, ctxt, extra_args=None, extra_env=None):
+        Tool.__init__(self, 'gcc', ctxt)
+        self.executable = executable
+        self.extra_args = extra_args
+        self.extra_env = extra_env
+        # We are only ever invoked with individual input files.
+        # Override any -o, to ensure we don't interfere with the output
+        # from the real compiler:
+        self.output_file = tempfile.NamedTemporaryFile()
+
+    def raw_invoke(self, gccinv, sourcefile):
+        args = [self.executable] + gccinv.argv[1:]
+        if self.extra_args:
+            args += self.extra_args
+        args += ['-o', self.output_file.name]
+
+        # The result parser requires the C locale
+        env = os.environ.copy()
+        env['LANG'] = 'C'
+        if self.extra_env:
+            for key, value in self.extra_env.iteritems():
+                env[key] = value
+        return self._run_subprocess(sourcefile, args, env=env)
+
+    def handle_output(self, result):
+        from firehose.parsers.gcc import parse_file
+
+        f = StringIO.StringIO(result.err)
+        analysis = parse_file(f, stats=make_stats(result.timer))
+        if result.sourcefile:
+            analysis.metadata.file_ = File(givenpath=result.sourcefile,
+                                           abspath=None)
+        self.set_custom_fields(result, analysis)
+
+        self.result = result
+
+        return analysis
+
+    def set_custom_fields(self, result, analysis):
+        analysis.set_custom_field('gcc-invocation',
+                                  ' '.join(result.argv))
+        result.set_custom_fields(analysis)
+
+
+class InvokeCustomGcc(InvokeRealGcc):
+    """
+    Tool subclass that invokes a custom build of gcc 6 I have.
+    This is a patched version of gcc that writes out a file
+    to DUMPBASE.custom-dump.txt, containing a series of
+    lines of the form
+       KEY: VALUE
+    """
+    def __init__(self, executable, ctxt, extra_args=None, extra_env=None):
+        InvokeRealGcc.__init__(self, executable, ctxt, extra_args, extra_env)
+        self.name = 'custom-gcc'
+
+    def handle_output(self, result):
+        analysis = InvokeRealGcc.handle_output(self, result)
+        analysis.metadata.generator.name = 'custom-gcc'
+        dumpbase = os.path.join(os.path.dirname(self.output_file.name),
+                                os.path.basename(result.sourcefile))
+        dumpfile_path = dumpbase + '.custom-dump.txt'
+        if os.path.exists(dumpfile_path):
+            self.log('found custom dumpfile: %s' % dumpfile_path)
+            with open(dumpfile_path) as f:
+                for line in f:
+                    self.log(line)
+                    # Expect lines of the form "KEY: VALUE"
+                    m = re.match('^(.+): (.+)$', line)
+                    self.log(str(m.groups()))
+                    key, value = m.groups()
+                    analysis.set_custom_field(key, value)
+        else:
+            self.log('could not find custom dumpfile: %s' % dumpfile_path)
+        return analysis
+
+class InvokeCpychecker(Tool):
+    """
+    Tool subclass that invoke the gcc-python-plugin's "cpychecker" code.
+
+    This currently requires the "firehose" branch of gcc-python-plugin
+    """
+
+    def __init__(self, ctxt):
+        Tool.__init__(self, 'cpychecker', ctxt)
+
+    def raw_invoke(self, gccinv, sourcefile):
+        # Invoke the plugin, but for robustness, do it in an entirely
+        # separate gcc invocation
+        argv = gccinv.argv[:]
+
+        self.outputxmlpath = '%s.firehose.xml' % sourcefile
+
+        # The plugin needs to be able to find its own modules, or we get:
+        #   ImportError: No module named libcpychecker
+        # We can either set PYTHONPATH in the environment,
+        # or provide a full path to the plugin in the invocation line:
+        # in the latter case, gcc-python.c:setup_sys sets up sys.path
+        # inside the plugin to include the directory containing the plugin
+        # if we provide a full path to the plugin here.
+
+        # That said, the plugin's Makefile installs the plugin
+        # as "python.so" to $(GCCPLUGINS_DIR)
+        # and the support modules to $(GCCPLUGINS_DIR)/$(PLUGIN_DIR)
+        # So let's do it via PYTHONPATH
+
+        # FIXME: hacked in path:
+        plugin_path = '/home/david/coding/gcc-python/gcc-python/cpychecker-firehose-output/'
+        plugin_gcc_c_api_path = '/home/david/coding/gcc-python/gcc-python/cpychecker-firehose-output/gcc-c-api'
+        plugin_sys_path = '/home/david/coding/gcc-python/gcc-python/cpychecker-firehose-output/'
+        plugin_full_name = os.path.join(plugin_path, 'python.so')
+        env = os.environ.copy()
+        env['PYTHONPATH'] = plugin_sys_path
+        env['LD_LIBRARY_PATH'] = plugin_gcc_c_api_path
+
+        # We would use the regular keyword argument syntax:
+        #   outputxmlpath='foo'
+        # but unfortunately gcc's option parser seems to not be able to
+        # cope with '='  within an option's value.  So we do it using
+        # dictionary syntax instead:
+        pycmd = ('from libcpychecker import main, Options; '
+                 'main(Options(**{"outputxmlpath":"%s", '
+                 '"verify_refcounting": True, '
+                 '"maxtrans": 1024, '
+                 '}))' % self.outputxmlpath)
+        # Note that some RPMs also rename the plugin from
+        # "python.so" to "python2.so", which would require further work.
+        argv += ['-fplugin=%s' % plugin_full_name,
+                 '-fplugin-arg-python-command=%s' % pycmd]
+
+        args = [get_real_executable(argv)] + argv[1:]
+
+        return self._run_subprocess(sourcefile, args, env)
+
+    def handle_output(self, result):
+        if os.path.exists(self.outputxmlpath):
+            with open(self.outputxmlpath) as f:
+                analysis = Analysis.from_xml(f)
+                analysis.metadata.file_ = make_file(result.sourcefile)
+                analysis.metadata.stats = make_stats(result.timer)
+        else:
+            analysis = \
+                self._make_failed_analysis(
+                    result.sourcefile, result.timer,
+                    msgtext=('Unable to locate XML output from %s'
+                             % self.name),
+                    failureid='no-output-found')
+        analysis.set_custom_field('cpychecker-invocation',
+                                  ' '.join(result.argv))
+        result.set_custom_fields(analysis)
+        return analysis
+
+############################################################################
+# Test suite
+############################################################################
+
+class ToolTests(unittest.TestCase):
+    def make_ctxt(self, capture_exceptions=True):
+        return Context(enable_logging=0, capture_exceptions=capture_exceptions)
+
+    def make_tool(self):
+        """Hook for self.make_compiler()"""
+        raise NotImplementedError
+
+    def verify_basic_metadata(self, analysis, sourcefile):
+        """Hook for self.invoke()"""
+        raise NotImplementedError
+
+    def invoke(self, sourcefile, extraargs = None):
+        """Invoke a tool and sanity-check the result"""
+        tool = self.make_tool()
+        argv = ['gcc', '-c', sourcefile]
+        if extraargs:
+            argv += extraargs
+        gccinv = GccInvocation(argv)
+        analysis = tool.checked_invoke(gccinv, sourcefile)
+
+        if 0:
+            print(analysis)
+
+        # Call a subclass hook to check basic metadata:
+        self.verify_basic_metadata(analysis, sourcefile)
+
+        # Verify that we can serialize to XML:
+        xml_bytes = analysis.to_xml_bytes()
+        self.assert_(xml_bytes.startswith(b'<analysis>'))
+
+        # Verify it can roundtrip through JSON:
+        js_bytes = json.dumps(analysis.to_json(), indent=1)
+        other = Analysis.from_json(json.loads(js_bytes))
+        #self.assertEqual(analysis, other)
+
+        return analysis
+
+    def assert_metadata(self, analysis,
+                        expected_generator_name, expected_given_path):
+        self.assertEqual(analysis.metadata.generator.name,
+                         expected_generator_name)
+        self.assertEqual(analysis.metadata.file_.givenpath, expected_given_path)
+        self.assertIn(expected_given_path, analysis.metadata.file_.abspath)
+
+    def assert_has_custom_field(self, analysis, name):
+        self.assert_(analysis.customfields)
+        self.assert_(name in analysis.customfields)
+
+class BuggyToolTests(ToolTests):
+    def make_tool(self):
+        """
+        Override base class impl, so that we can enable
+        exception-capture (and provide a custom tool)
+        """
+        class BuggyTool(Tool):
+            def raw_invoke(self, gccinv, sourcefile):
+                raise ValueError('test of raising an exception')
+
+        ctxt = self.make_ctxt(capture_exceptions=True)
+        tool = BuggyTool('buggy', ctxt)
+        return tool
+
+    def verify_basic_metadata(self, analysis, sourcefile):
+         self.assert_metadata(analysis, 'buggy', sourcefile)
+
+    def test_exception_handling(self):
+        analysis = self.invoke('test-sources/harmless.c')
+        #print(analysis)
+        self.assertEqual(len(analysis.results), 1)
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Failure)
+        self.assertEqual(r0.failureid, 'exception')
+        self.assertEqual(r0.message.text,
+                         ('Exception running buggy:'
+                          ' test of raising an exception'))
+        self.assert_(analysis.customfields['traceback'].startswith(
+            'Traceback (most recent call last):\n'))
+
+class RealGccTests(ToolTests):
+    def make_tool(self):
+        ctxt = self.make_ctxt()
+        return InvokeRealGcc('gcc', ctxt)
+
+    def verify_basic_metadata(self, analysis, sourcefile):
+        # Verify basic metadata:
+        self.assert_metadata(analysis, 'gcc', sourcefile)
+        self.assert_has_custom_field(analysis, 'gcc-invocation')
+        self.assert_has_custom_field(analysis, 'stdout')
+        self.assert_has_custom_field(analysis, 'stderr')
+
+    def test_file_not_found(self):
+        analysis = self.invoke('does-not-exist.c')
+        #print(analysis)
+        # Currently this gives no output:
+        self.assertEqual(len(analysis.results), 0)
+
+    def test_timeout(self):
+        sourcefile = 'test-sources/harmless.c'
+        tool = self.make_tool()
+        tool.timeout = 0
+        gccinv = GccInvocation(['gcc', sourcefile])
+        analysis = tool.checked_invoke(gccinv, sourcefile)
+        self.assert_metadata(analysis, tool.name, sourcefile)
+        self.assertEqual(len(analysis.results), 1)
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Failure)
+        self.assertEqual(r0.failureid, 'timeout')
+        self.assert_has_custom_field(analysis, 'timeout')
+        self.assert_has_custom_field(analysis, 'command-line')
+
+    def test_harmless_file(self):
+        analysis = self.invoke('test-sources/harmless.c')
+        #print(analysis)
+        self.assertEqual(len(analysis.results), 0)
+
+    def test_divide_by_zero(self):
+        analysis = self.invoke('test-sources/divide-by-zero.c', ['-Wall'])
+        self.assertEqual(len(analysis.results), 1)
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Issue)
+        self.assertEqual(r0.testid, 'div-by-zero')
+        self.assertEqual(r0.location.file.givenpath,
+                         'test-sources/divide-by-zero.c')
+        self.assertEqual(r0.location.function.name, 'divide_by_zero')
+        self.assertEqual(r0.location.point.line, 3)
+        self.assertEqual(r0.message.text, 'division by zero')
+        self.assertEqual(r0.severity, None)
+
+############################################################################
+# Entrypoint
+############################################################################
+
+def tool_main(argv, tool_class):
+    """
+    Entrypoint for use by the various per-tool scripts
+    """
+    # If we're invoked with "unittest" as the first param,
+    # run the unit test suite:
+    if len(argv) >= 2:
+        if argv[1] == 'unittest':
+            sys.argv = [argv[0]] + argv[2:]
+            return unittest.main()
+
+    ctxt = Context()
+    tool = tool_class(ctxt)
+
+    gccinv = GccInvocation(argv)
+    ctxt.log('  gccinv.sources: %r' % gccinv.sources)
+    assert len(gccinv.sources) == 1
+    sourcefile = gccinv.sources[0]
+    ctxt.log('    sourcefile: %r' % sourcefile)
+    analysis = tool.checked_invoke(gccinv, sourcefile)
+    json.dump(analysis.to_json(), sys.stdout, indent=1)
+
+# Run the unit tests
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())
diff --git a/fakegcc_clang_analyzer.py b/fakegcc_clang_analyzer.py
new file mode 100755
index 0000000..29f7b87
--- /dev/null
+++ b/fakegcc_clang_analyzer.py
@@ -0,0 +1,155 @@
+#!/usr/bin/env python
+#   Copyright 2012, 2013, 2015, 2017 David Malcolm <dmalcolm@redhat.com>
+#   Copyright 2012, 2013, 2015, 2017 Red Hat, Inc.
+#
+#   This 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 of the License, or
+#   (at your option) any later version.
+#
+#   This program 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 this program.  If not, see
+#   <http://www.gnu.org/licenses/>.
+
+import glob
+import os
+import sys
+import tempfile
+import traceback
+import unittest
+
+from gccinvocation import GccInvocation
+
+from fakegcc import Tool, Context, ToolTests, make_file, make_stats, \
+    tool_main
+
+from firehose.model import Analysis, Generator, Metadata, Failure, \
+    Location, File, Message, Issue, Trace
+from firehose.parsers.clanganalyzer import parse_plist
+
+class InvokeClangAnalyzer(Tool):
+    """
+    Tool subclass that invokes the clang analyzer
+    """
+    def __init__(self, ctxt):
+        Tool.__init__(self, 'clang-analyzer', ctxt)
+
+    def raw_invoke(self, gccinv, sourcefile):
+        self.resultdir = tempfile.mkdtemp()
+        args = ['scan-build', '-v', '-plist',
+                '--use-analyzer', '/usr/bin/clang', # rhbz 923834
+                '-o', self.resultdir,
+                'gcc'] + gccinv.argv[1:]
+        return self._run_subprocess(sourcefile, args)
+
+    def handle_output(self, result):
+        if result.returncode:
+            analysis = self._make_failed_analysis(result.sourcefile, result.timer,
+                                                  msgtext='Bad exit code running %s' % self.name,
+                                                  failureid='bad-exit-code')
+            self.set_custom_fields(result, analysis)
+            return analysis
+
+        # Given e.g. resultdir='/tmp/tmpQW2l2B', the plist files
+        # are an extra level deep e.g.:
+        #  '/tmp/tmpQW2l2B/2013-01-22-1/report-MlwJri.plist'
+        self.log(self.resultdir)
+        for plistpath in glob.glob(os.path.join(self.resultdir,
+                                                '*/*.plist')):
+            analysis = parse_plist(plistpath,
+                                   file_=make_file(result.sourcefile),
+                                   stats=make_stats(result.timer))
+            self.set_custom_fields(result, analysis)
+            analysis.set_custom_field('plistpath', plistpath)
+            return analysis # could there be more than one?
+
+        # Not found?
+        analysis = self._make_failed_analysis(
+            result.sourcefile, result.timer,
+            msgtext='Unable to locate plist file',
+            failureid='plist-not-found')
+        self.set_custom_fields(result, analysis)
+        return analysis
+
+    def set_custom_fields(self, result, analysis):
+        analysis.set_custom_field('scan-build-invocation',
+                                  ' '.join(result.argv))
+        result.set_custom_fields(analysis)
+
+class ClangAnalyzerTests(ToolTests):
+    def make_tool(self):
+        ctxt = self.make_ctxt()
+        return InvokeClangAnalyzer(ctxt)
+
+    def verify_basic_metadata(self, analysis, sourcefile):
+        # Verify basic metadata:
+        self.assert_metadata(analysis, 'clang-analyzer', sourcefile)
+        self.assert_has_custom_field(analysis, 'scan-build-invocation')
+        self.assert_has_custom_field(analysis, 'stdout')
+        self.assert_has_custom_field(analysis, 'stderr')
+
+    def test_file_not_found(self):
+        analysis = self.invoke('does-not-exist.c')
+        #print(analysis)
+        self.assertEqual(len(analysis.results), 1)
+        self.assertIsInstance(analysis.results[0], Failure)
+        self.assertEqual(analysis.results[0].failureid, 'bad-exit-code')
+
+    def test_timeout(self):
+        sourcefile = 'test-sources/harmless.c'
+        tool = self.make_tool()
+        tool.timeout = 0
+        gccinv = GccInvocation(['gcc', sourcefile])
+        analysis = tool.checked_invoke(gccinv, sourcefile)
+        self.assert_metadata(analysis, 'clang-analyzer', sourcefile)
+        self.assertEqual(len(analysis.results), 1)
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Failure)
+        self.assertEqual(r0.failureid, 'timeout')
+        self.assert_has_custom_field(analysis, 'timeout')
+        self.assert_has_custom_field(analysis, 'command-line')
+
+    def test_harmless_file(self):
+        analysis = self.invoke('test-sources/harmless.c')
+        #print(analysis)
+        self.assertEqual(len(analysis.results), 0)
+
+    def test_read_through_null(self):
+        analysis = self.invoke('test-sources/read-through-null.c')
+        #print(analysis)
+        #print(analysis.to_json())
+        self.assertEqual(len(analysis.results), 1)
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Issue)
+        self.assertEqual(r0.testid, None)
+        self.assertEqual(r0.location.file.givenpath,
+                         'test-sources/read-through-null.c')
+        self.assertEqual(r0.location.point.line, 3)
+        self.assertEqual(r0.message.text,
+                         "Dereference of null pointer")
+        self.assertEqual(r0.severity, None)
+        self.assertIsInstance(r0.trace, Trace)
+
+    def test_out_of_bounds(self):
+        analysis = self.invoke('test-sources/out-of-bounds.c')
+        #print(analysis)
+        self.assertEqual(len(analysis.results), 1)
+
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Issue)
+        self.assertEqual(r0.testid, None)
+        self.assertEqual(r0.location.file.givenpath,
+                         'test-sources/out-of-bounds.c')
+        self.assertEqual(r0.location.point.line, 5)
+        self.assertEqual(r0.message.text,
+                         "Undefined or garbage value returned to caller")
+        self.assertEqual(r0.severity, None)
+        self.assertIsInstance(r0.trace, Trace)
+
+if __name__ == '__main__':
+    sys.exit(tool_main(sys.argv, InvokeClangAnalyzer))
diff --git a/fakegcc_cppcheck.py b/fakegcc_cppcheck.py
new file mode 100755
index 0000000..74d86ed
--- /dev/null
+++ b/fakegcc_cppcheck.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python
+#   Copyright 2012, 2013, 2015, 2017 David Malcolm <dmalcolm@redhat.com>
+#   Copyright 2012, 2013, 2015, 2017 Red Hat, Inc.
+#
+#   This 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 of the License, or
+#   (at your option) any later version.
+#
+#   This program 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 this program.  If not, see
+#   <http://www.gnu.org/licenses/>.
+
+import sys
+import tempfile
+import unittest
+
+from gccinvocation import GccInvocation
+
+from fakegcc import Tool, Context, ToolTests, make_file, make_stats, \
+    tool_main
+
+from firehose.model import Analysis, Generator, Metadata, Failure, \
+    Location, File, Message, Issue, Trace
+from firehose.parsers.cppcheck import parse_file
+
+class InvokeCppcheck(Tool):
+    """
+    Tool subclass that invokes "cppcheck"
+    """
+    def __init__(self, ctxt):
+        Tool.__init__(self, 'cppcheck', ctxt)
+
+    def raw_invoke(self, gccinv, sourcefile):
+        args = ['cppcheck',
+                '--xml', '--xml-version=2',
+                sourcefile]
+        return self._run_subprocess(sourcefile, args)
+
+    def handle_output(self, result):
+        if result.returncode:
+            analysis = self._make_failed_analysis(result.sourcefile, result.timer,
+                                                  msgtext='Bad exit code running %s' % self.name,
+                                                  failureid='bad-exit-code')
+            self.set_custom_fields(result, analysis)
+            return analysis
+
+        # (there doesn't seem to be a way to have cppcheck directly
+        # save its XML output to a given location)
+
+        with tempfile.NamedTemporaryFile() as outfile:
+            outfile.write(result.err)
+            outfile.flush()
+
+            with open(outfile.name) as infile:
+                # Parse stderr into firehose XML format and save:
+                analysis = parse_file(infile,
+                                      file_=make_file(result.sourcefile),
+                                      stats=make_stats(result.timer))
+                self.set_custom_fields(result, analysis)
+                return analysis
+
+    def set_custom_fields(self, result, analysis):
+        analysis.set_custom_field('cppcheck-invocation',
+                                  ' '.join(result.argv))
+        result.set_custom_fields(analysis)
+
+class CppcheckTests(ToolTests):
+    def make_tool(self):
+        ctxt = self.make_ctxt()
+        return InvokeCppcheck(ctxt)
+
+    def verify_basic_metadata(self, analysis, sourcefile):
+        # Verify basic metadata:
+        self.assert_metadata(analysis, 'cppcheck', sourcefile)
+        self.assert_has_custom_field(analysis, 'cppcheck-invocation')
+        self.assert_has_custom_field(analysis, 'stdout')
+        self.assert_has_custom_field(analysis, 'stderr')
+
+    def test_file_not_found(self):
+        analysis = self.invoke('does-not-exist.c')
+        #print(analysis)
+        self.assertEqual(len(analysis.results), 1)
+        self.assertIsInstance(analysis.results[0], Failure)
+        self.assertEqual(analysis.results[0].failureid, 'bad-exit-code')
+
+    def test_timeout(self):
+        sourcefile = 'test-sources/harmless.c'
+        tool = self.make_tool()
+        tool.timeout = 0
+        gccinv = GccInvocation(['gcc', sourcefile])
+        analysis = tool.checked_invoke(gccinv, sourcefile)
+        self.assert_metadata(analysis, 'cppcheck', sourcefile)
+        self.assertEqual(len(analysis.results), 1)
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Failure)
+        self.assertEqual(r0.failureid, 'timeout')
+        self.assert_has_custom_field(analysis, 'timeout')
+        self.assert_has_custom_field(analysis, 'command-line')
+
+    def test_harmless_file(self):
+        analysis = self.invoke('test-sources/harmless.c')
+        #print(analysis)
+        self.assertEqual(len(analysis.results), 0)
+
+    def test_read_through_null(self):
+        analysis = self.invoke('test-sources/read-through-null.c')
+        self.assertEqual(len(analysis.results), 1)
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Issue)
+        self.assertEqual(r0.testid, 'nullPointer')
+        self.assertEqual(r0.location.file.givenpath,
+                         'test-sources/read-through-null.c')
+        self.assertEqual(r0.location.point.line, 3)
+        self.assertEqual(r0.message.text,
+                         "Null pointer dereference")
+        self.assertEqual(r0.severity, 'error')
+
+    def test_out_of_bounds(self):
+        analysis = self.invoke('test-sources/out-of-bounds.c')
+        #print(analysis)
+        self.assertEqual(len(analysis.results), 2)
+
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Issue)
+        self.assertEqual(r0.testid, 'arrayIndexOutOfBounds')
+        self.assertEqual(r0.location.file.givenpath,
+                         'test-sources/out-of-bounds.c')
+        self.assertEqual(r0.location.point.line, 5)
+        self.assertEqual(
+            r0.message.text,
+            "Array 'arr[10]' accessed at index 15, which is out of bounds.")
+        self.assertEqual(r0.severity, 'error')
+
+        r1 = analysis.results[1]
+        self.assertIsInstance(r1, Issue)
+        self.assertEqual(r1.testid, 'uninitvar')
+        # etc
+
+if __name__ == '__main__':
+    sys.exit(tool_main(sys.argv, InvokeCppcheck))
diff --git a/gcc/Makefile.in b/gcc/Makefile.in
index 3625c20..61fb948 100644
--- a/gcc/Makefile.in
+++ b/gcc/Makefile.in
@@ -1275,6 +1275,7 @@ OBJS = \
 	expr.o \
 	fibonacci_heap.o \
 	final.o \
+	firehose.o \
 	fixed-value.o \
 	fold-const.o \
 	fold-const-call.o \
@@ -1435,6 +1436,7 @@ OBJS = \
 	rtlhash.o \
 	rtlanal.o \
 	rtlhooks.o \
+	run-analyzers.o \
 	run-rtl-passes.o \
 	sbitmap.o \
 	sched-deps.o \
diff --git a/gcc/common.opt b/gcc/common.opt
index 4021622..e802698 100644
--- a/gcc/common.opt
+++ b/gcc/common.opt
@@ -2104,6 +2104,10 @@ frounding-math
 Common Report Var(flag_rounding_math) Optimization SetByCombined
 Disable optimizations that assume default FP rounding behavior.
 
+frun-analyzers=
+Common Joined Var(flag_run_analyzers)
+FIXME.
+
 fsched-interblock
 Common Report Var(flag_schedule_interblock) Init(1) Optimization
 Enable scheduling across basic blocks.
diff --git a/gcc/diagnostic.c b/gcc/diagnostic.c
index dbaf8d6..79419d8 100644
--- a/gcc/diagnostic.c
+++ b/gcc/diagnostic.c
@@ -67,7 +67,17 @@ const char *progname;
 /* A diagnostic_context surrogate for stderr.  */
 static diagnostic_context global_diagnostic_context;
 diagnostic_context *global_dc = &global_diagnostic_context;
+
 
+
+/* FIXME.  */
+
+diagnostic_info::diagnostic_info ()
+: message (), richloc (NULL), x_data (NULL), kind (DK_UNSPECIFIED),
+  option_index (0), external_tool (NULL), external_test_id (NULL)
+{
+}
+
 /* Return a malloc'd string containing MSG formatted a la printf.  The
    caller is responsible for freeing the memory.  */
 char *
@@ -813,6 +823,8 @@ diagnostic_report_diagnostic (diagnostic_context *context,
   diagnostic_t orig_diag_kind = diagnostic->kind;
   const char *saved_format_spec;
 
+  gcc_assert (diagnostic->kind != DK_UNSPECIFIED);
+
   /* Give preference to being able to inhibit warnings, before they
      get reclassified to something else.  */
   if ((diagnostic->kind == DK_WARNING || diagnostic->kind == DK_PEDWARN)
@@ -937,6 +949,38 @@ diagnostic_report_diagnostic (diagnostic_context *context,
   saved_format_spec = diagnostic->message.format_spec;
   if (context->show_option_requested)
     {
+      /* FIXME: this will need rebasing and then rewriting, so a hack for now.  */
+      if (diagnostic->external_tool)
+	{
+	  if (diagnostic->external_test_id)
+	    {
+	      const char *cs
+		= colorize_start (pp_show_color (context->printer),
+				  diagnostic_kind_color[diagnostic->kind]);
+	      const char *ce = colorize_stop (pp_show_color (context->printer));
+	      diagnostic->message.format_spec
+		= ACONCAT ((diagnostic->message.format_spec,
+			    " ",
+			    "[", cs, diagnostic->external_tool, ce,
+			    ":", cs, diagnostic->external_test_id, ce, "]",
+			    NULL));
+	    }
+	  else
+	    {
+	      const char *cs
+		= colorize_start (pp_show_color (context->printer),
+				  diagnostic_kind_color[diagnostic->kind]);
+	      const char *ce = colorize_stop (pp_show_color (context->printer));
+	      diagnostic->message.format_spec
+		= ACONCAT ((diagnostic->message.format_spec,
+			    " ",
+			    "[", cs, diagnostic->external_tool, ce, "]",
+			    NULL));
+	    }
+	}
+      else
+	{
+	  /* NOT YET REINDENTED.  */
       char *option_text;
 
       option_text = context->option_name (context, diagnostic->option_index,
@@ -955,6 +999,8 @@ diagnostic_report_diagnostic (diagnostic_context *context,
 			NULL));
 	  free (option_text);
 	}
+	  /* NOT YET REINDENTED.  */
+	}
     }
   diagnostic->message.x_data = &diagnostic->x_data;
   diagnostic->x_data = NULL;
diff --git a/gcc/diagnostic.h b/gcc/diagnostic.h
index 91a9642..8f123bd 100644
--- a/gcc/diagnostic.h
+++ b/gcc/diagnostic.h
@@ -29,6 +29,8 @@ along with GCC; see the file COPYING3.  If not see
    list in diagnostic.def.  */
 struct diagnostic_info
 {
+  diagnostic_info ();
+
   /* Text to be formatted.  */
   text_info message;
 
@@ -41,6 +43,9 @@ struct diagnostic_info
   diagnostic_t kind;
   /* Which OPT_* directly controls this diagnostic.  */
   int option_index;
+
+  const char *external_tool;
+  const char *external_test_id;
 };
 
 /* Each time a diagnostic's classification is changed with a pragma,
diff --git a/gcc/firehose.c b/gcc/firehose.c
new file mode 100644
index 0000000..6ffe42c
--- /dev/null
+++ b/gcc/firehose.c
@@ -0,0 +1,523 @@
+/* FIXME.
+   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 {
+
+// FIXME: move this to line-map.c
+
+static location_t
+get_location (const char *file, int line, int column)
+{
+  for (unsigned int i = 0; i < LINEMAPS_ORDINARY_USED (line_table); i++)
+    {
+      line_map_ordinary *ord_map = LINEMAPS_ORDINARY_MAP_AT (line_table, i);
+      if (0 == strcmp (file, ord_map->to_file))
+	{
+	  location_t loc
+	    = linemap_position_for_line_and_column (line_table, ord_map,
+						    line, column);
+	  /* Check that it's a valid location within ord_map.  */
+	  if (i + 1 < LINEMAPS_ORDINARY_USED (line_table))
+	    {
+	      line_map_ordinary *next_ord_map
+		= LINEMAPS_ORDINARY_MAP_AT (line_table, i + 1);
+	      if (loc >= next_ord_map->start_location)
+		continue;
+	    }
+
+	  return loc;
+	}
+    }
+
+  // FIXME: create an ordmap when this is the case.
+  return UNKNOWN_LOCATION;
+
+  // FIXME: there could be multiple ord_maps for FILE.
+}
+
+/* FIXME.  */
+
+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 = get_location (givenpath, line, column);
+  return true;
+}
+
+/* FIXME.  */
+
+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;
+}
+
+/* FIXME.  */
+
+static bool
+get_location_by_key (const json::value *jv, const char *name,
+		     location_t &out_value, char *&out_err)
+{
+  const json::value *location;
+  if (!jv->get_value_by_key (name, 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;
+}
+
+/* FIXME.  */
+
+state::state () : m_location (UNKNOWN_LOCATION), m_notes (NULL)
+{
+}
+
+/* FIXME.  */
+
+state::~state ()
+{
+  free (m_notes);
+}
+
+/* FIXME.  */
+
+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_by_key (jv, "location", 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;
+}
+
+/* FIXME.  */
+
+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;
+}
+
+/* FIXME.  */
+
+result::result ()
+: m_message (NULL), m_location (UNKNOWN_LOCATION)
+{
+}
+
+/* FIXME.  */
+
+result::~result ()
+{
+  free (m_message);
+}
+
+/* FIXME.  */
+
+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;
+  if (0 == strcmp (type, "Issue"))
+    {
+      return issue::from_json (jv, out_err);
+    }
+  // TODO: info and failure
+  return NULL;
+}
+
+/* FIXME.  */
+
+issue::issue () : result (), m_testid (NULL), m_trace (NULL)
+{
+}
+
+/* FIXME.  */
+
+issue::~issue ()
+{
+  free (m_testid);
+  delete m_trace;
+}
+
+/* FIXME.  */
+
+issue *
+issue::from_json (const json::value *jv, char *&out_err)
+{
+#if 0
+  if (!jv->as_object ())
+    {
+      out_err = xstdrup ("issue is not an object");
+      return NULL;
+    }
+#endif
+
+  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);
+
+  if (out_err)
+    {
+      delete r;
+      return NULL;
+    }
+
+  /* Extract the issue's message's text to m_message.  */
+  const json::value *message;
+  if (!jv->get_value_by_key ("message", message, out_err))
+    {
+      delete r;
+      return NULL;
+    }
+  const char *message_text;
+  if (!message->get_string_by_key ("text", message_text, out_err))
+    {
+      delete r;
+      return NULL;
+    }
+  r->m_message = xstrdup (message_text);
+
+  /* Extract the issue's location to m_location.  */
+  if (!get_location_by_key (jv, "location", r->m_location, out_err))
+    {
+      delete r;
+      return NULL;
+    }
+
+  /* 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;
+}
+
+/* FIXME.  */
+
+generator::generator ()
+: m_name (NULL), m_version (NULL)
+{
+}
+
+/* FIXME.  */
+
+generator::~generator ()
+{
+  free (m_name);
+  free (m_version);
+}
+
+/* FIXME.  */
+
+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;
+}
+
+/* FIXME.  */
+
+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;
+}
+
+/* FIXME.  */
+
+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.  */
+
+/* FIXME.  */
+
+static void
+test_parsing_clang_analyzer ()
+{
+  const char *filename = "../../src/test-clang-analyzer.json";
+  // FIXME: filename ^^^
+  char *buffer = selftest::read_file (SELFTEST_LOCATION, filename);
+  ASSERT_TRUE (buffer != NULL);
+
+  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);
+  firehose::analysis analysis;
+  analysis.from_json (jv, err);
+  ASSERT_TRUE (err == NULL);
+  delete jv;
+
+  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);
+}
+
+/* FIXME.  */
+
+static void
+test_parsing_cppcheck ()
+{
+  const char *filename = "../../src/test-cppcheck.json"; // FIXME
+  char *buffer = selftest::read_file (SELFTEST_LOCATION, filename);
+  ASSERT_TRUE (buffer != NULL);
+
+  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);
+  firehose::analysis analysis;
+  analysis.from_json (jv, err);
+  ASSERT_TRUE (err == NULL);
+  delete jv;
+
+  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);
+}
+
+/* Run all of the selftests within this file.  */
+
+void
+firehose_c_tests ()
+{
+  test_parsing_clang_analyzer ();
+  test_parsing_cppcheck ();
+}
+
+} // namespace selftest
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/firehose.h b/gcc/firehose.h
new file mode 100644
index 0000000..77fee1f
--- /dev/null
+++ b/gcc/firehose.h
@@ -0,0 +1,159 @@
+/* FIXME.
+   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
+
+#include "json.h"
+
+namespace firehose {
+
+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
+{
+  static trace *from_json (const json::value *jv, char *&out_err);
+
+  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
+{
+  // TODO
+};
+
+/* 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
+{
+  // TODO
+};
+
+/* FIXME.  */
+
+struct generator
+{
+  generator ();
+  ~generator ();
+
+  bool from_json (const json::value *jv, char *&out_err);
+
+  char *m_name;
+  char *m_version;
+};
+
+/* FIXME.  */
+
+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
+{
+  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/input.c b/gcc/input.c
index 8071810..28db43e 100644
--- a/gcc/input.c
+++ b/gcc/input.c
@@ -23,6 +23,7 @@ along with GCC; see the file COPYING3.  If not see
 #include "intl.h"
 #include "diagnostic-core.h"
 #include "selftest.h"
+#include "selftest-input.h"
 #include "cpplib.h"
 
 #ifndef HAVE_ICONV
@@ -1579,21 +1580,23 @@ test_should_have_column_data_p ()
 }
 
 /* Verify the result of LOCATION_FILE/LOCATION_LINE/LOCATION_COLUMN
-   on LOC.  */
+   on LOC.  Use REPORT_LOC as the effective location when reporting
+   any issues.  */
 
-static void
-assert_loceq (const char *exp_filename, int exp_linenum, int exp_colnum,
+void
+assert_loceq (const location &report_loc,
+	      const char *exp_filename, int exp_linenum, int exp_colnum,
 	      location_t loc)
 {
-  ASSERT_STREQ (exp_filename, LOCATION_FILE (loc));
-  ASSERT_EQ (exp_linenum, LOCATION_LINE (loc));
+  ASSERT_STREQ_AT (report_loc, exp_filename, LOCATION_FILE (loc));
+  ASSERT_EQ_AT (report_loc, exp_linenum, LOCATION_LINE (loc));
   /* If location_t values are sufficiently high, then column numbers
      will be unavailable and LOCATION_COLUMN (loc) will be 0.
      When close to the threshold, column numbers *may* be present: if
      the final linemap before the threshold contains a line that straddles
      the threshold, locations in that line have column information.  */
   if (should_have_column_data_p (loc))
-    ASSERT_EQ (exp_colnum, LOCATION_COLUMN (loc));
+    ASSERT_EQ_AT (report_loc, exp_colnum, LOCATION_COLUMN (loc));
 }
 
 /* Various selftests involve constructing a line table and one or more
@@ -1727,23 +1730,23 @@ test_accessing_ordinary_linemaps (const line_table_case &case_)
   linemap_add (line_table, LC_LEAVE, false, NULL, 0);
 
   /* Verify that we can recover the location info.  */
-  assert_loceq ("foo.c", 1, 1, loc_a);
-  assert_loceq ("foo.c", 1, 23, loc_b);
-  assert_loceq ("foo.c", 2, 1, loc_c);
-  assert_loceq ("foo.c", 2, 17, loc_d);
-  assert_loceq ("foo.c", 3, 700, loc_e);
-  assert_loceq ("foo.c", 4, 100, loc_back_to_short);
+  ASSERT_LOCEQ ("foo.c", 1, 1, loc_a);
+  ASSERT_LOCEQ ("foo.c", 1, 23, loc_b);
+  ASSERT_LOCEQ ("foo.c", 2, 1, loc_c);
+  ASSERT_LOCEQ ("foo.c", 2, 17, loc_d);
+  ASSERT_LOCEQ ("foo.c", 3, 700, loc_e);
+  ASSERT_LOCEQ ("foo.c", 4, 100, loc_back_to_short);
 
   /* In the very wide line, the initial location should be fully tracked.  */
-  assert_loceq ("foo.c", 5, 2000, loc_start_of_very_long_line);
+  ASSERT_LOCEQ ("foo.c", 5, 2000, loc_start_of_very_long_line);
   /* ...but once we exceed LINE_MAP_MAX_COLUMN_NUMBER column-tracking should
      be disabled.  */
-  assert_loceq ("foo.c", 5, 0, loc_too_wide);
-  assert_loceq ("foo.c", 5, 0, loc_too_wide_2);
+  ASSERT_LOCEQ ("foo.c", 5, 0, loc_too_wide);
+  ASSERT_LOCEQ ("foo.c", 5, 0, loc_too_wide_2);
   /*...and column-tracking should be re-enabled for subsequent lines.  */
-  assert_loceq ("foo.c", 6, 10, loc_sane_again);
+  ASSERT_LOCEQ ("foo.c", 6, 10, loc_sane_again);
 
-  assert_loceq ("bar.c", 1, 150, loc_f);
+  ASSERT_LOCEQ ("bar.c", 1, 150, loc_f);
 
   ASSERT_FALSE (is_location_from_builtin_token (loc_a));
   ASSERT_TRUE (pure_location_p (line_table, loc_a));
@@ -1773,7 +1776,7 @@ test_unknown_location ()
 static void
 test_builtins ()
 {
-  assert_loceq (_("<built-in>"), 0, 0, BUILTINS_LOCATION);
+  ASSERT_LOCEQ (_("<built-in>"), 0, 0, BUILTINS_LOCATION);
   ASSERT_PRED1 (is_location_from_builtin_token, BUILTINS_LOCATION);
 }
 
diff --git a/gcc/run-analyzers.c b/gcc/run-analyzers.c
new file mode 100644
index 0000000..0e8924a
--- /dev/null
+++ b/gcc/run-analyzers.c
@@ -0,0 +1,252 @@
+/* Running 3rd-party code analysis tools.
+   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 "options.h"
+#include "diagnostic.h"
+#include "selftest.h" // FIXME
+#include "firehose.h"
+#include "json.h"
+
+/* FIXME.  */
+
+static void
+print_any_trace (const firehose::issue *issue)
+{
+  if (!issue->m_trace)
+    return;
+  if (0)
+    inform (UNKNOWN_LOCATION, "got trace");
+  int i;
+  firehose::state *s;
+  int num_states = issue->m_trace->m_states.length ();
+  FOR_EACH_VEC_ELT (issue->m_trace->m_states, i, s)
+    {
+      if (s->m_notes)
+	inform (s->m_location, "state %i of %i: %s", i + 1, num_states,
+		s->m_notes);
+      else
+	inform (s->m_location, "state %i of %i", i + 1, num_states);
+    }
+}
+
+bool
+diagnostic_at_rich_loc_va (rich_location *richloc,
+			   diagnostic_info *diagnostic,
+			   const char *gmsgid,
+			   va_list *ap)
+{
+  gcc_assert (richloc);
+  gcc_assert (diagnostic);
+  gcc_assert (gmsgid);
+  gcc_assert (ap);
+
+  diagnostic_t kind = diagnostic->kind;
+#if 0
+  if (kind == DK_PERMERROR)
+    {
+      diagnostic_set_info (diagnostic, gmsgid, ap, richloc,
+			   permissive_error_kind (global_dc));
+      diagnostic.option_index = permissive_error_option (global_dc);
+    }
+  else
+#endif
+    {
+      diagnostic_set_info (diagnostic, gmsgid, ap, richloc, kind);
+#if 0
+      if (kind == DK_WARNING || kind == DK_PEDWARN)
+	diagnostic.option_index = opt;
+#endif
+    }
+  return report_diagnostic (diagnostic);
+}
+
+/* FIXME.  */
+
+void
+diagnostic_at (location_t loc, diagnostic_info *diagnostic,
+	       const char *gmsgid, ...)
+{
+  va_list ap;
+  va_start (ap, gmsgid);
+  rich_location richloc (line_table, loc);
+  diagnostic_at_rich_loc_va (&richloc, diagnostic, gmsgid, &ap);
+  va_end (ap);
+}
+
+
+/* Given UTF8_BUFFER, a non-NULL UTF-8-encoded buffer of JSON output
+   from a tool, in Firehose JSON format, emit the results through GCC's
+   diagnostic subsystem.  */
+
+static void
+handle_json (const char *utf8_buffer)
+{
+  /* Attempt to parse the buffer as UTF-8-encoded JSON.  */
+  char *err = NULL;
+  json::value *jv = json::parse_utf8_string (utf8_buffer, &err);
+  if (!jv)
+    {
+      gcc_assert (err);
+      error_at (UNKNOWN_LOCATION,
+		"unable to parse tool output as UTF-8 JSON: %s", err);
+      free (err);
+      return;
+    }
+
+  gcc_assert (err == NULL);
+
+  if (0)
+    {
+      jv->dump(stderr);
+      fprintf (stderr, "\n");
+    }
+
+  /* Attempt to parse the JSON values into Firehose objects.  */
+  firehose::analysis analysis;
+  analysis.from_json (jv, err);
+  if (err)
+    {
+      error_at (UNKNOWN_LOCATION, "error parsing JSON output: %qs", err);
+      free (err);
+      delete jv;
+      return;
+    }
+
+  gcc_assert (err == NULL);
+
+  /* Emit the results as GCC diagnostics.  */
+  int i;
+  firehose::result *result;
+  FOR_EACH_VEC_ELT (analysis.m_results, i, result)
+    {
+      switch (result->get_kind ())
+	{
+	case firehose::result::FIREHOSE_ISSUE:
+	  {
+	    firehose::issue *issue
+	      = static_cast <firehose::issue *> (result);
+	    // FIXME: generate "In function", perhaps based on blt_node ?
+	    // FIXME:  ^^^ better to get it from the JSON
+	    diagnostic_info diagnostic;
+	    diagnostic.kind = DK_ERROR;
+	    diagnostic.external_tool = analysis.m_metadata.m_generator.m_name;
+	    diagnostic.external_test_id = issue->m_testid;
+	    diagnostic_at (issue->m_location, &diagnostic, "%s",
+			   issue->m_message);
+	    print_any_trace (issue);
+	    // FIXME: identify metadata (which analyzer?)
+	  }
+	  break;
+
+	  // FIXME
+	default:
+	  gcc_unreachable ();
+	}
+    }
+
+  delete jv;
+}
+
+/* FIXME: taken from jit-playback.c.  */
+
+/* A subclass of auto_vec <char *> that frees all of its elements on
+   deletion.  */
+
+class auto_argvec : public auto_vec <char *>
+{
+ public:
+  ~auto_argvec ();
+};
+
+/* auto_argvec's dtor, freeing all contained strings, automatically
+   chaining up to ~auto_vec <char *>, which frees the internal buffer.  */
+
+auto_argvec::~auto_argvec ()
+{
+  int i;
+  char *str;
+  FOR_EACH_VEC_ELT (*this, i, str)
+    free (str);
+}
+
+/* FIXME.  */
+
+static void
+run_one_analyzer (const char *wrapper_path)
+{
+  auto_argvec argvec;
+#define ADD_ARG(arg) argvec.safe_push (xstrdup (arg))
+
+  ADD_ARG (wrapper_path);
+  ADD_ARG ("-B."); // FIXME
+  ADD_ARG ("-c");
+  ADD_ARG (main_input_filename);
+
+  /* pex argv arrays are NULL-terminated.  */
+  argvec.safe_push (NULL);
+
+  const char *errmsg;
+  int exit_status = 0;
+  int err = 0;
+  /* FIXME: avoid using known filename; read direct from stdout.  */
+  errmsg = pex_one (PEX_SEARCH | PEX_LAST, /* int flags, */
+		    wrapper_path,
+		    const_cast <char *const *> (argvec.address ()),
+		    "FIXME:ctxt_progname", /* const char *pname */
+		    "temp.json", /* const char *outname */
+		    NULL, /* const char *errname */
+		    &exit_status, /* int *status */
+		    &err); /* int *err*/
+  if (errmsg)
+    {
+      error_at (UNKNOWN_LOCATION, "error invoking tool: %s", errmsg);
+      return;
+    }
+  if (exit_status || err)
+    {
+      error_at (UNKNOWN_LOCATION,
+		"error invoking tool: exit_status: %i err: %i",
+		exit_status, err);
+      return;
+    }
+
+  // FIXME: use something other than a selftest for this!
+  char *utf8_buffer = selftest::read_file (SELFTEST_LOCATION,
+				      "temp.json"); // FIXME: filename
+  // FIXME: error-checking
+  handle_json (utf8_buffer);
+  free (utf8_buffer);
+
+  // FIXME: capture clang version in metadata
+}
+
+/* FIXME.  */
+
+void
+run_analyzers (const char *path)
+{
+  if (0)
+    inform (UNKNOWN_LOCATION, "run_analyzers (%qs)", path);
+
+  run_one_analyzer ("../../src/fakegcc_clang_analyzer.py");
+  run_one_analyzer ("../../src/fakegcc_cppcheck.py");
+}
diff --git a/gcc/selftest-input.h b/gcc/selftest-input.h
new file mode 100644
index 0000000..d56af36
--- /dev/null
+++ b/gcc/selftest-input.h
@@ -0,0 +1,54 @@
+/* Support for selftests of location handling.
+   Copyright (C) 2016-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_SELFTEST_INPUT_H
+#define GCC_SELFTEST_INPUT_H
+
+/* The selftest code should entirely disappear in a production
+   configuration, hence we guard all of it with #if CHECKING_P.  */
+
+#if CHECKING_P
+
+namespace selftest {
+
+/* input.c.  */
+
+/* Verify the result of LOCATION_FILE/LOCATION_LINE/LOCATION_COLUMN
+   on LOC.  Use REPORT_LOC as the effective location when reporting
+   any issues.  */
+
+extern void assert_loceq (const location &report_loc,
+			  const char *exp_filename, int exp_linenum,
+			  int exp_colnum, location_t loc);
+
+/* Evaluate EXP_FILENAME, EXP_LINENUM, EXP_COLNUM, and LOC.
+   Verify the result of LOCATION_FILE/LOCATION_LINE/LOCATION_COLUMN
+   on LOC.  */
+
+#define ASSERT_LOCEQ(EXP_FILENAME, EXP_LINENUM, EXP_COLNUM, LOC)	\
+  SELFTEST_BEGIN_STMT							\
+    ::selftest::assert_loceq (SELFTEST_LOCATION, (EXP_FILENAME),	\
+			      (EXP_LINENUM), (EXP_COLNUM), (LOC));	\
+  SELFTEST_END_STMT
+
+} /* end of namespace selftest.  */
+
+#endif /* #if CHECKING_P */
+
+#endif /* GCC_SELFTEST_INPUT_H */
diff --git a/gcc/selftest-run-tests.c b/gcc/selftest-run-tests.c
index b8fe4a1..4f72e6c 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_c_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 6b03e39..698d361 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_c_tests ();
 extern void function_tests_c_tests ();
 extern void gimple_c_tests ();
 extern void ggc_tests_c_tests ();
diff --git a/gcc/toplev.c b/gcc/toplev.c
index 54a4f05..1962859 100644
--- a/gcc/toplev.c
+++ b/gcc/toplev.c
@@ -2070,6 +2070,8 @@ toplev::run_self_tests ()
 #endif /* #if CHECKING_P */
 }
 
+extern void run_analyzers (const char *path);
+
 /* Entry point of cc1, cc1plus, jc1, f771, etc.
    Exit code is FATAL_EXIT_CODE if can't open files or if there were
    any errors, or SUCCESS_EXIT_CODE if compilation succeeded.
@@ -2154,6 +2156,10 @@ toplev::main (int argc, char **argv)
       pp_flush (&pp);
     }
 
+  /* FIXME.  */
+  if (flag_run_analyzers)
+    run_analyzers (flag_run_analyzers);
+
   diagnostic_finish (global_dc);
 
   finalize_plugins ();
diff --git a/invoke-cppcheck.py b/invoke-cppcheck.py
new file mode 100644
index 0000000..f647a12
--- /dev/null
+++ b/invoke-cppcheck.py
@@ -0,0 +1,133 @@
+import sys
+
+# http://pypi.python.org/pypi/subprocess32
+# so that we can use timeouts
+from subprocess32 import Popen, PIPE, STDOUT, TimeoutExpired
+
+from firehose.model import Analysis, Generator, Metadata, Failure, \
+    Location, File, Message, Issue, Trace
+
+class Tool:
+    def __init__(self, name, ctxt):
+        self.name = name
+        self.timeout = 60
+        self.ctxt = ctxt
+
+    def log(self, msg):
+        self.ctxt.log(msg)
+
+    def invoke(self, gccinv, sourcefile):
+        """
+        Run the tool, with a timeout, returning an Analysis instance.
+        May well raise an exception if something major went wrong.
+        """
+        raise NotImplementedError
+
+    def handle_output(self, result):
+        """
+        Given a SubprocessResult, return an Analysis instance.
+        """
+        raise NotImplementedError
+
+    def _make_failed_analysis(self, sourcefile, t, msgtext, failureid):
+        """
+        Something went wrong; build a failure report.
+        """
+        generator = Generator(name=self.name,
+                              version=None)
+        if t:
+            stats = make_stats(t)
+        else:
+            stats = None
+
+        metadata = Metadata(generator=generator,
+                            sut=None,
+                            file_ = make_file(sourcefile),
+                            stats=stats)
+        file_ = File(givenpath=sourcefile,
+                     abspath=None,
+                     hash_=None)
+        location = Location(file=file_,
+                            function=None,
+                            point=None,
+                            range_=None)
+        message = Message(msgtext)
+        results = [Failure(failureid=failureid,
+                           location=location,
+                           message=message,
+                           customfields=None)]
+        analysis = Analysis(metadata, results)
+        return analysis
+
+class InvokeCppcheck(Tool):
+    """
+    Tool subclass that invokes "cppcheck"
+    """
+    def __init__(self, ctxt):
+        Tool.__init__(self, 'cppcheck', ctxt)
+
+    def invoke(self, gccinv, sourcefile):
+        args = ['cppcheck',
+                '--xml', '--xml-version=2',
+                sourcefile]
+        return self._run_subprocess(sourcefile, args)
+
+    def handle_output(self, result):
+        from firehose.parsers.cppcheck import parse_file
+
+        if result.returncode:
+            analysis = self._make_failed_analysis(result.sourcefile, result.timer,
+                                                  msgtext='Bad exit code running %s' % self.name,
+                                                  failureid='bad-exit-code')
+            self.set_custom_fields(result, analysis)
+            return analysis
+
+        # (there doesn't seem to be a way to have cppcheck directly
+        # save its XML output to a given location)
+
+        with tempfile.NamedTemporaryFile() as outfile:
+            outfile.write(result.err)
+            outfile.flush()
+
+            with open(outfile.name) as infile:
+                # Parse stderr into firehose XML format and save:
+                analysis = parse_file(infile,
+                                      file_=make_file(result.sourcefile),
+                                      stats=make_stats(result.timer))
+                self.set_custom_fields(result, analysis)
+                return analysis
+
+    def set_custom_fields(self, result, analysis):
+        analysis.set_custom_field('cppcheck-invocation',
+                                  ' '.join(result.argv))
+        result.set_custom_fields(analysis)
+
+def main(argv):
+    capture_exceptions = False
+
+    ctxt = None # FIXME
+    tool = InvokeCppcheck(ctxt)
+    try:
+        analysis = self.invoke_tool(side_effect,
+                                    gccinv,
+                                    sourcefile)
+    except Exception, exc:
+        # Capture the exception as a Failure instance.
+        # Alternatively when debugging such failures, it can
+        # be easier to re-raise the exception:
+        if not capture_exceptions:
+            raise
+            analysis = tool._make_failed_analysis(
+                sourcefile, None,
+                msgtext=('Exception running %s: %s'
+                         % (tool.name, exc)),
+                failureid='exception')
+            tb_str = traceback.format_exc()
+            analysis.set_custom_field('traceback', tb_str)
+    if sourcefile:
+        analysis.metadata.file_.givenpath = sourcefile
+        analysis.metadata.file_.abspath = os.path.join(os.getcwd(),
+                                                       sourcefile)
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv))
diff --git a/test-clang-analyzer.json b/test-clang-analyzer.json
new file mode 100644
index 0000000..eda9abc
--- /dev/null
+++ b/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/test-cppcheck.json b/test-cppcheck.json
new file mode 100644
index 0000000..c9651ee
--- /dev/null
+++ b/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/test-sources/conditional-leak.c b/test-sources/conditional-leak.c
new file mode 100644
index 0000000..2ab46f5
--- /dev/null
+++ b/test-sources/conditional-leak.c
@@ -0,0 +1,17 @@
+#include <stdlib.h>
+
+void test ()
+{
+  void *ptr_1;
+  void *ptr_2;
+
+  ptr_1 = malloc (64);
+  if (!ptr_1)
+    return;
+  ptr_2 = malloc (64);
+  if (!ptr_2)
+    return;
+
+  free (ptr_2);
+  free (ptr_1);
+}
diff --git a/test-sources/cpychecker-demo.c b/test-sources/cpychecker-demo.c
new file mode 100644
index 0000000..b379729
--- /dev/null
+++ b/test-sources/cpychecker-demo.c
@@ -0,0 +1,110 @@
+/*
+   Copyright 2011 David Malcolm <dmalcolm@redhat.com>
+   Copyright 2011 Red Hat, Inc.
+
+   This 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 of the License, or
+   (at your option) any later version.
+
+   This program 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 this program.  If not, see
+   <http://www.gnu.org/licenses/>.
+*/
+
+/* Examples of mistakes made using the Python API */
+#include <Python.h>
+
+extern uint16_t htons(uint16_t hostshort);
+
+PyObject *
+socket_htons(PyObject *self, PyObject *args)
+{
+    unsigned long x1, x2;
+
+    if (!PyArg_ParseTuple(args, "i:htons", &x1)) {
+        return NULL;
+    }
+    x2 = (int)htons((short)x1);
+    return PyInt_FromLong(x2);
+}
+
+PyObject *
+not_enough_varargs(PyObject *self, PyObject *args)
+{
+   if (!PyArg_ParseTuple(args, "i")) {
+       return NULL;
+   }
+   Py_RETURN_NONE;
+}
+
+PyObject *
+too_many_varargs(PyObject *self, PyObject *args)
+{
+    int i, j;
+    if (!PyArg_ParseTuple(args, "i", &i, &j)) {
+	 return NULL;
+    }
+    Py_RETURN_NONE;
+}
+
+PyObject *
+kwargs_example(PyObject *self, PyObject *args, PyObject *kwargs)
+{
+    double x, y;
+    char *keywords[] = {"x", "y"};
+
+    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "(ff):kwargs_example", keywords, &x, &y)) {
+	 return NULL;
+    }
+    Py_RETURN_NONE;
+}
+
+
+extern int convert_to_ssize(PyObject *, Py_ssize_t *);
+
+PyObject *
+buggy_converter(PyObject *self, PyObject *args)
+{
+    int i;
+
+    if (!PyArg_ParseTuple(args, "O&", convert_to_ssize, &i)) {
+        return NULL;
+    }
+
+    Py_RETURN_NONE;
+}
+
+PyObject *
+make_a_list_of_random_ints_badly(PyObject *self,
+                                 PyObject *args)
+{
+    PyObject *list, *item;
+    long count, i;
+
+    if (!PyArg_ParseTuple(args, "i", &count)) {
+         return NULL;
+    }
+
+    list = PyList_New(0);
+
+    for (i = 0; i < count; i++) {
+        item = PyLong_FromLong(random());
+        PyList_Append(list, item);
+    }
+
+    return list;
+}
+
+/*
+  PEP-7
+Local variables:
+c-basic-offset: 4
+indent-tabs-mode: nil
+End:
+*/
diff --git a/test-sources/divide-by-zero.c b/test-sources/divide-by-zero.c
new file mode 100644
index 0000000..f40692d
--- /dev/null
+++ b/test-sources/divide-by-zero.c
@@ -0,0 +1,4 @@
+int divide_by_zero (int i)
+{
+  return i / 0;
+}
diff --git a/test-sources/harmless.c b/test-sources/harmless.c
new file mode 100644
index 0000000..c29f0cc
--- /dev/null
+++ b/test-sources/harmless.c
@@ -0,0 +1,9 @@
+#include <stdio.h>
+
+int main (int argc, const char **argv)
+{
+  int i;
+  for (i = 0; i < argc; i++)
+    printf ("argv[%i]: %s\n", i, argv[i]);
+  return 0;
+}
diff --git a/test-sources/multiple-1.c b/test-sources/multiple-1.c
new file mode 100644
index 0000000..8d26c69
--- /dev/null
+++ b/test-sources/multiple-1.c
@@ -0,0 +1,6 @@
+extern int helper (int argc, const char **argv);
+
+int main (int argc, const char **argv)
+{
+  return helper (argc, argv);
+}
diff --git a/test-sources/multiple-2.c b/test-sources/multiple-2.c
new file mode 100644
index 0000000..a7a20ac
--- /dev/null
+++ b/test-sources/multiple-2.c
@@ -0,0 +1,9 @@
+#include <stdio.h>
+
+int helper (int argc, const char **argv)
+{
+  int i;
+  for (i = 0; i < argc; i++)
+    printf ("argv[%i]: %s\n", i, argv[i]);
+  return 0;
+}
diff --git a/test-sources/out-of-bounds.c b/test-sources/out-of-bounds.c
new file mode 100644
index 0000000..4137389
--- /dev/null
+++ b/test-sources/out-of-bounds.c
@@ -0,0 +1,6 @@
+int out_of_bounds (void)
+{
+  int arr[10];
+
+  return arr[15];
+}
diff --git a/test-sources/read-through-null.c b/test-sources/read-through-null.c
new file mode 100644
index 0000000..2f0450c
--- /dev/null
+++ b/test-sources/read-through-null.c
@@ -0,0 +1,4 @@
+int read_through_null (void)
+{
+  return *(int *)0;
+}
-- 
1.8.5.3

