blob: c5fe27711c9711b4a7b5f3b86115f3280eec29d4 [file] [log] [blame]
Laszlo Nagybc687582016-01-12 22:38:41 +00001# -*- coding: utf-8 -*-
2# The LLVM Compiler Infrastructure
3#
4# This file is distributed under the University of Illinois Open Source
5# License. See LICENSE.TXT for details.
6""" This module implements the 'scan-build' command API.
7
8To run the static analyzer against a build is done in multiple steps:
9
10 -- Intercept: capture the compilation command during the build,
11 -- Analyze: run the analyzer against the captured commands,
12 -- Report: create a cover report from the analyzer outputs. """
13
Laszlo Nagybc687582016-01-12 22:38:41 +000014import re
15import os
16import os.path
17import json
Laszlo Nagybc687582016-01-12 22:38:41 +000018import logging
Laszlo Nagy258ff252017-02-14 10:43:38 +000019import tempfile
Laszlo Nagybc687582016-01-12 22:38:41 +000020import multiprocessing
Laszlo Nagy258ff252017-02-14 10:43:38 +000021import contextlib
22import datetime
Laszlo Nagy2e9c9222017-03-04 01:08:05 +000023from libscanbuild import command_entry_point, compiler_wrapper, \
Laszlo Nagy5270bb92017-03-08 21:18:51 +000024 wrapper_environment, run_build
25from libscanbuild.arguments import parse_args_for_scan_build, \
26 parse_args_for_analyze_build
Laszlo Nagybc687582016-01-12 22:38:41 +000027from libscanbuild.runner import run
28from libscanbuild.intercept import capture
Laszlo Nagy258ff252017-02-14 10:43:38 +000029from libscanbuild.report import document
Laszlo Nagy8bd63e52016-04-19 12:03:03 +000030from libscanbuild.compilation import split_command
Laszlo Nagybc687582016-01-12 22:38:41 +000031
Laszlo Nagy5270bb92017-03-08 21:18:51 +000032__all__ = ['scan_build', 'analyze_build', 'analyze_compiler_wrapper']
Laszlo Nagybc687582016-01-12 22:38:41 +000033
34COMPILER_WRAPPER_CC = 'analyze-cc'
35COMPILER_WRAPPER_CXX = 'analyze-c++'
36
37
38@command_entry_point
Laszlo Nagy5270bb92017-03-08 21:18:51 +000039def scan_build():
40 """ Entry point for scan-build command. """
Laszlo Nagybc687582016-01-12 22:38:41 +000041
Laszlo Nagy5270bb92017-03-08 21:18:51 +000042 args = parse_args_for_scan_build()
Laszlo Nagy57db7c62017-03-21 10:15:18 +000043 # will re-assign the report directory as new output
44 with report_directory(args.output, args.keep_empty) as args.output:
Laszlo Nagy5270bb92017-03-08 21:18:51 +000045 # Run against a build command. there are cases, when analyzer run
46 # is not required. But we need to set up everything for the
47 # wrappers, because 'configure' needs to capture the CC/CXX values
48 # for the Makefile.
49 if args.intercept_first:
50 # Run build command with intercept module.
51 exit_code = capture(args)
52 # Run the analyzer against the captured commands.
Laszlo Nagybc687582016-01-12 22:38:41 +000053 if need_analyzer(args.build):
Laszlo Nagy57db7c62017-03-21 10:15:18 +000054 run_analyzer(args)
Laszlo Nagybc687582016-01-12 22:38:41 +000055 else:
Laszlo Nagy5270bb92017-03-08 21:18:51 +000056 # Run build command and analyzer with compiler wrappers.
Laszlo Nagy57db7c62017-03-21 10:15:18 +000057 environment = setup_environment(args)
Laszlo Nagy52c1d7e2017-02-14 10:30:50 +000058 exit_code = run_build(args.build, env=environment)
Laszlo Nagy5270bb92017-03-08 21:18:51 +000059 # Cover report generation and bug counting.
Laszlo Nagy57db7c62017-03-21 10:15:18 +000060 number_of_bugs = document(args)
Laszlo Nagy5270bb92017-03-08 21:18:51 +000061 # Set exit status as it was requested.
62 return number_of_bugs if args.status_bugs else exit_code
63
64
65@command_entry_point
66def analyze_build():
67 """ Entry point for analyze-build command. """
68
69 args = parse_args_for_analyze_build()
Laszlo Nagy57db7c62017-03-21 10:15:18 +000070 # will re-assign the report directory as new output
71 with report_directory(args.output, args.keep_empty) as args.output:
Laszlo Nagy5270bb92017-03-08 21:18:51 +000072 # Run the analyzer against a compilation db.
Laszlo Nagy57db7c62017-03-21 10:15:18 +000073 run_analyzer(args)
Laszlo Nagy5270bb92017-03-08 21:18:51 +000074 # Cover report generation and bug counting.
Laszlo Nagy57db7c62017-03-21 10:15:18 +000075 number_of_bugs = document(args)
Laszlo Nagy5270bb92017-03-08 21:18:51 +000076 # Set exit status as it was requested.
77 return number_of_bugs if args.status_bugs else 0
Laszlo Nagybc687582016-01-12 22:38:41 +000078
79
80def need_analyzer(args):
81 """ Check the intent of the build command.
82
83 When static analyzer run against project configure step, it should be
84 silent and no need to run the analyzer or generate report.
85
86 To run `scan-build` against the configure step might be neccessary,
87 when compiler wrappers are used. That's the moment when build setup
88 check the compiler and capture the location for the build process. """
89
90 return len(args) and not re.search('configure|autogen', args[0])
91
92
Laszlo Nagy57db7c62017-03-21 10:15:18 +000093def run_analyzer(args):
Laszlo Nagybc687582016-01-12 22:38:41 +000094 """ Runs the analyzer against the given compilation database. """
95
96 def exclude(filename):
97 """ Return true when any excluded directory prefix the filename. """
98 return any(re.match(r'^' + directory, filename)
99 for directory in args.excludes)
100
101 consts = {
102 'clang': args.clang,
Laszlo Nagy57db7c62017-03-21 10:15:18 +0000103 'output_dir': args.output,
Laszlo Nagybc687582016-01-12 22:38:41 +0000104 'output_format': args.output_format,
105 'output_failures': args.output_failures,
Yury Gribova6560eb2016-02-18 11:08:46 +0000106 'direct_args': analyzer_params(args),
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000107 'force_debug': args.force_debug
Laszlo Nagybc687582016-01-12 22:38:41 +0000108 }
109
110 logging.debug('run analyzer against compilation database')
111 with open(args.cdb, 'r') as handle:
112 generator = (dict(cmd, **consts)
113 for cmd in json.load(handle) if not exclude(cmd['file']))
114 # when verbose output requested execute sequentially
115 pool = multiprocessing.Pool(1 if args.verbose > 2 else None)
116 for current in pool.imap_unordered(run, generator):
117 if current is not None:
118 # display error message from the static analyzer
119 for line in current['error_output']:
120 logging.info(line.rstrip())
121 pool.close()
122 pool.join()
123
124
Laszlo Nagy57db7c62017-03-21 10:15:18 +0000125def setup_environment(args):
Laszlo Nagybc687582016-01-12 22:38:41 +0000126 """ Set up environment for build command to interpose compiler wrapper. """
127
128 environment = dict(os.environ)
Laszlo Nagy2e9c9222017-03-04 01:08:05 +0000129 environment.update(wrapper_environment(args))
Laszlo Nagybc687582016-01-12 22:38:41 +0000130 environment.update({
Laszlo Nagy5270bb92017-03-08 21:18:51 +0000131 'CC': COMPILER_WRAPPER_CC,
132 'CXX': COMPILER_WRAPPER_CXX,
Laszlo Nagybc687582016-01-12 22:38:41 +0000133 'ANALYZE_BUILD_CLANG': args.clang if need_analyzer(args.build) else '',
Laszlo Nagy57db7c62017-03-21 10:15:18 +0000134 'ANALYZE_BUILD_REPORT_DIR': args.output,
Laszlo Nagybc687582016-01-12 22:38:41 +0000135 'ANALYZE_BUILD_REPORT_FORMAT': args.output_format,
136 'ANALYZE_BUILD_REPORT_FAILURES': 'yes' if args.output_failures else '',
Yury Gribova6560eb2016-02-18 11:08:46 +0000137 'ANALYZE_BUILD_PARAMETERS': ' '.join(analyzer_params(args)),
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000138 'ANALYZE_BUILD_FORCE_DEBUG': 'yes' if args.force_debug else ''
Laszlo Nagybc687582016-01-12 22:38:41 +0000139 })
140 return environment
141
142
Laszlo Nagy2e9c9222017-03-04 01:08:05 +0000143@command_entry_point
144def analyze_compiler_wrapper():
Laszlo Nagybc687582016-01-12 22:38:41 +0000145 """ Entry point for `analyze-cc` and `analyze-c++` compiler wrappers. """
146
Laszlo Nagy2e9c9222017-03-04 01:08:05 +0000147 return compiler_wrapper(analyze_compiler_wrapper_impl)
148
149
150def analyze_compiler_wrapper_impl(result, execution):
151 """ Implements analyzer compiler wrapper functionality. """
152
153 # don't run analyzer when compilation fails. or when it's not requested.
Laszlo Nagybc687582016-01-12 22:38:41 +0000154 if result or not os.getenv('ANALYZE_BUILD_CLANG'):
Laszlo Nagy2e9c9222017-03-04 01:08:05 +0000155 return
156
157 # check is it a compilation?
158 compilation = split_command(execution.cmd)
159 if compilation is None:
160 return
161 # collect the needed parameters from environment, crash when missing
162 parameters = {
163 'clang': os.getenv('ANALYZE_BUILD_CLANG'),
164 'output_dir': os.getenv('ANALYZE_BUILD_REPORT_DIR'),
165 'output_format': os.getenv('ANALYZE_BUILD_REPORT_FORMAT'),
166 'output_failures': os.getenv('ANALYZE_BUILD_REPORT_FAILURES'),
167 'direct_args': os.getenv('ANALYZE_BUILD_PARAMETERS',
168 '').split(' '),
169 'force_debug': os.getenv('ANALYZE_BUILD_FORCE_DEBUG'),
170 'directory': execution.cwd,
171 'command': [execution.cmd[0], '-c'] + compilation.flags
172 }
173 # call static analyzer against the compilation
174 for source in compilation.files:
175 parameters.update({'file': source})
176 logging.debug('analyzer parameters %s', parameters)
177 current = run(parameters)
178 # display error message from the static analyzer
179 if current is not None:
180 for line in current['error_output']:
181 logging.info(line.rstrip())
Laszlo Nagybc687582016-01-12 22:38:41 +0000182
183
Laszlo Nagy258ff252017-02-14 10:43:38 +0000184@contextlib.contextmanager
185def report_directory(hint, keep):
186 """ Responsible for the report directory.
187
188 hint -- could specify the parent directory of the output directory.
189 keep -- a boolean value to keep or delete the empty report directory. """
190
191 stamp_format = 'scan-build-%Y-%m-%d-%H-%M-%S-%f-'
192 stamp = datetime.datetime.now().strftime(stamp_format)
193 parent_dir = os.path.abspath(hint)
194 if not os.path.exists(parent_dir):
195 os.makedirs(parent_dir)
196 name = tempfile.mkdtemp(prefix=stamp, dir=parent_dir)
197
198 logging.info('Report directory created: %s', name)
199
200 try:
201 yield name
202 finally:
203 if os.listdir(name):
204 msg = "Run 'scan-view %s' to examine bug reports."
205 keep = True
206 else:
207 if keep:
208 msg = "Report directory '%s' contains no report, but kept."
209 else:
210 msg = "Removing directory '%s' because it contains no report."
211 logging.warning(msg, name)
212
213 if not keep:
214 os.rmdir(name)
215
216
Laszlo Nagybc687582016-01-12 22:38:41 +0000217def analyzer_params(args):
218 """ A group of command line arguments can mapped to command
219 line arguments of the analyzer. This method generates those. """
220
221 def prefix_with(constant, pieces):
222 """ From a sequence create another sequence where every second element
223 is from the original sequence and the odd elements are the prefix.
224
225 eg.: prefix_with(0, [1,2,3]) creates [0, 1, 0, 2, 0, 3] """
226
227 return [elem for piece in pieces for elem in [constant, piece]]
228
229 result = []
230
231 if args.store_model:
232 result.append('-analyzer-store={0}'.format(args.store_model))
233 if args.constraints_model:
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000234 result.append('-analyzer-constraints={0}'.format(
235 args.constraints_model))
Laszlo Nagybc687582016-01-12 22:38:41 +0000236 if args.internal_stats:
237 result.append('-analyzer-stats')
238 if args.analyze_headers:
239 result.append('-analyzer-opt-analyze-headers')
240 if args.stats:
241 result.append('-analyzer-checker=debug.Stats')
242 if args.maxloop:
243 result.extend(['-analyzer-max-loop', str(args.maxloop)])
244 if args.output_format:
245 result.append('-analyzer-output={0}'.format(args.output_format))
246 if args.analyzer_config:
247 result.append(args.analyzer_config)
248 if args.verbose >= 4:
249 result.append('-analyzer-display-progress')
250 if args.plugins:
251 result.extend(prefix_with('-load', args.plugins))
252 if args.enable_checker:
253 checkers = ','.join(args.enable_checker)
254 result.extend(['-analyzer-checker', checkers])
255 if args.disable_checker:
256 checkers = ','.join(args.disable_checker)
257 result.extend(['-analyzer-disable-checker', checkers])
258 if os.getenv('UBIVIZ'):
259 result.append('-analyzer-viz-egraph-ubigraph')
260
261 return prefix_with('-Xclang', result)