r282317 - [analyzer][scan-build-py] subprocess output handling reviewed in clang module

Laszlo Nagy via cfe-commits cfe-commits at lists.llvm.org
Fri Sep 23 17:21:00 PDT 2016


Author: rizsotto
Date: Fri Sep 23 19:20:59 2016
New Revision: 282317

URL: http://llvm.org/viewvc/llvm-project?rev=282317&view=rev
Log:
[analyzer][scan-build-py] subprocess output handling reviewed in clang module

Modified:
    cfe/trunk/tools/scan-build-py/libscanbuild/analyze.py
    cfe/trunk/tools/scan-build-py/libscanbuild/clang.py
    cfe/trunk/tools/scan-build-py/tests/unit/test_clang.py

Modified: cfe/trunk/tools/scan-build-py/libscanbuild/analyze.py
URL: http://llvm.org/viewvc/llvm-project/cfe/trunk/tools/scan-build-py/libscanbuild/analyze.py?rev=282317&r1=282316&r2=282317&view=diff
==============================================================================
--- cfe/trunk/tools/scan-build-py/libscanbuild/analyze.py (original)
+++ cfe/trunk/tools/scan-build-py/libscanbuild/analyze.py Fri Sep 23 19:20:59 2016
@@ -269,6 +269,9 @@ def validate(parser, args, from_build_co
     """ Validation done by the parser itself, but semantic check still
     needs to be done. This method is doing that. """
 
+    # Make plugins always a list. (It might be None when not specified.)
+    args.plugins = args.plugins if args.plugins else []
+
     if args.help_checkers_verbose:
         print_checkers(get_checkers(args.clang, args.plugins))
         parser.exit()

Modified: cfe/trunk/tools/scan-build-py/libscanbuild/clang.py
URL: http://llvm.org/viewvc/llvm-project/cfe/trunk/tools/scan-build-py/libscanbuild/clang.py?rev=282317&r1=282316&r2=282317&view=diff
==============================================================================
--- cfe/trunk/tools/scan-build-py/libscanbuild/clang.py (original)
+++ cfe/trunk/tools/scan-build-py/libscanbuild/clang.py Fri Sep 23 19:20:59 2016
@@ -15,142 +15,143 @@ from libscanbuild.shell import decode
 
 __all__ = ['get_version', 'get_arguments', 'get_checkers']
 
+# regex for activated checker
+ACTIVE_CHECKER_PATTERN = re.compile(r'^-analyzer-checker=(.*)$')
 
-def get_version(cmd):
-    """ Returns the compiler version as string. """
 
-    lines = subprocess.check_output([cmd, '-v'], stderr=subprocess.STDOUT)
-    return lines.decode('ascii').splitlines()[0]
+def get_version(clang):
+    """ Returns the compiler version as string.
+
+    :param clang:   the compiler we are using
+    :return:        the version string printed to stderr """
+
+    output = subprocess.check_output([clang, '-v'], stderr=subprocess.STDOUT)
+    return output.decode('utf-8').splitlines()[0]
 
 
 def get_arguments(command, cwd):
     """ Capture Clang invocation.
 
-    This method returns the front-end invocation that would be executed as
-    a result of the given driver invocation. """
-
-    def lastline(stream):
-        last = None
-        for line in stream:
-            last = line
-        if last is None:
-            raise Exception("output not found")
-        return last
+    :param command: the compilation command
+    :param cwd:     the current working directory
+    :return:        the detailed front-end invocation command """
 
     cmd = command[:]
     cmd.insert(1, '-###')
     logging.debug('exec command in %s: %s', cwd, ' '.join(cmd))
-    child = subprocess.Popen(cmd,
-                             cwd=cwd,
-                             universal_newlines=True,
-                             stdout=subprocess.PIPE,
-                             stderr=subprocess.STDOUT)
-    line = lastline(child.stdout)
-    child.stdout.close()
-    child.wait()
-    if child.returncode == 0:
-        if re.search(r'clang(.*): error:', line):
-            raise Exception(line)
-        return decode(line)
-    else:
-        raise Exception(line)
+
+    output = subprocess.check_output(cmd, cwd=cwd, stderr=subprocess.STDOUT)
+    # The relevant information is in the last line of the output.
+    # Don't check if finding last line fails, would throw exception anyway.
+    last_line = output.decode('utf-8').splitlines()[-1]
+    if re.search(r'clang(.*): error:', last_line):
+        raise Exception(last_line)
+    return decode(last_line)
 
 
 def get_active_checkers(clang, plugins):
-    """ To get the default plugins we execute Clang to print how this
-    compilation would be called.
+    """ Get the active checker list.
 
-    For input file we specify stdin and pass only language information. """
+    :param clang:   the compiler we are using
+    :param plugins: list of plugins which was requested by the user
+    :return:        list of checker names which are active
+
+    To get the default checkers we execute Clang to print how this
+    compilation would be called. And take out the enabled checker from the
+    arguments. For input file we specify stdin and pass only language
+    information. """
 
-    def checkers(language):
+    def get_active_checkers_for(language):
         """ Returns a list of active checkers for the given language. """
 
-        load = [elem
-                for plugin in plugins
-                for elem in ['-Xclang', '-load', '-Xclang', plugin]]
-        cmd = [clang, '--analyze'] + load + ['-x', language, '-']
-        pattern = re.compile(r'^-analyzer-checker=(.*)$')
-        return [pattern.match(arg).group(1)
-                for arg in get_arguments(cmd, '.') if pattern.match(arg)]
+        load_args = [arg
+                     for plugin in plugins
+                     for arg in ['-Xclang', '-load', '-Xclang', plugin]]
+        cmd = [clang, '--analyze'] + load_args + ['-x', language, '-']
+        return [ACTIVE_CHECKER_PATTERN.match(arg).group(1)
+                for arg in get_arguments(cmd, '.')
+                if ACTIVE_CHECKER_PATTERN.match(arg)]
 
     result = set()
     for language in ['c', 'c++', 'objective-c', 'objective-c++']:
-        result.update(checkers(language))
-    return result
-
-
-def get_checkers(clang, plugins):
-    """ Get all the available checkers from default and from the plugins.
-
-    clang -- the compiler we are using
-    plugins -- list of plugins which was requested by the user
+        result.update(get_active_checkers_for(language))
+    return frozenset(result)
 
-    This method returns a dictionary of all available checkers and status.
 
-    {<plugin name>: (<plugin description>, <is active by default>)} """
+def is_active(checkers):
+    """ Returns a method, which classifies the checker active or not,
+    based on the received checker name list. """
+
+    def predicate(checker):
+        """ Returns True if the given checker is active. """
+
+        return any(pattern.match(checker) for pattern in predicate.patterns)
+
+    predicate.patterns = [re.compile(r'^' + a + r'(\.|$)') for a in checkers]
+    return predicate
+
+
+def parse_checkers(stream):
+    """ Parse clang -analyzer-checker-help output.
+
+    Below the line 'CHECKERS:' are there the name description pairs.
+    Many of them are in one line, but some long named checker has the
+    name and the description in separate lines.
+
+    The checker name is always prefixed with two space character. The
+    name contains no whitespaces. Then followed by newline (if it's
+    too long) or other space characters comes the description of the
+    checker. The description ends with a newline character.
+
+    :param stream:  list of lines to parse
+    :return:        generator of tuples
+
+    (<checker name>, <checker description>) """
+
+    lines = iter(stream)
+    # find checkers header
+    for line in lines:
+        if re.match(r'^CHECKERS:', line):
+            break
+    # find entries
+    state = None
+    for line in lines:
+        if state and not re.match(r'^\s\s\S', line):
+            yield (state, line.strip())
+            state = None
+        elif re.match(r'^\s\s\S+$', line.rstrip()):
+            state = line.strip()
+        else:
+            pattern = re.compile(r'^\s\s(?P<key>\S*)\s*(?P<value>.*)')
+            match = pattern.match(line.rstrip())
+            if match:
+                current = match.groupdict()
+                yield (current['key'], current['value'])
 
-    plugins = plugins if plugins else []
 
-    def parse_checkers(stream):
-        """ Parse clang -analyzer-checker-help output.
-
-        Below the line 'CHECKERS:' are there the name description pairs.
-        Many of them are in one line, but some long named plugins has the
-        name and the description in separate lines.
-
-        The plugin name is always prefixed with two space character. The
-        name contains no whitespaces. Then followed by newline (if it's
-        too long) or other space characters comes the description of the
-        plugin. The description ends with a newline character. """
-
-        # find checkers header
-        for line in stream:
-            if re.match(r'^CHECKERS:', line):
-                break
-        # find entries
-        state = None
-        for line in stream:
-            if state and not re.match(r'^\s\s\S', line):
-                yield (state, line.strip())
-                state = None
-            elif re.match(r'^\s\s\S+$', line.rstrip()):
-                state = line.strip()
-            else:
-                pattern = re.compile(r'^\s\s(?P<key>\S*)\s*(?P<value>.*)')
-                match = pattern.match(line.rstrip())
-                if match:
-                    current = match.groupdict()
-                    yield (current['key'], current['value'])
-
-    def is_active(actives, entry):
-        """ Returns true if plugin name is matching the active plugin names.
-
-        actives -- set of active plugin names (or prefixes).
-        entry -- the current plugin name to judge.
-
-        The active plugin names are specific plugin names or prefix of some
-        names. One example for prefix, when it say 'unix' and it shall match
-        on 'unix.API', 'unix.Malloc' and 'unix.MallocSizeof'. """
+def get_checkers(clang, plugins):
+    """ Get all the available checkers from default and from the plugins.
 
-        return any(re.match(r'^' + a + r'(\.|$)', entry) for a in actives)
+    :param clang:   the compiler we are using
+    :param plugins: list of plugins which was requested by the user
+    :return:        a dictionary of all available checkers and its status
 
-    actives = get_active_checkers(clang, plugins)
+    {<checker name>: (<checker description>, <is active by default>)} """
 
     load = [elem for plugin in plugins for elem in ['-load', plugin]]
     cmd = [clang, '-cc1'] + load + ['-analyzer-checker-help']
 
     logging.debug('exec command: %s', ' '.join(cmd))
-    child = subprocess.Popen(cmd,
-                             universal_newlines=True,
-                             stdout=subprocess.PIPE,
-                             stderr=subprocess.STDOUT)
+    output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
+    lines = output.decode('utf-8').splitlines()
+
+    is_active_checker = is_active(get_active_checkers(clang, plugins))
+
     checkers = {
-        k: (v, is_active(actives, k))
-        for k, v in parse_checkers(child.stdout)
+        name: (description, is_active_checker(name))
+        for name, description in parse_checkers(lines)
     }
-    child.stdout.close()
-    child.wait()
-    if child.returncode == 0 and len(checkers):
-        return checkers
-    else:
+    if not checkers:
         raise Exception('Could not query Clang for available checkers.')
+
+    return checkers

Modified: cfe/trunk/tools/scan-build-py/tests/unit/test_clang.py
URL: http://llvm.org/viewvc/llvm-project/cfe/trunk/tools/scan-build-py/tests/unit/test_clang.py?rev=282317&r1=282316&r2=282317&view=diff
==============================================================================
--- cfe/trunk/tools/scan-build-py/tests/unit/test_clang.py (original)
+++ cfe/trunk/tools/scan-build-py/tests/unit/test_clang.py Fri Sep 23 19:20:59 2016
@@ -8,9 +8,19 @@ import libear
 import libscanbuild.clang as sut
 import unittest
 import os.path
+import sys
 
 
-class GetClangArgumentsTest(unittest.TestCase):
+class ClangGetVersion(unittest.TestCase):
+    def test_get_version_is_not_empty(self):
+        self.assertTrue(sut.get_version('clang'))
+
+    def test_get_version_throws(self):
+        with self.assertRaises(OSError):
+            sut.get_version('notexists')
+
+
+class ClangGetArgumentsTest(unittest.TestCase):
     def test_get_clang_arguments(self):
         with libear.TemporaryDirectory() as tmpdir:
             filename = os.path.join(tmpdir, 'test.c')
@@ -25,18 +35,60 @@ class GetClangArgumentsTest(unittest.Tes
             self.assertTrue('var="this is it"' in result)
 
     def test_get_clang_arguments_fails(self):
-        self.assertRaises(
-            Exception, sut.get_arguments,
-            ['clang', '-###', '-fsyntax-only', '-x', 'c', 'notexist.c'], '.')
+        with self.assertRaises(Exception):
+            sut.get_arguments(['clang', '-x', 'c', 'notexist.c'], '.')
+
+    def test_get_clang_arguments_fails_badly(self):
+        with self.assertRaises(OSError):
+            sut.get_arguments(['notexist'], '.')
 
 
-class GetCheckersTest(unittest.TestCase):
+class ClangGetCheckersTest(unittest.TestCase):
     def test_get_checkers(self):
         # this test is only to see is not crashing
         result = sut.get_checkers('clang', [])
         self.assertTrue(len(result))
+        # do check result types
+        string_type = unicode if sys.version_info < (3,) else str
+        for key, value in result.items():
+            self.assertEqual(string_type, type(key))
+            self.assertEqual(string_type, type(value[0]))
+            self.assertEqual(bool, type(value[1]))
 
     def test_get_active_checkers(self):
         # this test is only to see is not crashing
         result = sut.get_active_checkers('clang', [])
         self.assertTrue(len(result))
+        # do check result types
+        for value in result:
+            self.assertEqual(str, type(value))
+
+    def test_is_active(self):
+        test = sut.is_active(['a', 'b.b', 'c.c.c'])
+
+        self.assertTrue(test('a'))
+        self.assertTrue(test('a.b'))
+        self.assertTrue(test('b.b'))
+        self.assertTrue(test('b.b.c'))
+        self.assertTrue(test('c.c.c.p'))
+
+        self.assertFalse(test('ab'))
+        self.assertFalse(test('ba'))
+        self.assertFalse(test('bb'))
+        self.assertFalse(test('c.c'))
+        self.assertFalse(test('b'))
+        self.assertFalse(test('d'))
+
+    def test_parse_checkers(self):
+        lines = [
+            'OVERVIEW: Clang Static Analyzer Checkers List',
+            '',
+            'CHECKERS:',
+            '  checker.one       Checker One description',
+            '  checker.two',
+            '                    Checker Two description']
+        result = dict(sut.parse_checkers(lines))
+        self.assertTrue('checker.one' in result)
+        self.assertEqual('Checker One description', result.get('checker.one'))
+        self.assertTrue('checker.two' in result)
+        self.assertEqual('Checker Two description', result.get('checker.two'))




More information about the cfe-commits mailing list