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 <dmalc...@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