summaryrefslogtreecommitdiff
path: root/tools/scan-build-py
diff options
context:
space:
mode:
authorDimitry Andric <dim@FreeBSD.org>2017-01-02 19:18:08 +0000
committerDimitry Andric <dim@FreeBSD.org>2017-01-02 19:18:08 +0000
commitbab175ec4b075c8076ba14c762900392533f6ee4 (patch)
tree01f4f29419a2cb10abe13c1e63cd2a66068b0137 /tools/scan-build-py
parent8b7a8012d223fac5d17d16a66bb39168a9a1dfc0 (diff)
Notes
Diffstat (limited to 'tools/scan-build-py')
-rw-r--r--tools/scan-build-py/libscanbuild/analyze.py3
-rw-r--r--tools/scan-build-py/libscanbuild/clang.py191
-rw-r--r--tools/scan-build-py/libscanbuild/report.py4
-rw-r--r--tools/scan-build-py/libscanbuild/runner.py13
-rw-r--r--tools/scan-build-py/tests/unit/test_clang.py62
-rw-r--r--tools/scan-build-py/tests/unit/test_report.py13
-rw-r--r--tools/scan-build-py/tests/unit/test_runner.py14
7 files changed, 173 insertions, 127 deletions
diff --git a/tools/scan-build-py/libscanbuild/analyze.py b/tools/scan-build-py/libscanbuild/analyze.py
index 0ed0aef838737..244c34b75837d 100644
--- a/tools/scan-build-py/libscanbuild/analyze.py
+++ b/tools/scan-build-py/libscanbuild/analyze.py
@@ -269,6 +269,9 @@ def validate(parser, args, from_build_command):
""" 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()
diff --git a/tools/scan-build-py/libscanbuild/clang.py b/tools/scan-build-py/libscanbuild/clang.py
index 0c3454b16a76b..833e77d28bbe1 100644
--- a/tools/scan-build-py/libscanbuild/clang.py
+++ b/tools/scan-build-py/libscanbuild/clang.py
@@ -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
- def checkers(language):
+ 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 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
+ result.update(get_active_checkers_for(language))
+ return frozenset(result)
-def get_checkers(clang, plugins):
- """ Get all the available checkers from default and from the plugins.
+def is_active(checkers):
+ """ Returns a method, which classifies the checker active or not,
+ based on the received checker name list. """
- clang -- the compiler we are using
- plugins -- list of plugins which was requested by the user
+ def predicate(checker):
+ """ Returns True if the given checker is active. """
- This method returns a dictionary of all available checkers and status.
+ return any(pattern.match(checker) for pattern in predicate.patterns)
- {<plugin name>: (<plugin description>, <is active by default>)} """
+ predicate.patterns = [re.compile(r'^' + a + r'(\.|$)') for a in checkers]
+ return predicate
- plugins = plugins if plugins else []
- def parse_checkers(stream):
- """ Parse clang -analyzer-checker-help output.
+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.
+ 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 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. """
+ 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.
- # 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'])
+ :param stream: list of lines to parse
+ :return: generator of tuples
- def is_active(actives, entry):
- """ Returns true if plugin name is matching the active plugin names.
+ (<checker name>, <checker description>) """
- actives -- set of active plugin names (or prefixes).
- entry -- the current plugin name to judge.
+ 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'])
- 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'. """
- return any(re.match(r'^' + a + r'(\.|$)', entry) for a in actives)
+def get_checkers(clang, plugins):
+ """ Get all the available checkers from default and from the plugins.
+
+ :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
diff --git a/tools/scan-build-py/libscanbuild/report.py b/tools/scan-build-py/libscanbuild/report.py
index 5c33319e206df..766ddef719909 100644
--- a/tools/scan-build-py/libscanbuild/report.py
+++ b/tools/scan-build-py/libscanbuild/report.py
@@ -21,6 +21,7 @@ import glob
import json
import logging
import contextlib
+import datetime
from libscanbuild import duplicate_check
from libscanbuild.clang import get_version
@@ -34,7 +35,8 @@ def report_directory(hint, keep):
hint -- could specify the parent directory of the output directory.
keep -- a boolean value to keep or delete the empty report directory. """
- stamp = time.strftime('scan-build-%Y-%m-%d-%H%M%S-', time.localtime())
+ stamp_format = 'scan-build-%Y-%m-%d-%H-%M-%S-%f-'
+ stamp = datetime.datetime.now().strftime(stamp_format)
parentdir = os.path.abspath(hint)
if not os.path.exists(parentdir):
diff --git a/tools/scan-build-py/libscanbuild/runner.py b/tools/scan-build-py/libscanbuild/runner.py
index 628ad90d627a4..72d02c85fed15 100644
--- a/tools/scan-build-py/libscanbuild/runner.py
+++ b/tools/scan-build-py/libscanbuild/runner.py
@@ -205,19 +205,8 @@ def filter_debug_flags(opts, continuation=run_analyzer):
return continuation(opts)
-@require(['file', 'directory'])
-def set_file_path_relative(opts, continuation=filter_debug_flags):
- """ Set source file path to relative to the working directory.
-
- The only purpose of this function is to pass the SATestBuild.py tests. """
-
- opts.update({'file': os.path.relpath(opts['file'], opts['directory'])})
-
- return continuation(opts)
-
-
@require(['language', 'compiler', 'file', 'flags'])
-def language_check(opts, continuation=set_file_path_relative):
+def language_check(opts, continuation=filter_debug_flags):
""" Find out the language from command line parameters or file name
extension. The decision also influenced by the compiler invocation. """
diff --git a/tools/scan-build-py/tests/unit/test_clang.py b/tools/scan-build-py/tests/unit/test_clang.py
index 04414a85b8281..eef8c26bbd19b 100644
--- a/tools/scan-build-py/tests/unit/test_clang.py
+++ b/tools/scan-build-py/tests/unit/test_clang.py
@@ -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.TestCase):
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'))
diff --git a/tools/scan-build-py/tests/unit/test_report.py b/tools/scan-build-py/tests/unit/test_report.py
index 3f249ce2aa0c8..c82b5593e0dc7 100644
--- a/tools/scan-build-py/tests/unit/test_report.py
+++ b/tools/scan-build-py/tests/unit/test_report.py
@@ -146,3 +146,16 @@ class GetPrefixFromCompilationDatabaseTest(unittest.TestCase):
def test_empty(self):
self.assertEqual(
sut.commonprefix([]), '')
+
+class ReportDirectoryTest(unittest.TestCase):
+
+ # Test that successive report directory names ascend in lexicographic
+ # order. This is required so that report directories from two runs of
+ # scan-build can be easily matched up to compare results.
+ def test_directory_name_comparison(self):
+ with libear.TemporaryDirectory() as tmpdir, \
+ sut.report_directory(tmpdir, False) as report_dir1, \
+ sut.report_directory(tmpdir, False) as report_dir2, \
+ sut.report_directory(tmpdir, False) as report_dir3:
+ self.assertLess(report_dir1, report_dir2)
+ self.assertLess(report_dir2, report_dir3)
diff --git a/tools/scan-build-py/tests/unit/test_runner.py b/tools/scan-build-py/tests/unit/test_runner.py
index b4730a1c5191c..2d09062233292 100644
--- a/tools/scan-build-py/tests/unit/test_runner.py
+++ b/tools/scan-build-py/tests/unit/test_runner.py
@@ -219,20 +219,6 @@ class AnalyzerTest(unittest.TestCase):
self.assertEqual(['-DNDEBUG', '-UNDEBUG'], test(['-DNDEBUG']))
self.assertEqual(['-DSomething', '-UNDEBUG'], test(['-DSomething']))
- def test_set_file_relative_path(self):
- def test(expected, input):
- spy = Spy()
- self.assertEqual(spy.success,
- sut.set_file_path_relative(input, spy.call))
- self.assertEqual(expected, spy.arg['file'])
-
- test('source.c',
- {'file': '/home/me/source.c', 'directory': '/home/me'})
- test('me/source.c',
- {'file': '/home/me/source.c', 'directory': '/home'})
- test('../home/me/source.c',
- {'file': '/home/me/source.c', 'directory': '/tmp'})
-
def test_set_language_fall_through(self):
def language(expected, input):
spy = Spy()