From 4144b77ad4168c600302d2f4072a015baec5214e Mon Sep 17 00:00:00 2001
From: David Malcolm <dmalcolm@redhat.com>
Date: Wed, 2 Aug 2017 15:02:53 -0400
Subject: [PATCH 15/22] Add checkers/clang_analyzer.py

This patch adds a harness for invoking clang's static analyzer:
  https://clang-analyzer.llvm.org/
returning the results in JSON format.

It runs scan-build, then uses firehose.parsers.clanganalyzer.parse_plist
to parse the generated .plist file, turning them into firehose JSON.

checkers/ChangeLog:
	* clang_analyzer.py: New file.
---
 checkers/clang_analyzer.py | 145 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 145 insertions(+)
 create mode 100755 checkers/clang_analyzer.py

diff --git a/checkers/clang_analyzer.py b/checkers/clang_analyzer.py
new file mode 100755
index 0000000..ae41d93
--- /dev/null
+++ b/checkers/clang_analyzer.py
@@ -0,0 +1,145 @@
+#!/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
+
+from gccinvocation import GccInvocation
+from firehose.model import Failure, Issue, Trace
+from firehose.parsers.clanganalyzer import parse_plist
+
+from checker import Checker, CheckerTests, make_file, make_stats, \
+    tool_main
+
+class InvokeClangAnalyzer(Checker):
+    """
+    Checker subclass that invokes the clang analyzer
+    """
+    name = 'clang-analyzer'
+
+    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(CheckerTests):
+    def make_tool(self):
+        return self.make_tool_from_class(InvokeClangAnalyzer)
+
+    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')
+        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, 'Dereference of null pointer')
+        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')
+        self.assertEqual(len(analysis.results), 1)
+
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Issue)
+        self.assertEqual(r0.testid, 'Garbage return value')
+        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))
-- 
1.8.5.3

